读 ncnn 源码(XLI):`shape_inference`——图优化的“沙盘推演”与“状态重整”
读 ncnn 源码(XLI):shape_inference——图优化的“沙盘推演”与“状态重整”
ncnnoptimize的工作流是一系列对计算图的破坏性修改(融合、消除、替换)。这些操作的副作用是,许多中间Blob的维度信息(shape)可能会变得陈旧或不明确。shape_inferencePass 的职责并非“优化”,而是一个至关重要的**“工具型 Pass”**。它的核心任务是:通过一次模拟的“沙盘推演”(Dry Run),在不执行实际数值计算的前提下,强制 ncnn 的推理引擎(Extractor)完整地跑一遍优化后的计算图,从而“推演”并“记录”下每一个
Blob的确切形状信息。
TL;DR
- 目标: 在所有图优化 Pass 执行完毕后,为网络中的每一个
Blob重新计算并填充其shape属性(dims, w, h, c, elempack),并将其回填到每个Layer的bottom_shapes和top_shapes成员中。 - 为何必须:
- 状态更新: 优化 Pass(如
fuse_...)会改变Layer的参数(如activation_type),shape_inference中的recreate_pipeline步骤会强制Layer根据新参数重新生成其内部状态(如打包后的权重weight_data_tm)。 - 数据支撑: 后续的优化 Pass(如
estimate_memory_footprint)或最终save保存.param文件时,需要依赖这些精确的Blob形状信息。
- 状态更新: 优化 Pass(如
- 核心机制 (Dry Run):
- 重置管线: 遍历所有
Layer,调用destroy_pipeline和create_pipeline,强制它们根据优化后的新参数重新初始化。 - 创建提取器:
create_extractor()并设置为light_mode。 - 喂入“假”数据: 找到所有
Input层,根据其必须已定义的w, h, c形状,创建“假”的(未初始化)Mat,并通过ex.input()注入。 - 触发全图计算: 遍历所有
Layer,并对每个Layer的所有top_blob调用ex.extract(top_blob_index, m)。 - 惰性求值的妙用:
ex.extract()会触发 ncnn 的惰性求值机制,递归地计算所有依赖项。通过对 所有top_blob都调用一次extract,ncnn 被迫计算了图中每一个Blob。 - 形状记录: 每次
extract后,m中就包含了正确的形状元数据。blobs[top_blob_index].shape = m;这一句,就是将推演出的形状“偷”回并存入Blob的定义中。
- 重置管线: 遍历所有
- 收尾: 再次遍历所有
Layer,将blobs[...].shape的信息分别拷贝到layer->bottom_shapes和layer->top_shapes中,完成“状态重整”。 - 限制: 无法处理包含
custom_layer(自定义层)的网络(因为无法推断其形状),也无法处理Input层未定义输入形状的网络。
1. 动机:为何在优化后需要“形状推断”?
图优化 Pass(如 fuse_convolution_activation)会修改 Layer 的参数和连接关系,但并不会(也不应该)实时更新所有受影响的中间 Blob 的形状元数据。这会导致 Net 对象内部的 blobs 列表中的 shape 信息变得陈旧。
shape_inference 的目的就是解决这个“陈旧”问题。它在所有优化基本完成后,发起一次“总清算”,通过模拟推理,强制 Extractor 重新计算出所有 Blob 的实际形状,并回写到 blobs 列表中。
2. 准备工作:重置管线与喂入“种子”
在推演开始前,必须进行两项准备工作:
a) 重置层管线 (Recreate Pipeline):
1 | // recreat layer pipeline for param and weight changes |
分析: 这是至关重要的一步。create_pipeline 不仅分配资源,它还负责执行像权重打包 (convolution_transform_kernel_...) 这样的预处理。由于之前的 Pass(如 fuse_convolution_activation)已经修改了 Layer 的参数,必须调用 create_pipeline 来强制 Layer 根据其新的参数(例如,现在它知道自己需要融合激活)来重新生成其内部的优化状态(例如,选择一个支持融合激活的微核)。
b) 喂入“假”输入 (Feed Dummy Inputs):
1 | ncnn::Extractor ex = create_extractor(); |
分析: 形状推断必须有一个起点。Input 层就是这个起点。shape_inference 要求 Input 层必须有确定的形状。它创建 Mat 对象,但不关心其内容(m 未初始化),只关心其形状元数据。ex.input() 将这个带形状的“种子”放入 Extractor 的 blob_mats 缓存中。
3. 核心:遍历 extract 触发全图“沙盘推演”
这是 shape_inference 最核心的部分,它巧妙地利用了 Extractor 的惰性求值机制。
1 | fprintf(stderr, "shape_inference\n"); |
分析:
ex.extract(top_blob_index, m): 如同我们在第 XXXIV 篇中分析的,extract会检查blob_mats缓存。如果top_blob_index对应的Mat尚未计算 (dims == 0),它会递归地调用forward_layer计算其所有依赖。- 为何遍历所有层?: 简单地
extract最终的outputblob 只会计算图的主干路径。而通过for循环强制对网络中的每一个blob都执行一次extract,shape_inference确保了即使是那些“分支”上的、未被最终输出所依赖的blob,也会被计算,从而获取其形状。 blobs[top_blob_index].shape = m;:extract返回后,m虽然内容是无意义的(因为输入是“假”数据),但它的形状元数据(m.dims,m.w,m.h,m.c,m.elempack)是Extractor根据各层forward逻辑精确推导出来的。此行代码就是将这个“战利品”保存回Net对象的blobs列表中。
4. 收尾:将形状信息回填给 Layer
在 blobs 列表中的 shape 信息被完全更新后,最后一步是将其同步回 Layer 对象自身。
1 | // assign all layer blob shape |
分析: Layer 对象内部也维护了 bottom_shapes 和 top_shapes 列表。这一步将 blobs(全局信息源)中的权威形状数据,拷贝到每个 Layer 的本地缓存中。这使得 Layer 自身“知道”了其输入输出的形状,这对于 Net::save 函数正确写入 .param 文件至关重要。
5. 结语
shape_inference 并非一个优化 Pass,而是一个在优化流水线中承前启后的工具 Pass。它在图结构被修改(fuse/eliminate/replace)和层参数被更新后,通过 destroy/create_pipeline 强制层状态再生,并利用 Extractor 的惰性求值机制进行了一次全图“沙盘推演”。
这次推演的目的,就是为了重新收集和更新网络中每一个 Blob 和 Layer 的形状信息,使 Net 对象的内部状态恢复一致。这为后续的内存估算(estimate_memory_footprint)和模型序列化(save)提供了准确的数据基础,是 ncnnoptimize 流程中不可或缺的“状态重整”环节。





