跳转至

KV Cache 原理与优化

面试高频考点

  • KV Cache 是什么?解决了什么问题?
  • KV Cache 的显存占用如何计算?
  • PagedAttention 的思路?
  • GQA / MQA 为什么能减少 KV Cache?
  • Prefill 和 Decode 的区别?

外部图解:Prefill / Decode 与 KV Cache

Imagination Tech:LLM Prefill / Decode 与 KV Cache 示意

图源:Imagination Tech - LLM Acceleration on PowerVR。这张图把 prefill、decode 和 KV Cache 的关系画得很直观:prefill 建缓存,decode 复用缓存逐 token 生成。


一、为什么需要 KV Cache?

细化理解: 自回归生成中,每一步新增 token 都要看历史上下文。如果不缓存,生成第 n 个 token 时需要反复重算前面 n-1 个 token 的 attention 投影,成本会随输出长度快速累积。KV Cache 缓存历史 Key/Value,使每一步只计算新增 token 的投影并读取历史缓存,是 LLM 在线推理的基础优化。

自回归生成的问题

LLM 逐 token 生成时,每生成一个新 token,传统做法需要对所有历史 token 重新计算 Attention。这意味着:

  • 生成第 1 个 token:算 1 个 token 的 Attention
  • 生成第 2 个 token:算 2 个 token 的 Attention
  • 生成第 n 个 token:算 n 个 token 的 Attention

总计算量 = 1 + 2 + 3 + ... + n = O(n²),非常浪费。

KV Cache 的核心思想

Attention 计算中,已生成 token 的 Key 和 Value 在后续步骤中不会改变(因果掩码保证当前 token 看不到未来):

Step 1: 生成 "我" → 计算 K₁,V₁ 并缓存
Step 2: 生成 "爱" → 重用 K₁,V₁,只算 Q₂,K₂,V₂ → 缓存 K₂,V₂
Step 3: 生成 "你" → 重用 K₁,V₁,K₂,V₂,只算 Q₃,K₃,V₃ → 缓存 K₃,V₃
...

每步只计算 1 个新 token 的 Q,K,V,将新 K,V 追加到缓存中。

效果:每步计算量从 O(n²·d) 降至 O(n·d),推理速度大幅提升。

        ┌─────────────────────────────────────┐
        │          KV Cache 示意图              │
        │                                      │
        │  时间步 →                            │
        │                                      │
        │  t=1: [K₁V₁]                        │
        │  t=2: [K₁V₁][K₂V₂]                  │
        │  t=3: [K₁V₁][K₂V₂][K₃V₃]            │
        │  t=4: [K₁V₁][K₂V₂][K₃V₃][K₄V₄]      │
        │                                      │
        │  每步新计算的 Q_t 与所有历史 K 做点积    │
        │  Q₄ · [K₁ K₂ K₃ K₄] → softmax → × V  │
        └─────────────────────────────────────┘

为什么只缓存 K 和 V,而不缓存 Q?

Q(Query)只用于当前 token 查询历史信息,后续 token 不需要之前 token 的 Q。而 K(Key)和 V(Value)需要被未来的 token 查询和聚合。


二、KV Cache 显存计算

工程细节: KV Cache 大小近似正比于 batch size、序列长度、层数、KV head 数、head 维度和数据类型字节数。长上下文、多并发和多轮对话会让缓存迅速超过权重显存。GQA/MQA、KV 量化、PagedAttention 和请求调度,本质上都在缓解 KV Cache 带来的显存与带宽压力。

公式

KV Cache 大小 = 2 × n_layers × n_kv_heads × d_head × seq_len × batch_size × dtype_bytes

其中 2 代表 K 和 V 各一份。

逐层推导

LLaMA-2 7B(FP16,batch=1,seq_len=4096)为例:

参数 说明
n_layers 32 Transformer 层数
n_kv_heads 32(MHA 模式) KV 头数,GQA 下更少
d_head 128 每头维度
seq_len 4096 序列长度
dtype_bytes 2 FP16 = 2 bytes
KV Cache = 2 × 32 × 32 × 128 × 4096 × 1 × 2 bytes
         = 2 × 32 × 32 × 128 × 4096 × 2
         = 2,147,483,648 bytes ≈ 2 GB

不同模型的 KV Cache 对比

模型 参数量 KV 头数 seq_len=4096 FP16 seq_len=128K FP16
LLaMA-2 7B (MHA) 7B 32 ~2 GB ~64 GB
LLaMA-2 70B (GQA) 70B 8 ~1 GB ~33 GB
Mistral 7B (GQA) 7B 8 ~0.5 GB ~16 GB

关键发现:长上下文场景下,KV Cache 显存可远超模型权重本身(70B 模型权重 ~140GB,128K 序列单个 batch 的 KV Cache 就 33GB)。

