博客

ML 模型服务器资源节省——从高成本 GPU 到 Intel CPU 和 oneAPI 驱动的软件的性能转换

审阅人: Yunsang Ju (Naver GplaceAI 负责人), Min Jean Cho (英特尔), Jing Xu (英特尔), Mark Saroufim (Meta)

引言

本文将分享我们如何将 AI 工作负载从 GPU 服务器迁移至英特尔 CPU 服务器,且在不降低性能或质量的情况下,**每年节省约 34 万美元的成本**(请参阅“结论”部分)。

我们的目标是通过提供各类增强 O2O(线上到线下)体验的 AI 模型来为消费者创造价值。随着新模型需求的持续增长以及高成本 GPU 资源的局限性,我们需要将相对轻量级的 AI 模型从 GPU 服务器迁移至英特尔 CPU 服务器,以降低资源消耗。但在同等配置下,CPU 服务器面临着 RPS(每秒请求数)、推理耗时等性能下降数十倍的问题。通过应用多种工程技术及模型轻量化手段,我们成功解决了这一难题,最终仅通过三倍的扩容,便使 CPU 服务器实现了与 GPU 服务器持平甚至更优的性能。

有关我们团队的详细介绍,请参阅 NAVER Place AI 开发团队介绍

文中稍后会再次提及,在整体工作过程中,我们参考了由英特尔和 PyTorch 编写的《从第一性原理理解 PyTorch 英特尔 CPU 性能》,并从中获得了巨大帮助。

问题定义

1: 服务架构

Simplified service architecture

简化版服务架构(图片来源:NAVER GplaceAI)

为方便理解,先简要介绍我们的服务架构。CPU 密集型任务(如将输入预处理为张量格式并转发给模型,以及将推理结果后处理为人类可读格式,如自然语言和图像)在应用服务器 (FastAPI) 上执行。模型服务器 (TorchServe) 专门负责处理推理运算。为了保持服务的稳定运行,以下操作需要在具备足够吞吐量和低延迟的前提下完成。

具体处理流程如下:

  • 客户端通过 Traefik 网关向应用服务器提交请求。
  • 应用服务器对输入进行预处理(如缩放、转换),并将其转换为 Torch 张量,随后请求模型服务器。
  • 模型服务器执行推理并将特征返回给应用服务器。
  • 应用服务器通过后处理将特征转换为人类可理解的格式,并返回给客户端。

2: 吞吐量和延迟测量

Comparison of Image Scoring Models

图像评分模型比较

在所有其他条件不变的情况下,即便部署在三倍扩容的 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

TorchServe 框架用户提高吞吐量的首选做法通常是增加 Worker 数量。由于并行工作负载处理,这种方法在 GPU 服务器上非常有效,即便随着 Worker 扩展会导致内存线性增加。然而,我们在增加 Worker 数量时,性能反而下降。因此,识别 CPU 服务器上性能下降的原因成为了后续调查的关键。

4: 延迟方面的挑战

我们最担心的是延迟。通常只要系统实现符合扩展原则,吞吐量是可以提升的。但在图像评分模型的例子中,即便执行单次推理也耗时超过 1 秒;随着请求量增加,延迟甚至升至 4 秒。这导致即使单次推理也无法满足客户端的超时要求。

解决方案

我们需要从机器学习和工程两个角度进行改进。核心在于根本性地降低 CPU 推理耗时,并找出应用常规性能优化配置后导致性能下降的原因,从而找到最优配置。为此,我们与机器学习专家 (MLE) 合作,同步开展“在不影响性能的前提下轻量化模型”和“寻找实现峰值性能的最优配置”两项工作。通过上述方法,我们有效地将工作负载转移到了 CPU 服务器上。

1: 从工程角度解决低 RPS 问题

首先,增加 Worker 后性能反而下降的原因是 GEMM(通用矩阵乘法)运算中逻辑线程导致的“前端受限”(front-end bound)。通常增加 Worker 会预期带来并行度提升,如果性能反而下降,则说明存在对应的副作用。

CPU + GPU

图片来源:Nvidia

众所周知,CPU 模型推理性能不如 GPU 的原因在于硬件设计,特别是多线程能力的差异。深入来看,模型推理本质上是重复的 GEMM 运算,而这些 GEMM 运算在“融合乘加”(FMA) 或“点积”(DP) 执行单元中独立执行。如果 GEMM 运算成为 CPU 的瓶颈,增加并行度反而可能导致性能下降。在研究该问题时,我们在 PyTorch 文档中找到了相关信息。

当两个逻辑线程同时运行 GEMM 时,它们会共享相同的核心资源,导致前端受限。

该信息强调逻辑线程可能导致 CPU GEMM 运算瓶颈,这让我们直观地理解了为何增加 Worker 数反而导致性能下降——因为 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 数增加时,总线程数会随“物理核心数 * Worker 数”成倍增加,从而调用了逻辑线程。为了提升性能,我们将每个 Worker 的总线程数调整为与物理核心数一致。如下所示,当 Worker 数增加到 4 且总线程数与物理核心数对齐时,RPS 指标从 2.1 增加到了 6.3,实现了约三倍的提升。

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 数的倍数,以便充分利用 CPU 资源。

