KV cache 压缩:把历史装得更小
同样一段历史,能不能用更少的字节记住?
→ 逐步推进 · ← 回看 · 拖动时间轴观察数据流
先数清楚,缓存里有什么
一个标准 KV cache:层 × token × KV head × head dimension × 数值字节数。
每个小格代表一份 KV。长上下文、更多并发、更多层,都会让这些格子继续堆起来。
沿 head 轴做共享:GQA / MQA 减少每个 token 的 KV 头数。
沿表示或 token 轴做压缩,再降低每个数的精度。不同方法省的是不同的一维。
GQA:几个 Q,共用一组 K、V
Q 的头数保持不变,K、V 的头数减少。
左侧 8 个 query heads 原本各有一组 K、V,缓存需要保存 8 份。
把 query heads 分成两组,每组 4 个 Q 共用一组 K、V。KV 头数从 8 变成 2。
MQA 再走一步:所有 Q 共用一组 KV。不同 Q 仍会产生不同 attention 权重与输出。
8 / 2 / 1 为结构示意;GQA / MQA 是模型结构选择,通常需要训练或 uptraining。
把四种“更小”写成不同的变量
| 符号 | 含义 | 对应选择 |
|---|---|---|
| L | 需要保存状态的层数 | 混合架构的层布局 |
| Tstored | 实际保留的位置 / 摘要数 | 淘汰、窗口、序列压缩 |
| Cstored | 每个位置保存多少个数 | MHA / GQA / MLA |
| b | 每个数的字节数 | BF16 / INT8 / INT4 |
这是核算框架;不同层可有不同项,最后逐层求和。不要把多个“压缩倍数”不加条件地相乘。
GQA 省 KV,不按比例省全部参数
D = 4096、Hq = 32、Hkv = 8、d = 128。忽略 bias:
| 投影 | 权重形状 | 参数数 |
|---|---|---|
| WQ | 4096 × 4096 | 16,777,216 |
| WK、WV | 各 4096 × 1024 | 各 4,194,304 |
| WO | 4096 × 4096 | 16,777,216 |
KV 从 32 头降到 8 头,缓存缩小 4 倍;Q 与输出投影没变。四个投影合计从 67.11M 降到 41.94M 参数,缩小约 1.6 倍。
这是给定尺寸的结构算术。已训练的 MHA 不能直接丢弃 24 个头而保持原输出。
MLA:把多头 KV 存成低维 latent
多个头的内容可以先压进一小条联合表示 c,再用于 attention。
标准多头缓存直接存展开的 K、V。MLA 为每个 token 缓存低维 latent c。
内容路径由 c 提供多个头需要的 K、V 信息;推理中可通过矩阵乘法结合律,把部分上投影吸收到其他权重中。
位置路径单独保存用于 RoPE 的 key。缓存由“内容 latent + 位置 key”组成;长度仍然随 token 数增长。
示意结构来自 DeepSeek-V2 的 MLA;实际尺寸与权重吸收方式由架构决定。
MLA 缓存的到底是哪几个张量?
用一个 head 推导:c ∈ ℝʳ 是内容 latent;kᴿ ∈ ℝᵖ 是位置 key。内容 key / value 在逻辑上仍存在。
v = UV c,UV ∈ ℝᵈᵛ×ʳ
cache 每位置 = c + kᴿ,共 r + p 个数
示例:32 heads、每头 K/V 各 128 维,展开后是 8192 个数;若 r = 512、p = 64,latent cache 是 576 个数。
此处尺寸是教学示例。DeepSeek-V2 的 MLA 结构是这一推导的参考,不是任意模型的可替换压缩器。
结合律怎样避免把 K、V 整份展开?
aᵢ = softmax(scoreᵢ)
Σᵢ aᵢvᵢ = Σᵢ aᵢUVcᵢ = UV(Σᵢ aᵢcᵢ)
先把 query 投到 latent 空间,再与缓存的 c 匹配;value 路径先求 latent 的加权和,最后做 UV 投影。
每个 head 有自己的 query、UK、UV 与权重 a。位置分数 qᴿᵀkᴿ 另外加入 logits;不能把 RoPE 的位置旋转随意吸收到固定矩阵里。
减少缓存流量的同时,也改变了算子的维度与计算量。较小缓存不自动意味着任意 batch 都更快。
少存 token:合并,或淘汰
头数和维度都不变时,还能减少历史位置的数量。
一开始保存每个位置。越靠右越新,旧位置仍占着完整的 KV 空间。
合并:把一组 token 表示汇成一个摘要。位置变少,但摘要要承载这组内容。
淘汰:只保留被选中的 KV,例如重要历史加最近窗口;未来若要用到被丢弃的细节,就无法直接读取它。
动画只展示两种操作;实际策略可能按层、头、query 或累积 attention 得分选择。
淘汰一个位置,需要承担未来查询的代价
保存位置 [0, 1, 2, 3, 4, 5],当前只读 [0, 4, 5]。这一步没有读取 1,不代表下一步不会读取 1。
稀疏读取
保留完整 cache,只为当前 Q 选择子集。下个 Q 仍有机会找到先前没选中的 token。
真正释放
删掉 KV 后,后续查询只能依靠保留内容或重新计算。要用任务质量来评估这个不可逆的信息损失。
摘要还必须满足因果性:位置 t 的 query 不能读取包含 t 之后信息的压缩块。
压缩了位置,位置编码也要有定义
将位置 0…7 合成两个摘要。摘要内容、代表位置、可见时间,是三个需要分别规定的量。
| 问题 | 一种明确的设计 | 要检查什么 |
|---|---|---|
| 什么时候能读摘要? | 组内最后一个 token 完成后 | 不能泄露尚未出现的 token |
| 摘要位置信息来自哪? | 采用模型训练时定义的位置规则 | 不能临时把位置重排成 0、1 |
| 最近 token 怎么处理? | 保留原始局部窗口 | 摘要与原始 token 重叠是否被重复计权 |
训练过的压缩 attention 会定义这些规则。对普通 softmax 模型直接平均 K/V,一般不保持原输出。
量化:位置都留着,每个数更短
用低位整数和少量 scale 元数据近似原来的 K、V。
BF16 的每个数占 16 bit。降低位宽,就能让同一段历史少搬一些字节。
按组量化:x 大致表示为 s × (q − z)。把浮点数映射到有限整数等级,并保存 scale 等元数据。
attention kernel 读取后反量化,或使用支持低位数据的计算路径。压缩比例还要扣除元数据、残差区和对齐开销。
4 bit 为示意;KIVI 对 K 按通道、对 V 按 token 分组,以适应不同的离群值分布。
对一组数真的做一次 4 bit 量化
教学方案:对称有符号 q ∈ [−7, 7],scale s = max|x| / 7 = 1/7;x = [−1, −0.2, 0.3, 1]。
所有数值为手算教学示例。
K 的误差会改变权重,V 的误差会改变内容
Δlogitᵢ = qᵀΔkᵢ / √d
|Δlogitᵢ| ≤ ||q||₂ ||Δkᵢ||₂ / √d
K 误差经过 softmax
相近 logits 的排序可能改变;某些 query 会比平均误差更敏感。只测 K 的均方误差不足以判断输出质量。
先固定权重看 V
Δy = Σ aᵢΔvᵢ。若 aᵢ ≥ 0 且和为 1,则 ||Δy|| ≤ maxᵢ ||Δvᵢ||;K 也变化时还要计权重变化。
先分别量化 K 和 V,比较 logits、attention 输出与任务指标,能更快找到敏感路径。
4 bit 的实际压缩率由分组大小决定
N 个数,每组 g 个,整数载荷 4 bit / 数;每组保存 BF16 scale 与 BF16 zero,共 4 bytes。
每个数 ≈ 0.5 + 4/g bytes
| 组大小 g | 每数平均字节 | 相对 BF16 的压缩率 |
|---|---|---|
| 32 | 0.625 | 2 / 0.625 = 3.2× |
| 64 | 0.5625 | 3.556× |
| 128 | 0.53125 | 3.765× |
这是仿射量化的示例存储格式。对称方案可不存 zero;实际 packing、对齐与残差区需另外核算。
带残差区的压缩预算,逐项加起来
单层:T = 8192、8 KV heads、d = 128。最近 128 token 留 BF16,旧 token 用 INT4;g = 64,每组元数据 4 bytes。
所有数值为手算教学示例。
为什么分组轴会影响离群值?
一个矩阵里,若某个 K 通道经常有大值,跨通道共享 scale 会把其他小通道挤在少数几个等级里。
按通道观察 K
比较各通道跨 token 的分布。独立 scale 能把持续偏大的通道隔开;在线分组可能需要缓冲未满的一组。
按 token 观察 V
一个 token 的 value 会一起被加权读出。按 token 分组更容易适应不同 token 的范围差异。
KIVI 的 K 按通道、V 按 token 是一套具体设计。是否适合新模型,要看实测分布与端到端质量。
同一份缓存,四个节省杠杆
| 方法 | 减少什么 | 保留什么 | 主要代价 |
|---|---|---|---|
| GQA / MQA | KV heads | 每个历史位置 | 结构与训练要配合 |
| MLA | 每个 token 的表示 | 每个历史位置 | 投影、kernel 与布局 |
| 合并 / 淘汰 | 历史位置数 | 摘要 / 被选中位置 | 细粒度检索能力 |
| KV 量化 | 每个数的 bit 数 | 位置与张量形状 | 误差与反量化开销 |
PagedAttention 管理缓存的物理分块;prefix caching 复用相同前缀。这两者不属于数值或语义压缩。
把节省落实到字节数
以一个假想模型为例:32 层、8192 token、32 个 KV 头、head dim 128。
BF16 缓存:2 × 32 × 8192 × 32 × 128 × 2 = 4 GiB,每条序列一份。
换成 8 个 KV 头的 GQA,其他维度不变:4 GiB ÷ 4 = 1 GiB。
再把数值编码从 16 bit 降到 4 bit:纯载荷降至 0.25 GiB。实际占用还要加量化元数据等开销。
算术示例;模型需匹配相应的 GQA 结构与量化方案。
怎么判断压得是否值得?
先量准确性
在自己的长上下文、代码、检索任务上比较输出。尤其检查远距离细节、多轮对话和罕见实体。
再量系统收益
看峰值 KV 占用、可用并发、prefill 延迟和 decode 延迟。压得更小不自动等于端到端更快。
定位误差来源
头共享改变模型容量;位置压缩改变可见细节;低位量化改变数值。分别做消融,才知道瓶颈在哪。
让 kernel 接住收益
紧凑布局、融合反量化、合适的 block 大小,才能把字节减少兑现成带宽节省。
省带宽能否覆盖解码开销?
假想 profiling:压缩前 KV 读取 4 ms,其他 6 ms;压缩后 KV 读取 1.2 ms,反量化 0.8 ms,其他仍 6 ms。
新延迟 = 1.2 + 0.8 + 6 = 8 ms
加速 = 1.25×,延迟减少 20%
如果 batch 很小、历史很短,原 KV 读取只有 0.4 ms,而新增反量化仍 0.8 ms,就可能变慢。融合 kernel 与选择性启用都值得测。
测量按阶段拆分,避免把容量允许的更大 batch 收益,误当成固定 batch 的单请求加速。
用成对实验寻找质量退化的位置
| 实验轴 | 保持其他条件不变 | 观察指标 |
|---|---|---|
| 仅量化 K / 仅量化 V | 相同请求、种子与解码策略 | 逐层输出误差、logits KL |
| 2K / 8K / 32K 历史 | 相同关键证据与问题 | 远距离检索、实体和数字准确率 |
| 分组 32 / 64 / 128 | 相同 bit 与残差区 | 质量、元数据占用、TPOT |
| 近期保留 0 / 128 / 512 | 相同分组方式 | 稳定性与实际 KV 字节 |
先用固定 token 序列比较数值,再让模型自由生成比较任务表现。自由生成分叉后,逐 token logits 已不再对应同一段历史。
一个“4 倍压缩”的说法少算了什么?
展开参考解答
载荷 = 1,048,576 / 2 = 524,288 bytes。组数 = 16,384,元数据 = 65,536 bytes。
合计 589,824 bytes = 576 KiB。基线 2,097,152 bytes = 2048 KiB;压缩率 = 2048 / 576 ≈ 3.556×。
先写出假设和单位,再代入数字。
latent 缓存减少了多少?
展开参考解答
展开 KV:2 × 32 × 128 × 2 = 16,384 bytes。latent: (512 + 64) × 2 = 1,152 bytes。
压缩率 = 16,384 / 1,152 ≈ 14.222×。还需检查吸收后的 query 维度、投影 FLOP 和 kernel;这个比值不是延迟加速比。
先写出假设和单位,再代入数字。
少读 90%,是否就能释放 90% KV?
展开参考解答
当前 Q 的选择集合与下一个 Q 可能不同;被释放位置无法再次参与匹配,因此不再等价。
固定完整历史,构造连续查询分别命中不同远处位置;比较保留全 cache 的稀疏读取与释放版本。若要真正淘汰,应单独评估这种策略的任务质量。
先写出假设和单位,再代入数字。
把压缩误差与字节预算一起跑通
用 NumPy 在 CPU 上跑一个可检查的参考实现。小尺寸便于把中间张量打印出来。
python3 -m pip install numpy python3 lab.py python3 lab.py --seed 7
INT4 round-trip bound: PASS K-only / V-only error measured metadata: 576 KiB; ratio = 3.556 residual example: 299.500 MiB
把 group size 从 64 改成 32 / 128,分别记录误差与元数据;再给一个 K 通道放大 20 倍,比较按通道与按 token 的误差。
下载实验代码 lab.py这是正确性与成本的教学实验;耗时不代表 GPU kernel 性能。
参考与延伸阅读
- GQA · Ainslie et al.:头共享与 uptraining
- DeepSeek-V2:Multi-head Latent Attention
- KIVI:KV cache 的非对称低位量化
- H₂O:保留重要历史与最近 token
继续看:KV cache · Sparse attention · LLM 量化