PyTorch 2.2 中文官方教程(十五)(3)

简介: PyTorch 2.2 中文官方教程(十五)

PyTorch 2.2 中文官方教程(十五)(2)https://developer.aliyun.com/article/1482583

从第一原则理解 PyTorch Intel CPU 性能

原文:pytorch.org/tutorials/intermediate/torchserve_with_ipex.html

译者:飞龙

协议:CC BY-NC-SA 4.0

一个关于使用Intel® PyTorch 扩展*优化的 TorchServe 推理框架的案例研究。

作者:Min Jean Cho, Mark Saroufim

审阅者:Ashok Emani, Jiong Gong

在 CPU 上获得强大的深度学习开箱即用性能可能有些棘手,但如果您了解影响性能的主要问题,如何衡量它们以及如何解决它们,那么就会更容易。

简而言之

问题 如何衡量 解决方案
瓶颈的 GEMM 执行单元
通过核心固定将线程亲和性设置为物理核心以避免使用逻辑核心
非均匀内存访问(NUMA)
  • 本地与远程内存访问
  • UPI 利用率
  • 内存访问延迟
  • 线程迁移
通过核心固定将线程亲和性设置为特定插槽以避免跨插槽计算

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

多插槽系统具有非均匀内存访问(NUMA),这是一种共享内存架构,描述了主存储模块相对于处理器的放置方式。但是,如果一个进程不是 NUMA 感知的,那么在运行时,当线程通过Intel Ultra Path Interconnect (UPI)跨插槽迁移时,会频繁访问慢远程内存。我们通过将 CPU 线程亲和性设置为特定插槽以通过核心固定解决这个问题。

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

在这篇博客中,我们将带您了解CPU 性能调优指南中应该注意的重要运行时配置,解释它们的工作原理,如何对其进行分析以及如何将其集成到像TorchServe这样的模型服务框架中,通过一个易于使用的启动脚本,我们已经原生地集成了。

我们将从第一原则以及大量概要直观地解释所有这些想法,并向您展示我们如何应用我们的学习,以改善 TorchServe 的开箱即用 CPU 性能。

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

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

通常,避免逻辑核心进行深度学习工作负载会提高性能。为了理解这一点,让我们回到 GEMM。

优化 GEMM 优化深度学习

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

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

如果 2 个逻辑线程同时运行 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™ Profiler来进行分析和验证优化。我们将在一台配备两个 Intel® Xeon® Platinum 8180M CPU 的机器上运行所有练习。CPU 信息如图 2.1 所示。

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

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

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

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

执行摘要的 Top Hotspots 部分显示,__kmp_fork_barrier占用了 4.589 秒的 CPU 时间 - 在 CPU 执行时间的 9.33%期间,线程在这个屏障处旋转以进行线程同步。

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

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

通过不使用逻辑线程(而是每个物理核心使用 1 个线程),我们避免了逻辑线程争夺相同核心资源。Top Hotspots 部分还显示了__kmp_fork_barrier时间从 4.589 秒改善到 3.530 秒的相对改善。

本地内存访问始终比远程内存访问快。

通常我们建议将一个进程绑定到本地插槽,以便该进程不会在不同插槽之间迁移。通常这样做的目的是利用本地内存上的高速缓存,并避免远程内存访问,后者可能慢大约 2 倍。

图 1. 双插槽配置

图 1. 显示了一个典型的双插槽配置。请注意,每个插槽都有自己的本地内存。插槽通过 Intel Ultra Path Interconnect (UPI)连接到彼此,这允许每个插槽访问另一个插槽的本地内存,称为远程内存。本地内存访问始终比远程内存访问快。

图 2.1. CPU 信息

