读 ncnn 源码(XXX):`replace_reduction_with_global_pooling`——算子替换:识别低效模式
读 ncnn 源码(XXX):replace_reduction_with_global_pooling——算子替换:识别低效模式
在
ncnnoptimize的优化 Pass 中,我们已经见证了大量“算子融合”(Operator Fusion)操作,它们通过合并相邻的线性层(如Conv+BN)来减少计算和访存开销。本篇,我们将探讨另一种同样重要的优化技术:算子替换 (Operator Replacement)。replace_reduction_with_global_pooling函数就是其典型代表。它的核心任务是识别出一种低效的、由多个通用层组合而成的计算模式,并将其替换为一个功能等价但性能高得多的专用层。
TL;DR
- 目标: 识别并替换一种用两次连续的
Reduction(Mean) 操作来实现“全局平均池化”(Global Average Pooling) 的低效模式。 - 模式匹配: 遍历
layers,查找Reduction -> Reduction的直接连接模式。 - 严格条件: 两个
Reduction层都必须是Mean操作 (operation == 3),非全局 (reduce_all == 0),且系数为 1 (coeff == 1.f)。此外,它们必须各自只对一个轴进行操作 (axes.w == 1),并且轴的组合(如axis=2或3后接axis=2,这可能对应于 H 和 W 轴)符合 ncnn 对特定框架(如 ONNX/TensorFlow 转换而来)全局池化模式的识别规则。 - 替换动作:
- 动态创建一个全新的
Pooling层 (ncnn::create_layer_cpu("Pooling"))。 - 将新
Pooling层的参数设置为pooling_type = 1(Average) 和global_pooling = 1。 - 新的
Pooling层在layers向量中替换掉第二个Reduction层的 位置 (layers[j] = pooling)。
- 动态创建一个全新的
- 图结构修改:
- 将第一个
Reduction层标记为无效 (reduction1->type = "ncnnfused")。 - 将新
Pooling层的bottom(输入)直接连接到第一个Reduction层的bottom,彻底绕过这个双层Reduction结构。
- 将第一个
- 效果: 这种替换是巨大的性能提升。它将两个通用的、可能基于循环实现的
Reduction层的计算和访存开销,替换为一个专用的、内部有高效 SIMD 实现的GlobalAveragePooling层的开销。

1. 融合之外的另一条路——算子替换
图优化的目标是等价地提升性能。除了将相邻层合并(融合),另一种强大的技术是识别出一个由多个通用层组成的、意图上等价于某个专用层的模式,然后执行替换。
GlobalAveragePooling (GAP) 是一个非常常见的操作,它将特征图的 H 和 W 维度上的所有像素值取平均,最终输出一个 [C] 维度的向量。然而,一些深度学习框架(如 TensorFlow 或 ONNX 转换器)在导出模型时,可能会将这个操作“降级”为两次连续的 ReduceMean 操作:
ReduceMean(axis=H):对 H 维度(高)取均值。ReduceMean(axis=W):对上一步的结果再对 W 维度(宽)取均值。
这个 Reduction -> Reduction 的组合在数学上等价于 GAP,但在 ncnn 中执行效率低下:
- 两次层调度: 需要两次
forward_layer调用。 - 两次内存读写: 第一个
Reduction将结果写入一个中间blob,第二个Reduction再将其读出。 - 通用实现:
Reduction层是一个通用的n维规约操作,其实现可能不如专用的Pooling层针对二维空间的全局池化优化得那么极致。
replace_reduction_with_global_pooling 的使命,就是识别出这个低效模式,并将其“升级”为 ncnn 内部最高效的 GlobalAveragePooling 实现。
2. 模式匹配:寻找连续的空间均值
该函数的匹配逻辑非常严格,旨在精确捕获上述模式:
1 | int NetOptimize::replace_reduction_with_global_pooling() |
- 模式:
Reduction(op=Mean, axis=2 or 3) -> Reduction(op=Mean, axis=2)。 - 意图: 这里的轴索引
2和3很可能对应于特定模型格式(如 ONNX[N, C, H, W]的axes=[2, 3])在 ncnnMat内部(可能是[W, H, C]或[W, H, D, C])的轴映射。代码的意图是匹配在两个空间维度上的连续ReduceMean。
3. 执行替换:Pooling 层的“偷天换日”
一旦模式匹配成功,优化器会执行一次“偷天换日”:
1 | // 1. 创建一个全新的 Pooling 层 |
核心: pooling->global_pooling = 1 是关键。当 Pooling 层的这个标志位被设为 1 时,它的 forward 函数会忽略所有 kernel_w, stride 等参数,转而调用一个专门的、高度优化的 pooling_global_... 函数,该函数直接对整个 W 和 H 维度进行求平均。
4. 计算图重连接 (Graph Rewiring)
替换完成后,还需要修正计算图的连接,让这个新层真正生效:
1 | // 1. 获取 reduction1 的原始输入 |
效果: 原本 Blob_A -> Reduction1 -> Blob_B -> Reduction2 -> Blob_C 的计算流,被重塑为 Blob_A -> GlobalPooling -> Blob_C。Reduction1 和 Blob_B 在推理时被完全跳过。
5. 结语
replace_reduction_with_global_pooling 是一个典型的算子替换优化 Pass。它展示了 ncnnoptimize 不仅能进行局部的算子融合,还能识别出由多个低效、通用算子组成的、具有特定语义模式(如全局池化)的子图,并将其替换为单一的、高性能的专用算子。这种“理解”模型意图并进行重构的能力,是图优化工具提升模型性能的关键所在,它允许 ncnn 用自己最高效的 SIMD 内核(如 pooling_global_avx)来替代转换器产生的低效实现。





