0.8MB 跑通 Qwen|第 17-2 篇:GGUF → VQF——推理引擎的 Q4_0…Q8_K 反量化再量化

简介: 本系列《0.8MB跑通Qwen》聚焦ARM零依赖纯C推理引擎,适配Qwen3-VL多版本模型,在RK3588平台实测。本文详解GGUF→VQF的“反量化再量化”路径:将llama.cpp已量化权重(Q4_0/Q8_0等)先还原为F32,再经统一量化与重排固化为VQF格式,确保内核兼容性与可验证性。(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 篇 · 总纲(阿里云社区)

上一篇: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_row 319、gguf_dequant_range 587、层循环 795–847、gguf_load_model 658)、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 与逐张量哈希摆上桌,做一次严格的位级一致性对拍,并诚实标注"什么情况下一致、什么情况下必然不一致"。
相关文章
|
1天前
|
JSON 网络协议 前端开发
0.8MB 跑通 Qwen|第 20-3 篇:SSE 流式——推理引擎响应怎么写一半就发给客户端
本系列《0.8MB跑通Qwen》用纯C手搓零依赖推理引擎,适配Qwen3-VL多尺寸模型,在RK3588(aarch64)实测SSE流式响应:首token延迟130ms,后续约50ms/token,支持标准+自定义事件,真正实现端侧“打字机”体验。(239字)
|
1天前
|
算法 定位技术 C语言
0.8MB 跑通 Qwen|第 16-3 篇:推理引擎的 tensor 目录与布局重排的落盘顺序
《0.8MB跑通Qwen》系列实录:30天手搓零依赖纯C推理引擎,适配Qwen3-VL多模态模型(2B/8B/30B-A3B),真机跑通RK3588(aarch64)。详解VQFTensor目录结构、64字节对齐落盘、FNV校验及mmap加载机制,含零依赖Python解析器。
|
1天前
|
C++
0.8MB 跑通 Qwen|第 10-3 篇:推理引擎里诚实的坑——哪些场景稀疏无收益,甚至负优化
本篇实测揭示稀疏注意力的真相:短上下文(&lt;1K)开`--sparse-attn`反成负优化!RK3588板端验证,详列8条“禁用清单”,每条附源码行号与实测数据。明确边界:q4 KV、推测解码、L3驱逐等均与稀疏互斥,并坦承`test-sparse`自检崩溃缺陷。(239字)
|
1天前
|
存储 缓存 C++
0.8MB 跑通 Qwen|第 9-1 篇:推理引擎的 KV 也量化——q8 KV 把缓存与 decode 带宽压到一半
本系列手搓零依赖纯C推理引擎,仅0.8MB即可在RK3588上跑通Qwen3-VL多模型。本文详解q8 KV量化:将f16 KV压至INT8,按token每头独立定标,缓存与decode带宽降低约48%,兼顾精度与效率,f32仅作调试探针。
|
1天前
|
编解码 缓存 计算机视觉
0.8MB 跑通 Qwen|第 19-1 篇:ViT 编码——推理引擎里图片是怎么变成视觉 token 的
本系列《0.8MB跑通Qwen》用纯C手写零依赖推理引擎,实现在RK3588(aarch64)上部署Qwen3-VL多模态模型。真机实测:64×64图经24层ViT编码得4个视觉token,耗时仅295.3ms,支持图文理解与视频分析。(239字)
|
1天前
|
缓存 NoSQL 区块链
0.8MB 跑通 Qwen|第 15-3 篇:推理引擎单步走一次生成——token 循环与 KV 追加
本系列《0.8MB跑通Qwen》用纯C手搓零依赖推理引擎,实测RK3588板端运行Qwen3-VL多模型;本文以gdb单步追踪decode循环,直观展示“采样→前向→KV追加→停判”全过程,将大模型逐token生成钉在胶片上。(239字)
|
1天前
|
调度 C++
0.8MB 跑通 Qwen|第 7-2 篇:推理引擎的混合精度路由——flags 决定谁用 Q8、谁用 Q4
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多模态模型,在RK3588上实现实测。核心创新是混合精度路由:prefill阶段用Q8保精度,decode阶段用Q4省带宽,通过flags比特位动态调度,兼顾速度与质量。(239字)
|
1天前
|
开发工具 C++ git
0.8MB 跑通 Qwen|第 5-3 篇:推理引擎的参考实现对拍——怎么读懂 rel err 的数量级
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎开发,实测适配Qwen3-VL多模型,在RK3588上完成Q8/Q4量化与NEON加速。核心创新在于“两把尺子”误差分析法:内核一致性(1e-7)验证手写代码正确性,量化损失(1e-2)界定精度边界,实现可验证、可调试、可落地的轻量级LLM部署。(239字)
|
1天前
|
API 调度 C++
0.8MB 跑通 Qwen|第 14-1 篇:大模型推理的连续批处理——iteration-level 调度思想
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多模态模型,在RK3588上实测落地。本文详解iteration-level连续批处理:动态聚合多请求的decode步,单次前向服务多人,兼顾并发与确定性。(239字)
|
1天前
|
安全 索引
0.8MB 跑通 Qwen|第 12-2 篇:推理引擎的磁盘 KV 快照格式——0.47 GB 从哪来
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,实测RK3588上部署Qwen3-VL多模态模型。本文详解0.47GB磁盘KV快照格式:64字节头+token IDs+28层F32原样KV,逐字段对账到字节级,诠释“宁大勿错”的可靠性设计。(239字)

热门文章

最新文章