读 ncnn 源码(XXXVI):算子消除——`eliminate_dropout/pooling1x1/noop/split`
读 ncnn 源码(XXXVI):算子消除——eliminate_dropout/pooling1x1/noop/split
在
ncnnoptimize的图优化工具箱中,除了“算子融合”(如Conv+BN)和“算子替换”(如PReLU->ReLU),还有一类至关重要的优化 Pass:算子消除 (Operator Elimination)。这类优化的目标是识别并彻底移除那些在推理阶段计算无效或结构冗余的层。本篇,我们将集中分析
eliminate_dropout,eliminate_pooling1x1,eliminate_noop, 和eliminate_split这四个函数。它们虽然处理的层类型不同,但其核心思想和图修改手法几乎完全一致:找到“无效层”,并将其从计算图中“短路”掉。
TL;DR
- 目标: 识别并消除四种在特定条件下对计算图无实际贡献的层:
Dropout(推理时)、Pooling(1x1, s1)、Noop、Split(单路输出)。 - 核心原理 (Identity Mapping): 这些层在特定配置下都退化成了恒等映射 (Identity Mapping),即
Output = Input。它们的存在除了引入额外的层调度和内存访问外,对计算结果毫无贡献。 - 模式匹配: 遍历
layers,查找目标层类型,并检查其退化为恒等映射的关键条件:eliminate_dropout:dropout->scale == 1.f(这是 Inverted Dropout 在推理时的标准行为)。eliminate_pooling1x1:kernel=1x1,stride=1x1,pad=0, 且非global_pooling。eliminate_noop: 层类型为Noop(Noop层本身就是恒等映射,用于占位)。eliminate_split:Split层的有效消费者(consumer != -1)不多于 1 个。
- 图结构修改 (Short-Circuiting): 这是所有消除类 Pass 的通用手法:
- 找到被消除层
L的唯一输入 Blob (bottom_blob_index)。 - 向上游反向查找到该
blob的生产者层Any。 - 找到
L层的唯一有效输出 Blob (top_blob_index_final)。 - 执行短路:将
Any层的输出top直接修改为top_blob_index_final。 - 更新
top_blob_index_final的producer为Any层的索引j。 - 将
L层(Dropout,Pooling,Noop,Split)的type标记为"ncnnfused",以便在推理时跳过。
- 找到被消除层
- 效果: 彻底从计算图中移除了冗余节点,减少了层调度的开销、消除了中间
blob的内存读写,使得计算图更加紧凑和高效。
1. 算子消除的动机:移除“恒等映射”
在推理阶段,网络中任何等价于 Output = Input 的操作都是冗余的。ncnnoptimize 通过一系列 Pass 来专门清除这些“路障”。
Dropout: 如第 29 篇所述,推理时scale == 1.0(Inverted Dropout)的Dropout层就是恒等映射。Pooling(k=1, s=1): 一个1x1的池化核,以1为步长滑动,不进行任何补边,其唯一的计算就是output[x,y] = input[x,y](无论是 Max 还是 Avg),这显然是恒等映射。Noop: “No Operation” 层,它被设计出来就是作为占位符或调试锚点,其forward实现就是直接返回输入,是纯粹的恒等映射。Split(单路输出):Split层的本意是将一个输入复制到多个输出分支。如果经过其他优化 Pass(例如分支剪枝)后,这个Split层只剩下一个或零个有效消费者,那么它就退化成了一个简单的“直通”管道,同样是恒等映射。
2. 模式匹配:识别“恒等”条件
这四个函数的前半部分都是在进行模式匹配,以确认目标层是否满足“恒等映射”的退化条件。
eliminate_dropout:
1 | // ... (find Dropout layer i) ... |
eliminate_pooling1x1:
1 | // ... (find Pooling layer i) ... |
eliminate_noop:
1 | // ... (find Noop layer i) ... |
eliminate_split:
1 | // ... (find Split layer i) ... |
3. 图结构修改:通用的“短路” (Short-Circuiting) 手法
一旦模式匹配成功,所有这四个函数都会执行一套几乎完全相同的图修改逻辑,即“短路”掉当前层。
通用逻辑 (以 eliminate_pooling1x1 为例):
1 | // 0. 确认匹配成功 |
图示:
- 优化前:
Layer[j]->Blob[A]->Layer[i](e.g., Pooling1x1) ->Blob[B]-> … - 优化后:
Layer[j]->Blob[B]-> … Blob[A]变为孤岛 (producer=j, consumer=-1),Layer[i]变为"ncnnfused"。
eliminate_split 的逻辑类似,它将 any->tops[top_i] 直接连接到 split->tops[real_split_top_blob_index](Split 唯一的有效输出)。eliminate_noop 也是如此。
4. 结语
eliminate_dropout, eliminate_pooling1x1, eliminate_noop, 和 eliminate_split 共同展示了 ncnn 图优化中“算子消除”这一重要策略。这些 Pass 专注于识别并移除计算图中的“恒等映射”节点。
它们的核心实现(模式匹配 + 图结构短路)高度一致,体现了图优化的通用范式。通过移除这些冗余层,ncnnoptimize 不仅减少了运行时的层调度开销,更重要的是消除了不必要的中间 blob 内存读写,使得计算图更加紧凑、高效,为端侧推理的极致性能奠定了坚实基础。





