系列:《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 篇 · 总纲(阿里云社区)
上一篇:17-1《safetensors → VQF:vqf_write 一次成型》| 下一篇:17-3《位级一致性实验:两次转换产物逐字节比对》
真机实测通过:本文实验已在 RK3588 板端实测完成(2026-09;方法学与原始记录见仓库 docs 与《实验脚本》目录)
一句话导读:GGUF 转 VQF 的反量化再量化:llama.cpp 生态里已量好的 Q4_0/Q8_0 与 k-quants,先反量化回 F32,再走推理引擎同一套量化与重排函数,板端用 f16 与 Q4_0 两份 GGUF 各转一次看固有边界。
关键词:手搓 Qwen 推理引擎、千问大模型推理、GGUF、反量化再量化、Q4_0、VQF、量化、Qwen3-VL、零依赖纯 C
导语:llama.cpp 生态里已经量化好的 Q4_0、Q8_0 甚至 k-quants,怎么能喂给另一套引擎?这篇讲 VQF 的做法:先把每种 GGUF 量化类型反量化回 F32,再走自家那套量化与重排函数重新固化。板端用同一模型的 f16 版与 Q4_0 版各转一次,看清这条"反量化再量化"绕路的固有边界。
GGUF 是 llama.cpp 生态的容器:模型可能在别人那儿已经被量化成了 Q4_0、Q8_0 甚至 k-quants。VQF 引擎怎么吃下它?答案很直白:先把 GGUF 的每种量化反量化回 F32,再走自己那套 f32_to_q8_0/q4_0 → repack 重新量化("反量化再量化")。今天板端用同一模型的 f16 版与 Q4_0 版 GGUF 各转一次,看这条绕路路径的输入输出,以及它固有的边界。
1. 知识点:别人家的格式,先还原成"通用中间态"再固化
两种格式的哲学不同:
- VQF:只认自己的 qtype + 布局(Q8_0 34B/块、Q4_0 18B/块、4x4/8x8 重排),因为内核是按这些字节布局手写的(Day 6/7);
- GGUF:qtype 是 llama.cpp 的(Q4_0/Q4_1/Q5_0/Q5_1/Q8_0 与 Q2_K…Q8_K 等 k-quants),布局是 llama 的块格式。
要让 GGUF 权重能被 VQF 内核消费,唯一稳妥的路是解铃还须系铃人式的反着走:
GGUF (Q4_0/Q8_0/k-quants/F16/BF16)
→ 反量化器逐块还原成 F32 (vllm_gguf.c 覆盖 Q4_0…Q8_K)
→ f32_to_q8_0 / f32_to_q4_0 (与 safetensors 路径同一个函数)
→ repack(8x8 tiled / 4x4) (同一个 repack)
→ vqf_write 固化
引擎在这条路上没有发明第三种量化格式,而是把"别人已经量过一遍"的权重还原成 F32 后,再用自己的量化器"重算一遍"。这正是 17-1 里"与 serve 共用同一套量化代码"红线的延续:无论权重来自 safetensors 还是 GGUF,进入 VQF 之前都必须变成同一套引擎量化函数的输出,否则内核不认识。
两种源的差别在入口:
| 环节 | safetensors 路径(17-1) | GGUF 路径(本篇) |
|---|---|---|
| 权重源精度 | F32(原生存储) | F16/BF16 直接转;已量化张量先反量化 |
| 张量名 | HF 风格(model.language_model.*) |
llama 风格(blk.N.attn_q / token_embd) |
| 处理单元 | 按名字找张量 | 同样按名字映射槽位(q/k/v/o/gate/up/down) |
| 最终量化 | 同一套 f32_to_* + repack_* |
完全同一个函数 |
2. 对应代码:反量化器 + 逐张量"解→量→重排"循环
vllm_gguf.c 头注释(第 11–22 行)直接交代了这条路的全部要素:
反量化覆盖 Q4_0/Q4_1/Q5_0/Q5_1/Q8_0 + Q2_K/Q3_K/Q4_K/Q5_K/Q6_K/Q8_K(block 布局与 llama.cpp ggml-common.h 对齐);F16/F32/BF16 直接转。IQ 系列 / TQ / MXFP4 极稀有,不支持(明确报错)。
加载布局与 safetensors 路径完全一致:st_weights_alloc_layers_q8ffn → 逐张量 dequant(F32) → f32_to_q8_0/q4_0 → repack(8x8 tiled / 4x4)。
两个关键函数:
static void gguf_dequant_row(int qtype, const uint8_t *src, float *dst, int64_t n); /* 第 319 行 */
/* 反量化张量区间到 dst(dst 容纳 nelem*4 字节) */
static void gguf_dequant_range(int qtype, const uint8_t *data, ...); /* 第 587 行 */
逐层处理循环(第 822–847 行)展示了"解→量→重排"三步:
if (f32dst) {
gguf_dequant_range(qt, td, 0, elems, f32dst); /* ① 整块反量化到 F32 */
} else {
... chunked:每 2048 行一块 ...
gguf_dequant_range(qt, td, r0*cols, ne, tmp); /* ① 分块反量化 */
if (q8dst) gguf_quant_q8(q8dst + ..., tmp, ne, q8b4); /* ②a 引擎 Q8 量化 */
if (q4dst) f32_to_q4_0(q4dst + ..., tmp, ne); /* ②b 引擎 Q4 量化 */
...
}
if (rep8 && q8dst && g_st_q8_repack)
repack_q8_0_tiled_inplace(q8dst, rows, cols); /* ③ 8x8 重排 */
if (rep4 && q4dst && g_st_q4_repack)
repack_q4_0_4x4_inplace(q4dst, rows, cols); /* ③ 4x4 重排 */
入口在 main.c 第 5038–5086 行:--convert-gguf <out.vqf> --model <xxx.gguf> → gguf_load_model(内部就干上面这些)→ 组装 flags → vqf_write → reload 自检。注意注释(第 5039–5040 行):
GGUF 已量化(Q4_0/Q8_0/k-quants),加载后按当前 wmode 生成引擎布局副本。
"已量化"三个字是这整篇的题眼:这条路的输入可能已经损失过一轮精度,输出是"从有损源再量化",不是"从原始权重量化"。
3. 改动后果:板端用同一模型的两份 GGUF 各转一次
实测口径:RK3588(Orange Pi 5 Plus)/ aarch64 / Release 构建 / 2026-09-07。GGUF 源在
/mnt/emmc/llama.cpp-master/models/:Qwen3-VL-2B-f16.gguf(3,447,350,464 B)与Qwen3-VL-2B-Q4_0.gguf(1,054,424,256 B)——同一模型的两种精度源。
Run G16:f16.gguf → G16.vqf(--convert-gguf G16.vqf --model .../Qwen3-VL-2B-f16.gguf)
[GGUF] arch=qwen3vl dim=2048 layers=28 heads=16 kv=8 hd=128 ff=6144
vocab=151936 rope_theta=5000000 eps=1e-06 q_norm=1 mrope=1
[ST-Q8] Estimated memory: 4.5 GB (F32 token + Q8_0 + Q4_0 nibble weights)
[GGUF] loaded 308 weight tensors ← llama 风格目录里找到 308 个文本张量
[GGUF] output.weight absent -> tied embeddings ← lm_head 与 token_embd 绑定(与 safetensors 一致)
[VQF] collect 22 tensors, writing...
[VQF] wrote G16.vqf: 22 tensors, 3260.2 MB, flags=0x23 (seq=28) memsum=f9fb70fc9e16783d file_sum=f9fb70fc9e16783d
耗时:1 分 28 秒(02:02:38 → 02:04:06)
GGUF 的头部自报 arch=qwen3vl——这份 GGUF 就是从 Qwen3-VL 转的,模型几何(dim/layers/heads/vocab…)与 17-1 的 safetensors 完全一致。output.weight absent → tied 也和 safetensors 路径的处理一致(lm_head == embed_tokens,main.c 第 4153–4162 行的 tied 回退)。
边界一:GGUF 路径产出的是纯文本 VQF(flags=0x23,无 VISION)。llama.cpp 的 GGUF 不含视觉塔(视觉在 mmproj 侧文件里),所以 collect 只有 22 个文本张量(embed F16 + 5 norm + Q8×8 + Q4×8),没有 17-1 里 A1 的 27 个 v_* 张量。要带视觉的多模态 VQF,只能走 safetensors 源——这是"GGUF → VQF"的适配边界,不是 bug(README 与文档同口径)。
Run G40:Q4_0.gguf → G40.vqf
[VQF] wrote G40.vqf: 22 tensors, 3260.2 MB, flags=0x23 (seq=28) memsum=af31c8dec207b440 file_sum=af31c8dec207b440
耗时:1 分 24 秒(02:04:14 → 02:05:38)
产物尺寸与 G16 完全相同(3,418,562,568 B,22 张量)——因为无论源是 F16 还是 Q4_0,进入引擎量化器后输出布局与体积一样。"体积一样"不等于"字节一样":源 Q4_0 已丢过一轮精度,反量化回来的 F32 与原始 F32 有误差,再量化自然不同。这个差异到底多大、落在哪,正是 17-3 的对拍实验。
两个产物的自检(G40 日志为例):
[VQF] self-check PASS: G40.vqf reload OK (flags=0x3)
转换完立刻 mmap 重载一次验证(checksum/布局/指针挂载),与 17-1 的 safetensors 路径同一套 self-check。
诚实标注:GGUF 反量化器只支持注释里列的 qtype;若手头的 GGUF 用了 IQ/TQ/MXFP4 这类稀有格式,引擎会明确报错而不是静默给错数——宁缺毋错,这是"可验证性高于便利性"的又一例。另外,这份 f16.gguf 是文本部分的 GGUF(3.44 GB ≈ f32 4.25 GB 的一半),与 HF safetensors 同源同精度(f16→f32 无损),这让 17-3 里"两条无损路径殊途同归"的对比成为可能。
4. 学员调试任务
- A 档(板端动手):用
--gguf-info看 GGUF 目录(308 个张量的名字/类型分布),再分别对 f16 与 Q4_0 两份 GGUF 跑--convert-gguf,对比两次 [VQF] wrote 的 tensors/MB/flags 与耗时;用parse_vqf.py确认两个产物都没有v_*张量、flags 无 VISION 位。 - B 档(纯读源码):读 vllm_gguf.c 第 11–22 行注释与
gguf_load_model(658 行起)的层处理循环(795–847),回答:① 为什么 GGUF 已量化(如 Q4_0)仍要"反量化再量化"而不是直接改目录 qtype 复用?② k-quants(Q4_K 等)的块布局与 VQF 的 Q4_0 有何不同,直接复用会怎样?③output.weight absent -> tied embeddings这条日志在 safetensors 路径的对应物是什么(提示:main.c 4153–4162)?
预期输出:你能画出"GGUF 各 qtype → 反量化 F32 → 引擎量化 → 重排 → 固化"的管道图,并口头说清:为什么这条路对已量化源会有二次精度损失,而 VQF 里没有"第三种格式"。
收尾
- 本篇源码点名:vllm_gguf.c(头注释 11–22、
gguf_dequant_row319、gguf_dequant_range587、层循环 795–847、gguf_load_model658)、main.c(--convert-gguf分发与主干 5038–5086)、vllm_safetensors.c(量化器 2916/2957/3368)、技术文档 §4.7 - 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
- 下篇预告:两条转换路都跑通了(safetensors → A1/A3q4,GGUF → G16/G40),而且 G16 的 22 个文本张量与 A1 逐字节相同。17-3 把所有产物的 sha256 与逐张量哈希摆上桌,做一次严格的位级一致性对拍,并诚实标注"什么情况下一致、什么情况下必然不一致"。