审阅者: Yunsang Ju (Naver GplaceAI Leader), Min Jean Cho(Intel), Jing Xu(Intel), Mark Saroufim(Meta)
简介
在这里,我们将分享我们如何在不降低性能或质量的情况下,将 AI 工作负载从我们的 GPU 服务器迁移到我们的英特尔 CPU 服务器的经验,并在此过程中每年节省约 34 万美元(参见结论)。
我们旨在通过提供各种 AI 模型来增强在线到离线 (O2O) 体验,从而为消费者提供价值。随着对新模型需求的持续增长以及高成本 GPU 资源的有限性,我们需要将相对轻量级的 AI 模型从 GPU 服务器迁移到英特尔 CPU 服务器,以减少资源消耗。然而,在相同的设置下,CPU 服务器存在 RPS、推理时间等性能降低数十倍的问题。我们应用了各种工程技术并对模型进行了轻量化处理以解决此问题,并成功地以与 GPU 服务器相同或更好的性能迁移到英特尔 CPU 服务器,仅需三倍的扩展。
有关我们团队的更详细介绍,请参阅 NAVER Place AI 开发团队介绍。
我将在中间再次提及,但在整个工作中,我从英特尔和 PyTorch 撰写的 从第一性原理深入理解 PyTorch 英特尔 CPU 性能中获得了大量帮助。
问题定义
1:服务架构

简化服务架构(图片来源:NAVER GplaceAI)
为了便于理解,将简要介绍我们的服务架构。CPU 密集型任务,例如将输入预处理为张量格式(然后转发到模型)以及将推理结果后处理为人类可读的输出(例如自然语言和图像格式),在 App Server(FastAPI)上执行。Model Server(TorchServe)专门处理推理操作。为了服务的稳定运行,需要以足够的吞吐量和低延迟执行以下操作。
具体处理顺序如下
- 客户端通过 Traefik 网关向应用服务器提交请求。
- 应用服务器通过执行调整大小和转换等操作,将输入预处理并将其转换为 Torch 张量,然后向模型服务器发出请求。
- 模型服务器执行推理并将特征返回给应用服务器
- 应用服务器通过后处理将特征转换为人类可理解的格式,并将其返回给客户端
2:吞吐量和延迟测量

图像评分模型比较
在所有其他条件保持不变的情况下,部署在增加三倍的 CPU 服务器 Pod 上,但值得注意的是,RPS(每秒请求数)和响应时间恶化了十倍以上。虽然 CPU 推理性能不如 GPU 并不令人意外,但挑战性的情况显而易见。鉴于在有限资源内保持性能的目标,实现大约 10 到 20 倍的性能提升是必要的,除非进行额外的扩展。
3:吞吐量方面的挑战
Type Name # reqs # fails | Avg Min Max Med | req/s failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
POST /predictions/image-scoring 37 0(0.00%) | 9031 4043 28985 8200 | 1.00 0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
Aggregated 37 0(0.00%) | 9031 4043 28985 8200 | 1.00 0.00
TorchServer 框架用户为提高吞吐量可能采取的第一个步骤之一是增加 TorchServe 中的工作进程数。这种方法在 GPU 服务器上很有效,因为可以并行处理工作负载,而不会随着工作进程的扩展而线性增加内存使用。然而,我们发现在增加工作进程数时性能反而更差。识别 CPU 服务器上性能下降的原因需要进一步调查。
4:延迟方面的挑战
我们主要关注的是延迟。通常,当系统实现忠实于横向扩展原则时,吞吐量改善是可以实现的,除了极少数最坏情况。然而,在图像评分模型示例中,即使执行一次推理也需要超过 1 秒,并且随着请求量的增加,延迟增加到多达 4 秒。这是一种即使进行单次推理也无法满足客户端超时标准的情况。
拟议解决方案
需要从 ML 和工程角度进行改进。根本上减少 CPU 上的推理时间至关重要,并且要识别在应用通常能提高性能的配置时导致性能下降的原因,以找到最佳配置值。为了实现这一目标,与 MLE 专业人员建立了协作,同时执行“在不损害性能的情况下对模型进行轻量化”和“确定实现峰值性能的最佳配置”的任务。使用上述方法,我们能够有效地将工作负载处理过渡到我们的 CPU 服务器。
1:从工程角度解决低 RPS 问题
首先,即使在增加 worker 数量后性能下降的原因是 GEMM 操作中逻辑线程导致的前端瓶颈。通常,当增加 worker 数量时,预期的改进效果是并行性的增加。相反,如果性能下降,则可以推断出相应的权衡效果。

