欢迎光临
我们一直在努力

Day 17·2 GGUF转VQF:Q4_0到Q8_K的反量化再量化

1. 知识点:别人家的格式,先还原成"通用中间态"再固化

两种格式的哲学不同:

  • VQF:只认自己的 qtype + 布局(Q8_0 34B/块、Q4_0 18B/块、4×4/8×8 重排),因为内核是按这些字节布局手写的(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(8×8 tiled / 4×4) (同一个 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(8×8 tiled / 4×4)。

两个关键函数:

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); /* ③ 8×8 重排 */
if (rep4 && q4dst && g_st_q4_repack)
repack_q4_0_4x4_inplace(q4dst, rows, cols); /* ③ 4×4 重排 */

入口在 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)(源码可得双许可:学习 / 学术研究免费)
  • 下篇预告:两条转换路都跑通了(safetensors → A1/A3q4,GGUF → G16/G40),而且 G16 的 22 个文本张量与 A1 逐字节相同。17-3 把所有产物的 sha256 与逐张量哈希摆上桌,做一次严格的位级一致性对拍,并诚实标注"什么情况下一致、什么情况下必然不一致"。
  • 关键词:GGUF、反量化再量化、Q4_0、VQF、量化tyle="display:none"> 关键词:
赞(0)
未经允许不得转载:171主机测评 » Day 17·2 GGUF转VQF:Q4_0到Q8_K的反量化再量化
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址