0.8MB 跑通 Qwen|第 16-2 篇:逐字段解剖——推理引擎权重格式的 VQFHeader / VQFSig / VQFTensor

简介: 本篇深度解析Qwen推理引擎VQF v2文件头:432字节固定布局、小端序、64B对齐目录起始(448偏移),通过`_Static_assert`严控结构体尺寸,以`magic/version/size`三重门禁保障零依赖纯C加载安全。真机RK3588实测,揭穿注释与字节不符彩蛋,夯实字节序纪律。(239字)

#

系列:《0.8MB 跑通 Qwen:从零实现 ARM 零依赖纯 C 推理引擎》(30 天 × 90 篇) | 适配模型:Qwen3-VL-8B-Instruct(千问3_VL_8B_Instruct)· Qwen3-VL-2B-Instruct · Qwen3-30B-A3B | 测试设备:RK3588(4×Cortex-A76 + 4×Cortex-A55,aarch64)

系列总纲:《0.8MB 跑通 Qwen》30 天 90 篇 · 总纲(阿里云社区)

上一篇:16-1《VQF 的野心:把量化与布局固化到文件》| 下一篇:16-3《tensor 目录与布局重排的落盘顺序》

真机实测通过:本文实验已在 RK3588 板端实测完成(2026-09;方法学与原始记录见仓库 docs 与《实验脚本》目录)

一句话导读:推理引擎的 VQFHeader 逐字段解剖:从 432 字节的 v2 头部出发,沿固定偏移拆 VQFSig 与 VQFTensor,看 _Static_assert 与版本门禁如何把关,并起底一处注释与真实字节不符的彩蛋。

关键词:手搓 Qwen 推理引擎、千问大模型推理、VQFHeader、VQFSig、VQFTensor、VQF、字节序、Qwen3-VL、零依赖纯 C

导语:上一回看清了 VQF 的整体野心,这一篇把镜头推到字节级:432 字节的文件头如何用固定偏移而非自描述来存放 VQFSig 与 VQFTensor,_Static_assert 又怎么替这组数字上保险。跟着 od 的真机对照,读懂决定千问模型权重能否被零依赖纯 C 引擎直接挂载的头部契约。

16-1 看了"野心",今天把 432 字节的头部拆到字节级:每个字段在文件里的偏移、真实文件里对应的十六进制、以及 _Static_assert 为什么是这组数字的保险。真机上 od 逐字节对照,顺便抓一个"文档注释与真实字节不符"的彩蛋——这正是 Day 3 字节序纪律的实战。

1. 知识点:小头 + 大目录 + 页对齐的固定布局

VQF v2 的文件头是一段不靠自描述、靠固定偏移的 C 结构体直接落盘(little-endian):

偏移 大小 字段 本文件实测
0 4 magic = VQF_MAGIC 46 51 46 57
4 4 version = VQF_VERSION 02 00 00 00 → 2
8 4 flags 63 00 00 00 → 0x063
12 4 n_tensors 29 00 00 00 → 41
16 164 arch(VQFArch,模型属性自包含) dim=2048 / layers=28 / …
180 4 (对齐填充到 8B 边界) 00 00 00 00
184 8 data_offset(数据区起始,4KB 页对齐) 00 10 00 00 00 00 00 00 → 4096
192 8 file_len(不含尾部 tag) 00 10 f5 8a 00 00 00 00 → 2,331,316,224
200 16 enc_iv[16](加密时有效) 全 0(明文)
216 16 enc_rsvd[16] 全 0
232 200 sig(VQFSig,SM2 签名块) 全 0(未签名)
432 16 (保留,头部结构体之外) 全 0
448 n×64 目录 VQFTensor[] 41 项,token_embed 起

头部固定 432 B(_Static_assert(sizeof(VQFHeader) == 432)),目录从 448 起——432 与 448 之间那 16 B 是结构体外的保留区,为什么不是 432 直接从目录开始?因为目录要"64B 对齐起步":dir_off = (432 + 63) & ~63 = 448(vqf.c 第 316 行)。头 + 目录整体塞进第一个 4KB 页(本文件目录占 41×64 = 2,624 B,448 + 2,624 = 3,072 < 4,096,正好一页;张量再多会整体把数据区推到下一页,见 16-3 的公式)。

为什么字段全用固定偏移、不用"tag + 变长"? 因为加载器要把头直接当作 C 结构体读(const VQFHeader *h = (const VQFHeader *)m.data;,vqf.c 第 560 行),任何变长都会让这个转换失效。代价是版本不兼容必须显式管理——这就是 version 字段存在的意义:v1 与 v2 的头部大小都不同(v1 没有 200 B 的 VQFSig,头 232 B,目录从 256 起),读取器绝不能拿 v2 的结构体去读 v1 的文件,所以 version 是 mmap 之后第一个要判的字段(vqf.c 第 562–566 行:[VQF] version %u unsupported)。