图片来源: Nvidia
众所周知,CPU 上模型推理性能不如 GPU 的原因在于硬件设计的差异,尤其是在多线程能力方面。深入来看,模型推理本质上是 GEMM(通用矩阵乘法)操作的重复,这些 GEMM 操作在 “融合乘加”(FMA)或 “点积”(DP)执行单元中独立执行。如果 GEMM 操作成为 CPU 上的瓶颈,增加并行性实际上可能会导致性能下降。在研究问题时,我们在 PyTorch 文档中找到了相关信息。
当两个逻辑线程同时运行 GEMM 时,它们将共享相同的核心资源,导致前端瓶颈
此信息强调,逻辑线程可能导致 CPU GEMM 操作的瓶颈,这有助于我们直观地理解为什么在增加工作线程数时性能会下降。这是因为 torch 线程的默认值对应于 CPU 的物理核心值。
root@test-pod:/# lscpu
…
Thread(s) per core: 2
Core(s) per socket: 12
…
root@test-pod:/# python
>>> import torch
>>> print(torch.get_num_threads())
24
当 worker_num 增加时,总线程数会以物理核心数 * worker 数的乘积增加。因此,会利用逻辑线程。为了提高性能,每个 worker 的总线程数被调整为与物理核心数对齐。下面可以看到,当 worker_num 增加到 4 并且总线程数与物理核心数对齐时,指标 RPS 增加了大约三倍,达到 6.3(从之前的 2.1)。
Type Name # reqs # fails | Avg Min Max Med | req/s failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
POST /predictions/image-scoring 265 0(0.00%) | 3154 1885 4008 3200 | 6.30 0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
Aggregated 265 0(0.00%) | 3154 1885 4008 3200 | 6.30 0.00
注意事项 1:我们的团队使用 Kubernetes 来维护部署。因此,我们正在根据 Pod 的 CPU 资源限制进行调整,而不是使用 lscpu 命令检查的节点物理核心数。(将每个 worker 的 torch 线程设置为 8/4 = 2 或 24/4 = 6 都会导致性能下降。)
注意事项 2:由于每个 worker 的 torch 线程设置只能配置为整数,因此建议将 CPU 限制设置为可被 worker_num 整除,以便充分利用 CPU 使用率。

例如) 核心数=8,worker_num=3 的情况下:int(8/worker_num) = 2, 2*worker_num/8 = 75%

例如) 核心数=8,worker_num=4 的情况下:int(8/worker_num) = 2, 2*worker_num/8 = 100%
我们还分析了模型容器,以了解为什么在 worker 数量增加四倍的情况下,性能仅提高了三倍。我们监控了各种资源,其中核心利用率被认为是根本原因。

即使将总线程数调整为与 CPU(第二代英特尔® 至强® 银牌 4214)限制(8 核)匹配,也存在从逻辑线程到逻辑核心执行计算的情况。由于存在 24 个物理核心,编号为 25 到 48 的核心被归类为逻辑核心。将线程执行仅限于物理核心似乎有可能进一步提高性能。此解决方案的参考资料可以在 PyTorch-geometric 文章中提到的原始文档中找到,该文章警告了 CPU GEMM 瓶颈。
根据文档中的说明,英特尔提供了 Intel® Extension for PyTorch,我们可以简单地将核心固定到特定插槽。应用方法也非常简单,只需将以下设置添加到 torchserve config.properties 文件中。(使用了 intel_extension_for_pytorch==1.13.0)
ipex_enable=true
CPU_launcher_enable=true

