从基本原理理解 PyTorch Intel CPU 性能(第 2 部分)¶
Created On: Oct 14, 2022 | Last Updated: Jan 16, 2024 | Last Verified: Not Verified
作者: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) 进行性能分析并展示后端瓶颈(Memory Bound,Core Bound)通常是未优化或未调优的深度学习工作负载的主要瓶颈,并演示通过 Intel® Extension for PyTorch* 优化后端瓶颈的技术。我们将使用 toplev <https://github.com/andikleen/pmu-tools/wiki/toplev-manual>`_(`pmu-tools 中的工具)以及基于 Linux perf 创建的 TMA 工具。
我们还将使用 Intel® VTune™ 分析器的仪器化和追踪技术 (ITT) 进行更细粒度的性能分析。
自上而下微架构分析方法 (TMA)¶
在调优 CPU 以获得最佳性能时,了解瓶颈位置非常有用。大多数 CPU 核心都具有芯片上的性能监测单元 (PMU)。PMU 是 CPU 核心内专门的逻辑在系统发生硬件事件时进行计数。示例事件包括缓存未命中或分支预测错误。PMU 用于自上而下微架构分析 (TMA) 以识别瓶颈。TMA 包括分层级别,如所示:
第一级指标收集 退休操作 (Retiring)、错误推测 (Bad Speculation)、前端瓶颈 (Front End Bound) 和 后端瓶颈 (Back End Bound)。CPU 的管道可以从概念上简化为两部分:前端和后端。前端 负责获取程序代码并将其解码为低级硬件操作(称为微操作或者 uOps)。然后将 uOps 在一个分配过程被发送到 后端。分配后,后端负责在可用执行单元执行 uOp。完成 uOp 的执行称为 退休 (Retirement)。而 错误推测 是指在退休前取消预测性获取的 uOps。例如分支预测错误的情况。每个指标可以进一步分解到后续级别以找到瓶颈的位置。
针对后端瓶颈调优¶
大多数未调优的深度学习工作负载都会受到后端瓶颈的影响。解决后端瓶颈通常意味着解决导致退休操作比必要时间更长的延迟原因。如上所述,后端瓶颈有两个子指标—核心瓶颈 (Core Bound) 和内存瓶颈 (Memory Bound)。
内存瓶颈的停滞通常与内存子系统有关。例如,末级缓存 (LLC 或 L3 缓存) 未命中导致访问 DRAM。扩展深度学习模型通常需要显著的计算能力。而高计算利用率要求在执行单元需要执行 uOps 时数据是可用的。这需要提前获取数据并重复使用缓存中的数据,而不是从主内存多次获取相同的数据,这会导致数据在返回时执行单元被饿死。整个教程中,我们将展示更高效的内存分配器、操作符融合和内存布局格式优化,如何通过缓存局部性减少内存瓶颈的开销。
核心瓶颈指示在没有未完成的内存访问时对可用执行单元的使用不优化。例如,一组连续的通用矩阵乘法 (GEMM) 指令竞争融合乘加 (FMA) 或点积 (DP) 执行单元可能会导致核心瓶颈。关键的深度学习内核,包括 DP 内核,已通过 `oneDNN 库 <https://github.com/oneapi-src/oneDNN>`_(oneAPI 深度神经网络库)进行了优化,从而减少核心瓶颈的开销。
诸如 GEMM、卷积、反卷积等操作是计算密集型的。而诸如池化、批归一化、ReLU 等激活函数则是内存密集型的。
Intel® VTune™ 分析器的仪器化和追踪技术 (ITT)¶
Intel® VTune 分析器的 ITT API 是一个有用的工具,可以标注工作负载中的区域,进行追踪以在更细粒度的标注级别—操作 (OP)/函数/子函数粒度进行可视化和分析。通过在 PyTorch 模型操作级别进行标注,Intel® VTune 分析器的 ITT 支持操作级分析。Intel® VTune 分析器的 ITT 已集成到 PyTorch Autograd 分析器 中。 1
该功能需要显式启用,通过 with torch.autograd.profiler.emit_itt()。
结合 Intel® Extension for PyTorch* 使用 TorchServe¶
Intel® Extension for PyTorch* 是一个扩展 PyTorch 的 Python 包,针对 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)上的任务,与默认 PyTorch 内存分配器 PTMalloc 相比,TCMalloc 或 JeMalloc 通常能够获得更好的内存使用效果。
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 限制而成为瓶颈。而另一方面,JeMalloc 由于 L1 限制而成为瓶颈——JeMalloc 将数据移至核心更近,因此执行更快。
让我们看看 Intel® VTune Profiler ITT 跟踪。在示例脚本中,我们为推理循环的每个 step_x 进行了注释。
每一步都在时间线图中被记录。最后一步(step_99)中模型推理的持续时间从 304.308 毫秒减少到 261.843 毫秒。
与 TorchServe 的练习¶
让我们对 PTMalloc 和 JeMalloc 与 TorchServe 进行性能分析。
我们将使用 ResNet50 FP32、批量大小 32、并发性 32、请求数 8960 的 TorchServe apache-bench 基准测试。所有其他参数与 默认参数 相同。
在前面的练习中,我们将使用启动器指定内存分配器,并将工作负载绑定至第一个插槽的物理核心。为此,用户只需在 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® PyTorch 扩展*¶
Intel® PyTorch 扩展* 的三个主要 优化技术,运算符、图、运行时,如下:
Intel® PyTorch 扩展*优化技术 |
||
---|---|---|
运算符 |
图 |
运行时 |
|
|
|
运算符优化¶
优化的运算符和内核通过 PyTorch 的分派机制注册。这些运算符和内核通过 Intel 硬件的本地向量化功能和矩阵计算功能进行了加速。在执行期间,Intel® PyTorch 扩展*拦截 ATen 运算符的调用,并用这些优化的运算符替代原始运算符。Intel® PyTorch 扩展* 已对卷积、线性等流行运算符进行了优化。
练习¶
让我们使用 Intel® 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® PyTorch 扩展*与 TorchScript 进行进一步图优化。为了进一步优化性能,Intel® PyTorch 扩展*支持一DNN融合常用的 FP32/BF16 运算符模式,如 Conv2D+ReLU、Linear+ReLU 等,以减少运算符/内核调用开销,并改善缓存局部性。一些运算符融合允许保持临时计算、数据类型转换、数据布局以改善缓存局部性。对于 INT8,Intel® PyTorch 扩展*还具有内置量化方案,为流行的深度学习工作负载(包括 CNN、NLP 和推荐模型)提供良好的统计准确性。量化模型随后利用一DNN融合支持进行优化。
练习¶
让我们为 FP32 图优化进行 TorchScript 性能分析。
如前面的练习中,我们将工作负载绑定至第一个插槽的物理核心。
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 倍。一DNN eltwise 后续操作支持将一个基元与元素操作基元融合。这是最流行的融合类型之一:一个元素操作(通常是一个激活函数,例如 ReLU)与前面的卷积或内积结合。请查看下一节中的一DNN详细日志。
频道最后内存格式¶
调用 ipex.optimize 模型时,Intel® 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)
我们将使用 一DNN详细模式,它是一个工具,可帮助收集一DNN图层级信息,例如运算符融合、内核执行时间等。更多信息,请参考 一DNN文档。
上述是来自频道优先的一DNN详细信息。我们可以验证从权重和数据中进行重新排序,然后进行计算,最后将输出重新排序回去。
上述是来自频道最后的一DNN详细信息。我们可以验证频道最后内存格式避免了不必要的重新排序。
使用 Intel® PyTorch 扩展*的性能提升¶
以下总结了 TorchServe 使用 Intel® PyTorch 扩展*为 ResNet50 和 BERT-base-uncased的性能提升。
与 TorchServe 的练习¶
让我们为 TorchServe 使用 Intel® PyTorch 扩展*优化进行性能分析。
我们将使用 ResNet50 FP32 TorchScript、批量大小 32、并发性 32、请求数 8960 的 TorchServe apache-bench 基准测试。所有其他参数与 默认参数 相同。
如前面的练习中,我们将使用启动器将工作负载绑定至第一个插槽的物理核心。为此,用户只需在 config.properties 中添加几行代码即可:
cpu_launcher_enable=true
cpu_launcher_args=--node_id 0
让我们收集一级 TMA 指标。
一级 TMA 显示两者都受到后端的限制。如前所述,未经优化的深度学习工作负载中的大多数往往会受到后端限制。注意后端限制从 70.0 减少到 54.1。让我们深入一级。
如前所述,后端限制有两个子指标——内存限制和核心限制。内存限制指示工作负载未优化或未充分利用,理想情况下可以通过优化操作和改善缓存局部性将内存限制的操作改进为核心限制的操作。二级 TMA 显示后端限制从内存限制改进为核心限制。让我们深入一级。
将深度学习模型扩展到像 TorchServe 这样的模型服务框架用于生产需要高计算利用率。这需要数据通过预取在执行单元需要时可用,并在缓存中重复使用数据以执行 uOps。三级 TMA 显示后端内存限制从 DRAM 限制改进至核心限制。
如使用 TorchServe 的前一练习中,让我们使用 Intel® VTune Profiler ITT 标注 TorchServe 推理范围,以便以推理级别的粒度进行性能分析。
每个推理调用都在时间线图中被记录。最后一次推理调用的持续时间从 215.731 毫秒减少到 95.634 毫秒——加速了 2.3 倍。
时间线图可以展开查看操作级性能分析结果。注意到卷积加ReLU已经被融合,持续时间从6.393毫秒加1.731毫秒减少到3.408毫秒 - 提升了2.4倍性能。
结论¶
在本教程中,我们使用了顶层微架构分析 (TMA) 和 Intel® VTune™ Profiler 的仪器和跟踪技术 (ITT) 来演示:
通常,未优化或未调优的深度学习工作负载的主要瓶颈是后端受限,其中包括两个子指标:内存受限和核心受限。
通过 Intel® PyTorch* 扩展提供更高效的内存分配器、操作融合和内存布局格式优化,可改善内存受限问题。
Intel® PyTorch* 扩展和 oneDNN 库已经对关键深度学习原语,如卷积、矩阵乘法、点积等进行了优化,提升了核心受限问题。
Intel® PyTorch* 扩展已整合到 TorchServe 中,并提供了易于使用的 API。
使用 Intel® PyTorch* 扩展的 TorchServe 对 ResNet50 提供了 7.71 倍吞吐量加速,对 BERT 提供了 2.20 倍吞吐量加速。
致谢¶
我们感谢 Ashok Emani (Intel) 和 Jiong Gong (Intel) 在本教程的多个阶段提供了巨大的指导和支持,以及全面的反馈和审阅。同时感谢 Hamid Shojanazeri (Meta) 和 Li Ning (AWS) 在代码评审和教程中的有益反馈。