KV Cache 瘦身术:从 GQA 到 PagedAttention
上一篇我们知道了 KV Cache 的核心矛盾:不存不行(O(n²)太慢),全存也不行(内存爆炸)。这篇来看解决方案——从”少存一点”到”存得更聪明”。
策略一:共享 KV 头——MQA / GQA / MLA
问题:32 个头真的需要 32 套独立的 KV 吗?
标准 Multi-Head Attention(MHA)中,每个注意力头有自己独立的 Query、Key、Value 投影。32 个头 = 32 套 K 和 V 存储在 cache 中。
但仔细想想——不同头的 Key 和 Value 之间高度相关。它们都在表示同一段文本的信息,只是从不同”角度”看。这种冗余可以利用。
从 MQA 到 GQA 到 MLA:越来越精细的共享
MQA (2019):所有 query 头共享一组 KV。压缩最狠(32×),但质量有损——一个 KV 头承载所有信息,成了瓶颈。
GQA (2023, LLaMA 2 采用):把 32 个 query 头分成 8 组,每组共享一个 KV 头。4× 压缩,质量损失几乎可忽略。这是当前最流行的方案。
MLA (2024, DeepSeek-V2):最激进——不存 K 和 V 本身,而是存一个低秩压缩潜向量 $c_t$,需要时再解压还原。压缩 16-32×,质量反而更好(因为联合优化了压缩方式)。
策略二:量化——用更少的位数存
KV Cache 默认用 FP16(16位)。但实验发现,attention 对 K/V 的数值精度并不那么敏感。
- INT8 量化:每个值从 2 字节降到 1 字节 → 2× 内存减半,质量损失 < 0.5%
- INT4 量化:4 位存储 → 4× 压缩,需要更精细的量化策略
- KIVI (2024):发现 Key 和 Value 有不同的量化特性(Key 按通道有异常值,Value 按 token 有异常值),针对性地用不同策略量化到 2 位,质量几乎无损
量化可以和 GQA/MLA 叠加:GQA (4×) + INT4 (4×) = 16× 总压缩。
策略三:PagedAttention——像操作系统管内存
碎片化才是真正的敌人
传统系统给每个请求预分配一大块连续内存(准备好最大可能长度)。但大部分请求用不了那么多——就像停车场全画大车位,结果来的都是小车。
研究表明 60-80% 的 KV Cache 内存被碎片化浪费。
PagedAttention 的操作系统思维
vLLM 的 PagedAttention 直接借鉴了操作系统的虚拟内存管理:
核心思想:把 KV Cache 切成固定大小的页(blocks),散落在 GPU 内存的任何位置。用一个”页表”记录每个序列的 blocks 在哪。按需分配,用完即释。
效果:内存浪费从 60-80% 降到 < 4%,同一张 GPU 可以同时服务 2-4× 更多请求。
策略四:选择性遗忘——不是每个 token 都值得记住
如果上下文实在太长,即使压缩+分页也装不下呢?一个激进但有效的思路:只保留重要的 token,扔掉不重要的。
H₂O (Heavy Hitter Oracle):追踪每个 token 累积获得的 attention 分数。高分的(”重量级选手”)保留,低分的淘汰。只保留 20% 的 KV Cache 就能维持质量。
StreamingLLM:发现了一个有趣现象——序列最开头的几个 token(不管内容是什么)会获得异常高的 attention,它们是”注意力水槽”(attention sinks)。如果你把它们删了,模型就崩了。解决方案:永远保留前 4 个 token + 最近 L 个 token 的滑动窗口。可以稳定生成超过400 万 token 而内存不增长。
滑动窗口 (Mistral):更简单粗暴——只保留最近 W 个 token 的 KV(W=4096),超过的直接覆盖。通过多层堆叠,有效感受野远大于 W。
这些技术可以叠加
一个生产级系统可能同时使用:
- GQA (架构层面) → 4× 压缩
- INT4 量化 (精度层面) → 4× 压缩
- PagedAttention (内存管理) → 消除 60-80% 浪费
- H₂O 淘汰 (容量控制) → 再减 5×
总效果:相比朴素 MHA + FP16 + 连续分配,可以实现 50-100× 的等效内存节省。
下一篇我们来看一个更上层的优化:如果很多请求的 prompt 开头都一样(比如系统提示、few-shot 示例),能不能只算一次前缀的 KV Cache,后面所有请求复用?这就是 Prompt Caching——一个能帮你省 90% API 账单的技术。