2. 对应代码:三个结构体 + 三个 _Static_assert

vqf_format.h 里依次是三个结构体:

typedef struct {
                          /* VQFSig,200 B(第 69–77 行) */
    uint8_t  pub[64];                  /* SM2 公钥 x||y */
    uint8_t  r[32], s[32];             /* 签名分量 */
    uint8_t  digest[32];               /* 被签名的摘要 D */
    uint8_t  id[VQF_SIG_ID_MAX];       /* ID_A(自描述) */
    uint32_t id_len, rsvd;
} VQFSig;

typedef struct {
                          /* VQFHeader,432 B(第 79–90 行) */
    uint32_t magic, version, flags, n_tensors;
    VQFArch  arch;                     /* 164 B */
    uint64_t data_offset, file_len;
    uint8_t  enc_iv[16], enc_rsvd[16];
    VQFSig   sig;
} VQFHeader;

typedef struct {
                          /* VQFTensor,64 B(第 92–98 行) */
    char     name[32];                 /* 内部张量名:q4_q / v_q8_fc1 / token_embed ... */
    uint32_t qtype;                    /* VQF_QT_* */
    uint32_t rows, cols;               /* 几何(诊断用) */
    uint64_t offset, bytes;            /* 64B 对齐的偏移与字节数 */
} VQFTensor;

头文件末尾(第 101–103 行)的三行是"三方共享头"的保险:

_Static_assert(sizeof(VQFSig)   == 200, "VQFSig layout drift");
_Static_assert(sizeof(VQFHeader) == 432, "VQFHeader layout drift");
_Static_assert(sizeof(VQFTensor) == 64,  "VQFTensor layout drift");

这个头被引擎(vqf.c)、离线签名工具、KAT 测试三方共用(头注释第 1–7 行)。任何一方改了字段导致尺寸漂移,编译期直接失败——VQFTensor 尤其关键:它内部有 4 B 对齐填充(name[32] 之后 qtype/rows/cols 各 4 B 到 44 B,offset 是 u64 需要 8B 对齐,所以在 44–48 处自动插 4 B 填充,offset 从 48 起),如果谁用 sizeof(VQFTensor) 之外的假设去遍历目录,就会像 Day 2 讲的 _Static_assert 一样当场暴露。

3. 改动后果:板端十六进制逐字段对照

实测口径:RK3588(Orange Pi 5 Plus)/ aarch64 / 2026-09-07。文件:/mnt/emmc/Modl/Qwen3-VL-2B-Instruct/model.vqf;工具:od -A x -t x1。

3.1 头部前 16 字节(偏移 0–15)

$ od -A x -t x1 -j 0 -N 16 model.vqf
000000 46 51 46 57 02 00 00 00 63 00 00 00 29 00 00 00

逐字段:magic = 46 51 46 57;version = 02 00 00 00;flags = 63 00 00 00;n_tensors = 29 00 00 00 = 41。

彩蛋来了:文档与头注释都把这个 magic 标注为 "VQFW"(技术文档 §4.1 第 202 行 magic "VQFW" 0x57465146,vqf_format.h 第 21 行 /* "VQFW" */)。可真实文件头 4 字节是 46 51 46 57,按 ASCII 读是 FQFW。为什么对不上?

因为常量才是真,注释只是人写的。数值 0x57465146 的最高字节是 0x57('W'),最低字节是 0x46('F');小端落盘时最低字节在前,于是磁盘上是 46 51 46 57。若想让字节序列拼出 VQFW(56 51 46 57),常量得是 0x57465156——和实际的 0x57465146 只差最低一字节(0x56='V' vs 0x46='F')。也就是说这个 ASCII 标注大概率是从"本意"抄错了。好在 magic 只参与数值比较(vqf.c 第 561 行 h->magic != VQF_MAGIC),标注不影响任何校验;但这正是一个完美的教学样本:字节序纪律下,要以 od/xxd 的字节为准,注释和文档都可能骗你(Day 3 的"十六进制纪律"在这里抓到一个活的例子)。

3.2 中段:data_offset / file_len(偏移 184 / 192)

$ od -A x -t x1 -j 184 -N 24 model.vqf
0000b8 00 10 00 00 00 00 00 00 00 10 f5 8a 00 00 00 00
0000c8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  • data_offset(184)= 00 10 00 00 00 00 00 00 → 0x1000 = 4096(4KB 页对齐 ✅);
  • file_len(192)= 00 10 f5 8a 00 00 00 00 → 0x000000008af51000 = 2,331,316,224;
  • 文件总长 2,331,316,232 = file_len + 8(尾部 FNV checksum)→ 加载器的 size 兜底(vqf.c 第 569–573 行:m.len != h->file_len + tag_len 即拒)在每次加载都会过一遍;
  • 200 起 32 B 全 0 = enc_iv + enc_rsvd 均空(明文文件,符合 flags 无 ENC 位)。

