算子分类与特性:访存密集型 vs 计算密集型
做算子融合,第一步不是看代码,而是看算子的「脾气」。
我入行那会儿,有个老前辈跟我说过一句话,我一直记着:「你连算子是吃内存还是吃算力都分不清,就别谈融合了。」这话糙,理不糙。
算子的分类,说白了就两大维度:访存密集型和计算密集型。再往下细分,还有逐元素算子、归约算子、卷积算子这些具体类型。咱们一个一个聊。
1. 访存密集型 vs 计算密集型
先问个问题:一个算子执行的时候,瓶颈在哪?
如果大部分时间都花在等数据从内存搬过来,那就是访存密集型。如果数据一到,计算单元就满负荷跑,那就是计算密集型。
核心判断标准:计算时间 vs 访存时间的比值。比值远小于1,访存密集型;远大于1,计算密集型。
我举个例子。你想想看,一个简单的向量加法:C[i] = A[i] + B[i]。每个元素就做一次加法,但要从内存读两个数,写一个数。访存操作远多于计算操作。这就是典型的访存密集型。
反过来,一个7x7的卷积核,在256x256的特征图上做卷积。每个输出点要算49次乘加,数据复用率极高。计算量远大于访存量。这就是计算密集型。
| 算子类型 | 典型例子 | 瓶颈 | 融合策略 |
|---|---|---|---|
| 访存密集型 | 逐元素加、ReLU、Copy | 内存带宽 | 尽量合并访存,减少中间结果写回 |
| 计算密集型 | 卷积、矩阵乘 | 计算单元 | 尽量提升计算密度,减少数据搬运 |
我个人习惯:拿到一个模型,先跑一遍profile,看算子的算术强度(Arithmetic Intensity)。低于1的,我直接归类为访存密集型。高于10的,计算密集型。中间那档,得具体分析。
2. 逐元素算子
逐元素算子,说白了就是「一个萝卜一个坑」。每个输出元素只依赖于对应的一个或几个输入元素,跟周围邻居没关系。
典型的包括:ReLU、Sigmoid、加法、乘法、广播操作等。
特性:
- 计算逻辑简单,通常就几条指令
- 数据访问模式规律,一般是顺序访问
- 访存带宽是绝对瓶颈
- 非常适合融合——因为融合后能减少中间结果的写回
我记得有一次做BERT模型优化,里面有一连串的LayerNorm + Add + ReLU。如果每个算子单独做kernel launch,光启动开销就占了三成。我把它们融合成一个kernel,吞吐直接提升了40%。
我曾经踩过一个坑:逐元素算子融合时,要注意广播的维度。两个shape不完全匹配的张量做逐元素操作,融合后的索引计算很容易写错。一定要先做shape对齐,再写融合逻辑。
3. 归约算子
归约算子,是把一堆数据「浓缩」成一个或少数几个值。比如求和、求最大值、求均值。
特性:
- 计算过程有数据依赖——比如求和,必须等前面的部分和算完
- 访存模式是「多读少写」——读一大堆,写一点点
- 并行度受限——归约树结构决定了并行度会逐渐降低
归约算子融合时有个难点:中间结果没法直接复用。你想想看,一个Softmax里面既有求最大值,又有求和,还有逐元素除。如果分开做,要写回三次中间结果。融合做,就得在寄存器级别把最大值和部分和都留住。
避坑指南:我曾经把Softmax的max和sum分开写,结果发现性能还不如不融合。后来改成一次扫描同时算max和sum,才真正看到收益。归约融合的关键是「一次扫描,多结果输出」。
4. 卷积算子
卷积算子,是深度学习里的「重工业」。计算量大,数据复用率高,优化空间也最大。
特性:
- 计算密集型——算力瓶颈远大于访存瓶颈
- 数据复用模式多样——输入复用、权重复用、输出复用
- 存在多种实现方式——im2col、Winograd、FFT、直接卷积
卷积算子的融合,通常不是把两个卷积融合在一起(那样计算量会爆炸),而是把卷积前后的逐元素算子融合进去。比如 Conv + BN + ReLU 这种经典组合。
我做过一个实验:把ResNet50里的Conv + BN + ReLU全部融合,推理速度提升了1.8倍。为什么?因为融合后,卷积的输出直接留在寄存器里给BN用,BN的结果直接给ReLU用,全程不用写回显存。
| 融合模式 | 收益来源 | 实现难度 |
|---|---|---|
| Conv + BN | BN参数可以吸收到Conv的weight和bias中 | 低(数学等价变换) |
| Conv + ReLU | ReLU可以inline到卷积的写回阶段 | 中(需要改kernel代码) |
| Conv + BN + ReLU | 三者合一,减少两次中间结果写回 | 高(需要同时处理吸收和inline) |
我建议:做卷积融合时,优先考虑「吸收型融合」,比如Conv+BN。这种融合是数学上等价的,不会引入精度误差。其次是「inline型融合」,比如Conv+ReLU,需要改kernel但收益稳定。
知识体系总览
下面这张图,是我自己整理算子分类时画的。你可以把它当作一个「导航图」,后面讲到具体融合策略时,随时回来对照。
这张图把算子分类、特性、融合策略串在了一起。你仔细看就会发现,访存密集型算子的融合重点是减少访存次数,而计算密集型算子的融合重点是提升计算密度。方向完全不同。
嗯,这一章的内容就到这儿。记住一句话:分类是融合的前提,特性决定了策略。后面讲具体融合算法时,你会反复用到今天这些分类思路。
公众号:蓝海资料掘金营,微信deep3321