Skip to content

约 7 分钟阅读

03. KV Cache:用显存换计算 ​

上一篇看到,Decode 每一步都要把新 token 接到输入末尾,再算一遍模型。如果每一步都把整段历史从头算一遍,生成越往后越慢。这一篇解释模型每一步到底要用到历史的什么信息,为什么可以把算过的部分存起来复用,以及为什么只存 K 和 V。读完你应该能说清 KV Cache 省掉了什么、没省掉什么,以及它为什么会成为显存里最难管的那一块。

注意力:新 token 怎样“回头看” ​

先补一点必要的背景。Transformer 模型由几十层相同结构的模块叠成(Llama-3.1-8B 有 32 层)。每一层里有一步叫注意力(attention),作用是让当前位置的 token 参考前面所有 token 的信息。比如在“年假怎么申请?先”之后预测下一个词,模型需要回头看“申请”“年假”这些词,才知道接“提交”比较合理。注意力之后还有一步前馈网络(常简称 MLP),对每个位置各自做两三次大的矩阵乘法,模型的大部分参数在这里。每一层算出来、交给下一层的中间结果,叫作激活(activation)。

注意力的做法是:每个 token 在每一层都会算出三组数字(向量),分别叫 Q(Query,查询)、K(Key,键)和 V(Value,值)。可以用查资料来理解:

  • 当前 token 拿着自己的 Q 去问:“我需要什么信息?”
  • 每个历史 token 用自己的 K 回答:“我这里有什么?”Q 和每个 K 比一比,得到一组相关程度的分数。
  • 分数换算成权重以后,按权重把各个历史 token 的 V 加起来,就是当前 token 从历史里取到的信息。

Q、K、V 都是模型用参数算出来的数值,不是人工编写的关键词。这个类比只帮助记住它们的分工:Q 用来提问,K 用来匹配,V 是被取走的内容。

注意力的公式
text
Attention(Q, K, V) = softmax(Q·Kᵀ / √d + 因果遮罩) · V

Q·Kᵀ 算出当前位置与每个历史位置的相关分数,除以 √d(d 是每个头的维度)让数值稳定,softmax 把分数变成和为 1 的权重,再乘以 V 做加权求和。因果遮罩保证每个位置只能看到它之前的位置,看不到“未来”。实际模型会把 Q、K、V 拆成多个“头”并行计算,每个头关注不同方面的信息。

不缓存的话,要重复算多少 ​

回到 Decode。生成第 N 个 token 时,新 token 的 Q 要和前面所有 token 的 K、V 做注意力。问题是:前面那些 token 的 K、V,上一步不是已经算过了吗?

如果什么都不存,每生成一个新 token,就要把整段历史重新送进模型,在每一层重新算出所有历史 token 的 K、V。算一笔账:输入 500 个 token,生成 200 个 token。

  • 不缓存:第 1 步处理 500 个位置,第 2 步处理 501 个,……第 200 步处理 699 个,一共要处理 500 + 501 + … + 699 = 119,900 个位置。
  • 缓存:Prefill 处理 500 个位置并把它们的 K、V 存下来;之后每一步只处理 1 个新位置,再算 199 步,一共 699 个位置。

两者差了约 170 倍,而且输出越长差距越大:不缓存时要处理的位置总数随长度平方增长,缓存后只随长度线性增长。

不缓存时每一步都重算全部历史,缓存后每一步只为新 token 计算 K 和 V

KV Cache 的核心思想就是:把已经算过的 K 和 V 存起来,下一步直接拿来用,不再重算。 这是典型的“用空间换时间”:多占一些显存,换掉大量重复计算。

下面对照有无缓存时,每一步要新建立多少份 K/V:

年假怎么申请?先提交读题新算新算新算新算——接着写复用复用复用复用新算—再写复用复用复用复用复用新算
本轮计算已有状态

三轮共建立 4 + 1 + 1 = 6 个位置的 K/V。历史便笺保留下来,只为新位置建立状态。

词块仅为教学 token。这里只数 K/V 位置的重复建立;每个新位置仍要查询历史,表格不代表整体 FLOP 或加速倍数。

为什么只存 K 和 V,不存 Q ​

注意到一个不对称:每一步,只有当前这个新 token 需要提问,所以只用到它自己的 Q;而所有历史 token 的 K 和 V 每一步都要被查询一遍。历史 token 的 Q 只在它们自己作为“当前 token”的那一步用过,之后再也用不到。

所以缓存的是 K 和 V,这也是 “KV Cache” 名字的由来。

保存历史计算便笺后,新位置只建立自己的K/V,仍然查询历史

缓存成立还有一个前提:历史 token 的 K、V 不会因为后面来了新 token 而改变。因果遮罩保证了这一点,每个位置只依赖它之前的内容。反过来,如果历史内容变了,哪怕只改了开头一个字,后面所有位置的 K、V 都要重算。第 10 篇讲跨请求共享缓存时,这个性质会非常关键。

