常见融合模式:横向融合、纵向融合、交叉融合

算子融合这事儿,说白了就是把多个小算子合并成一个大算子。为什么要这么干?因为每个算子执行时都有启动开销、内存读写开销。你想想看,十个算子各跑一遍,和两个算子跑一遍,哪个快?

我个人习惯把融合模式分成三类:横向、纵向、交叉。这三类各有各的适用场景,也各有各的坑。今天咱们一个一个聊。

横向融合:把兄弟算子合并

横向融合,也叫同层融合。它处理的是同一计算图中,没有依赖关系的算子。

举个例子,一个输入数据同时要做 ReLU、Sigmoid、Tanh 三种激活。这三个激活函数互不依赖,完全可以并行计算。但 GPU 上每个 kernel 启动都有开销,分开跑就是三次启动、三次内存访问。

我做过一个项目,模型里有十几个并行的 BatchNorm 分支。一开始每个分支单独写一个 kernel,跑一次推理要 2.3 毫秒。后来我把它们横向融合成一个 kernel,一次启动搞定,时间直接降到 0.8 毫秒。

横向融合的核心思路:把多个独立的、同类型的算子合并成一个 kernel,减少启动次数和内存访问。

横向融合的典型场景:

  • 多个激活函数并行(ReLU + Sigmoid + Tanh)
  • 多个 BatchNorm 分支
  • 多个独立的卷积层
  • 多个 Element-wise 操作

小技巧:横向融合时,要注意每个算子的输出形状是否一致。如果不一致,需要做 padding 或 reshape。我一般会先检查 shape,再决定是否融合。

纵向融合:把父子算子合并

纵向融合,也叫流水线融合。它处理的是有依赖关系的算子,比如 Conv + BN + ReLU 这种经典组合。

纵向融合的思路很简单:把前一个算子的输出直接喂给后一个算子,中间不写回显存。这样能省掉一次内存读写,而且能利用 GPU 的流水线特性。

我记得有一次优化一个 ResNet 模型,里面全是 Conv + BN + ReLU 的组合。每个组合都拆成三个 kernel 跑,显存带宽被浪费得厉害。我把它们纵向融合成一个 kernel,推理速度提升了 40%。

纵向融合的典型模式:

  • Conv + BN + ReLU
  • Conv + Add + ReLU
  • MatMul + Add + Softmax
  • LayerNorm + Add + Dropout

注意:纵向融合不是万能的。如果两个算子的计算量差距太大,融合后反而可能降低并行度。比如一个轻量级的 ReLU 和一个重量级的 Conv 融合,ReLU 部分可能拖慢 Conv 的执行。我曾经踩过这个坑,后来加了阈值判断才解决。

交叉融合:跨层跨分支的合并

交叉融合是最复杂的一种。它同时涉及横向和纵向的依赖关系,需要跨层、跨分支进行合并。

举个例子,Transformer 里的 Multi-Head Attention。它里面有多个并行的 Q、K、V 计算,每个计算又包含 MatMul + Scale + Softmax 等操作。这些操作既有横向的并行关系,又有纵向的依赖关系。

交叉融合的难点在于:

  1. 依赖关系复杂,容易出错
  2. 内存复用策略需要精心设计
  3. 调试难度大,出 bug 很难定位

我做过一个交叉融合的案例:把 BERT 模型里的 QKV 投影、Attention 计算、输出投影全部融合成一个 kernel。代码写了三天,调试花了一周。但效果确实好,推理速度提升了 2.1 倍。

交叉融合的适用场景:

  • Transformer 的 Attention 模块
  • ResNet 的残差块
  • Inception 的多分支结构
  • DenseNet 的密集连接

三种融合模式的对比

融合模式 依赖关系 复杂度 收益 典型场景
横向融合 无依赖 低 减少启动次数 并行激活、多分支 BN
纵向融合 有依赖 中 减少内存访问 Conv+BN+ReLU
交叉融合 混合依赖 高 综合收益最大 Attention、残差块

三种融合模式的知识体系

下面这张图展示了三种融合模式的关系和适用场景。我画这张图时参考了多个框架的实现,包括 TensorRT、TVM 和 XLA。

算子融合三种模式 横向融合 无依赖关系 多个独立算子合并 减少 kernel 启动次数 并行激活、多分支 BN 复杂度:低 纵向融合 有依赖关系 父子算子合并 减少内存访问 Conv+BN+ReLU 复杂度:中 交叉融合 混合依赖关系 跨层跨分支合并 综合收益最大 Attention、残差块 复杂度:高 融合策略选择建议 • 如果算子之间没有依赖 → 优先考虑横向融合 • 如果算子之间有流水线关系 → 优先考虑纵向融合 • 如果模型结构复杂、跨层跨分支 → 考虑交叉融合 • 实际项目中,三种融合模式往往混合使用 • 建议先用 profiler 分析瓶颈,再决定融合策略 —— 融合策略选择建议 ——

实战中的选择策略

实际项目中,这三种融合模式往往不是单独使用的。我一般会按以下步骤来:

  1. 先用 profiler 跑一遍,看看哪些算子耗时最多
  2. 分析算子之间的依赖关系,画出计算图
  3. 优先做纵向融合,因为收益最直接
  4. 再看横向融合,把并行的兄弟算子合并
  5. 最后考虑交叉融合,处理复杂结构

我的经验:不要一开始就想着做交叉融合。先做简单的纵向和横向融合,往往能解决 80% 的性能问题。交叉融合收益大,但风险也大,适合作为最后的优化手段。

嗯,三种融合模式就聊到这儿。每种模式都有它的用武之地,关键是要根据实际场景灵活选择。记住一点:融合不是目的,加速才是。

想要更多资料,联系可可