读 ncnn 源码(Ⅲ):ParamDict 解析、featmask 按层屏蔽、词法器与 blob 索引(含解析实录)
读 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=int
、3=float
、5=int[]
、6=float[]
、7=string
。 - 保留键:
30
为 top shape hint(每个 top 4 个 int:dims,w,h,c
);31
为 featmask。 featmask
通过get_masked_option(opt, featmask)
按位关闭 fp16/bf16/int8/Vulkan/Winograd/线程数等能力,实现按层回退与裁剪。find_blob_index_by_name
把 blob 名映射成索引,配合“先占位、后生产”的策略,把计算图连起来。- 一段实录展示:从文件头到
Input
与Convolution
两层,逐 token 解析、连线、校验权重大小、辅助计算空间尺寸。
1) ParamDict::load_param
:一行 k=v
是怎样被“吃”掉的
核心循环的思路很简单:不停尝试 scan("%d=",&id)
,能读到就进入一次“读值”的流程,直到读不到下一个 id=
为止(意味着到达该层行尾或下一层行首)。
要点分解(伪代码 + 规则):
1 | while (dr.scan("%d=", &id) == 1) { |
类型系统一览:
- 标量:
2=int
、3=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 | static bool vstr_is_float(const char vstr[16]) { |
- 为什么自己写:限制 token 长度(
vstr[16]
)、更强的错误控制、更小的依赖面。 - 判定策略:简化而实用,例如
"1e0"
归为 float;"123"
归为 int;"abc"
或"\"hello"
走字符串路径。
3) featmask
:把全局 Option
变成“按层局部选项”
读取 pd
后,框架会取 featmask = pd.get(31, 0)
,然后做一轮“按位屏蔽”:
1 | static Option get_masked_option(const Option& opt, int featmask) |
理解要点:
- 这是按层的能力裁剪:哪怕全局允许 fp16/int8/Vulkan,只要该层设了对应位,就局部禁用。
- 有些位是成组的:比如
use_fp16_storage
和use_fp16_packed
同时受bit1
控制;int8 的三项一起关。 bit4
同时关掉use_vulkan_compute
与use_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 | int Net::find_blob_index_by_name(const char* name) const |
配套策略:*如果找不到(返回 -1),load_param
会*新建一个占位 blob(用当前 blob_index
,并命名为该 bottom 的名字),以支持“先引用,后定义”的写法。
当某层随后把这个名字作为 top 产出时,就会正好“对上先前的占位槽”,从而把producer/consumer** 链接完整。
心智图:Layer 是节点,Blob 是边;这里就是把“边名 → 边号”的路由找出来(必要时先建一个空边位)。
5) 解析实录:SqueezeNet 的前两层(逐 token)
给定 .param
片段:
1 | 7767517 |
(1) 文件头
magic=7767517
✅;layer_count=48
,blob_count=56
;分配d->layers
与d->blobs
;blob_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]=0
;blob_index++
→ 1。 ParamDict
:依次扫描0=227
、1=227
、2=3
:- 都是 int 标量(没有
.
或e/E
); - 语义(Input 层):
w=227, h=227, c=3
;类型码type=2
。
- 都是 int 标量(没有
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=layer1
,layer->bottoms[0]=0
。 - tops:读
"conv1_relu_conv1"
→ 使用当前blob_index=1
新建 blob:name=...
,producer=layer1
,layer->tops[0]=1
;blob_index++
→ 2。 ParamDict
:扫描一串键值0=64
→num_output=64
(int)1=3
→kernel=3
(int)3=2
→stride=2
(int)5=1
→bias_term=1
(int)6=1728
→weight_data_size=1728
(int)9=1
→activation_type=1
(fused ReLU,卷积内置激活)
- 快速校验:
weight_data_size = kw*kh*in_c*out_c
- 此时
in_c=3
(来自上一层),kw=kh=3
,out_c=64
→3×3×3×64 = 1728
✅
- 此时
- 空间尺寸(辅助理解):
227×227×3 --(3×3,s=2,pad=0)--> floor((227-3)/2)+1 = 113
→113×113×64
。
此时:
- blob #0
"data"
:被conv1
消费;- blob #1
"conv1_relu_conv1"
:将被下一层(比如pool1
)作为 bottom 消费。
6) 速查小卡
- ParamDict 类型码:
2=int
、3=float
、5=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
负责把“边名 → 边号”对上,必要时先占位 - 按层回退:
featmask
→get_masked_option
→若 Vulkan 不可/禁用则重建 CPU 版本层;CPU 路径内部再按 AVX512/FMA/AVX… 逐层回退
结语
至此,*“从一行 .param
到层参数落地、到图上 blob 的连接与索引、到按层屏蔽与回退的决策”*这条微观路径就完整了。