KV Cache 没有省掉什么 ​

KV Cache 省掉的是“重新计算历史 K、V”,没有省掉“查询历史”。每一步 Decode,新 token 的 Q 仍然要和所有历史 K 比较、对所有历史 V 加权求和。上下文有 8000 个 token,每一步就要读 8000 个位置的 K、V,而且是 32 层每层都读。

这带来两个后果:

  1. 上下文越长,每一步 Decode 越慢。 同一个模型,带着一整本手册聊天,出字速度会比空白对话慢。
  2. KV Cache 越大,每一步要从显存读取的数据越多。 第 6 篇会看到,Decode 的瓶颈本来就是读取速度,KV 的读取会和模型参数的读取叠加在一起。

所以说“有了 KV Cache,生成就从平方复杂度变成线性”需要说清楚是哪一部分。变成线性的是需要重新计算的位置数:每一步从“整段历史”降到“1 个新 token”,这部分是模型计算的大头(每一层的大矩阵乘法)。注意力这一部分,第 N 步仍要读 N 个历史位置,生成 n 个 token 的注意力总工作量仍随 n² 增长,只是每一步的量从“重算 N 个位置的全部计算”降到了“读 N 个位置的 K、V”。上下文不太长时,前一部分占绝对多数,KV Cache 的效果非常明显;上下文到几万、几十万 token 时,注意力这一部分会越来越重要,这也是第 12 篇的注意力算子要专门优化 Decode 的原因。

一个请求的 KV Cache 怎样产生和消失 ​

一个请求的 KV Cache 会经历四个阶段:

KV Cache 的生命周期:分配、填充、使用、释放

  1. 分配:请求进来,推理引擎为它预留存放 KV 的显存。
  2. 填充:Prefill 一次性算出整段输入的 K、V 写进去;之后 Decode 每一步再追加 1 个 token 的 K、V。
  3. 使用:每一步 Decode 都要读取全部历史 K、V 来做注意力。
  4. 释放:生成结束(遇到结束符、达到长度上限或用户取消),显存归还,给别的请求用。

KV Cache 属于一个请求,而不是一段对话。多轮聊天时,第二轮是一个新请求,它的输入里带着第一轮的全部内容。第一轮的 KV 能否被第二轮接着用,取决于引擎是否开启了前缀缓存(第 10 篇),并且那些块还没有被回收;没有命中,第二轮就要把历史重新 Prefill 一遍。应用层“保持会话”不等于显存里一直留着这段对话的 KV。

这个生命周期里藏着一个麻烦:分配时不知道这个请求最终会生成多长。 预留少了,生成到一半不够用;按最大长度预留,大部分请求根本用不完,显存就被白白占着。第 8 篇的 PagedAttention 正是为了解决这件事。

训练时为什么用不上 ​

KV Cache 是推理阶段的技术,训练时一般不用。训练用的文本是完整已知的,模型要同时学会“根据前 1 个词预测第 2 个、根据前 2 个词预测第 3 个……”。因果遮罩保证每个位置只看前面的内容,于是一次前向就能并行算出所有位置的预测,和 Prefill 的计算方式相同,不存在“一步一步生成”的过程,也就没有需要跨步保存的 K、V。

只有训练流程里需要模型自己生成内容的环节才会用到它,比如强化学习中让模型先作答、再根据结果打分的采样阶段。这部分本质上就是推理,通常直接调用推理引擎来完成。

KV 占用太大时有哪些办法 ​

KV Cache 用显存换来了速度,长上下文和高并发时,它往往是第一个耗尽显存的东西。后面几篇讨论的办法,可以按它们动的是哪个环节分成四类:

思路做法代价章节
每个 token 存得更少GQA、MQA、MLA 让多个头共用或压缩 KV;滑动窗口只保留最近的部分需要模型结构本身支持第 4 篇
每个数存得更小KV 量化,从 16 位降到 8 位甚至更低精度误差,需要验证质量第 14 篇
少浪费PagedAttention 按块分配,消除预留和碎片读取时多一次查表第 8 篇
多人共用前缀缓存,相同的系统提示、资料只存一份只对开头完全相同的部分有效第 10 篇
暂时挪走把暂时不用的 KV 卸载到 CPU 内存或磁盘,需要时再搬回;或者干脆释放、以后重算搬运或重算的时间第 9、10 篇

前三类让同样的显存装下更多 token,第四类用别的存储或计算时间换显存。实际部署往往同时用好几种。

KV Cache 把历史 token 的 K、V 存下来,让 Decode 每一步只为新 token 做大部分计算,需要重新计算的位置数从平方级降到线性;代价是显存,每一步仍要读取全部历史,KV 属于单个请求,跨轮复用要靠前缀缓存。下一篇算一笔具体的账:一个请求的 KV Cache 到底占多少显存,一张 80 GB 的卡又能同时装下多少个长对话。

别急,先让缓存热一下。