从第一性原理理解 PyTorch 英特尔 CPU 性能(第二部分)¶
创建于:2022 年 10 月 14 日 | 最后更新:2024 年 1 月 16 日 | 最后验证:未验证
作者:Min Jean Cho, Jing Xu, Mark Saroufim
在 从第一性原理理解 PyTorch 英特尔 CPU 性能 教程中,我们介绍了如何调整 CPU 运行时配置、如何分析其性能,以及如何将其集成到 TorchServe 中以获得优化的 CPU 性能。
在本教程中,我们将演示如何通过 英特尔® PyTorch* 扩展启动器 利用内存分配器提升性能,以及如何通过 英特尔® PyTorch* 扩展 利用 CPU 上的优化内核提升性能,并将它们应用于 TorchServe,展示 ResNet50 吞吐量加速 7.71 倍,BERT 吞吐量加速 2.20 倍。
先决条件¶
在本教程中,我们将使用 自顶向下微架构分析 (TMA) 来分析性能,并展示后端受限(内存受限、核心受限)通常是未优化或欠优化的深度学习工作负载的主要瓶颈,并演示通过英特尔® PyTorch* 扩展改进后端受限的优化技术。我们将使用 toplev,它是 pmu-tools 的一部分,构建于 Linux perf 之上,用于 TMA。
我们还将使用 英特尔® VTune™ Profiler 的 Instrumentation and Tracing Technology (ITT) 来进行更精细的性能分析。
自顶向下微架构分析方法 (TMA)¶
在调整 CPU 以获得最佳性能时,了解瓶颈在哪里非常有用。大多数 CPU 核心都有片上性能监控单元 (PMU)。PMU 是 CPU 核心内的专用逻辑单元,用于计算系统上发生的特定硬件事件。这些事件的示例可能是缓存未命中或分支预测错误。PMU 用于自顶向下微架构分析 (TMA) 以识别瓶颈。TMA 由分层级别组成,如下所示
顶层,即 level-1,指标收集执行、错误推测、前端受限、后端受限。CPU 的流水线在概念上可以简化并分为两部分:前端和后端。前端负责获取程序代码并将其解码为称为微操作 (uOps) 的低级硬件操作。然后,uOps 被馈送到后端,这个过程称为分配。一旦分配,后端负责在可用的执行单元中执行 uOp。uOp 执行完成称为执行。相比之下,错误推测是指推测性获取的 uOp 在执行之前被取消,例如在分支预测错误的情况下。这些指标中的每一个都可以在后续级别中进一步分解,以查明瓶颈。
为后端受限进行调优¶
大多数未调优的深度学习工作负载将是后端受限。解决后端受限通常是解决导致执行时间比必要时间长的延迟来源。如上所示,后端受限有两个子指标——核心受限和内存受限。
内存受限的停顿与内存子系统有关。例如,末级缓存(LLC 或 L3 缓存)未命中导致访问 DRAM。扩展深度学习模型通常需要大量的计算。高计算利用率要求当执行单元需要数据来执行 uOp 时,数据是可用的。这需要预取数据并在缓存中重用数据,而不是多次从主内存中获取相同的数据,这会导致执行单元在数据返回时处于饥饿状态。在本教程中,我们将展示更高效的内存分配器、运算符融合、内存布局格式优化减少了内存受限的开销,并具有更好的缓存局部性。
核心受限的停顿表明在没有未完成的内存访问时,可用执行单元的使用效率低下。例如,一系列通用矩阵-矩阵乘法 (GEMM) 指令争用融合乘加 (FMA) 或点积 (DP) 执行单元可能会导致核心受限的停顿。包括 DP 内核在内的关键深度学习内核已通过 oneDNN 库 (oneAPI Deep Neural Network Library) 进行了充分优化,从而减少了核心受限的开销。
像 GEMM、卷积、反卷积这样的操作是计算密集型的。而像池化、批归一化、ReLU 这样的激活函数是内存受限的。
英特尔® VTune™ Profiler 的 Instrumentation and Tracing Technology (ITT)¶
英特尔® VTune Profiler 的 ITT API 是一个有用的工具,用于注释工作负载的区域以进行跟踪,从而在更精细的注释粒度(OP/函数/子函数粒度)下进行性能分析和可视化。通过在 PyTorch 模型的 OP 粒度上进行注释,英特尔® VTune Profiler 的 ITT 实现了 OP 级性能分析。英特尔® VTune Profiler 的 ITT 已集成到 PyTorch Autograd Profiler 中。 1
该功能必须通过 with torch.autograd.profiler.emit_itt() 显式启用。
带有英特尔® PyTorch* 扩展的 TorchServe¶
英特尔® PyTorch* 扩展 是一个 Python 包,用于扩展 PyTorch,针对英特尔硬件进行优化,以获得额外的性能提升。
英特尔® PyTorch* 扩展已经集成到 TorchServe 中,以开箱即用地提高性能。 2 对于自定义处理程序脚本,我们建议添加 intel_extension_for_pytorch 包。
该功能必须通过在 config.properties 中设置 ipex_enable=true 显式启用。
在本节中,我们将展示后端受限通常是未优化或欠优化的深度学习工作负载的主要瓶颈,并演示通过英特尔® PyTorch* 扩展改进后端受限的优化技术,后端受限有两个子指标 - 内存受限和核心受限。更高效的内存分配器、运算符融合、内存布局格式优化可以改善内存受限。理想情况下,通过优化的运算符和更好的缓存局部性,可以将内存受限改善为核心受限。英特尔® 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))
让我们收集 level-1 TMA 指标。
Level-1 TMA 表明 PTMalloc 和 JeMalloc 都受后端限制。超过一半的执行时间被后端停顿。让我们深入一层。
Level-2 TMA 表明后端受限是由内存受限引起的。让我们深入一层。
内存受限下的大多数指标都识别出从 L1 缓存到主内存的内存层次结构中的哪一层是瓶颈。给定级别的热点表明大部分数据是从该缓存或内存级别检索的。优化应侧重于将数据移动到更靠近核心的位置。Level-3 TMA 表明 PTMalloc 受 DRAM 受限的限制。另一方面,JeMalloc 受 L1 受限的限制 - JeMalloc 将数据移动到更靠近核心的位置,因此执行速度更快。
让我们看一下英特尔® 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 指标。
让我们深入一层。
让我们使用英特尔® VTune Profiler ITT 来注释 TorchServe 推理范围,以便在推理级别粒度下进行性能分析。由于 TorchServe 架构 由几个子组件组成,包括用于处理请求/响应的 Java 前端和用于在模型上运行实际推理的 Python 后端,因此使用英特尔® VTune Profiler ITT 将跟踪数据的收集限制在推理级别是有帮助的。
每个推理调用都在时间线图中跟踪。最后一个模型推理的持续时间从 561.688 毫秒减少到 251.287 毫秒 - 速度提升 2.2 倍。
可以展开时间线图以查看 OP 级性能分析结果。aten::conv2d 的持续时间从 16.401 毫秒减少到 6.392 毫秒 - 速度提升 2.6 倍。
在本节中,我们已经证明,通过高效的线程本地缓存改进后端受限,JeMalloc 可以提供比默认 PyTorch 内存分配器 PTMalloc 更好的性能。
英特尔® PyTorch* 扩展¶
三个主要的 英特尔® PyTorch* 扩展 优化技术,运算符、图和运行时,如下所示
英特尔® PyTorch* 扩展优化技术 |
||
---|---|---|
运算符 |
图 |
运行时 |
|
|
|
运算符优化¶
优化的运算符和内核通过 PyTorch 分派机制注册。这些运算符和内核通过英特尔硬件的本机向量化功能和矩阵计算功能加速。在执行期间,英特尔® 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。通过打印模型对象,我们得到以下输出。
让我们收集 level-1 TMA 指标。
请注意,后端受限从 68.9 减少到 38.5 – 速度提升 1.8 倍。
此外,让我们使用 PyTorch Profiler 进行性能分析。
请注意,CPU 时间从 851 微秒减少到 310 微秒 – 速度提升 2.7 倍。
图优化¶
强烈建议用户利用带有 TorchScript 的英特尔® PyTorch* 扩展来进行进一步的图优化。为了通过 TorchScript 进一步优化性能,英特尔® PyTorch* 扩展支持 oneDNN 融合常用的 FP32/BF16 运算符模式,例如 Conv2D+ReLU、Linear+ReLU 等,以减少运算符/内核调用开销,并获得更好的缓存局部性。一些运算符融合允许维护临时计算、数据类型转换、数据布局,以获得更好的缓存局部性。对于 INT8,英特尔® PyTorch* 扩展也内置了量化配方,为包括 CNN、NLP 和推荐模型在内的常用 DL 工作负载提供良好的统计精度。然后,使用 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 进行性能分析。
请注意,使用英特尔® PyTorch* Conv + ReLU 运算符被融合,CPU 时间从 803 微秒减少到 248 微秒 – 速度提升 3.2 倍。oneDNN eltwise 后操作使原始操作能够与逐元素原始操作融合。这是最流行的融合类型之一:逐元素操作(通常是激活函数,如 ReLU)与前面的卷积或内积融合。请查看下一节中显示的 oneDNN 详细日志。
Channels Last 内存格式¶
在模型上调用 ipex.optimize 时,英特尔® PyTorch* 扩展会自动将模型转换为优化的内存格式 channels last。Channels last 是一种对英特尔架构更友好的内存格式。与 PyTorch 默认的 channels first NCHW(批大小、通道数、高度、宽度)内存格式相比,channels last NHWC(批大小、高度、宽度、通道数)内存格式通常可以加速卷积神经网络,并具有更好的缓存局部性。
需要注意的一点是,转换内存格式的开销很大。因此,最好在部署之前一次性转换内存格式,并在部署期间保持最小的内存格式转换。当数据通过模型的层传播时,channels last 内存格式会通过连续的 channels last 支持层(例如,Conv2d -> ReLU -> Conv2d)保留,并且转换仅在 channels last 不支持的层之间进行。有关更多详细信息,请参阅 内存格式传播。
练习¶
让我们演示 channels last 优化。
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 文档。
以上是来自 channels first 的 oneDNN 详细信息。我们可以验证是否存在来自权重和数据的重新排序,然后进行计算,最后将输出重新排序回去。
以上是来自 channels last 的 oneDNN 详细信息。我们可以验证 channels last 内存格式避免了不必要的重新排序。
TorchServe 练习¶
让我们分析 TorchServe 中英特尔® 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 指标。
Level-1 TMA 显示两者都受后端 (backend) 限制。正如之前讨论的,大多数未经调优的深度学习工作负载都将是后端受限 (Back End Bound) 的。请注意,后端受限 (Back End Bound) 从 70.0 降至 54.1。让我们更深入一层。
正如之前讨论的,后端受限 (Back End Bound) 有两个子指标——内存受限 (Memory Bound) 和核心受限 (Core Bound)。内存受限 (Memory Bound) 表示工作负载未充分优化或未充分利用,理想情况下,可以通过优化 OP 和提高缓存局部性,将内存受限操作改进为核心受限。Level-2 TMA 显示后端受限 (Back End Bound) 从内存受限 (Memory Bound) 改进为核心受限 (Core Bound)。让我们更深入一层。
在模型服务框架(如 TorchServe)上扩展用于生产的深度学习模型需要高计算利用率。这要求数据通过预取可用,并在执行单元需要它来执行 uOp 时,在缓存中重用数据。Level-3 TMA 显示后端内存受限 (Back End Memory Bound) 从 DRAM 受限改进为核心受限 (Core Bound)。
与之前的 TorchServe 练习一样,让我们使用 Intel® VTune™ 分析器 ITT 来注释 TorchServe 推理范围,以便在推理级别粒度上进行性能分析。
每个推理调用都在时间线图中被追踪。上次推理调用的持续时间从 215.731 毫秒减少到 95.634 毫秒,速度提升了 2.3 倍。
可以展开时间线图以查看算子级别的性能分析结果。请注意,Conv + ReLU 已融合,持续时间从 6.393 毫秒 + 1.731 毫秒减少到 3.408 毫秒,速度提升了 2.4 倍。
结论¶
在本教程中,我们使用了自顶向下微架构分析 (Top-down Microarchitecture Analysis, TMA) 和 Intel® VTune™ 分析器的 Instrumentation and Tracing Technology (ITT) 来证明:
通常,未充分优化或未充分调优的深度学习工作负载的主要瓶颈是后端受限 (Back End Bound),它有两个子指标:内存受限 (Memory Bound) 和核心受限 (Core Bound)。
Intel® PyTorch 扩展* 提供的更高效的内存分配器、算子融合、内存布局格式优化可以改善内存受限 (Memory Bound) 的情况。
关键的深度学习原语,例如卷积、矩阵乘法、点积等,已通过 Intel® PyTorch 扩展* 和 oneDNN 库进行了良好优化,从而改善了核心受限 (Core Bound) 的情况。
Intel® PyTorch 扩展* 已集成到 TorchServe 中,并提供易于使用的 API。
TorchServe 与 Intel® PyTorch 扩展* 结合使用,ResNet50 的吞吐量提高了 7.71 倍,BERT 的吞吐量提高了 2.20 倍。
致谢¶
我们要感谢 Ashok Emani (Intel) 和 Jiong Gong (Intel) 在本教程的许多步骤中给予的巨大指导和支持,以及全面的反馈和评论。我们还要感谢 Hamid Shojanazeri (Meta) 和 Li Ning (AWS) 在代码审查和教程中提供的有益反馈。