用户可以通过在他们的 Linux 机器上运行lscpu命令来获取他们的 CPU 信息。图 2.1. 显示了在一台装有两个 Intel® Xeon® Platinum 8180M CPU 的机器上执行lscpu的示例。请注意,每个插槽有 28 个核心,每个核心有 2 个线程(即启用了超线程)。换句话说,除了 28 个物理核心外,还有 28 个逻辑核心,每个插槽总共有 56 个核心。而且有 2 个插槽,总共有 112 个核心(每个核心的线程数 x 每个插槽的核心数 x 插槽数)。

图 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 示例。

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

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

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

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

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

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

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

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

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

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

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

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

通过核心固定实现多工作线程推理的高效 CPU 使用率

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

使用 TorchServe 进行练习

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

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

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

  1. 默认的 TorchServe 设置(没有核心固定)
  2. torch.set_num_threads = 物理核心数 / 工作线程数(没有核心固定)
  3. 通过启动脚本进行核心固定(要求 Torchserve>=0.6.1)

经过这个练习,我们将验证我们更喜欢避免逻辑核心,而是通过核心固定实现本地内存访问,使用真实的 TorchServe 用例。

1. 默认的 TorchServe 设置(没有核心固定)

base_handler没有明确设置torch.set_num_threads。因此,默认线程数是物理 CPU 核心数,如这里所述。用户可以通过在 base_handler 中使用torch.get_num_threads来检查线程数。每个 4 个主要工作线程启动了一个物理核心数(56)的线程,总共启动了 56x4 = 224 个线程,这比总核心数 112 多。因此,核心肯定会严重重叠,逻辑核心利用率高-多个工作线程同时使用多个核心。此外,由于线程没有固定到特定的 CPU 核心,操作系统会定期将线程调度到位于不同插槽中的核心。

  1. CPU 使用率

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

  1. 核心绑定停顿

我们观察到 88.4%的非均匀内存访问,降低了管道效率。核心绑定停顿表示 CPU 中可用执行单元的使用不佳。例如,连续的几个 GEMM 指令竞争由超线程核心共享的融合乘加(FMA)或点积(DP)执行单元可能导致核心绑定停顿。正如前一节所述,逻辑核心的使用加剧了这个问题。

未填充微操作(uOps)的空管道槽位被归因于停顿。例如,没有核心固定时,CPU 使用率可能不会有效地用于计算,而是用于来自 Linux 内核的其他操作,如线程调度。我们可以看到__sched_yield贡献了大部分自旋时间。

  1. 线程迁移

没有核心固定,调度程序可能会将在一个核心上执行的线程迁移到另一个核心。线程迁移可能会使线程与已经获取到缓存中的数据分离,导致更长的数据访问延迟。在 NUMA 系统中,当线程在插座之间迁移时,这个问题会加剧。已经获取到本地内存高速缓存的数据现在变成了远程内存,速度要慢得多。

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

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

  1. 非均匀内存访问分析

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

2. torch.set_num_threads = 物理核心数/工作线程数(无核心固定)

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

torch.set_num_threads(num_physical_cores/num_workers) 

与之前一样,没有核心固定,这些线程没有与特定 CPU 核心关联,导致操作系统周期性地在位于不同插座的核心上调度线程。

  1. CPU 使用率

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

  1. 核心绑定停顿

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

  1. 线程迁移

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

  1. 非均匀内存访问分析

尽管相比原始的 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 使用率

启动了 4 个主要工作线程,然后每个工作线程启动了num_physical_cores/num_workers数量(14)的线程,这些线程与分配的物理核心亲和。

  1. 核心绑定的停顿

核心绑定的停顿从原始的 88.4%显著减少到 46.2% - 几乎提高了 2 倍。

我们验证了通过核心绑定,大部分 CPU 时间有效地用于计算 - 自旋时间为 0.256 秒。

  1. 线程迁移

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

  1. 非均匀内存访问分析

现在几乎所有,89.52%,内存访问都是本地访问。

结论