example

例如:核心数=8,Worker 数=3 时:int(8/3) = 2,2*3/8 = 75%

example

例如:核心数=8,Worker 数=4 时:int(8/4) = 2,2*4/8 = 100%

我们还分析了模型容器,试图找出为什么 Worker 数增加四倍后性能仅提升了三倍。通过监控各种资源,我们将“核心利用率”确定为根本原因。

threads

即使在总线程数与 CPU(第二代英特尔® 至强® Silver 4214)限制(8 核心)一致时,计算仍存在从逻辑线程调度到逻辑核心的情况。由于有 24 个物理核心,第 25 到 48 号核心被归类为逻辑核心。将线程执行限制在物理核心内似乎是进一步提升性能的途径。该方案的参考依据可以在 PyTorch-geometric 关于 CPU GEMM 瓶颈的文章中找到。

按照文档说明,英特尔提供了 Intel® Extension for PyTorch,我们可以简单地将核心绑定到特定的插槽(Socket)。应用方法也很简单,只需在 torchserve config.properties 文件中添加以下设置。(使用 intel_extension_for_pytorch==1.13.0)

ipex_enable=true
CPU_launcher_enable=true
two-socket configuration

图片来源:PyTorch

除了通过插槽绑定消除逻辑线程的影响外,还有一个消除 UPI 缓存命中开销的额外效果。由于 CPU 包含多个插槽,当在插槽 1 上调度的线程被重新调度到插槽 2 时,通过英特尔 Ultra Path Interconnect (UPI) 访问插槽 1 的缓存会产生缓存命中。此时,UPI 对本地缓存的访问速度比本地缓存访问慢两倍以上,从而导致更多的瓶颈。通过使用基于 oneAPI 的 Intel® Extension for PyTorch 将线程绑定到插槽单元,我们观察到 RPS 处理能力最高提升了 4 倍。

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.5 rps -> 17.5 rps)。

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: 通过模型轻量化解决高延迟问题

我们还使用了知识蒸馏 (Knowledge Distillation, KD) 来进一步降低延迟。众所周知,KD 是一种将大网络(教师网络)的知识传递给更小、更轻量的网络(学生网络)的技术,后者资源消耗更少,且更易于部署。详细信息请参阅最初提出该概念的论文《Distilling the Knowledge in a Neural Network》。

neural networks

KD 技术种类繁多,由于我们主要关注最小化准确率损失,因此采用了 2022 年发表的论文《Knowledge Distillation from A Stronger Teacher》中的方法。其概念很简单:与仅利用模型输出值的传统蒸馏方法不同,该方法让学生网络学习教师网络中各类别之间的相关性。在实际应用中,我们观察到模型在保持高准确率的同时,权重得到了有效精简。以下是我们对若干候选学生模型进行实验的结果,选择标准是准确率的保持水平。

table of services

对于图像评分系统,我们还采取了额外的措施来减小输入尺寸。考虑到之前使用的是基于 CPU 的机器学习技术 SVR(2 阶段:CNN + SVR),即使将其精简为 1 阶段模型,在 CPU 推理中也未观察到显著的速度优势。为了使精简产生实际意义,推理过程中学生模型的输入尺寸需要进一步减小。因此,我们将尺寸从 384*384 减小到了 224*224 进行实验。

进一步简化转换后,我们将 2 阶段(CNN + SVR)方法统一为一个更大的 ConvNext 1 阶段模型,并使用轻量级 EfficientNet 应用 KD,从而解决了准确率权衡问题。实验中,我们将 Img_resize 改为 224 后,MAE(平均绝对误差)从 0.4007 降至 0.4296。由于输入尺寸减小,原始训练图像中应用的各种预处理技术(如 Affine, RandomRotate90, Blur, OneOf [GridDistortion, OpticalDistortion, ElasticTransform], VerticalFlip)反而产生了负面影响。通过调整这些预处理措施,我们实现了学生模型的有效训练,MAE 值比之前提升了 25%(从 .518 降至 .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 网关的镜像功能,将生产环境的流量镜像到 Staging 环境进行了一个月的验证,确认无误后现已部署至生产环境。

有关镜像的详细信息超出了本文范围,在此不再赘述。有兴趣的读者可参阅文档:https://doc.traefik.io/traefik/routing/services/#mirroring-service

结论

以上就是我们在保证服务质量的前提下,从 GPU 模型服务器迁移到 CPU 服务器的讨论。通过这项工作,我们团队在韩国和日本分别节省了 15 张 GPU,从而实现了每年约 34 万美元的成本节省。虽然 NAVER 是直接购买和使用 GPU,但我们是基于 AWS EC2 实例对稳定支持 T4 GPU 的成本进行了粗略折算。

instance sizes

计算公式:1.306(1年期预留实例有效每小时成本)* 24(小时)* 365(天)* 15(GPU 数量)* 2(韩国 + 日本)

这些腾出的 GPU 资源将被用于进一步推动和加强我们团队的 AI 服务,带来更卓越的服务体验。衷心感谢您的鼓励与期待。:)

探索更多