K-V Cache(键值缓存)是通过空间换时间的方式,极大地降低了大模型计算的时间复杂度。
1. 什么是 K-V Cache?
在 Transformer 的注意力机制(Attention Mechanism)中,计算依赖于三个核心矩阵:Query (Q) 、Key (K) 和 Value (V) 。
由于大模型生成文本是自回归的(即每次根据之前生成的所有 Token 预测下一个 Token),每次生成新词时,都需要计算当前上下文中所有 Token 的注意力。 如果不使用缓存,模型在生成第 t 个 Token 时,需要重新计算前 t−1 个 Token 的 K 和 V 向量。K-V Cache 的核心思想就是:将之前已经计算过的历史 Token 的 K 和 V 向量保存下来(放入显存中)。 在计算当前 Token 时,只需计算当前新 Token 的 Q、K、V,并将其 K、V 与缓存中的历史 K、V 拼接,从而避免了重复计算。
Q、K、V 听起来有点抽象,我们可以使用一个类比来理解:
在大模型的“大脑”(注意力机制)里,为了理解上下文,它会把每一个词转换成三种身份,分别是 Q、K、V。你可以把这想象成我们在图书馆查资料:
- Q (Query / 查询) :代表**“我现在想找什么”**。就像你在图书馆检索系统里输入的关键词。
- K (Key / 键) :代表**“资料的标签/标题”**。就像每本书的索引号和书名。
- V (Value / 值) :代表**“资料的具体内容”**。就是那本书里写的具体知识。
模型怎么理解一个词的意思呢?它会拿着当前词的 Q(查询),去和前面所有词的 K(标签)做对比。哪个标签最匹配,它就提取哪个词的 V(内容),把这些内容综合起来,模型就“懂”了当前的上下文。
2. 为什么需要 K-V Cache?
假设我们要让大模型续写这句话: “今天天气很好,我们去” ,模型需要预测出下一个字是**“玩”**。大模型是一个字一个字往外蹦的。
做法一:不用 K-V Cache(一个没有记忆的“笨办法”)
当模型准备生成“玩”这个字时,它现在的输入是:“今天天气很好,我们去”。
- 模型要计算“今”的 Q、K、V。
- 模型要计算“天”的 Q、K、V。
- 以此类推,把前面 8 个字的 Q、K、V 全部重新算一遍。
- 拿着“去”的 Q,去匹配前面所有字的 K,拿到对应的 V。
- 终于算出了下一个字:“玩”。
问题在哪里? 假设接下来还要生成“吧”字,输入变成了“今天天气很好,我们去玩”。模型又是个“失忆症患者”,它又要从“今”字开始,把前面 9 个字的所有东西再重新算一遍!如果一篇文章有 1000 个字,生成第 1001 个字时,前面 1000 个字的功课都要重做,这简直是折磨。
做法二:使用 K-V Cache(“好记性不如烂笔头”)
既然前面字的“标签”(K) 和“内容”(V) 已经算过了,而且它们是固定不变的,我们为什么不找个笔记本把它们记下来呢?这个“笔记本”就是 K-V Cache(键值缓存) 。
当模型准备生成“玩”这个字时,输入依然是:“今天天气很好,我们去”。
- 模型翻开它的“笔记本”(Cache),发现里面已经记好了“今天天气很好,我们”这 8 个字的 K 和 V。
- 模型这次只需要专门计算当前最新字“去”的 Q、K、V。
- 模型把“去”的 K 和 V 补充写进“笔记本”里。
- 模型拿着“去”的 Q,对着“笔记本”里记好的所有 K 进行匹配,拿到需要的 V。
- 轻松算出下一个字:“玩”。
这样做的好处是什么? 不管文章有多长,模型每次只需要计算最新那一个字的东西。历史的包袱全被存放在了“笔记本”里,直接拿来用就行了。这就是大模型能快速和你聊天的核心秘密。
K-V Cache 流程图
例子:根据输入:“今天天气很好”,生成输出:“我们去玩”
sequenceDiagram
participant User as 用户
participant LLM as 模型大脑 (矩阵计算)
participant Cache as 笔记本 (K-V Cache)
Note over User, Cache: 阶段 1:读题阶段 (Prefill) - 批量处理
User->>LLM: 输入提示词: "今天天气很好," (假设共6个Token)
LLM->>LLM: 瞬间并行计算这 6 个 Token 的 Q, K, V
LLM->>Cache: 批量存入笔记:这 6 个 Token 的 K 和 V
LLM-->>User: 结合笔记,预测并输出第 1 个词:"我"
Note over User, Cache: ==========================================
Note over User, Cache: 阶段 2:答题阶段 (Decode) - 循环自回归生成
%% 生成“们”的过程
Note right of LLM: 准备生成“们”
LLM->>LLM: ① 接收上一步的词:"我" (作为新一轮的输入)
LLM->>LLM: ② 【计算】将"我"转换为向量,并算出"我"的 Q, K, V
LLM->>Cache: ③ 【存笔记】将算好的 "我" 的 K,V 存入缓存
LLM-->>User: ④ 拿着"我"的 Q,查阅更新后的笔记,输出:"们"
%% 生成“去”的过程
Note right of LLM: 准备生成“去”
LLM->>LLM: ① 接收上一步的词:"们" (作为新一轮的输入)
LLM->>LLM: ② 【计算】算出"们"的 Q, K, V
LLM->>Cache: ③ 【存笔记】将算好的 "们" 的 K,V 存入缓存
LLM-->>User: ④ 拿着"们"的 Q,查阅更新后的笔记,输出:"去"
%% 生成“玩”的过程
Note right of LLM: 准备生成“玩”
LLM->>LLM: ① 接收新鲜出炉的词:"去" (作为新一轮的输入)
LLM->>LLM: ②【计算】算出"去"的 Q, K, V
LLM->>Cache: ③【存笔记】将算好的 "去" 的 K,V 存入缓存
LLM-->>User: ④ 拿着"去"的 Q,查阅目前最完整的笔记,输出:"玩"
温馨提示:大模型的最小计算单元是 token,上述说明中的为了方便是按文字来划分的,比如“今天天气”,如果划分为 token,可能是:["今天", "天气"] 2个 token,也可能是:["今天", "天", "气"] 3 个 token,不同模型的划分可能不一样。