读 ncnn 源码(XXXIX):消除冗余塑形(下)——`eliminate_reshape_before_binaryop`
读 ncnn 源码(XXXIX):消除冗余塑形(下)——eliminate_reshape_before_binaryop
ncnnoptimize的图优化 Pass 中,对Reshape和Flatten等塑形层(Shape-manipulation layers)的消除是一个重要主题。上一篇我们看到了GlobalAveragePooling和InnerProduct因其输出在语义上已是“扁平”的,从而使其后的Reshape/Flatten变得多余。本篇分析的
eliminate_reshape_before_binaryop则利用了BinaryOp层自身实现的健壮性(Robustness),来消除其输入端的多余Reshape操作。
TL;DR
- 目标: 识别并消除
... -> Reshape(to 1x1xC) -> BinaryOp这样的模式。 - 核心原理 (BinaryOp 的“形状无关性”):
ncnn::BinaryOp(如Add,Mul等)在执行两个张量的逐元素操作时,其forward实现在根本上是“形状无关”的。它主要关心两个张量的总元素数量total()是否相等。 - 冗余分析: 只要两个输入张量的总元素数相同,
BinaryOp就能正确地逐个处理它们(通过各自的w,h,c,cstep遍历)。因此,一个Reshape层如果仅仅是将一个张量从[W, H, C]拍平为[1, 1, W*H*C](总元素数不变),而不改变其内存中的数据顺序,那么这个Reshape操作对于BinaryOp来说就是完全冗余的。 - 模式匹配: 遍历
layers,查找Reshape层,并检查其是否满足“拍平”条件(reshape->w == 1且reshape->h == 1)。然后查找紧随其后的、消费该blob的BinaryOp。 - 图结构修改 (短路): 执行标准的“短路”操作。将
Reshape层的输入blob(bottom_blob_index_final) 直接连接为BinaryOp层的输入,并标记Reshape层为"ncnnfused"。 - 效果: 移除了一个不产生任何计算、仅改变元数据的冗余层,简化了计算图,减少了层调度开销。
1. 动机:BinaryOp 的“形状无关性”
ncnn 中的 BinaryOp 层(当 with_scalar=0 时)被设计用来处理两个张量的逐元素运算。虽然它支持复杂的广播(Broadcasting)规则,但在最常见的情况下——两个输入张量具有相同的总元素数——它的 forward 实现本质上是一个遍历 total() 次的循环。
例如,一个 BinaryOp(Add) 操作,它需要计算 C = A + B。
- 如果
A的形状是[224, 224, 3](total=150528) - 而
B的形状是[150528, 1, 1](total=150528)
BinaryOp 的 forward 内核完全有能力处理这种情况。它会各自使用 A 和 B 的 w, h, cstep 来迭代各自的数据指针,执行 total() 次加法。
因此,如果一个 Reshape 层的唯一作用是将 A 从 [224, 224, 3] 变为 [150528, 1, 1](或 [1, 1, 150528]),那么这个 Reshape 层对于后续的 BinaryOp 来说是完全多余的。ncnnoptimize 正是抓住了这一点来进行优化。
2. 源码解析:eliminate_reshape_before_binaryop
该函数的实现逻辑清晰地展现了这一思路:
1 | int NetOptimize::eliminate_reshape_before_binaryop() |
3. 结语
eliminate_reshape_before_binaryop 是 ncnnoptimize 中又一个基于语义等价的算子消除 Pass。它与 eliminate..._after_global_pooling 系列 Pass 互为补充,共同清理了计算图中因形状操作而引入的冗余节点。
此优化利用了 BinaryOp 层 forward 实现的健壮性——即在处理逐元素操作时,对输入的 (W, H, C) 维度不敏感,只关心 total() 元素总数。通过消除这些不必要的“视图转换”层,ncnnoptimize 进一步简化了计算图,减少了层调度开销,使网络结构更加精简。





