• 教程 >
  • 从原理出发深入理解 PyTorch Intel CPU 性能
快捷键

从原理出发深入理解 PyTorch Intel CPU 性能 ¶

创建于:2025 年 4 月 1 日 | 最后更新:2025 年 4 月 1 日 | 最后验证:2024 年 11 月 5 日

使用 Intel® Extension for PyTorch*优化的 TorchServe 推理框架案例研究

作者:Min Jean Cho, Mark Saroufim

审稿人:Ashok Emani, Jiong Gong

在 CPU 上获得强大的开箱即用性能对于深度学习来说可能很棘手,但如果你了解影响性能的主要问题、如何衡量它们以及如何解决它们,那就容易多了。

TL;DR

问题

如何进行测量

解决方案

拥塞的 GEMM 执行单元

通过核心绑定设置线程亲和性以避免使用逻辑核心

非均匀内存访问(NUMA)

  • 本地与远程内存访问

  • UPI 利用率

  • 内存访问的延迟

  • 线程迁移

通过设置线程亲和性到特定插座来避免跨插座计算

GEMM(通用矩阵乘法)在融合乘加(FMA)或点积(DP)执行单元上运行,当启用超线程时,这将成为瓶颈并导致线程在同步屏障处等待/空转延迟 - 因为使用逻辑核心会导致所有工作线程的并发性不足,因为每个逻辑线程都争夺相同的核心资源。相反,如果我们每个物理核心使用一个线程,我们就可以避免这种竞争。因此,我们通常建议通过核心绑定将 CPU 线程亲和性设置为物理核心。

多插座系统具有非一致性内存访问(NUMA),这是一种共享内存架构,描述了主内存模块相对于处理器的放置。但是,如果一个进程不是 NUMA 感知的,当线程在运行时通过英特尔超路径互连(UPI)跨插座迁移时,会频繁访问缓慢的远程内存。我们通过通过核心绑定将 CPU 线程亲和性设置到特定插座来解决此问题。

知道这些原则,适当的 CPU 运行时配置可以显著提升即开即用的性能。

在本文中,我们将带您了解从 CPU 性能调优指南中应了解的重要运行时配置,解释它们的工作原理、如何进行性能分析以及如何通过我们集成的易于使用的启动脚本将它们集成到 TorchServe 等模型服务框架中。

我们将使用大量的性能分析结果,从基本原理出发直观地解释所有这些想法,并展示我们如何将所学知识应用于提高 TorchServe 上 CPU 的即开即用性能。

  1. 该功能必须通过在 config.properties 中设置 cpu_launcher_enable=true 来显式启用。

避免使用逻辑核心进行深度学习 ¶

避免为深度学习工作负载使用逻辑核心通常可以提高性能。为了理解这一点,让我们回顾一下 GEMM。

优化 GEMM 优化深度学习

深度学习训练或推理的大部分时间都花在了数百万次重复的 GEMM 操作上,而 GEMM 是全连接层的核心。全连接层自多层感知器(MLP)证明是任何连续函数的通用逼近器以来已经使用了数十年。任何 MLP 都可以完全表示为 GEMM。甚至通过使用 Toepliz 矩阵,卷积也可以表示为 GEMM。

回到原始话题,大多数 GEMM 算子从使用非超线程中受益,因为在深度学习训练或推理过程中,大部分时间都花在了数百万次重复的 GEMM 操作上,这些操作在由超线程核心共享的融合乘加(FMA)或点积(DP)执行单元上运行。启用超线程时,OpenMP 线程将争夺相同的 GEMM 执行单元。

../_images/1_.png

如果两个逻辑线程同时运行 GEMM,它们将共享相同的核心资源,导致前端绑定,因此这种前端绑定的开销大于同时运行两个逻辑线程的收益。

因此,我们通常建议避免使用逻辑核心进行深度学习工作负载以获得良好的性能。默认情况下,启动脚本仅使用物理核心;然而,用户可以通过简单地切换 --use_logical_core 启动脚本旋钮来轻松地实验逻辑核心与物理核心。

练习

我们将使用以下示例来为 ResNet50 提供虚拟张量:

import torch
import torchvision.models as models
import time

model = models.resnet50(pretrained=False)
model.eval()
data = torch.rand(1, 3, 224, 224)

# warm up
for _ in range(100):
    model(data)

start = time.time()
for _ in range(100):
    model(data)
end = time.time()
print('Inference took {:.2f} ms in average'.format((end-start)/100*1000))

在整个博客中,我们将使用 Intel® VTune™分析器进行性能分析和验证优化。所有练习将在配备两个 Intel(R) Xeon(R) Platinum 8180M CPU 的机器上运行。CPU 信息如图 2.1 所示。

