读 ncnn 源码(Ⅵ):模型权重加载链路 —— DataReader / ModelBin / create_pipeline
读 ncnn 源码(Ⅵ):模型权重加载链路 —— DataReader / ModelBin / create_pipeline
前面我们把
.param从 文本 → ParamDict → 图结构 → I/O 名单 的路径讲圆了。本篇继续往下:如何把.bin里的权重读入每一层,以及在“读完权重”后做的 create_pipeline() 预处理(如权重变换、缓存准备等)。
TL;DR
Net::load_model()需要 图已建立(先load_param)。它为每层依次执行:layer->load_model(ModelBin&)→layer->create_pipeline(opt1)。opt1是按层featmask局部裁剪后的Option。- 数据来源 通过多种
DataReader封装:FromStdio / FromMemory / FromAndroidAsset;上层统一交给ModelBinFromDataReader解析。 - 权重编码(在
.bin)由ModelBinFromDataReader::load(w, type)自动识别:0x01306B47→ float16;0x000D4B38→ int8(逐字节);0x0002C056→ float32(带额外缩放的 raw);- 其它:如果
flag!=0→ 表驱动量化(读 256 个 float 的查表 + 索引数组);若flag==0 且 f0==0→ float32 raw。
- 零拷贝/对齐:优先尝试
DataReader::reference()做 zero-copy;不足则回落到read()并按 4 字节对齐(alignSize(...,4))。 - 读完模型后,若启用 Vulkan,会 上传权重;若启用本地内存池,会创建 PoolAllocator 作为
blob/workspace的默认分配器。

1) 顶层流程:Net::load_model
1 | int Net::load_model(const DataReader& dr) |
要点:
- 顺序是“先读权重,再建 pipeline”。例如卷积会在
create_pipeline()里把权重变换到 sgemm/winograd 等布局;激活子层也可能在这里构造好。 get_masked_option(opt, layer->featmask)让每层可以关闭/回退某些能力(fp16/int8/Vulkan/Winograd/线程数等),这一点我们在(Ⅲ)里详解过。
2) 读什么?从哪读?—— DataReader* 抽象层
你可以把模型数据放在不同介质里,ncnn 用统一接口读取:
DataReaderFromStdio:FILE*封装(fopen)scan()(仅在NCNN_STRING时用于文本)read()读二进制
DataReaderFromMemory:从内存 buffer 读取- 适合把模型 mmap/打包进可执行或放在自定义内存区域
DataReaderFromAndroidAsset(Android)- 直接从 APK asset 读,免去落盘
所有这些在 Net::load_model(FILE*)/load_model(const char*) 里只是薄薄的封装,最终都会走同一个实现:load_model(const DataReader& dr)。
3) 读完之后做什么?—— create_pipeline() 与后处理
load_model() 的第二步是:
1 | Option opt1 = get_masked_option(opt, layer->featmask); |
- 典型操作包括:权重预变换(例如卷积的 sgemm/im2col 布局、Winograd 权重)、激活子层构建、算法路径选择器的预计算、以及各后端的 kernel/pipeline 准备。
- 若启用了 Vulkan,
load_model()末尾还会upload_model()(把常量/权重从 CPU 侧上传到 GPU)。
4) 两个“实用分叉”:内存池与 Vulkan
-
本地内存池(可选):
1
2
3
4
5if (opt.use_local_pool_allocator) {
if (!opt.blob_allocator) d->local_blob_allocator = new PoolAllocator;
if (!opt.workspace_allocator) d->local_workspace_allocator = new PoolAllocator;
// set_size_compare_ratio(0.f) → 严格相等再复用,减少内存抖动
}这样不用你手动注入分配器,也能获得不错的复用效果。
-
Vulkan pipeline cache(可选):
1
2
3
4
5
6if (opt.use_vulkan_compute) {
if (!opt.pipeline_cache) {
if (!d->pipeline_cache) d->pipeline_cache = new PipelineCache(d->vkdev);
opt.pipeline_cache = d->pipeline_cache;
}
}保存着已编译的着色器/pipeline,避免每次 warm-up 都重复构建。
5) 常见坑与定位
- “network graph not ready”
你在load_param之前就调用了load_model。顺序必须是:先图,后权重。 - “parameter file has inconsistent content”
循环时发现某个 layer 指针为nullptr,说明.param与已注册层不一致(拼写、裁剪或版本问题)。 - 权重长度对不上
层内部会用weight_data_size校验;若不匹配,通常是.param的in_c/out_c/kernel或.bin的导出不一致。 - 类型分支不命中
.bin头部 4 字节不是预期的 tag,又flag==0但数据也不对 → 检查导出/对齐;同时注意大小端差异。 - Vulkan 相关失败
先在 CPU 跑通;确认use_vulkan_compute与设备/驱动可用,再打开。ncnn 会按层自动回退 CPU。
6) 速查表
ModelBin 标志位(4 字节) → 解析路径
0x01306B47→ float16(对齐、可零拷贝、小端优先 reference)0x000D4B38→ int8(按字节,4 字节对齐)0x0002C056→ float32(raw;“带额外缩放”的语义由层控制)- 其它:
flag != 0→ 表驱动量化(256×float 查表 + 索引数组)flag_struct.f0 == 0→ float32 raw
加载顺序
load_param(建图/ParamDict/featmask)load_model:逐层load_model(mb)→create_pipeline(opt1)- (可选)Vulkan
upload_model/ 本地PoolAllocator准备 - 推理期
forward()使用已准备好的 pipeline
结语
至此,.bin 的读法、权重在每层的落位、以及读完后的 pipeline 构建都串起来了。
该封面图片由Joachim Schnürle在Pixabay上发布
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 James的成长之路!
评论





