• Tutorials >
  • (Beta) PyTorch在AWS Graviton处理器上的推理性能优化
Shortcuts

(Beta) PyTorch在AWS Graviton处理器上的推理性能优化

作者: Sunita Nadampalli

AWS Graviton 是一系列由AWS设计的基于ARM的处理器。AWS Graviton3处理器针对机器学习(ML)工作负载进行了优化,包括支持 bfloat16、可扩展向量扩展(SVE)以及比Graviton2高两倍的单指令多数据(SIMD)带宽。

PyTorch为机器学习算子(如卷积、矩阵乘法、relu等)提供了原生参考ATen内核。这些算子可以通过来自基本线性代数(BLAS)库的特定于平台的内核实现进行加速。在AWS Graviton CPU上,MKLDNN与Arm Compute Library (ACL) 和 OpenBLAS 库为一部分算子提供了优化实现。从PyTorch 2.0版本开始,这两个库都集成到了PyTorch中。

在本教程中,我们将介绍如何通过 bfloat16 内核和正确的后端选择,在AWS Graviton3 CPU (AWS c7g实例) 上实现线性层神经网络的最佳推理性能。

内容

  1. 基本用法

  2. 使用Bfloat16快速数学内核加速推理

  3. 对于较小的批次维度,使用OpenBLAS提高推理性能

  4. 使用Linux透明大页优化内存分配开销

  5. 总结

Note

要成功运行本教程并重现下面显示的加速数字,您需要来自Graviton3系列(c7g/r7g/m7g)的硬件实例。对于本教程,我们使用了 c7g.xl (4vcpu)实例

基本用法

从PyTorch 2.0版本开始,PyTorch原生支持AWS Graviton3优化。 更多详细信息请参阅此 博客

  1. 运行以下命令安装PyTorch:

    python3 -m pip install torch
    
  2. 我们将从导入所需的依赖项并定义将在其上运行的设备开始:

import torch
import torch.nn as nn
from torch.profiler import profile, record_function, ProfilerActivity

# AWS Graviton3 cpu
device = ("cpu")
print(f"Using {device} device")
  1. 鉴于线性层是许多神经网络(包括Transformer)的核心,我们在此演示中使用线性层。我们通过子类化 nn.Module 并在 __init__ 中初始化层来定义我们的神经网络。我们使用典型的大型语言模型参数构建网络,以匹配真实世界场景:

class MyNeuralNetwork(nn.Module):
  def __init__(self):
      super().__init__()
      self.flatten = nn.Flatten()
      self.linear_relu_stack = nn.Sequential(
          nn.Linear(4096, 4096),
          nn.ReLU(),
          nn.Linear(4096, 11008),
          nn.ReLU(),
          nn.Linear(11008, 10),
      )

  def forward(self, x):
      x = self.flatten(x)
      logits = self.linear_relu_stack(x)
      return logits
  1. 让我们创建一个 MyNeuralNetwork 的实例,并将其移动到设备上:

model = MyNeuralNetwork().to(device)
print(model)

接下来,让我们通过将它们传递给 nn.Softmax 模块的实例来获取预测概率:

X = torch.rand(1, 64, 64, device=device)
logits = model(X)
pred_probab = nn.Softmax(dim=1)(logits)
y_pred = pred_probab.argmax(1)
print(f"Predicted class: {y_pred}")

输出:

Predicted class: tensor([2])

我们已验证了网络功能。接下来,我们将分析性能。让我们检查两种不同的情况:小批次维度和大批次维度。

情况1: 较大的批次维度,例如256:

# 首先进行预热,并循环多次以获得足够的执行时间

X = torch.rand(256, 64, 64, device=device)

with torch.set_grad_enabled(False):
    for _ in range(50):
        model(X) #Warmup
    with profile(activities=[ProfilerActivity.CPU]) as prof:
        with record_function("mymodel_inference"):
            for _ in range(100):
                model(X)

print(prof.key_averages().table(sort_by="self_cpu_time_total"))

