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 与逐张量哈希摆上桌,做一次严格的位级一致性对拍,并诚实标注"什么情况下一致、什么情况下必然不一致"。
相关文章
|
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

热门文章

最新文章