读 ncnn 源码(XXXVIII):消除冗余塑形——`eliminate_flatten/reshape` 的瘦身之道
读 ncnn 源码(XXXVIII):消除冗余塑形——eliminate_flatten/reshape 的瘦身之道
在
ncnnoptimize的图优化工具箱中,“算子消除”是一个重要分支。它旨在移除那些对计算结果没有贡献的冗余层。Dropout(推理时)、Pooling(1x1)和Noop是因其“恒等映射”特性而被消除的典型。本篇,我们将分析另一类可被消除的层:Reshape和Flatten,它们在特定上下文中同样是“恒等映射”。
eliminate_..._after_global_pooling和eliminate_flatten_after_innerproduct这一系列 Pass 关注的就是GlobalAveragePooling或InnerProduct之后紧跟的Flatten或Reshape操作。
TL;DR
- 目标: 识别并消除
GlobalAveragePooling -> Flatten/Reshape和InnerProduct -> Flatten这样的冗余塑形模式。 - 核心原理 (语义上的 No-op):
GlobalAveragePooling: 其输出blob的形状为[1, 1, C](或 ncnnMat中w=1, h=1, c=channels)。InnerProduct: 其输出blob的形状为[num_output, 1, 1](或 ncnnMat中w=num_output, h=1, c=1)。Flatten/Reshape(to 1D): 这些层的作用是将多维张量“拍平”成一维向量。- 结论:
GlobalAveragePooling和InnerProduct的输出在逻辑上已经是“平”的(一维向量)。对一个已经是一维(或1x1xC)的张量再次执行Flatten或等效的Reshape,是一个语义上的冗余操作(Semantic No-op)。
- 模式匹配:
eliminate..._after_global_pooling: 查找Pooling层,并检查pooling->global_pooling == 1。然后查找其后紧跟的Flatten层或特定参数的Reshape层(如reshape->h == -233 && reshape->c == -233,这在 ncnn 中常用于表示“拍平”)。eliminate_flatten_after_innerproduct: 查找InnerProduct层,然后查找其后紧跟的Flatten层。
- 图结构修改 (短路): 这三个函数均采用我们在第 36 篇中分析过的标准“短路”(Short-Circuiting)手法。将前驱层(
Pooling或InnerProduct)的topblob 直接指向后继层(Flatten/Reshape)的topblob,并更新blob的producer,最后将Flatten/Reshape层标记为"ncnnfused"。 - 效果: 移除了冗余的层调度和元数据操作,简化了计算图,使网络结构更清晰。
1.融合动机:当塑形(Reshape/Flatten)遇到“已塑形”
Flatten 和 Reshape 层的作用是改变张量的逻辑视图,通常不涉及实际的计算(在 ncnn 中,如果内存连续,reshape 只是修改元数据,是零拷贝操作)。但即使是零拷贝,它在计算图中仍然是一个独立的层,会带来层调度的开销。
ncnnoptimize 敏锐地识别到,某些层的输出在语义上已经是“扁平”的:
Pooling(global_pooling=1): 无论输入[W, H, C]有多大,输出都是[1, 1, C]。InnerProduct: 无论输入是什么,输出都是[num_output, 1, 1]。
当这些层后面跟一个 Flatten 层时(Flatten 的目标是产生一个 [W*H*C, 1, 1] 的向量),Flatten 实际上什么也不用做,因为输入 [1, 1, C] 拍平后还是 [C, 1, 1](在 InnerProduct 情况下同理)。这种操作是完全多余的。
为什么这个“多余”的层会出现?
在 PyTorch 或 TensorFlow 中,模型的定义非常严格:
- 一个
GlobalAveragePooling层的输出维度是[N, C, 1, 1]。 - 一个
InnerProduct(全连接,nn.Linear) 层严格要求它的输入是2D的,即[N, Features]。
因此,模型作者必须在两者之间手动插入一个 Flatten 操作,把 [N, C, 1, 1] 显式地“压平”成 [N, C]。
1 | # PyTorch 示例 |
当这个模型被导出到 ONNX 时,ONNX 会忠实地记录下这个结构: [Pooling] -> [Flatten] -> [InnerProduct]
为什么 ncnn 认为它“多余”?
ncnn 的 InnerProduct 层实现得更“智能”或者说更“灵活”。
ncnn::InnerProduct 层的设计哲学是:“我不在乎你输入的是几维,我只管把它‘隐式地’(implicitly)拍平,然后做矩阵乘法。”
- 你给
InnerProduct输入[N, C, 1, 1]?- 它会把它当作
[N, C*1*1](即[N, C]) 来处理。
- 它会把它当作
- 你给
InnerProduct输入[N, C, 7, 7]?- 它会把它当作
[N, C*7*7]来处理。
- 它会把它当作
这就是关键点:对于 ncnn::InnerProduct 层来说,[Flatten] 层完全是多余的。InnerProduct 层自己就内置了“拍平”这个动作。
2. 源码解析:eliminate..._after_global_pooling
eliminate_flatten_after_global_pooling 和 eliminate_reshape_after_global_pooling 的逻辑几乎相同,我们以前者为例:
1 | int NetOptimize::eliminate_flatten_after_global_pooling() |
eliminate_reshape_after_global_pooling 的逻辑与此一致,只是将 type != "Flatten" 替换为 type != "Reshape",并增加了对 Reshape 参数的检查 (reshape->h == -233 && reshape->c == -233),以确保它是一个执行“拍平”操作的 Reshape。
3. 源码解析:eliminate_flatten_after_innerproduct
这个 Pass 的逻辑与 eliminate_flatten_after_global_pooling 完全相同,只是将生产者层的类型从 Pooling 换成了 InnerProduct。
1 | int NetOptimize::eliminate_flatten_after_innerproduct() |
4. 结语
这一系列 eliminate_... Pass 展现了 ncnnoptimize 在图优化层面上的“智能”。它不仅能处理代数层面的融合(如 Conv+BN),还能理解算子在形状(Shape)语义上的冗余。
GlobalAveragePooling 和 InnerProduct 的输出在逻辑上已经是 1D 向量,任何后续的 Flatten 或等效 Reshape 操作都是不必要的。通过“短路”这些冗余的塑形层,ncnnoptimize 进一步简化了计算图,减少了不必要的层调度开销,使得最终执行的网络更加精简高效。