在本博客中,我们展示了正确设置 CPU 运行时配置如何显著提升开箱即用的 CPU 性能。

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

  • 在启用超线程的系统中,通过核心绑定将线程亲和性设置为仅在物理核心上,避免逻辑核心。
  • 在具有 NUMA 的多插槽系统中,通过将线程亲和性设置为特定插槽,避免跨插槽的远程内存访问。

我们从基本原理上直观地解释了这些想法,并通过性能分析验证了性能提升。最后,我们将所有学到的东西应用到 TorchServe 中,以提升开箱即用的 TorchServe CPU 性能。

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

对于感兴趣的读者,请查看以下文档:

请继续关注有关通过Intel® Extension for PyTorch*优化 CPU 内核和高级启动器配置(如内存分配器)的后续文章。

致谢

我们要感谢 Ashok Emani(Intel)和 Jiong Gong(Intel)在这篇博客的许多步骤中给予我们巨大的指导和支持,以及详尽的反馈和审查。我们还要感谢 Hamid Shojanazeri(Meta)、Li Ning(AWS)和 Jing Xu(Intel)在代码审查中提供的有用反馈。以及 Suraj Subramanian(Meta)和 Geeta Chauhan(Meta)在博客中提供的有用反馈。

从第一原则开始理解 PyTorch 英特尔 CPU 性能(第 2 部分)

原文:pytorch.org/tutorials/intermediate/torchserve_with_ipex_2.html

译者:飞龙

协议:CC BY-NC-SA 4.0

作者:Min Jean ChoJing XuMark Saroufim

Grokking PyTorch 英特尔 CPU 性能从第一原则开始教程中,我们介绍了如何调整 CPU 运行时配置,如何对其进行性能分析,以及如何将它们集成到TorchServe中以获得优化的 CPU 性能。

在本教程中,我们将通过英特尔® PyTorch*扩展启动器演示如何通过内存分配器提高性能,并通过英特尔® PyTorch*扩展在 CPU 上优化内核,并将它们应用于 TorchServe,展示 ResNet50 的吞吐量提升了 7.71 倍,BERT 的吞吐量提升了 2.20 倍。

先决条件

在整个本教程中,我们将使用自顶向下的微体系结构分析(TMA)来对性能进行分析,并展示未经优化或未调整的深度学习工作负载通常主要受到后端受限(内存受限、核心受限)的影响,并通过英特尔® PyTorch*扩展演示优化技术以改善后端受限。我们将使用toplev,这是pmu-tools的一部分工具,构建在Linux perf之上,用于 TMA。

我们还将使用英特尔® VTune™ Profiler 的仪器化和跟踪技术(ITT)以更精细的粒度进行性能分析。

自顶向下的微体系结构分析方法(TMA)

在调整 CPU 以获得最佳性能时,了解瓶颈所在是很有用的。大多数 CPU 核心都有芯片上的性能监控单元(PMUs)。PMUs 是 CPU 核心内的专用逻辑单元,用于计算系统上发生的特定硬件事件。这些事件的示例可能是缓存未命中或分支误预测。PMUs 用于自顶向下的微体系结构分析(TMA)以识别瓶颈。TMA 包括如下层次结构:

顶层,即一级,指标收集退休错误猜测前端受限后端受限。CPU 的流水线在概念上可以简化并分为两部分:前端和后端。前端负责获取程序代码并将其解码为称为微操作(uOps)的低级硬件操作。然后将 uOps 传送到后端,这个过程称为分配。一旦分配,后端负责在可用的执行单元中执行 uOp。uOp 的执行完成称为退休。相反,错误猜测是指在退休之前取消了被推测获取的 uOps,例如在误预测分支的情况下。这些指标中的每一个都可以进一步细分为后续级别,以确定瓶颈。

调整后端受限

大多数未调整的深度学习工作负载将是后端受限的。解决后端受限通常是解决导致退休时间超过必要时间的延迟源。如上所示,后端受限有两个子指标 - 核心受限和内存受限。

