Day 9·1 KV 也量化——q8 KV 把缓存与 decode 带宽压到一半

源码精读篇:本文为源码/方法论精读,无独立实测;文中数字均引述仓库 docs 的板端实测记录
一句话导读:q8 KV:把 K、V 从 f16 压到 INT8,按每 token 每头定标量化,逐字节算清缓存与 decode 扫描带宽省约一半的账,并讲清 KV 为何敢量化、f32 为何只是调试探针。

8-3 我们把 KV 的布局讲清楚了,可一个残酷的事实还在:KV 是模型里除权重外最肥的东西。8K 上下文 × 28 层 × 8 KV 头 × 128 维,若用 f16 存 K、V 各一份,光这一块就约 560MB------decode 每词还要把它们全扫一遍。今天把 KV 也量化成 q8,看带宽怎么压到约一半。

1. 知识点:为什么 KV 敢量化

权重能 Q4/Q8(第 5 天),KV 为什么不能?量化 KV 的顾虑是"精度",但注意两个事实:

  1. decode 只"读"KV:K/V 一旦写入就只被乘与累加,量化误差只影响当次注意力打分/加权,不像权重误差会一层层传播叠加;
  2. KV 每维都有独立的 scale:引擎对每个 (token, head) 单独定标(见下),128 维共享一个 scale,误差被锁在"一行的局部",不会累积。

于是引擎默认把内存 KV 压成 INT8(q8) :每个元素从 2 字节(f16)降到 1 字节,K、V 各一份直接砍半 ;再配合 8-3 的"decode 扫全 KV"路径,每词的 KV 读取带宽同比例减半(另有更狠的 --kv-q4 模式,K/V 再压一半,约 1.65 倍更密,Day 24 思路同源)。

2. 对应代码:q8 KV 的存储与"一 token 一 scale"

引擎在分配阶段默认走 q8(vllm_safetensors.c 第 8208--8244 行):

c 复制代码
/* Compressed KV cache: INT8 (near-lossless) by default; --kv-q4 switches
 * to the Q4_0 payload cache (~1.65x denser, decode dot without dequant). */
st->use_kv_q8 = g_kv_q4 ? 0 : 1;
...
st->k_cache_q8[l] = alloc_kv_blocks_i8(n_blocks, bs, kv_dim, ...);
st->k_scale[l] = cf_calloc_guard((size_t)max_seq * nkv, sizeof(float), ...); /* 每 token 每头一个 scale */

写入时按"每 token 每头"做 max-abs INT8 量化(第 9466--9476 行):

c 复制代码
if (st->use_kv_q8) {
    /* Per-token per-head max-abs INT8 K/V quantization (near-lossless). */
    int8_t *kdst = kv_row_i8(st->k_cache_q8[l], cl, kv_dim, st->kv_bs);
    int8_t *vdst = kv_row_i8(st->v_cache_q8[l], cl, kv_dim, st->kv_bs);
    kv_quantize_per_head(kdst, vdst,
                         st->k_scale[l] + (size_t)cl * nkv,
                         st->v_scale[l] + (size_t)cl * nkv,
                         st->k_buf, st->v_buf, nkv, hd);
    /* Also store in float cache */  /* fp32 镜像仍保留(调试/对照用) */
    memcpy(... k_buf, kv_dim * sizeof(float)); ...
}

逐字节算笔账(2B 模型参数:nkv=8hd=128,即每个 token 每层 K 与 V 各 kv_dim = 1024):

复制代码
f16 KV:  K 1024×2B + V 1024×2B            = 4096 B / token / 层
q8  KV:  K 1024×1B + V 1024×1B = 2048 B
         + scale:8 头 × (K、V 各一) × 4B  =  64 B
         合计                              = 2112 B / token / 层
→ 4096 → 2112:缓存字节与 decode 全量扫描带宽均 ×0.52(省约 48%,接近一半)

scale 的粒度是"每个 (token, 头)"------比"整层一个 scale"细得多,这正是 q8 KV 敢自称 near-lossless 的原因(9-3 会给实测误差)。

