从第一原理掌握 PyTorch Intel CPU 性能(第二部分)¶
作者:Min Jean Cho、Jing Xu、Mark Saroufim
在 从第一原理掌握 PyTorch Intel CPU 性能 教程中,我们介绍了如何调整 CPU 运行时配置、如何分析它们以及如何将它们集成到 TorchServe 中以优化 CPU 性能。
在本教程中,我们将演示如何通过 Intel® Extension for PyTorch* 启动器 使用内存分配器来提高性能,并通过 Intel® Extension for PyTorch* 在 CPU 上优化内核,并将它们应用到 TorchServe,展示了 ResNet50 的 7.71 倍吞吐量加速和 BERT 的 2.20 倍吞吐量加速。
先决条件¶
在本教程中,我们将使用 自上而下的微体系结构分析 (TMA) 来分析并显示后端绑定(内存绑定、核心绑定)通常是未优化或未调整的深度学习工作负载的主要瓶颈,并演示通过 Intel® Extension for PyTorch* 提高后端绑定的优化技术。我们将使用 toplev,这是 pmu-tools 的一部分,它基于 Linux perf 构建,用于 TMA。
我们还将使用Intel® VTune™ Profiler 的 Instrumentation and Tracing Technology (ITT) 进行更细粒度的分析。
自顶向下的微架构分析方法 (TMA)¶
在调整 CPU 以获得最佳性能时,了解瓶颈所在非常有用。大多数 CPU 内核都有片上性能监控单元 (PMU)。PMU 是 CPU 内核中专用的逻辑部分,用于在系统发生时统计特定的硬件事件。这些事件的示例可能是缓存未命中或分支预测错误。PMU 用于自顶向下的微架构分析 (TMA) 来识别瓶颈。TMA 由如所示的层次结构组成
顶层,level-1,指标收集退休、错误推测、前端绑定、后端绑定。CPU 的流水线从概念上可以简化并分为两个部分:前端和后端。前端负责获取程序代码并将它们解码成称为微操作 (uOp) 的低级硬件操作。然后 uOp 被输入到后端,这个过程称为分配。分配后,后端负责在可用的执行单元中执行 uOp。uOp 执行完成称为退休。相反,错误推测是指在退休之前取消推测性获取的 uOp,例如在分支预测错误的情况下。每个指标都可以进一步分解到后续级别中,以确定瓶颈。
针对后端绑定进行调整¶
大多数未调优的深度学习工作负载将是后端绑定。解决后端绑定通常意味着解决导致退休时间过长的延迟来源。如上所示,后端绑定有两个子指标——核心绑定和内存绑定。
内存绑定停滞的原因与内存子系统相关。例如,最后一级缓存 (LLC 或 L3 缓存) 未命中导致访问 DRAM。扩展深度学习模型通常需要大量的计算。而高计算利用率要求数据在执行单元需要时可用,以便执行 uOp。这需要预取数据并在缓存中重复使用数据,而不是从主内存中多次获取相同数据,这会导致执行单元在返回数据时处于饥饿状态。在本教程中,我们将展示更有效的内存分配器、操作符融合、内存布局格式优化如何通过更好的缓存局部性来减少内存绑定的开销。
核心绑定停滞表明在没有未完成的内存访问的情况下,对可用执行单元的使用效率低下。例如,一行中的多个通用矩阵乘法 (GEMM) 指令争用融合乘加 (FMA) 或点积 (DP) 执行单元可能会导致核心绑定停滞。oneDNN 库 (oneAPI 深度神经网络库) 已针对关键深度学习内核(包括 DP 内核)进行了优化,减少了核心绑定的开销。
像 GEMM、卷积、反卷积这样的操作是计算密集型的。而像池化、批归一化、ReLU 这样的激活函数是内存绑定的。
Intel® VTune™ Profiler 的 Instrumentation and Tracing Technology (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()显式启用此功能。
TorchServe 与 Intel® Extension for PyTorch*¶
Intel® Extension for PyTorch* 是一个 Python 包,用于扩展 PyTorch,并为 Intel 硬件上的额外性能提升提供优化。
Intel® Extension for PyTorch* 已经集成到 TorchServe 中,以提高开箱即用的性能。2 对于自定义处理程序脚本,我们建议添加intel_extension_for_pytorch 包。
必须通过在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))
让我们收集 level-1 TMA 指标。
Level-1 TMA 显示 PTMalloc 和 JeMalloc 都受后端限制。超过一半的执行时间被后端停滞。让我们深入一层。
Level-2 TMA 显示后端绑定是由内存绑定引起的。让我们深入一层。
内存绑定下的大多数指标都标识了从 L1 缓存到主内存的内存层次结构中的哪个级别是瓶颈。在给定级别上发生的热点表明大多数数据正在从该缓存或内存级别检索。优化应侧重于将数据移近核心。Level-3 TMA 显示 PTMalloc 受 DRAM 绑定限制。另一方面,JeMalloc 受 L1 绑定限制——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
让我们收集 level-1 TMA 指标。
让我们深入一层。
让我们使用 Intel® VTune Profiler ITT 来注释TorchServe 推断范围,以便以推断级粒度进行分析。由于TorchServe 架构由多个子组件组成,包括用于处理请求/响应的 Java 前端和用于在模型上运行实际推断的 Python 后端,因此使用 Intel® VTune Profiler ITT 将跟踪数据收集限制在推断级别非常有用。
每个推断调用都在时间轴图中进行跟踪。最后一个模型推断的持续时间从 561.688 毫秒减少到 251.287 毫秒——提高了 2.2 倍的速度。
可以扩展时间轴图以查看 OP 级别的分析结果。aten::conv2d 的持续时间从 16.401 毫秒减少到 6.392 毫秒——提高了 2.6 倍的速度。
在本节中,我们已经证明 JeMalloc 比默认的 PyTorch 内存分配器 PTMalloc 具有更好的性能,其中有效的线程本地缓存改进了后端绑定。
Intel® Extension for PyTorch*¶
三种主要的Intel® Extension for PyTorch* 优化技术,操作符、图、运行时,如所示
Intel® Extension for PyTorch* 优化技术 |
||
---|---|---|
操作符 |
图 |
运行时 |
|
|
|
操作符优化¶
优化后的操作符和内核是通过 PyTorch 分发机制注册的。这些操作符和内核是通过 Intel 硬件的本机矢量化功能和矩阵计算功能加速的。在执行期间,Intel® Extension for PyTorch* 会拦截 ATen 操作符的调用,并将原始操作符替换为这些优化后的操作符。Intel® Extension for PyTorch* 中已针对 Convolution、Linear 等流行操作符进行了优化。
练习¶
让我们使用 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。通过打印模型对象,我们得到以下输出。
让我们收集 level-1 TMA 指标。
注意,后端绑定从 68.9 减少到 38.5——提高了 1.8 倍的速度。
此外,让我们使用 PyTorch Profiler 进行分析。
注意,CPU 时间从 851 微秒减少到 310 微秒——提高了 2.7 倍的速度。
图优化¶
强烈建议用户利用 Intel® Extension for PyTorch* 与 TorchScript 进行进一步的图优化。为了进一步优化 TorchScript 的性能,Intel® Extension for PyTorch* 支持 oneDNN 融合常用的 FP32/BF16 算子模式,例如 Conv2D+ReLU、Linear+ReLU 等,以减少算子/内核调用开销,并提高缓存局部性。一些算子融合可以保留临时计算、数据类型转换和数据布局,从而提高缓存局部性。对于 INT8,Intel® Extension for PyTorch* 具有内置量化配方,可为流行的 DL 工作负载(包括 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)
让我们收集 level-1 TMA 指标。
注意,后端边界从 67.1 减少到 37.5,速度提高了 1.8 倍。
此外,让我们使用 PyTorch Profiler 进行分析。
注意,使用 Intel® Extension for PyTorch*,Conv + ReLU 算子已融合,CPU 时间从 803 us 减少到 248 us,速度提高了 3.2 倍。oneDNN eltwise 后操作允许将一个原语与一个逐元素原语融合在一起。这是最流行的融合类型之一:一个 eltwise(通常是激活函数,例如 ReLU)与前面的卷积或内积相结合。请查看下一节中显示的 oneDNN 详细日志。
通道最后内存格式¶
当在模型上调用 ipex.optimize 时,Intel® Extension for PyTorch* 会自动将模型转换为优化的内存格式,即通道最后。通道最后是一种对 Intel 架构更友好的内存格式。与 PyTorch 默认的通道优先 NCHW(批次、通道、高度、宽度)内存格式相比,通道最后的 NHWC(批次、高度、宽度、通道)内存格式通常通过更好的缓存局部性来加速卷积神经网络。
需要注意的是,转换内存格式的成本很高。因此,最好在部署前转换一次内存格式,并在部署期间将内存格式转换次数降至最低。随着数据通过模型的各层传播,通道最后的内存格式将通过连续的通道最后支持的层(例如,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® Extension for PyTorch* 提升性能¶
以下总结了 TorchServe 使用 Intel® Extension for PyTorch* 对 ResNet50 和 BERT-base-uncased 的性能提升。
使用 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
让我们收集 level-1 TMA 指标。
一级 TMA 显示两者都受后端限制。如前所述,大多数未调优的深度学习工作负载将是后端限制的。注意,后端边界从 70.0 减少到 54.1。让我们更深入地了解一下。
如前所述,后端边界有两个子指标:内存边界和核心边界。内存边界表明工作负载优化不足或利用不足,理想情况下,可以通过优化操作并改善缓存局部性来将内存边界操作改进为核心边界。二级 TMA 显示,后端边界从内存边界改进为核心边界。让我们更深入地了解一下。
在模型服务框架(如 TorchServe)上扩展生产环境中的深度学习模型需要高计算利用率。这要求数据可通过预取获取,并在执行单元需要执行 uOps 时在缓存中重用数据。三级 TMA 显示,后端内存边界从 DRAM 边界改进为核心边界。
与之前的使用 TorchServe 的练习一样,让我们使用 Intel® VTune 性能分析器 ITT 来注释 TorchServe 推理范围,以便在推理级别粒度进行性能分析。
每个推理调用都在时间线图中进行跟踪。最后一个推理调用的持续时间从 215.731 毫秒减少到 95.634 毫秒,速度提高了 2.3 倍。
时间线图可以扩展以查看操作级别的性能分析结果。注意,Conv + ReLU 已融合,持续时间从 6.393 毫秒 + 1.731 毫秒减少到 3.408 毫秒,速度提高了 2.4 倍。
结论¶
在本教程中,我们使用了自顶向下的微体系结构分析 (TMA) 和 Intel® VTune™ 性能分析器的仪器和跟踪技术 (ITT) 来证明
通常,未优化或未调优的深度学习工作负载的主要瓶颈是后端边界,它有两个子指标:内存边界和核心边界。
Intel® Extension for PyTorch* 更有效的内存分配器、算子融合、内存布局格式优化改善了内存边界。
Intel® Extension for PyTorch* 和 oneDNN 库已经很好地优化了关键深度学习原语,例如卷积、矩阵乘法、点积等,从而改善了核心边界。
Intel® Extension for PyTorch* 已通过易于使用的 API 集成到 TorchServe 中。
使用 Intel® Extension for PyTorch* 的 TorchServe 显示,ResNet50 的吞吐量速度提高了 7.71 倍,BERT 的吞吐量速度提高了 2.20 倍。
致谢¶
我们要感谢 Ashok Emani(英特尔)和 Jiong Gong(英特尔)在整个教程的许多步骤中为我们提供的巨大指导和支持,以及他们对我们的详细反馈和审查。我们还要感谢 Hamid Shojanazeri(Meta)和 Li Ning(AWS)在代码审查和教程中提供的宝贵反馈。