读 ncnn 源码(XLII):`ncnnoptimize` 的“编排艺术”——优化 Pass 的依赖与顺序 (ncnnoptimize 完结篇)
读 ncnn 源码(XLII):ncnnoptimize 的“编排艺术”——优化 Pass 的依赖与顺序 (ncnnoptimize 完结篇)
在
ncnnoptimize系列的前序篇章中,我们已经像解剖学家一样,逐一分析了fuse_...(融合)、eliminate_...(消除)和replace_...(替换)三大类优化 Pass 的具体实现。我们理解了Conv+BN融合的代数原理,eliminate_dropout对推理的净化,以及Conv->IP替换的语义等价性。现在,是时候退后一步,从
ncnnoptimize.cpp的main函数视角,欣赏这出优化大戏是如何编排的。main函数中那几十行看似平铺直叙的optimizer.fuse_...()调用,绝非随意的罗列,而是一个经过精心设计的、存在严格依赖关系的多遍(Multi-Pass)优化流水线。本篇,我们就来揭示这个“编排”背后的工程智慧。
TL;DR
ncnnoptimize的本质: 它是一个多遍(Multi-Pass)图优化编译器。main函数定义了所有优化 Pass 的固定执行顺序。- 顺序的意义: 优化 Pass 之间存在依赖关系。某些 Pass 的执行会创造出可供后续 Pass 优化的新模式,或者依赖于前序 Pass 完成的清理工作。
- 流水线阶段 (Pipeline Stages):
- 阶段 1:代数融合 (Algebraic Fusion):
fuse_..._batchnorm/mul/add。此阶段专注于合并连续的线性计算,将BN、Scale、Mul、Add等操作的参数“烘焙”进Conv/IP等计算密集型层。这是最基础、最重要的优化。 - 阶段 2:语义替换与非线性融合 (Semantic & Activation Fusion):
replace_prelu...,fuse_..._activation。此阶段处理非线性激活函数,并替换低效模式。fuse_..._activation必须在代数融合之后,以便将激活操作融合到已经吸收了 BN 等参数的“最终版”Conv/IP层中。 - 阶段 3:冗余消除 (Redundancy Elimination):
eliminate_dropout,_pooling1x1,_noop,_split,_reshape...。此阶段负责清理所有语义上的“恒等映射”层。 - 阶段 4:高级替换 (High-Level Replacement):
replace_convolution_with_innerproduct...。此 Pass 在图结构基本清理干净后执行,将GAP -> Conv或IP -> Conv这种“大材小用”的模式替换为更轻量的IP。 - 阶段 5:最终清理 (Final Cleanup):
eliminate_flatten_after_innerproduct,eliminate_orphaned_memorydata。此阶段负责**“收尾”,清理掉因前序 Pass(如阶段 4)而新产生**的冗余(如IP后的Flatten)或“孤儿”节点。 - 阶段 6:终结 (Finalization):
shape_inference()必须在所有图结构修改之后、save()之前运行。它通过一次“沙盘推演”来重新计算所有blob的形状,更新layer的bottom/top_shapes,确保.param文件的正确性。
- 阶段 1:代数融合 (Algebraic Fusion):
main函数就是这个依赖图的“拓扑排序”:它确保了每一项优化都能在其依赖项已完成后执行,从而实现最大化的优化效果。

