博客

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

审稿人: Yunsang Ju(Naver GplaceAI 负责人), Min Jean Cho(Intel), Jing Xu(Intel), Mark Saroufim(Meta)

引言

在这里,我们将分享我们如何在不影响性能或质量的情况下,将 AI 工作负载从 GPU 服务器迁移到 Intel CPU 服务器的经验,并且在此过程中**节省了约 34 万美元的年成本**(请参阅**结论**)。

我们的目标是通过提供各种增强线上到线下 (O2O) 体验的 AI 模型来为我们的客户创造价值。随着新模型需求的不断增长以及高成本 GPU 资源的有限性,我们需要将相对轻量级的 AI 模型从 GPU 服务器迁移到 Intel CPU 服务器,以减少资源消耗。但在相同环境下,CPU 服务器却出现了 RPS、推理时间等性能下降几十倍的问题。我们应用了各种工程技术并对模型进行了轻量化来解决这个问题,并且仅通过三倍的扩展,就成功地以与 GPU 服务器相同甚至更好的性能迁移到了 Intel CPU 服务器。

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

我将在中间再次提及,但在整个工作中,Intel 和 PyTorch 撰写的《深入理解 PyTorch Intel 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

TorchServer 框架用户可以采取的第一个提高吞吐量的步骤是增加 TorchServe 中的工作进程数量。这种方法在 GPU 服务器上是有效的,因为可以并行处理工作负载,除了工作进程扩展时线性内存使用量的增加。然而,我们发现在增加工作进程数量时性能反而变差了。找出 CPU 服务器上性能下降的原因需要进一步调查。

4:延迟方面的挑战

我们的主要关注点是延迟。只要系统的实现符合扩展性原则,吞吐量改进通常是可以实现的,除了非常罕见的极端情况。然而,就图像评分模型示例而言,即使是单个推理也需要超过 1 秒,并且随着请求量的增加,延迟高达 4 秒。在这种情况下,即使是单个推理也无法满足客户端的超时标准。

提出的解决方案

需要从 ML 和工程两个角度进行改进。从根本上减少 CPU 上的推理时间,并找出在应用通常能提高性能的配置时性能下降的原因,以找到最佳配置值至关重要。为了实现这一点,我们与 MLE 专业人员合作,同步执行包括“在不影响性能的情况下进行模型轻量化”和“找出实现峰值性能的最佳配置”的任务。通过上述方法,我们能够有效地将工作负载处理转移到我们的 CPU 服务器。

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

首先,即使在增加工作进程数量后性能下降的原因是 GEMM 操作中的逻辑线程导致的“前端瓶颈”。通常,增加工作进程数量时,预期的改进效果是并行度的增加。相反,如果性能下降,可以推断出相应的权衡效应。

CPU + GPU

图片来源: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_num 增加到 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 命令查看)的物理核心数。(将每个工作进程的 torch 线程设置为 8/4 = 2 或 24/4 = 6 导致性能下降。)

注意事项 2:由于每个工作进程的 torch 线程设置只能配置为整数,因此建议将 CPU 限制设置为可被工作进程数整除的值,以充分利用 CPU 资源。

example

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

example

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

我们还分析了模型容器,以了解为什么在工作进程数量增加四倍的情况下,性能仅提高了三倍。我们监控了各种资源,其中核心利用率被确定为根本原因。

threads

即使在总线程数调整为匹配 CPU(第二代 Intel(R) Xeon(R) Silver 4214)限制(8 核)的情况下,也出现了计算从逻辑线程执行到逻辑核心的情况。由于存在 24 个物理核心,编号为 25 到 48 的核心被归类为逻辑核心。将线程执行限制在物理核心内的可能性似乎为进一步提高性能提供了潜力。在 PyTorch-geometric 文章中提到的警告 CPU GEMM 瓶颈的来源文档中可以找到对该解决方案的引用。

根据文档中的说明,Intel 提供了 Intel® Extension for PyTorch,我们可以简单地将核心固定到特定的套接字。应用程序方法也非常简单,只需在 **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 时,访问套接字 1 的缓存会发生缓存命中。此时,通过 Intel Ultra Path Interconnect (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 及更低版本不支持多工作进程固定(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》。

neural networks

有多种 KD 技术可用,由于我们主要关注**准确性损失最小化**,因此我们采用了 2022 年发表的论文《Knowledge Distillation from A Stronger Teacher》中的方法。该概念很简单。与仅使用模型预测值的传统蒸馏方法不同,所选方法涉及让学生网络学习教师网络中类之间的关联。在实际应用中,我们观察到了有效的模型权重减少,同时保持了高精度。以下是我们对几种候选学生模型进行上述知识蒸馏技术实验的结果,选择是基于保持的准确性水平。

table of services

对于图像评分系统,我们采取了额外的措施来减小输入尺寸。考虑到之前使用的是基于 CPU 的 ML 技术 SVR(支持向量回归)(两阶段:CNN + SVR),即使将其简化为单阶段模型,在 CPU 推理中也没有观察到显着的性能优势。为了使简化具有意义,学生模型在推理期间的输入尺寸需要进一步减小。因此,我们进行了将尺寸从 384*384 减小到 224*224 的实验。

通过进一步简化转换,将两阶段(CNN + SVR)方法统一为使用更大的 ConvNext 的单阶段模型,然后使用轻量级的 EfficientNet 进行 KD,以解决准确性权衡问题。在实验过程中,我们遇到了一个问题,将 Img_resize 更改为 224 导致 MAE 值从 0.4007 下降到 0.4296。由于输入尺寸的减小,应用于原始训练图像的各种预处理技术(如 Affine、RandomRotate90、Blur、OneOf [GridDistortion、OpticalDistortion、ElasticTransform]、VerticalFlip)产生了适得其反的效果。通过采取这些措施,实现了学生模型的有效训练,并且**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 实例**计算了大致的成本节省。

instance sizes

计算:1.306(1 年预留实例有效小时成本)* 24(小时)* 365(天)* 15(GPU 数量)* 2(KR + JP)

这些腾出的 GPU 将用于进一步推进和增强我们团队的 AI 服务,提供卓越的服务体验。我们衷心感谢您的鼓励和期待。:)

探索更多