系列:《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 篇 · 总纲(阿里云社区)
上一篇:4-3《核绑定实验:为什么"勿设 8 线程"》 | 下一篇:5-2《Q8 对称量化:scale 从哪来》
真机实测通过:本文实验已在 RK3588 板端实测完成(2026-09;方法学与原始记录见仓库 docs 与《实验脚本》目录)
一句话导读:推理引擎的边缘推理带宽瓶颈:以 decode 每生成一个词都要重读全部权重为起点,讲清推理时间近似等于权重字节数除以内存带宽的墙,落点在真实模型文件按量化类型的字节分解与紧凑排布的有效带宽实测。
关键词:手搓 Qwen 推理引擎、千问大模型推理、Q4/Q8 量化、带宽瓶颈、decode、内存带宽、RK3588、Qwen3-VL、零依赖纯 C
导语:推理引擎卡住的往往不是算力,而是带宽——decode 每生成一个词都要重读全部权重。本篇从「时间≈权重字节÷内存带宽」这堵墙出发,用真实模型文件的量化字节分解,与紧凑排布对 64B 对齐排布的有效带宽实测,说明 Q4/Q8 为何是边缘推理的生存前提。
第 4 天我们把算力调度清楚了,可推理引擎真正卡住的往往不是算力,是带宽——权重要从内存喂给 CPU,喂得慢,核再快也空转。这一篇讲清楚:为什么量化(Q4/Q8)不是"可选的压缩技巧",而是边缘推理的生存前提。
1. 知识点:decode 是"每 token 重读全部权重"的机器
先看 decode(生成一个 token)在做什么:要把整个网络从前到后跑一遍——每一层的 Q/K/V、gate/up/down 权重都要被读一次。也就是说:
decode 的时间 ≈ 全部权重的字节数 ÷ 内存带宽。
举例:一个 2B 模型若以 fp16 存权重(每元素 2 字节),每生成一个 token 至少要读约 4.2 GB 权重。哪怕内存带宽有 30 GB/s,光喂权重就要 ~0.14 秒/词——这就是"带宽墙":多核并行救不了它,因为它不是算不过来,是"吃不下"。
那怎么办?两条路一起走:
- 降字节:把每元素从 2 字节(fp16)降到 1 字节(Q8)甚至半字节(Q4),decode 每词要搬的字节数直接砍半/砍到 1/4;
- 紧凑布局:数据排得越密,一次突发传输能用的有效字节越多(第 3 节实测:同样的数据,紧凑排 vs 对齐大步长排,有效带宽差 3 倍)。
量化就是"用精度换字节"的工程:多花一点点数值误差,把内存与带宽需求砍到能塞进边缘设备。
2. 对应代码:真实模型文件里,量化到底省了多少
直接看板端真实的 2B 模型。同源权重两个文件并排躺着:
model.safetensors 4,255,140,312 B (4.25 GB,源权重)
model.vqf 2,331,316,232 B (2.33 GB,引擎格式 = Q8/Q4 混合 + F16 embed)
2.33 GB 只是 4.25 GB 的 ~55%——但这是"混合档"。我们用一段 20 行 Python 解析 VQF 头部目录(41 个张量),按量化类型统计真实字节占比(板端实测,2026-09):
n_tensors=41
token_embed F16 622.3 MB (151936 词表 × 2048 维,F16 半精度)
norm 层 F32 ~13 MB (attn/ffn/q/k norm 合计,量小不值得量化)
q4_q Q4_0 66.1 MB ... 28 层 × Q/K/V/O/gate/up/down
q8_* Q8_0 320.9 MB (attention 的 Q8 副本)
q4_* Q4_0 967.8 MB (FFN 为主的 Q4 副本)
v_* (视觉塔) F32 420.4 MB (vision tower 仍是 F32)
TOTAL bytes by qtype: {F32: 420.4, Q8_0: 320.9, Q4_0: 967.8, F16: 622.3} MB
grand total MB: 2331.3 (≈ 文件大小 2331316232 B,对得上)
这张表本身就是"量化策略":大头(FFN 的 gate/up/down)用 Q4 最省;attention 用 Q8 保精度;embedding 是查表密集、用 F16;视觉塔暂留 F32。要是全 fp32,光文本部分就是 ~8 GB——板上根本放不下。量化不是"要不要",是"哪部分用几 bit"的分配问题。
3. 改动后果:布局决定你能用多少带宽
量化把字节数降下来了,但同样的字节,排法不同,有效带宽能差 3 倍。引擎的 --bench-mixed 里内置了一个"真实 DRAM 探测":在板上扫 131072 行 × 128 块的数据,比较三种块排布能达到的有效吞吐(板端实测):
--- Real-DRAM wall probe (131072 rows x 128 blocks/row) ---
compact 18B/block : 19.6 ms -> 15.4 GB/s payload
compact 34B/block : 36.1 ms -> 15.8 GB/s payload
64B-aligned/block : 58.2 ms -> 5.2 GB/s payload (raw 18.5 GB/s)
读同样的数据总量,紧凑排布 ~15.5 GB/s 有效,而"每个块都从 64B 边界取"的排布只有 5.2 GB/s——因为你读 64 字节只用到其中 18 字节,约 70% 的带宽被"跳格"浪费了(原始突发带宽 18.5 GB/s 其实差不多)。这就是 Q4_0 每 32 个权重只占 18 字节({f16 scale; 16 字节 nibble})这种紧凑格式的价值:省字节 + 不浪费突发传输,两个维度一起省。
顺带一提:同一次 bench 还给出引擎两种量化路径的真实内核吞吐(合成 4K 权重,273MB 双份):decode 档 Q8 fused QKV 1.695ms(29.7 GFLOPS)、Q4 3.566ms(14.1 GFLOPS)等——这些数字在第 6–7 天我们逐行拆解时会反复引用。
4. 学员调试任务
- A 档(板端动手):跑
./build-rk3588/vllm_kestrel --bench-mixed,找到Real-DRAM wall probe三行,确认你板上的紧凑 vs 64B 对齐排布的有效带宽差;再用第 2 节的小脚本(读 VQF 头目录按 qtype 求和)复现你模型文件的体积分解。 - B 档(纯读源码):读 vllm_safetensors.c 第 2907–2956 行的格式注释(Q8_0/Q4_0 的 block 定义),写出"32 个 f32 → Q8_0 34 字节 / Q4_0 18 字节"的换算公式。
预期输出:你能解释"decode 每词重读全部权重"为什么让带宽成为瓶颈、Q4/Q8 各砍到多少字节/元素,以及紧凑布局为什么能再多榨一倍有效带宽。
收尾
- 本篇源码点名:vllm_safetensors.c(Q8_0/Q4_0 格式注释第 2907–2956 行、
--bench-mixed的 DRAM 探针)、真实model.vqf(41 张量按 qtype 分解)。 - 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
- 下篇预告:Q4/Q8 把字节砍了,可"还原"靠的是一个叫 scale 的标量——这个标量从哪来、存成什么样?下一篇 5-2 拆 Q8 对称量化的标定公式。