系列:《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 篇 · 总纲(阿里云社区)
上一篇:5-2《Q8 对称量化:scale 从哪来》 | 下一篇:第 6 天《NEON 手写 GEMM》
真机实测通过:本文实验已在 RK3588 板端实测完成(2026-09;方法学与原始记录见仓库 docs 与《实验脚本》目录)
一句话导读:推理引擎参考实现对拍的误差量纲:把量化代码算得对不对落到两把尺子上,分清内核一致性期望的 1e-7 与量化损失天然的 1e-2,落点在改坏内核后看 rel err 数量级爆表的回归实验。
关键词:手搓 Qwen 推理引擎、千问大模型推理、参考对拍、rel err、相对误差、内核一致性、量化损失、Qwen3-VL、零依赖纯 C
导语:「误差」两个字不能一概而论。本篇把参考对拍量纲化:引擎里其实有两把尺子——内核一致性看 1e-7,量化损失看 1e-2。先从 --test-l3 的 7.68e-08 读懂「算的是同一件事」,再把内核改坏一次,看 rel err 如何一步爆到 1e-1 量级。
5-2 把量化的"标定"讲完了,可量化后的代码到底算得对不对?Day 1 我们说过引擎靠"参考对拍"自证——今天把这件事量纲化:rel err = 1e-7 意味着什么?1e-2 又意味着什么?先会读误差的数量级,才谈得上写加速内核。
1. 知识点:引擎里其实有"两把尺子"
很多人看到"误差"就紧张,其实引擎里对拍在量两种完全不同的东西:
| 尺子 | 比什么 | 期望数量级 | 回答的问题 |
|---|---|---|---|
| 内核一致性 | 手写内核 vs 朴素 C 参考 | 1e-7(fp32 逐位级) |
"我写的快版和慢版算的是不是同一件事" |
| 量化损失 | 量化后 vs 原始浮点 | 1e-2(Q8 量化粒度) |
"我用 8bit 换的带宽,精度上亏了多少" |
两把尺子不能混:内核一致性要的是"几乎逐位一致"(1e-7),而量化损失天生就是"差几个百分点"(1e-2 上下)。如果你拿 1e-2 的标准去验收内核对拍,一个累计方向上的小 bug 会被放行;拿 1e-7 去要求量化,则永远无法落地。先分清在比哪把尺子,再谈误差大小。
2. 对应代码:两把尺子在引擎里的落点
尺子一(内核对拍):--test-l3 里 Q4 点积的标量参考对拍(Day 1 主角)。板端实测(RK3588 / Release / 2026-09 当日重跑):
$ ./build-rk3588/vllm_kestrel --test-l3 | grep scalar
[PASS] q4 dot vs scalar reference (rel 7.68e-08)
7.68e-08 ≈ 1e-7 量级:NEON 点积路径与朴素 C 参考路径在 fp32 累加里只有最后一位的舍入差——它们算的是同一个东西,只是累加顺序略不同(这就是 Day 1 说的"及格线 1e-5 内 PASS"为何敢用)。
尺子二(混合精度门):--bench-mixed 用同一份合成权重同时准备 Q4_0 与 Q8_0 双份("Weights: dual Q8_0+Q4_0 copies (273 MB)"),把两种量化路径的核都真跑一遍,最后打印 Q4 vs Q8 的数值差距并收尾。板端实测当日运行:
$ ./build-rk3588/vllm_kestrel --bench-mixed
=== Mixed-Precision Kernel Benchmark (Q4_0 vs Q8_0) ===
dim=4096 ... batch=32 | pool threads=4
Weights: dual Q8_0+Q4_0 copies (273 MB)
... (decode / prefill / DRAM 探针,见 5-1) ...
=== Mixed-Precision benchmark done === (exit 0 = 全路径跑通)
这里输出的 Q4 vs Q8 gate+up numerical gap 一行数值巨大(max|Q8| = inf 出现)——别慌,也别忽略:它比的是"两条不同量化路径的输出"(Q4 与 Q8 的量化误差本就不同,量级天然大),是诊断信息不是 PASS/FAIL 线。真正能当"门"的是:exit code = 0、全部内核跑完不崩、以及 5-2 里量化往返误差落在预期量级。这就是"诚实读误差"的现场教学:看到大数字先问'它在比什么'。
量化的"本征误差":5-2 的往返实验给出 Q8 一档的往返误差 0.033(相对输入幅度 ~3,约 1%),这就是"尺子二"的正常地板——量化必然损失 ~1e-2,不可消除,只能控制。
3. 改动后果:改坏内核,看 1e-7 怎么变成 1
"内核一致性"这把尺子的价值在于:它能把你无意中改坏的累加顺序或越界从"静默错误"变成"数字爆表"。Day 1 我们做过"把参考实现改坏"的实验;反过来改坏被测内核效果一样:把 NEON 点积里某条 lane 的累加注释掉,rel 会从 1e-7 直接跳到接近 0.25(少了 1/4 的贡献)甚至更大——一亿分之一的差 vs 百分之几十的差,一眼可辨。
实际动手姿势(板端):改 vllm_l3.c 的 l3_q4_dot64_neon,让它少累加一段,./build_rk3588.sh 重建后跑 --test-l3 | grep scalar,你会看到 [FAIL] q4 dot vs scalar reference (rel 2.5e-01) 之类——然后 git checkout -- src/core/vllm_l3.c 还原。
# 改动后预期(示意):rel 从 7.68e-08 爆到 ~2.5e-01
./build-rk3588/vllm_kestrel --test-l3 | grep scalar
这就是"对拍"作为开发工具的用法:它不是上线前的仪式,而是每次改内核后 3 秒内给答案的回归。
4. 学员调试任务
- A 档(板端动手):
- 跑
--test-l3与--bench-mixed,分别记录"内核对拍 rel"与"混合精度门 exit code"; - 故意改坏一次内核(少累加一段),重建后看
rel从 1e-7 量级爆到多少,再还原; - 用自己的话写三句话:
1e-7、1e-2、1e-1分别在你眼里代表"代码处于什么状态"。
- 跑
- B 档(纯读源码):在
vllm_l3.c里找出"参考实现"与"被测内核"各一段,数清两者的累加顺序差异,解释为什么顺序不同却只差 1e-7。
预期输出:你能分清"内核一致性(1e-7)"与"量化损失(1e-2)"两把尺子,并在 3 秒内读懂一条 rel 输出意味着什么。
收尾
- 本篇源码点名:vllm_safetensors.c(
--bench-mixed混合精度门)、vllm_l3.c(scalar reference 对拍)。 - 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
- 下篇预告:对拍证明"算得对"了,可 CPU 标量路径每秒只能算几十 GFLOPS——想要几百 GFLOPS,只能让 NEON 向量指令上。第 6 天进 NEON 手写 GEMM:
vdotq_s32一条指令做 4 个点积。