3.3 arch 与 config.json 的对照(自包含的边界)

解析器把 arch(偏移 16 起,164 B)读出来,与模型目录的 config.json 对照(板端 jq):

字段 VQF arch(文件) config.json text_config 一致?
dim / hidden_size 2048 2048 ✅
n_layers 28 28 ✅
n_heads / n_kv_heads 16 / 8 16 / 8 ✅
head_dim 128 128 ✅
ffn_dim / intermediate_size 6144 6144 ✅
vocab_size 151,936 151,936 ✅
rope_theta 5,000,000 5,000,000 ✅
max_seq_len 8192 max_position_embeddings = 262,144 ❌

不一致的 max_seq_len 恰恰是"自包含 ≠ 照抄 HF"的证据:转换时引擎把它截断到引擎支持的 8192(main.c 第 4047 行 if (cfg.max_seq_len > 8192) cfg.max_seq_len = 8192;,safetensors 路径同样在第 4291 行,注释写明"262144 若不截断会在初始化时崩掉缓存")。所以 VQF arch 记录的是"这台引擎实际可用的属性",不是 HF 配置的镜像——文件自包含的是运行时事实,不是上游事实。serve 日志 [SERVE] model=... max_seq=8192 打印的正是这个截断值。

3.4 版本门禁:拿真实 v1 文件头页实测"逐字段把关"

model 目录里存着旧版文件 model.vqf.v1.bak(4,159,295,496 B)。它头部前 16 字节是:

$ xxd -g1 -l 16 model.vqf.v1.bak
46 51 46 57 01 00 00 00 63 00 00 00 31 00 00 00

对比 v2:magic 相同(同族格式),version = 01(v1),flags 相同(0x063),n_tensors = 49(v1 带 x8 副本张量,文件更大)。v1 头没有 200 B 的 VQFSig,头部只有 232 B、目录从 256 起——结构体完全不同,绝不能混读。

板端实测:把 v1 文件的真实头页(4 KB)放进一个 v2 模型的目录结构里 serve(只拷头页,数据区未拷贝——因为版本检查在读数据之前就会拦下):

[SERVE] auto-load: loading model .../v1dir (wmode=0)
[VQF] version 1 unsupported (need 2)          ← vqf.c:563 拦截
[SERVE] FATAL: model load failed (see [ST]/[TOK]/[VIS] errors above)
[SERVE] VQF load failed: .../v1dir/model.vqf

注意日志顺序:version 检查发生在 size 兜底之前(vqf.c 第 562 行 version 判定在 569 行 size 判定之前),所以哪怕文件被截断到只剩头页,读取器也先报"版本不支持"而不是"大小不对"——字段把关有严格的先后顺序:magic(文件是不是 VQF 族)→ version(我能不能用当前结构体读你)→ size(文件有没有被截断/拼接)→ 布局 flags(数据长什么样跟我认不认)→ 尾部 tag(内容有没有被篡改)。每一关都只需要上一个字段的结论,这是固定偏移头部才做得到的。

4. 学员调试任务

  • A 档(板端动手):① 用 od 分别 dump 偏移 0、184、192 处,把字节抄下来并换算成十进制,与 parse_vqf.py 输出核对;② 对 model.vqf.v1.bak 读前 16 字节,回答:它和 v2 的差异字段是哪个?③ 用 Python struct.unpack('<I', data[8:12]) 读 flags,再手动解出 0x063 的每个置位。
  • B 档(纯读源码):读 权重保护与可验证推理方案.md §3.3 的字段表与 vqf_format.h,回答:① 为什么 VQFSig 会让目录起点从 v1 的 256 挪到 v2 的 448?(算一下 232+200 后的 64B 对齐)② 若有人把 n_tensors 改成 42 而不动目录,加载器先在哪一关失败?③ file_len 为什么"不含尾部 tag"?若含了,size 兜底还能不能成立?

预期输出:一张你自己抄过字节、亲手换算过的头部字段表;并能解释"version 决定能否继续读、size 决定有没有被截断"两道闸的分工。

收尾

  • 本篇源码点名:vqf_format.h(VQFSig 69–77、VQFHeader 79–90、VQFTensor 92–98、_Static_assert 101–103)、vqf.c(magic 561、version 562–566、size 569–573、layout flags 702–713)、main.c(max_seq 截断 4047/4291)、权重保护与可验证推理方案 §3.3
  • 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
  • 下篇预告:头部看完了,真正告诉你"权重在哪、多大"的是目录。16-3 解剖 64 B 的 VQFTensor、数据区的落盘顺序,并交给你一个 10 行 Python 解析器 + FNV 校验复算的完整闭环。
