Transformer模型部署之性能优化指南

一、前言

随着深度学习的快速发展,Transformer 算法在各类 AI 任务中都得到了广泛关注。然而,Transformer 的部署面临计算量大、延迟高等挑战,地平线计算芯片能够支撑 Transformer 算法的高效推理,可以成为用户在边缘平台部署 Transformer 算法的优先选择。本文将对 Transformer 性能优化方法做出详细说明。

二、性能优化策略

尽管地平线计算芯片和配套的算法工具链具备高效部署 Transformer 算法的能力,但由于 Transformer 结构的复杂性和特殊性,直接部署公版模型有时会不可避免地会遇到性能瓶颈,导致端侧推理速度较慢。这里介绍几种部署 Transformer 算法时常见的性能问题并提供优化策略。所有算子的耗时分析均基于模型编译时生成的 html 性能预估文件进行。

2.1 优化 Softmax 维度

以 DETR 算法为例,公版算法 17 个 Softmax 的总耗时约为 58ms,优化算法 17 个 Softmax 总耗时约为 42ms,有 16ms 的优化。

这些差异主要体现在 SelfAttention 部分,公版算法是对 1x8x1050x1 的 Shape 做 Softmax,而优化算法是对 8x25x42x1 的 Shape 做 Softmax。这个调整没有改变计算量,但是重排了 Softmax 的计算维度与批次结构,从而提高了并行度与缓存命中率。多个小 Softmax(42 维)比单个大 Softmax(1050 维)更容易并行化,并能让访存更加连续,减少访存延迟。多个 Softmax 同时分配到不同计算单元,使 BPU 计算资源被更高效地利用,从而有效降低计算耗时。

公版算法 Softmax(1x8x1050x1)的静态耗时预估:

​ ​

地平线优化算法 Softmax(8x25x42x1)的静态耗时预估:

Softmax 算子的量化策略为:将其替换为 6 个等效算子,再逐一对每个算子进行量化。

2.2 优化 BPU 硬件对齐耗时

这里还是以 DETR 算法为例,对于 encoder 中 layers0 的线性层计算,公版算法和地平线优化算法的耗时情况如下:

公版算法

地平线优化算法

可以看到,无论是公版算法还是优化算法,这两层结构的计算量都是相同的,均为 1.1GOPs。但由于输入张量的 Shape 不同,导致耗时差异明显。公版算法的 Shape 是 1x1050x1x2048 和 1x1050x256,受到 BPU 硬件对齐规则限制,第三维从 1 Padding 到 8,大量计算资源消耗在无效数据上,造成了严重的算力浪费。而优化算法的 Shape 是 1x25x42x2048 和 1x25x42x256,Padding 规则仅仅将第二维的 25 变成 26,第三维的 42 变成 48,相比公版 Shape 节约了大量的 BPU 计算资源。仅这一处优化就可降低约 3.6 毫秒耗时,整个模型有多处相似结构,累计的性能提升非常明显。

2.3 优化数据搬运耗时

数据搬运特指 Reshape/Transpose 这类不做数值计算,只做搬运操作的算子。对于 SwinT 算法,公版和优化版本在 Transformer 核心算子(如 Softmax,Layernorm,Matmul)的计算耗时方面表现相当,主要区别在于 Reshape 和 Transpose 的耗时差异,具体如下表所示:

这种优化手段需要用户通过算法设计,尽可能规避模型中出现耗时明显的 Transpose 和 Reshape 算子。但也不是所有的 Transpose 和 Reshape 都有大量耗时,需要结合 html 静态性能报告进一步分析排查。

如果用户选择使用 QAT 链路进行 Transformer 算法的量化部署,算法工具链还提供了更多了性能优化手段。

2.4 使用 QAT 算子避免数据搬运

在 Transformer 结构中,经常需要对 Matmul 的其中一个输入 Transpose 变换维度,这个 Transpose 往往会带来一定的性能开销。用户可以使用 QAT 封装的 Matmul 算子,该算子可以通过入参识别用户希望对哪个输入做维度变换,并在内部做高效处理,从而避免显式的 Transpose 操作。QAT 封装的 Matmul 算子使用示例如下:

Plain 复制代码
# 原本使用方式
k = k.transpose(-1, -2)
attention = torch.matmul(q, k)