3. 改动后果:关掉 q8 KV 试试(--kv-q4 / 全 f32 的代价)

引擎把 KV 模式做成可切:

  • 默认 q8:near-lossless,解码走 INT8 单遍(9-2 主角);
  • --kv-q4:切到 Q4_0 payload 缓存,比 q8 再密 ~1.65 倍,decode 点积免反量化直接吃 nibble;
  • 全 f32 的 KV:代码注释明确记为 DEBUG 用途 (第 8210--8214 行:曾用来排查 K 缓存损坏,定位后恢复生产默认)------f32 KV 不是给生产用的,是调试探针

想亲眼看到"KV 模式影响带宽":连跑 --bench-mixed 之外的 decode 场景不现实(无直接开关计时),但可以换位验证------8-3 说过 decode 每词扫全 KV;KV 从 4096B/token/层(f16)降到 2112B(q8)后,decode 的 KV 扫描带宽需求直接 ×0.52,这正是基准报告里 8K decode 引擎能维持 ~137ms/词(vs llama f16-KV 全扫 411ms)的重要来源之一(叠加 Day 10 的稀疏,详见 9-2/10 天)。

4. 学员调试任务

  • A 档(板端动手)
    1. 跑引擎 --kv-q4 与默认模式各一次短生成(同一 prompt),记录输出 token 是否一致(q4 KV 比 q8 更激进,肉眼可看的场景值得留个心眼);
    2. 用 5-1 的解析脚本确认模型 KV 相关张量名,结合本页算一遍"2B 8K 上下文 q8 KV 约多少 MB"(8K×28 层×2112B≈?)。
  • B 档(纯读源码) :读 alloc_kv_blocks_i8/kv_quantize_per_head(在 vllm_safetensors.c 内搜索),画出"一个 token 的 K 行 → 1024 int8 + 8 个 float scale"的落盘/落存图。

预期输出:你能算清 f16→q8 KV 省多少字节、scale 为什么按"每 token 每头"、以及 f32 KV 为何只是调试探针。

收尾

  • 本篇源码点名vllm_safetensors.c(q8 KV 分配与开关第 8208--8244 行、per-head 量化写入第 9466--9476 行)。
  • 开源仓库Kestrel-LLM (Gitee)(源码可得双许可:学习 / 学术研究免费)
  • 下篇预告 :q8 KV 存好了,decode 怎么读它才最省?下一篇 9-2 进 flash_attn_single_q_q8_neon------量化 Q、在线 rescale、一条路径扫完全部 KV。
  • 关键词:q8 KV、KV cache、INT8 量化、带宽、decode

上一篇: Day 8·3 KV Cache按头存储vs按token存储:内存布局

下一篇: Day 9·2 flash_attn_single_q_q8_neon------单 q 单遍扫全 KV

相关推荐
陈童学哦1 小时前
AI术语大全
人工智能
tuanxiang1 小时前
用去AI味工具调整大模型生成的技术文档,我踩了检测阈值的坑
人工智能
天辛大师1 小时前
天辛大师谈AI时代的沉思录,All in AI 持续做事与放大效应
大数据·人工智能·随机森林·重构·启发式算法
CoderYanger1 小时前
Java EE 进阶:2.3 JavaScript
java·开发语言·前端·javascript·css·职场和发展·java-ee
不要生病了1 小时前
Brain-JEPA:用功能梯度定位与时空遮蔽预训练 fMRI 基础模型
人工智能·深度学习
艺杯羹1 小时前
穿透深度慢思考黑盒:DeepSeek-R1 蒸馏模型量化压缩、vLLM 极限吞吐与私网落地实战
人工智能·大模型·部署
韦韦(Carina)1 小时前
技术博客结构化:让 AI 准确引用你的 7 条排版铁律
大数据·人工智能
Runwise创新社区1 小时前
Anthropic多智能体架构怎么落地?Lead Agent、共享记忆与并行任务的设计边界
人工智能·架构·多智能体
angushine1 小时前
文本转视频2
人工智能·音视频·语音识别