环境变量 OMP_NUM_THREADS 用于设置并行区域的线程数。我们将比较 OMP_NUM_THREADS=2 与(1)使用逻辑核心和(2)仅使用物理核心的情况。

  1. 两个 OpenMP 线程试图利用由超线程核心(0,56)共享的同一 GEMM 执行单元。

我们可以通过在 Linux 上运行 htop 命令来可视化这一点,如下所示。

../_images/2.png
../_images/3.png

我们注意到“旋转时间”被标记,不平衡或串行旋转导致了大部分时间 - 8.982 秒中的 4.980 秒。使用逻辑核心时的不平衡或串行旋转是由于工作线程的并发不足,因为每个逻辑线程都在争夺相同的核心资源。

执行摘要中的“热点”部分显示, __kmp_fork_barrier 消耗了 4.589 秒的 CPU 时间 - 在 9.33%的 CPU 执行时间内,线程在这个屏障上只是旋转,因为线程同步。

  1. 每个 OpenMP 线程在各自的物理核心(0,1)上利用 GEMM 执行单元。

../_images/4.png
../_images/5.png

我们首先注意到,通过避免使用逻辑核心,执行时间从 32 秒降低到 23 秒。虽然仍然存在一些不可忽视的不平衡或串行自旋,但我们注意到相对改进从 4.980 秒降低到 3.887 秒。

通过不使用逻辑线程(而是每个物理核心使用一个线程),我们避免了逻辑线程竞争相同的核心资源。顶部热点部分也表明, __kmp_fork_barrier 时间从 4.589 秒降低到 3.530 秒的相对改进。

本地内存访问总是比远程内存访问快 ¶

我们通常建议将进程绑定到本地套接字,这样进程就不会在套接字之间迁移。这样做的一般目标是利用本地内存上的高速缓存,并避免远程内存访问,这可能会慢 2 倍左右。

../_images/6.png

图 1. 双插槽配置

图 1 显示了典型的双插槽配置。请注意,每个插槽都有自己的本地内存。插槽之间通过英特尔超路径互连(UPI)相互连接,允许每个插槽访问另一个插槽的本地内存,称为远程内存。本地内存访问始终比远程内存访问更快。

../_images/7.png

图 2.1. CPU 信息

用户可以通过在 Linux 机器上运行 lscpu 命令来获取他们的 CPU 信息。图 2.1 显示了在具有两个英特尔(R)至强(R)铂金 8180M CPU 的机器上执行 lscpu 的示例。请注意,每个插槽有 28 个核心,每个核心有 2 个线程(即启用了超线程)。换句话说,除了 28 个物理核心外,还有 28 个逻辑核心,每个插槽总共 56 个核心。并且有 2 个插槽,总共 112 个核心( Thread(s) per core x Core(s) per socket x Socket(s) )。

../_images/8.png

图 2.2. CPU 信息

2 个插槽分别映射到 2 个 NUMA 节点(NUMA 节点 0,NUMA 节点 1)。物理核心在逻辑核心之前进行索引。如图 2.2 所示,第一个插槽上的前 28 个物理核心(0-27)和前 28 个逻辑核心(56-83)位于 NUMA 节点 0。第二个插槽上的后 28 个物理核心(28-55)和后 28 个逻辑核心(84-111)位于 NUMA 节点 1。同一插槽上的核心共享本地内存和最后一级缓存(LLC),这比通过 Intel UPI 进行跨插槽通信要快得多。

现在我们已经了解了 NUMA、跨插槽(UPI)流量、多处理器系统中的本地与远程内存访问,让我们来分析和验证我们的理解。

练习

我们将重用上面的 ResNet50 示例。

由于我们没有将线程固定到特定插槽的处理器核心上,操作系统会定期在不同的插槽上的处理器核心上调度线程。

../_images/9.gif

图 3. 非 NUMA 感知应用程序的 CPU 使用情况。启动了 1 个主工作线程,然后在该系统的所有核心上启动了物理核心数(56)的线程,包括逻辑核心。

(旁白:如果未通过 torch.set_num_threads 设置线程数,则默认线程数是启用超线程的系统中的物理核心数。这可以通过 torch.get_num_threads 进行验证。因此,我们上面看到大约一半的核心正忙于运行示例脚本。)

../_images/10.png

图 4. 非均匀内存访问分析图

图 4. 比较了本地与远程内存访问随时间的变化。我们验证了远程内存的使用情况,这可能导致性能不佳。

设置线程亲和性以减少远程内存访问和跨套接字(UPI)流量