# QAT使用方式
import horizon_plugin_pytorch.nn.quantized as quantized
self.matmul = quantized.FloatFunctional()
attention = self.matmul.matmul(q, k, x_trans=False, y_trans=True)

公版的 Layernorm 仅支持对最后若干维度做计算,如果要对中间维度做 Layernorm,需要先 Transpose。而使用 QAT 封装算子可以避免 Transpose,直接对中间维度做 Layernorm。具体使用方式如下:

Plain 复制代码
# 原本使用方式
# NCHW -> NHWC
x = x.permute(0, 2, 3, 1)
# NHWC 按通道 C 做归一化
self.layernorm = nn.LayerNorm((C))
x = self.layernorm(x)
# NHWC -> NCHW
x = x.permute(0, 3, 1, 2)

# QAT使用方式

2.5 使用四维算子代替三维算子

BPU 的硬件特性决定了其对四维数据的支持更加高效,在优化版本的 Transformer 算法中,大量使用了四维算子替换三维算子,以充分发挥 BPU 的计算特性。QAT 也提供了封装好的四维 Transformer 模块供用户使用,如 MultiHeadAttention,PatchMerging 等。

此处展示 QAT 四维算子 HorizonMultiHeadAttention 的部分定义,可在 GPU Docker 中访问/usr/local/lib/python3.10/dist-packageshat/models/base_modules/attention.py 查看完整代码:

Plain 复制代码
class HorizonMultiheadAttention(nn.Module):
    """modify torch.nn.MultiheadAttention to support quantization.

    Args:
        embed_dim: Total dimension of the model.
        num_heads: Number of parallel attention heads.
            Note that ``embed_dim`` will be split across ``num_heads``,
            i.e. each head will have dimension ``embed_dim // num_heads``.
        dropout: Dropout probability. Default: ``0.0`` (no dropout).
        bias: If specified, adds bias to input / output projection layers.

DETR 算法的 PatchMerging 结构也可以使用四维算子做替换,示例如下:

Plain 复制代码
# 原始的使用方式
class PatchMerging(nn.Module):
    def _init_(self, dim, norm_layer):
        super().__init_()
        self.dim = dim
        self.reduction = nn.Linear(4 * dim, 2 * dim, bias=False)
        self.norm = norm_layer(4 * dim)
        self.cat = quantized.FloatFunctional()
    def forward(self,x,H,W):
        B, L, C = x.shape

2.6 使用 Conv2d 代替 Vector 计算

这里 Vector 并不是指 std::vector 或者一维向量,而是指模型中可以并行处理的一批数据元素,例如一个通道内的像素值。Vector 计算通常指逐元素操作,如均值/求和/取最大值等,与之相对应的是 Tensor 计算。Transformer 算法中比较典型的 Vector 计算有 Elementwise、Reduce 等,模型中如果 Vector 计算占比较多,会影响 BPU 利用率,导致部署性能下降。

在一些情况下,可以使用 Conv2d 等效替换 Vector 操作(如 Mul/Reduce 等)。像是 Conv->ReduceSum->Conv 串接的结构,其中 ReduceSum 就可以替换成 Conv2d:比如 reduce on C 可以构建为 input channel = C,output channel = 1 的 Conv2d;reduce on H/W 可以构建为 kernel_h = H 或 kernel_w = W 的 Conv2d 等。使用 Conv2d 代替 Vector 计算,会提升算法的部署性能,同时不影响计算精度。

相关推荐
地平线开发者2 小时前
人在途中:从“编译失败”到“模型可落地”——CUDA 自定义算子
算法·自动驾驶
半个落月4 小时前
从递归到快速排序:用 JavaScript 把分治思想讲明白
javascript·算法·面试
小月土星5 小时前
JavaScript 快速排序:从 pivot、双指针到分治思想
javascript·算法·面试
小月土星5 小时前
JavaScript 递归入门:从 1 到 n 求和,再到数组扁平化
javascript·算法·面试
To_OC21 小时前
LC 1 两数之和:面试第一道必考题,暴力解法直接被面试官 pass
javascript·算法·leetcode
鱼鱼不愚与1 天前
《原来如此 | 第01期:为什么导航软件能预测红绿灯倒计时?》
算法
复杂网络1 天前
论最小 Agent 计算机的形态
算法
kisshyshy2 天前
🍦 雪糕、食堂、火车厢:三幅漫画吃透栈、队列与链表
javascript·算法