KV Cache 完全解析:大模型推理提速的第一功臣

上一篇《一次 LLM 推理的完整旅程》里反复出现一个词:KV Cache。Prefill 阶段产出它,Decode 阶段每一步都要读它,显存被它吃掉大半,PagedAttention、GQA、前缀缓存全都围着它转。

这一篇就把它彻底讲透:它缓存的到底是什么?为什么只缓存 K 和 V、不缓存 Q?它凭什么把推理速度提升一个数量级,又为什么成了显存的头号杀手?


一、问题的起点:自回归生成的重复计算

Transformer 大模型生成文字是逐字预测的:

  1. 输入一句话 → 输出第 1 个 token;
  2. 把原文 + 刚输出的第 1 个 token 再喂进去 → 输出第 2 个 token;
  3. 循环直到结束。

每一轮都要执行自注意力(Self-Attention):对所有历史 token 生成 Query、Key、Value 三组向量,用 Q 和所有 K 做点积打分,再用分数加权 V 得到输出。

问题来了:每生成一个新字,都要把全部历史 token 的 K、V 从头重算一遍。生成第 100 个 token 时重算前 99 个的 K/V,生成第 500 个时重算前 499 个——而这些值和上一轮算出来的一模一样。纯粹的重复劳动,越往后越卡,整段生成的注意力计算量随长度平方级增长。

KV Cache 的思路简单到近乎理所当然:算过的就存下来,别再算了。 每个新 token 只计算自己的 K、V,追加进显存里的缓存;历史 token 的 K、V 直接复用。一次乘法,终身受益。

就这一个改动,推理速度提升几倍到十几倍,成为所有线上大模型服务的必备优化——聊天机器人、代码补全、本地部署 LLM,只要是自回归生成,无一例外。


二、先补底层:Q、K、V 到底是什么

要理解"为什么只缓存 K 和 V",得先弄清这三个向量各自的分工。

从文字到向量

每个 token 先被转换成固定维度的词嵌入向量(embedding)——一个代表语义的数字数组。模型内置三组独立的可训练权重矩阵 WQW_QWQWKW_KWKWVW_VWV,把嵌入矩阵 XXX(全部历史 token 打包)分别投影成三套向量:

Q=XWQ,K=XWK,V=XWV Q = X W_Q,\quad K = X W_K,\quad V = X W_V Q=XWQ,K=XWK,V=XWV

一次矩阵乘法,每个 token 就有了自己的一组 (Q, K, V)。

三者的分工:一次数据库检索

最直观的类比是数据库查询:

  • Q(Query,查询向量):你的搜索关键词——代表当前正在计算的 token:“我是谁,我要找什么样的上下文”;
  • K(Key,键向量):数据库里每条记录的索引标签——每个历史 token 的特征摘要,专门用来被 Q 匹配;
  • V(Value,值向量):每条记录的完整正文——历史 token 携带的实际内容信息。

检索流程:拿关键词(Q)和所有索引(K)做内积打分 → 分数过 softmax 归一化成权重 → 按权重加权求和所有正文(V)→ 得到当前 token 的输出特征。

一个具体例子:上下文是「猫咪在窗边睡觉」,当前要处理的词是"它"。"它"的 Q 和"猫咪"的 K 点积分数最高,softmax 后"猫咪"的 V 占最大权重——模型由此知道"它"指代猫。K 负责打分匹配,V 负责提供内容,缺一不可、分工明确。

走一遍完整流程

上下文:[我, 今天, 吃, 了, 草莓],预测下一个字:

  1. 5 个 token 转成嵌入矩阵 X;
  2. X 分别乘 WQW_QWQWKW_KWKWVW_VWV → 各得 5 行 Q、K、V 向量;
  3. 最后一个 token「草莓」的 Q,和 5 个 K 逐一算相似度;
  4. 相似度加权融合 5 个 V → 输出特征 → 采样出下一个字(比如「很甜」)。

注意第 3 步:真正被用到的 Q 只有最后一个 token 的。这是全文最关键的伏笔。


三、为什么只缓存 K、V,不缓存 Q?

自回归生成时,我们只需要预测下一个 token。查询的主体永远是"最新的那一个 token"——它的 Q 去匹配全部历史的 K 和 V。

而历史 token 的 Q 呢?它们在各自"当过一次新 token"时用过一次,之后再也不会被任何计算引用。缓存它们没有任何意义。

反观 K 和 V:每个历史 token 的 K/V 在之后的每一步都要被新 token 的 Q 匹配、加权——它们才是被反复读取的"资产"。

一句话:Q 是一次性的查询请求,K/V 是被反复查询的数据库。 所以缓存叫 KV Cache,而不是 QKV Cache。


四、KV Cache 的工作流程

把缓存机制套进生成过程,对比一下:

没有 KV Cache(以「我今天吃了草莓」→ 续写「很甜」为例):

  • 生成「很」:把 5 个历史 token 全部重新过一遍 WKW_KWKWVW_VWV,再算注意力;
  • 生成「甜」:把 6 个 token(含刚生成的「很」)又全部重算一遍 K/V;
  • 每一步都从头来,上下文越长每步越慢。

有 KV Cache

  • Prefill 阶段:5 个输入 token 一次并行算出 5 组 K/V,存入显存缓存;
  • 生成「很」:只算「很」自己的 1 组 K/V,追加进缓存;注意力直接用缓存里的 6 组 K/V;
  • 生成「甜」:只算「甜」的 1 组,复用前面 7 组;
  • 历史 token 的 K/V 全程零重算。

