前几篇我们把注意力怎么算、Transformer 怎么搭都拆开了,但你有没有想过一个问题:模型在训练时是一次性处理整段序列,可到了生成阶段,它是 一个词一个词往外蹦 的——你每次打开 ChatGPT,答案都是流式吐出来的。那每蹦一个词,模型是不是要把整段输入从头到尾重算一遍?今天这篇就回答这个:KV cache 如何消除推理中的重复计算,前缀缓存又如何让多个请求共享同一段开头。
本篇问题边界
只讲推理阶段的两个提速机制:KV cache 与前缀缓存。FlashAttention、GQA、PagedAttention 这些更深的工程优化只点到名字,放在「下一步」。
生成一个词,为什么是重复劳动
回顾 QKV 那篇:每个 token 都算出自己的 、、,用 去点积所有 得到权重,再加权所有 。推理时,模型把新 token 接到序列末尾,走一遍前向,取最后一个位置的输出作为下一个词。
关键在因果掩码:新 token 只能看它前面的 token。所以 旧 token 的表示不会因为新 token 的到来而改变——它们由自身和更早的内容决定。但最朴素的实现每步都把整段序列重新过一遍:所有 、 重算,整张注意力矩阵重算,只为取最后一行。算过的全被扔掉。
看这张图:追加 token 后,旧行一个都没动 2。
KV cache:把算过的 K、V 存起来
既然旧 token 的 、 永远不变,就别重算——算一次,存起来。这就是 KV cache 12。生成因此分成两阶段 3:
- prefill(预填充):把整段 prompt 并行算一遍,把每个 token 每层的 、 写进缓存;
- decode(解码):每步只算新 token 的 、、,把 、 追加进缓存,再用新 与缓存里全部 、 做注意力。
注意只有 、 被缓存, 不缓存—— 只服务于当前这一步,用完即弃 2。代价是显存:、 要一直存到序列结束,长上下文时它的体积会超过模型权重本身。
计算量对比:一个直觉数字
假设 prompt 有 1000 个 token,还要生成 100 个新词。朴素实现第 步要重新处理约 个位置,100 步合计约 10 万次 token 计算;用 KV cache 只需算 100 个新 token,投影部分省了约千倍 4。但注意:每一步新 仍要和缓存里全部 做点积,这一步扫描成本随序列变长线性增长——KV cache 消除了重复投影,没有消除注意力扫描 4。
前缀缓存:多个请求共享同一段开头
KV cache 是在一个请求内部省重复。再看请求之间:很多请求的开头完全一样——同一个 system prompt、同一段对话历史、同一批 few-shot 示例。同样一串 token 用同样的权重,算出的 、 必然逐位相同,重复算就是纯浪费。
前缀缓存(prefix caching)就是干这个的:把算过的 KV 按前缀组织起来,新请求先匹配公共前缀,命中的部分直接复用,只算新增的部分 56。两个主流实现:
- vLLM:把 KV cache 切成固定大小的块(如 16 个 token 一块),每块用「块内 token + 前面所有 token」的哈希做标识,多个请求共享同一哈希的块就指向同一块内存 5;
- SGLang:用 radix tree(基数树)按 token 粒度组织共享前缀,支持更细的分支复用 6。
类比:就像盖楼时共享地基——新楼不必重新挖地基。对话越长、system prompt 越长,缓存价值越大。注意前缀缓存只省 prefill、不省 decode 5,生成新词的部分该算还得算。
下一步
KV cache 引出了推理阶段最值钱的两个问题:注意力扫描怎么加速(FlashAttention)、缓存怎么省显存(GQA、PagedAttention)。这是「大模型如何跑得快」的下一站。