图片来源:PyTorch
除了通过插槽固定消除逻辑线程外,还有一个额外的效果是消除 UPI 缓存命中开销。由于 CPU 包含多个插槽,当在插槽 1 上调度的线程在插槽 2 上重新调度时,在通过 Intel Ultra Path Interconnect (UPI) 访问插槽 1 缓存的情况下会发生缓存命中。此时,UPI 访问本地缓存的速度比本地缓存访问慢两倍以上,导致更多的瓶颈。通过 oneAPI 支持的 Intel® Extension for PyTorch 将线程固定到插槽单元,我们观察到 RPS 处理量比存在瓶颈时增加了多达 四倍。
Type Name # reqs # fails | Avg Min Max Med | req/s failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
POST /predictions/image-scoring 131 0(0.00%) | 3456 1412 6813 3100 | 7.90 0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
Aggregated 131 0(0.00%) | 3456 1412 6813 3100 | 7.90 0.00
注意事项 1:Intel® Extension for PyTorch 专注于神经网络(以下简称“nn”)推理优化,因此 nn 之外的其他技术的性能提升可能很小。事实上,在作为示例突出显示的图像评分系统实例中,推理后应用了 svr(支持向量回归),性能提升仅限于 4 倍。然而,对于纯 nn 推理模型,例如食物识别模型,检测到 7 倍的性能提升(2.5rps -> 17.5rps)。
Type Name # reqs # fails | Avg Min Max Med | req/s failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
POST /predictions/food-classification 446 0(0.00%) | 1113 249 1804 1200 | 17.50 0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
Aggregated 446 0(0.00%) | 1113 249 1804 1200 | 17.50 0.00
注意事项 2:应用 Intel® Extension for PyTorch 需要 torchserve 版本 0.6.1 或更高版本。由于我们团队使用的是 0.6.0 版本,因此出现了插槽固定功能无法正常工作的问题。目前,我们已经对指南文档进行了修改,指定了所需的版本。
在 WorkerLifeCycle.java 中,0.6.0 及以下版本不支持多 worker 固定(ninstance 硬编码为 1)
// 0.6.0 version
public ArrayList<String> launcherArgsToList() {
ArrayList<String> arrlist = new ArrayList<String>();
arrlist.add("-m");
arrlist.add("intel_extension_for_pytorch.cpu.launch");
arrlist.add(" — ninstance");
arrlist.add("1");
if (launcherArgs != null && launcherArgs.length() > 1) {
String[] argarray = launcherArgs.split(" ");
for (int i = 0; i < argarray.length; i++) {
arrlist.add(argarray[i]);
}
}
return arrlist;
}
// master version
if (this.numWorker > 1) {
argl.add(" — ninstances");
argl.add(String.valueOf(this.numWorker));
argl.add(" — instance_idx");
argl.add(String.valueOf(this.currNumRunningWorkers));
}
2:通过模型轻量化解决慢延迟问题
我们还使用 知识蒸馏(通常缩写为 KD)简化了我们的模型,以进一步减少延迟。众所周知,KD 是一种将来自较大网络(教师网络)的知识传递给较小、轻量级网络(学生网络)的技术,后者资源消耗更少,更容易部署。有关更多详细信息,请参阅最初引入此概念的论文,标题为《从神经网络中提取知识》(Distilling the Knowledge in a Neural Network)。

有多种 KD 技术可用,由于我们主要关注 准确度损失最小化,我们采用了 2022 年发表的论文 《从更强的教师那里进行知识蒸馏》 中的方法。这个概念很简单。与仅利用模型属性值的传统蒸馏方法不同,所选方法涉及让学生网络学习教师网络中类别之间的相关性。在实际应用中,我们观察到有效的模型权重减少,同时保持高准确度。以下是我们对几种候选学生模型进行知识蒸馏技术实验的结果,其中选择是基于保持的准确度水平。

