从 Q/K/V 到 KV Cache 与缓存命中:大模型推理缓存机制详解
本文以主流 decoder-only 自回归 Transformer 为例梳理 Q/K/V 的计算方式、KV Cache 的作用以及服务指标中“缓存命中”的确切含义。不同模型在位置编码、注意力变体、KV Cache 布局上可能有所差异但主干逻辑一致。一、结论先行在展开细节之前先给出几个关键结论模型参数是预定义的(W_Q、W_K、W_V)、FFN、LayerNorm 等权重在训练完成后固定不变。Q/K/V 是运行时计算的每个 token 在每一层、每个注意力头中都会根据当前隐藏状态和该层权重动态算出 Q、K、V。Q 用于查询K 用于被查询V 用于被加权取内容。每个输入 token 在每一层都要计算输出而非只计算最后一个 token。层与层之间传递的是隐藏状态序列。下一层拿到上一层所有位置的输出后用自己的权重重新计算下一层的 Q/K/V。Q 用完即弃K/V 被缓存。未来新 token 的 Q 会反复查询历史 K并加权历史 V。预测下一个 token 只用最后一层最后一个位置的输出经过 LM Head 映射到词表。单次生成中的 KV Cache 不叫缓存命中。服务指标中的缓存命中通常指跨请求复用相同前缀的每层 K/V。下面逐层展开。二、Q/K/V 是什么如何计算输入文本先被 tokenizer 切分为 token。每个 token 先经过 embedding 层变成一个向量。进入 Transformer 后每一层都会进行线性投影[Q H W_Q][K H W_K][V H W_V]其中(H) 是当前层的输入隐藏状态(W_Q、W_K、W_V) 是该层、该注意力头训练好的权重(Q、K、V) 是运行时根据 (H) 算出的。因此预定义的是权重矩阵不是 Q/K/V 本身。不同输入、不同请求、不同层、不同头算出的 Q/K/V 都不同。注意力计算可简写为[\text{Attention}(Q,K,V)\text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}M\right)V]其中 (M) 是因果 mask保证当前位置只能看到自己及之前的 token。直观理解Q当前 token 想查询什么K历史 token 能被什么查询到V历史 token 实际提供的内容。Q 与 K 匹配出权重权重再用来加权 V。三、每个输入 token 都要计算输出这是最容易产生困惑的地方。在 Transformer 中并非只计算最后一个 token。每一层都会对所有位置并行计算得到一组输出向量输入今天 天气 很 好 第 1 层输出h1_今天, h1_天气, h1_很, h1_好 第 2 层输出h2_今天, h2_天气, h2_很, h2_好 ... 第 N 层输出hN_今天, hN_天气, hN_很, hN_好这些输出不是文字也不是 token而是高维向量。它们代表每个 token 在当前层、当前上下文下的表示。为什么每个位置都要计算因为下一层需要上一层所有位置的输出作为输入。下一层拿到这些输出后会用它自己的 (W_Q、W_K、W_V) 重新计算下一层的 Q/K/V[Q^l H^{l-1} W_Q^l][K^l H^{l-1} W_K^l][V^l H^{l-1} W_V^l]也就是说下一层不是直接使用上一层的 K/V而是根据上一层每个 token 的输出重新计算下一层自己的 K/V。如果上一层只输出了最后一个位置下一层就没有其他位置的输入无法计算它们的 K/V最后一个位置的 Q 也无法与它们做注意力。因此每一层都必须把所有位置算完。四、层是什么一层通常指一个 Transformer Block常见结构为输入 ↓ LayerNorm ↓ 多头自注意力 ↓ 残差连接 ↓ LayerNorm ↓ 前馈网络 FFN ↓ 残差连接 ↓ 输出大模型会堆叠很多这样的层。每一层都有自己独立的参数自己的 (W_Q、W_K、W_V)、自己的 FFN、自己的 LayerNorm。因此一个 token 在每一层都会产生一组新的 Q/K/V。KV Cache 缓存的也是每一层自己的历史 K/V层与层之间不共享。五、Prefill 与 DecodeKV Cache 的产生自回归生成分为两个阶段。5.1 Prefill 阶段用户输入 prompt例如今天 天气 很 好模型一次性处理整个输入。每一层中每个 token 根据当前隐藏状态算出 Q/K/V每个位置的 Q 查询同层因果可见范围内的 K用注意力权重加权对应的 V得到该位置在当前层的输出把该层所有 token 的 K/V 存入 KV CacheQ 用完即弃不长期保存。最后一层最后一个位置的输出经过 LM Head得到词表上的概率分布采样出第一个新 token。5.2 Decode 阶段生成第一个新 token 后例如“”“”作为新 token 进入模型在每一层它根据自己的输入算出自己的 Q/K/V它的 Q 查询该层 KV Cache 中所有历史 K用注意力权重加权所有历史 V得到该层输出传给下一层最后一层输出经过 LM Head预测再下一个 token把“”在该层的 K/V 追加到 KV Cache。然后循环。因此旧 token 的 Q 用完就丢旧 token 的 K/V 被缓存新 token 会自己算新的 Q。KV Cache 里只有 K 和 V没有 Q。六、KV Cache 缓存了什么KV Cache 缓存的是每一层、每个历史 token 的 Key 和 Value。以 32 层模型为例第 1 层到第 32 层各自维护自己的历史 K/V。生成新 token 时它会在每一层用自己的 Q 查询该层的历史 K并加权该层的历史 V。KV Cache 的显存占用通常与层数、注意力头数、序列长度、每个头的维度、精度等因素有关。序列越长KV Cache 越大这也是长上下文推理昂贵的重要原因之一。七、缓存命中是什么在 LLM 服务中“缓存命中”通常不是指单次生成中的 KV Cache 使用而是指跨请求复用相同前缀的 KV Cache也称为 Prefix Caching 或 Prompt Caching。例如请求 A系统提示 S 用户问题 U1请求 B同一个系统提示 S 用户问题 U2。如果 S 完全相同请求 A 在 prefill 阶段已经把 S 在每一层的 K/V 算出来并缓存。请求 B 就可以直接复用 S 的每层 K/V只计算 U2 新增部分的 Q/K/V。命中后不需要重新 prefill S不需要重新计算 S 的 Q/K/V首 token 延迟 TTFT 明显降低吞吐提升重复前缀的 prefill 算力被节省。但要注意缓存命中复用的是前缀的每层 K/V不是 Q不是回答文本也不是隐藏状态本身。对 decode 阶段的 TPOT 帮助有限因为每步仍然要读取全部 KV。Prefix cache 通常要求从第一个 token 开始连续匹配常见条件包括tokenizer 相同、模型权重和版本相同、chat template 相同、位置编码与精度一致、前缀 token ID 完全一致、缓存分块对齐、缓存未过期等。工程上建议将固定内容放在前面动态内容放在后面保持模板和工具顺序稳定以提高命中率。八、综合实例假设一个简化模型只有 2 层。有两个请求请求 A系统提示 S 用户问题 U1请求 B同一个系统提示 S 用户问题 U2。其中S “你是严谨的助手” U1 “什么是 KV Cache” U2 “什么是注意力”8.1 请求 A第一次 prefill输入 token 序列你是 严谨 的 助手 什么 是 KV Cache 第 1 层对每个 token用 embedding 得到初始隐藏状态 (h^0)用第 1 层的 (W_Q1、W_K1、W_V^1) 算出每个位置的 (q1、k1、v^1)在因果注意力下每个位置的 Q 只能查询自己及之前的 K用注意力权重加权对应的 V得到第 1 层每个位置的输出 (h^1)把第 1 层所有 token 的 (k1、v1) 存入 KV Cache丢弃 (q^1)。第 2 层把第 1 层所有位置的输出 (h^1) 作为输入。第 2 层不直接使用第 1 层的 K/V而是用自己的 (W_Q2、W_K2、W_V^2)根据 (h^1) 重新计算 (q2、k2、v^2)再做注意力得到第 2 层每个位置的输出 (h^2)。把第 2 层所有 token 的 (k2、v2) 存入 KV Cache丢弃 (q^2)。预测第一个新 token取最后一层、最后一个位置的输出 (h^2_{\text{}})经过 LM Head[\text{logits} h^2_{\text{}} W_{LM}]再 softmax得到词表概率分布采样出第一个回答 token。Decode 循环新 token 进入模型第 1 层算自己的 (q1、k1、v1)(q1) 查第 1 层缓存中的历史 K加权历史 V输出 (h^1)把新 (k1、v1) 追加到第 1 层缓存。第 2 层拿第 1 层输出 (h^1)算自己的 (q2、k2、v2)(q2) 查第 2 层缓存中的历史 K加权历史 V输出 (h^2)把新 (k2、v2) 追加到第 2 层缓存。最后一层输出经过 LM Head预测下一个 token。8.2 请求 B缓存命中请求 B 的输入是 S U2。如果 S 的 token 序列与请求 A 完全一致并且满足 tokenizer、模型、模板、分块对齐等条件那么 Prefix Cache 可以命中。命中后直接复用请求 A 中 S 在第 1 层的 (k1、v1)以及在第 2 层的 (k2、v2)。不需要重新 prefill S不需要重新计算 S 的 Q。只需要计算 U2 新增 token 在每一层的 Q/K/V。U2 的 Q 会查询 S 的缓存 K 以及 U2 内部的历史 K。最后一层最后一个位置输出经过 LM Head预测回答。命中的是 S 在每一层的 K/V不是 S 的 Q不是隐藏状态也不是回答文本。8.3 实例中的关键点回顾通过这个例子可以清晰地看到输入 token 的 K/V 对同一模型和输入是确定的但它们是运行时算出的不是预定义的。Q 用于查询这些 K/V没有 Q 就无法决定从哪些历史 token 取多少信息。输入 token 的 Q 不是用来直接预测下一个 token而是用来计算该 token 在当前层的上下文表示。每个位置都要更新自己的表示下一层才能继续处理。每一层都需要所有位置的输出因为下一层要根据这些输出重新计算自己的 Q/K/V。如果只算最后一个位置下一层就缺少其他位置的输入。层间传递的是隐藏状态下一层用自己的权重重新计算 Q/K/V而不是直接使用上一层的 K/V。第一个新 token 来自最后一层最后一个位置的输出经过 LM Head 和采样得到。旧 Q 丢弃不影响新 token 进入模型后会自己计算新的 Q/K/V。缓存命中复用相同前缀在每一层的 K/V跳过的是重复前缀的 prefill 计算。九、常见误解误解KV 是系统提前定义好的。纠正提前定义的是模型权重KV 是运行时根据输入动态计算的。误解每个输入 token 只算最后一个。纠正每一层所有位置都要算输出最后一层最后位置才用于预测下一个 token。误解下一层直接使用上一层的 K/V。纠正下一层拿到上一层所有位置的输出用自己的权重重新计算下一层的 Q/K/V。误解Q 也需要缓存。纠正Q 只用于当前层当前 token 的输出用完即弃。未来 token 只查询历史 K/V。误解缓存命中命中 Q 或回答。纠正Prefix cache 命中通常指复用前缀在每一层的 K/V。误解输入 token 的 Q 用来预测下一个 token。纠正预测下一个 token 只用最后一层最后位置的输出输入 token 的 Q 用于计算它自己在该层的上下文表示。十、总结模型参数是提前训练好的Q/K/V 是运行时根据输入和已生成 token 动态算出的。每一层都计算所有位置的输出层间传递隐藏状态下一层重新计算自己的 Q/K/V。Q 用完即弃K/V 被缓存供未来新 token 的 Q 查询。预测下一个 token 只用最后一层最后位置的输出。缓存命中指的是跨请求复用相同前缀在每一层的 K/V从而跳过重复 prefill降低首 token 延迟并提升吞吐。