• Tutorials >
  • Grokking PyTorch Intel CPU performance from first principles (Part 2)
Shortcuts

从第一性原理理解PyTorch在Intel CPU上的性能(第二部分)

创建于:2022年10月14日 | 最后更新:2024年1月16日 | 最后验证:未验证

作者: 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倍。

../_images/1.png

先决条件

在本教程中,我们将使用自上而下的微架构分析(TMA)来分析并展示未优化或未调优的深度学习工作负载中,后端瓶颈(内存瓶颈、核心瓶颈)通常是主要瓶颈,并通过Intel® Extension for PyTorch*展示优化技术以改善后端瓶颈。我们将使用toplev,这是一个基于Linux perf构建的pmu-tools工具,用于TMA。

我们还将使用Intel® VTune™ Profiler的仪器和追踪技术(ITT)来进行更细粒度的性能分析。

自上而下的微架构分析方法 (TMA)

在调整CPU以获得最佳性能时,了解瓶颈所在是非常有用的。大多数CPU核心都有片上性能监控单元(PMUs)。PMUs是CPU核心内的专用逻辑单元,用于在系统上发生特定硬件事件时进行计数。这些事件的示例可能包括缓存未命中或分支预测错误。PMUs用于自上而下的微架构分析(TMA)以识别瓶颈。TMA由如下所示的层次结构组成:

../_images/26.png

顶层,即第一级指标收集退休错误推测前端瓶颈后端瓶颈。CPU的流水线在概念上可以简化为两部分:前端和后端。前端负责获取程序代码并将其解码为称为微操作(uOps)的低级硬件操作。然后,uOps通过一个称为分配的过程被送入后端。一旦分配完成,后端负责在可用的执行单元中执行uOp。uOp执行的完成称为退休。相反,错误推测是指在退休之前取消推测性获取的uOps,例如在分支预测错误的情况下。这些指标中的每一个都可以在后续级别中进一步细分,以精确定位瓶颈。

后端绑定调优

大多数未经调优的深度学习工作负载将受到后端限制。解决后端限制通常意味着解决导致延迟的源头,使得退休时间比必要的时间更长。如上所示,后端限制有两个子指标——核心限制和内存限制。

内存限制的停滞与内存子系统相关的原因有关。例如,最后一级缓存(LLC或L3缓存)未命中导致访问DRAM。扩展深度学习模型通常需要大量的计算。而高计算利用率要求在执行单元需要执行uOps时数据是可用的。这需要预取数据并在缓存中重用数据,而不是多次从主内存中获取相同的数据,这会导致执行单元在数据返回时被饿死。在本教程中,我们将展示更高效的内存分配器、操作符融合、内存布局格式优化通过更好的缓存局部性减少内存限制的开销。

核心绑定停滞表示在没有未完成的内存访问时,可用执行单元的使用不够优化。例如,连续几个通用矩阵-矩阵乘法(GEMM)指令竞争融合乘法加法(FMA)或点积(DP)执行单元可能会导致核心绑定停滞。包括DP内核在内的关键深度学习内核已经由oneDNN库(oneAPI深度神经网络库)进行了很好的优化,减少了核心绑定的开销。

像GEMM、卷积、反卷积这样的操作是计算密集型的。而像池化、批量归一化、激活函数如ReLU这样的操作是内存受限的。

Intel® VTune™ Profiler 的仪器和追踪技术 (ITT)

Intel® VTune Profiler 的 ITT API 是一个有用的工具,用于注释工作负载的某个区域,以便在更细粒度的注释(OP/函数/子函数粒度)下进行跟踪、分析和可视化。通过在 PyTorch 模型的 OP 粒度上进行注释,Intel® VTune Profiler 的 ITT 实现了操作级别的性能分析。Intel® VTune Profiler 的 ITT 已集成到 PyTorch Autograd Profiler 中。1

  1. 该功能必须通过with torch.autograd.profiler.emit_itt()显式启用。

使用英特尔® PyTorch* 扩展的 TorchServe

Intel® Extension for PyTorch* 是一个Python包,用于扩展PyTorch,以在Intel硬件上提供额外的性能优化。

Intel® Extension for PyTorch* 已经集成到 TorchServe 中,以提高开箱即用的性能。2 对于自定义处理程序脚本,我们建议添加 intel_extension_for_pytorch 包。

  1. 该功能必须通过在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))

