大模型生成文字的时候,看起来像是在一口气回答,其实内部更像“文字接龙”:先看已有内容,猜下一个 token;再把这个 token 接到后面,继续猜下一个。
问题是,聊得越长,前面的内容越多。如果每生成一个新 token,都把前面所有内容重新看一遍、重新算一遍,速度会越来越慢,也会浪费很多算力。
一、一句话理解 KV Cache
可以把 KV Cache 理解成 Transformer 推理时的“历史注意力笔记”。
每生成一个新 token,模型都要问一句:我现在这个位置,应该重点参考前面哪些内容?这个“找参考资料”的过程,就是 Attention。
Attention 里常见的 Q/K/V,可以先用一个很土但好懂的比喻来记:
- Query(Q):当前 token 提的问题,“我现在想找什么信息”;
- Key(K):历史 token 身上的目录标签,“我这里大概是什么内容”;
- Value(V):历史 token 真正提供的内容,“匹配上以后拿走什么信息”。
自回归生成时,过去已经写出来的 token 不会再变。它们对应的 K 和 V 也不会再变。既然这份“目录标签”和“正文内容”已经算过了,就没有必要每一步都重算。
二、先区分 Prefill 和 Decode
理解 KV Cache,最好先把一次大模型请求拆成两个阶段。英文名字有点硬,但意思很简单。
Prefill 可以理解成“先把题目读完”。用户输入一整段 prompt,模型先把这段内容整体看一遍,算出每一层、每个 token 的 K/V,并把它们放进缓存里。
Decode 可以理解成“一个字一个字往外写”。之后每一步只处理一个新 token。模型只需要计算这个新 token 的 Q/K/V,然后拿 Q 去翻前面缓存好的 K/V,决定下一个 token 应该是什么。
三、KV Cache 缓存的到底是什么
这里有一个很容易误解的点:KV Cache 不是把 token ID 存起来。token ID 本来就很小,而且一直都在,单独存它并不能省多少计算。
KV Cache 真正存的是:历史 token 进入每一层 Attention 后,已经算好的 Key 和 Value。你可以先把它理解成“模型读完历史内容后留下的中间笔记”。
为什么要“每一层”都存?因为 Transformer 不是只读一遍文本。它每过一层,都会把信息重新加工一次。第 1 层看到的东西,和第 20 层看到的东西已经不一样了,所以每一层都要有自己的 K/V 缓存。
K cache: [batch, kv_heads, seq_len, head_dim]
V cache: [batch, kv_heads, seq_len, head_dim]
整个模型还要再乘上 num_layers。很多框架内部会为了访存效率换一种布局,但含义基本不变。
四、一步 Decode 里 Q/K/V 怎么流动
假设现在已经有一段历史上下文,KV Cache 里也已经存好了它们的 K/V。现在新来了一个 token,模型大概会这么做:
- 新 token 进入当前 Transformer 层;
- 这一层只为这个新 token 计算 Q、K、V;
- 把新 token 的 K/V 追加到这一层的 KV Cache 后面;
- 用新 token 的 Q 去查“历史 K + 新 K”,看看应该关注谁;
- 根据注意力权重,从“历史 V + 新 V”里取出需要的信息;
- 这一层处理完后,结果进入下一层,下一层重复同样过程。
五、KV Cache 的显存公式
KV Cache 最大的代价是显存。说白了:笔记越多,占的地方就越大。
如果公式看着有点吓人,只要先记住一句话:层数越多、上下文越长、并发请求越多、KV 头数越多,KV Cache 就越大。
这里的 2 来自 K 和 V 两份缓存。bytes_per_element 取决于精度:FP16/BF16 通常是 2 bytes,FP32 是 4 bytes。如果对 KV Cache 做量化,每个元素占用的字节数还能再降。
举个粗略例子:32 层、batch=1、seq_len=4096、kv_heads=32、head_dim=128、FP16。
= 2,147,483,648 bytes
≈ 2 GiB
注意,这还只是一个请求。并发多了、上下文长了、batch 变大了,KV Cache 很快就会成为很大的显存账单。
六、为什么长上下文更容易卡住
KV Cache 省掉了“重复计算历史 K/V”,但它没有省掉“读取历史 K/V”。
也就是说,它让模型不用重新做笔记,但每次回答时还是要翻笔记。上下文越长,笔记越厚,每一步要翻的内容就越多。于是 Decode 阶段常常会卡在显存读写上:算力不一定满,但显存带宽已经很忙。
这也是为什么长上下文服务不仅要看模型大小,还要看 KV Cache 管理能力。模型权重是固定成本,KV Cache 是跟请求长度和并发一起涨的动态成本。
七、MHA、MQA、GQA 对缓存有什么影响
这一节只要抓住一个关键词:kv_heads。KV 头数越多,要存的 K/V 就越多,缓存也越大。
在普通 Multi-Head Attention(MHA)里,每个 attention head 都有自己的 K/V,所以 kv_heads = attention_heads。这很直观,但缓存也大。
Multi-Query Attention(MQA)把所有 query heads 共享同一组 K/V,相当于 kv_heads = 1。它可以显著减少 KV Cache 和带宽压力,但可能带来一定效果损失。
Grouped-Query Attention(GQA)在两者之间折中:多个 query heads 分成若干组,每组共享一组 K/V。很多现代 LLM 都会采用这种方式,在质量和推理效率之间找平衡。
八、KV Cache 和 FlashAttention 的关系
KV Cache 和 FlashAttention 都在优化 Attention,但它们不是一回事。
- KV Cache 解决“旧内容别重复算”的问题:历史 K/V 不变,所以存下来复用;
- FlashAttention 解决“Attention 计算时少搬数据”的问题:不要把巨大的注意力矩阵反复写进 HBM,而是分块在更快的 SRAM 里完成。
所以二者可以同时存在。Prefill 阶段可以用 FlashAttention 更高效地处理整段 prompt;Decode 阶段继续用 KV Cache 复用历史 K/V。
九、KV Cache 和 PagedAttention 的关系
KV Cache 告诉我们“要缓存什么”。PagedAttention 更像是在回答“这么多缓存怎么管”。
可以把显存想成仓库。传统做法容易给每个请求预留一大块连续仓库空间。问题是不同请求长短不一:有的很快结束,有的越聊越长,仓库就容易出现空洞和浪费。
PagedAttention 借鉴操作系统分页思想,把 KV Cache 切成固定大小的 block。一个请求的逻辑序列可以映射到多个物理 block,不要求连续。这样显存管理更灵活,也更适合连续批处理和高并发服务。
十、工程实现里要注意什么
读到这里,原理基本就清楚了。真正做推理服务时,还要把这些缓存管好。工程上通常要关注这些问题:
- 缓存生命周期:请求结束、生成停止、用户断开连接时,缓存要及时释放;
- 最大上下文:超过窗口后要截断、滑动窗口或做长上下文策略,否则显存会一直涨;
- 并发调度:不同请求的 decode 步长不同,需要连续批处理把多个请求拼在一起跑;
- 显存碎片:长短请求混在一起时,最好用 block/paged 管理;
- 缓存量化:把 K/V 用更低精度存储可以省显存,但要评估效果损失;
- 前缀复用:多个请求有相同 system prompt 或长前缀时,可以考虑 prefix caching,但这和“单个请求内部的 KV Cache”不是完全一回事。
十一、最后总结
KV Cache 的核心并不复杂:历史 token 的 Key 和 Value 不变,所以把它们存下来,后面生成新 token 时直接复用。
用一句更口语的话说:KV Cache 不是让模型“不看历史”,而是让模型“别把历史重新算一遍”。它让推理快了很多,但也带来了显存压力。上下文越长、并发越高,这本“历史笔记”就越厚。真正做推理服务时,KV Cache 往往是吞吐、延迟、显存容量和长上下文能力之间最重要的那本账。
参考资料
- Attention Is All You Need
- Fast Transformer Decoding: One Write-Head is All You Need
- GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints
- Efficient Memory Management for Large Language Model Serving with PagedAttention
- FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness