什么是 KV cache?
生成第 500 个 token 不该需要把第 1 到第 499 个 token 重算一遍。避免这件事的缓存,以及它转而花掉的内存。
生成是一个循环:预测一个 token,把它追加到序列末尾,再拿变长的序列跑一遍模型。
按字面这么干,浪费巨大。要产出第 500 个 token,就得把第 1 到第 499 个 token 的全部计算重做一遍——这些活已经干过 499 次了。
KV cache 消除了这份重复,而搞懂它为什么成立,也就解释了长上下文推理为什么贵——大部分原因都在这里。
冗余在哪
在自注意力里,每个 token 都从自己的向量算出 query、key 和 value。
因果掩码让每个 token 只能看到自己和更早的位置。第 3 个 token 永远看不到第 4 个。所以第 3 个 token 的 key 和 value 不依赖它之后的任何东西——序列是 10 个 token 还是 1 万个 token,它们都一样。
于是:一个 token 的 key 和 value 一旦算出来,就再也不会变。
整个洞见就这一句。把它们存下来。
两个阶段
有了缓存,生成就分成两个性能特征截然不同的操作。
预填充(prefill)。 一次性处理整个输入 prompt,为每个位置算出 key 和 value 并存储。这一步高度并行——所有位置同时算——所以摊到每个 token 上很快。它也是成本随 prompt 长度平方增长的那个阶段,因为每个 token 都要关注其他所有 token。
解码(decode)。 对每个新 token:只算它自己的 query、key 和 value,与缓存中所有历史位置的 key 和 value 做注意力,再把新的这一对追加进缓存。工作量是一个 token 的量,而不是整个序列的量。
这就是为什么长 prompt 的首个 token 出得慢,之后却流式吐得很快。预填充是一大批工作,解码则是一个快速的逐 token 循环。服务商把这两者分开计价和上报——首 token 延迟量的是预填充,每秒 token 数量的是解码。
代价是什么
缓存拿计算换内存,而换来的内存开销相当可观。
它的大小是这些量的乘积:序列长度、层数、注意力头数量、每个头的维度、2(key 和 value),以及每个数值占的字节数。
这几项每一项都是乘数。大模型上的长上下文,所需的 KV cache 可能比模型自己的参数还大。做服务化时,这通常才是硬约束——不是模型多大,而是 cache 大小乘以并发用户数。
由此直接引出两个后果。长上下文在推理时很吃内存,即使预填充的成本已经付过了。同时服务很多用户意味着要同时持有许多份 cache,这就是为什么服务商设的上下文上限,远低于架构理论上能支撑的水平。
压力是怎么被缓解的
缓存成为瓶颈,实实在在地推动了架构上的改变。
分组查询注意力(grouped-query attention) 让头分小组共享 key 和 value,而不是各自维护一份。cache 大小按分组倍数下降,质量只掉一点。多查询注意力(multi-query attention) 是极端情形:所有头共享同一套 key 和 value。当前大多数大模型用的都是这两种之一。
分页注意力(paged attention) 借鉴了虚拟内存的做法,按固定大小的块分配 cache,而不是每个请求预留一整块连续空间。这样就消除了「为一个大多数请求根本到不了的最大长度而超额预留」造成的浪费,也正是它让服务系统能在同一批硬件上塞下更多并发请求。
量化 cache 用更低的精度存 key 和 value,拿一点精度换容量。
前缀缓存是你作为用户最容易看到的那一个。如果很多请求共享一段完全相同的前缀——同一个 system prompt、同一批检索到的文档——这段前缀的 key 和 value 可以只算一次然后复用。那些针对重复前缀降本的 prompt 缓存功能,靠的就是这个机制;也是为什么稳定的内容该放在 prompt 的开头,可变内容放在末尾。见砍掉 API 账单。
为什么多轮对话依然会变贵
缓存去掉的是单次生成内部的重复计算,去不掉重发对话历史的成本。
每一轮对话都要把完整记录重发一遍。没有前缀缓存,就意味着要对整段历史再付一次预填充的钱。有前缀缓存,共享的那段前缀便宜了,但不断增长的那截尾部不便宜。两种情况下,成本都随对话变长而上爬——缓存改的是常数,不是曲线的形状。
要点回顾
- 历史 token 的 key 和 value 不会变,因为因果掩码使它们不依赖后面的 token——所以可以缓存。
- 预填充并行处理整个 prompt;解码对着缓存一次生成一个 token。
- cache 大小随长度 × 层数 × 头数 × 头维度增长,常常超过模型自身的参数内存。
- 分组查询注意力、分页注意力、前缀缓存,都是为了缓解这个瓶颈而存在。
- 把稳定内容放在 prompt 开头,前缀缓存才有效。
下一篇: 长上下文为什么这么贵