4. 算子性能分析
各位同学,咱们今天聊聊算子性能分析。说白了,就是看看 Conv2d、MatMul、Softmax 这些核心算子在 NPU 上到底是怎么跑的,瓶颈又卡在哪里。
我个人习惯,拿到一个模型先不看网络结构,先看算子分布。为什么?因为算子是 NPU 的“粮食”,算子跑不快,模型推理就别想快。我在项目中遇到过不少次,模型精度调得挺好,一上 NPU 推理,延迟直接翻倍。查到最后,往往就是某个算子的实现没踩对点。
Conv2d:NPU 的“主食”,但容易消化不良
Conv2d 是 CNN 模型的绝对主力。在 NPU 上,它的计算模式其实很固定:
- 输入特征图:通常按 CHW 或 NHWC 布局
- 权重:按 OIHW 或 OHWI 布局
- 计算核心:im2col + GEMM,或者直接走 Winograd
嗯,这里要注意。NPU 和 GPU 不一样,NPU 的 MAC 阵列(乘累加单元)通常有固定的尺寸,比如 16x16 或 32x32。如果你的卷积参数和 MAC 阵列不匹配,就会产生“空洞”——说白了就是计算单元闲着没事干。
核心瓶颈:
- 数据搬运:Conv2d 的输入输出数据量巨大,NPU 的片上缓存(SRAM)有限,频繁的 DDR 访问是最大的瓶颈。我曾经调一个 3x3 卷积,发现计算只占了 20% 的时间,剩下 80% 都在等数据。
- MAC 利用率:当卷积的输入通道数或输出通道数不是 MAC 阵列尺寸的整数倍时,利用率会下降。比如 MAC 阵列是 16x16,你输入通道数是 17,那就得补 15 个 0,白白浪费资源。
- Winograd 的取舍:小卷积核(3x3)用 Winograd 能减少乘法次数,但会增加加法次数和中间结果的存储。我建议,如果 NPU 的乘法器比加法器快很多,Winograd 是首选;否则,老老实实走 im2col。
避坑指南:我曾经在某个项目里,把 Conv2d 的 padding 从 SAME 改成 VALID,结果推理速度反而慢了。为什么?因为 VALID 模式下输出尺寸变小,但 NPU 的 tile 切分策略没变,导致每个 tile 的计算量不均衡,反而增加了调度开销。所以,改参数之前,先看看 NPU 的 tile 策略。
MatMul:全连接层的灵魂,也是带宽的噩梦
MatMul(矩阵乘法)在 Transformer 和全连接层里随处可见。它的计算模式很简单:A x B = C。但 NPU 上跑起来,问题就来了。
你想想看,MatMul 的计算强度(ops/byte)其实很高,但为什么还是慢?因为 NPU 的 MAC 阵列虽然快,但数据喂不进去。说白了,就是内存带宽成了瓶颈。
| 矩阵尺寸 | 计算量 (FLOPs) | 数据量 (Bytes) | 计算强度 (FLOPs/Byte) |
|---|---|---|---|
| M=1024, K=1024, N=1024 | 2.1G | 12MB | 175 |
| M=64, K=4096, N=4096 | 2.1G | 66MB | 32 |
你看,同样 2.1G 的计算量,第二个矩阵的数据量是第一个的 5 倍多。计算强度越低,对带宽的要求越高。NPU 的带宽通常就几百 GB/s,一旦计算强度低于某个阈值(比如 50),性能就会从“计算受限”变成“带宽受限”。
我的建议:
- 对于小矩阵(比如 M < 128),尽量用 NPU 的向量单元而不是矩阵单元。因为矩阵单元启动开销大,小矩阵跑不满。
- 对于大矩阵,一定要做 tiling(分块)。把大矩阵切成小块,让每个块都能塞进片上缓存。我习惯把 tile 尺寸设成 MAC 阵列尺寸的整数倍,比如 64x64 或 128x128。
- 注意数据布局。NPU 通常喜欢 row-major 或 column-major 的连续布局。如果输入数据是稀疏的,先做重排,否则带宽利用率会很低。
我记得有一次,一个客户说他们的 MatMul 在 NPU 上跑得比 CPU 还慢。我一看代码,好家伙,矩阵尺寸是 1023x1023,不是 1024。NPU 的 MAC 阵列是 32x32,1023 除以 32 余 31,导致最后一个 tile 只有 31x31,利用率不到 50%。改成 1024 后,速度直接翻倍。
Softmax:小算子,大麻烦
Softmax 看起来简单:求指数、求和、归一化。但在 NPU 上,它是个典型的“带宽敏感型”算子。
为什么?因为 Softmax 的计算量很小,但需要访问所有输入数据。比如一个 1024 维的向量,Softmax 只需要做 1024 次指数运算和 1 次求和,但数据量是 4KB(FP32)。计算强度只有 0.25 FLOPs/Byte,低得可怜。
说白了,Softmax 的性能瓶颈不在计算,而在数据搬运。NPU 的 MAC 阵列再快,也帮不上忙,因为 Softmax 没有矩阵乘法,只有逐元素运算和规约运算。
注意事项:
- Softmax 的指数运算在 NPU 上通常用查找表(LUT)或多项式近似实现。精度和速度需要权衡。我建议,如果模型对精度要求高(比如分类任务),用 LUT;如果对速度要求高(比如检测任务),用多项式近似。
- 求和操作(reduce sum)在 NPU 上可能不是硬件原生支持的。有些 NPU 需要手动实现树形求和,或者用向量单元逐次累加。这时候,向量化程度越高越好。
- Softmax 的数值稳定性:减去最大值再求指数。这个步骤在 NPU 上可能增加额外的数据搬运。我习惯把减最大值和求指数合并成一个 kernel,减少一次数据读写。
我曾经在调一个 BERT 模型时,发现 Softmax 占了整个推理时间的 15%。一开始我以为是计算慢,后来 profiling 一看,是数据在 DDR 和 SRAM 之间来回搬了 3 次。优化后,我把 Softmax 的输入输出都放在 SRAM 里,一次搞定,时间降到了 5%。
知识体系总览
下面这张图,是我对本章核心逻辑的总结。你可以把它当作一个“算子性能分析地图”。
这张图把 Conv2d、MatMul、Softmax 的计算模式和核心瓶颈都串起来了。你可以看到,三个算子的瓶颈虽然各有侧重,但归根结底,都是“计算 vs 带宽”的博弈。Conv2d 和 MatMul 在计算强度高时是计算受限,低时是带宽受限;Softmax 则几乎永远是带宽受限。