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字)

系列:《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-3《tensor 目录与布局重排的落盘顺序》| 下一篇:17-2《GGUF → VQF:Q4_0…Q8_K 反量化再量化》

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

一句话导读:推理引擎的 safetensors 转 VQF 一次成型:4.25 GB 的源模型如何变成 VQF 单文件,转换与 serve 共用同一条加载量化路径、重排只在转换时发生一次,板端同源重转验证位级可复现。

关键词:手搓 Qwen 推理引擎、千问大模型推理、safetensors、vqf_write、VQF、量化、位级一致、Qwen3-VL、零依赖纯 C

导语:权重从 safetensors 变成 VQF 单文件,凭什么能保证推理时读到的就是转换时写入的?答案藏在一条共用代码路径里——转换与 serve 走同一套量化、重排函数,重排只在转换期发生一次。这篇看 vqf_write 如何一次成型,并用板端同源重转检验千问权重的位级可复现。

Day 16 把 VQF 从头解剖到字节。今天看"写"的一侧:一个 4.25 GB 的 safetensors 模型,怎么变成 2.33~4.16 GB 的 VQF 单文件?答案在两条纪律里:转换序列与 serve 加载完全同一条代码路径(保证位级一致),量化与重排只在转换时发生一次(运行时零转换)。板端真机重转三次给你看。

1. 知识点:转换 = "加载一遍,原样固化"

关键认知:VQF 转换不是"转格式",而是"把内存里已经量化重排好的权重,按原布局抄进文件"。引擎在 serve 时是怎么把 safetensors 变成可用权重的?分配 → 逐层读 F32 → 量化成 Q8_0/Q4_0 → 8x8/4x4 重排(Day 5–7 讲的全过程)。VQF 转换走的是完全相同的函数序列,只是最后多一步 vqf_write 把内存布局按目录固化落盘。

于是"位级一致性"是构造出来的,不是碰巧的:转换与推理共用 f32_to_q8_0 / f32_to_q4_0 / repack_* 同一套代码(vqf.c 头注释第 9–11 行的红线声明、技术文档 §4.7"与 serve 完全相同的加载/量化序列")。推理时 mmap 直挂的字节,就是转换时写盘的那些字节。

转换的耗时大头因此不在"写",而在"前面那遍加载 + 量化"。板端数据:全量 dual(Q8+Q4+视觉)一次 1 分 42 秒(含从 eMMC 读 4.25 GB safetensors、28 层逐层量化重排、写出 4.16 GB)。

2. 对应代码:main 的转换主干与 vqf_write 的职责

离线转换入口在 main.c(第 5024–5031 行分发到 run_convert_vqf)。主干(第 4041–4102 行)分四段:

static int run_convert_vqf(const char *model_dir, const char *out_path) {
   
    STModelConfig cfg;
    st_parse_config(model_dir, &cfg);               /* ① 读 config.json */
    if (cfg.max_seq_len > 8192) cfg.max_seq_len = 8192;  /* ② 引擎上限(Day16 的 8192 之谜) */
    ...
    load_quant_weights(&cfg, &w);                   /* ③ 与 serve 同路径的 加载+量化+重排 */
    ... vision 权重加载(st_vision_load_weights,失败降级纯文本 VQF)...
    flags = ... 布局位(Q8_8X8/Q4_4X4/X8/Q8BUF_Q4/G256)...
    int rc = vqf_write(out_path, &w, &cfg, flags);  /* ④ 固化落盘 */
    if (rc == 0) {
    vqf_load(&w2, out_path); ... }   /* ⑤ 转换后立即 reload 自检 */
}

第 ③ 步是"共用量化路径"的心脏,main.c 第 4104–4107 行 的注释明说:

加载全部量化权重:serve 与 --convert-vqf 共用同一代码路径,保证 VQF 转换与内存加载位级一致(fixedpoint_quantize_saturate red line)。

vqf_write(vqf.c 第 248–286 行)则只做固化本身:调 vqf_collect 把"当前内存里所有非空张量"按固定顺序收进 dump 表(第 139–234 行:F16/F32 区 → Q8 区 → Q4 区 → X8 区 → Vision 区,谁非空收谁),随后写头、写目录、按目录链式偏移逐张量写数据(第 316–336 行),边写边累计 FNV(第 338–344 行),写完回填 file_len 再从文件重算一次校验和(第 485–511 行),最后打印对比:

