读 ncnn 源码(XIII):`Extractor::extract`——触发推理的引擎核心
读 ncnn 源码(XIII):Extractor::extract——触发推理的引擎核心
在之前的篇章中,我们已经探讨了 ncnn 如何加载模型、优化权重布局。当模型准备就绪,输入数据也通过
Extractor::input()绑定后,最后一步就是调用Extractor::extract()来获取我们关心的输出结果。这个看似简单的函数调用,背后却隐藏着 ncnn 推理引擎的核心调度逻辑,涵盖了惰性求值、后端分发、内存管理等多个关键环节。本篇,我们将深入
Extractor::extract及其调用的NetPrivate::forward_layer、NetPrivate::do_forward_layer函数,剖析 ncnn 是如何根据依赖关系,按需、高效地执行神经网络推理的。
TL;DR
Extractor::extract是推理触发器: 它并非立即计算整个网络,而是采用惰性求值 (Lazy Evaluation)。只有当用户请求某个特定的blob时,才会触发计算该blob所需的最少网络路径。- 核心流程:
extract(blob_name/index)首先检查blob_mats缓存中是否已存在目标blob。若不存在,则找到其生产者层 (Producer Layer),并调用NetPrivate::forward_layer来执行计算。 forward_layer的递归机制: 这是实现惰性求值的关键。它采用深度优先搜索 (DFS) 的方式:要计算第L层,必须先递归调用forward_layer确保其所有输入blob(即layer->bottoms)都已被计算出来。do_forward_layer实际执行: 当某一层的所有输入blob都准备就绪后,do_forward_layer负责调用该层真正的forward或forward_inplace方法。它还处理内存优化 (Light Mode)、数据布局转换 (Layout Conversion) 以及结果缓存。- 后端分发与后处理:
extract内部包含对 Vulkan 后端的分发逻辑。在获取到计算结果后,还会进行必要的布局转换(Packed -> Plain)、数据类型转换(FP16/BF16/INT8 -> FP32)以及内存池解绑 (clone),确保返回给用户的Mat是标准的、独立的 FP32 数据。

1. Extractor::extract:用户接口与惰性求值触发
Extractor 对象是用户与 ncnn 网络交互的主要接口。input() 负责设置输入,而 extract() 则负责获取输出。
1.1 函数重载
Extractor 提供了两个 extract 重载版本:
extract(const char* blob_name, Mat& feat, int type = 0): 通过blob的名称来提取。内部会先调用net->find_blob_index_by_name()找到对应的索引。extract(int blob_index, Mat& feat, int type = 0): 通过blob的索引来提取。这是核心实现。
type 参数用于指定期望的输出数据类型和布局(暂不深入)。
1.2 惰性求值核心逻辑 (extract by index)
1 | int Extractor::extract(int blob_index, Mat& feat, int type) |
关键点解析:
d->blob_mats: 这是Extractor内部维护的一个std::vector<Mat>,其大小与网络中的blob数量相同。它充当了计算结果的缓存。初始状态下,所有Mat的dims都为 0。if (d->blob_mats[blob_index].dims == 0): 这是惰性求值的判断核心。只有当请求的blob尚未计算时,才会进入计算流程。如果之前某个extract调用已经计算过这个blob(或其依赖路径上的中间blob),它的dims将不为 0,可以直接从缓存返回。- 后端分发: 代码清晰地展示了优先尝试 Vulkan 后端(如果启用),失败或未启用则回退到 CPU 执行 (
forward_layer) 的逻辑。 forward_layer调用: 这是将计算任务委托给Net内部实现的关键一步,标志着递归计算的开始。
1.3 结果后处理
在获取到 blob_mats[blob_index] 后,extract 还会进行一系列重要的后处理,确保返回给用户的 feat 是标准的 FP32、非 Packed、且内存独立的数据:
- 布局转换: 如果网络内部使用了 Packed Layout(
feat.elempack != 1)以利用 SIMD,而用户未指定保留 (type == 0),则调用convert_packing将其转回 Plain Layout。 - 数据类型转换: 如果网络内部使用了低精度存储(FP16, BF16, INT8),而用户未指定保留 (
type == 0),则调用cast_..._to_float32将其转回 FP32。 - 内存池解绑: 如果使用了本地内存池 (
use_local_pool_allocator),且feat仍然在使用该池的内存,则调用feat = feat.clone()进行深拷贝。这使得用户可以在获取feat后安全地销毁Net对象,而不会导致feat的内存失效。
2. NetPrivate::forward_layer:递归的依赖解析器
这个函数是 CPU 推理路径的核心调度器,它完美体现了递归和依赖解析的思想。
1 | int NetPrivate::forward_layer(int layer_index, std::vector<Mat>& blob_mats, const Option& opt) const |
关键点解析:
- 递归调用:
for循环检查当前层layer的所有输入blob(layer->bottoms)。如果发现某个输入blob尚未计算 (dims == 0),它不会立即计算当前层,而是递归地调用forward_layer去计算那个输入blob的生产者层。 - DFS 顺序: 这个递归过程本质上是在网络的计算图(DAG)上进行深度优先搜索。只有当一个节点(层)的所有前驱节点(产生其输入的层)都已被访问(计算)后,该节点才会被访问(计算)。
do_forward_layer调用: 当循环结束,意味着当前层的所有输入都已在blob_mats中准备就绪。此时,才调用do_forward_layer来执行当前层的实际计算。
3. NetPrivate::do_forward_layer:层计算执行与内存管理
这个函数负责调用具体 Layer 对象的 forward 或 forward_inplace 方法,并处理相关的内存优化。
1 | int NetPrivate::do_forward_layer(const Layer* layer, std::vector<Mat>& blob_mats, const Option& opt) const |
关键点解析:
one_blob_only分支: 针对单输入输出和多输入输出的层,分别处理输入Mat的获取。- Light Mode 优化 (
opt.lightmode):- In-place 计算: 如果层支持
forward_inplace且开启了 Light Mode,优先调用原地计算版本,直接在输入Mat的内存上写入结果,避免了为输出Mat分配新内存。 - Clone on Shared: 如果输入
Mat的引用计数*refcount大于 1(表示有其他地方也在用这块内存),即使支持 in-place,也必须先clone一份副本,以防原地修改影响其他使用者。 - 输入释放: 计算完成后,调用
blob_mats[bottom_blob_index].release()释放输入Mat的引用。如果引用计数变为 0,内存会被回收。这是 Light Mode 显著降低峰值内存占用的关键。
- In-place 计算: 如果层支持
convert_layout: 在调用forward之前,可能需要根据层的要求(例如,某些层只能处理 Plain Layout)对输入Mat进行布局转换。forward/forward_inplace调用: 这是真正执行该层计算的地方。- 结果缓存: 将计算得到的
top_blob(s)存回blob_mats向量中,供后续层使用或最终由extract返回。
4. 结语
Extractor::extract 并非一个简单的函数调用,而是 ncnn 推理引擎的大门。它巧妙地利用 blob_mats 作为缓存,结合 forward_layer 的递归依赖解析,实现了高效的惰性求值。do_forward_layer 则负责具体的层执行和精细的内存管理策略(尤其是 Light Mode)。整个流程清晰地展现了 ncnn 在追求高性能的同时,对内存占用、计算效率和后端异构性的周全考虑,是理解其核心工作原理的关键环节。
该封面图片由Rezső Terbe在Pixabay上发布





