核心思想

KV 缓存的本质:在自回归生成中,过去 token 的 K、V 向量永远不变——与其每步重算,不如算一次存起来。代价是 GPU 显存:它会随序列长度线性增长,常常成为规模化部署时的真正瓶颈。
建缓存很贵,读缓存很便宜。

你一定注意到过:每次用 ChatGPT 或者 Claude,第一个 token 总是要慢半拍才冒出来,但之后剩下的内容几乎是瞬间流出来的。

这背后藏着一个刻意为之的工程决策,叫做 KV 缓存(KV Caching),目的就是让 LLM 推理跑得更快。

在进入技术细节之前,先来一段对比:开启和关闭 KV 缓存时,LLM 推理到底差多少。

接下来,我们从第一性原理出发,看看它到底是怎么工作的。

第一部分:LLM 是怎么生成 token 的

Transformer 会处理所有输入 token,给每一个都产出一份隐藏状态(hidden state)。然后这些隐藏状态被投影到词表空间(vocabulary space),得到 logits——也就是给词表里每个词打的一个分数。

但真正有用的,只有最后一个 token 的 logits。从这些分数里采样,就能得到下一个 token,然后把它拼回输入末尾,重复这个过程。

这里有一个关键洞察:要生成下一个 token,你只需要最近那一个 token 的隐藏状态。其他所有 token 的隐藏状态,都只是中间副产物。

第二部分:注意力到底在算什么

在每一层 Transformer 里,每个 token 都会得到三个向量:查询(Q)、键(K)、值(V)。注意力机制把查询和键相乘得到分数,再用这些分数对值做加权。

现在,把目光聚焦到最后一个 token 上。

QK^T 的最后一行用到的是:

  • 最后一个 token 的查询向量
  • 整个序列里所有的键向量

而那一行最终的注意力输出用到的是:

  • 同一个查询向量
  • 所有的键向量和值向量

所以,要算出我们真正需要的那个隐藏状态,每一层注意力都需要:最新 token 的 Q,以及所有 token 的 K 和 V。

第三部分:藏在里面的重复计算

生成第 50 个 token 时,需要 token 1 到 50 的 K 和 V 向量;生成第 51 个 token 时,又需要 token 1 到 51 的 K 和 V 向量。

可是 token 1 到 49 的 K 和 V 向量在上一步就已经算过了,它们根本没变——同样的输入,同样的输出。但模型每一步都要从头再算一遍。

这就是每一步 O(n) 的冗余计算。整个生成过程加起来,浪费的算力是 O(n²)。

第四部分:怎么修

与其每一步都重算所有 K 和 V 向量,不如把它们存起来。每来一个新 token,就这样做:

  1. 只为最新这一个 token 算 Q、K、V。
  2. 把新的 K 和 V 追加到缓存里。
  3. 从缓存里取出之前所有的 K 和 V 向量。
  4. 用新的 Q 对完整的 K、V 缓存做注意力计算。

这就是 KV 缓存。每一步、每一层,只新增一个 K 和一个 V,其它的全都从内存里读出来。

注意力计算本身仍然会随序列长度而增长(毕竟你要对所有的键和值做注意力),但生成 K 和 V 的那些昂贵投影运算,每个 token 只会发生一次,而不是每一步都来一遍。

第五部分:首 token 时延(Time-to-First-Token)

到这里,你就能明白为什么第一个 token 慢了。

当你发送一个 prompt,模型会在一次前向传播里把整段输入跑完,并为每个 token 计算并缓存好 K 和 V 向量。这个阶段叫预填充(prefill),也是整个请求里最吃算力的部分。

一旦缓存预热完毕,后面每一个 token 只需要跑一次单 token 的前向传播。

那段最初的等待时间,就是首 token 时延(time-to-first-token, TTFT)。prompt 越长,预填充就越久,等待时间也越长。围绕 TTFT 的优化(分块预填充、投机解码、提示词缓存)本身就是一个深水区话题,但底层规律永远一样:建缓存很贵,读缓存很便宜。

第六部分:背后的取舍

KV 缓存的本质,是用显存换计算。每一层、每一个 token 的 K 和 V 向量都得存下来。以 Qwen 2.5 72B 为例(80 层、32K 上下文、隐藏维度 8192),单个请求的 KV 缓存就能吃掉好几 GB 的 GPU 显存。当并发请求达到几百个时,KV 缓存往往比模型权重本身还要大。

这正是分组查询注意力(grouped-query attention, GQA)和多查询注意力(multi-query attention, MQA)出现的原因:让多个查询头共享键/值头,砍掉一大块显存,而质量损失几乎可以忽略。

这也是为什么把上下文长度翻倍这么难。窗口翻倍,每个请求的 KV 缓存就翻倍,能同时服务的用户也就更少了。

还有一种思路叫分页注意力(Paged attention),也是在解决这个问题。我最近专门聊过它(参见原文链接的引用推文)。

tl;dr

KV 缓存消除了自回归生成中的冗余计算。先前的 token 永远会产出相同的 K 和 V 向量,所以算一次存起来就够了,每一个新 token 只需要算它自己的 Q、K、V,然后让注意力对完整的缓存做计算。

实测能带来 5 倍加速。代价是 GPU 显存——这在规模化部署时就成了那道决定性的瓶颈。每一个 LLM 服务栈(vLLM、TGI、TensorRT-LLM)都建立在这个思想之上。

到这里就讲完了!

如果你喜欢这篇教程:

来找我 → @_avichawla

我每天都会分享关于数据科学(DS)、机器学习(ML)、LLM 和 RAG 的教程与思考。