Grokking PyTorch Intel CPU 性能的第一原则(第 2 部分)¶
创建时间: 2022年10月14日 |上次更新时间:2024 年 1 月 16 日 |上次验证时间:未验证
作者: Min Jean Cho, Jing Xu, Mark Saroufim
在 Grokking PyTorch 第一性原理的 Intel CPU 性能教程中 ,我们介绍了如何调整 CPU 运行时配置、如何对其进行分析以及如何将它们集成到 TorchServe 中以优化 CPU 性能。
在本教程中,我们将演示通过英特尔® Extension for PyTorch* Launcher 使用内存分配器提高性能,以及通过英特尔® Extension for PyTorch* 优化 CPU 内核,并将它们应用于 TorchServe,展示 ResNet50 的 7.71 倍吞吐量加速和 BERT 的 2.20 倍吞吐量加速。
先决条件¶
在本教程中,我们将使用自上而下的微架构分析 (TMA) 来分析和展示后端绑定(内存绑定、内核绑定)通常是优化不足或调整不足的深度学习工作负载的主要瓶颈,并通过英特尔® Extension for PyTorch* 演示优化技术,以改进后端绑定。我们将使用 toplev,这是基于 Linux perf 构建的 pmu-tools 的工具部分,用于 TMA。
我们还将使用英特尔® VTune™ Profiler 的检测和跟踪技术 (ITT) 进行更精细的分析。
自上而下的微架构分析方法 (TMA)¶
在优化 CPU 以获得最佳性能时,了解瓶颈在哪里很有用。大多数 CPU 内核都有片上性能监控单元 (PMU)。PMU 是 CPU 内核中的专用逻辑片段,用于在系统上发生特定硬件事件时对其进行计数。这些事件的示例可能是 Cache Misses 或 Branch Mispredictions。PMU 用于自上而下的微架构分析 (TMA) 以识别瓶颈。TMA 由分层级别组成,如下所示:
顶级 1 级指标收集 Retireing、Bad Speculation、Front End Bound、Back End Bound。CPU 的管道在概念上可以简化并分为两部分:前端和后端。前端负责获取程序代码并将其解码为称为微操作 (uOps) 的低级硬件操作。然后,uOps 在一个称为 allocation 的过程中被馈送到后端。分配后,后端负责在可用的执行单元中执行 uOp。uOp 的执行完成称为 retirement。相反,一个糟糕的推测是推测性获取的 uOps 在停用之前被取消,例如在预测错误的分支的情况下。这些指标中的每一个都可以在后续级别中进一步细分,以查明瓶颈。
针对后端绑定进行优化¶
大多数未优化的深度学习工作负载将是后端绑定的。解决后端绑定通常是解决延迟源,导致停用花费的时间超过必要的时间。如上所示,Back End Bound 有两个子指标 – Core Bound 和 Memory Bound。
Memory Bound 停顿的原因与内存子系统有关。例如,最后一级缓存(LLC 或 L3 缓存)未命中,导致对 DRAM 的访问。扩展深度学习模型通常需要大量的计算。高计算利用率要求数据在执行单元需要执行 uOps 时可用。这需要预取数据并在缓存中重用数据,而不是从主内存中多次获取相同的数据,这会导致执行单元在返回数据时匮乏。在本教程中,我们将展示更高效的内存分配器、运算符融合、内存布局格式优化可以通过更好的缓存局部性来减少 Memory Bound 的开销。
Core Bound 停顿表示可用执行单元的使用不理想,而没有未完成的内存访问。例如,连续几个通用矩阵矩阵乘法 (GEMM) 指令竞争融合乘加 (FMA) 或点积 (DP) 执行单元可能会导致 Core Bound 停顿。包括 DP 内核在内的关键深度学习内核已通过 oneDNN 库(oneAPI 深度神经网络库)进行了很好的优化,减少了 Core Bound 的开销。
GEMM、卷积、反卷积等操作是计算密集型的。而池化、批量归一化、ReLU 等激活函数等操作是内存受限的。
英特尔® VTune™ Profiler 的检测和跟踪技术 (ITT)¶
英特尔® VTune Profiler 的 ITT API 是一个有用的工具,可以注释工作负载的某个区域,以便以更精细的注释粒度 - OP/函数/子函数粒度进行跟踪、分析和可视化。通过对 PyTorch 模型的 OP 粒度进行注释,Intel® VTune Profiler 的 ITT 支持操作级分析。英特尔® VTune Profiler 的 ITT 已集成到 PyTorch Autograd Profiler 中。1
该功能必须通过 torch.autograd.profiler.emit_itt() 显式启用。
TorchServe 与面向 PyTorch* 的英特尔®扩展¶
面向 PyTorch* 的英特尔®扩展是一个 Python 软件包,用于扩展 PyTorch,并优化以进一步提升英特尔硬件的性能。
面向 PyTorch* 的英特尔®扩展已集成到 TorchServe 中,以提高开箱即用的性能。2对于自定义处理程序脚本,我们建议在 intel_extension_for_pytorch 中添加 package。
该功能必须通过在 config.properties 中设置 ipex_enable=true 来显式启用。
在本节中,我们将展示后端绑定通常是优化不足或调优不足的深度学习工作负载的主要瓶颈,并通过英特尔® PyTorch* 扩展演示用于改进后端绑定的优化技术,后端绑定有两个子指标 - 内存绑定和内核绑定。更高效的内存分配器、运算符融合、内存布局格式优化改进了 Memory Bound。理想情况下,可以通过优化的运算符和更好的缓存局部性将 Memory Bound 改进为 Core Bound。关键的深度学习基元,如卷积、矩阵乘法、点积,已通过英特尔® Extension for PyTorch* 和 oneDNN 库进行了很好的优化,从而改进了内核绑定。
利用高级启动器配置:内存分配器¶
从性能的角度来看,内存分配器起着重要的作用。更高效的内存使用可以减少不必要的内存分配或销毁的开销,从而加快执行速度。对于实际应用中的深度学习工作负载,尤其是在 TorchServe、TCMalloc 或 JeMalloc 等大型多核系统或服务器上运行的工作负载,通常可以比默认的 PyTorch 内存分配器 PTMalloc 获得更好的内存使用率。
TCMalloc、JeMalloc、PTMalloc¶
TCMalloc 和 JeMalloc 都使用线程本地缓存来减少线程同步的开销,并分别使用自旋锁和每线程 arenas 来锁定争用。TCMalloc 和 JeMalloc 减少了不必要的内存分配和释放的开销。这两个分配器都按大小对内存分配进行分类,以减少内存碎片的开销。
使用启动器,用户可以通过选择三个启动器旋钮之一来轻松尝试不同的内存分配器 –enable_tcmalloc (TCMalloc)、–enable_jemalloc (JeMalloc)、–use_default_allocator (PTMalloc)。
锻炼¶
让我们分析 PTMalloc 与 JeMalloc。
我们将使用 Launcher 来指定内存分配器,并将工作负载绑定到第一个 socket 的物理内核,以避免任何 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))
我们来收集 1 级 TMA 指标。
1 级 TMA 显示 PTMalloc 和 JeMalloc 都受后端限制。超过一半的执行时间被后端停滞。让我们更深入一点。
级别 2 TMA 显示后端绑定是由内存绑定引起的。让我们更深入一点。
Memory Bound 下的大多数指标都确定了从 L1 高速缓存到主内存的内存层次结构的哪个级别是瓶颈。在给定级别界定的热点表示大多数数据是从该缓存或内存级别检索的。优化应侧重于将数据更靠近核心。3 级 TMA 显示 PTMalloc 受到 DRAM Bound 的瓶颈。另一方面,JeMalloc 受到 L1 Bound 的瓶颈——JeMalloc 将数据移动到更靠近核心的位置,从而加快了执行速度。
我们来看一下英特尔® VTune Profiler ITT 跟踪。在示例脚本中,我们对推理循环的每个step_x进行了注释。
每个步骤都在时间线图中跟踪。模型在最后一步 (step_99) 的推理持续时间从 304.308 毫秒减少到 261.843 毫秒。
使用 TorchServe 进行锻炼¶
让我们分析 PTMalloc 与 TorchServe 的 JeMalloc 。
我们将使用 TorchServe apache-bench 基准测试和 ResNet50 FP32,批量大小 32,并发 32,请求 8960。所有其他参数与默认参数相同。
与上一个练习一样,我们将使用 launcher 来指定内存分配器,并将工作负载绑定到第一个套接字的物理内核。为此,用户只需在 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
我们来收集 1 级 TMA 指标。
让我们更深入一点。
让我们使用英特尔® VTune Profiler ITT 对 TorchServe 推理范围进行注释,以在推理级粒度进行分析。由于 TorchServe 架构由多个子组件组成,包括用于处理请求/响应的 Java 前端和用于在模型上运行实际推理的 Python 后端,因此使用英特尔® VTune Profiler ITT 在推理级别限制跟踪数据的收集非常有用。
每个推理调用都在时间线图中进行跟踪。最后一次模型推理的持续时间从 561.688 毫秒减少到 251.287 毫秒,速度提高了 2.2 倍。
可以展开时间线图以查看运算级分析结果。aten::conv2d 的持续时间从 16.401 毫秒减少到 6.392 毫秒 - 加速了 2.6 倍。
在本节中,我们证明了 JeMalloc 可以提供比默认 PyTorch 内存分配器 PTMalloc 更好的性能,高效的线程本地缓存改进了后端绑定。
面向 PyTorch* 的英特尔®扩展¶
英特尔 PyTorch* 扩展的三大®优化技术,即运算符、图形、运行时,如下所示:
英特尔® Extension for PyTorch* 优化技术 |
||
---|---|---|
算子 |
图 |
运行 |
|
|
|
操作员优化¶
优化后的算子和内核通过 PyTorch 调度机制注册。这些算子和内核是从 Intel 硬件的原生矢量化功能和矩阵计算功能加速而来的。在执行过程中,英特尔® PyTorch* 扩展会拦截 ATen 运算符的调用,并将原始运算符替换为这些优化的运算符。卷积、线性等常用运算符已在英特尔® PyTorch* 扩展中进行了优化。
锻炼¶
让我们使用英特尔® 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。通过打印 model 对象,我们得到以下输出。
我们来收集 1 级 TMA 指标。
请注意,后端边界从 68.9 降低到 38.5 – 速度提高了 1.8 倍。
此外,让我们使用 PyTorch Profiler 进行性能分析。
请注意,CPU 时间从 851 微秒减少到 310 微秒 – 速度提高了 2.7 倍。
图形优化¶
强烈建议用户利用带有 TorchScript 的英特尔® PyTorch* 扩展来进一步优化图形。为了进一步优化 TorchScript 的性能,面向 PyTorch* 的英特尔®扩展支持常用 FP32/BF16 运算符模式(如 Conv2D+ReLU、Linear+ReLU 等)的 oneDNN 融合,以减少运算符/内核调用开销,并实现更好的缓存局部性。一些运算符融合允许维护临时计算、数据类型转换、数据布局以获得更好的缓存位置。与 INT8 一样,面向 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)
我们来收集 1 级 TMA 指标。
请注意,后端边界从 67.1 降低到 37.5 – 加速了 1.8 倍。
此外,让我们使用 PyTorch Profiler 进行性能分析。
请注意,借助面向 PyTorch* 的英特尔®扩展,Conv + ReLU 运算符被融合,CPU 时间从 803 微秒减少到 248 微秒,速度提高了 3.2 倍。oneDNN eltwise 后操作支持将基元与元素基元融合。这是最流行的融合类型之一:具有前卷积或内积的 eltwise(通常是 ReLU 等激活函数)。请查看下一节中显示的 oneDNN 详细日志。
通道上次内存格式¶
在模型上调用 ipex.optimize 时,英特尔® PyTorch* 扩展会自动将模型转换为优化的内存格式,通道最后。Channels last 是一种对 Intel 架构更友好的内存格式。与 PyTorch 默认通道前 NCHW(批处理、通道、高度、宽度)内存格式相比,通道后 NHWC(批处理、高度、宽度、通道)内存格式通常以更好的缓存局部性加速卷积神经网络。
需要注意的一点是转换内存格式很昂贵。因此,最好在部署之前转换一次内存格式,并在部署期间保持最短的内存格式转换。当数据在模型的层中传播时,通道最后一个内存格式通过连续通道最后一个支持的层(例如,Conv2d -> ReLU -> Conv2d)保留,并且仅在最后一个不支持的层的通道之间进行转换。有关更多详细信息,请参阅 Memory Format Propagation 。
锻炼¶
我们来演示 channels last optimization。
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 详细内容,首先来自通道。我们可以验证 weight 和 data 是否有重新排序,然后进行计算,最后将输出重新排序回来。
以上是来自最后一个频道的 oneDNN 详细内容。我们可以验证 channels last memory format 避免了不必要的重新排序。
通过面向 PyTorch* 的英特尔®扩展实现性能提升¶
下面总结了 TorchServe 与面向 ResNet50 和 BERT-base-uncased 的英特尔® PyTorch* 扩展的性能提升。
使用 TorchServe 进行锻炼¶
我们来分析一下使用 TorchServe 的英特尔® Extension for PyTorch* 优化。
我们将使用 TorchServe apache-bench 基准测试和 ResNet50 FP32 TorchScript,批量大小 32,并发 32,请求 8960。所有其他参数与默认参数相同。
与上一个练习一样,我们将使用 launcher 将工作负载绑定到第一个 socket 的物理内核。为此,用户只需在 config.properties 中添加几行:
cpu_launcher_enable=true
cpu_launcher_args=--node_id 0
我们来收集 1 级 TMA 指标。
Level-1 TMA 显示两者都受后端的约束。如前所述,大多数未调整的深度学习工作负载将是后端绑定的。请注意,后端边界从 70.0 减少到 54.1。让我们更深入一点。
如前所述,后端绑定有两个子指标 – 内存绑定和核心绑定。内存受限表示工作负载优化不足或未充分利用,理想情况下,可以通过优化 OP 和改进缓存局部性将内存受限操作改进为核心受限。2 级 TMA 显示后端绑定从 Memory Bound 改进到 Core Bound。让我们更深入一点。
在 TorchServe 等模型服务框架上扩展深度学习模型以进行生产需要高计算利用率。这要求当执行单元需要执行 uOps 时,可以通过预取和重用缓存中的数据来使用数据。3 级 TMA 显示后端内存绑定从 DRAM 绑定改进到内核绑定。
与之前使用 TorchServe 的练习一样,我们使用英特尔® VTune Profiler ITT 对 TorchServe 推理范围进行注释,以推理级粒度进行分析。
每个推理调用都在时间线图中进行跟踪。上次推理调用的持续时间从 215.731 毫秒减少到 95.634 毫秒,速度提高了 2.3 倍。
可以展开时间线图以查看运算级分析结果。请注意,Conv + ReLU 已融合,持续时间从 6.393 毫秒 + 1.731 毫秒减少到 3.408 毫秒 - 加速 2.4 倍。
结论¶
在本教程中,我们使用了自上而下的微架构分析 (TMA) 和英特尔® VTune™ Profiler 的检测和跟踪技术 (ITT) 来演示这一点
通常,优化不足或调整不足的深度学习工作负载的主要瓶颈是后端绑定,它有两个子指标,即内存绑定和内核绑定。
英特尔® Extension for PyTorch* 提供的更高效的内存分配器、运算符融合、内存布局格式优化改进了内存限制。
关键的深度学习基元,如卷积、矩阵乘法、点积等,已通过英特尔® Extension for PyTorch* 和 oneDNN 库进行了很好的优化,从而改进了内核边界。
英特尔® PyTorch* 扩展已通过易于使用的 API 集成到 TorchServe 中。
带有面向 PyTorch* 的英特尔®扩展的 TorchServe 显示 ResNet50 的吞吐量加速 7.71 倍,BERT 的吞吐量加速 2.20 倍。
确认¶
我们要感谢 Ashok Emani (Intel) 和 Jiong Gong (Intel) 在本教程的许多步骤中提供的大量指导和支持,以及全面的反馈和审查。我们还要感谢 Hamid Shojanazeri (Meta) 和 Li Ning (AWS) 在代码审查和教程中提供的有益反馈。