如果你用过本地大模型,或者接触过 vLLM、TensorRT-LLM、llama.cpp 这类推理框架,基本都会看到一个词:
KV Cache。
很多人第一次看到它,会把它理解成一种普通缓存:
把模型算过的结果存起来,下次直接用。
这个理解方向没错,但还不够准确。
KV Cache 缓存的并不是“模型回答”,也不是完整的中间结果,而是 Transformer Attention 中已经计算过的:
Key 和 Value。
它之所以重要,是因为自回归语言模型每生成一个新 Token,都需要重新运行一轮 Transformer。
如果没有 KV Cache,大模型会不断重复计算过去已经算过的内容。
而 KV Cache 做的事情,就是:
不要重复计算过去 Token 的 Key 和 Value。
这看起来只是一个简单优化,但实际上,它几乎是现代大模型推理能够高效运行的基础。
先回到 Transformer
在上一篇 Transformer 中,我们已经提到 Self-Attention 里的三个核心变量:
- Query
- Key
- Value
通常写作:
$ Q,\ K,\ V $
假设某一层 Transformer 收到隐藏状态 \(X\),会通过三个不同的线性变换生成:
$ Q = XW_Q $
$ K = XW_K $
$ V = XW_V $
然后计算 Attention:
$ Attention(Q,K,V) = softmax\left(\frac{QK^T}{\sqrt{d_k}}\right)V $
这里最关键的一点是:
当前 Token 的 Query,需要和过去所有 Token 的 Key 进行比较。
然后根据 Attention 权重,从过去所有 Value 中读取信息。
也就是说,当模型生成一个新 Token 时,它仍然需要访问整个历史上下文。
这就是 KV Cache 出现的原因。
大语言模型是一个 Token 一个 Token 生成的
GPT、Llama、Qwen 这类 Decoder-only Transformer,本质上都是自回归模型。
假设输入:
今天天气
模型先预测:
很
现在上下文变成:
今天天气很
下一轮再预测:
好
然后继续:
今天天气很好
每生成一个 Token,模型都要再次运行 Transformer。
问题也随之出现。
假设已经生成了 1000 个 Token。
准备生成第 1001 个 Token 时,Attention 需要知道:
新 Token 应该关注前面哪些 Token?
因此它需要过去 1000 个 Token 的 Key 和 Value。
如果不做缓存,就意味着模型需要重新对前面 1000 个 Token 计算一次 K 和 V。
生成第 1002 个 Token 时,又要把前面 1001 个重新算一遍。
这显然非常浪费。
没有 KV Cache 会发生什么?
假设一句话只有几个 Token:
I love AI
第一次模型处理:
I
第二次:
I love
第三次:
I love AI
如果每次都完整重新计算,那么模型实际上重复处理了很多东西。
比如 I:
第一次算过一次。
第二次又算。
第三次还要再算。
当上下文只有几十个 Token 时,这种浪费可能还不明显。
但如果上下文是:
- 8K
- 32K
- 128K
- 1M
这种重复计算就会变得非常夸张。
于是自然会产生一个想法:
过去 Token 的 Key 和 Value 已经算过了,而且之后不会改变,为什么不直接保存下来?
这就是 KV Cache。
KV Cache 到底缓存了什么?
假设当前已经有:
$ Token1, Token2, Token_3 $
模型已经计算出了:
$ K1, K2, K_3 $
以及:
$ V1, V2, V_3 $
这些结果会被保存起来。
下一次生成 \(Token_4\) 时,不需要重新计算前三个 Token 的 K 和 V。
模型只需要计算新 Token 的:
$ Q4,\ K4,\ V_4 $
然后把新的 \(K4\) 和 \(V4\) 加到缓存里。
此时 Attention 使用的是:
$ Q_4 $
去查询:
$ K1,K2,K3,K4 $
再读取:
$ V1,V2,V3,V4 $
于是下一轮 KV Cache 就变成了:
$ K1,K2,K3,K4 $
和:
$ V1,V2,V3,V4 $
然后继续向后增长。
所以 KV Cache 本质上是一份:
已经生成过的 Token 在每一层 Attention 中对应的 Key 和 Value 历史。
为什么只缓存 K 和 V,不缓存 Q?
这是理解 KV Cache 时一个很关键的问题。
因为 Query 的作用是:
当前 Token 想查什么?
每次生成新的 Token,都需要新的 Query。
过去 Token 的 Query 对当前计算已经没有用了。
但 Key 和 Value 不一样。
它们代表的是历史 Token 可供后续 Token 查询的信息。
未来每一个新 Token,都可能继续关注这些过去的 Key 和 Value。
因此:
Q 是一次性的,K 和 V 是可以复用的。
这就是为什么叫:
KV Cache
而不是 QKV Cache。
KV Cache 带来了什么性能提升?
它最大的收益,就是避免重复计算历史 Token。
没有 KV Cache 时,每生成一个新 Token,都需要重新处理整个上下文。
有 KV Cache 以后,在 Decode 阶段,每一步主要只需要处理当前新产生的 Token。
假设已经有 10,000 个 Token。
生成第 10,001 个 Token 时:
没有 KV Cache 的思路是重新计算:
Token 1 ~ Token 10,001
而使用 KV Cache 后,只需要为:
Token 10,001
计算新的 Q、K、V。
过去 10,000 个 Token 的 K 和 V 直接从显存中读取。
这会极大减少重复计算。
因此今天的 LLM 推理系统几乎都会使用 KV Cache。
Prefill 和 Decode
KV Cache 也可以帮助理解两个非常重要的推理概念:
Prefill
和:
Decode
假设用户输入了一段 Prompt:
请帮我解释一下 Transformer 的工作原理……
这个 Prompt 可能有 2000 个 Token。
模型在真正开始生成回答之前,首先需要处理这 2000 个 Token。
这一阶段叫:
Prefill。
Prefill 会一次性处理整段输入,并为每一层 Transformer 生成对应的 K 和 V。
这些结果随后被放入 KV Cache。
接下来模型开始生成回答。
假设第一个输出 Token 是:
Transformer
模型把这个新 Token 的 K 和 V 加入 KV Cache。
然后生成下一个 Token。
这一阶段就叫:
Decode。
所以可以这样理解:
Prefill:先把已有上下文全部读一遍,并建立 KV Cache。
Decode:一个 Token 一个 Token 地生成,并不断扩充 KV Cache。
这两个阶段的计算特点也完全不同。
Prefill 更吃算力,Decode 更吃带宽
Prefill 一次处理很多 Token。
矩阵运算规模很大,因此 GPU 可以进行高效并行计算。
所以 Prefill 通常更加:
Compute Bound,也就是计算受限。
而 Decode 阶段,每次往往只处理一个新 Token。
虽然计算量不大,但每次都需要从显存中读取:
- 模型参数
- KV Cache
这时候 GPU 的大量算力可能没有被完全利用,反而是显存带宽成为瓶颈。
因此 Decode 往往更加:
Memory Bandwidth Bound,也就是内存带宽受限。
这也是为什么大模型推理并不能只看 GPU 的 TFLOPS。
对于生成速度来说:
显存带宽同样非常重要。
为什么上下文越长,KV Cache 越大?
因为每增加一个 Token,都需要为它保存新的 K 和 V。
假设上下文从:
$ 1000 $
增加到:
$ 10000 $
那么 KV Cache 也会大约按 Token 数量线性增长。
这和模型权重不同。
模型加载以后,权重大小基本是固定的。
但 KV Cache 是动态增长的。
生成得越长,占用显存越多。
这也是为什么你可能会发现:
模型明明可以装进显存,为什么一开 128K Context 就 OOM 了?
因为显存里不只有模型权重。
还有 KV Cache。
KV Cache 大小和什么有关?
大致来说,KV Cache 的大小和几个因素有关:
层数、上下文长度、KV Head 数量、Head Dimension、数据精度。
可以粗略写成:
$ KV\ Cache \propto Layers \times Tokens \times KVHeads \times HeadDim \times 2 $
这里的 2,来自同时需要保存:
K 和 V。
最后还需要乘上每个元素占用的字节数。
例如 FP16 / BF16 通常是:
$ 2\ bytes $
如果是 FP8,则更小。
因此上下文窗口一旦拉长,KV Cache 可能很快吃掉大量显存。
一个简单的估算
假设某个模型有:
- 32 层 Transformer
- 32 个 KV Head
- 每个 Head Dimension 是 128
- 使用 FP16
- Context Length 是 4096
那么单个 Token 的 KV Cache 大致需要:
$ 32 \times 32 \times 128 \times 2 \times 2 $
约等于:
$ 524288\ bytes $
也就是大约:
$ 512KB $
4096 个 Token 就已经大约需要:
$ 2GB $
这还只是一个简单示例。
不同模型使用的 Attention 结构不同,实际大小可能差异很大。
也正因为如此,后来大量模型开始采用 MQA 和 GQA。
MHA 为什么让 KV Cache 很大?
原始 Multi-Head Attention,也就是 MHA 中,每一个 Attention Head 都拥有自己的:
- Query
- Key
- Value
假设模型有 32 个 Attention Head。
那么就意味着有:
- 32 组 Q
- 32 组 K
- 32 组 V
这时候 KV Cache 必须保存所有 Head 的 K 和 V。
随着模型越来越大、Context 越来越长,显存压力就会非常高。
于是出现了:
Multi-Query Attention。
MQA:多个 Query 共享 K 和 V
MQA,全称:
Multi-Query Attention。
它做了一个非常直接的改变。
Query 仍然有很多 Head。
但是所有 Query Head 共享同一组:
Key 和 Value。
例如原来是:
32 个 Q Head 32 个 K Head 32 个 V Head
使用 MQA 以后可能变成:
32 个 Q Head 1 个 K Head 1 个 V Head
这样 KV Cache 直接缩小很多。
Decode 时需要读取的数据也明显减少。
因此 MQA 对大模型推理非常友好。
但共享得太狠,也可能影响模型能力。
于是又出现了一个折中方案。
GQA:现代 LLM 常见的选择
GQA,全称:
Grouped Query Attention。
它位于 MHA 和 MQA 之间。
不是每个 Query Head 都拥有自己的 KV。
也不是所有 Query Head 都共享同一组 KV。
而是:
一组 Query Head 共享一组 Key 和 Value。
例如:
32 个 Query Head
可能只对应:
8 个 KV Head
这样既能减少 KV Cache,又不会像 MQA 那样压缩得过于激进。
所以现在很多现代 LLM 都会使用 GQA。
如果你去看 Llama、Qwen、Mistral 一类模型的配置文件,经常会看到两个参数:
{
"num_attention_heads": 32,
"num_key_value_heads": 8
}
这两个数字不一样,通常就意味着模型使用了 GQA。
为什么服务器同时跑很多用户时,KV Cache 特别重要?
到这里还只是单个用户。
真正部署 LLM 服务以后,情况会更加复杂。
假设服务器同时处理:
100 个用户。
每个用户都有:
8K Context。
那就意味着服务器需要同时保存 100 份不同的 KV Cache。
模型参数可以共享。
但是每个用户的对话上下文不同,所以 KV Cache 不能直接共享。
因此在线 LLM 推理中,显存很容易变成:
权重占一部分,KV Cache 又占掉很大一部分。
这也是为什么推理框架一直在研究:
怎么更高效地管理 KV Cache?
PagedAttention
vLLM 中一个很有代表性的技术叫:
PagedAttention。
它解决的重点不是 Attention 数学公式本身,而是:
KV Cache 的内存管理。
传统做法往往需要给一个请求预留连续的显存空间。
但不同用户生成长度不同。
有人生成 100 Token 就结束了,有人生成 5000 Token。
这样很容易造成显存碎片和空间浪费。
PagedAttention 借鉴了操作系统中的分页思想。
KV Cache 不必全部存储在连续内存中,而是被切分成多个 Block。
逻辑上它仍然是一段连续的 KV Cache。
物理上可以分散存储。
这让显存利用率明显提高,也帮助 vLLM 实现更高的并发吞吐。
如果以后学习 LLM Serving,PagedAttention 基本是绕不开的概念。
Prefix Cache
KV Cache 还能继续做优化。
假设很多用户都使用同一个 System Prompt:
You are a helpful assistant...
后面还有一大段相同的系统规则。
如果每一个请求都重新 Prefill 一次这些相同内容,其实也是重复计算。
于是推理框架可以缓存公共 Prompt 对应的 KV Cache。
下一次遇到相同前缀时直接复用。
这通常叫:
Prefix Caching
或者:
Prompt Caching。
例如一个 Agent 系统可能有 5000 Token 的固定 System Prompt。
如果每轮请求都重新 Prefill 这 5000 Token,会浪费大量算力。
使用 Prefix Cache 后,固定前缀的 KV 可以直接复用。
这会显著降低:
TTFT,也就是 Time To First Token。
为什么 KV Cache 不能无限保存?
既然 KV Cache 可以加速,那是不是全部留下来就好?
问题还是显存。
假设对话一直持续:
10K Token 50K Token 100K Token 500K Token
KV Cache 会不断增长。
最终显存一定会成为瓶颈。
因此长上下文模型必须解决两个问题:
一方面:
模型能不能理解这么长的 Context?
另一方面:
系统能不能承担这么大的 KV Cache?
这是两个不同的问题。
一个模型声称支持 128K Context,并不意味着你的硬件一定能轻松运行 128K。
上下文窗口是模型能力。
显存占用是工程问题。
KV Cache 量化
既然 KV Cache 很占显存,一个很自然的方法就是:
降低它的数据精度。
例如原本使用:
FP16 / BF16
保存 KV。
可以尝试压缩到:
FP8
甚至更低精度。
这样可以明显减少 KV Cache 的显存占用。
这种技术通常称为:
KV Cache Quantization。
它和模型权重量化很像,但对象不同。
模型量化压缩的是:
Weight。
KV Cache Quantization 压缩的是:
推理过程中动态产生的 K 和 V。
如果精度下降得合理,可以换来更长的 Context 或者更高的并发能力。
KV Cache 和模型参数不是一回事
这是一个很容易混淆的地方。
比如一个模型是:
7B 参数。
这只是在描述模型本身的参数规模。
它不包含 KV Cache。
模型参数在加载后基本固定。
KV Cache 则跟当前请求动态相关。
同一个 7B 模型:
跑 2K Context
和跑 128K Context
显存需求可能完全不同。
所以估算一个 LLM 到底需要多少显存时,不能只算:
$ Parameters \times Precision $
还必须考虑:
KV Cache。
特别是长上下文和高并发场景。
KV Cache 和 Agent 有什么关系?
如果只做普通聊天,KV Cache 似乎只是推理底层的事情。
但到了 Agent 系统里,它会变得更重要。
一个 Agent 可能拥有:
- 很长的 System Prompt
- Tool 定义
- Memory
- RAG 检索结果
- 多轮 Tool Call
- Observation
- Reasoning History
这些内容都会进入 Context。
Context 越长:
Prefill 越慢。
KV Cache 越大。
Decode 时需要读取的历史 KV 越多。
所以 Agent 并不是“Prompt 能塞多少就塞多少”。
Prompt Engineering 到了一定规模以后,本质上也会变成:
Context Engineering。
哪些信息应该保留?
哪些应该压缩?
哪些应该放进 RAG?
哪些应该写入长期 Memory?
哪些历史消息应该被删除?
背后不仅影响模型效果,也直接影响推理成本。
为什么长 Context 不一定越好?
很多模型现在支持:
128K、256K,甚至更长的 Context Window。
但这不代表每次请求都应该把 Context 塞满。
因为长 Context 会带来几个现实问题。
首先是 Prefill 延迟。
输入越长,模型开始输出第一个 Token 之前,需要处理的内容越多。
其次是 KV Cache。
Context 越长,KV Cache 占用越大。
最后还有 Attention 成本。
即使有各种优化,极长上下文仍然会带来明显的计算和内存压力。
所以一个好的 LLM 系统,不是:
能塞进去多少,就塞进去多少。
而是:
只让模型看到真正需要的信息。
这也是 RAG、Memory Management、Context Compression 等技术存在的重要原因。
再看一次完整的推理过程
现在可以把一次 LLM 请求重新理解一遍。
用户发送 Prompt。
模型首先进入 Prefill。
Prompt 中的所有 Token 经过 Transformer,并为每一层生成对应的 Key 和 Value。
这些 K 和 V 被保存到 KV Cache。
随后开始 Decode。
模型生成第一个 Token。
这个 Token 对应的新 K 和 V 被追加到 KV Cache。
再生成第二个 Token。
继续追加。
整个回答生成过程中,KV Cache 会不断变长。
所以 KV Cache 实际上就是模型在当前推理过程中保存的一份:
Attention 历史状态。
它让模型不用每一步都重新计算过去。
代价则是:
用显存换计算。
这也是理解 KV Cache 最重要的一句话。
从 KV Cache 继续往下学
理解 KV Cache 以后,很多 LLM 推理相关的概念都会突然连起来。
比如:
为什么首 Token 很慢?
去看 Prefill 和 TTFT。
为什么后续 Token 一个个出来?
去看 Decode。
为什么大模型服务器特别看重显存带宽?
去看 Decode 的 Memory-Bound 特性。
为什么 GQA 很重要?
因为它可以减少 KV Head 数量和 KV Cache。
为什么 vLLM 吞吐量高?
其中一个核心就是 PagedAttention 和更高效的 KV Cache 管理。
为什么 Agent 的 Context 不能无限增加?
因为长 Context 不只影响 Attention,还会让 KV Cache 持续膨胀。
所以 KV Cache 看起来只是 Transformer 里的一个工程优化,实际上它连接了:
模型架构、GPU、显存、推理速度、上下文长度以及 LLM Serving。
如果说 Transformer 解释了“大模型为什么能理解上下文”,那么 KV Cache 解决的就是另一个现实问题:
已经理解过的上下文,为什么还要再算一遍?
答案是:
不需要。
把 Key 和 Value 保存下来就行。