目录

从第一性原理理解PyTorch Intel CPU性能(第2部分)

创建时间:2022年10月14日 | 最后更新时间:2024年1月16日 | 最后验证时间:未验证

作者: Min Jean Cho, Jing Xu, Mark Saroufim

深入理解 PyTorch Intel CPU 性能从第一性原理开始 教程中,我们介绍了如何调整 CPU 运行时配置、如何对其进行分析,以及如何将其集成到 TorchServe 中以优化 CPU 性能。

在这个教程中,我们将通过 Intel® Extension for PyTorch* Launcher 展示如何通过内存分配器提升性能,并通过 Intel® Extension for PyTorch* 在CPU上使用优化内核,并将其应用于TorchServe,实现ResNet50的7.71倍吞吐量加速和BERT的2.20倍吞吐量加速。

../_images/1.png

预备知识

在整个教程中,我们将使用 自上而下的微架构分析(TMA) 来分析并展示后端绑定(内存绑定、核心绑定)通常是未优化或未调优的深度学习工作负载的主要瓶颈,并通过 Intel® Extension for PyTorch* 演示优化技术以改进后端绑定。我们将使用 toplev,这是基于 Linux perf 构建的 pmu-tools 工具的一部分,用于 TMA。

我们还将使用 Intel® VTune™ 分析器的仪器化和追踪技术 (ITT) 进行更细粒度的分析。

自顶向下的微架构分析方法 (TMA)

在对CPU进行调优以获得最佳性能时,了解瓶颈所在是非常有用的。大多数CPU核心都配备了片上性能监控单元(PMUs)。PMUs是CPU核心中专门的逻辑部件,用于统计系统中发生的特定硬件事件。这些事件的示例可能包括缓存未命中或分支预测错误。PMUs用于自顶向下的微架构分析(TMA),以识别瓶颈。TMA由如所示的层级结构组成:

../_images/26.png

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

后端边界调优

大多数未经调优的深度学习工作负载都将受到后端限制。解决后端限制通常就是解决导致退休过程比必要时间更长的延迟源。如上所示,后端限制有两个子指标——核心限制和内存限制。

内存受限的停滞通常与内存子系统有关。例如,最后一级缓存(LLC 或 L3 缓存)未命中导致访问 DRAM。扩展深度学习模型通常需要大量的计算能力。而高计算利用率要求在执行单元需要数据时数据已经可用。这就需要提前预取数据并在缓存中重用数据,而不是多次从主内存中获取相同的数据,这会导致执行单元在数据返回期间处于空闲状态。在整个本教程中,我们将展示更高效的内存分配器、操作符融合以及内存布局格式优化可以减少内存受限的开销,并提高缓存局部性。

Core Bound stalls 表示在没有未完成的内存访问时,执行单元的使用不理想。例如,连续的多个通用矩阵乘法(GEMM)指令争夺融合乘加(FMA)或点积(DP)执行单元可能会导致 Core Bound stalls。包括 DP 内核在内的关键深度学习内核已通过 oneDNN 库(oneAPI 深度神经网络库)进行了优化,减少了 Core Bound 上的开销。

像GEMM、卷积、反卷积这样的操作计算密集。而像池化、批量归一化以及ReLU等激活函数的操作则受内存限制。

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

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

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

PyTorch* 与 Intel® 扩展 for PyTorch*

Intel® Extension for PyTorch* 是一个 Python 包,用于通过针对 Intel 硬件的优化来增强 PyTorch 的性能。

Intel® PyTorch* 扩展已经集成到 TorchServe 中,以提升开箱即用的性能。 2 对于自定义处理程序脚本,我们建议添加 intel_extension_for_pytorch 包。

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

在整个本节中,我们将展示后端瓶颈(Back End Bound)通常是未优化或未调优的深度学习工作负载的主要瓶颈,并通过 Intel® Extension for PyTorch* 展示优化技术,以改善后端瓶颈。后端瓶颈包含两个子指标——内存瓶颈(Memory Bound)和核心瓶颈(Core Bound)。更高效的内存分配器、算子融合以及内存布局格式优化可以改善内存瓶颈。理想情况下,通过优化算子和更好的缓存局部性,内存瓶颈可以提升到核心瓶颈。而像卷积、矩阵乘法、点积等关键的深度学习原语,已经通过 Intel® Extension for PyTorch* 和 oneDNN 库进行了良好的优化,从而改善核心瓶颈。

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

内存分配器从性能角度来看起着重要作用。更高效的内存使用可以减少不必要的内存分配或销毁操作所带来的开销,从而加快执行速度。在实际的深度学习工作负载中,尤其是运行在大型多核系统或服务器(如 TorchServe)上的场景中,TCMalloc 或 JeMalloc 通常比默认的 PyTorch 内存分配器 PTMalloc 具有更好的内存使用效果。

TCMalloc, JeMalloc, PTMalloc

TCMalloc 和 JeMalloc 都使用线程本地缓存来减少线程同步的开销,并分别通过自旋锁和每线程 arena 来降低锁竞争。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指标。

../_images/32.png

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

../_images/41.png

Level-2 TMA 显示后端瓶颈是由内存瓶颈引起的。让我们再深入一层。

../_images/51.png

内存受限(Memory Bound)下的大部分指标可以识别从L1缓存到主内存的内存层次结构中哪个层级是瓶颈。如果热点被限制在某一特定层级,表明大部分数据是从该缓存或内存层级获取的。优化应着重于将数据更靠近核心。三级TMA显示PTMalloc受到DRAM受限的影响。另一方面,JeMalloc受到L1受限的影响——JeMalloc将数据更靠近核心,因此执行速度更快。

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

../_images/61.png

每个步骤在时间线图中被追踪。模型在最后一步(step_99)的推理持续时间从 304.308 ms 减少到 261.843 ms。

使用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指标。

../_images/71.png

让我们深入一层。

../_images/81.png

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

../_images/9.png

每个推理调用都会在时间线图中被追踪。最后一次模型推理的持续时间从 561.688 ms 减少到 251.287 ms - 速度提升了 2.2 倍。

../_images/101.png

时间线图可以展开以查看操作级别的分析结果。aten::conv2d 的持续时间从 16.401 ms 减少到 6.392 ms - 速度提升了 2.6 倍。

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

Intel® PyTorch* 扩展

PyTorch* 的三个主要 Intel® 扩展优化技术,操作符、图、运行时,如下所示:

Intel® Extension for PyTorch* 优化技巧

操作符

运行时

  • 向量化和多线程

  • 低精度 BF16/INT8 计算

  • 数据布局优化以提高缓存局部性

  • 常量折叠以减少计算

  • 操作融合以提高缓存局部性

  • 线程亲和性

  • 内存缓冲池

  • GPU 运行时

  • 启动器

算子优化

优化的算子和内核是通过 PyTorch 分发机制进行注册的。这些算子和内核利用了 Intel 硬件原生向量化功能和矩阵计算功能进行加速。在执行过程中,Intel® Extension for PyTorch* 会拦截 ATen 算子的调用,并将原始算子替换为这些优化后的算子。像卷积(Convolution)、线性层(Linear)这样的常用算子已经在 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。通过打印模型对象,我们得到以下输出。

../_images/11.png

让我们收集一级TMA指标。

../_images/121.png

请注意后端限制从 68.9 减少到 38.5 – 1.8 倍加速。

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

../_images/131.png

请注意 CPU 时间从 851 微秒减少到 310 微秒 – 速度提升了 2.7 倍。

图优化

强烈建议用户利用Intel® Extension for PyTorch* 和 TorchScript 进行进一步的图优化。为了通过TorchScript进一步优化性能,Intel® Extension for PyTorch* 支持常用FP32/BF16操作符模式(如Conv2D+ReLU、Linear+ReLU等)的oneDNN融合,以减少操作符/内核调用开销,并提高缓存局部性。一些操作符融合允许维护临时计算、数据类型转换和数据布局,以获得更好的缓存局部性。对于INT8,Intel® Extension for PyTorch* 提供内置量化方案,为包括CNN、NLP和推荐模型在内的流行深度学习工作负载提供良好的统计精度。然后使用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指标。

../_images/141.png

请注意后端限制从 67.1 减少到 37.5 – 速度提升了 1.8 倍。

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

../_images/151.png

请注意,使用 Intel® Extension for PyTorch* 时,Conv + ReLU 操作符被融合,CPU 时间从 803 us 减少到 248 us – 速度提升了 3.2 倍。oneDNN eltwise post-op 支持将一个 primitive 与一个 elementwise primitive 进行融合。这是最常见的一种融合类型:一个 eltwise(通常是激活函数,如 ReLU)与前面的卷积或内积操作进行融合。请查看下一节中显示的 oneDNN 详细日志。

通道最后内存格式

