算子分类与特性:访存密集型 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