📑 本页目录(点开跳转)
16 · KV Cache
⏱ 52 分钟 | ⭐⭐ 推理系统里最贵的那块显存
🎯 一句话
不缓存,每生成一个 token 都要把前面所有 token 重算一遍(O(n²) 的浪费)。 缓存了,它就吃掉你一半的显存。 整个现代推理引擎的设计,本质上都在解决这个矛盾。
🔁 一、为什么需要它
自回归生成:"我" → "我爱" → "我爱吃" → "我爱吃面"
❌ 不缓存:每一步都把整个前缀重新过一遍模型
生成第 n 个 token 要处理 n 个 token
→ 总计算量 O(n²) 💀
✅ 缓存:
注意到每一层的 K、V 只依赖【已经生成的 token】,
而它们【不会再变】⭐
→ 存下来,新 token 只算自己那一份,拼上去即可
→ 总计算量 O(n)
第 3 步生成时:
Q: 只有当前 token 的 [q₃]
K: 缓存的 + 新的 [k₁ k₂ | k₃] ← 前两个是缓存的
V: 缓存的 + 新的 [v₁ v₂ | v₃]
attention(q₃, [k₁k₂k₃], [v₁v₂v₃])
🔑 为什么只缓存 K 和 V,不缓存 Q: 因为 Q 只用一次(当前 token 查询),而 K、V 会被后面所有 token 反复用到。
💸 二、它有多大
$$\text{KV Cache} = 2 \times L \times n \times d_{kv} \times \text{batch} \times \text{字节数}$$
(2 = K 和 V,L = 层数,n = 序列长度,$d_{kv}$ = KV 的总维度)
Llama-2 7B(32 层,hidden 4096,32 头 MHA):
每个 token 每层:2 × 4096 × 2 字节 = 16 KB
32 层 = 512 KB / token ⭐
→ seq=2048: 1 GB / 请求
→ seq=2048,batch=32: 32 GB 💀 比模型本身(14GB)还大!
→ seq=32768,batch=1: 16 GB
| 模型 | 每 token 的 KV | 32K 上下文单请求 |
|---|---|---|
| Llama-2 7B(MHA) | 512 KB | 16 GB |
| Llama-2 70B(GQA-8) | 320 KB | 10 GB |
| Mistral 7B(GQA-8) | 128 KB ⭐ | 4 GB |
🔑 注意 GQA 那两行 —— 这是下面要讲的关键优化。
⭐ 三、GQA / MQA:直接砍掉 KV 的大小
| 方案 | K、V 怎么分 | Q | K | V | 结果 |
|---|---|---|---|---|---|
| MHA(多头注意力) | 每个头有自己的 K、V | 32 头 | 32 头 | 32 头 | ← KV Cache 最大 |
| MQA(多查询) | 所有 Q 头【共享一组】K、V | 32 头 | 1 头 | 1 头 | ← KV 缩小 32 倍!但质量下降 |
| GQA(分组查询)⭐ | 分成 g 组,每组共享 | 32 头 | 8 头 | 8 头 | ← 缩小 4 倍,质量几乎无损 |
| 方案 | KV 大小 | 质量 | 使用者 |
|---|---|---|---|
| MHA | 1× | 基准 | 早期模型 |
| GQA-8 ⭐ | 1/4 ~ 1/8 | 几乎无损 | Llama-2 70B、Mistral、大多数现代模型 |
| MQA | 1/32 | 有下降 | Falcon、PaLM |
| MLA(DeepSeek) | ~1/10 甚至更小 ⭐ | 好 | 把 KV 压到低秩隐空间,下一节详讲 |
💡 GQA 是这几年最实用的架构改动之一: 几乎不掉质量,KV Cache 直接砍到 1/4 到 1/8 —— 而 KV Cache 是推理吞吐的瓶颈,所以这等于吞吐翻好几倍。
⚠️ 注意这是模型架构层面的选择,推理时改不了 —— 选模型时就要看。
🧬 四、MLA:换一条思路压 KV
MHA → MQA → GQA 这条线,做的其实是同一件事:少存几个头。 KV 头数从 32 砍到 8,再砍到 1 —— 这条路有个明显的天花板:砍到 1 就没得砍了,而且质量已经开始掉。
MLA(Multi-head Latent Attention,DeepSeek-V2/V3 用的方案)走的是另一条路: 头数一个不减,但换一种东西存 —— 存一个压缩过的低秩向量,用的时候再还原。
① 压缩:存潜向量,不存 K 和 V
MHA / GQA 缓存的是:k₁…k_h, v₁…v_h (一堆按头切好的向量)
MLA 缓存的是: c = W_DKV · h (一个短向量,长度 d_c)
要用的时候再展开: k = W_UK · c v = W_UV · c
W_DKV 是下投影(把 hidden 压到 d_c 维),W_UK / W_UV 是上投影(还原回每个头的 K、V)。
DeepSeek-V2 / V3 的实际配置是 128 个头、head_dim 128、d_c = 512。
同样这套注意力配置,如果用 MHA 存,每 token 每层要存 2 × 128 × 128 = 32768 个数;MLA 只存 512 个(外加下面要讲的 64 个)。
🔑 这不是"无损压缩",但也不是"硬砍": 低秩投影当然会丢信息。关键是
W_DKV/W_UK/W_UV是和整个模型一起训出来的 —— 模型自己学会了"该往这 512 维里塞什么"。⭐ 和 GQA 的本质差别:GQA 的压缩是结构性的(强行让 4 个 Q 头共用一组 KV, 这几个头的差异是真的没了);MLA 的压缩是学出来的(每个头还是独立的,只是换了个存法)。 这就是为什么 MLA 能同时做到"存得更少"和"质量不比 MHA 差"。
② 吸收:为什么"展开"不用额外花钱
第一反应会担心:每一步都要把 c 展开成 128 个头的 K 和 V,那不是省了显存又搭上算力?
关键在于 attention 分数可以这样重写:
$$q^{T}k = q^{T}(W_{UK}\,c) = (W_{UK}^{T}q)^{T}c$$
也就是说,上投影矩阵可以挪到 q 那一边去,和 q 自己的投影矩阵合并成一个矩阵 —— 这一步叫吸收(absorption)。合并之后:
⭐ 推理时【根本不用把 K 展开】
直接拿"吸收过的 q"和缓存里的 c 做内积
同理 W_UV 可以吸收进输出投影 W_O
→ Decode 阶段等于【直接在潜空间里算注意力】
⚠️ 代价不是零:吸收之后的矩阵更大,每一步的乘法量比 GQA 多。 但回到第 15 章那条结论 —— Decode 是带宽瓶颈,算力有 95% 闲着。
⭐ MLA 做的正是"用闲置的算力,换紧张的带宽" —— 和第 8 章的 FlashAttention(多算 10-20% 换少搬 97% 的数据)、 第 19 章的投机解码(多算几个 token 换少读几遍权重)是完全同一类交易。
⚠️ 反过来说,Prefill 阶段算力是紧张的,这时候老老实实展开成 K/V 再算反而更划算。 所以真实实现会在 Prefill 和 Decode 走不同的算法路径。
③ RoPE 解耦:两个优化打架的时候
现在有个麻烦。RoPE 是按位置给 k 转一个角度:位置 t 的 k 要乘上一个旋转矩阵 $R_t$。
如果 $k_t = W_{UK}c_t$,加了 RoPE 就变成 $R_t W_{UK} c_t$。再想做吸收:
$$q_s^{T}\,R_{t}\,W_{UK}\,c_t$$
⭐ 中间夹着一个"随位置变化的" $R_t$,W_UK 挪不过去了。
更准确地说:RoPE 是相对位置编码,落到分数上是 $R_{t-s}$ —— 它同时依赖查询位置和被查位置,所以没法预先和任何一边的投影矩阵合并。
🔗 这一句是整节的支点,如果读着别扭,先去 02b · 现代 LLM 架构的四处改动 补 RoPE 那一节。 ⭐ 关键不是「RoPE 是旋转」这个比喻,是那一章讲清楚的两件事: ① 它加在每一层的 Q/K 上(不像正弦编码只加在输入端,所以躲不开); ② 它的效果落到注意力分数上是纯相对的 $R_{t-s}$。 这两条合起来就是上面「$W_{UK}$ 挪不过去」的全部原因——不是 DeepSeek 不想吸收,是数学上挪不动。
结果是二选一,而且两个都不能接受:
| 选择 | 后果 |
|---|---|
| 放弃吸收,每步展开全部 K | 显存和算力全吃回去,等于白压缩 💀 |
| 给每个位置偏移各存一份合并矩阵 | 数量随上下文长度爆炸 💀 |
DeepSeek 的解法叫「解耦式 RoPE」(decoupled RoPE):把 k 拆成两半,各管各的。
| 部分 | 怎么来 | 带 RoPE 吗 | 缓存什么 |
|---|---|---|---|
| 内容部分 k^C | 从潜向量 c 展开 | ❌ 不带 | 只存 c(512 维) |
| 位置部分 k^R | 从 hidden 直接投影出一个小向量 | ✅ 带 | 单独存这 64 维 ⭐ |
注意力分数变成两项相加:分数 = q^C·k^C + q^R·k^R。
- 内容那一项走潜空间,吸收照常做 ✅
- 位置那一项维度只有 64,而且所有头共享同一份(这一点和 MQA 一样),存起来几乎不占地方 ✅
⭐ 这一段值得单独记住,它超出 MLA 本身: 当两个优化互相打架时,工程上的解法常常不是"选一个", 而是把冲突的那一小部分单独拆出来,让它按老办法走。 RoPE 解耦是这个套路最干净的一个例子 —— 64 维的额外开销,买回了整个压缩方案。
④ 到底省了多少
按 DeepSeek-V3 的注意力配置(128 头、head_dim 128、BF16),每 token 每层:
| 方案 | 缓存的数字个数 | 相对 MHA | 每 token 每层字节 |
|---|---|---|---|
| MHA | 2 × 128 × 128 = 32768 | 1× | 64 KB |
| GQA-8 | 2 × 8 × 128 = 2048 | 1/16 | 4 KB |
| MQA | 2 × 1 × 128 = 256 | 1/128 | 0.5 KB |
| MLA ⭐ | c 512 + k^R 64 = 576 | ≈ 1/57 | 1.1 KB ⭐ |
⭐ 换个说法(DeepSeek-V2 论文里的说法): MLA 的缓存量相当于"GQA 只分 2.25 组" —— 比 GQA-8 还小 3.5 倍, 而论文报告的质量不低于 MHA。 这是"既要马儿跑又要马儿不吃草"里少见的真做到了的一次。
⚠️ 三条要注意的:
- 和 GQA 一样,MLA 是架构层面的选择,推理时改不了 —— 选模型时就要看。
- 💥 它需要推理引擎专门支持(要实现吸收路径和解耦 RoPE 的 kernel)。 不支持的引擎会退化成"先展开成 MHA 再算" —— 显存优势直接归零。 这是一个真实的坑:同一个模型在支不支持 MLA 的两个引擎上,能开的并发能差好几倍, 而且它不会报错,只会表现为"这模型怎么这么吃显存"。
- 上面那些倍数是 DeepSeek 的配置。换个模型比例就变 —— 它头特别多(128 个),所以 MHA 那一列显得格外夸张。
🗑️ 五、朴素管理方式的浪费
❌ 传统做法:为每个请求预分配【最大长度】的连续显存
请求 A(实际用 100 token,max 2048):
[████░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 用了 5%,浪费 95%
请求 B(实际用 1500):
[████████████████████████░░░░░░░░] 浪费 27%
⭐ 实测:传统方式的 KV Cache 显存利用率只有 20-40% 💀
三种浪费:
| 浪费类型 | 原因 |
|---|---|
| 内部碎片 | 预分配 max_len 但实际用不到 |
| 外部碎片 | 请求结束后留下不连续的空洞 |
| 预留浪费 | 为"可能还要生成"预留的空间 |
🔑 这就是 PagedAttention 要解决的问题 —— 它把操作系统的虚拟内存分页思想搬到了 KV Cache 上, 把利用率从 20-40% 提到 90%+。 🔗 第 17 章详讲。
🧰 六、四个能立刻用的优化
① KV Cache 量化 ⭐
FP16 → INT8:显存减半,能开 2 倍 batch
FP16 → FP8/INT4:减到 1/4(质量要测)
⚠️ 经验:K 比 V 对量化更敏感(K 影响 attention 分布)
→ 常见做法:K 用 INT8,V 用 INT4
# vLLM
llm = LLM(model="...", kv_cache_dtype="fp8") # ⭐ 一行
② 前缀缓存(Prefix Caching)⭐⭐
场景:所有请求都带同一个 2000 token 的系统提示词
❌ 每个请求都重新 Prefill 这 2000 token
✅ 缓存它的 KV,所有请求共享 → Prefill 成本接近零
⭐ 在 Agent、RAG、多轮对话场景收益巨大
(多轮对话里,前面所有轮次都是可复用的前缀)
llm = LLM(model="...", enable_prefix_caching=True) # ⭐
③ 滑动窗口 / 注意力汇聚
只保留最近 W 个 token 的 KV → 显存从 O(n) 降到 O(W)
⚠️ 会丢失长距离信息
⭐ StreamingLLM 的发现:保留【最开始的几个 token】(attention sink)
+ 最近的窗口,效果比纯滑动窗口好很多
④ 卸载到 CPU / 磁盘
把不活跃请求的 KV Cache 换出到 CPU 内存
⚠️ 走 PCIe(第 3 章:慢约 40 倍)
⭐ 适合"多轮对话中用户正在思考"这种长间隔场景
🧮 七、动手:算你能开多大 batch
def max_batch(gpu_gb, model_gb, layers, kv_heads, head_dim,
seq_len, dtype_bytes=2, overhead=0.1):
"""能同时服务多少个请求"""
avail = gpu_gb * (1 - overhead) - model_gb
kv_per_token = 2 * layers * kv_heads * head_dim * dtype_bytes
kv_per_req = kv_per_token * seq_len / 1e9
return int(avail / kv_per_req), kv_per_req
# Llama-2 7B(GQA 前,32 个 KV 头),A100 80G,seq=2048
n, per = max_batch(80, 14, 32, 32, 128, 2048)
print(f"MHA:每请求 {per:.2f} GB,最多 {n} 并发")
# MHA:每请求 1.07 GB,最多 60 并发
# 同样的模型改成 GQA-8
n, per = max_batch(80, 14, 32, 8, 128, 2048)
print(f"GQA-8:每请求 {per:.2f} GB,最多 {n} 并发") # ⭐
# GQA-8:每请求 0.27 GB,最多 242 并发 ← 4 倍!
⭐ 这个脚本值得存下来:容量规划、选模型、定 SLO 都要用它。
🔗 和站内其他章的关系
| 相关的地方 | 这里的位置 |
|---|---|
| 第 15 章 批处理受限于 KV Cache | 本章是那个限制的细节 ⭐ |
| 第 15 章 Decode 是带宽瓶颈 | KV Cache 也要被读,加剧了带宽压力 |
| 第 17 章 PagedAttention | 解决碎片问题 |
| 第 18 章 量化 | KV Cache 量化 |
| 第 8 章 多算 10-20% 换少搬 97% | MLA 的吸收技巧是同一类交易:用闲置算力换紧张的带宽 ⭐ |
| 第 19 章 用闲置算力换延迟 | 和 MLA 一样,都建立在"Decode 算力有 95% 闲着"这个事实上 |
| 全景导论第 2 章 多头注意力 | GQA 改的就是它 |
| 《大模型全景导论》02b 现代 LLM 架构的四处改动 RoPE ⭐ | 本章「解耦式 RoPE」的前置。要看的不是「RoPE 是什么」,是那一章讲的RoPE 加在每一层的 Q/K 上、且落到分数上是相对的 $R_{t-s}$——⭐ 正因为它同时依赖查询位置和被查位置,才没法和任何一边的投影矩阵合并,MLA 的吸收才会被卡住。不知道这一条,第四节读起来就只是「DeepSeek 拆了个 64 维出来」,不知道为什么非拆不可 |
| 《大模型全景导论》03 Tokenizer 与上下文 RoPE 的长度外推 | 上一条的下游:RoPE 能外推到多长决定了上下文窗口能撑多大,而窗口撑大的代价就是本章的 KV Cache |
| 《大模型全景导论》03 Tokenizer 与上下文 上下文窗口是怎么撑大的 | 撑大窗口的代价就在本章:KV Cache 随上下文线性涨 ⭐ |
| 《智能体工程教程》11 检索与 RAG 长系统提示词 + 检索结果 | 前缀缓存收益最大的场景;那边的切分方式直接决定命中率 ⭐ |
✅ 检查点
- 为什么需要 KV Cache?不用它的计算量是多少?
- 为什么只缓存 K 和 V,不缓存 Q?
- KV Cache 的大小公式是什么?7B 模型 seq=2048、batch=32 是多少?
- MHA、GQA、MQA 的区别?GQA 为什么是现在的主流?
- GQA 是推理时能改的吗?
- MLA 和 MQA/GQA 走的是同一条路吗?它缓存的是什么?
- MLA 的"吸收"是什么意思?为什么展开 K、V 不用额外花钱?它拿什么换了什么?
- 为什么压缩之后的 K 不能直接加 RoPE?DeepSeek 怎么绕过去的?
- 传统 KV Cache 管理的利用率是多少?三种浪费分别是什么?
- KV Cache 量化时,K 和 V 哪个更敏感?
- 前缀缓存在什么场景收益最大?
- StreamingLLM 的发现是什么?
👀 答案
- 因为不缓存的话每生成一个 token 都要把整个前缀重新过一遍模型,总计算量 O(n²)。缓存后是 O(n)。
- 因为 Q 只用一次(当前 token 查询),而 K、V 会被后面所有 token 反复用到。
- 2 × 层数 × 序列长度 × KV维度 × batch × 字节数。7B/seq2048/batch32 ≈ 32 GB,比模型本身(14GB)还大。
- MHA 每个头有自己的 K/V;MQA 所有 Q 头共享一组 K/V(缩小 32 倍但质量下降);GQA 分成 g 组,每组共享(缩小 4-8 倍,质量几乎无损)。GQA 是主流因为几乎不掉质量却把 KV Cache 砍到 1/4-1/8,而 KV Cache 是吞吐瓶颈,等于吞吐翻好几倍。
- 不能,这是模型架构层面的选择,推理时改不了——选模型时就要看。
- 不是同一条路。MQA/GQA 做的是"少存几个头"(砍 KV 头数),MLA 头数一个不减,缓存的是一个低秩潜向量 c(DeepSeek 配置里 512 维),用的时候再靠上投影矩阵还原成每个头的 K、V。差别的关键:GQA 的压缩是结构性的(几个头的差异真的没了),MLA 的压缩是和模型一起训出来的(每个头还是独立的,只是换了个存法) ——所以 MLA 能同时做到存得更少和质量不比 MHA 差。
- 因为 $q^{T}(W_{UK}c) = (W_{UK}^{T}q)^{T}c$,上投影矩阵可以挪到 q 那一边、和 q 的投影矩阵合并,于是推理时根本不用把 K 展开,直接在潜空间里算注意力(W_UV 同理吸收进输出投影)。代价是合并后的矩阵更大、每步乘法量比 GQA 多 ——它拿的是闲置的算力(Decode 算力 95% 闲着),换的是紧张的带宽,和 FlashAttention、投机解码是同一类交易。⚠️ 反过来 Prefill 阶段算力紧张,那时展开着算反而更划算。
- 因为 RoPE 是按位置给 k 乘一个旋转矩阵,而且落到分数上是相对位置 $R_{t-s}$ ——它同时依赖查询位置和被查位置,卡在 q 和 c 中间,上投影矩阵挪不过去,吸收就做不成了。DeepSeek 的解法是解耦式 RoPE:把 k 拆成内容部分(从潜向量展开、不带 RoPE)和位置部分 k^R(从 hidden 直接投影出 64 维、带 RoPE、所有头共享),分数 = 两项相加。⭐ 更一般的教训:两个优化打架时,把冲突的那一小部分单独拆出来让它按老办法走 ——64 维的开销买回了整个压缩方案。
- 只有 20-40%。三种浪费:内部碎片(预分配 max_len 用不到)、外部碎片(请求结束留下不连续空洞)、预留浪费。
- K 更敏感(它影响 attention 分布)。常见做法:K 用 INT8,V 用 INT4。
- 所有请求共享同一个长系统提示词的场景——Agent、RAG、多轮对话(前面所有轮次都是可复用前缀)。能把 Prefill 成本降到接近零。
- 保留最开始的几个 token(attention sink)+ 最近的窗口,效果比纯滑动窗口好很多。
🛑 可以停在这里
⚡ 走神救援
⭐⭐KV Cache 是推理系统里最贵的那块显存。不缓存则每生成一个 token 都要重算整个前缀(O(n²));缓存 K、V 是因为它们会被后面所有 token 反复用到,而 Q 只用一次。⭐大小 = 2 × 层数 × 序列长 × KV维度 × batch × 字节数;Llama-2 7B 每 token 512KB,seq2048×batch32 = 32GB,比模型本身 14GB 还大。⭐⭐GQA 是这几年最实用的架构改动:MHA 每头独立 K/V、MQA 全共享一组(小 32 倍但掉质量)、GQA 分 g 组共享(小 4-8 倍、质量几乎无损);KV Cache 是吞吐瓶颈,所以这等于吞吐翻好几倍;⚠️它是模型架构层面的选择,推理时改不了——选模型时就要看。🧬⭐MLA(DeepSeek-V2/V3)走的是完全不同的另一条路:MQA/GQA 都在「少存几个头」,MLA 头数一个不减,改成缓存一个低秩潜向量 c(512 维),用的时候靠上投影还原;⭐区别在于 GQA 的压缩是结构性的(几个头的差异真没了),MLA 的压缩是和模型一起训出来的(每个头还独立,只换了存法),所以它能同时做到存得更少和质量不比 MHA 差。"吸收"技巧:因为 qᵀ(W_UK·c) = (W_UKᵀq)ᵀ·c,上投影矩阵能挪到 q 那边合并进去 → 推理时根本不用展开 K,直接在潜空间里算注意力;⭐代价是每步乘法变多——它拿闲置算力换紧张带宽,和 FlashAttention、投机解码是同一类交易(⚠️Prefill 算力紧张时反而该展开着算)。⚠️⭐RoPE 解耦为什么必要:RoPE 是按位置给 k 转个角度、落到分数上是相对位置 R_{t−s},它卡在 q 和 c 中间,上投影矩阵挪不过去,吸收就废了 → 解法是把 k 拆两半:内容部分从潜向量展开且不带 RoPE,位置部分单独投影出 64 维、带 RoPE、所有头共享,分数 = 两项相加。⭐这一段的通用教训超出 MLA 本身:两个优化打架时,把冲突的那一小部分拆出来让它按老办法走——64 维的开销买回了整个压缩方案。数字:DeepSeek 配置下(128 头 × head_dim 128)MHA 每 token 每层 32768 个数、GQA-8 是 2048、MLA 只有 512+64=576(≈1/57,相当于"GQA 只分 2.25 组");💥⚠️它需要引擎专门支持,不支持的引擎会退化成展开成 MHA 再算,显存优势归零而且不报错。💀传统管理方式利用率只有 20-40%:三种浪费是内部碎片(预分配 max_len)、外部碎片(请求结束的空洞)、预留浪费 → 这就是 PagedAttention 要解决的(第17章,提到 90%+)。四个能立刻用的优化:①KV Cache 量化(FP16→INT8 减半,⚠️K 比 V 更敏感因为它影响 attention 分布,常见 K 用 INT8、V 用 INT4)②⭐⭐前缀缓存(同一个长系统提示词只算一次,Agent/RAG/多轮对话收益巨大,
enable_prefix_caching=True)③滑动窗口(⭐StreamingLLM 的发现:保留最开始几个 token(attention sink)+ 最近窗口,比纯滑动好很多)④卸载到 CPU(⚠️走 PCIe 慢 40 倍,适合长间隔场景)。⭐记住那个算并发数的脚本:A100 80G 上 7B 模型 seq2048,MHA 只能 60 并发,GQA-8 能 242 并发(4 倍)。
下一节 👉 17-连续批处理与PagedAttention.md ⭐⭐