内存绑定停顿与内存子系统相关。例如,最后一级缓存(LLC 或 L3 缓存)缺失导致访问 DRAM。扩展深度学习模型通常需要大量计算。高计算利用率要求在执行单元需要执行 uOps 时数据可用。这需要预取数据并在缓存中重用数据,而不是多次从主存储器中获取相同的数据,这会导致执行单元在数据返回时被饿死。在整个教程中,我们将展示更高效的内存分配器、操作融合、内存布局格式优化如何减少内存绑定的开销,提高缓存局部性。

核心绑定停顿表示可用执行单元的子优化使用,而没有未完成的内存访问。例如,连续的多个通用矩阵乘法(GEMM)指令竞争融合乘加(FMA)或点积(DP)执行单元可能导致核心绑定停顿。关键的深度学习核心,包括 DP 核心,已经通过oneDNN 库(oneAPI 深度神经网络库)进行了优化,减少了核心绑定的开销。

GEMM、卷积、反卷积等操作是计算密集型的。而池化、批量归一化、激活函数如 ReLU 等操作是内存绑定的。

Intel® VTune™ Profiler 的仪器化和跟踪技术(ITT)

Intel® VTune Profiler 的 ITT API 是一个有用的工具,用于注释您的工作负载的区域,以便以更细粒度的注释-OP/函数/子函数粒度进行跟踪和可视化。通过在 PyTorch 模型的 OP 级别进行注释,Intel® VTune Profiler 的 ITT 实现了 OP 级别的性能分析。Intel® VTune Profiler 的 ITT 已经集成到PyTorch Autograd Profiler中。¹

  1. 该功能必须通过*with torch.autograd.profiler.emit_itt()*来显式启用。

带有 Intel® Extension for PyTorch*的 TorchServe

Intel® Extension for PyTorch*是一个 Python 包,用于通过在 Intel 硬件上进行额外性能优化来扩展 PyTorch。

Intel® Extension for PyTorch已经集成到 TorchServe 中,以提高性能。² 对于自定义处理程序脚本,我们建议添加intel_extension_for_pytorch*包。

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

在本节中,我们将展示后端绑定通常是未经优化或未调整的深度学习工作负载的主要瓶颈,并通过 Intel® Extension for PyTorch演示优化技术,以改善后端绑定,后端绑定有两个子指标-内存绑定和核心绑定。更高效的内存分配器、操作融合、内存布局格式优化改善了内存绑定。理想情况下,通过优化操作符和更好的缓存局部性,内存绑定可以改善为核心绑定。关键的深度学习基元,如卷积、矩阵乘法、点积,已经通过 Intel® Extension for PyTorch和 oneDNN 库进行了优化,改善了核心绑定。

利用高级启动器配置:内存分配器

从性能的角度来看,内存分配器起着重要作用。更高效的内存使用减少了不必要的内存分配或销毁的开销,从而实现更快的执行。在实践中的深度学习工作负载中,特别是在像 TorchServe 这样的大型多核系统或服务器上运行的工作负载中,TCMalloc 或 JeMalloc 通常比默认的 PyTorch 内存分配器 PTMalloc 具有更好的内存使用。

TCMalloc,JeMalloc,PTMalloc

TCMalloc 和 JeMalloc 都使用线程本地缓存来减少线程同步的开销,并通过使用自旋锁和每个线程的竞技场来减少锁争用。TCMalloc 和 JeMalloc 减少了不必要的内存分配和释放的开销。两个分配器通过大小对内存分配进行分类,以减少内存碎片化的开销。

使用启动器,用户可以通过选择三个启动器旋钮之一来轻松尝试不同的内存分配器 –enable_tcmalloc(TCMalloc)、–enable_jemalloc(JeMalloc)、–use_default_allocator(PTMalloc)。

练习

让我们对比 PTMalloc 和 JeMalloc 进行分析。

我们将使用启动器指定内存分配器,并将工作负载绑定到第一个插槽的物理核心,以避免任何 NUMA 复杂性 – 仅对内存分配器的影响进行分析。

