读 ncnn 源码(Ⅲ):ParamDict 解析、featmask 按层屏蔽、词法器与 blob 索引(含解析实录)

承接前两篇:我们已经理清了 Net::load_param 的主链路与层工厂/覆盖/CPU 指令集优选。本篇把镜头拉近到每一层行尾那串 k=v 参数,以及与之相关的三个关键点:
ParamDict::load_param 的语法与类型系统;
featmask 如何把全局 Option 变成按层屏蔽的局部选项;
find_blob_index_by_name / vstr_* 这些“小工具”在建图与解析中的位置。最后用一段真实 .param(SqueezeNet 的前两层)做逐 token 解析实录

TL;DR

  • ParamDict::load_param(dr) 循环读取 id=value / id=v1,v2,...,支持 int / float / string / int[] / float[] 五种类型;同时兼容“旧式数组语法”(负 id 前缀 + 显式长度)。
  • 解析结果落在 d->params[id]type 编码:2=int3=float5=int[]6=float[]7=string
  • 保留键30top shape hint(每个 top 4 个 int:dims,w,h,c);31featmask
  • featmask 通过 get_masked_option(opt, featmask) 按位关闭 fp16/bf16/int8/Vulkan/Winograd/线程数等能力,实现按层回退与裁剪。
  • find_blob_index_by_nameblob 名映射成索引,配合“先占位、后生产”的策略,把计算图连起来。
  • 一段实录展示:从文件头到 InputConvolution 两层,逐 token 解析、连线、校验权重大小、辅助计算空间尺寸。


1) ParamDict::load_param:一行 k=v 是怎样被“吃”掉的

核心循环的思路很简单:不停尝试 scan("%d=",&id),能读到就进入一次“读值”的流程,直到读不到下一个 id= 为止(意味着到达该层行尾或下一层行首)。

要点分解(伪代码 + 规则):

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
26
27
28
29
30
31
32
33
34
35
36
37
while (dr.scan("%d=", &id) == 1) {
bool is_array = (id <= -23300); // 旧式数组:负 id 表示
if (is_array) id = -id - 23300;

// 边界保护
if (id >= NCNN_MAX_PARAM_COUNT) return -1;

if (is_array) {
// 旧式数组:-233xx=LEN,elem0,elem1,...
int len; dr.scan("%d", &len);
v.create(len);
for j in 0..len-1:
scan 下一个 ",VALUE" → 判断是 float 还是 int
写入并设置 type = 6/5
continue;
}

// 读第一个 token(可能是数字、也可能以引号/字母开头)
char vstr[16]; dr.scan("%15[^,\n ]", vstr);

if (vstr_is_string(vstr)) {
// 字符串:继续把整段补完,去掉末尾引号,type=7
...
continue;
}

bool is_float = vstr_is_float(vstr); // 看到 '.' 或 e/E 就当 float
is_array = (dr.scan("%1[,]", comma) == 1); // 紧跟逗号?→ 新式数组

if (is_array) {
// 新式数组:id=v0,v1,v2,...
收集到 vector,再拷到 Mat v,type=6/5
} else {
// 标量:int 或 float
写入 i/f,type=2/3
}
}

类型系统一览:

  • 标量2=int3=float
  • 数组5=int[]6=float[](承载在 Mat v 中)
  • 字符串7=string
  • 取值:例如 pd.get(30, Mat())(数组/Mat 类参数),或 pd.get(0, 0) / pd.get(1, 1.f)(标量)。

旧式数组(负 id)主要是历史兼容;新项目建议使用新式数组(正常 id + 逗号分隔到行尾),更直观。


2) 小词法器:vstr_is_float / vstr_is_string / vstr_to_float

这些轻量词法函数ParamDict 的底层积木,完全不依赖标准库的解析器,确保行为可控、跨平台一致:

1
2
3
4
5
6
7
8
9
10
11
static bool vstr_is_float(const char vstr[16]) {
// 看到 '.' 或 'e/E' 就按 float 处理
}

static bool vstr_is_string(const char vstr[16]) {
// 以字母或 '"' 开头 → 字符串
}

static float vstr_to_float(const char vstr[16]) {
// 手写 atof:sign → 整数部分 → 小数部分 → e/E 指数(分段乘 1e8 避免误差)
}
  • 为什么自己写:限制 token 长度(vstr[16])、更强的错误控制、更小的依赖面。
  • 判定策略:简化而实用,例如 "1e0" 归为 float;"123" 归为 int;"abc""\"hello" 走字符串路径。

3) featmask:把全局 Option 变成“按层局部选项”

读取 pd 后,框架会取 featmask = pd.get(31, 0),然后做一轮“按位屏蔽”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static Option get_masked_option(const Option& opt, int featmask)
{
Option opt1 = opt;
opt1.use_fp16_arithmetic &= !(featmask & (1<<0));
opt1.use_fp16_storage &= !(featmask & (1<<1));
opt1.use_fp16_packed &= !(featmask & (1<<1)); // 和 storage 绑一起关
opt1.use_bf16_storage &= !(featmask & (1<<2));
opt1.use_int8_packed &= !(featmask & (1<<3));
opt1.use_int8_storage &= !(featmask & (1<<3));
opt1.use_int8_arithmetic &= !(featmask & (1<<3));
opt1.use_vulkan_compute &= !(featmask & (1<<4));
opt1.use_tensor_storage &= !(featmask & (1<<4));
opt1.use_sgemm_convolution &= !(featmask & (1<<5));
opt1.use_winograd_convolution&= !(featmask & (1<<6));
if (featmask & (1<<7)) opt1.num_threads = 1;
return opt1;
}