[VQF] wrote %s: %d tensors, %.1f MB, flags=0x%x (seq=%d) memsum=%016llx file_sum=%016llx

memsum == file_sum 意味着"内存权重算出的校验和"与"落盘后从文件重算的校验和"一致——写盘没有丢字节、没有错位。

flags 的两层组装值得注意(Day 16 的 0x63 从哪来):调用方(main.c 第 4074–4083 行)只给布局位(Q8_8X8|Q4_4X4|X8|Q8BUF_Q4|G256);vqf_write 内部再补存储位(vqf.c 第 257–261 行:embed 是 F16 则置 EMB_F16、含 vision 则置 VISION)。所以自检日志打 reload OK (flags=0x3)、而文件里真正写的是 flags=0x63——前者是调用方视角,后者才是文件里查得到的(Day 16 解析到的 0x63 就是这么来的)。

3. 改动后果:板端同源重转三次,逐字节看"一次成型"

实测口径:RK3588(Orange Pi 5 Plus)/ aarch64 / Release build-rk3588(gcc 11.4,-O2 -march=armv8.2-a+dotprod)/ 2026-09-07。源:/mnt/emmc/Modl/Qwen3-VL-2B-Instruct(Qwen3-VL-2B safetensors 4,255,140,312 B)。

Run 1:默认 wmode(dual)→ A1.vqf(--convert-vqf A1.vqf --model <dir>)

[SERVE] loading & quantizing lm_head (Q8_0 + Q4_0)...
[SERVE] lm_head quantized OK (peak staging 128 MB)     ← lm_head 分块量化,峰值暂存 128MB
[VQF] vision weights loaded (ViT 24 layers, hidden=1024)
[VQF] collect 49 tensors, writing...
[VQF] wrote A1.vqf: 49 tensors, 3966.6 MB, flags=0x63 (seq=28) memsum=760ead0fd28edceb file_sum=760ead0fd28edceb
[VQF] self-check PASS: A1.vqf reload OK (flags=0x3)    ← 自检重载通过(打印的是调用方局部 flags)
A1 耗时:1 分 42 秒(01:56:19 → 01:58:01)

49 张量 = 文本 22(embed F16 + 5 个 norm + Q8_0×8 + Q4_0×8)+ 视觉 27。default 是 dual:prefill 用 Q8、decode 用 Q4,两套文本权重都在文件里(技术文档 §5.1)。memsum == file_sum:写盘校验通过。

Run 2:同样的命令再来一次 → A2.vqf

sha256sum A1.vqf A2.vqf
bc18d4d8df85c0d8164a3750ada165bd2494a02dcb67a51605dc4820de13e47f  A1.vqf
bc18d4d8df85c0d8164a3750ada165bd2494a02dcb67a51605dc4820de13e47f  A2.vqf

两次独立转换 sha256 完全一致——量化与重排是确定性的(fixedpoint 定点取整、无浮点求和序依赖),"一次成型"里的"一次"可以随时重来而不变。

Run 3:换 wmode → A3q4.vqf(VLLM_WMODE=q4)

[VQF] collect 41 tensors, writing...
[VQF] wrote A3q4.vqf: 41 tensors, 2223.3 MB, flags=0x63 (seq=28) memsum=defe73f3f5d84337 file_sum=defe73f3f5d84337
耗时:53 秒(02:11:42 → 02:12:35)
sha256sum A3q4.vqf .../model.vqf
f2c168a59c8454b86ac1b9660067704b30b22ab6d910a32089607eb5f91636e6  A3q4.vqf
f2c168a59c8454b86ac1b9660067704b30b22ab6d910a32089607eb5f91636e6  .../Modl/Qwen3-VL-2B-Instruct/model.vqf

两个亮点:

  1. 41 张量 = Day 16 解剖的那份生产 model.vqf 同款(2,331,316,232 B,flags 0x63)。也就是说:Day 16 讲的文件是 wmode=q4 产物(文本只存 Q4,无 Q8 文本、无 x8);q4 转换更省(53 s,少算一遍 Q8 文本量化重排,体积 2.22 GB vs dual 的 3.97 GB)。
  2. sha256 完全等于线上 model.vqf——那份文件是 9 月 5 日另一个构建转的;9 月 7 日当前构建用 q4 模式重转,产物逐字节一致。位级可复现跨过了构建与日期。这正是 vqf.c 头注释那条红线的兑现:转换与推理共用同一套定点量化代码,量化是纯函数,输入一致输出必一致。