让我们收集一级TMA指标。

../_images/32.png

一级TMA显示,PTMalloc和JeMalloc都受到后端的限制。超过一半的执行时间被后端阻塞。让我们深入一层。

../_images/41.png

Level-2 TMA 显示后端瓶颈是由内存瓶颈引起的。让我们进一步深入分析。

../_images/51.png

内存限制下的大多数指标识别从L1缓存到主内存的哪个层次是瓶颈。在给定层次上的热点表明大部分数据是从该缓存或内存层次检索的。优化应侧重于将数据移动到更接近核心的位置。Level-3 TMA显示PTMalloc受DRAM限制的瓶颈。另一方面,JeMalloc受L1限制的瓶颈——JeMalloc将数据移动到更接近核心的位置,从而执行速度更快。

让我们看一下Intel® VTune Profiler ITT跟踪。在示例脚本中,我们已经注释了推理循环中的每个step_x

../_images/61.png

每个步骤都在时间线图中进行了追踪。最后一步(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指标。

../_images/71.png

让我们再深入一层。

../_images/81.png

让我们使用Intel® VTune Profiler ITT来标注TorchServe推理范围,以便在推理级别粒度上进行性能分析。由于TorchServe架构由多个子组件组成,包括用于处理请求/响应的Java前端和用于在模型上运行实际推理的Python后端,使用Intel® VTune Profiler ITT来限制在推理级别收集跟踪数据是有帮助的。

../_images/9.png

每次推理调用都在时间线图中进行追踪。最后一次模型推理的持续时间从561.688毫秒减少到251.287毫秒 - 速度提升了2.2倍。

../_images/101.png

时间线图可以展开以查看操作级别的分析结果。aten::conv2d的持续时间从16.401毫秒减少到6.392毫秒 - 速度提升了2.6倍。

在本节中,我们已经展示了JeMalloc可以提供比默认的PyTorch内存分配器PTMalloc更好的性能,通过高效的线程本地缓存改善了后端限制。

英特尔® PyTorch* 扩展

三大Intel® Extension for PyTorch*优化技术,操作符、图、运行时,如下所示:

英特尔® PyTorch* 扩展优化技术

操作符

图表

运行时间

  • 向量化和多线程

  • 低精度 BF16/INT8 计算

  • 数据布局优化以提高缓存局部性

  • 常量折叠以减少计算

  • 操作融合以提高缓存局部性

  • 线程亲和性

  • 内存缓冲池

  • GPU 运行时

  • 启动器

操作符优化

优化的操作符和内核通过PyTorch调度机制注册。这些操作符和内核从Intel硬件的原生向量化特性和矩阵计算特性中加速。在执行过程中,Intel® Extension for PyTorch* 拦截ATen操作符的调用,并用这些优化的操作符替换原始的操作符。在Intel® Extension for PyTorch* 中,像卷积、线性这样的流行操作符已经被优化。

练习

让我们使用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。通过打印模型对象,我们得到以下输出。

../_images/11.png

让我们收集一级TMA指标。

../_images/121.png

注意后端绑定从68.9减少到38.5 – 1.8倍加速。

此外,让我们使用 PyTorch Profiler 进行分析。

../_images/131.png

注意CPU时间从851微秒减少到310微秒——速度提升了2.7倍。

图优化

强烈建议用户利用Intel® Extension for PyTorch*与TorchScript进行进一步的图优化。为了通过TorchScript进一步优化性能,Intel® Extension for PyTorch*支持常用FP32/BF16操作符模式(如Conv2D+ReLU、Linear+ReLU等)的oneDNN融合,以减少操作符/内核调用开销,并提高缓存局部性。一些操作符融合允许保留临时计算、数据类型转换和数据布局,以提高缓存局部性。对于INT8,Intel® Extension for PyTorch*内置了量化方案,为包括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)

让我们收集一级TMA指标。

../_images/141.png

注意后端绑定从67.1减少到37.5 – 1.8倍加速。

此外,让我们使用PyTorch Profiler进行分析。

../_images/151.png

请注意,使用Intel® Extension for PyTorch*,Conv + ReLU 操作符被融合,CPU 时间从 803 微秒减少到 248 微秒——速度提升了 3.2 倍。oneDNN 的 eltwise 后操作允许将原语与元素级原语融合。这是最流行的融合类型之一:将 eltwise(通常是激活函数,如 ReLU)与前面的卷积或内积操作融合。请查看下一节中显示的 oneDNN 详细日志。

