读 ncnn 源码(XIX):`fuse_convolution_add`——融合逐通道加法,进一步合并线性计算
读 ncnn 源码(XIX):fuse_convolution_add——融合逐通道加法,进一步合并线性计算
本篇,我们将继续沿着合并连续线性计算的优化思路,分析 ncnn 中另一个重要的融合 Pass:
fuse_convolution_add。此 Pass 专门针对 **
Convolution层后紧跟一个特定模式的逐通道加法(通过BinaryOp实现)**的场景。这种情况可能出现在某些网络结构(如 ResNet bottleneck 中的 residual connection 如果退化为加偏置)或者模型转换过程中。将其融合,可以消除冗余的加法操作,进一步提升推理效率。
TL;DR
- 目标: 将
Convolution层后接一个扮演逐通道加法 (Per-Channel Bias Addition) 角色的BinaryOp层(操作类型为Add,且第二个输入来自MemoryData)进行融合。 - 模式匹配: 查找
Convolution -> BinaryOp结构,并附加严格条件:BinaryOp的类型必须是加法 (op_type == 0)。BinaryOp不能是与标量相加 (!with_scalar)。BinaryOp的第二个输入bottoms[1]必须来自一个MemoryData层。- 该
MemoryData层的数据维度必须符合逐通道加法的广播要求(例如,形状为[channels]或[channels, 1, 1],ncnn 代码中检查了[channels]和[1, 1, channels]两种情况)。
- 数学原理:
- Conv:
- BinaryOp(Add): (其中 是来自
MemoryData的、维度为[channels]的偏置向量,B会被广播到 的空间维度) - 融合后:
- 推导得: (卷积核 保持不变)
- 代码实现:
- 从
memorydata->data中提取偏置向量B(通过reshape确保其为一维)。 - 检查
Convolution层是否已有偏置 (bias_term == 1)。 - 如果没有偏置,则直接将
B赋值给convolution->bias_data,并将bias_term设为 1。 - 如果已有偏置 ,则将
B逐元素地加到convolution->bias_data上 (bias[i] = bias[i] + bias_data[i])。
- 从
- 图结构修改: 将
Convolution层的top指向原BinaryOp层的top,更新blob的producer,并将BinaryOp层标记为"ncnnfused"。 - 效果: 消除了
BinaryOp层引入的逐元素加法计算和额外的内存读写,将加偏置操作无损地合并到卷积的偏置项中。

1. 融合动机:合并连续的偏置添加
Convolution 层的计算结果 本身就包含一个偏置项 。如果其后紧跟的 BinaryOp 只是对每个通道加上一个固定的偏置值 (来自 MemoryData),那么这两个连续的加法操作 ( 和 ) 显然可以在代数上合并为一次加法 。
将这个合并后的偏置 直接存入 Convolution 层的 bias_data 中,就可以在推理时完全跳过 BinaryOp 层,从而减少计算量和内存访问。
2. 模式匹配:确保是“逐通道加偏置”
fuse_convolution_add 的模式匹配与 fuse_convolution_mul 非常相似,关键在于确保 BinaryOp 确实是在执行“逐通道加偏置”这一特定任务。
1 | int NetOptimize::fuse_convolution_add() |
这个匹配过程确保了只有当 BinaryOp 确实是“使用一个固定的偏置向量 B 对来自 Convolution 的特征图 y 进行逐通道相加 y + B”时,融合才会触发。代码特别检查了 MemoryData 的两种常见广播形状。
3. 数学原理与代码实现:合并偏置项
融合的核心是将 BinaryOp 的偏置向量 B (即 memorydata->data) 加到 Convolution 的偏置 b_{conv} 上。
a) 数学推导:
我们希望找到 使得:
显然,只需令:
卷积核 保持不变。
b) 代码实现:
1 | // 1. 将 MemoryData 的数据 reshape 成一维向量 B |
memorydata->data.reshape(channels): 确保bias_data是一个[channels]的一维Mat,方便后续访问。- 处理
bias_term == 0: 如果原卷积层没有偏置,融合后就需要添加偏置项,并将MemoryData的数据直接赋给它。 - 处理
bias_term == 1: 如果原卷积层已有偏置,则将MemoryData的数据逐元素加到现有偏置上。
4. 图结构修改
参数更新完毕后,同样需要更新计算图的拓扑结构:
1 | int top_blob_index_final = binaryop->tops[0]; // 获取 BinaryOp 原来的输出 Blob |
这与 fuse_convolution_mul 和 fuse_convolution_batchnorm 的图修改逻辑完全一致。
5. 意义与限制
- 意义:
- 减少计算量: 消除了
BinaryOp层对整个特征图进行逐元素加法的计算。 - 减少内存访问: 避免了读取
MemoryData、读取Conv输出、写入BinaryOp输出的内存操作。 - 优化线性链: 与乘法融合类似,加法融合有助于进一步合并连续的线性操作。
- 减少计算量: 消除了
- 限制:
- 仅限
Add: 只处理加法类型的BinaryOp。 - 仅限
MemoryData: 只处理第二个操作数来自固定MemoryData的情况。 - 广播模式:
MemoryData的形状必须符合逐通道加偏置的广播规则。
- 仅限
6. 结语
fuse_convolution_add 作为 ncnnoptimize 工具箱中的一员,进一步完善了对 Convolution 层后接线性操作的融合优化能力。它通过简单的偏置项合并,有效地消除了冗余的逐通道加法运算。这一系列的融合 Pass(包括 BN、Scale、Mul、Add 等)共同作用,使得 ncnn 能够将原始的、可能带有冗余操作的网络计算图,转化为一个更加紧凑、计算和访存都更高效的执行形式,为端侧部署提供了坚实的性能基础。
该封面图片由Roderick Qiu在Pixabay上发布