使用默认PyTorch配置时的分析器输出如下:

Name

Self CPU %

Self CPU

CPU total %

CPU total

CPU time avg

# of Calls

aten::addmm

97.61%

15.813s

98.61%

15.977s

53.255ms

300

aten::clamp_min

1.09%

177.032ms

1.09%

177.032ms

885.160us

200

aten::copy

1.00%

162.054ms

1.00%

162.054ms

540.180us

300

mymodel_inference

0.22%

35.738ms

100.00%

16.201s

16.201s

1

aten::linear

0.02%

2.955ms

98.66%

15.985s

53.282ms

300

aten::t

0.01%

2.421ms

0.03%

5.043ms

16.810us

300

aten::relu

0.01%

2.356ms

1.11%

179.388ms

896.940us

200

Self CPU time total: 16.201s

使用 bfloat16 Fast Math Kernels加速推理

AWS Graviton3处理器支持 bfloat16 MMLA指令。Arm Compute Library (ACL) 为AWS Graviton处理器提供了优化的 bfloat16 通用矩阵乘法(GEMM)内核,并从PyTorch 2.0版本开始通过MKLDNN后端集成到PyTorch中。可以使用快速数学GEMM内核优化推理性能。默认情况下不启用快速数学模式,因为这些内核以 bfloat16 精度而不是 float 执行GEMM,因此会导致模型推理精度略有下降。但是,精度下降在 torchbench 测试套件中为 bfloat16 后端定义的 余弦相似度 阈值范围内,因此对大多数应用程序来说是可以接受的。要启用快速数学GEMM内核,请设置以下环境变量:

$ export DNNL_DEFAULT_FPMATH_MODE=BF16

当您运行上述推理脚本时,应该会看到启用MKLDNN快速数学模式后的分析器输出:

Name

Self CPU %

Self CPU

CPU total %

CPU total

CPU time avg

# of Calls

aten::addmm

95.61%

6.943s

97.10%

7.052s

23.507ms

300

aten::clamp_min

2.31%

167.653ms

2.31%

167.653ms

838.265us

200

aten::copy

1.48%

107.593ms

1.48%

107.593ms

358.643us

300

mymodel_inference

0.43%

31.167ms

100.00%

7.262s

7.262s

1

aten::linear

0.04%

2.911ms

97.21%

7.060s

23.533ms

300

aten::t

0.03%

2.414ms

0.07%

4.892ms

16.307us

300

aten::relu

0.03%

2.281ms

2.34%

169.934ms

849.670us

200

Self CPU time total: 7.262s

这比默认配置快约 2倍 (7.262s vs 16.201s)。接下来,让我们看看较小批次维度的情况。

场景 2: 较小的批量维度,例如 32:

X = torch.rand(32, 64, 64, device=device)
with torch.set_grad_enabled(False):
    for _ in range(50):
        model(X) #预热
    with profile(activities=[ProfilerActivity.CPU]) as prof:
        with record_function("mymodel_inference"):
            for _ in range(100):
                model(X)

print(prof.key_averages().table(sort_by="self_cpu_time_total"))

使用 PyTorch 默认配置运行上述脚本时,您应该会看到以下 profiler 输出:

自身 CPU 总计: 6.094s

以下是启用 MKLDNN 快速数学模式时的 profiler 输出:

$ export DNNL_DEFAULT_FPMATH_MODE=BF16

自身 CPU 总计: 4.123s

MKLDNN 快速数学模式为较小的批量维度提供了大约 1.47x (4.123s vs 6.094s) 的性能提升。 尽管性能提升明显,但整体仍有提升空间。因为来自 oneDNN 和 ACL 后端的运行时开销(权重重排和内核启动时间) 超过了 ACL GEMM 内核对较小批量计算的计算优势。

使用 OpenBLAS 提高较小批量维度的推理性能