通道最后内存格式

当在模型上调用ipex.optimize时,Intel® Extension for PyTorch* 会自动将模型转换为优化的内存格式,即通道最后。通道最后是一种对Intel架构更友好的内存格式。与PyTorch默认的通道优先NCHW(批次、通道、高度、宽度)内存格式相比,通道最后NHWC(批次、高度、宽度、通道)内存格式通常通过更好的缓存局部性加速卷积神经网络。

需要注意的是,转换内存格式的成本很高。因此,最好在部署前一次性转换内存格式,并在部署期间尽量减少内存格式的转换。当数据通过模型的层传播时,通道最后的内存格式会在连续的通道最后支持的层之间保留(例如,Conv2d -> ReLU -> Conv2d),并且仅在通道最后不支持的层之间进行转换。有关更多详细信息,请参阅Memory Format Propagation

练习

让我们演示一下通道最后的优化。

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文档

../_images/161.png
../_images/171.png

上面是来自通道优先的oneDNN详细输出。我们可以验证权重和数据有重新排序,然后进行计算,最后将输出重新排序回去。

../_images/181.png

上面是来自通道最后的一个DNN详细输出。我们可以验证,通道最后的内存格式避免了不必要的重新排序。

使用英特尔® PyTorch* 扩展提升性能

以下总结了TorchServe与Intel® Extension for PyTorch*在ResNet50和BERT-base-uncased上的性能提升。

../_images/191.png

使用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指标。

../_images/20.png

一级TMA显示两者都受到后端的限制。正如之前讨论的,大多数未调优的深度学习工作负载将是后端受限的。注意后端受限从70.0减少到54.1。让我们更深入地探讨一下。

../_images/211.png

如前所述,后端瓶颈有两个子指标——内存瓶颈和核心瓶颈。内存瓶颈表示工作负载未优化或未充分利用,理想情况下,通过优化操作和提高缓存局部性,可以将内存瓶颈操作改进为核心瓶颈。二级TMA显示,后端瓶颈从内存瓶颈改进为核心瓶颈。让我们进一步深入探讨。

../_images/221.png

在像TorchServe这样的模型服务框架上扩展深度学习模型以用于生产需要高计算利用率。这要求在执行单元需要执行uOps时,通过预取和重用缓存中的数据来提供数据。Level-3 TMA显示,后端内存限制从DRAM限制改善为核心限制。

与之前使用TorchServe的练习一样,让我们使用Intel® VTune Profiler ITT来注释TorchServe推理范围,以便在推理级别的粒度上进行性能分析。

../_images/231.png

每次推理调用都在时间线图中进行追踪。最后一次推理调用的持续时间从215.731毫秒减少到95.634毫秒 - 速度提升了2.3倍。

../_images/241.png

时间线图可以展开以查看操作级别的分析结果。请注意,Conv + ReLU 已被融合,持续时间从 6.393 毫秒 + 1.731 毫秒减少到 3.408 毫秒 - 速度提升了 2.4 倍。

结论

在本教程中,我们使用了自上而下的微架构分析(TMA)和Intel® VTune™ Profiler的仪器和追踪技术(ITT)来证明

  • 通常,未优化或未调整的深度学习工作负载的主要瓶颈是后端限制,它有两个子指标,内存限制和核心限制。

  • 通过英特尔® PyTorch* 扩展的更高效内存分配器、操作符融合和内存布局格式优化,提高了内存限制。

  • 关键的深度学习原语,如卷积、矩阵乘法、点积等,已由Intel® Extension for PyTorch*和oneDNN库进行了优化,从而提高了核心性能。

  • Intel® Extension for PyTorch* 已通过易用的 API 集成到 TorchServe 中。

  • TorchServe 与 Intel® Extension for PyTorch* 结合使用,ResNet50 的吞吐量提升了 7.71 倍,BERT 的吞吐量提升了 2.20 倍。

致谢

我们要感谢Ashok Emani(英特尔)和Jiong Gong(英特尔)在本教程的许多步骤中提供的巨大指导和支持,以及全面的反馈和审查。我们还要感谢Hamid Shojanazeri(Meta)和Li Ning(AWS)在代码审查和教程中提供的宝贵反馈。

优云智算