以下示例测量了 ResNet50 的平均推理时间:

import torch
import torchvision.models as models
import time
model = models.resnet50(pretrained=False)
model.eval()
batch_size = 32
data = torch.rand(batch_size, 3, 224, 224)
# warm up
for _ in range(100):
    model(data)
# measure
# Intel® VTune Profiler's ITT context manager
with torch.autograd.profiler.emit_itt():
    start = time.time()
    for i in range(100):
   # Intel® VTune Profiler's ITT to annotate each step
        torch.profiler.itt.range_push('step_{}'.format(i))
        model(data)
        torch.profiler.itt.range_pop()
    end = time.time()
print('Inference took {:.2f} ms in average'.format((end-start)/100*1000)) 

让我们收集一级 TMA 指标。

一级 TMA 显示 PTMalloc 和 JeMalloc 都受后端限制。超过一半的执行时间被后端阻塞。让我们再深入一层。

二级 TMA 显示后端受限是由内存受限引起的。让我们再深入一层。

大多数内存受限指标确定了从 L1 缓存到主存储器的内存层次结构中的瓶颈。在给定级别上受限的热点表明大部分数据是从该缓存或内存级别检索的。优化应该专注于将数据移动到核心附近。三级 TMA 显示 PTMalloc 受 DRAM Bound 限制。另一方面,JeMalloc 受 L1 Bound 限制 – JeMalloc 将数据移动到核心附近,从而实现更快的执行。

让我们看看 Intel® VTune Profiler ITT 跟踪。在示例脚本中,我们已经注释了推理循环的每个 step_x

时间轴图中跟踪了每个步骤。在最后一步(step_99)的模型推理持续时间从 304.308 毫秒减少到 261.843 毫秒。

使用 TorchServe 进行练习

让我们使用 TorchServe 对比 PTMalloc 和 JeMalloc 进行分析。

我们将使用 TorchServe apache-bench 基准测试 进行 ResNet50 FP32、批量大小 32、并发数 32、请求数 8960. 其他所有参数与 默认参数 相同。

与之前的练习一样,我们将使用启动器指定内存分配器,并将工作负载绑定到第一个插槽的物理核心。为此,用户只需在 config.properties 中添加几行即可:

PTMalloc

cpu_launcher_enable=true
cpu_launcher_args=--node_id 0 --use_default_allocator 

JeMalloc

cpu_launcher_enable=true
cpu_launcher_args=--node_id 0 --enable_jemalloc 

让我们收集一级 TMA 指标。

让我们再深入一层。

让我们使用 Intel® VTune Profiler ITT 对 TorchServe 推理范围 进行注释,以便以推理级别的粒度进行分析。由于 TorchServe 架构 包括几个子组件,包括用于处理请求/响应的 Java 前端和用于在模型上运行实际推理的 Python 后端,因此使用 Intel® VTune Profiler ITT 限制在推理级别收集跟踪数据是有帮助的。

每个推断调用都在时间线图中被跟踪。最后一个模型推断的持续时间从 561.688 毫秒减少到 251.287 毫秒 - 加速 2.2 倍。

时间线图可以展开以查看操作级别的性能分析结果。aten::conv2d的持续时间从 16.401 毫秒减少到 6.392 毫秒 - 加速 2.6 倍。

在本节中,我们已经证明 JeMalloc 可以比默认的 PyTorch 内存分配器 PTMalloc 提供更好的性能,有效的线程本地缓存可以改善后端绑定。

Intel® Extension for PyTorch*

三个主要的Intel® Extension for PyTorch*优化技术,运算符、图形、运行时,如下所示:

Intel® Extension for PyTorch* 优化技术
运算符

|

  • 矢量化和多线程
  • 低精度 BF16/INT8 计算
  • 为了更好的缓存局部性进行的数据布局优化

|

  • 常量折叠以减少计算
  • 为了更好的缓存局部性而进行的操作融合

