读 ncnn 源码(XXXIV):`fuse_memorydata_binaryop`——将“常量”烘焙进“算子”
读 ncnn 源码(XXXIV):fuse_memorydata_binaryop——将“常量”烘焙进“算子”
在
ncnnoptimize的图优化篇章中,我们已经见证了多种“算子融合”(Operator Fusion),它们将两个计算层(如Conv和BN)合并为一个。本篇,我们将探讨一种不同但同样重要的优化:将数据层(MemoryData)与计算层(BinaryOp)融合。这在本质上是一种常量折叠 (Constant Folding) 或 常量传播 (Constant Propagation)。
fuse_memorydata_binaryop的核心任务是识别出BinaryOp(如加、减、乘、除)的一个输入是来自MemoryData的标量(Scalar),并将这个标量“烘焙”到BinaryOp的参数中,将其从一个昂贵的“张量-张量”操作,降级为一个高效的“张量-标量”操作。
TL;DR
- 目标: 识别
BinaryOp的一个输入是来自MemoryData层的单个标量值的模式。 - 优化: 将
BinaryOp转换为其高效的**“标量模式”**(with_scalar = 1),并将该标量值(memorydata->data[0])直接存入BinaryOp层的b参数中。 - 图修改: 断开
MemoryData层与BinaryOp层的连接(binaryop->bottoms.erase(...)),并将MemoryData层标记为"ncnnfused"以便后续移除。 - 关键技巧 (RSUB/RDIV): 为处理非交换操作(减法、除法),如果标量是第一个操作数(如
scalar - tensor),BinaryOp的操作类型会被巧妙地切换为反向操作(如RSUB,即tensor - scalar),从而保持数学等价性。 - 两遍执行 (Two-Pass):
- Pass 1: 处理简单的
MemoryData -> BinaryOp模式。 - Pass 2: 处理更复杂的
MemoryData -> Split -> BinaryOp模式,即一个标量通过Split层广播给多个BinaryOp。此 Pass 会逐个融合BinaryOp,并最终清理掉Split层和MemoryData层。
- Pass 1: 处理简单的

1. 融合动机:昂贵的“张量-标量”操作
在神经网络中,有时会需要对整个特征图执行一个标量运算,例如 output = feature_map * 0.5。在 ncnn 的计算图中,这通常表示为:
- 一个
MemoryData层,它持有一个Mat(w=1, h=0, c=0),其中只包含一个float值0.5。 - 一个
BinaryOp(Mul) 层,它接收feature_map和MemoryData的输出作为两个输入。
BinaryOp 的通用实现需要处理两个张量输入,它必须加载两个 Mat,处理复杂的广播逻辑。但如果它事先知道第二个输入永远是一个标量,它就可以切换到一个高度优化的“标量模式” (with_scalar = 1):
BinaryOp(Mat, Mat): 昂贵。需要加载两个(可能很大的)Mat,内存带宽压力大。BinaryOp(Mat, float): 高效。只需加载一个Mat,标量b可以被立即加载到 SIMD 寄存器的所有通道中,计算和访存开销都大大降低。
fuse_memorydata_binaryop 的使命就是自动完成这种从“昂贵模式”到“高效模式”的转换。
2. Pass 1: 简单的 MemoryData -> BinaryOp 融合
函数的第一个循环处理最简单的 MemoryData -> BinaryOp 直连模式。
1 | int NetOptimize::fuse_memorydata_binaryop() |
关键点:
memorydata->w != 1 ...: 精确识别标量。RSUB/RDIV切换: 这是处理非交换律操作的精妙之处,使得融合得以在更多情况下(即使标量是第一个操作数)发生。with_scalar = 1: 这是BinaryOp层内部实现的“开关”,forward函数会根据此标志,选择(Mat, Mat)还是(Mat, float)的计算路径。
3. Pass 2: 处理 MemoryData -> Split -> BinaryOp 模式
在某些网络中,一个常量(如 MemoryData)可能被 Split 层复制多份,供多个后续层使用。第一个 Pass 无法处理这种情况,因此需要第二个 Pass。
1 | // 第二个 for 循环 |
关键点:
Split层处理: 融合的核心逻辑(BinaryOp变为标量模式)不变,但增加了对Split层的清理逻辑。split->tops.erase: 每融合一个BinaryOp,Split的一个输出就被剪除。split->tops.empty(): 当Split的所有输出都被融合后,Split层本身和其上游的MemoryData层就成了“孤岛”,可以被一并标记为"ncnnfused"。i--: 由于layers列表可能在迭代中被隐式修改(split被融合),i--确保for循环在下一次迭代时能重新检查当前索引i,防止跳过紧邻的下一个MemoryData。
4. 结语
fuse_memorydata_binaryop 是一个精巧的、基于常量传播思想的优化 Pass。它通过将 MemoryData 标量“烘焙”进 BinaryOp 的参数中,将昂贵的“张量-张量”操作降级为高效的“张量-标量”操作。该 Pass 通过两遍扫描,分别处理了简单直连和通过 Split 广播的复杂情况,充分展现了 ncnnoptimize 在简化计算图、降低内存带宽压力方面的细致考量。





