算子融合的执行时机
完整的时间线
模型加载阶段(一次) 运行时阶段(多次推理)
↓ ↓
┌─────────────────────┐ ┌─────────────┐
│ 1. 解析ONNX模型 │ │ 输入数据 │
│ 2. 构建IR Graph │ │ ↓ │
│ 3. 图优化(融合) │ ←── │ 执行融合算子 │
│ 4. 图分割(多硬件) │ │ ↓ │
│ 5. 编译成执行计划 │ │ 输出结果 │
└─────────────────────┘ └─────────────┘
关键点:
-
融合发生在模型加载时,只执行一次
-
运行时直接执行融合后的算子,不再重新融合
-
因此,融合的误差是固定的,不会随输入变化
OpenPPL 的算子融合功能主要通过以下优化器实现:
| 文件 | 功能说明 |
|---|---|
| act_reorder_for_conv_optimizer.cc | 激活函数重排序优化。将卷积后的激活函数(如ReLU)提前或调整顺序,以便与卷积核融合或减少中间内存开销 |
| constant_node_optimizer.cc | 常量折叠。将输入全为常量的节点在编译时预计算,替换为常量节点 |
| engine_graph_partitioner.cc | 引擎图分割器。根据硬件后端(如x86、CUDA)将计算图分割成多个子图,每个子图由对应引擎执行 |
| fuse_bn_optimizer.cc | Conv + BatchNorm 融合。将 BN 的参数融合到 Conv 的权重和偏置中,删除 BN 节点 |
| fuse_constant_optimizer.cc | Conv + Add/Mul 融合。将 Add 或 Mul 的常量值融合到 Conv 的 bias 中 |
| fuse_parallel_node_optimizer.cc | 并行节点融合。融合可以并行执行的无依赖节点 |
| fuse_shape_optimizer.cc | Shape 相关算子融合。将 Shape → Gather → Slice → Concat → Add/Div 等多个算子融合成一个 PPL.ShapeOperation 自定义算子 |
| identity_node_optimizer.cc | Identity 节点删除。Identity 算子(恒等映射)在推理时无实际作用,直接删除并重新连接输入输出 |
| skip_dropout_optimizer.cc | Dropout 删除。推理时 Dropout 无效,直接删除 |
这9个优化器可以分为几类:
| 优化器 | 实现方式 | 说明 |
|---|---|---|
| fuse_bn_optimizer.cc | 只改参数 | 将BN参数融合到Conv的weight/bias中,Conv节点类型不变 |
| fuse_constant_optimizer.cc | 只改参数 | 将Add/Mul的常量融合到Conv的bias中,Conv节点类型不变 |
| constant_node_optimizer.cc | 只改参数 | 常量折叠,预计算结果替换原节点,不产生新算子类型 |
| skip_dropout_optimizer.cc | 只改参数 | 删除Dropout节点,不产生新算子 |
| identity_node_optimizer.cc | 只改参数 | 删除Identity节点,不产生新算子 |
| act_reorder_for_conv_optimizer.cc | 只改参数 | 调整激活函数顺序,不产生新算子 |
| engine_graph_partitioner.cc | 非融合 | 图分割,不是算子融合 |
| fuse_parallel_node_optimizer.cc | 非融合 | 并行节点融合,实际是调度优化 |
| fuse_shape_optimizer.cc | 生成新算子 | 将Shape→Gather→Slice→Concat→Add/Div等多个算子融合成一个新的 自定义算子 |
在**fuse_shape_**optimizer.cc的****Optimize里
node->SetType(ir::Node::Type{"pmx", "Shape", 1});
实际看到的是pmx.Shape算子
一、卷积相关融合(最核心)
1. Conv + BN 融合
-
实现位置 :
FuseBNOptimizer -
融合方式:将BatchNorm的参数融合到Conv的权重和偏置中
-
官方记录 :2021年12月已支持
batchnorm+relu融合
2. Conv + Add/Mul 融合
-
实现位置 :
FuseConstantOptimizer -
融合方式:将Conv后面的Add或Mul常量节点融合到Conv的bias中
3. Conv + ReLU/ReLU6 融合
-
官方记录:x86后端所有卷积算法支持 ReLU/ReLU6 融合
-
融合方式:卷积计算后直接执行激活函数,避免中间结果回写内存
4. Conv + DepthwiseConv 融合
- 官方记录 :x86后端支持
Conv+DepthwiseConv融合,使用AVX512/FMA指令集优化
5. Conv + Concat 融合
- 修复记录:2022年12月修复了Conv+Concat融合时channel不能整除的计算问题
二、Shape相关融合(动态模型专用)
PPL.ShapeOperation 自定义算子
OpenPPL设计了专门的自定义算子 PPL.ShapeOperation 来处理Shape相关计算,支持融合以下算子 :
| 融合类型 | 具体算子 |
|---|---|
| 提取类 | Gather, Slice |
| 拼接类 | Concat |
| 数值计算类 | Add, Div, Mul, Sub |
融合示例(Shufflenet网络):
-
融合前:Shape → Gather → Div → Concat 等多个算子
-
融合后:单个
PPL.ShapeOperation算子,维护系数矩阵表示完整计算逻辑 -
效果:融合共计20个Shape相关算子
三、其他算子融合
1. Identity 算子融合
- 官方记录:支持 identity 算子的融合
2. QuickGelu 算子融合
- 官方记录:Onnxruntime后端已支持 QuickGelu 算子融合
3. Add/Sub/Mul/Div 就地计算(Inplace)
- 官方记录:x86后端支持 Add/Sub/Mul/Div 就地计算,减少内存分配
四、各后端支持汇总
| 融合类型 | x86 | CUDA | 说明 |
|---|---|---|---|
| Conv + BN | ✅ | ✅ | CUDA支持batchnorm+relu融合 |
| Conv + ReLU | ✅ | ✅ | x86所有卷积算法支持 |
| Conv + Add | ✅ | ✅ | FuseConstantOptimizer实现 |
| Conv + DepthwiseConv | ✅ | ❌ | AVX512/FMA指令集优化 |
| Shape融合 | ✅ | ✅ | PPL.ShapeOperation自定义算子 |
| Identity融合 | ✅ | ✅ | 2022年12月加入 |
| QuickGelu | ❌ | ✅ | Onnxruntime后端支持 |
算子融合在大多数情况下确实只是修改节点信息而不生成新算子类型,但在某些情况下也会生成全新的算子类型。
两种融合模式
模式1:修改节点信息(最常见)
这种模式不生成新算子类型,只是修改现有节点的属性或删除冗余节点。
典型例子:Conv + BN 融合
cpp
// 融合前
Conv节点:weight=W, bias=b
BN节点:gamma=γ, beta=β, mean=μ, var=σ²
// 融合后
Conv节点:weight=W', bias=b' ← 只修改了属性
BN节点:被删除
发生了什么:
-
没有创建新节点类型
-
Conv节点的类型仍然是
"Conv" -
只是更新了它的权重和偏置
-
删除了BN节点
模式2:生成新算子类型(少见但有)
这种模式会创建全新的节点类型,通常用于:
-
融合模式无法用现有算子表达
-
后端有专门优化的融合Kernel
典型例子:Conv + Add + Relu 融合(有专门优化时)
// 融合前
Conv → Add → Relu
// 融合后(如果有专门优化的Kernel)
FusedConvAddRelu ← 新类型
为什么需要新类型:
-
Conv + Add + Relu 融合后,数据流从
Conv→Add→Relu变成了一个端到端的计算 -
如果后端有专门的手工汇编优化版本(如CUDA的
cudnnConvolutionBiasActivationForward),就需要一个新的算子类型来调用它
为什么大多数融合不生成新算子
1. 数学等价性
很多融合(如Conv+BN)可以完全用修改参数来表达,不需要改变计算结构。
2. Kernel复用
修改属性后,现有的Conv Kernel仍然可以正确执行,只是参数不同。
3. 避免组合爆炸
如果为每种组合都创建新算子:
ConvBN
ConvBNRelu
ConvAdd
ConvAddRelu
ConvBNAdd
ConvBNAddRelu
... 组合数 = 2^n 爆炸增长
4. 保持图结构简单
算子类型越少,图越简单,后续优化和调试越容易。
什么时候需要生成新算子
| 场景 | 原因 | 例子 |
|---|---|---|
| 后端有专门优化 | 利用硬件特性(如CUDA的Fused Conv) | ConvReluFused |
| 数据布局改变 | 融合后需要特殊的内存排布 | NC4HW4Conv |
| 无法用原算子表达 | 融合后的计算模式与原算子不同 | DepthwiseConvBN |
| 性能关键路径 | 为特定融合模式手写汇编优化 | ConvAddReluInt8 |