KV Cache:大模型推理为什么不用每个字都从头算

大模型生成文字的时候,看起来像是在一口气回答,其实内部更像“文字接龙”:先看已有内容,猜下一个 token;再把这个 token 接到后面,继续猜下一个。

问题是,聊得越长,前面的内容越多。如果每生成一个新 token,都把前面所有内容重新看一遍、重新算一遍,速度会越来越慢,也会浪费很多算力。

先给结论:KV Cache 就像模型给历史上下文做的“读书笔记”。它缓存的不是原始文字,也不是最终答案,而是 Transformer 每一层 Attention 已经算好的 Key 和 Value。后面生成新 token 时,旧内容的 K/V 不会变,直接翻这份笔记就行。

一、一句话理解 KV Cache

可以把 KV Cache 理解成 Transformer 推理时的“历史注意力笔记”。

每生成一个新 token,模型都要问一句:我现在这个位置,应该重点参考前面哪些内容?这个“找参考资料”的过程,就是 Attention。

Attention 里常见的 Q/K/V,可以先用一个很土但好懂的比喻来记:

  • Query(Q):当前 token 提的问题,“我现在想找什么信息”;
  • Key(K):历史 token 身上的目录标签,“我这里大概是什么内容”;
  • Value(V):历史 token 真正提供的内容,“匹配上以后拿走什么信息”。

自回归生成时,过去已经写出来的 token 不会再变。它们对应的 K 和 V 也不会再变。既然这份“目录标签”和“正文内容”已经算过了,就没有必要每一步都重算。

生成第 t 个 token 时,浪费发生在哪里 没有 KV Cache 每一步都把历史 token 重新过一遍网络 喜欢 代码 重新计算所有 token 的 Q/K/V 旧 token 已经算过,还是重复算 有 KV Cache 历史 K/V 直接从缓存里读,只算新 token K/V K/V K/V K/V 只计算新 token 的 Q/K/V 然后把新 K/V 追加到缓存
KV Cache 的直觉:历史 token 的 K/V 已经确定,后续步骤只复用,不重复计算。

二、先区分 Prefill 和 Decode

理解 KV Cache,最好先把一次大模型请求拆成两个阶段。英文名字有点硬,但意思很简单。

Prefill 可以理解成“先把题目读完”。用户输入一整段 prompt,模型先把这段内容整体看一遍,算出每一层、每个 token 的 K/V,并把它们放进缓存里。

Decode 可以理解成“一个字一个字往外写”。之后每一步只处理一个新 token。模型只需要计算这个新 token 的 Q/K/V,然后拿 Q 去翻前面缓存好的 K/V,决定下一个 token 应该是什么。

一次请求里的两段工作 Prefill:读完整 prompt token 1 token 2 token n 建立初始 KV Cache Decode:一次生成一个 token 新 token 读历史 K/V追加新 K/V 循环直到结束
Prefill 阶段负责把 prompt 的缓存建起来;Decode 阶段一边生成,一边往缓存末尾追加。

三、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。很多框架内部会为了访存效率换一种布局,但含义基本不变。
KV Cache 是“每层一份”的缓存 Layer 1: K/V Cache Layer 2: K/V Cache Layer 3: K/V Cache ... Layer L: K/V Cache 单层缓存可以想成一个四维表 [batch, kv_heads, seq_len, head_dim] seq_len 会随着上下文长度线性增长 K V 每个请求、每层、每个历史 token 都会占空间
KV Cache 的显存压力来自“层数 × 序列长度 × KV 头数 × head 维度”。

四、一步 Decode 里 Q/K/V 怎么流动

假设现在已经有一段历史上下文,KV Cache 里也已经存好了它们的 K/V。现在新来了一个 token,模型大概会这么做:

  1. 新 token 进入当前 Transformer 层;
  2. 这一层只为这个新 token 计算 Q、K、V;
  3. 把新 token 的 K/V 追加到这一层的 KV Cache 后面;
  4. 用新 token 的 Q 去查“历史 K + 新 K”,看看应该关注谁;
  5. 根据注意力权重,从“历史 V + 新 V”里取出需要的信息;
  6. 这一层处理完后,结果进入下一层,下一层重复同样过程。
第 t 步 Decode:新 token 只带来一份新 Q/K/V 新 token Q_t K_t V_t 追加 K_t / V_t Cache 长度 +1 Attention Q_t × K_cache 权重 × V_cache 输出当前层结果
注意:旧 token 的 Q 通常不用再算,因为它们不会再作为“当前查询”去生成过去的 token。

五、KV Cache 的显存公式

KV Cache 最大的代价是显存。说白了:笔记越多,占的地方就越大。

如果公式看着有点吓人,只要先记住一句话:层数越多、上下文越长、并发请求越多、KV 头数越多,KV Cache 就越大。

KV Cache bytes = 2 × num_layers × batch_size × seq_len × kv_heads × head_dim × bytes_per_element

这里的 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 × 32 × 1 × 4096 × 32 × 128 × 2 bytes
= 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 头数,就是减少缓存账单 MHA 每个 head 一份 K/V Q K V Q K V KV 多,缓存大 MQA 所有 Q 共享一份 K/V Q Q Q K V KV 最省 GQA 一组 Q 共享一份 K/V Q Q K V Q Q K V 质量和效率折中
MHA、MQA、GQA 的核心差别之一,就是 K/V 是否被多个 query heads 共享。

八、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,不要求连续。这样显存管理更灵活,也更适合连续批处理和高并发服务。

PagedAttention:把 KV Cache 切成块来管理 逻辑序列 0 1 2 物理 KV blocks Block A Block B Block C Block D Block E 逻辑连续,物理不必连续
PagedAttention 不改变 Attention 数学,它主要改变 KV Cache 的内存管理方式。

十、工程实现里要注意什么

读到这里,原理基本就清楚了。真正做推理服务时,还要把这些缓存管好。工程上通常要关注这些问题:

  • 缓存生命周期:请求结束、生成停止、用户断开连接时,缓存要及时释放;
  • 最大上下文:超过窗口后要截断、滑动窗口或做长上下文策略,否则显存会一直涨;
  • 并发调度:不同请求的 decode 步长不同,需要连续批处理把多个请求拼在一起跑;
  • 显存碎片:长短请求混在一起时,最好用 block/paged 管理;
  • 缓存量化:把 K/V 用更低精度存储可以省显存,但要评估效果损失;
  • 前缀复用:多个请求有相同 system prompt 或长前缀时,可以考虑 prefix caching,但这和“单个请求内部的 KV Cache”不是完全一回事。
一个常见混淆:很多云厂商说的 prompt cache / prefix cache,通常指跨请求复用相同前缀带来的计费或延迟优化;这和模型 decode 时每个请求内部维护的 KV Cache 有关系,但不是同一个概念。

十一、最后总结

KV Cache 的核心并不复杂:历史 token 的 Key 和 Value 不变,所以把它们存下来,后面生成新 token 时直接复用。

用一句更口语的话说:KV Cache 不是让模型“不看历史”,而是让模型“别把历史重新算一遍”。它让推理快了很多,但也带来了显存压力。上下文越长、并发越高,这本“历史笔记”就越厚。真正做推理服务时,KV Cache 往往是吞吐、延迟、显存容量和长上下文能力之间最重要的那本账。

参考资料