对于图像评分系统,我们还采取了额外措施来减小输入大小。考虑到之前使用了基于 CPU 的 ML 技术 SVR(支持向量回归)(两阶段:CNN + SVR),即使将其简化为一阶段模型,在 CPU 推理中也没有观察到显著的速度优势。为了使简化有意义,推理时学生模型的输入大小需要进一步减小。因此,我们进行了实验,将大小从 384*384 减小到 224*224。
进一步简化转换,将两阶段(CNN + SVR)方法统一为一个使用较大 ConvNext 的一阶段模型,然后使用轻量级 EfficientNet 应用 KD 以解决准确性权衡。在实验过程中,我们遇到了一个问题,即将 Img_resize 更改为 224 导致 MAE 从 0.4007 下降到 0.4296。由于输入大小的减小,应用于原始训练图像的各种预处理技术(如仿射变换、随机旋转 90 度、模糊、OneOf [网格畸变、光学畸变、弹性变换]、垂直翻转)产生了反作用。通过采用这些措施,实现了有效的学生训练,MAE 值比之前提高了 25%(从 0.518 到 0.3876)。
验证
1:最终性能测量
以下显示了使用 CPU 服务器对本文中提到的三个模型进行的最终性能改进。
# Food photo classifier (pod 3): 2.5rps -> 84 rps
Type Name # reqs # fails | Avg Min Max Med | req/s failures/s
--------|----------------------------------------------------------------------------|------|------------|-------|------|-------|-------|--------|---------
POST /predictions/food-classification 2341 0(0.00%) | 208 130 508 200 | 84.50 0.00
--------|----------------------------------------------------------------------------|--------|-------------|------|-------|--------|------|--------|----------
Aggregated 2341 0(0.00%) | 208 130 508 200 | 84.50 0.00
# Image scoring (pod 3): 2.1rps -> 62rps
Type Name #reqs #fails | Avg Min Max Median | req/s failures/s
--------|---------------------------------------------------------------------------------|--------|-------------|--------|-------|--------|---------|--------|---------
POST /predictions/image-scoring 1298 0 (0.00%) | 323 99 607 370 | 61.90 0.00
--------|---------------------------------------------------------------------------------|--------|-------------|--------|------|--------|---------|--------|----------
Aggregated 1298 0(0.00%) | 323 99 607 370 | 61.90 0.00
# receipt classifier(pod 3) : 20rps -> 111.8rps
Type Name # reqs # fails | Avg Min Max Med | req/s failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
POST /predictions/receipt-classification 4024 0(0.00%) | 266 133 2211 200 | 111.8 0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
Aggregated 4020 0(0.00%) | 266 133 2211 200 | 111.8 0.00
2:流量镜像
如前所述,我们团队的服务架构采用“traefik”工具作为应用服务器前方的网关,正如文章开头简要介绍的那样。为了进行最终验证,我们利用 traefik 网关的镜像功能,将生产环境的流量镜像到预演环境进行一个月的验证,然后才将其应用于生产环境,目前已投入运营。
有关镜像的详细信息超出了本主题的范围,因此此处省略。感兴趣的读者请参阅文档:https://doc.traefik.io/traefik/routing/services/#mirroring-service。
总结
本次讨论就此结束,内容涉及在保持服务质量的同时,从 GPU 模型服务器过渡到 CPU 服务器。通过这项工作,我们团队在韩国和日本分别节省了 15 个 GPU,从而每年节省了大约 34 万美元的成本。尽管我们在 NAVER 内部直接购买和使用 GPU,但我们根据可稳定支持 T4 GPU 的 AWS EC2 实例计算了一个粗略的成本降低。

计算:1.306(1 年期预留实例有效每小时成本)* 24(小时)* 365(天)* 15(GPU 数量)* 2(韩国 + 日本)
这些已获得的 GPU 将被利用,以进一步推进和增强我们团队的 AI 服务,提供卓越的服务体验。我们真诚地感谢您的鼓励和期待。:)
探索更多
- https://www.intel.com/content/www/us/en/developer/ecosystem/pytorch-foundation.html
- https://pytorch-geometric.readthedocs.io/en/latest/advanced/CPU_affinity.html#binding-processes-to-physical-cores
- https://arxiv.org/pdf/2205.10536.pdf