诚实标注:① "跨构建一致"只在本仓库这些天里实测成立——两个构建的量化路径没改过;若有人改动 f32_to_q4_0 的舍入或 repack 的排列,旧文件依旧能加载(格式不变),但重转不会逐字节相同——这正是需要"转换后 self-check + sha256 记录"的原因。② dual 的 A1(49 张量)与 q4 的 A3q4(41 张量)目录项不同,属预期(不同 wmode 固化不同内容),不是 bug。

4. 学员调试任务

  • A 档(板端动手):对 --model 目录分别跑默认与 VLLM_WMODE=q4 两次 --convert-vqf,抄下 [VQF] wrote 行的 tensors/MB/flags/memsum/file_sum 与耗时;用 sha256sum 验证"同命令两次转换一致";用 parse_vqf.py(Day 16)看 q4 与 dual 产物的目录项差异。
  • B 档(纯读源码):读 main.c run_convert_vqf(4041–4102)与 vqf_collect(vqf.c 139–234),回答:① "转换与 serve 共用加载路径"具体指哪些函数?② 为什么 vqf_collect 的 ADD 要判空(指针为 NULL 就跳过)?③ 为什么 self-check 打印 flags=0x3 而文件里是 0x63(提示:看 vqf_write 内部分别补了哪两个位)?

预期输出:你能画出"config → 加载量化 → collect → 头部组装 → 目录链 → 数据落盘 → 回填 file_len → 双校验 → reload 自检"的完整流程图,并解释 wmode 如何改变固化内容(张量集合与体积)。

收尾

  • 本篇源码点名:main.c(分发 5024–5031、run_convert_vqf 4041–4102、load_quant_weights 4104+)、vqf.c(头注释红线 1–11、vqf_collect 139–234、vqf_write 248–286、flags 补位 257–261、写盘 316–344、双校验 485–518)、vllm_safetensors.c(f32_to_q8_0 2916、f32_to_q4_0 2957、4x4 repack 3368)、技术文档 §4.7 / §9
  • 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
  • 下篇预告:safetensors 是自己家的源。要是手里只有 GGUF(llama.cpp 生态的模型)呢?17-2 用板端现成的两份 GGUF 走 --convert-gguf,看"反量化再量化"怎么绕路,以及为什么这条路产出的 VQF 没有视觉塔。
相关文章
|
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天前
|
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天前
|
调度
0.8MB 跑通 Qwen|第 13-3 篇:推理引擎的回退与收益边界——哪些场景 spec 才划算
本文精析Speculative Decoding在ARM端的收益边界:基于RK3588实测,揭示“62%命中率仍变慢”的根源——单步Decode仅44ms时,验证开销与KV回滚反致负增益;明确开启条件:长上下文、高重复性、大模型、贪婪采样。零依赖纯C,适配Qwen3-VL系列。
|
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语言
0.8MB 跑通 Qwen|第 5-2 篇:推理引擎的 Q8 对称量化——scale 从哪来
本系列聚焦ARM端零依赖纯C推理引擎,实测RK3588跑通Qwen3-VL多模态模型。本文详解Q8对称量化核心——scale标定:为何用f16存、为何除127、误差如何从0.033暴增至0.44,揭示量化精度的生死线。(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天前
|
JSON 测试技术 API
0.8MB 跑通 Qwen|第 20-2 篇:推理引擎的路由与自研最小 JSON
本系列《0.8MB跑通Qwen》用纯C手搓零依赖推理引擎,适配Qwen3-VL多模型,在RK3588(aarch64)实测通过。核心含strcmp路由表与自研轻量JSON解析器,精准拦截非法请求(如`msgs`→400),严守兼容边界,代码精简可控,专注教学与边缘部署。(239字)
|
1天前
|
缓存 安全 数据安全/隐私保护
0.8MB 跑通 Qwen|第 16-2 篇:逐字段解剖——推理引擎权重格式的 VQFHeader / VQFSig / VQFTensor
本篇深度解析Qwen推理引擎VQF v2文件头:432字节固定布局、小端序、64B对齐目录起始(448偏移),通过`_Static_assert`严控结构体尺寸,以`magic/version/size`三重门禁保障零依赖纯C加载安全。真机RK3588实测,揭穿注释与字节不符彩蛋,夯实字节序纪律。(239字)

热门文章

最新文章