每一步的计算量从"正比于上下文长度"降到"一个 token 的常数量",整段生成的注意力计算从平方级降回线性级。

补充:多头注意力下也一样

实际大模型用的是多头注意力(Multi-Head Attention):把 WQ/WK/WVW_Q/W_K/W_VWQ/WK/WV 拆成多组独立的头,每头各自生成一套 QKV、单独做匹配,最后拼接融合——相当于多路并行检索,捕捉更细粒度的语义关联。KV Cache 的逻辑完全不变,只是每层要为每个头都存一份 K/V。这个"每层 × 每头"的乘法,正是下一节显存账单的来源。


五、天下没有免费的午餐:显存账单

KV Cache 的本质是用显存换算力。省下的计算实实在在,付出的显存也实实在在。算一笔账:

每个 token 需要缓存的字节数 =

2×层数×KV头数×每头维度×每元素字节数 2 \times \text{层数} \times \text{KV头数} \times \text{每头维度} \times \text{每元素字节数} 2×层数×KV头数×每头维度×每元素字节数

(开头的 2 = K 和 V 各一份。)

以 Llama-3-70B 为例:80 层 × 8 个 KV 头 × 128 维 × 2 字节(FP16),乘上 K 和 V 两份:

2×80×8×128×2≈327,680 字节≈320KB / token 2 \times 80 \times 8 \times 128 \times 2 \approx 327{,}680 \text{ 字节} \approx 320\text{KB / token} 2×80×8×128×2327,680 字节320KB / token

  • 一条 8K token 的对话 → 约 2.6 GB 显存,只为这一条请求的缓存;
  • 128K 长上下文 → 约 40 GB——比很多整卡显存还大;
  • 线上服务同时挂几十条并发,KV Cache 轻松超过模型权重本身的占用。

由此得出 KV Cache 的两大缺点:

  1. 显存占用随长度线性暴涨:万字长文、超长对话很容易把显存顶爆(OOM),并发量直接受限于缓存能塞下多少;
  2. 越长越慢:decode 每一步都要把整个缓存从显存读出来参与注意力计算,上下文越长,每步搬运的数据越多——这正是上一篇说的 memory-bound 瓶颈的一部分,长对话"越聊越慢"的直接原因。

六、围绕 KV Cache 的优化生态

正因为 KV Cache 同时是速度的功臣和显存的杀手,围绕它长出了一整套优化技术。按思路分四类:

1. 管得更好:PagedAttention

传统做法为每条请求预留一整段连续显存(按最大长度预留),实际用不满,碎片浪费惊人。PagedAttention 借鉴操作系统的内存分页:把 KV Cache 切成固定大小的小块(block),按需分配、离散存放,用"页表"记录逻辑顺序。显存碎片几乎归零,同一张卡能塞下数倍的并发请求。这是 vLLM 的成名作,如今 SGLang、TensorRT-LLM 等主流框架均已标配。

2. 存得更小:量化与结构压缩

  • KV Cache 量化(INT8/INT4):K/V 张量从 FP16 降到低精度存储,显存减半甚至减至 1/4,精度损失轻微;
  • MQA / GQA / MLA:从模型结构上砍 KV 头数——MQA 所有头共用一组 K/V,GQA 分组共用(Llama 系的选择,70B 从 64 个 Q 头压到 8 个 KV 头,缓存直接缩到 1/8),DeepSeek 的 MLA 更进一步把 K/V 压成低秩隐向量。头数少了,上面公式里的乘数就小了。

3. 丢得聪明:滑动窗口

超长对话只保留最近 N 个 token 的 K/V,久远历史直接丢弃,把显存占用钉在一个固定上限内(Mistral 等模型采用)。代价是模型"记不住"窗口之外的内容,适合只依赖近期上下文的场景。

4. 用得更值:前缀缓存与投机解码

  • Prefix Caching(前缀缓存):请求结束后不立即释放缓存,相同前缀(system prompt、多轮对话历史)的下一条请求直接复用,跳过大部分 prefill——API 厂商"缓存命中的输入便宜 10 倍"就是它;
  • 投机解码(Speculative Decoding):小模型先猜几个 token、大模型一次并行验证,验证过程同样离不开 KV Cache 的支撑,进一步压低单 token 的生成耗时。

七、总结

把整篇压缩成五句话:

  1. 自回归生成会反复重算历史 token 的 K/V,KV Cache 用"算过就存"把平方级计算降回线性;
  2. 只缓存 K/V 不缓存 Q,因为 Q 是一次性的查询、K/V 是被每一步反复读取的数据库;
  3. 代价是显存随上下文线性暴涨(70B 级模型约每 token 数百 KB),长对话越聊越慢、越聊越占;
  4. PagedAttention 管碎片、量化和 GQA/MLA 压体积、滑动窗口设上限、前缀缓存复用成果——现代推理引擎的半壁江山都在伺候这块缓存;
  5. 理解了 KV Cache,就理解了大模型推理为什么快、为什么贵、以及优化的主战场在哪里。

它可能是整个大模型推理栈里性价比最高的一个 idea:一行"存下来别重算"的朴素直觉,撑起了今天所有线上 LLM 服务的可用性。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