• 教程 >
  • (Beta) AWS Graviton 处理器上的 PyTorch 推理性能调优
快捷方式

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

作者: Sunita Nadampalli

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

PyTorch 为机器学习操作符(如卷积、矩阵乘法、ReLU 等)提供原生参考 ATen 内核。这些操作符可以通过来自基本线性代数 (BLAS) 库的平台特定内核实现来加速。在 AWS Graviton CPU 上,带有 Arm 计算库 (ACL) 的 MKLDNN 和 OpenBLAS 库为部分操作符提供了优化的实现。这两个库都已集成到 PyTorch 2.0 版本中。

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

内容

  1. 基本用法

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

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

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

  5. 结论

注意

为了成功运行本教程并复现以下所示的加速数值,您需要一个来自 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

# warm it up first and loop over multiple times to have enough execution time

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 配置的分析器输出

名称

自身 CPU %

自身 CPU

CPU 总 %

CPU 总计

CPU 时间平均值

调用次数

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

自身 CPU 时间总计: 16.201s

使用 bfloat16 快速数学内核加速推理

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

$ export DNNL_DEFAULT_FPMATH_MODE=BF16

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

名称

自身 CPU %

自身 CPU

CPU 总 %

CPU 总计

CPU 时间平均值

调用次数

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

自身 CPU 时间总计: 7.262s

使用 bfloat16 快速数学内核可以提高大约 2x (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) #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 默认配置运行时,您应该会看到以下分析器输出

名称

自身 CPU %

自身 CPU

CPU 总 %

CPU 总计

CPU 时间平均值

调用次数

aten::addmm

95.51%

5.821s

97.04%

5.914s

19.713ms

300

aten::clamp_min

2.33%

142.244ms

2.33%

142.244ms

711.220us

200

aten::copy

1.51%

92.322ms

1.51%

92.322ms

307.740us

300

mymodel_inference

0.45%

27.713ms

100.00%

6.094s

6.094s

1

aten::linear

0.04%

2.495ms

97.16%

5.921s

19.736ms

300

aten::t

0.03%

2.131ms

0.07%

4.441ms

14.803us

300

aten::relu

0.03%

1.942ms

2.37%

144.186ms

720.930us

200

自身 CPU 时间总计: 6.094s

以下是启用 MKLDNN 快速数学模式时的分析器输出

$ export DNNL_DEFAULT_FPMATH_MODE=BF16

名称

自身 CPU %

自身 CPU

CPU 总 %

CPU 总计

CPU 时间平均值

调用次数

aten::addmm

93.31%

3.848s

95.66%

3.944s

13.148ms

300

aten::clamp_min

3.43%

141.309ms

3.43%

141.309ms

706.545us

200

aten::copy

2.33%

95.916ms

2.33%

95.916ms

319.720us

300

mymodel_inference

0.67%

27.431ms

100.00%

4.123s

4.123s

1

aten::linear

0.06%

2.471ms

95.83%

3.951s

13.170ms

300

aten::t

0.05%

2.027ms

0.10%

4.243ms

14.143us

300

aten::relu

0.05%

1.928ms

3.47%

143.237ms

716.185us

200

自身 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 后端的分析器输出

名称

自身 CPU %

自身 CPU

CPU 总 %

CPU 总计

CPU 时间平均值

调用次数

aten::addmm

96.25%

1.958s

97.51%

1.984s

6.612ms

300

aten::clamp_min

1.28%

26.124ms

1.28%

26.124ms

130.620us

200

aten::copy

1.23%

24.951ms

1.23%

24.951ms

83.170us

300

mymodel_inference

0.86%

17.423ms

100.00%

2.034s

2.034s

1

aten::linear

0.08%

1.691ms

97.74%

1.988s

6.628ms

300

aten::t

0.07%

1.520ms

0.14%

2.945ms

9.817us

300

aten::relu

0.06%

1.258ms

1.35%

27.382ms

136.910us

200

自身 CPU 时间总计: 2.034s

如上所示,与默认的 MKLDNN 后端配置相比,切换到 OpenBLAS 将性能提高了一倍 (2.034s vs 4.123s)。这对于更小的批次维度也变得很重要,例如,对于批次维度为 10 的情况

X = torch.rand(10, 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"))

以下是使用 MKLDNN 快速数学模式的分析器输出

名称

自身 CPU %

自身 CPU

CPU 总 %

CPU 总计

CPU 时间平均值

调用次数

aten::addmm

87.81%

3.613s

91.90%

3.781s

12.604ms

300

aten::clamp_min

7.18%

295.437ms

7.18%

295.437ms

1.477ms

200

aten::copy

4.07%

167.516ms

4.07%

167.516ms

558.387us

300

mymodel_inference

0.67%

27.708ms

100.00%

4.115s

4.115s

1

aten::linear

0.06%

2.499ms

92.06%

3.788s

12.627ms

300

aten::t

0.05%

1.982ms

0.11%

4.385ms

14.617us

300

aten::relu

0.05%

1.932ms

7.23%

297.369ms

1.487ms

200

自身 CPU 时间总计: 4.115s

以下是使用 OpenBLAS 后端的分析器输出

$ export TORCH_MKLDNN_MATMUL_MIN_DIM=64

名称

自身 CPU %

自身 CPU

CPU 总 %

CPU 总计

CPU 时间平均值

调用次数

aten::addmm

92.66%

1.179s

95.23%

1.211s

4.038ms

300

aten::clamp_min

2.83%

36.060ms

2.83%

36.060ms

180.300us

200

aten::copy

2.52%

32.013ms

2.52%

32.013ms

106.710us

300

mymodel_inference

1.38%

17.521ms

100.00%

1.272s

1.272s

1

aten::linear

0.14%

1.750ms

95.60%

1.216s

4.054ms

300

aten::t

0.12%

1.475ms

0.24%

3.033ms

10.110us

300

aten::relu

0.10%

1.285ms

2.94%

37.345ms

186.725us

200

自身 CPU 时间总计: 1.272s

在这里,我们通过适当地调整后端阈值观察到 3.2x (1.272s vs 4.115s) 的性能提升。

使用 Linux 透明大页 (THP) 优化内存分配开销

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

$ export THP_MEM_ALLOC_ENABLE=1

对于批次维度为 256 且启用 MKLDNN 快速数学模式的情况

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"))

以下是启用 THP 内存分配的分析器输出

名称

自身 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 快速数学模式的基础上额外提高了 1.08x 或 8% (6.697s vs 7.262s) 的性能。

结论

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

文档

访问 PyTorch 的全面开发者文档

查看文档

教程

获取针对初学者和高级开发人员的深入教程

查看教程

资源

查找开发资源并获得问题的解答

查看资源