相关文章
|
1天前
|
人工智能 Rust 自然语言处理
彻底放弃TypeScript,微软用Copilot+Rust重构Copilot
GitHub用Copilot AI将自身运行时从TS迁至Rust:14.5周生成80万行Rust代码,性能提升16倍、内存降90%;耗AI Token 12万美元,仅需1工程师审3周。Rust胜在嵌入性、安全与资源效率,但TS仍适合快速迭代——重写非银弹,重在权衡。(239字)
|
1天前
|
缓存 人工智能 索引
0.8MB 跑通 Qwen|第 12-1 篇:LLM 推理的跨进程恢复——服务重启了,会话不能断
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,实现在RK3588上部署Qwen3-VL等多尺寸模型。本文详解`--disk-kv`机制:将KV Cache落盘持久化,支持进程重启后按前缀精准恢复,实现“会话不断连”,2K上下文prefill加速达23.6×,真正打通边缘AI服务可用性最后一环。(239字)
|
1天前
|
缓存
0.8MB 跑通 Qwen|第 8-3 篇:推理引擎的 KV 缓存结构——按 token 还是按头存
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多模态模型,在RK3588上实测。本文精析KV缓存的token-major布局设计原理,揭示其如何兼顾prefill与decode访存效率,直击带宽瓶颈。(239字)
|
1天前
|
JSON 测试技术 API
0.8MB 跑通 Qwen|第 20-2 篇:推理引擎的路由与自研最小 JSON
本系列《0.8MB跑通Qwen》用纯C手搓零依赖推理引擎,适配Qwen3-VL多模型,在RK3588(aarch64)实测通过。核心含strcmp路由表与自研轻量JSON解析器,精准拦截非法请求(如`msgs`→400),严守兼容边界,代码精简可控,专注教学与边缘部署。(239字)
|
1天前
|
编解码 Java 测试技术
0.8MB 跑通 Qwen|第 18-3 篇:MRoPE——推理引擎的 3D 位置编码给视觉留的席位
本篇详解Qwen3-VL多模态推理中MRoPE三维位置编码机制:通过`mrope_section=[24,20,20]`将64维旋转频率分给时间/高/宽三轴,支持视觉token网格定位;并数值推演“强行降维至1D”的相位偏差——7×7图像下最高达4.71弧度(近270°),40/64维严重失准,导致注意力失效。纯文本因t=h=w自动退化,故该bug无法被文本测试捕获。(239字)
|
1天前
|
C语言 C++
0.8MB 跑通 Qwen|第 17-1 篇:safetensors → VQF——推理引擎的 vqf_write 一次成型
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多模态模型(2B/8B/30B),在RK3588平台实测通过。核心突破:safetensors→VQF转换与推理共用同一量化路径,实现位级可复现,体积压缩至2.2–4.2GB,真机重转SHA256完全一致。(239字)
|
1天前
|
C++
0.8MB 跑通 Qwen|第 10-2 篇:推理引擎的 sparse top-k 块选择——"只看该看的"到底怎么选
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,实测RK3588上高效运行Qwen3-VL多模态模型。本文深度剖析sparse_attn_head的top-k块选择机制——探针抽样、prefill重要性预留、贪心补满与recency保险四段代码,揭示长上下文稀疏注意力如何精准“找针”,并实证k=1时板端翻车根源。(239字)
|
1天前
|
定位技术 C语言 C++
0.8MB 跑通 Qwen|第 7-1 篇:大模型量化的 Q4_0 格式——与 GGUF 对齐的 4bit 布局
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多模态模型,在RK3588上实测通过。详解GGUF对齐的Q4_0量化:32个float压缩至18字节(f16 scale + 16 nibble),剖析nibble打包规则与11倍误差代价,为端侧高效部署夯实底层基础。(239字)
|
1天前
|
缓存 NoSQL 区块链
0.8MB 跑通 Qwen|第 15-2 篇:推理引擎的 prefill 与 decode——两条路径为何分开
本篇详解Qwen推理引擎中prefill与decode双路径设计:prefill一次性处理整段prompt(如18 token),批量写入KV缓存;decode逐token循环生成,追加KV。通过RK3588真机gdb断点实证,明确二者独立入口、状态流转与性能动因,手搓零依赖纯C引擎的核心逻辑。(239字)
|
1天前
|
存储 安全 C语言
0.8MB 跑通 Qwen|第 5-2 篇:推理引擎的 Q8 对称量化——scale 从哪来
本系列聚焦ARM端零依赖纯C推理引擎,实测RK3588跑通Qwen3-VL多模态模型。本文详解Q8对称量化核心——scale标定:为何用f16存、为何除127、误差如何从0.033暴增至0.44,揭示量化精度的生死线。(239字)

热门文章

最新文章