KV Cache 原理与 PagedAttention
KV Cache 原理与 PagedAttention
为什么 70B 推理权重只占 140GB(FP16),但并发一上来显存照样爆?答案是 KV Cache——它随请求数、序列长度动态增长,是推理显存真正的”隐形大户”。本文讲清它怎么来的、为什么传统分配会浪费、PagedAttention 怎么解决。
一、KV Cache 从哪来
Decoder-only 自回归生成:第 t 个 token 的注意力要”看到”前面所有 token(1..t-1)。但注意力的 K/V 投影只依赖各 token 自身,所以前面 token 算过的 K、V 可以缓存复用,不必每生成一个新 token 都重算全部历史。
生成第 t 个 token 时:
需要:K_1..K_t, V_1..V_t ← 历史 K/V
其中 K_1..K_{t-1}, V_1..V_{t-1} 已在上一轮算过,缓存即可
只需新算 K_t, V_t,拼到缓存末尾
显存占用公式
单层 KV Cache 大小 = 2 × (num_kv_heads) × seq_len × head_dim × dtype_bytes
总 KV Cache = 上述 × num_layers
以 LLaMA-70B(80 层, GQA: 8 KV heads, head_dim=128, BF16=2B)为例:
单层 = 2 × 8 × seq_len × 128 × 2 = 4096 × seq_len bytes
总 = 4096 × seq_len × 80 = 327680 × seq_len bytes
seq_len=4096 → ≈ 1.34 GB / 每请求
seq_len=32K → ≈ 10.7 GB / 每请求
关键洞察:KV Cache 与 序列长度成正比、与请求数成正比。100 个并发、平均 4K 上下文 → 仅 KV 就 ~134 GB,已经超过很多卡的权重占用。
GQA / MQA 为什么省 KV Cache
- MHA(每注意力头独立 KV):KV heads = num_heads(最费)
- GQA(LLaMA-2/3、Qwen):KV heads 降到 8/4 个,KV Cache 同比缩小 → 直接降低推理显存与 TPOT
- MQA(PaLM):KV heads=1,最省但质量略降
二、传统连续分配的碎片问题
朴素实现给每个请求预留一块连续、且按最大长度预留的 KV 显存:
请求 A 预留 2K tokens 空间(实际只用 1.3K) → 浪费 0.7K
请求 B 预留 4K 空间(实际只用 0.5K) → 浪费 3.5K
两个问题:
- 内部碎片:预留上限远高于实际使用
- 外部碎片:请求结束释放的连续块,难以拼给新请求(长度不匹配)
- 无法跨请求共享:多轮对话里相同的 system prompt,每个请求各算各的 KV
结果:GPU 显存明明还有空,却因碎片和预留策略报 OOM,并发数被人为压低。
三、PagedAttention:把 KV Cache 像虚拟内存一样分页
vLLM 的核心创新(2023)。思想直接借用 OS 的分页 + 页表:
传统:每个请求一块连续 KV 显存
PagedAttention:每个请求 = 一组固定大小的逻辑块(block),
物理上分散存放,用「块表(block table)」映射
工作方式
1. KV Cache 切成固定大小的 block(如每块 16 个 token 的 K/V)
2. 每个请求的 block table 记录:逻辑块 i → 物理块 p
3. 生成时按需分配新 block,用完释放,物理显存像内存池一样复用
4. 注意力计算时,按 block table 把分散的物理块 gather 起来
收益
| 维度 | 连续分配 | PagedAttention |
|---|---|---|
| 内部碎片 | 高(按 max 预留) | 仅最后一块不足部分(<1 block) |
| 外部碎片 | 高 | 近零(块可任意复用) |
| 显存利用率 | 20-40% | 90%+(接近 gpu-memory-utilization 上限) |
| 并发数 | 低 | 提升 2-4× |
| 跨请求共享 | ❌ | ✅(copy-on-write 共享相同前缀块) |
最后一个收益最关键:相同 prompt 前缀的多个请求,物理上共享同一批 KV 块,直到某个请求生成的分支不同才 copy-on-write 分裂。这正是 SGLang RadixAttention 把”共享”做到极致的底层原理(见 LLM 推理引擎对比与选型)。
四、KV Cache 量化(进一步压显存/带宽)
KV Cache 不仅占显存,decode 时每个 token 都要读全部历史 KV(访存密集),所以量化 KV 一举两得:
| 方案 | 做法 | 效果 |
|---|---|---|
| FP8 KV Cache | KV 以 FP8 存,计算前反量化 | 显存/带宽减半,精度损失极小 |
| INT8 KV Cache | per-tensor/per-channel 量化 | 显存减半,常用且稳 |
| INT4 KV Cache | 更激进,需校准 | 显存 1/4,长上下文场景收益大 |
| KV Cache 稀疏 | 只保留重要 token 的 KV | 研究阶段,生产少用 |
vLLM 启用示例:
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2.5-7B-Instruct \
--kv-cache-dtype fp8_e5m2 \
--gpu-memory-utilization 0.95
五、Prefix Cache:跨请求复用长 prompt
除了分页,还可以显式缓存长 prompt 的 KV,避免重复 prefill:
场景:100 个请求都带同一份 8K 的 system prompt + 文档
朴素:每个请求都 prefill 这 8K → 100 倍浪费
Prefix Cache:第一次算完 8K 的 KV 存下来,其余 99 个直接复用
- vLLM:
--enable-prefix-caching(自动按 prompt hash 命中) - SGLang:RadixAttention(树状缓存,更细粒度)
- 收益:长上下文/多轮 Agent/RAG 场景,TTFT 大幅下降、GPU 算力省下来给 decode
六、监控 KV Cache 使用
# vLLM /metrics 关键指标
vllm:gpu_cache_usage_sys # KV Cache 显存占用比例(>0.9 要警惕 OOM)
vllm:num_preemptions # 因 KV 不足被抢占的请求数(越高越说明显存紧张)
当
gpu_cache_usage_sys长期 > 0.9 且num_preemptions> 0,说明并发/上下文已超容量——要么降--max-model-len、要么开 KV 量化、要么加卡(张量并行)。
关联知识
- LLM 推理服务化知识总览 — KV Cache 是推理显存动态部分的核心
- LLM 推理引擎对比与选型 — PagedAttention vs RadixAttention 的引擎实现差异
- 高并发调度与吞吐优化 — Continuous Batching 与 PagedAttention 配合拉满利用率
- ../llm-training/大模型架构对比 — GQA/MQA 直接降低 KV Cache 大小
- ../llm-training/显存计算详解 — 推理显存 = 权重 + KV Cache
参考资源
学习时间
| 阶段 | 时间 | 备注 |
|---|---|---|
| 内容创建 | 2026-07-20 | KV Cache 公式、GQA 影响、碎片问题、PagedAttention 分页思想、KV 量化、Prefix Cache |
状态标记
📖 已掌握 — KV 显存公式、传统碎片根因、PagedAttention 分页+共享、KV 量化、Prefix Cache 📝 待补充 — FlashAttention 与 PagedAttention 的协同、KV 卸载到 CPU/NVMe