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 没有视觉塔。
相关文章
|
13天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
8001 15
|
11天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
1772 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
12天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
1923 12
|
10天前
|
人工智能 编解码 并行计算
MiniMax-H3 一键整合包技术文档:8G 显存运行 AI 漫剧制作 —— 角色替换 / 动作迁移 / 文图生视频部署与调参指南
MiniMax H3 是 MiniMax 开源的全模态视频生成模型,支持文/图/音/视多条件输入,输出最高2K、15秒带双声道音频视频。本文档详述其Int8量化版在8GB显存下的本地一键部署、三段式工作流(EDIT/REPLACE/CONTINUE)、参数调优及常见问题排查。(239字)
|
6天前
|
人工智能 Linux 开发者
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
Codex是OpenAI推出的AI编程智能体,可读取本地项目、理解需求并自动修改代码。支持桌面GUI、命令行(CLI)及VS Code/Cursor插件三种形态,覆盖可视化操作、终端高效开发与编辑器无缝集成场景,助开发者用自然语言驱动编码全流程。(239字)
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
|
25天前
|
人工智能 自然语言处理 安全
阿里云千问办公 QwenWork详细介绍:产品核心能力、典型场景、价格及常见问题解答
千问办公是阿里云推出的一站式AI办公平台,主打"不止于对话,更注重交付",依托通义千问旗舰大模型,用户一句话即可完成数据分析、PPT生成、视频剪辑等复杂任务,直接输出可用成果。产品深度打通钉钉生态与企业OA,覆盖桌面端、网页端,提供企业标准版198元/人/月等多档订阅方案,新用户注册即赠2000积分,适配工程师、HR、财务等多职业办公场景,成为能动手干活的"全能AI同事"。
3823 10
|
20天前
|
缓存 IDE Java
【保姆级】Android Studio下载、安装和汉化教程(2026最新)
Android Studio 是 Google 官方推出的免费 Android 应用开发集成环境,基于 IntelliJ IDEA,内置模拟器、调试器、性能分析及 Compose 界面工具,功能全面,文档丰富,是安卓开发首选工具。(239字)
2108 1

热门文章

最新文章