动画图解 LLM 系统

KV cache 压缩:把历史装得更小

同样一段历史,能不能用更少的字节记住?

→ 逐步推进 · ← 回看 · 拖动时间轴观察数据流

动画图解 LLM 系统

先数清楚,缓存里有什么

一个标准 KV cache:层 × token × KV head × head dimension × 数值字节数。

每个小格代表一份 KV。长上下文、更多并发、更多层,都会让这些格子继续堆起来。

沿 head 轴做共享:GQA / MQA 减少每个 token 的 KV 头数。

沿表示或 token 轴做压缩,再降低每个数的精度。不同方法省的是不同的一维。

动画图解 LLM 系统

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。

工程细节

把四种“更小”写成不同的变量

M ≈ L × Tstored × Cstored × b + metadata
符号含义对应选择
L需要保存状态的层数混合架构的层布局
Tstored实际保留的位置 / 摘要数淘汰、窗口、序列压缩
Cstored每个位置保存多少个数MHA / GQA / MLA
b每个数的字节数BF16 / INT8 / INT4

这是核算框架;不同层可有不同项,最后逐层求和。不要把多个“压缩倍数”不加条件地相乘。

工程细节

GQA 省 KV,不按比例省全部参数

D = 4096、Hq = 32、Hkv = 8、d = 128。忽略 bias:

投影权重形状参数数
WQ4096 × 409616,777,216
WK、WV各 4096 × 1024各 4,194,304
WO4096 × 409616,777,216

KV 从 32 头降到 8 头,缓存缩小 4 倍;Q 与输出投影没变。四个投影合计从 67.11M 降到 41.94M 参数,缩小约 1.6 倍。

这是给定尺寸的结构算术。已训练的 MHA 不能直接丢弃 24 个头而保持原输出。

动画图解 LLM 系统

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 在逻辑上仍存在。

kᶜ = UK c,UK ∈ ℝᵈᶜ×ʳ
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 整份展开?

qᶜᵀkᶜ = qᶜᵀUKc = (UKᵀqᶜ)ᵀc
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 都更快。

动画图解 LLM 系统

少存 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,一般不保持原输出。

动画图解 LLM 系统

量化:位置都留着,每个数更短

用低位整数和少量 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]。

量化 q = round(x / s):[−7, −1, 2, 7]反量化 x̂ = s q:[−1, −0.143, 0.286, 1]最大误差:max |x − x̂| = 0.0571430

所有数值为手算教学示例。

工程细节

K 的误差会改变权重,V 的误差会改变内容

K̂ = K + ΔK
Δ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。

bytes = ceil(N / 2) + ceil(N/g) × 4
每个数 ≈ 0.5 + 4/g bytes
组大小 g每数平均字节相对 BF16 的压缩率
320.6252 / 0.625 = 3.2×
640.56253.556×
1280.531253.765×

这是仿射量化的示例存储格式。对称方案可不存 zero;实际 packing、对齐与残差区需另外核算。

逐步算一遍

带残差区的压缩预算,逐项加起来

单层:T = 8192、8 KV heads、d = 128。最近 128 token 留 BF16,旧 token 用 INT4;g = 64,每组元数据 4 bytes。

旧载荷:2 × 8064 × 8 × 128 × 0.5 bytes旧元数据:2 × 8064 × 8 × 128 / 64 × 4最近 128 token:2 × 128 × 8 × 128 × 20

所有数值为手算教学示例。

工程细节

为什么分组轴会影响离群值?

一个矩阵里,若某个 K 通道经常有大值,跨通道共享 scale 会把其他小通道挤在少数几个等级里。

按通道观察 K

比较各通道跨 token 的分布。独立 scale 能把持续偏大的通道隔开;在线分组可能需要缓冲未满的一组。

按 token 观察 V

一个 token 的 value 会一起被加权读出。按 token 分组更容易适应不同 token 的范围差异。

KIVI 的 K 按通道、V 按 token 是一套具体设计。是否适合新模型,要看实测分布与端到端质量。

动画图解 LLM 系统

同一份缓存,四个节省杠杆

方法 减少什么 保留什么 主要代价
GQA / MQA KV heads 每个历史位置 结构与训练要配合
MLA 每个 token 的表示 每个历史位置 投影、kernel 与布局
合并 / 淘汰 历史位置数 摘要 / 被选中位置 细粒度检索能力
KV 量化 每个数的 bit 数 位置与张量形状 误差与反量化开销

PagedAttention 管理缓存的物理分块;prefix caching 复用相同前缀。这两者不属于数值或语义压缩。

动画图解 LLM 系统

把节省落实到字节数

以一个假想模型为例: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 结构与量化方案。

动画图解 LLM 系统

怎么判断压得是否值得?

先量准确性

在自己的长上下文、代码、检索任务上比较输出。尤其检查远距离细节、多轮对话和罕见实体。

再量系统收益

看峰值 KV 占用、可用并发、prefill 延迟和 decode 延迟。压得更小不自动等于端到端更快。

定位误差来源

头共享改变模型容量;位置压缩改变可见细节;低位量化改变数值。分别做消融,才知道瓶颈在哪。

让 kernel 接住收益

紧凑布局、融合反量化、合适的 block 大小,才能把字节减少兑现成带宽节省。

工程细节

省带宽能否覆盖解码开销?

假想 profiling:压缩前 KV 读取 4 ms,其他 6 ms;压缩后 KV 读取 1.2 ms,反量化 0.8 ms,其他仍 6 ms。

原延迟 = 10 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 已不再对应同一段历史。

练习 1

一个“4 倍压缩”的说法少算了什么?

有 1,048,576 个 BF16 数。使用 INT4,g = 64;每组 scale 与 zero 合计 4 bytes。求总字节与压缩率。
展开参考解答

载荷 = 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×。

先写出假设和单位,再代入数字。

练习 2

latent 缓存减少了多少?

教学结构:32 heads,K/V 各 128 维;latent r = 512,位置 key p = 64,全部 BF16。求每 token 每层的两种字节数与压缩率。
展开参考解答

展开 KV:2 × 32 × 128 × 2 = 16,384 bytes。latent: (512 + 64) × 2 = 1,152 bytes。

压缩率 = 16,384 / 1,152 ≈ 14.222×。还需检查吸收后的 query 维度、投影 FLOP 和 kernel;这个比值不是延迟加速比。

先写出假设和单位,再代入数字。

练习 3

少读 90%,是否就能释放 90% KV?

一个动态选择器每步只读取 10% 历史。工程师据此释放另外 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 性能。

参考资料

参考与延伸阅读