融合策略基础:水平融合与垂直融合
说到算子融合,我最早接触这个概念是在做移动端推理优化的时候。那时候模型跑在手机上,又慢又烫,老板天天催。后来我发现,很多性能问题其实就出在「算子之间来回折腾」上。说白了,融合就是把这些来回折腾的步骤合并起来,减少中间环节的开销。
今天咱们聊聊融合策略里最基础的两个方向:水平融合和垂直融合。这两个概念,你搞懂了,后面再聊更复杂的融合策略就轻松多了。
水平融合:把「兄弟」算子合并
水平融合,我习惯叫它「兄弟融合」。什么意思呢?就是多个独立的、没有数据依赖关系的算子,它们可以并行执行,但为了减少调度开销,我们干脆把它们合并成一个。
举个例子,你想想看,一个模型里经常会有多个并行的卷积层,比如 Inception 结构。每个卷积各自算各自的,最后再拼起来。如果不做融合,GPU 要启动好几次 kernel,每次启动都有固定开销。水平融合就是把这些并行的卷积合并成一个大的 kernel 来执行。
核心收益:
- 减少 kernel 启动次数(这个开销在 GPU 上挺明显的)
- 提高计算密度,让硬件吃饱
- 减少中间结果的读写
我在项目中遇到过这样一个场景:一个检测模型里有 4 个并行的 3x3 卷积,输入都是同一个 feature map。如果不融合,每个卷积都要单独读一遍输入,写一遍输出。融合之后,输入只读一次,4 个卷积同时算,输出一次性写回。显存带宽的压力一下子就下来了。
垂直融合:把「上下游」算子合并
垂直融合,就是「上下游融合」。把一连串串行执行的算子合并成一个。比如 Conv + BN + ReLU,这是最经典的垂直融合案例。
为什么会这样?因为 Conv 的输出紧接着给 BN,BN 的输出紧接着给 ReLU。如果不融合,Conv 算完要写回显存,BN 读出来算完再写回,ReLU 再读再写。这一来一回,显存带宽全浪费在搬运数据上了。
垂直融合的做法,就是把 Conv 的计算、BN 的缩放、ReLU 的截断,全部塞到一个 kernel 里。算完 Conv 的某个输出元素,直接做 BN 和 ReLU,然后才写回显存。
我的经验:垂直融合是收益最稳定、最不容易出错的融合方式。我建议你在做任何模型优化时,先把 Conv+BN+ReLU 这种经典组合给融合了,基本不会踩坑。
融合的粒度选择:粗还是细?
这里有个很实际的问题:融合到什么程度算合适?是只融合相邻的两三个算子,还是把一大串算子全塞进去?
我个人习惯把融合粒度分成三个级别:
| 粒度 | 典型场景 | 优点 | 缺点 |
|---|---|---|---|
| 细粒度 | Conv+BN、Conv+ReLU | 实现简单,调试容易 | 收益有限 |
| 中粒度 | Conv+BN+ReLU、Conv+Add+ReLU | 收益明显,稳定性好 | 需要处理中间数据对齐 |
| 粗粒度 | 整个 block 融合(如 ResNet 的 bottleneck) | 收益最大,减少全局调度 | 实现复杂,容易出错 |
嗯,这里要注意:不是粒度越粗越好。我曾经试过把一个 ResNet 的整个 bottleneck 全部融合成一个 kernel,结果寄存器压力太大,编译器直接 spill 了,性能反而下降了。你想想看,寄存器不够用的时候,数据被挤到 L1 cache 甚至显存里,那还不如不融合。
所以我的建议是:从细粒度开始,逐步扩大融合范围,每次都要做性能验证。别一上来就想搞个大新闻。
融合的约束条件:什么情况下不能融合?
融合不是万能的。有些情况下,强行融合反而会出问题。我总结了几条常见的约束条件:
约束一:数据依赖冲突
如果算子 A 的输出被多个下游算子使用,那 A 就不能随便和某个下游融合。因为融合后,A 的计算结果只流向了那个下游,其他下游就拿不到数据了。
举个例子,一个卷积的输出同时送给两个不同的全连接层。如果你把卷积和第一个全连接融合了,第二个全连接就收不到卷积的输出了。这种情况,要么不做融合,要么用更复杂的「多输出融合」技术。
约束二:计算形状不匹配
有些算子对数据的形状有特殊要求。比如 reshape、transpose 这类算子,它们改变了数据的排布方式。如果强行把 reshape 前后的算子融合,kernel 内部的索引计算会变得非常复杂,甚至无法实现。
我记得有一次,我想把 Conv + Reshape + FC 融合在一起。结果发现,Reshape 改变了数据的维度顺序,Conv 的输出是 NHWC 格式,FC 需要 NCHW 格式。如果要在 kernel 内部做这个转换,索引计算量直接翻倍,得不偿失。
约束三:算子语义不可合并
有些算子的语义决定了它们不能简单合并。比如 softmax,它需要全局的统计信息(所有元素的和)。如果你把 softmax 和前面的全连接层融合,那全连接层的每个输出元素都要等所有元素算完才能做 softmax,这反而破坏了计算的流水线。
说白了,融合的本质是「用计算换访存」。如果融合后计算量增加太多,或者破坏了原有的并行性,那就不值得做。
知识体系总览
下面这张图是我自己整理的融合策略知识体系,你可以对照着看:
这张图把咱们今天聊的内容串起来了。水平融合和垂直融合是两种基本策略,融合粒度决定了你做到什么程度,而约束条件告诉你哪些地方不能碰。四者结合起来,才能做出高效的融合方案。
想要更多资料,联系可可