将线程固定在相同套接字上的核心上有助于保持内存访问的局部性。在本例中,我们将固定到第一个 NUMA 节点(0-27)的物理核心上。通过启动脚本,用户可以通过简单地切换 --node_id 启动脚本旋钮来轻松地实验 NUMA 节点配置。

现在让我们可视化 CPU 使用情况。

../_images/11.gif

图 5. NUMA 感知应用的 CPU 使用情况

启动了 1 个主工作线程,然后在该 NUMA 节点的所有物理核心上启动了线程。

../_images/12.png

图 6. 非均匀内存访问分析图

如图 6 所示,现在几乎所有的内存访问都是局部访问。

使用核心绑定提高多工作线程推理的 CPU 效率 ¶

在运行多工作者推理时,核心在工作者之间重叠(或共享),导致 CPU 使用效率低下。为了解决这个问题,启动脚本将可用的核心数平均分配给工作者数量,使得每个工作者在运行时被固定到分配的核心上。

TorchServe 实践练习

对于这个练习,让我们将我们迄今为止讨论的 CPU 性能调优原则和建议应用到 TorchServe apache-bench 基准测试中。

我们将使用 ResNet50,4 个工作线程,并发 100,请求数量为 10,000。所有其他参数(例如 batch_size、input 等)与默认参数相同。

我们将比较以下三种配置:

  1. 默认的 TorchServe 设置(无核心绑定)

  2. torch.set_num_threads = number of physical cores / number of workers (无核心绑定)

  3. 通过启动脚本进行核心绑定(必需的 Torchserve>=0.6.1)

经过这次练习,我们将验证我们更喜欢避免逻辑核心,并更喜欢通过核心绑定进行本地内存访问,以实际的 TorchServe 用例为例。

1. 默认 TorchServe 设置(无核心绑定) ¶

基础处理器没有显式设置 torch.set_num_threads,因此默认线程数是物理 CPU 核心数,如这里所述。用户可以通过 base_handler 中的 torch.get_num_threads 检查线程数。每个 4 个主要工作线程都会在每个物理核心(56 个)上启动线程,总共启动 56x4 = 224 个线程,这超过了核心总数 112。因此,核心保证会被高度重叠,逻辑核心利用率高——多个工作线程同时使用多个核心。此外,由于线程没有绑定到特定的 CPU 核心,操作系统会定期调度线程到位于不同插槽的核心上。

  1. CPU 使用率

../_images/13.png

启动了 4 个主要工作线程,然后每个线程在所有核心上启动了物理核心数(56 个)的线程,包括逻辑核心。

  1. 核心绑定停滞

../_images/14.png

我们观察到核心绑定停顿高达 88.4%,导致流水线效率降低。核心绑定停顿表明 CPU 中可用执行单元的使用不理想。例如,连续的 GEMM 指令可能因为超线程核心共享的融合乘加(FMA)或点积(DP)执行单元而引起核心绑定停顿。正如前文所述,逻辑核心的使用加剧了这个问题。

../_images/15.png
../_images/16.png

空的流水线槽位没有填充微操作(uOps)被认为是停顿。例如,如果没有核心固定,CPU 的使用可能不是在计算上,而是在其他操作上,如 Linux 内核的线程调度。上面我们看到 __sched_yield 对大部分的自旋时间做出了贡献。

  1. 线程迁移

没有核心固定,调度器可能会将运行在核心上的线程迁移到另一个核心。线程迁移可能导致线程与已经加载到缓存中的数据解关联,从而增加数据访问的延迟。在 NUMA 系统中,当线程跨越插槽迁移时,这个问题会加剧。现在被加载到本地内存的高速缓存中的数据变成了远程内存,这要慢得多。

../_images/17.png

通常,线程总数应小于或等于核心支持的线程总数。在上面的例子中,我们注意到在 core_51 上执行了大量的线程,而不是预期的 2 个线程(因为 Intel(R) Xeon(R) Platinum 8180 CPU 中的超线程已启用)。这表明线程迁移。

../_images/18.png

此外,请注意,线程(TID:97097)在大量的 CPU 核心上执行,表明 CPU 迁移。例如,此线程曾在 cpu_81 上执行,然后迁移到 cpu_14,然后迁移到 cpu_5,等等。此外,请注意,此线程在多个插槽之间来回迁移多次,导致内存访问效率非常低。例如,此线程在 cpu_70(NUMA 节点 0)上执行,然后迁移到 cpu_100(NUMA 节点 1),然后迁移到 cpu_24(NUMA 节点 0)。

  1. 非均匀内存访问分析

../_images/19.png

比较本地与远程内存访问随时间的变化。我们观察到大约一半,即 51.09%的内存访问是远程访问,表明 NUMA 配置不理想。