GQA 如何减少 KV Cache

MHA(Multi-Head Attention):
  Q₁ Q₂ Q₃ Q₄ → 各自对应独立的 K₁/V₁, K₂/V₂, K₃/V₃, K₄/V₄
  KV 份数 = 头数 h

GQA(Grouped-Query Attention, G=4):
  Q₁ Q₂ → 共享 K₁/V₁
  Q₃ Q₄ → 共享 K₂/V₂
  KV 份数 = h/G

MQA(Multi-Query Attention):
  Q₁ Q₂ Q₃ Q₄ → 全部共享同一组 K/V
  KV 份数 = 1

GQA 节省比例 = h / G。例如 LLaMA-2 70B 用 GQA with 8 KV heads(共 64 个 Q heads),KV Cache 减少 8 倍。


三、Prefill 与 Decode 阶段

两个阶段对比

┌──────────────────────────────────────────────────────┐
│                   推理两个阶段                         │
├──────────────────────┬───────────────────────────────┤
│   Prefill (预填充)     │   Decode (解码)               │
├──────────────────────┼───────────────────────────────┤
│ 输入:整个 prompt      │ 输入:上一时刻生成的 1 个 token │
│ 计算:并行处理所有 token │ 计算:只算当前 token + KV Cache │
│ 瓶颈:Compute-Bound   │ 瓶颈:Memory-Bound            │
│ GPU 算力受限           │ 显存带宽受限                   │
│ 产出:初始 KV Cache    │ 产出:逐 token 追加到 KV Cache │
└──────────────────────┴───────────────────────────────┘

为什么 Decode 是 Memory-Bound?

每次 Decode 步只处理 1 个 token,计算量极小(约 2 × 参数量 FLOPs),但需要:

  1. 从显存加载全部模型权重(7B 模型 = 14GB FP16)
  2. 从显存加载全部 KV Cache
  3. 每次只做极少的矩阵乘法

类比:从仓库(显存)运一卡车货物(权重),只为了加工一个小零件(1 token)。大部分时间在运输,不在加工。

分离式 Prefill/Decode(Disaggregated P/D)

        ┌─────────────┐    KV Cache     ┌─────────────┐
用户 ──→│ Prefill 集群 │ ──────────────→ │ Decode 集群  │──→ 输出
        │ 高算力 GPU   │    (网络传输)    │ 大显存 GPU   │
        └─────────────┘                  └─────────────┘

优势:
- Prefill 用 H100(高算力),Decode 用大显存卡
- 两个阶段独立扩缩容
- 整体吞吐提升 40%+

四、PagedAttention 详解

传统 KV Cache 的问题

传统方案:为每个请求预分配连续的最大长度空间

请求 A(max_len=2048): [████████░░░░░░░░░░░░]  浪费 50%
请求 B(max_len=4096): [███░░░░░░░░░░░░░░░░░]  浪费 85%
请求 C(max_len=1024): [████████████████████]  浪费 0%

内部碎片 + 外部碎片 → 总利用率 < 40%

PagedAttention 方案

将 KV Cache 切成固定大小的 Block(如 16 token/block),通过 Block Table 映射:

物理 Block 池:
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ B0 │ B1 │ B2 │ B3 │ B4 │ B5 │ B6 │ B7 │  ← 按需分配
└────┴────┴────┴────┴────┴────┴────┴────┘
  ↓    ↓    ↓    ↓         ↓    ↓
 请求A  请求A  请求B  请求B   请求C  请求A (追加)

请求 A 的 Block Table: [B0, B1, B7]      ← 不连续物理块,逻辑连续
请求 B 的 Block Table: [B2, B3]
请求 C 的 Block Table: [B5]

Copy-on-Write 实现 Prefix Sharing

请求 A: system_prompt = "你是一个..."  → Block [B0, B1]
请求 B: system_prompt = "你是一个..."  → 共享 Block [B0, B1](只读,不复制)

只有当某个请求修改了共享 Block 中的内容时,才触发复制。
对于推理场景(KV Cache 只追加不修改),大量请求可共享相同 system prompt 的 KV Cache。

PagedAttention 的效果

指标 传统方案 PagedAttention
显存利用率 < 40% > 90%
内存碎片 严重 几乎无
Prefix Sharing 不支持 原生支持
吞吐量 基线 2-4x 提升

五、其他 KV Cache 优化技术

1. KV Cache 量化(KVQuant / KIVI)

不量化权重,只量化 KV Cache:
- K 和 V 以 FP16 计算,存储时压缩为 INT8/INT4
- 读取时解压回 FP16 参与 Attention 计算
- 关键:K 的分布通常不均匀(有 outlier 通道),需要 per-channel 量化
- 可节省 50-75% 的 KV Cache 显存

