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 + 因果遮罩) · VQ·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 倍,而且输出越长差距越大:不缓存时要处理的位置总数随长度平方增长,缓存后只随长度线性增长。
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” 名字的由来。
缓存成立还有一个前提:历史 token 的 K、V 不会因为后面来了新 token 而改变。因果遮罩保证了这一点,每个位置只依赖它之前的内容。反过来,如果历史内容变了,哪怕只改了开头一个字,后面所有位置的 K、V 都要重算。第 10 篇讲跨请求共享缓存时,这个性质会非常关键。
KV Cache 没有省掉什么
KV Cache 省掉的是“重新计算历史 K、V”,没有省掉“查询历史”。每一步 Decode,新 token 的 Q 仍然要和所有历史 K 比较、对所有历史 V 加权求和。上下文有 8000 个 token,每一步就要读 8000 个位置的 K、V,而且是 32 层每层都读。
这带来两个后果:
- 上下文越长,每一步 Decode 越慢。 同一个模型,带着一整本手册聊天,出字速度会比空白对话慢。
- 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 的显存。
- 填充:Prefill 一次性算出整段输入的 K、V 写进去;之后 Decode 每一步再追加 1 个 token 的 K、V。
- 使用:每一步 Decode 都要读取全部历史 K、V 来做注意力。
- 释放:生成结束(遇到结束符、达到长度上限或用户取消),显存归还,给别的请求用。
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 的卡又能同时装下多少个长对话。