理解要点:

  • 这是按层的能力裁剪:哪怕全局允许 fp16/int8/Vulkan,只要该层设了对应位,就局部禁用
  • 有些位是成组的:比如 use_fp16_storageuse_fp16_packed 同时受 bit1 控制;int8 的三项一起关。
  • bit4 同时关掉 use_vulkan_computeuse_tensor_storage,保证回退到 CPU 路径。
  • bit7 让本层强制单线程,有时用于规避数据竞争或便于调试。

随后,如果该层最初拿到的是 Vulkan 实现,但 opt1.use_vulkan_compute 被关或层本身 support_vulkan=false就地回退为 CPU 实现(保持连接、重走一次 load_param(pd))。这与我们在第Ⅰ篇看到的“按层回退”呼应。


4) find_blob_index_by_name:图连线的“路由表”

Net::load_param 的主循环里,遇到 bottom 名会调用它来找索引:

1
2
3
4
5
6
7
8
int Net::find_blob_index_by_name(const char* name) const
{
for (size_t i = 0; i < d->blobs.size(); i++)
if (d->blobs[i].name == name) return (int)i;

NCNN_LOGE("find_blob_index_by_name %s failed", name);
return -1;
}

配套策略:*如果找不到(返回 -1),load_param 会*新建一个占位 blob(用当前 blob_index,并命名为该 bottom 的名字),以支持“先引用,后定义”的写法。
当某层随后把这个名字作为 top 产出时,就会正好“对上先前的占位槽”,从而把
producer/consumer** 链接完整。

心智图:Layer 是节点,Blob 是边;这里就是把“边名 → 边号”的路由找出来(必要时先建一个空边位)。


5) 解析实录:SqueezeNet 的前两层(逐 token)

给定 .param 片段:

1
2
3
4
7767517
48 56
Input data 0 1 data 0=227 1=227 2=3
Convolution conv1 1 1 data conv1_relu_conv1 0=64 1=3 3=2 5=1 6=1728 9=1

(1) 文件头

  • magic=7767517 ✅;
  • layer_count=48blob_count=56;分配 d->layersd->blobsblob_index=0

(2) 第 1 层:Input

  • 头部:type="Input", name="data", bottom_count=0, top_count=1
  • tops:读到 top 名 "data" → 使用当前 blob_index=0 新建 blob:name="data", producer=layer0,并 layer->tops[0]=0blob_index++ → 1。
  • ParamDict:依次扫描 0=2271=2272=3
    • 都是 int 标量(没有 .e/E);
    • 语义(Input 层):w=227, h=227, c=3;类型码 type=2
  • layer->load_param(pd):落入成员,结束该层。

此时:blob #0 = "data",由 Input 产出,是网络输入。

(3) 第 2 层:Convolution

  • 头部:type="Convolution", name="conv1", bottom_count=1, top_count=1
  • bottoms:读 "data"find_blob_index_by_name("data") 命中 0;标记 consumer=layer1layer->bottoms[0]=0
  • tops:读 "conv1_relu_conv1" → 使用当前 blob_index=1 新建 blob:name=..., producer=layer1layer->tops[0]=1blob_index++ → 2。
  • ParamDict:扫描一串键值
    • 0=64num_output=64(int)
    • 1=3kernel=3 (int)
    • 3=2stride=2 (int)
    • 5=1bias_term=1(int)
    • 6=1728weight_data_size=1728(int)
    • 9=1activation_type=1fused ReLU,卷积内置激活)
  • 快速校验:weight_data_size = kw*kh*in_c*out_c
    • 此时 in_c=3(来自上一层),kw=kh=3out_c=643×3×3×64 = 1728
  • 空间尺寸(辅助理解):227×227×3 --(3×3,s=2,pad=0)--> floor((227-3)/2)+1 = 113113×113×64

此时

  • blob #0 "data":被 conv1 消费;
  • blob #1 "conv1_relu_conv1":将被下一层(比如 pool1)作为 bottom 消费。

6) 速查小卡

  • ParamDict 类型码2=int3=float5=int[]6=float[]7=string
  • 保留键30=top shape hints(每个 top:dims,w,h,c),31=featmask
  • 数组写法:倾向使用 id=v0,v1,...新式数组;旧式 -233xx=LEN,v0,... 仅用于兼容
  • layer/Blob 关系:Layer 是节点,Blob 是find_blob_index_by_name 负责把“边名 → 边号”对上,必要时先占位
  • 按层回退featmaskget_masked_option→若 Vulkan 不可/禁用则重建 CPU 版本层;CPU 路径内部再按 AVX512/FMA/AVX… 逐层回退

结语

至此,*“从一行 .param 到层参数落地、到图上 blob 的连接与索引、到按层屏蔽与回退的决策”*这条微观路径就完整了。

该封面图片由WONHO SONPixabay上发布