2. torch.set_num_threads = number of physical cores / number of workers (无核心绑定) ¶

为了与启动器的核心绑定进行苹果对苹果的比较,我们将线程数设置为核心数除以工作进程数(启动器内部会这样做)。在 base_handler 中添加以下代码片段:

torch.set_num_threads(num_physical_cores/num_workers)

与之前无核心绑定的情况一样,这些线程没有绑定到特定的 CPU 核心,导致操作系统定期在不同的插槽上调度线程。

  1. CPU 使用率

../_images/20.gif

启动了 4 个主要工作线程,然后每个线程在所有核心上(包括逻辑核心)启动了 num_physical_cores/num_workers 个线程(14 个)。

  1. 核心绑定停滞

../_images/21.png

尽管核心绑定停顿的百分比从 88.4%下降到 73.5%,但核心绑定仍然非常高。

../_images/22.png
../_images/23.png
  1. 线程迁移

../_images/24.png

与之前相似,没有核心固定线程(TID:94290)在大量 CPU 核心上执行,表明 CPU 迁移。我们再次注意到跨插槽线程迁移,导致内存访问非常低效。例如,此线程在 cpu_78(NUMA 节点 0)上执行,然后迁移到 cpu_108(NUMA 节点 1)。

  1. 非均匀内存访问分析

../_images/25.png

虽然比原始的 51.09%有所改进,但仍有 40.45%的内存访问是远程的,表明 NUMA 配置不理想。

3. 启动器核心绑定 ¶

启动器将内部平均分配物理核心给工作进程,并将它们绑定到每个工作进程。提醒一下,启动器默认只使用物理核心。在本例中,启动器将工作进程 0 绑定到核心 0-13(NUMA 节点 0),工作进程 1 绑定到核心 14-27(NUMA 节点 0),工作进程 2 绑定到核心 28-41(NUMA 节点 1),工作进程 3 绑定到核心 42-55(NUMA 节点 1)。这样做确保了核心在工作进程之间不会重叠,并避免了逻辑核心的使用。

  1. CPU 使用率

../_images/26.gif

启动了 4 个主要工作线程,然后每个线程启动了 14 个绑定到分配的物理核心的线程。

  1. 核心绑定停滞

../_images/27.png

核绑定停滞率从原始的 88.4%显著下降到 46.2% - 几乎提高了 2 倍。

../_images/28.png
../_images/29.png

我们验证,在核绑定的情况下,大部分 CPU 时间都有效地用于计算 - 旋转时间为 0.256 秒。

  1. 线程迁移

../_images/30.png

我们验证,OMP 主线程#0 被绑定到指定的物理核心(42-55),并且没有跨插槽迁移。

  1. 非均匀内存访问分析

../_images/31.png

现在几乎全部,89.52%,的内存访问都是局部访问。

结论 ¶

在这篇博客中,我们展示了正确设置 CPU 运行时配置可以显著提升 CPU 的即插即用性能。

我们已经介绍了一些通用的 CPU 性能调优原则和建议:

  • 在启用超线程的系统上,通过核心绑定仅设置线程亲和度到物理核心,避免使用逻辑核心。

  • 在具有 NUMA 的多插槽系统中,通过核心绑定将线程亲和度设置到特定的插槽,避免跨插槽远程内存访问。

我们从第一原理出发对这些想法进行了可视化解释,并通过性能分析验证了性能提升。最后,我们将所学知识应用于 TorchServe,以提升 TorchServe 的 CPU 性能。

这些原则可以通过一个易于使用的启动脚本自动配置,该脚本已经集成到 TorchServe 中。

感兴趣的读者请参阅以下文档:

请关注后续关于通过 Intel® Extension for PyTorch* 优化内核在 CPU 上的文章,以及高级启动器配置,如内存分配器。

致谢

我们要感谢 Ashok Emani(英特尔)和 Jiong Gong(英特尔)在许多步骤中的巨大指导和支持,以及详尽的反馈和审查。我们还要感谢 Hamid Shojanazeri(Meta)、Li Ning(AWS)和 Jing Xu(英特尔)在代码审查中的有益反馈。同时感谢 Suraj Subramanian(Meta)和 Geeta Chauhan(Meta)对博客的有益反馈。


评分这个教程

© 版权所有 2024,PyTorch。

使用 Sphinx 构建,主题由 Read the Docs 提供。
//暂时添加调查链接

文档

访问 PyTorch 的全面开发者文档

查看文档

教程

获取初学者和高级开发者的深入教程

查看教程

资源

查找开发资源并获得您的疑问解答

查看资源