2. Sliding Window Attention(Mistral 风格)

只保留最近 W 个 token 的 KV Cache,丢弃更早的:

[丢弃] [丢弃] [保留 W=4096]

优点:KV Cache 大小恒定 O(W),不随序列增长
缺点:丢失长距离依赖(对某些任务不适用)

3. StreamingLLM(Attention Sink)

发现:前几个 token("attention sink")对模型稳定性至关重要

StreamingLLM 策略:
[保留前 4 个 token] + [滑动窗口最近 W 个 token]

即使超过窗口长度,保留开头几个 token 就能维持模型性能

4. Prefix Caching

多个请求共享相同的 system prompt 时:

请求 1 system: "你是一个专业的代码助手..." → 计算一次 KV
请求 2 system: "你是一个专业的代码助手..." → 复用!跳过计算
请求 3 system: "你是一个专业的代码助手..." → 复用!跳过计算

vLLM 和 SGLang 均原生支持。适合聊天应用(system prompt 固定)。

5. Multi-Query / Grouped-Query Attention

方案 KV 份数 KV Cache 质量
MHA h 最大 最好
GQA h/G 中等 极轻微损失
MQA 1 最小 有轻微损失

当前主流:GQA(LLaMA 2/3、Mistral、Qwen 均采用),在质量和效率间取得最佳平衡。


六、面试延伸

Q:KV Cache 在推理时占多少显存?如何估算?

公式:2 × n_layers × n_kv_heads × d_head × seq_len × batch_size × dtype_bytes

以 LLaMA-2 13B(40层, 40头, d_head=128, FP16, batch=8, seq_len=2048)为例: 2 × 40 × 40 × 128 × 2048 × 8 × 2 ≈ 13.4 GB

13B 模型权重约 26GB,KV Cache 占了 13.4GB,总计约 40GB,需要 A100(40GB/80GB)才能跑 batch=8。

Q:Prefill 和 Decode 的瓶颈不同,实际如何优化?

Prefill(compute-bound):增大 batch 提高 GPU 算力利用率;用 FP8 加速矩阵运算;Chunked Prefill 将长 prompt 分块与 decode 交错执行。

Decode(memory-bound):减少需要从显存读的数据量——GQA 减少 KV 头数、KV Cache INT8 量化压缩存储、PagedAttention 减少浪费。

最新方向:Disaggregated Prefill/Decode,将两个阶段部署在不同硬件上独立优化。

Q:GQA 为什么几乎不损失精度?

不同 Q 头关注不同的语义模式(句法、语义、位置等),但它们查询的 K/V 空间高度重叠。实验表明共享 K/V 对多数任务影响极小(< 0.5%),而 KV Cache 节省显著。GQA 在 MHA 和 MQA 之间取得了实用平衡。

Q:PagedAttention 的 Block Size 如何选择?

Block size 越大,Block Table 越小,管理开销低,但内存碎片增多。Block size 越小,内存利用率越高,但 Block Table 变大。vLLM 默认 block_size=16(tokens),经过实验验证是较好的平衡点。对于超长序列(128K+),可适当增大到 32-64。


补充:FlashAttention 与 KV Cache 的关系

FlashAttention 本身是 Attention 计算的 IO 优化(减少 HBM 读写),但它和 KV Cache 有协同效应:

  • FlashAttention:让每次 Attention 计算更快、更省显存
  • PagedAttention / KV Cache:让多次 Attention 计算复用中间结果

两者结合(vLLM 的做法)能获得最大的推理加速:计算快 + 少重复计算 + 显存高效管理。


原始论文

论文 链接
Efficient Memory Management for LLM Serving with PagedAttention (Kwon et al., SOSP 2023) arxiv.org/abs/2309.06180
GQA: Training Generalized Multi-Query Transformer Models (Ainslie et al., 2023) arxiv.org/abs/2305.13245
FlashAttention: Fast and Memory-Efficient Exact Attention (Dao et al., NeurIPS 2022) arxiv.org/abs/2205.14135
StreamingLLM: Efficient Streaming Language Models with Attention Sink (Xiao et al., 2023) arxiv.org/abs/2309.17453
KVQuant: Towards 10 Million Context Length (Hooper et al., 2024) arxiv.org/abs/2401.18079
Mooncake: KVCache-centric Disaggregated Architecture (Qin et al., 2024) arxiv.org/abs/2407.00079

延伸阅读与视频

平台 标题 说明
📺 B站 vLLM PagedAttention 论文精读 近2小时完整解读 PagedAttention 论文
📺 B站 怎么加快大模型推理?10分钟学懂vLLM内部原理 12.4万播放,最受欢迎的 vLLM+PagedAttention 讲解