可以通过将较小的形状从 MKLDNN 卸载到 OpenBLAS 后端来提高较小批量维度的推理性能。我们正在努力为未来版本实现自动化的后端选择,并具有健壮的启发式算法。在实现启发式算法之前,可以通过增加 MKLDNN 后端选择的阈值将较小的形状卸载到 OpenBLAS。在以下示例中,我们使用 64 作为阈值,因此批量维度为 32 的输入不会分派到 MKLDNN。相反,它会被分派到 OpenBLAS。

$ export TORCH_MKLDNN_MATMUL_MIN_DIM=64

以下是使用 OpenBLAS 后端时的 profiler 输出:

自身 CPU 总计: 2.034s

如您所见,切换到 OpenBLAS 将性能提高了一倍 (2.034s vs 4.123s) 与默认的 MKLDNN 后端配置相比。 对于更小的批量维度,例如批量维度为 10,这一点更加显著:

X = torch.rand(10, 64, 64, device=device)
with torch.set_grad_enabled(False):
    for _ in range(50):
        model(X) #预热
    with profile(activities=[ProfilerActivity.CPU]) as prof:
        with record_function("mymodel_inference"):
            for _ in range(100):
                model(X)

print(prof.key_averages().table(sort_by="self_cpu_time_total"))

以下是启用 MKLDNN 快速数学模式时的 profiler 输出:

自身 CPU 总计: 4.115s

以下是使用 OpenBLAS 后端时的 profiler 输出:

$ export TORCH_MKLDNN_MATMUL_MIN_DIM=64

自身 CPU 总计: 1.272s

这里我们观察到通过适当调整后端阈值,**性能提高了3.2倍(1.272s vs 4.115s)**。

使用 Linux Transparent Huge Pages (THP) 优化内存分配开销

我们还观察到,对于这些较大的网络,张量内存分配占推理延迟的很大一部分。这可以通过从PyTorch C10内存分配器 启用 THP 来优化。目前,该功能默认未启用,因为它会略微增加内存占用。设置以下环境变量以启用它:

$ export THP_MEM_ALLOC_ENABLE=1

对于批量维度为 256 且启用 MKLDNN Fast Math 模式:

X = torch.rand(256, 64, 64, device=device)
with torch.set_grad_enabled(False):
    for _ in range(50):
        model(X) #预热
    with profile(activities=[ProfilerActivity.CPU]) as prof:
        with record_function("mymodel_inference"):
            for _ in range(100):
                model(X)

print(prof.key_averages().table(sort_by="self_cpu_time_total"))

启用THP内存分配后,profiler的输出如下:

名称

自身CPU%

自身CPU

CPU总%

CPU总

CPU平均时间

调用次数

aten::addmm

91.31%

6.115s

94.39%

6.321s

21.069ms

300

aten::clamp_min

4.82%

322.568ms

4.82%

322.568ms

1.613ms

200

aten::copy

3.06%

204.602ms

3.06%

204.602ms

682.007us

300

mymodel_inference

0.61%

40.777ms

100.00%

6.697s

6.697s

1

aten::linear

0.05%

3.082ms

94.51%

6.329s

21.097ms

300

aten::relu

0.04%

2.547ms

4.85%

325.115ms

1.626ms

200

自身CPU总时间: 6.697s

这比上面测量的已优化的 MKLDNN Fast Math 模式又提高了 1.08倍或8%(6.697s vs 7.262s)

结论

在本教程中,我们介绍了在AWS Graviton3实例上的PyTorch推理,包括基本用法、使用快速数学内核的加速、 比较不同批量维度下不同后端的性能,以及如何使用Linux透明大页面优化张量内存分配延迟。 对于较大的张量形状,建议使用MKLDNN后端和Bfloat16快速数学模式以及THP内存分配;对于较小的张量形状, 建议使用OpenBLAS后端。希望您能尝试一下!

Docs

Access comprehensive developer documentation for PyTorch

View Docs

Tutorials

Get in-depth tutorials for beginners and advanced developers

View Tutorials

Resources

Find development resources and get your questions answered

View Resources