当在模型上调用 ipex.optimize 时,Intel® Extension for PyTorch* 会自动将模型转换为优化后的内存格式,通道最后。通道最后是一种对 Intel 架构更友好的内存格式。与 PyTorch 默认的通道优先 NCHW (batch, channels, height, width) 内存格式相比,通道最后 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 verbose mode,这是一个用于收集 oneDNN 图级别信息的工具,例如算子融合、内核执行时间等。如需更多信息,请参阅 oneDNN 文档

../_images/161.png
../_images/171.png

上面是 channels first 的 oneDNN 详细输出。我们可以验证,权重和数据都进行了重新排序,然后进行计算,最后将输出重新排序回来。

../_images/181.png

上面是 channels last 的 oneDNN 详细输出。我们可以验证 channels last 内存格式避免了不必要的重新排序。

使用Intel® PyTorch*扩展提升性能

以下总结了使用 Intel® Extension for PyTorch* 对 TorchServe 性能提升的情况,适用于 ResNet50 和 BERT-base-uncased。

../_images/191.png

使用TorchServe练习

让我们使用 TorchServe 对 Intel® Extension for PyTorch* 的优化进行性能分析。

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

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

cpu_launcher_enable=true
cpu_launcher_args=--node_id 0

让我们收集一级TMA指标。

../_images/20.png

Level-1 TMA 显示两者均受后端限制。如前所述,大多数未经调优的深度学习工作负载都将受到后端限制。请注意后端限制从 70.0 降低到了 54.1。让我们深入一层。

../_images/211.png

如前所述,后端瓶颈(Back End Bound)有两个子指标——内存瓶颈(Memory Bound)和核心瓶颈(Core Bound)。内存瓶颈表示工作负载未被优化或未被充分利用,理想情况下,通过优化操作(OPs)和改善缓存局部性,内存瓶颈的操作可以提升为核心瓶颈。二级TMA显示,后端瓶颈已从内存瓶颈改善为核心瓶颈。让我们更深入一层。

../_images/221.png

在使用像TorchServe这样的模型服务框架将深度学习模型部署到生产环境时,需要高计算利用率。这要求数据通过预取方式可用,并且当执行单元需要数据来执行uOps时,能够重用缓存中的数据。三级TMA显示,后端内存受限情况已从DRAM受限改善为核心受限。

如同之前的TorchServe练习,让我们使用Intel® VTune Profiler ITT来注释TorchServe推理范围,以便在推理级别粒度上进行分析。

../_images/231.png

每个推理调用都会在时间线图中被追踪。最后一次推理调用的持续时间从 215.731 毫秒减少到 95.634 毫秒 - 速度提升了 2.3 倍。

../_images/241.png

时间线图可以展开以查看操作级别的分析结果。请注意,Conv + ReLU 已被融合,持续时间从 6.393 ms + 1.731 ms 减少到 3.408 ms,速度提升了 2.4 倍。

结论

在本教程中,我们使用了自顶向下的微架构分析(TMA)和 Intel® VTune™ Profiler 的指令插入与跟踪技术(ITT)来演示以下内容

  • 通常,未优化或未调优的深度学习工作负载的主要瓶颈是后端受限(Back End Bound),它包含两个子指标:内存受限(Memory Bound)和核心受限(Core Bound)。

  • 一种更高效的内存分配器、算子融合以及内存布局格式优化,由 Intel® Extension for PyTorch* 提供,可提升内存受限性能。

  • 关键的深度学习原语,如卷积、矩阵乘法、点积等,已通过 Intel® Extension for PyTorch* 和 oneDNN 库进行了良好的优化,从而提升核心性能。

  • Intel® Extension for PyTorch* 已经通过易于使用的 API 集成到 TorchServe 中。

  • TorchServe with Intel® Extension for PyTorch* 在 ResNet50 上实现了 7.71 倍的吞吐量加速,在 BERT 上实现了 2.20 倍的吞吐量加速。

致谢

我们感谢 Ashok Emani (Intel) 和 Jiong Gong (Intel) 在本教程的多个步骤中提供的巨大指导和支持,以及详尽的反馈和审阅。我们也要感谢 Hamid Shojanazeri (Meta) 和 Li Ning (AWS) 在代码审查和教程过程中提供的有帮助的反馈。

文档

访问 PyTorch 的全面开发人员文档

查看文档

教程

获取面向初学者和高级开发人员的深入教程

查看教程

资源

查找开发资源并解答您的问题

查看资源