0.8MB 跑通 Qwen|第 7-1 篇:大模型量化的 Q4_0 格式——与 GGUF 对齐的 4bit 布局

简介: 本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多模态模型,在RK3588上实测通过。详解GGUF对齐的Q4_0量化:32个float压缩至18字节(f16 scale + 16 nibble),剖析nibble打包规则与11倍误差代价,为端侧高效部署夯实底层基础。(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 篇 · 总纲(阿里云社区)

上一篇:6-3《4x4 asm 内核:从 llama.cpp 提取的 MIT 代码》 | 下一篇:7-2《混合精度路由:flags 决定谁用 Q8》

真机实测通过:本文实验已在 RK3588 板端实测完成(2026-09;方法学与原始记录见仓库 docs 与《实验脚本》目录)

一句话导读:推理引擎的 Q4_0 格式:拆 32 个浮点如何装进 18 字节(f16 scale 加 16 个 nibble),讲清低半字节归位规则、与 GGUF 对齐的两层含义,并给出往返误差约为 Q8 十一倍的板端实测。

关键词:手搓 Qwen 推理引擎、千问大模型推理、Qwen3-VL、零依赖纯 C、Q4_0、GGUF、nibble 打包、Q8 量化、RK3588

导语:权重是推理引擎里最大的一笔开销:手搓 Qwen 推理引擎选用与 GGUF 对齐的 Q4_0 给它瘦身——32 个浮点如何装进 18 个字节、f16 scale 与 nibble 打包怎样配合、低半字节如何归位。这篇把这套 4bit 布局拆开讲清。

5-1 说引擎文件里 Q4_0 占了近 1GB(大头),可"4bit"到底是怎么把 32 个浮点塞进 18 个字节的?今天拆 Q4_0 的字节布局——并且强调一件事:这个布局和 GGUF 的 Q4_0 是同一个,因为引擎要能直接读 GGUF 权重。

1. 知识点:一个字节装两个数(nibble 打包)

Q4_0 每个元素只有 4 bit(半字节,nibble),表示范围 [-8, +7](借用第 8 个值做"干净"的符号位,同 Q8 用 127 的动机)。32 个元素 × 4bit = 128 bit = 16 字节,加上 2 字节的 f16 scale,整块 32 元素只占 18 字节:

Q8_0:  32 f32 (128B) ->  34 B   = { f16 d; int8 qs[32] }     (每元素 1 B)
Q4_0:  32 f32 (128B) ->  18 B   = { f16 d; uint8 qs[16] }    (每元素 0.5 B)

关键在 qs[16]:每个字节的低 4 bit 存一个元素、高 4 bit 存另一个。两个 4bit 数挤在一个字节里,就是"nibble 打包"。ggml/GGUF 的 Q4_0 反量化里,这个排布是标准化的(vllm_gguf.c 第 302 行与第 334–344 行):

typedef struct {
    uint16_t d; uint8_t qs[16]; }  gf_block_q4_0;   /* 18 B/32 元素 */

case GGUF_T_Q4_0: {
   
    const gf_block_q4_0 *x = (const gf_block_q4_0 *)src;
    int64_t nb = n / 32;
    for (int64_t i = 0; i < nb; i++) {
   
        const float d = gf_f16_to_f32(x[i].d);
        for (int j = 0; j < 16; ++j) {
   
            const int x0 = (x[i].qs[j] & 0x0F) - 8;    /* 低半字节 -> elem j      */
            const int x1 = (x[i].qs[j] >> 4) - 8;      /* 高半字节 -> elem j+16   */
            dst[i*32 + j]      = x0 * d;
            dst[i*32 + j + 16] = x1 * d;
        }
    }
    break;
}

注意元素的归位规则:byte j 的低半字节给元素 j,高半字节给元素 j+16(前 16 个元素在低半字节、后 16 个在高半字节)——不是"偶数在低、奇数在高"。写错这个归位,反量化出来就是"两个错位的半波"。(引擎自己的 f32_to_q4_0,vllm_safetensors.c 第 2957 行起,注释里的排布与这里一致——因为引擎要读 GGUF,两边的 Q4_0 必须同构,第 2951–2956 行的注释写得很清楚。)

2. 对应代码:转换路径里 Q4_0 怎么进来

引擎加载 GGUF 时,是把 GGUF 的 k-quant 块逐张量反量化成 f32,再用自己的 f32_to_q8_0/q4_0 重新量化(vllm_gguf.c 第 21 行注释:逐张量 dequant(F32) → f32_to_q8_0/q4_0 → repack)。所以"与 GGUF 对齐"有两层含义:

  1. 格式层:gf_block_q4_0 与引擎的 Q4_0 块同构(都 18B、都低半字节在前 16 元素);
  2. 转换层:即使 GGUF 里是 Q4_K/Q5_K 等花式 k-quant,引擎也是先解回 f32 再按自己的块布局重量化——引擎只认一种 Q4_0,复杂度被"先解再量"消掉了(代价是转换期算一遍,换来运行期单一格式,这是第 17 天讲转换时会细算的账)。

3. 改动后果:同一段数据,Q4 与 Q8 的代价差多少

用 5-2 同一段测试数据([-3,3] 正弦 32 点),做 Q4_0 往返,板端实测(RK3588 / gcc 11.4 / 2026-09):

Q4_0: d=0.372287  |  32 f32 -> 18 bytes (f16 d + 16 nibbles)
  roundtrip max|err|=0.37229   (same vector, Q8_0 err was 0.03325)
  byte[3]=0xdf -> low nibble 15 (=elem3), high 13 (=elem19)
  • 字节:128B → 18B,只有 Q8_0(34B)的一半——这就是 Q4 在 5-1 里占大头却仍被选中的原因(decode 每词少读一半权重字节);
  • 误差:往返最大误差 0.372 vs Q8 的 0.033——约 11 倍。这是 4bit 的物理代价:量化步长是 Q8 的两倍(d=max/8 vs max/127),误差天然大一档。

Q4 不是"免费的 Q8 减半",它是"用 11 倍误差换一半字节"的交易。所以引擎不会让所有层都上 Q4——哪些层能用 Q4、哪些必须 Q8,是 7-2 混合精度路由要回答的问题。

4. 学员调试任务

  • A 档(板端动手):复刻第 3 节 demo(任意 [-3,3] 的 32 点向量),打印 Q4 与 Q8 各自的字节数与往返误差,确认"18 vs 34 字节、误差约 11 倍";再手写解码 byte[3]=0xdf 验证低半字节=15 对应 elem3。
  • B 档(纯读源码):读 vllm_gguf.c 第 302 行 gf_block_q4_0 与第 334–344 行反量化,画出"一个 18B 块 → 32 个浮点"的字节地图(哪 2 字节是 d、哪 16 字节装谁)。

预期输出:你能不看资料说出 Q4_0 的块结构(18B = f16 d + 16 nibble)、nibble 的归位规则,以及"4bit 换来 11 倍误差"这笔交易。

收尾

  • 本篇源码点名:vllm_gguf.c(gf_block_q4_0 第 302 行、反量化第 334–344 行)、vllm_safetensors.c(f32_to_q4_0 第 2957 行起)。
  • 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
  • 下篇预告:Q4 省字节但误差 11 倍——那为什么模型里还有 Q8?下一篇 7-2 讲混合精度路由:flags 怎么决定"哪层 Q8、哪层 Q4"。
相关文章
|
1天前
|
应用服务中间件
阿里云轻量应用服务器最新费用:2核2G、2核4G、4核8G、4核16G都有活动,秒杀38元1年起
阿里云轻量应用服务器2026年最新报价:新用户专享,2核2G仅38元/年(秒杀)、2核4G 379元、4核8G 1159元、4核16G 1599元;全系标配200M峰值带宽+不限流量,性价比突出。(239字)
30 0
|
1天前
|
调度 C++
0.8MB 跑通 Qwen|第 7-3 篇:推理引擎的预热与测量抖动——为什么报告要"重复 3 次取中位"
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎开发,实测适配Qwen3-VL多模型,在RK3588上验证性能抖动——单内核耗时波动达1.7倍,确立“预热+3次取中位”为可信基准口径。(239字)
|
23小时前
|
缓存 安全 数据安全/隐私保护
0.8MB 跑通 Qwen|第 16-2 篇:逐字段解剖——推理引擎权重格式的 VQFHeader / VQFSig / VQFTensor
本篇深度解析Qwen推理引擎VQF v2文件头:432字节固定布局、小端序、64B对齐目录起始(448偏移),通过`_Static_assert`严控结构体尺寸,以`magic/version/size`三重门禁保障零依赖纯C加载安全。真机RK3588实测,揭穿注释与字节不符彩蛋,夯实字节序纪律。(239字)
|
1天前
|
存储 安全 C语言
0.8MB 跑通 Qwen|第 5-2 篇:推理引擎的 Q8 对称量化——scale 从哪来
本系列聚焦ARM端零依赖纯C推理引擎,实测RK3588跑通Qwen3-VL多模态模型。本文详解Q8对称量化核心——scale标定:为何用f16存、为何除127、误差如何从0.033暴增至0.44,揭示量化精度的生死线。(239字)
|
1天前
|
缓存 NoSQL 区块链
0.8MB 跑通 Qwen|第 15-2 篇:推理引擎的 prefill 与 decode——两条路径为何分开
本篇详解Qwen推理引擎中prefill与decode双路径设计:prefill一次性处理整段prompt(如18 token),批量写入KV缓存;decode逐token循环生成,追加KV。通过RK3588真机gdb断点实证,明确二者独立入口、状态流转与性能动因,手搓零依赖纯C引擎的核心逻辑。(239字)
|
23小时前
|
JSON 测试技术 API
0.8MB 跑通 Qwen|第 20-2 篇:推理引擎的路由与自研最小 JSON
本系列《0.8MB跑通Qwen》用纯C手搓零依赖推理引擎,适配Qwen3-VL多模型,在RK3588(aarch64)实测通过。核心含strcmp路由表与自研轻量JSON解析器,精准拦截非法请求(如`msgs`→400),严守兼容边界,代码精简可控,专注教学与边缘部署。(239字)
|
23小时前
|
编解码 Java 测试技术
0.8MB 跑通 Qwen|第 18-3 篇:MRoPE——推理引擎的 3D 位置编码给视觉留的席位
本篇详解Qwen3-VL多模态推理中MRoPE三维位置编码机制:通过`mrope_section=[24,20,20]`将64维旋转频率分给时间/高/宽三轴,支持视觉token网格定位;并数值推演“强行降维至1D”的相位偏差——7×7图像下最高达4.71弧度(近270°),40/64维严重失准,导致注意力失效。纯文本因t=h=w自动退化,故该bug无法被文本测试捕获。(239字)
|
1天前
|
调度
0.8MB 跑通 Qwen|第 13-3 篇:推理引擎的回退与收益边界——哪些场景 spec 才划算
本文精析Speculative Decoding在ARM端的收益边界:基于RK3588实测,揭示“62%命中率仍变慢”的根源——单步Decode仅44ms时,验证开销与KV回滚反致负增益;明确开启条件:长上下文、高重复性、大模型、贪婪采样。零依赖纯C,适配Qwen3-VL系列。
|
1天前
|
缓存 人工智能 索引
0.8MB 跑通 Qwen|第 12-1 篇:LLM 推理的跨进程恢复——服务重启了,会话不能断
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,实现在RK3588上部署Qwen3-VL等多尺寸模型。本文详解`--disk-kv`机制:将KV Cache落盘持久化,支持进程重启后按前缀精准恢复,实现“会话不断连”,2K上下文prefill加速达23.6×,真正打通边缘AI服务可用性最后一环。(239字)
|
1天前
|
C++
0.8MB 跑通 Qwen|第 10-2 篇:推理引擎的 sparse top-k 块选择——"只看该看的"到底怎么选
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,实测RK3588上高效运行Qwen3-VL多模态模型。本文深度剖析sparse_attn_head的top-k块选择机制——探针抽样、prefill重要性预留、贪心补满与recency保险四段代码,揭示长上下文稀疏注意力如何精准“找针”,并实证k=1时板端翻车根源。(239字)

热门文章

最新文章