系列:《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 篇 · 总纲(阿里云社区)
上一篇:7-1《Q4_0 格式:与 GGUF 对齐的 4bit 布局》 | 下一篇:7-3《预热与测量抖动》
真机实测通过:本文实验已在 RK3588 板端实测完成(2026-09;方法学与原始记录见仓库 docs 与《实验脚本》目录)
一句话导读:推理引擎的混合精度路由:flags 用比特位声明 Q8/Q4 双份副本,读 use_q4 判定与 prefill/decode 门控,讲清 prefill 走 Q8 保精度、decode 走 Q4 省带宽的取舍。
关键词:手搓 Qwen 推理引擎、千问大模型推理、Qwen3-VL、零依赖纯 C、混合精度、flags、Q8 量化、Q4_0、RK3588
导语:既然 Q4 的误差远大于 Q8,引擎为何不全用 Q8,也不全用 Q4?手搓 Qwen 推理引擎给出混合精度路由:文件头用 flags 的比特位声明 Q8/Q4 双份副本,运行期按 prefill 走 Q8 保精度、decode 走 Q4 省带宽来分流。这篇讲清这套路由的判断与落地。
7-1 说 Q4 的误差是 Q8 的 11 倍——那引擎为什么不全上 Q8?反过来,为什么又不全上 Q4?答案:不同的矩阵对精度的敏感度不同,对带宽的压力也不同。引擎用"双份权重 + 运行时路由"来同时吃到两种量化的好处。
1. 知识点:混合精度的两问
一个推理 step 里要跑几类矩阵:attention 的 Q/K/V/O、FFN 的 gate/up/down。它们问同一个问题,答案不同:
- 这层数值敏感吗? attention 的输出直接进 softmax,误差会被放大(softmax 是"赢家通吃"),所以 QKV 更想要 Q8;FFN 的 gate/up 是逐点激活(SiLU 之类),对量化误差宽容得多,可以 Q4。
- 这层带宽压力大吗? decode 每词重读权重(5-1),矩阵越大越希望字节少——FFN 的 gate+up 是模型里最大的矩阵,Q4 在这里省得最多。
所以理想形态是:每个矩阵按"敏感度"选精度。工程上落地成两件事:
- 转换期:同一份权重存两份——Q8 副本 + Q4 副本(加个 flags 记着"这文件是混合的");
- 运行期:按阶段路由——prefill 走 Q8(要把上下文算准),decode 走 Q4(省带宽)。
2. 对应代码:flags 与运行期路由
转换期:文件头的 flags 用比特位记录"有哪些副本"。看板端真实 model.vqf 的头部(Day 3 的 xxd 已经见过):
flags = 0x63
bit0 VQF_FLAG_Q8_8X8 (1) : 有 Q8 副本(8x8 布局)
bit1 VQF_FLAG_Q4_4X4 (2) : 有 Q4 副本(4x4 布局)
bit5 VQF_FLAG_EMB_F16 (32) : embedding 存 F16
bit6 VQF_FLAG_VISION (64) : 含视觉张量
0x63 = 64+32+2+1 -> 同时声明了 Q8 与 Q4 副本 = 混合精度文件
引擎读头时若两个 bit 都置位,就知道这份权重是"双份"的,运行期才有得选。
运行期:真正的路由策略在 vllm_safetensors.c 第 10493–10495 行(及 11311–11312 行的同构代码):
/* Mixed precision: --prefill-q8 forces this prefill GEMM onto Q8_0 while
* decode keeps Q4_0 (see g_st_prefill_q8). */
const int use_q4 = w->has_q4 && (!g_st_prefill_q8 || !w->q8_q_weight);
读法:在 prefill 的 GEMM 里,"用不用 Q4"取决于 --prefill-q8(默认开,g_st_prefill_q8=1,第 213 行):开了且 Q8 副本在 → 用 Q8;否则用 Q4。而 decode 侧的单 token GEMV 走 Q4 路径(第 3849 行起的 M4f)。一句话策略:prefill 用 Q8 保精度,decode 用 Q4 省带宽。
3. 改动后果:双份权重同台实测,读混合精度分档
引擎的 --bench-mixed 会拿同一份合成权重(dual Q8_0+Q4_0 copies)把 Q4/Q8 两套内核在 decode(M=1)与 prefill(M=32)都跑一遍。板端实测(RK3588 / 2026-09,当日一轮):
Weights: dual Q8_0+Q4_0 copies (273 MB)
--- Decode (M=1) ---
Q4 fused QKV 1.581 ms 31.8 GFLOPS
Q8 fused QKV (dec) 1.358 ms 37.1 GFLOPS <- QKV:Q8 更快
Q4 fused gate+up 2.728 ms 73.8 GFLOPS
Q8 fused gate+up (dec) 4.765 ms 42.2 GFLOPS <- gate+up:Q4 快 1.7x
Q4 fused down+res 1.285 ms 78.4 GFLOPS
Q8 fused down+res (dec) 1.985 ms 50.7 GFLOPS <- down+res:Q4 快 1.5x
--- Prefill (M=32, batched) ---
Q4 batched QKV 5.370 ms 299.9 GFLOPS
Q8 batched QKV 5.782 ms 278.6 GFLOPS
Q4 batched gate+up 20.143 ms 319.8 GFLOPS
Q8 batched gate+up 22.425 ms 287.3 GFLOPS
Q4 batched down+res 10.884 ms 296.0 GFLOPS
Q8 batched down+res 11.592 ms 277.9 GFLOPS
这张表本身就是"混合精度为什么存在"的答案:
- 形状差异:decode 里 QKV 窄、Q8 更快(37.1 vs 31.8 GFLOPS);gate+up/down+res 宽、Q4 快得多(73.8 vs 42.2、78.4 vs 50.7)——同一种精度在不同形状上输赢相反;
- 相位差异:prefill(M=32)里 Q4 全面领先(300+ GFLOPS vs ~280),但引擎仍让 prefill 走 Q8——因为选 Q8 的理由是精度不是速度(bench 的合成权重没有"输出质量"可言,真实模型里 prefill 结果要喂后续 decode,误差要压住);
- 注意数值会抖:同一轮 bench 内部是 best-of 的稳定读数,但跨轮/跨天会有 ±20% 级别波动(7-3 专门讲怎么读才可信)。
想亲手改路由:
--prefill-q8关掉(或让模型只有 Q4 副本)时,use_q4恒真,prefill 也走 Q4——精度让位于带宽。真实模型上对比同 prompt 的输出即可观察差异(任务 4 给出姿势)。
4. 学员调试任务
- A 档(板端动手):
- 解析你的
.vqf头(Day 3 脚本),读出flags并逐位解释"有哪些副本"; - 跑一次
--bench-mixed,抄下 decode 与 prefill 的 Q4/Q8 分档表,标出"哪些格子 Q4 赢、哪些 Q8 赢"; - 若用
--prefill-q8关掉再跑同一模型的一次短生成,与默认对比输出的 rel err(若有对拍基准)。
- 解析你的
- B 档(纯读源码):读第 10493–10495 行的
use_q4判定与第 213 行g_st_prefill_q8默认值,画出"prefill / decode × Q8 / Q4"的四格路由表。
预期输出:你能解释 flags 两个 bit 怎么声明"混合精度文件"、运行期 prefill/decode 为何不同精度,以及"快 vs 准"在路由里的取舍。
收尾
- 本篇源码点名:vllm_safetensors.c(
g_st_prefill_q8第 213 行、混合精度路由第 10493–10495 行、M4f decode GEMV 第 3849 行起)。 - 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
- 下篇预告:看表别急着下结论——同一个 bench 连跑五次,gate+up 能从 3.9ms 抖到 6.7ms。下一篇 7-3 讲预热与测量抖动:为什么基准报告"重复 3 次取中位"。