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

(Beta) 基于 AWS Graviton 处理器的 PyTorch 推理性能优化

Created On: Jan 24, 2024 | Last Updated: Jan 24, 2024 | Last Verified: Nov 05, 2024

作者: Sunita Nadampalli

AWS Graviton 是 AWS 设计的一系列基于 ARM 的处理器。AWS Graviton3 处理器专为机器学习(ML)工作负载优化,包括支持 bfloat16、可扩展矢量扩展(SVE)和比 Graviton2 更高的单指令多数据(SIMD)带宽。

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

在本教程中,我们将讨论如何在 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])

我们的网络功能已验证完成。接下来我们将进行性能分析。让我们检查两个不同的场景:小批量和大批量尺寸。

场景一: 较大的批量尺寸,例如 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 计算库 (ACL) 提供了针对 AWS Graviton 处理器优化的 bfloat16 通用矩阵乘法(GEMM)内核,并从 PyTorch 2.0 开始通过 MKLDNN 后端集成到 PyTorch 中。可以通过快速数学 GEMM 内核优化推理性能。快速数学模式默认未启用,因为这些内核以 bfloat16 精度而非 float 进行 GEMM 操作,因此会导致模型推理精度略有下降。然而,该精度下降在 torchbench 测试套件中 bfloat16 后端定义的余弦相似性阈值范围内,因此对大多数应用程序而言是可以接受的。要启用快速数学 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

这是约 2 (7.262s vs 16.201s) 的性能提升,得益于 bfloat16 快速数学内核。然而,让我们看看小批量尺寸场景。

场景二: 较小的批量尺寸,例如 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.47 倍 (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.958秒

97.51%

1.984秒

6.612毫秒

300

aten::clamp_min

1.28%

26.124毫秒

1.28%

26.124毫秒

130.620微秒

200

aten::copy

1.23%

24.951毫秒

1.23%

24.951毫秒

83.170微秒

300

mymodel_inference

0.86%

17.423毫秒

100.00%

2.034秒

2.034秒

1

aten::linear

0.08%

1.691毫秒

97.74%

1.988秒

6.628毫秒

300

aten::t

0.07%

1.520毫秒

0.14%

2.945毫秒

9.817微秒

300

aten::relu

0.06%

1.258毫秒

1.35%

27.382毫秒

136.910微秒

200

自CPU时间总计: 2.034秒

如上所示,与默认的MKLDNN后端配置相比,切换到OpenBLAS使性能提高了一倍 (2.034秒 vs 4.123秒)。这种提升对更小的批次尺寸会更加显著,例如,批次尺寸为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.613秒

91.90%

3.781秒

12.604毫秒

300

aten::clamp_min

7.18%

295.437毫秒

7.18%

295.437毫秒

1.477毫秒

200

aten::copy

4.07%

167.516毫秒

4.07%

167.516毫秒

558.387微秒

300

mymodel_inference

0.67%

27.708毫秒

100.00%

4.115秒

4.115秒

1

aten::linear

0.06%

2.499毫秒

92.06%

3.788秒

12.627毫秒

300

aten::t

0.05%

1.982毫秒

0.11%

4.385毫秒

14.617微秒

300

aten::relu

0.05%

1.932毫秒

7.23%

297.369毫秒

1.487毫秒

200

自CPU时间总计: 4.115秒

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

$ export TORCH_MKLDNN_MATMUL_MIN_DIM=64

名称

自身 CPU %

自身 CPU 时间

总 CPU %

总 CPU 时间

平均 CPU 时间

调用次数

aten::addmm

92.66%

1.179秒

95.23%

1.211秒

4.038毫秒

300

aten::clamp_min

2.83%

36.060毫秒

2.83%

36.060毫秒

180.300微秒

200

aten::copy

2.52%

32.013毫秒

2.52%

32.013毫秒

106.710微秒

300

mymodel_inference

1.38%

17.521毫秒

100.00%

1.272秒

1.272秒

1

aten::linear

0.14%

1.750毫秒

95.60%

1.216秒

4.054毫秒

300

aten::t

0.12%

1.475毫秒

0.24%

3.033毫秒

10.110微秒

300

aten::relu

0.10%

1.285毫秒

2.94%

37.345毫秒

186.725微秒

200

自CPU时间总计: 1.272秒

通过适当地调整后端阈值,我们观察到**3.2倍(1.272秒 vs 4.115秒)**的性能提升。

使用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.115秒

94.39%

6.321秒

21.069毫秒

300

aten::clamp_min

4.82%

322.568毫秒

4.82%

322.568毫秒

1.613毫秒

200

aten::copy

3.06%

204.602毫秒

3.06%

204.602毫秒

682.007微秒

300

mymodel_inference

0.61%

40.777毫秒

100.00%

6.697秒

6.697秒

1

aten::linear

0.05%

3.082毫秒

94.51%

6.329秒

21.097毫秒

300

aten::relu

0.04%

2.547毫秒

4.85%

325.115毫秒

1.626毫秒

200

自CPU时间总计: 6.697秒

在已经优化的MKLDNN快速数学模式基础上,这又是一项**1.08倍或8%(6.697秒 vs 7.262秒)**的改进。

结论

在本教程中,我们通过介绍AWS Graviton3实例上的PyTorch推理的基本用法、演示快速数学核的加速、对比不同批次尺寸的不同后端以及如何通过Linux透明巨页优化张量内存分配延迟,全面探讨了PyTorch推理。推荐对于更大的张量形状使用Bfloat16快速数学模式和THP内存分配的MKLDNN后端,而对于较小的张量形状使用OpenBLAS后端。希望您能尝试一下!

文档

访问 PyTorch 的详细开发者文档

查看文档

教程

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

查看教程

资源

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

查看资源