3. 深度学习算子分类:从卷积到归一化

好,咱们今天聊聊算子的分类。说实话,这章内容看着基础,但我在实际做算子映射的时候,发现很多人栽就栽在这些基础概念上。你想想看,连算子长什么样都没搞清楚,怎么给它安排合适的硬件资源?

深度学习模型,说白了就是一堆算子的组合。我习惯把它们分成五大类:卷积、池化、全连接、激活函数、归一化。每一类都有自己的脾气秉性,映射到芯片上时,考虑的点也完全不同。

3.1 卷积(Conv2D)—— 算力大户

卷积是咱们的老朋友了。做AI芯片的人,一半以上的精力都在伺候它。为什么?因为它吃算力吃得最狠。

一个典型的 Conv2D 算子,输入是 (N, C, H, W),卷积核是 (K, C, R, S),输出是 (N, K, H_out, W_out)。计算量有多大?我算给你看:

计算量 ≈ N × K × H_out × W_out × C × R × S

嗯,这里要注意,R 和 S 通常是 3×3 或 5×5。一旦输入通道 C 和输出通道 K 上了几百,这个乘法的规模就非常恐怖了。

核心特点:

  • 计算密集:MAC(乘加运算)占比极高
  • 数据复用:同一个卷积核会在空间上滑动,输入特征图会被反复读取
  • 并行潜力大:不同输出通道、不同空间位置之间没有数据依赖

我在项目中遇到过一个问题:某个团队把 Conv2D 直接映射到通用计算核上,结果发现带宽根本跟不上。为什么?因为卷积的访存模式是「不规则滑动」,通用缓存结构很难适配。后来我们专门设计了行缓存(Line Buffer)结构,才把性能提上来。

我的建议:做卷积映射时,优先考虑数据复用。输入特征图尽量留在片上,别频繁搬进搬出。我曾经见过一个方案,因为没做数据复用,90%的时间都在等数据,算力利用率不到10%。

3.2 池化(Pooling)—— 降维利器

池化算子,说白了就是「下采样」。常见的两种:最大池化(MaxPool)和平均池化(AvgPool)。

它的计算逻辑很简单:在一个窗口内取最大值或平均值。比如 2×2 的最大池化,步长为2,输出尺寸直接减半。

// 伪代码示例:2x2 MaxPool
for (h = 0; h < H_out; h++) {
    for (w = 0; w < W_out; w++) {
        output[h][w] = max(input[2*h][2*w], input[2*h][2*w+1],
                           input[2*h+1][2*w], input[2*h+1][2*w+1]);
    }
}

池化算子的计算量其实不大,但它有个特点:没有可学习的参数。这意味着它不需要反向传播时更新权重,前向推理时也省去了权重加载的麻烦。

我个人习惯把池化看作「数据整理工」。它不改变通道数,只改变空间尺寸。映射到芯片上时,我建议用专用的硬件单元来做,因为逻辑太简单了,用通用计算核反而浪费。

避坑指南:我曾经在某个项目中,把池化算子用通用ALU去模拟,结果发现延迟反而比卷积还高。后来一查,是因为池化的访存模式是「非对齐」的,通用ALU的缓存命中率极低。所以,池化最好用专用硬件,或者至少做访存优化。

3.3 全连接(FC)—— 参数炸弹

全连接层,也叫线性层。它的数学形式很简单:y = Wx + b。但问题在于,W 的尺寸通常非常大。

举个例子:输入是 1024 维,输出是 512 维,那权重矩阵就是 1024×512 = 524288 个参数。如果模型有多个 FC 层,参数量轻松上千万。

算子 计算特点 存储特点 映射难点
Conv2D 计算密集,数据复用 权重共享,参数量适中 访存模式不规则
Pooling 计算简单,无参数 无需存储权重 访存对齐问题
FC 计算密集,矩阵乘法 参数量巨大 带宽瓶颈