|

  • 线程亲和性
  • 内存缓冲池
  • GPU 运行时
  • 启动器

|

运算符优化

优化的运算符和内核通过 PyTorch 调度机制进行注册。这些运算符和内核从英特尔硬件的本机矢量化特性和矩阵计算特性加速。在执行过程中,Intel® Extension for PyTorch拦截 ATen 运算符的调用,并用这些优化的运算符替换原始运算符。像卷积、线性等流行的运算符已经在 Intel® Extension for PyTorch中进行了优化。

练习

让我们使用 Intel® Extension for PyTorch*对优化的运算符进行性能分析。我们将比较在代码中添加和不添加这些行的变化。

与以前的练习一样,我们将将工作负载绑定到第一个插槽的物理核心。

import torch
class Model(torch.nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.conv = torch.nn.Conv2d(16, 33, 3, stride=2)
        self.relu = torch.nn.ReLU()
    def forward(self, x):
        x = self.conv(x)
        x = self.relu(x)
        return x
model = Model()
model.eval()
data = torch.rand(20, 16, 50, 100)
#################### code changes ####################
import intel_extension_for_pytorch as ipex
model = ipex.optimize(model)
######################################################
print(model) 

该模型由两个操作组成——Conv2d 和 ReLU。通过打印模型对象,我们得到以下输出。

让我们收集一级 TMA 指标。

注意后端绑定从 68.9 减少到 38.5 - 加速 1.8 倍。

此外,让我们使用 PyTorch Profiler 进行性能分析。

注意 CPU 时间从 851 微秒减少到 310 微秒 - 加速 2.7 倍。

图形优化

强烈建议用户利用 Intel® Extension for PyTorchTorchScript进一步优化图形。为了通过 TorchScript 进一步优化性能,Intel® Extension for PyTorch支持常用 FP32/BF16 运算符模式的 oneDNN 融合,如 Conv2D+ReLU、Linear+ReLU 等,以减少运算符/内核调用开销,提高缓存局部性。一些运算符融合允许保持临时计算、数据类型转换、数据布局以提高缓存局部性。此外,对于 INT8,Intel® Extension for PyTorch*具有内置的量化配方,为包括 CNN、NLP 和推荐模型在内的流行 DL 工作负载提供良好的统计精度。量化模型然后通过 oneDNN 融合支持进行优化。

练习

让我们使用 TorchScript 对 FP32 图形优化进行性能分析。

与以前的练习一样,我们将将工作负载绑定到第一个插槽的物理核心。

import torch
class Model(torch.nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.conv = torch.nn.Conv2d(16, 33, 3, stride=2)
        self.relu = torch.nn.ReLU()
    def forward(self, x):
        x = self.conv(x)
        x = self.relu(x)
        return x
model = Model()
model.eval()
data = torch.rand(20, 16, 50, 100)
#################### code changes ####################
import intel_extension_for_pytorch as ipex
model = ipex.optimize(model)
######################################################
# torchscript
with torch.no_grad():
    model = torch.jit.trace(model, data)
    model = torch.jit.freeze(model)

让我们收集一级 TMA 指标。

注意后端绑定从 67.1 减少到 37.5 - 加速 1.8 倍。

此外,让我们使用 PyTorch Profiler 进行性能分析。

请注意,使用 Intel® PyTorch 扩展,Conv + ReLU 操作符被融合,CPU 时间从 803 微秒减少到 248 微秒,加速了 3.2 倍。oneDNN eltwise 后操作使得可以将一个原语与一个逐元素原语融合。这是最流行的融合类型之一:一个 eltwise(通常是激活函数,如 ReLU)与前面的卷积或内积。请查看下一节中显示的 oneDNN 详细日志。

Channels Last 内存格式

在模型上调用ipex.optimize时,Intel® PyTorch 扩展会自动将模型转换为优化的内存格式,即 channels last。Channels last 是一种更适合 Intel 架构的内存格式。与 PyTorch 默认的 channels first NCHW(batch, channels, height, width)内存格式相比,channels last NHWC(batch, height, width, channels)内存格式通常可以加速卷积神经网络,具有更好的缓存局部性。

需要注意的一点是,转换内存格式是昂贵的。因此最好在部署之前将内存格式转换一次,并在部署过程中尽量减少内存格式转换。当数据通过模型的层传播时,最后通道的内存格式会通过连续的支持最后通道的层(例如,Conv2d -> ReLU -> Conv2d)保持不变,转换仅在不支持最后通道的层之间进行。有关更多详细信息,请参阅内存格式传播

练习

让我们演示最后通道优化。

import torch
class Model(torch.nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.conv = torch.nn.Conv2d(16, 33, 3, stride=2)
        self.relu = torch.nn.ReLU()
    def forward(self, x):
        x = self.conv(x)
        x = self.relu(x)
        return x
model = Model()
model.eval()
data = torch.rand(20, 16, 50, 100)
import intel_extension_for_pytorch as ipex
############################### code changes ###############################
ipex.disable_auto_channels_last() # omit this line for channels_last (default)
############################################################################
model = ipex.optimize(model)
with torch.no_grad():
    model = torch.jit.trace(model, data)
    model = torch.jit.freeze(model) 

我们将使用oneDNN 详细模式,这是一个帮助收集有关 oneDNN 图级别信息的工具,例如操作融合、执行 oneDNN 原语所花费的内核执行时间。有关更多信息,请参考oneDNN 文档

以上是来自通道首的 oneDNN 详细信息。我们可以验证从权重和数据进行重新排序,然后进行计算,最后将输出重新排序。

以上是来自通道最后的 oneDNN 详细信息。我们可以验证通道最后的内存格式避免了不必要的重新排序。

使用 Intel® PyTorch 扩展提升性能

以下总结了 TorchServe 与 Intel® Extension for PyTorch*在 ResNet50 和 BERT-base-uncased 上的性能提升。

使用 TorchServe 进行练习

让我们使用 TorchServe 来分析 Intel® Extension for PyTorch*的优化。

我们将使用 ResNet50 FP32 TorchScript 的TorchServe apache-bench 基准测试,批量大小为 32,并发数为 32,请求为 8960。所有其他参数与默认参数相同。

与上一个练习一样,我们将使用启动器将工作负载绑定到第一个插槽的物理核心。为此,用户只需在config.properties中添加几行代码:

cpu_launcher_enable=true
cpu_launcher_args=--node_id 0 

让我们收集一级 TMA 指标。

Level-1 TMA 显示两者都受到后端的限制。正如之前讨论的,大多数未调整的深度学习工作负载将受到后端的限制。注意后端限制从 70.0 降至 54.1。让我们再深入一层。

如前所述,后端绑定有两个子指标 - 内存绑定和核心绑定。内存绑定表示工作负载未经优化或未充分利用,理想情况下,内存绑定操作可以通过优化 OPs 和改善缓存局部性来改善为核心绑定。Level-2 TMA 显示后端绑定从内存绑定改善为核心绑定。让我们深入一层。

在像 TorchServe 这样的模型服务框架上为生产扩展深度学习模型需要高计算利用率。这要求数据通过预取并在执行单元需要执行 uOps 时在缓存中重复使用。Level-3 TMA 显示后端内存绑定从 DRAM 绑定改善为核心绑定。

与 TorchServe 之前的练习一样,让我们使用 Intel® VTune Profiler ITT 来注释TorchServe 推断范围,以便以推断级别的粒度进行分析。

时间轴图中跟踪了每个推断调用。最后一个推断调用的持续时间从 215.731 毫秒减少到 95.634 毫秒 - 2.3 倍加速。

时间轴图可以展开以查看操作级别的分析结果。请注意,Conv + ReLU 已经被融合,持续时间从 6.393 毫秒+1.731 毫秒减少到 3.408 毫秒 - 2.4 倍加速。

结论

在本教程中,我们使用了自顶向下微架构分析(TMA)和 Intel® VTune™ Profiler 的仪器化和跟踪技术(ITT)来演示

  • 通常,未经优化或调整不足的深度学习工作负载的主要瓶颈是后端绑定,它有两个子指标,即内存绑定和核心绑定。
  • Intel® PyTorch*的更高效的内存分配器、操作融合、内存布局格式优化改善了内存绑定。
  • 关键的深度学习基元,如卷积、矩阵乘法、点积等,已经被 Intel® PyTorch*扩展和 oneDNN 库进行了优化,提高了核心绑定。
  • Intel® PyTorch*扩展已经集成到 TorchServe 中,具有易于使用的 API。
  • TorchServe 与 Intel® PyTorch*扩展展示了 ResNet50 的 7.71 倍吞吐量提升,以及 BERT 的 2.20 倍吞吐量提升。

相关阅读

自顶向下微架构分析方法

自顶向下性能分析方法

使用 Intel® PyTorch*扩展加速 PyTorch

致谢

我们要感谢 Ashok Emani(Intel)和 Jiong Gong(Intel)在本教程的许多步骤中提供的巨大指导和支持,以及全面的反馈和审查。我们还要感谢 Hamid Shojanazeri(Meta)和 Li Ning(AWS)在代码审查和教程中提供的有用反馈。

PyTorch 2.2 中文官方教程(十五)(4)https://developer.aliyun.com/article/1482586

相关文章
|
1天前
|
PyTorch 算法框架/工具 异构计算
PyTorch 2.2 中文官方教程(二十)(4)
PyTorch 2.2 中文官方教程(二十)
30 0
PyTorch 2.2 中文官方教程(二十)(4)
|
1天前
|
PyTorch 算法框架/工具 并行计算
PyTorch 2.2 中文官方教程(二十)(3)
PyTorch 2.2 中文官方教程(二十)
47 0
|
1天前
|
Android开发 PyTorch 算法框架/工具
PyTorch 2.2 中文官方教程(二十)(2)
PyTorch 2.2 中文官方教程(二十)
47 0
PyTorch 2.2 中文官方教程(二十)(2)
|
1天前
|
iOS开发 PyTorch 算法框架/工具
PyTorch 2.2 中文官方教程(二十)(1)
PyTorch 2.2 中文官方教程(二十)
50 0
PyTorch 2.2 中文官方教程(二十)(1)
|
1天前
|
PyTorch 算法框架/工具 并行计算
PyTorch 2.2 中文官方教程(十九)(4)
PyTorch 2.2 中文官方教程(十九)
32 0
|
1天前
|
PyTorch 算法框架/工具 异构计算
PyTorch 2.2 中文官方教程(十九)(3)
PyTorch 2.2 中文官方教程(十九)
29 0
PyTorch 2.2 中文官方教程(十九)(3)
|
1天前
|
异构计算 PyTorch 算法框架/工具
PyTorch 2.2 中文官方教程(十九)(2)
PyTorch 2.2 中文官方教程(十九)
57 0
PyTorch 2.2 中文官方教程(十九)(2)
|
1天前
|
PyTorch 算法框架/工具 异构计算
PyTorch 2.2 中文官方教程(十九)(1)
PyTorch 2.2 中文官方教程(十九)
80 1
PyTorch 2.2 中文官方教程(十九)(1)
|
1天前
|
机器学习/深度学习 PyTorch 算法框架/工具
PyTorch 2.2 中文官方教程(十八)(4)
PyTorch 2.2 中文官方教程(十八)
55 1
|
1天前
|
机器学习/深度学习 PyTorch 算法框架/工具
PyTorch 2.2 中文官方教程(十八)(3)
PyTorch 2.2 中文官方教程(十八)
34 1
PyTorch 2.2 中文官方教程(十八)(3)