1. 优化流水线:一个精心编排的剧本
ncnnoptimize 的 main 函数(精简后)的执行顺序如下:
1 | int main(...) |
2. 依赖解析:为什么必须是这个顺序?
阶段 1 (代数融合) 必须在 阶段 2 (激活融合) 之前
-
依赖关系:
fuse_convolution_activation依赖于fuse_convolution_batchnorm。 -
原因:
Conv -> BN -> ReLU是最常见的模式。fuse_convolution_batchnorm(阶段 1) 先执行,将Conv -> BN融合成一个新的Conv'。- 此时计算图变为
Conv' -> ReLU。 fuse_convolution_activation(阶段 2) 接着执行,才能匹配到Conv' -> ReLU模式,并将其融合为Conv'(with_activation)。
- 如果顺序颠倒,
fuse_convolution_activation会因为Conv后面跟的是BN而不是ReLU而匹配失败,导致融合失效。
阶段 3 (消除) 必须在 阶段 1 & 2 (融合) 之后
- 依赖关系:
eliminate_split/eliminate_orphaned_memorydata依赖于fuse_..._activation/fuse_memorydata_binaryop。 - 原因 (Dead Code Elimination):
eliminate_split:考虑一个Split层,其两个输出分别送往ReLU_A和ReLU_B。在阶段 3 开始时,Split有两个有效消费者,不能被消除。fuse_..._activation(阶段 2) 执行,将ReLU_A和ReLU_B都标记为"ncnnfused"。eliminate_split(阶段 3) 再次执行时,它检查Split的两个输出blob,发现它们的消费者(ReLU_A和ReLU_B)都已经是"ncnnfused"。它判定Split不再有任何有效消费者,因此可以安全地将其消除。eliminate_orphaned_memorydata同理,它必须在fuse_memorydata_binaryop将MemoryData“孤立”之后运行,才能将其“回收”。
阶段 4 (高级替换) 必须在 阶段 1 & 2 之后
- 依赖关系:
replace_convolution_with_innerproduct...依赖于fuse_convolution_activation。 - 原因 (继承优化):
Conv层和IP层内部都有activation_type和activation_params成员。- 图优化首先处理
GAP -> Conv -> ReLU。 fuse_convolution_activation(阶段 2) 将ReLU融合进Conv,Conv层的activation_type被设为 1。replace_convolution_with_innerproduct_...(阶段 4) 执行,匹配到GAP -> Conv(with_activation)模式。- 在执行替换时,它会创建一个新的
IP层,并将Conv层的所有参数(包括activation_type=1)完整地迁移给IP层。 - 最终,图被优化为
GAP -> IP(with_activation),实现了优化的传递和最大化。
- 图优化首先处理
阶段 5 (最终清理) 必须在 阶段 4 之后
- 依赖关系:
eliminate_flatten_after_innerproduct依赖于replace_convolution_with_innerproduct...。 - 原因 (清理新产生的冗余):
- 原始图可能是
GAP -> Conv -> Flatten。 replace_..._after_global_pooling(阶段 4) 执行,将Conv替换为IP,图变为GAP -> IP -> Flatten。- 此时,
eliminate_flatten_after_innerproduct(阶段 5) 才能匹配到IP -> Flatten这一新产生的冗余模式,并将其消除。
- 原始图可能是
阶段 6 (终结) 必须在所有图修改之后
- 依赖关系:
shape_inference/save依赖于 之前所有的图优化 Pass。 - 原因: 所有的融合、消除、替换操作都严重破坏了
Net内部blobs和layers中存储的形状信息 (shape,bottom_shapes,top_shapes) 和连接关系 (tops,bottoms) 的一致性。 shape_inference(如第 41 篇所述) 是必不可少的“状态重整”步骤。它通过一次“沙盘推演”重新计算所有blob的形状,并回填到layer中。save必须在shape_inference之后,才能将这个最终的、一致的、优化后的图结构正确地写入新的.param和.bin文件。
5. 结语 (ncnnoptimize 篇章完结)
ncnnoptimize 的 main 函数不仅是一个简单的脚本,它是一个精心设计的图优化流水线。这个流水线通过依赖排序,确保了优化 Pass 能够以最高效的方式协同工作:从底层的代数合并(Conv+BN)开始,到中层的语义融合(Conv+ReLU),再到高层的模式替换(Conv->IP),最后通过多轮的清理(eliminate...)和终结(shape_inference)来确保图的简洁性与正确性。
通过对这一系列 Pass 的深入分析,我们不仅理解了 ncnn 如何实现单一的优化技巧,更重要的是,我们窥见了 ncnn 如何将这些技巧“编排”成一个鲁棒、高效的自动化优化流程。这种分阶段、有依赖、层层递进的优化思想,是所有现代深度学习编译器(如 TVM, MLIR)的核心,也是 ncnnoptimize 能够化腐朽为神奇,为端侧推理带来显著性能提升的根本原因。