全连接层的映射,核心挑战是带宽。你想想看,每次推理都要把整个权重矩阵从DDR搬到片上,如果带宽不够,计算单元就得干等着。

我记得有一次做边缘端芯片,模型里有个 2048×1024 的 FC 层。按理论算力,处理这一层只需要 0.1ms,但实际跑了 2ms。为什么?因为权重加载花了 1.9ms。后来我们做了权重压缩和预取,才把时间降下来。

我的建议:如果芯片的片上存储有限,尽量把 FC 层的权重做量化(比如 INT8),或者用稀疏化技术。别让带宽成为瓶颈。

3.4 激活函数(ReLU / Sigmoid)—— 非线性调味剂

激活函数的作用,就是给模型引入非线性。没有它,再多层也只是线性变换,表达能力有限。

常见的激活函数有:

  • ReLU:f(x) = max(0, x)。计算极简单,一个比较器就搞定。
  • Sigmoid:f(x) = 1 / (1 + e^{-x})。计算复杂,涉及指数和除法。
  • Tanh:类似 Sigmoid,输出范围 [-1, 1]。

ReLU 是现在用得最多的。为什么?因为它计算快,而且能缓解梯度消失问题。我在做芯片设计时,ReLU 几乎不占用什么资源,一个比较器加一个多路选择器就搞定了。

Sigmoid 就麻烦多了。指数运算在硬件上很贵,通常需要用查找表(LUT)或者多项式逼近来实现。我建议,除非模型必须用 Sigmoid(比如二分类的输出层),否则尽量用 ReLU 或其变体(Leaky ReLU、PReLU)替代。

映射要点:

  • ReLU:用比较器实现,延迟极低,可内联到卷积或FC的计算流水线中
  • Sigmoid/Tanh:用查找表或CORDIC算法实现,注意精度和面积的权衡

3.5 归一化(BatchNorm / LayerNorm)—— 训练稳定器

归一化算子,很多人觉得它不重要。其实不然。没有归一化,深层网络很难训练。

BatchNorm:对每个通道,在 batch 维度上做归一化。训练时维护均值和方差,推理时直接用固定的统计量。

LayerNorm:对每个样本,在特征维度上做归一化。Transformer 里用得很多。

归一化的计算步骤:

y = gamma * (x - mean) / sqrt(var + epsilon) + beta

看起来简单,但涉及除法、开方、乘加。在硬件上,除法和开方都是比较贵的操作。我一般建议用查找表或者近似算法来实现倒数开方(rsqrt)。

我在项目中遇到过一个问题:BatchNorm 在推理时,如果 batch size 很小(比如 1),统计量会很不稳定。后来我们改用了 GroupNorm,效果好了很多。

避坑指南:我曾经把 BatchNorm 和卷积融合在一起做,结果发现精度下降了。为什么?因为卷积的权重更新后,BatchNorm 的统计量没有及时更新。所以,融合时要小心,最好在训练时就做联合优化。

3.6 知识体系总览

下面这张图,是我自己总结的算子分类和映射要点。你可以把它当作一个快速参考。

深度学习算子分类与映射要点 卷积 Conv2D 池化 Pooling 全连接 FC 激活函数 归一化 映射核心关注点 • 卷积:数据复用 > 计算并行 > 访存优化 • 池化:专用硬件 > 访存对齐 > 延迟优化 • 全连接:带宽优化 > 权重压缩 > 量化 • 激活函数:ReLU用比较器,Sigmoid用查找表 • 归一化:融合到前层,避免单独计算

这张图里,我把每个算子的核心关注点都列出来了。你仔细看会发现,访存优化是贯穿所有算子的主线。不管你做什么算子映射,先把数据搬移的问题想清楚,性能就成功了一半。

好了,这一章的内容就到这里。记住,算子分类不是死记硬背,而是要理解每个算子的「性格」——它吃算力还是吃带宽?它能不能并行?它有没有参数?想清楚这些,映射方案自然就有了。

想要更多资料,联系可可