3、融合策略分类:水平融合、垂直融合、混合融合策略详解
算子融合这事儿,说白了就是「把多个小操作打包成一个」。但怎么打包?往哪个方向打包?这里头门道不少。
我刚开始接触融合时,以为就是把相邻的算子硬塞到一起。后来踩了坑才发现——融合策略分三种,每种适用场景完全不同。你选错了策略,性能可能不升反降。
今天咱们就把这三种策略掰开揉碎讲清楚。
3.1 水平融合:同层操作的「横向合并」
水平融合,也叫横向融合。它针对的是同一计算层级、处理逻辑相似的算子。
核心思想:把多个独立的、相同类型的操作合并成一个。
举个例子,你有一批数据,需要分别做 ReLU、Sigmoid、Tanh 三种激活。传统做法是三个算子串行执行。水平融合后,变成一个算子一次性处理完。
我在项目中遇到过这样一个案例:一个 Transformer 模型里,Q、K、V 三个矩阵分别做 LayerNorm。原本是三个独立的 Norm 算子,每个都要读一次内存、写一次内存。水平融合后,一次性读完三个矩阵,连续做完 Norm,再一次性写回。
你想想看,内存访问次数从 6 次降到了 2 次。这提速效果,立竿见影。
// 水平融合前:三个独立算子
y1 = relu(x1)
y2 = sigmoid(x2)
y3 = tanh(x3)
// 水平融合后:一个算子搞定
[y1, y2, y3] = fused_activations(x1, x2, x3)
3.2 垂直融合:流水线上的「纵向合并」
垂直融合,也叫纵向融合。它针对的是计算流水线上前后依赖的算子。
核心思想:把「生产者-消费者」关系的多个算子合并,减少中间结果的存储和传输。
这就像工厂流水线——原本每个工位做完活,要把半成品搬到仓库,下一个工位再去取。垂直融合后,两个工位直接对接,半成品「手递手」传递。
最经典的例子就是 Conv-BN-ReLU 融合。卷积做完,紧接着做 BatchNorm,然后做 ReLU。这三个算子天然就是前后依赖关系。
我曾经在一个部署项目里,把整个模型的 Conv-BN-ReLU 全部做了垂直融合。推理速度直接快了 30%。为什么?因为中间的特征图不需要写回显存了,全部在寄存器里完成。
// 垂直融合前:三步走
h = conv2d(x, w)
h = batch_norm(h)
y = relu(h)
// 垂直融合后:一步到位
y = fused_conv_bn_relu(x, w)
3.3 混合融合:水平 + 垂直的「组合拳」
混合融合,就是把水平融合和垂直融合结合起来用。说白了,先做垂直融合把流水线缩短,再做水平融合把同层操作合并。
为什么需要混合融合?因为实际模型里,算子关系往往是「既有前后依赖,又有并行分支」。
举个例子,一个残差块里:主路径是 Conv-BN-ReLU,分支路径是 Conv-BN。最后两个路径相加。这时候,你可以先对主路径做垂直融合(Conv-BN-ReLU 合并),对分支路径也做垂直融合(Conv-BN 合并)。然后,把两个路径的加法操作做水平融合。
我个人的习惯是:先画计算图,标出所有「生产者-消费者」关系,做垂直融合。然后看哪些同层操作可以合并,做水平融合。最后检查有没有冲突。
下面这张图,是我自己总结的融合策略选择流程,你可以参考一下:
3.4 三种策略的对比与选择
说了这么多,到底什么时候用哪种?我整理了一张表,你直接对照着看:
| 策略 | 适用场景 | 核心收益 | 典型算子组合 | 风险点 |
|---|---|---|---|---|
| 水平融合 | 同层、同类型、无依赖 | 减少 kernel launch 次数 | 多个 ReLU、多个 Add | 操作类型差异大时无效 |
| 垂直融合 | 前后依赖、流水线 | 减少中间结果读写 | Conv+BN+ReLU | 训练场景需保留中间结果 |
| 混合融合 | 复杂子图、残差结构 | 综合收益最大化 | 残差块、Inception 模块 | 融合策略冲突需手动调优 |
3.5 实战中的融合策略选择
最后,分享一个我自己的决策流程:
- 画计算图:把模型的计算图画出来,标出所有算子节点。
- 找依赖链:找出所有「生产者-消费者」关系的算子链,优先做垂直融合。
- 找同层节点:看有没有多个同类型、无依赖的算子,做水平融合。
- 检查冲突:确保融合后的算子不会破坏计算图的正确性。
- 性能验证:跑 benchmark,对比融合前后的延迟和吞吐。
嗯,这套流程我用了好几年,基本没出过大问题。你刚开始做融合时,也可以按这个步骤来。等经验多了,自然一眼就能看出哪些算子该合、哪些不该合。
说白了,融合策略没有银弹。水平融合、垂直融合、混合融合,都是工具。关键是你得知道什么时候用哪个工具。