Transformer 解剖:从 Attention 到推理系统
第 15 章 KV Cache 工程学:PagedAttention 解决了什么
第 15 章 KV Cache 工程学:PagedAttention 解决了什么
第 14 章我们看到 Decode 阶段被 HBM 带宽锁死、每生成一个 token 都要把整个模型权重读一遍。但还有一个问题没讲清楚:K 和 V 是从哪儿读的?每次 decode 要不要重算前面 N-1 个 token 的 K 和 V?
答案是:不要重算,缓存起来反复用——这就是 KV Cache。
KV Cache 是 LLM 推理的核心数据结构。没有它,自回归生成的总复杂度从 O(N²) 退化为 O(N³);有它,第 i 步 decode 只算 1 个新 token 的 K、V 和 1×i 的 attention。
这一章我们把 KV Cache 从概念到工程一路讲透:为什么需要、长什么样、为什么是显存杀手、PagedAttention 怎么救它、Prefix Caching 怎么省钱、MLA 怎么把它压到 1/10。
读完这章你能:
- 推导出 KV Cache 显存的精确公式(含 GQA、MLA 影响);
- 解释 PagedAttention 的核心抽象(page、block table、virtual memory analogy);
- 估算给定 GPU 配置下能跑多少并发用户;
- 解释 prefix caching 在 OpenAI / Anthropic API 上为什么能省 50% 输入成本;
- 对比 KV 量化、MLA、KV offloading 等不同压缩方案的取舍。
15.1 没有 KV Cache 会怎样
先看一个反事实:如果不缓存 KV,每次生成新 token 时全部重算,会怎样?
设当前已经生成了 i-1 个 token,准备生成第 i 个。完整的 forward 需要:
- 把所有 i 个 token 都过一遍 embedding
- 计算它们的 Q、K、V
- 算 i × i 的 attention
- FFN 处理 i 个 token
- 取最后一个位置的 logits 采样
计算量:每次 decode 都做一次完整的 prefill 工作量(O(i² · d))。生成 N 个 token 的总计算量:
——cubic in N。对一个生成 1000 token 的请求来说,没 KV Cache 时计算量是有 KV Cache 时的 1000 倍。完全不可接受。
KV Cache 的核心想法非常朴素:第 i 步 decode 时,前 i-1 个 token 的 K、V 早在前面已经算过——把它们存起来,每次只算第 i 个新 token 的 K、V,再做一次 1×i 的 attention。
flowchart LR
subgraph "无 KV Cache 第 i 步"
DM_X["输入: i 个 token"] --> DM_QKV["重算所有 K, V"]
DM_QKV --> DM_ATT["i x i attention"]
NOTE1["O(i²) FLOPs"]
end
subgraph "有 KV Cache 第 i 步"
DK_X["输入: 1 个新 token"] --> DK_QKV["只算新 K, V"]
DK_QKV --> DK_CONCAT[拼接到 KV Cache]
DK_CONCAT --> DK_ATT["1 x i attention"]
NOTE2["O(i) FLOPs"]
end
KV Cache 让 decode 第 i 步的计算量从 O(i²) 降到 O(i)——总复杂度 ,比无 KV Cache 时的 少 N 倍。这就是为什么所有 LLM 推理引擎都必须用 KV Cache。
15.2 KV Cache 长什么样
KV Cache 是按层组织的——每一层 Transformer Block 都有自己的 K Cache 和 V Cache。
形状(每层):
其中:
- :batch size
- :KV head 数(GQA 下小于 query head 数)
- :当前序列长度(持续累积)
- :每个 head 的维度
对一个 层模型,整体 KV Cache 是 个这样的 (K, V) 对。
单 token 单层 KV Cache 大小:
「2」是 K 和 V 各一份。bytes 是 dtype 的字节数(FP16/BF16 是 2,INT8 是 1,INT4 是 0.5)。
整模型 KV Cache 大小(每个 token):
举例:
| 模型 | L | h_kv | d_h | dtype | size/token |
|---|---|---|---|---|---|
| Llama-2 7B | 32 | 32 (MHA) | 128 | FP16 | 512 KiB |
| Llama-3 70B | 80 | 8 (GQA) | 128 | FP16 | 320 KiB |
| Llama-3 8B | 32 | 8 (GQA) | 128 | FP16 | 128 KiB |
| Mistral 7B v0.3 | 32 | 8 (GQA) | 128 | FP16 | 128 KiB |
| GPT-3 175B | 96 | 96 (MHA) | 128 | FP16 | 4.5 MiB |
| DeepSeek-V3 | 61 | 128 (MLA, kv_lora=512) | – | FP16 | 约 69 KiB |
可以看到 GQA 把 Llama-3 70B 的单 token KV Cache 从 MHA 时代的几 MB 压到 320 KiB——8× 压缩。MLA 进一步压到 70 KB——60× 压缩。
15.3 KV Cache 是显存杀手
把 size/token 乘以 context 长度:
举例 Llama-3 70B:
| Context | KV Cache(单序列) | 单 H100 80GB 能装多少并发用户 |
|---|---|---|
| 4K | 1.25 GiB | 个 |
| 8K | 2.5 GiB | 25 个 |
| 32K | 10 GiB | 6 个 |
| 128K | 40 GiB | 1 个 |
实际还要扣模型权重(70B BF16 = 140 GB,单卡放不下需要 TP 拆到 2 卡才能装下,KV Cache 跟着分摊)。再扣激活值、临时 buffer——可用于 KV Cache 的显存只有总显存的 30-40%。
KV Cache 决定了一个推理集群的并发能力上限——不是算力,是显存。
flowchart LR GPU[GPU 80 GB] --> W[模型权重 70 GB] GPU --> KV[KV Cache 池] GPU --> ACT[激活 / 中间 buffer] W --> R1[剩余 ~30 GB] R1 --> KV R1 --> ACT KV --> CAP[KV Cache 容量决定并发用户数]
要装下更多用户,要么减小单用户 KV Cache(GQA、MLA、量化),要么扩集群(更多卡 + 更多显存)。这是大模型推理工程最关键的一笔账。
15.4 第一个工程问题:碎片化
理论上每用户「自己的 KV Cache 占多少显存」是清楚的。但实际工程里有个微妙问题:每个用户不知道自己会生成多少 token。
假设你有一个 80 GB H100,模型权重占 60 GB,剩 20 GB 给 KV Cache。每用户 1 GB(4K context),理论上能装 20 个用户。但工程实现里:
做法 1:预分配最大长度
每个用户进来就分配它「可能用到的最大长度」(比如 4K)的 KV Cache。
- 优点:分配简单,每用户独立的连续内存
- 缺点:如果用户实际只生成了 100 token(远小于 4K),剩下的 3900 token 显存白浪费——大量碎片
实测平均利用率只有 20-40%,剩下 60-80% 显存浪费在「预分配但用不上」的空间里。
做法 2:动态扩展
每用户生成时按需扩展 KV Cache(比如每次按 256 token 加一段)。
- 优点:不浪费显存
- 缺点:扩展时需要 reallocation——把旧 KV 拷到新缓冲区,开销大、还可能 fragmented
更糟的是:不同用户的 KV Cache 在显存里散落——有些早结束、有些还在跑、有些刚开始——显存被切得稀碎,新用户进来时找不到一段连续的足够大的空间。
flowchart LR
subgraph "预分配最大长度<br/>50% 利用率"
A1[U1: 4K 分配<br/>但只用了 100 token]
A2[U2: 4K 分配<br/>用了 500 token]
A3[U3: 4K 分配<br/>用了 2K token]
A4[剩余: 不够装新用户]
end
subgraph "动态分配<br/>显存碎片化"
B1[U1: 100 token 已结束]
B2[U2: 500 token 还在跑]
B3[空洞 50 KB]
B4[U3: 2K 还在跑]
B5[空洞 200 KB]
B6[U4: 待加入<br/>找不到连续空间]
end
这是 LLM 推理服务化的真实痛点——显存的「物理碎片」让实际可用容量远低于纸面容量。
15.5 PagedAttention:从操作系统借的智慧
2023 年 Kwon 等人在 Efficient Memory Management for Large Language Model Serving with PagedAttention (SOSP 2023) 论文里提出了 PagedAttention——这是 vLLM 的核心创新,也是 2023 之后所有主流推理引擎的标配。
它的核心想法借鉴了操作系统的虚拟内存(virtual memory):
不要让一个用户的 KV Cache 占用一段连续显存。把 KV Cache 切成固定大小的小块(page),每个用户维护一个『虚拟到物理的页表』,需要时按页分配,不需要时按页释放。
具体地:
- 物理 KV Cache 被切成固定大小的
block(典型 16 token 一块)。 - 每个序列有自己的 block table——一个数组,第 i 项指向「序列的第 i 块对应的物理 block 地址」。
- 生成新 token 时:当前 block 没填满就直接 append;填满了就分配一个新的物理 block,更新 block table。
- 序列结束:释放它所有的物理 block 回到 free pool。
flowchart LR
subgraph "物理 KV Cache 池"
PB1[block 1]
PB2[block 2]
PB3[block 3 free]
PB4[block 4]
PB5[block 5 free]
PB6[block 6]
PB7[block 7]
PB8[block 8 free]
end
subgraph "用户 1 序列"
U1L["block table:<br/>[1, 4, 6]"]
end
subgraph "用户 2 序列"
U2L["block table:<br/>[2, 7]"]
end
U1L -.指向.-> PB1
U1L -.指向.-> PB4
U1L -.指向.-> PB6
U2L -.指向.-> PB2
U2L -.指向.-> PB7
这相当于操作系统中的 page table——CPU 看的是逻辑地址,page table 翻译到物理地址。LLM 推理引擎看的是「序列的虚拟 KV Cache」,PagedAttention 翻译到「散落在物理显存里的 block」。
PagedAttention 的好处
1. 内存利用率从 ~30% 提升到 ~95%
每个序列只占用「实际生成 token 数 / block size」上取整这么多 block,没有「预分配但没用上」的空间。vLLM 论文报告显存利用率从 PyTorch 朴素实现的 20-40% 提升到 96%。
2. 没有外部碎片
所有 block 大小相同,只要有空闲 block 就能给任何用户用。不会出现「显存有 1 GB 空闲但分散在 100 个小空洞里、谁都用不上」的情况。
3. 支持复杂的内存共享模式
不同序列的 block table 可以指向同一个物理 block——这是 prefix caching 的基础(下一节)。
PagedAttention 的代价
PagedAttention 不是免费的。它有一些工程代价:
1. 块大小是 trade-off
太小(比如 4 token)→ block table 太长、查表开销大、内存利用率高 太大(比如 64 token)→ block table 短、查表快、但回到「最后一块半空」的浪费
vLLM 默认 16 token,是综合考虑下的甜点。
2. Attention kernel 必须改写
朴素 attention 假设 K、V 在显存中连续。PagedAttention 下 K、V 散落在不同 block 里,attention 计算时要按 block table 跳着读。需要专门的 paged attention kernel——vLLM、SGLang、TensorRT-LLM 都有自己的实现。
3. 调度复杂
每生成一个 token 要查 block table;block 用完要分配新的;序列结束要释放——所有这些事比朴素的「预分配大块连续显存」复杂得多。
但这些代价远小于内存利用率提升带来的收益。今天 vLLM、SGLang、TensorRT-LLM 默认开启 PagedAttention 几乎是绝对默认。
15.6 Prefix Caching:跨用户的 KV 共享
PagedAttention 有一个非常有价值的副产品:当不同序列共享前缀时,它们的前缀 block 可以指向同一个物理 block。
flowchart LR
subgraph "用户 1 prompt 'You are a helpful assistant. Answer: 2+2='"
U1["block 1: 'You are'<br/>block 2: 'a helpful'<br/>block 3: 'assistant. Answer: 2+2='"]
end
subgraph "用户 2 prompt 'You are a helpful assistant. Answer: 3+3='"
U2["block 1: 'You are'<br/>block 2: 'a helpful'<br/>block 3: 'assistant. Answer: 3+3='"]
end
PB12["共享物理 block:<br/>'You are'"]
PB22["共享物理 block:<br/>'a helpful'"]
U1 -.前两块.-> PB12
U1 -.前两块.-> PB22
U2 -.前两块.-> PB12
U2 -.前两块.-> PB22
如果两个用户的 prompt 前 32 token 一样(共享同一个 system prompt + 文档前缀),他们的前 2 个 block(16 token × 2 = 32 token)可以共享 K 和 V——省一半 KV Cache!
更重要的是 省 Prefill 计算。前缀的 KV 在第一个用户来时已经算过、缓存住了,第二个用户来时直接复用——Prefill 跳过共享前缀部分,只算 diverging 之后的 token。
这就是 Prefix Caching(前缀缓存),是今天 LLM API 的关键优化。
实际场景:
- System prompt:同一个应用所有请求的 system prompt(描述 AI 角色、规则)一致——通常 500-2000 token,每次请求都能命中。
- Few-shot examples:在 prompt 里给的例子是固定的——可以复用。
- 长文档 QA:用户对同一份 100 页文档问不同问题——文档部分可复用。
- Agent 多轮:Agent 多步对同一个上下文做不同操作——历史可复用。
OpenAI 在 2024 年正式推出 Prompt Caching API,对命中前缀的部分输入按 50% 价格收费——是实打实的省钱。Anthropic Claude API 也有类似机制。
技术上,prefix caching 需要:
- 基于哈希的 block 标识:每个 block 的内容哈希作为 key,让相同内容的 block 自动匹配
- LRU 淘汰策略:缓存满时淘汰最久不用的 prefix
- 持久化层(可选):把热门 prefix 写到磁盘 / 共享缓存,跨节点共享
vLLM、SGLang 都内置 prefix caching。
15.7 主流 KV Cache 压缩方案
KV Cache 是显存杀手——即使有了 PagedAttention 和 Prefix Caching,要在长上下文 + 高并发场景下放下足够的 KV,还需要把单 token 的 KV Cache 进一步压缩。主流方案:
1. GQA / MQA(架构层)
第 3.6 节已经讲过——把 KV head 数从 query head 数减少 8× 或更多。今天主流模型(Llama-3、Mistral、Qwen)都用 GQA。
2. MLA(Multi-Head Latent Attention,DeepSeek-V2/V3)
更激进的方案:把 K、V 通过一个低秩压缩投影到低维 latent 空间,KV Cache 只存这个 latent 向量:
其中 (DeepSeek-V3 用 ,相比 是 14× 压缩)。
推理时按需从 解压出 K 和 V:
这样 KV Cache 只存一个 维向量(每 token、每层),不存所有 head 的 K 和 V。
flowchart LR X["输入 x_t"] --> CTX["低秩压缩<br/>c_KV (d_c=512)"] CTX --> KV["KV Cache 只存 c_KV<br/>每 token 1 KB"] KV -.推理时按需解压.-> K1["k_t^(1), k_t^(2), ..."] KV -.推理时按需解压.-> V1["v_t^(1), v_t^(2), ..."]
DeepSeek-V3 的 KV Cache 比 GQA 还小 5 倍。但代价是需要额外的解压计算——MLA 比 GQA / MHA 计算量略大。这是「显存省 → 算力略增」的 trade-off。
不过推理时算力本来就用不满(memory-bound),花一点算力换大量显存是非常合算的——这是 MLA 在 DeepSeek-V3 这种长上下文 / 高并发场景下的关键优势。
3. KV Cache 量化
把 KV Cache 用低精度存储——FP16 → INT8 / INT4。
FP16 → INT8:每元素从 2 byte 降到 1 byte,显存减半。质量几乎无损(< 0.5% 精度损失)。
FP16 → INT4:每元素从 2 byte 降到 0.5 byte,显存减 4 倍。质量略有损失(1-2% 精度),但在长上下文下损失可接受。
具体方法:
- KIVI(Liu et al., 2024):把 K 沿 channel、V 沿 token 分别量化,2-bit 量化下精度损失很小
- SmoothQuant for KV:用平滑因子让量化误差均匀
这些方案都把 KV Cache 显存进一步压 2-4 倍。代价是额外的 dequant 算子,但因为 KV 的访问已经是 memory-bound 主力,量化后访存量减少更多——实际推理还能加速。
4. KV Cache Offloading
把暂时不需要的 KV Cache(比如序列前部、不太可能马上被访问到的)从 HBM 卸载到 CPU 内存或 NVMe SSD,需要时再加载。
适合场景:超长上下文(128K+)、对延迟不敏感的离线推理。
代价:HBM ↔ CPU 内存的 PCIe 带宽(~64 GB/s)远低于 HBM 带宽(3.35 TB/s),加载时延迟巨大。
综合对比
| 方法 | 显存压缩比 | 计算开销 | 质量损失 | 适用 |
|---|---|---|---|---|
| GQA | 4-8× | 几乎无 | 几乎无 | 主流模型默认 |
| MLA | 5-10× | 略增 | 几乎无 | DeepSeek 系 |
| INT8 量化 | 2× | 略增 | < 0.5% | 通用 |
| INT4 量化 | 4× | 略增 | 1-2% | 长上下文 |
| Offload | 5-10× | 大(PCIe) | 无 | 离线 / 超长 |
实际工业部署常常是叠加使用:GQA + INT8 KV 量化 + PagedAttention + Prefix Caching——四重组合让一张 H100 能服务的并发数翻 50-100 倍。
15.8 一个完整的并发能力估算
把所有内容串起来。我们估算一下:用 8×H100 80GB 服务 Llama-3 70B(FP16),能撑多少并发用户?
Step 1:模型权重占用
70B × 2 byte = 140 GB。8 卡 TP 平均每卡 17.5 GB。
Step 2:剩余显存
每卡 80 - 17.5 ≈ 62 GB,扣激活、buffer、临时 workspace ≈ 50 GB 给 KV Cache。8 卡共 400 GB。
Step 3:单 token 单层 KV Cache
Llama-3 70B GQA: , , , FP16:
Step 4:每用户 KV Cache(假设平均 8K context)
Step 5:并发用户数
个并发
但这是「平均每用户 8K context」的假设。如果 context 更长(比如 32K 平均),并发数减到 ~40;如果更短(4K 平均),能撑到 ~320。
Step 6:开启 INT8 KV 量化后
每 token 减半到 160 KiB,每用户 8K 只占 1.25 GiB——并发能到 ~320。
Step 7:开启 prefix caching 命中 50%
如果 50% 的用户共享同一个前缀(system prompt):
- 前缀 KV Cache 只占一份,省下重复存储
- 推理省下重复 Prefill 计算
总并发能力大概再翻 1.5-2 倍——综合到 ~500。
Step 8:实际工业(DeepSeek-V3 + MLA + INT8 + prefix caching)
671B 模型 + MLA + INT8 KV + prefix caching 综合下来,DeepSeek 公开报告 H100 集群能跑到几千 QPS——这个量级已经接近 OpenAI / Anthropic 的产品水平。
flowchart TB N1["朴素 PyTorch<br/>10-20 个并发"] --> N2["+ PagedAttention<br/>30-50 个"] N2 --> N3["+ GQA 架构<br/>~150 个"] N3 --> N4["+ INT8 KV<br/>~300 个"] N4 --> N5["+ Prefix Caching<br/>~500 个"] N5 --> N6["+ MLA + FP8<br/>~1000+ 个"]
15.9 KV Cache 的工程实战 tips
最后一些非常实操的经验:
Tip 1:监控 KV Cache 占用率
服务化推理引擎要实时监控 kv_cache_used / kv_cache_total。如果 > 80% 就要触发拒绝新请求或降级(比如缩短 context)——避免 OOM。
Tip 2:设置合理的 max_model_len
vLLM / SGLang 启动时可配置 max_model_len——单序列允许的最大 token 数。设太大浪费 KV Cache 池的预留;设太小用户长 prompt 失败。需要根据实际请求分布定。
Tip 3:Prompt 长度直接决定 throughput
如果你的应用平均 prompt 是 32K(长文档场景)vs 平均 1K(chat 场景),同样硬件吞吐能差 30 倍。设计 RAG / Agent 时尽量把不必要的上下文拿掉。
Tip 4:长尾用户拖累集群
一个用户生成 4K token 占用 KV Cache 4K × 320KB = 1.3GB 持续 30 秒——同期可能阻塞其他几十个短用户。如果服务质量敏感,需要做 timeout / preemption。
Tip 5:跨用户复用 prefix 设计 system prompt
把 system prompt、文档摘要、few-shot 这些「用户间共通」的内容写在一起、放在 prompt 最前——这样 prefix caching 命中率最高。同一个应用所有请求共享相同的 system prompt 是最理想的。
本章小结
- KV Cache 把自回归生成的复杂度从 O(N³) 降到 O(N²)——是 LLM 推理可行的核心数据结构。
- 单 token KV Cache = L × 2 × h_kv × d_h × bytes——这个公式决定了一切显存账。
- KV Cache 是显存杀手——长上下文 + 多并发让它远超模型权重大小。
- 碎片化是工程痛点:预分配浪费、动态分配碎片化。
- PagedAttention 借鉴 OS 虚拟内存——把 KV Cache 切成 block + 用 block table,显存利用率从 30% 提升到 95%。
- Prefix Caching 复用相同前缀的 KV——OpenAI / Anthropic API 50% 折扣的来源。
- GQA / MLA 在架构层减小 KV head 数——Llama-3 用 GQA 减 8 倍,DeepSeek-V3 用 MLA 再减 5-10 倍。
- 量化(INT8/INT4)和 offloading 是显存的进一步压缩手段。
- 综合优化能让一台 H100 集群从「20 并发」提升到「数百到数千并发」——这就是过去 2 年 LLM 推理工程的核心成就。
下一章我们看推理优化的另一条主线——量化。从 FP16 到 INT8、INT4、FP8 的演化,每一刀都在「质量损失 vs 显存计算节省」之间找最优点。
延伸阅读
- Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention, SOSP 2023——vLLM / PagedAttention 论文。
- Liu et al., KIVI: A Tuning-Free Asymmetric 2bit Quantization for KV Cache, ICML 2024——KV 量化代表作。
- DeepSeek-AI, DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model, 2024——MLA 引入。
- Sheng et al., FlexGen: High-Throughput Generative Inference of Large Language Models with a Single GPU, ICML 2023——KV offloading 代表作。
- vLLM 文档: https://docs.vllm.ai/
- SGLang 文档: https://github.com/sgl-project/sglang
- OpenAI Prompt Caching 公告: https://openai.com/index/api-prompt-caching/
- Anthropic Prompt Caching 文档: https://docs.anthropic.com/claude/docs/prompt-caching