3、AI算子分类与特性:卷积(Conv)系列、矩阵乘(GEMM)系列、激活函数(ReLU/Sigmoid)系列、归一化(BatchNorm/LayerNorm)系列
好,咱们今天聊聊AI算子的分类。说实话,这话题看着基础,但里面坑不少。我当年刚接触芯片映射时,就吃过“算子特性没吃透”的亏——明明硬件算力够,跑起来却慢得离谱。后来才明白,不同算子的计算模式、数据流特征完全不同,映射策略也得跟着变。
我把AI算子分成四大类:卷积、矩阵乘、激活函数、归一化。每一类都有它的脾气。咱们一个一个看。
3.1 卷积(Conv)系列:计算密集型的“老大哥”
卷积是CNN的绝对核心。你想想看,一张224x224的图片,经过几十层卷积,计算量能到几GFLOPs。为什么会这么重?因为它是典型的“计算密集型”算子。
我个人习惯把卷积拆成三个维度看:
- 输入特征图:H x W x C(高、宽、通道数)
- 卷积核:K x K x C x M(核大小、输入通道、输出通道)
- 输出特征图:H' x W' x M
这里有个关键点:数据复用。卷积核在输入图上滑动时,同一个输入像素会被多个卷积核重复使用。我在项目中遇到过,如果芯片的缓存设计不合理,这种复用反而会变成瓶颈——数据反复从DDR搬进搬出,功耗直接飙升。
映射要点:卷积映射到芯片上,核心是“循环展开”。通常把输出通道M作为最外层循环,输入通道C作为中间层,空间维度H/W作为最内层。这样能最大化数据局部性。
举个例子,一个3x3卷积,步长为1,padding为1。计算量大约是:
计算量 ≈ H' * W' * M * C * K * K
≈ 224 * 224 * 64 * 3 * 3 * 3
≈ 86.7 MFLOPs
嗯,这里要注意:这只是单层。实际网络里几十层堆起来,计算量轻松上G。
3.2 矩阵乘(GEMM)系列:Transformer的“心脏”
矩阵乘,说白了就是线性代数里的矩阵乘法。但它在AI里地位特殊——Transformer全靠它撑场面。你想想看,自注意力机制里的Q、K、V计算,本质上全是矩阵乘。
GEMM的特性是:计算密度极高,但数据复用性差。为什么?因为矩阵乘的每个输出元素,都需要输入矩阵的一整行和一整列。数据访问模式是“行-列”交叉,对缓存很不友好。
我建议把GEMM映射到芯片时,重点关注分块(tiling)策略。比如把大矩阵切成小块,让每个小块的数据能塞进片上缓存。我曾经踩过一个坑:没做分块,直接拿大矩阵去算,结果缓存命中率不到30%,性能直接腰斩。
| 矩阵大小 | 计算量(FLOPs) | 数据量(Bytes) | 计算密度(FLOPs/Byte) |
|---|---|---|---|
| 1024x1024 | 2.1G | 8M | 262 |
| 4096x4096 | 137G | 128M | 1070 |
你看,矩阵越大,计算密度越高。但数据量也大得吓人。所以映射时,内存带宽往往是瓶颈,而不是算力。
避坑指南:我曾经在映射BERT模型时,发现GEMM占了80%的耗时。后来把矩阵分块从64x64改成128x128,性能提升了15%。分块大小不是越大越好,得根据芯片缓存大小来调。
3.3 激活函数(ReLU/Sigmoid)系列:计算轻量,但“暗藏杀机”
激活函数,说白了就是给神经网络加非线性。ReLU最简单:y = max(0, x)。Sigmoid复杂点:y = 1 / (1 + e^{-x})。
你可能会想:这玩意儿计算量这么小,有啥好优化的?嗯,这里要注意:激活函数虽然计算轻,但数据流模式特殊——它是逐元素操作,没有数据复用。每个输入只产生一个输出,计算和访存的比例是1:1。
我个人的经验是:激活函数映射到芯片上,关键是利用向量化指令。比如ReLU,可以用一条SIMD指令同时处理16个元素。Sigmoid稍微麻烦点,需要查表或近似计算。
举个例子,ReLU的映射代码(伪代码):
for i in range(N):
y[i] = (x[i] > 0) ? x[i] : 0
这代码看着简单,但如果你用标量指令一个一个算,效率极低。我建议用向量化版本:
for i in range(0, N, 16):
vec_x = load(x[i:i+16])
vec_y = max(vec_x, 0)
store(y[i:i+16], vec_y)
你看,一次处理16个元素,效率直接提升16倍。当然,前提是芯片支持SIMD。
注意:Sigmoid的指数运算在硬件上很贵。我见过一些芯片用分段线性近似来加速,精度损失在1%以内,但速度能快5倍。如果你的应用对精度不敏感,可以试试。
3.4 归一化(BatchNorm/LayerNorm)系列:训练和推理的“双面人”
归一化,说白了就是让数据分布更稳定。BatchNorm在CNN里用得多,LayerNorm在Transformer里用得多。它们的计算模式类似:先算均值和方差,再对每个元素做归一化。
BatchNorm的计算公式:
y = (x - mean) / sqrt(var + eps) * gamma + beta
这里有个关键点:训练和推理的行为完全不同。训练时,mean和var是从当前batch算的;推理时,用的是训练时累积的全局mean和var。我刚开始做芯片映射时,没注意这个区别,结果推理时结果全错——因为用了训练时的动态mean。
LayerNorm的计算类似,但它是沿着特征维度归一化,而不是batch维度。在Transformer里,LayerNorm是每个token独立做的。
映射要点:归一化的计算量不大,但数据依赖性强。你得先算完mean,才能算var,最后才能算归一化。这导致流水线很难做。我建议把mean和var的计算合并到一个pass里,减少数据搬运。
举个例子,BatchNorm的映射流程:
- 加载输入数据x(H x W x C)
- 计算每个通道的mean(C个值)
- 计算每个通道的var(C个值)
- 加载gamma和beta(C个值)
- 对每个元素做归一化
你看,步骤2和3都需要遍历整个输入数据。如果芯片的缓存不够大,数据得反复从DDR搬,效率很低。我建议把步骤2和3合并:遍历一次数据,同时累加mean和var的中间结果。
避坑指南:我曾经在映射LayerNorm时,发现计算var需要先知道mean,导致流水线停顿。后来用了“在线更新”的方法——边遍历数据边更新mean和var的估计值,虽然精度略有损失,但速度提升了30%。
3.5 知识体系总览
好了,四大类算子都聊完了。我画了一张图,帮你理清它们的关系:
这张图把四大类算子的核心特性和映射策略都串起来了。你想想看,卷积和矩阵乘都是计算密集型,但数据复用模式完全不同;激活函数和归一化计算量小,但数据流模式各有特点。映射时,得根据这些特性“对症下药”。
好了,这一章就到这儿。记住:算子特性决定映射策略,没有万能药方。多动手调参,多踩坑,慢慢就有感觉了。