由桑俊珺(Naver GplaceAI MLOps)、刘俊英(Naver GplaceAI MLE)、闵俊浩(Naver GplaceAI MLE)著

审稿人:朱云桑(Naver GplaceAI 领导),赵明珍(英特尔),徐静(英特尔),马克·萨拉菲姆(Meta)

简介

在本文中,我们将分享我们将 AI 工作负载从我们的 GPU 服务器迁移到我们的英特尔 CPU 服务器的过程,在此过程中没有出现性能或质量下降,并且节省了约 34 万美元的年度成本(参见结论部分)。

我们旨在通过提供各种增强线上线下(O2O)体验的 AI 模型,为消费者创造价值。随着对新模型需求的不断增长以及高成本资源 GPU 的有限性,我们需要将相对轻量级的 AI 模型从 GPU 服务器迁移到英特尔 CPU 服务器,以减少资源消耗。然而,在相同的设置中,CPU 服务器存在性能问题,rps、推理时间等性能降低了数十倍。我们应用了各种工程技术,并对模型进行了轻量化,以解决这个问题,并成功迁移到英特尔 CPU 服务器,其性能与 GPU 服务器相当甚至更好,只需扩展三倍即可。

欲了解更多关于我们团队的信息,请参阅《NAVER Place AI 开发团队简介》。

我再次提到,我在整个工作中得到了《从原理出发深入理解 PyTorch 在英特尔 CPU 上的性能》这本书的很大帮助,该书由英特尔和 PyTorch 共同撰写。

问题定义

1: 服务架构

Simplified service architecture

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

为便于理解,将简要介绍我们的服务架构。在 App Server(FastAPI)上执行 CPU 密集型任务,如将输入预处理为张量格式(然后转发到模型)以及将推理结果后处理为人类可读的输出(例如自然语言和图像格式)。Model Server(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 秒。这是一个即使单个推理也无法满足客户端超时标准的情况。

提出的解决方案

从机器学习和工程两个角度都需要进行改进。在 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 数量的乘积。因此,逻辑线程被利用。为了提高性能,每个 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 使用率。

example

示例) core=8,在 worker_num=3 的情况下:int(8/worker_num) = 2,2*worker_num/8 = 75%

example

示例:core=8,在 worker_num=4 的情况下:int(8/worker_num) = 2,2*worker_num/8 = 100%

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

threads

即使将总线程数调整到与 CPU(第二代,英特尔(R)至强(R)银牌 4214)限制(8 核心)相匹配,也存在从逻辑线程到逻辑核心执行计算的情况。由于存在 24 个物理核心,编号为 25 至 48 的核心被归类为逻辑核心。将线程执行仅限于物理核心的可能性似乎为性能进一步提升提供了潜力。有关此解决方案的参考可以在 PyTorch-geometric 文章中找到,该文章警告了 CPU GEMM 瓶颈。

  • 参考文档:《从第一原理理解 PyTorch 英特尔 CPU 性能》

根据文档中的说明,英特尔提供了 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 时,会通过英特尔超路径互连(UPI)访问插槽 1 的缓存,从而发生缓存命中。此时,对本地缓存的 UPI 访问速度比本地缓存访问速度慢两倍以上,导致更多瓶颈。通过由 oneAPI 驱动的英特尔® 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 是一种将大网络(教师网络)中的知识传递给较小、轻量级网络(学生网络)的技术,这种网络资源消耗更少,更容易部署。有关更详细的信息,请参阅最初介绍这一概念、题为《在神经网络中蒸馏知识》的论文。

neural networks

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

table of services

为了图像评分系统,采取了额外措施以减小输入大小。考虑到先前使用基于 CPU 的机器学习技术 SVR(支持向量回归)(两阶段:CNN + SVR),即使将其简化为单阶段模型,在 CPU 推理中也没有观察到显著的速度优势。为了简化具有意义,需要在推理过程中进一步减小学生模型的输入大小。因此,进行了从 384384 减小到 224224 大小的实验。

进一步简化变换,将两阶段(CNN + SVR)方法统一为单阶段模型,并使用更大的 ConvNext,然后使用轻量级的 EfficientNet 应用 kd 以解决精度权衡问题。在实验过程中,我们发现将 Img_resize 改为 224 会导致 MAE 性能从 0.4007 下降到 0.4296。由于输入尺寸减小,对原始训练图像应用的各种预处理技术(如仿射变换、随机旋转 90 度、模糊、OneOf [GridDistortion, OpticalDistortion, ElasticTransform]、垂直翻转)产生了反效果。通过采取这些措施,实现了学生的有效训练,与之前相比,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 服务,提供卓越的服务体验。我们衷心感谢您的鼓励和期待。:)

探索更多