从第一性原理理解 PyTorch 在 Intel CPU 上的性能 (第二部分)
作者: Min Jean Cho, Jing Xu, Mark Saroufim
在 Grokking PyTorch Intel CPU Performance From First Principles 教程中, 我们介绍了如何调整 CPU 运行时配置, 如何分析它们, 以及如何将它们集成到 TorchServe 中以优化 CPU 性能。
在本教程中, 我们将展示如何通过 Intel® Extension for PyTorch* Launcher 利用内存分配器提升性能, 以及通过 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 的仪器和追踪技术 (ITT) 来进行更细粒度的性能分析。
自上而下的微架构分析方法 (TMA)
在调优 CPU 以达到最佳性能时,了解瓶颈所在非常有用。大多数 CPU 核心都集成了性能监控单元 (PMUs)。PMUs 是 CPU 核心内的专用逻辑单元,用于在系统运行时计数特定的硬件事件。这些事件的例子可能包括缓存未命中或分支预测错误。PMUs 被用于自顶向下微架构分析 (TMA) 以识别瓶颈。TMA 包含如下所示的层级结构:
顶层的 level-1 指标收集了 Retiring(退休)、Bad Speculation(错误推测)、Front End Bound(前端瓶颈)和 Back End Bound(后端瓶颈)。CPU 的流水线在概念上可以简化为两个部分:前端和后端。前端 负责获取程序代码并将其解码为称为微操作(uOps)的低级硬件操作。然后,这些 uOps 通过一个称为分配(allocation)的过程被送入 后端。一旦分配完成,后端负责在可用的执行单元中执行这些 uOp。uOp 执行完成的过程称为 retirement(退休)。相反,bad speculation(错误推测)是指在退休之前取消推测性获取的 uOps,例如在分支预测错误的情况下。这些指标中的每一个都可以在后续的层级中进一步细分,以精确定位瓶颈所在。
针对后端瓶颈进行调优
大多数未经调优的深度学习工作负载都会受到后端瓶颈的影响。解决后端瓶颈通常意味着消除导致指令退休时间过长的一些延迟源。如上所述,后端瓶颈有两个子指标——核心瓶颈和内存瓶颈。
内存瓶颈的停滞与内存子系统相关。例如,末级缓存(LLC 或 L3 缓存)未命中导致访问 DRAM。扩展深度学习模型通常需要大量的计算资源,而高计算利用率要求在执行单元需要执行微操作时数据能够及时可用。这需要预取数据并在缓存中重复使用数据,而不是多次从主内存中获取相同的数据,否则会导致执行单元在等待数据返回时处于饥饿状态。在本教程中,我们将展示更高效的内存分配器、算子融合、内存布局格式优化如何通过更好的缓存局部性来减少内存瓶颈的开销。
核心资源瓶颈(Core Bound stalls)表明在没有未完成的内存访问时,可用执行单元的使用并不理想。例如,连续多个通用矩阵乘法(GEMM)指令争用融合乘法加法(FMA)或点积(DP)执行单元,可能导致核心资源瓶颈。关键深度学习内核,包括DP内核,已经通过oneDNN库(oneAPI深度神经网络库)进行了充分优化,从而减少了核心资源瓶颈的开销。
像GEMM、卷积、反卷积这样的操作是计算密集型的。而像池化、批归一化、以及类似ReLU的激活函数这样的操作则是内存密集型的。
Intel® VTune™ Profiler 的插桩与追踪技术 (ITT)
Intel® VTune Profiler 的 ITT API 是一个有用的工具,可以为您的工作负载的某个区域添加注释,以便在更细粒度的注释级别(操作/函数/子函数粒度)进行跟踪、分析和可视化。通过在 PyTorch 模型的操作粒度上添加注释,Intel® VTune Profiler 的 ITT 实现了操作级别的性能分析。Intel® VTune Profiler 的 ITT 已集成到 PyTorch Autograd Profiler 中。 1
- 该功能必须通过 with torch.autograd.profiler.emit_itt() 显式启用。
使用 Intel® Extension for PyTorch* 的 TorchServe
Intel® Extension for PyTorch* 是一个 Python 包,用于扩展 PyTorch,通过在 Intel 硬件上进行优化以进一步提升性能。
Intel® Extension for PyTorch* 已经集成到 TorchServe 中,以开箱即用地提升性能。对于自定义处理脚本,我们建议添加 intel_extension_for_pytorch 包。
- 该功能需要通过将 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 的平均推理时间:
importtorch
importtorchvision.modelsasmodels
importtime
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都受到后端的限制。超过一半的执行时间是由后端阻塞的。让我们深入一层分析。
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
让我们收集一级 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® Extension for PyTorch* 的三大优化技术,算子(Operator)、图(Graph)、运行时(Runtime),如下所示:
| Intel® Extension for PyTorch* 优化技术 | | --- | --- | --- | | 算子 | 图 | 运行时 | | * 向量化和多线程 * 低精度 BF16/INT8 计算 * 数据布局优化以提升缓存局部性 | * 常量折叠以减少计算 * 算子融合以提升缓存局部性 | * 线程亲和性 * 内存缓冲池 * GPU 运行时 * 启动器 |
算子优化
优化的算子和内核通过 PyTorch 的调度机制进行注册。这些算子和内核受益于 Intel 硬件的原生向量化功能和矩阵计算功能,从而获得加速。在执行过程中,Intel® Extension for PyTorch* 会拦截 ATen 算子的调用,并将其替换为优化后的版本。在 Intel® Extension for PyTorch* 中,诸如卷积(Convolution)和线性(Linear)等常用算子均已得到优化。
练习
让我们使用 Intel® Extension for PyTorch* 来分析优化后的算子。我们将比较在代码更改中添加和删除这些行的效果。
与之前的练习一样,我们将把工作负载绑定到第一个插槽的物理核心上。
importtorch
classModel(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()
defforward(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 ####################
importintel_extension_for_pytorchasipex
model = ipex.optimize(model)
######################################################
print(model)
该模型由两个操作组成——Conv2d 和 ReLU。通过打印模型对象,我们得到以下输出。
让我们收集一级 TMA 指标。
请注意,Back End Bound 从 68.9 减少到了 38.5,实现了 1.8 倍的加速。
此外,让我们使用 PyTorch Profiler 进行分析。
请注意,CPU 时间从 851 微秒减少到 310 微秒 —— 速度提升了 2.7 倍。
图优化
强烈建议用户利用 TorchScript 结合 Intel® Extension for PyTorch* 进行进一步的图优化。为了通过 TorchScript 进一步优化性能,Intel® Extension for PyTorch* 支持对常用的 FP32/BF16 操作符模式(如 Conv2D+ReLU、Linear+ReLU 等)进行 oneDNN 融合,以减少操作符/内核调用的开销,并提高缓存局部性。某些操作符融合允许保留临时计算、数据类型转换和数据布局,以进一步提升缓存局部性。对于 INT8,Intel® Extension for PyTorch* 内置了量化方案,能够为包括 CNN、NLP 和推荐模型在内的热门深度学习工作负载提供良好的统计精度。量化模型随后通过 oneDNN 融合支持进行优化。
练习
让我们使用 TorchScript 来分析 FP32 图的优化。
与之前的练习一样,我们将把工作负载绑定到第一个插槽的物理核心上。
importtorch
classModel(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()
defforward(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 ####################
importintel_extension_for_pytorchasipex
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® Extension for PyTorch* 时,Conv + ReLU 操作符被融合,CPU 时间从 803 微秒减少到 248 微秒,实现了 3.2 倍的加速。oneDNN 的 eltwise 后置操作允许将一个基本操作与一个元素操作进行融合。这是最流行的融合类型之一:将一个 eltwise 操作(通常是激活函数,如 ReLU)与前面的卷积或内积操作进行融合。请查看下一节中显示的 oneDNN 详细日志。
通道最后的内存格式
当在模型上调用 ipex.optimize 时,Intel® Extension for PyTorch* 会自动将模型转换为优化的内存格式,即通道最后(channels last)。通道最后是一种对英特尔架构更为友好的内存格式。与 PyTorch 默认的通道优先 NCHW(批次、通道、高度、宽度)内存格式相比,通道最后 NHWC(批次、高度、宽度、通道)内存格式通常能通过更好的缓存局部性来加速卷积神经网络。
需要注意的是,内存格式的转换开销较大。因此,最好在部署之前一次性转换内存格式,并在部署期间尽量减少内存格式的转换。当数据在模型层之间传播时,通道最后的内存格式会在连续的通道最后支持的层之间保留(例如,Conv2d -> ReLU -> Conv2d),只有在通道最后不支持的层之间才会进行转换。更多详情请参阅内存格式传播。
练习
让我们来演示通道最后优化。
importtorch
classModel(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()
defforward(self, x):
x = self.conv(x)
x = self.relu(x)
return x
model = Model()
model.eval()
data = torch.rand(20, 16, 50, 100)
importintel_extension_for_pytorchasipex
############################### 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 文档。
以上是来自 channels first 模式的 oneDNN 详细日志。我们可以验证存在权重和数据的重排操作,然后进行计算,最后将输出重排回去。
以上是关于 channels last 的 oneDNN 详细输出。我们可以验证,channels last 内存格式避免了不必要的重排序。
使用 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
让我们收集一级 TMA 指标。
一级TMA分析表明,两者都受到后端的限制。正如前面所讨论的,大多数未经调优的深度学习工作负载都会受到后端限制。请注意,后端限制从70.0降低到了54.1。让我们进一步深入分析。
如前所述,后端瓶颈(Back End Bound)包含两个子指标——内存瓶颈(Memory Bound)和核心瓶颈(Core Bound)。内存瓶颈表明工作负载未优化或未充分利用,理想情况下,可以通过优化操作(OPs)和提升缓存局部性,将内存瓶颈操作改进为核心瓶颈操作。第二级 TMA 显示,后端瓶颈从内存瓶颈改进为核心瓶颈。让我们进一步深入分析。
在生产环境中使用 TorchServe 等模型服务框架扩展深度学习模型需要高效的计算利用率。这要求通过预取数据并在执行单元需要执行微操作(uOps)时重用缓存中的数据来确保数据的可用性。Level-3 TMA 显示,后端内存瓶颈从 DRAM Bound 改善为 Core Bound。
与之前在 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)来展示
-
在未优化或未调优的深度学习工作负载中,主要的性能瓶颈通常是后端限制(Back End Bound),它包含两个子指标:内存限制(Memory Bound)和核心限制(Core Bound)。
-
通过 Intel® Extension for PyTorch* 提供的内存分配器优化、算子融合以及内存布局格式优化,可以显著改善内存限制(Memory Bound)。
-
Intel® Extension for PyTorch* 和 oneDNN 库对深度学习的核心操作(如卷积、矩阵乘法、点积等)进行了深度优化,从而提升了核心限制(Core Bound)的表现。
-
Intel® Extension for PyTorch* 已集成到 TorchServe 中,并提供了易于使用的 API。
-
使用 Intel® Extension for PyTorch* 的 TorchServe 在 ResNet50 上实现了 7.71 倍的吞吐量提升,在 BERT 上实现了 2.20 倍的吞吐量提升。
致谢
我们要感谢 Ashok Emani(Intel)和 Jiong Gong(Intel)在本教程的多个步骤中提供的巨大指导和支持,以及详细的反馈和审阅。同时,我们也要感谢 Hamid Shojanazeri(Meta)和 Li Ning(AWS)在代码审查和教程中提供的宝贵反馈。