读 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) 自动识别:
    • 0x01306B47float16
    • 0x000D4B38int8(逐字节);
    • 0x0002C056float32(带额外缩放的 raw)
    • 其它:如果 flag!=0表驱动量化(读 256 个 float 的查表 + 索引数组);若 flag==0 且 f0==0float32 raw
  • 零拷贝/对齐:优先尝试 DataReader::reference()zero-copy;不足则回落到 read() 并按 4 字节对齐(alignSize(...,4))。
  • 读完模型后,若启用 Vulkan,会 上传权重;若启用本地内存池,会创建 PoolAllocator 作为 blob/workspace 的默认分配器。


1) 顶层流程:Net::load_model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int Net::load_model(const DataReader& dr)
{
if (d->layers.empty()) { NCNN_LOGE("network graph not ready"); return -1; }

// Vulkan pipeline cache 准备(如启用)
ModelBinFromDataReader mb(dr);

for (int i = 0; i < layer_count; i++)
{
Layer* layer = d->layers[i];
if (!layer) { /* param 与图不一致 */ break; }

// 1) 逐层读取权重
if (layer->load_model(mb) != 0) { /* 报错并中止 */ break; }

// 2) 按层裁剪 Option(featmask),构建 pipeline(权重预处理等)
Option opt1 = get_masked_option(opt, layer->featmask);
if (layer->create_pipeline(opt1) != 0) { /* 报错并中止 */ break; }
}

// 3) 可选:本地内存池(blob/workspace)准备
// 4) 可选:Vulkan 模型上传(权重/常量上 GPU)

return ret;
}

要点:

  • 顺序是“先读权重,再建 pipeline”。例如卷积会在 create_pipeline() 里把权重变换到 sgemm/winograd 等布局;激活子层也可能在这里构造好。
  • get_masked_option(opt, layer->featmask) 让每层可以关闭/回退某些能力(fp16/int8/Vulkan/Winograd/线程数等),这一点我们在(Ⅲ)里详解过。

2) 读什么?从哪读?—— DataReader* 抽象层

你可以把模型数据放在不同介质里,ncnn 用统一接口读取:

  • DataReaderFromStdioFILE* 封装(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
2
Option opt1 = get_masked_option(opt, layer->featmask);
layer->create_pipeline(opt1);
  • 典型操作包括:权重预变换(例如卷积的 sgemm/im2col 布局、Winograd 权重)、激活子层构建、算法路径选择器的预计算、以及各后端的 kernel/pipeline 准备。
  • 若启用了 Vulkan,load_model() 末尾还会 upload_model()(把常量/权重从 CPU 侧上传到 GPU)。

4) 两个“实用分叉”:内存池与 Vulkan

  • 本地内存池(可选):

    1
    2
    3
    4
    5
    if (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
    6
    if (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 校验;若不匹配,通常是 .paramin_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

加载顺序

  1. load_param(建图/ParamDict/featmask)
  2. load_model:逐层 load_model(mb)create_pipeline(opt1)
  3. (可选)Vulkan upload_model / 本地 PoolAllocator 准备
  4. 推理期 forward() 使用已准备好的 pipeline

结语

至此,.bin 的读法、权重在每层的落位、以及读完后的 pipeline 构建都串起来了。

该封面图片由Joachim SchnürlePixabay上发布