18 · 推理 · 8 min

为什么第2个 token 比第1个快

KV 缓存与自回归生成。Prefill vs decode、TTFT,以及为什么缓存改变了一切。

响应时间的错觉

你向 ChatGPT 提一个问题。它停顿大约一秒后才开始回答。然后字几乎是瞬间一起冒出来的,比你阅读还快。

这种不对称不是界面上的小心思。它是一项基础优化的指纹——没有这项优化,用 LLM 生成文本的成本会是现在的一百倍:KV cache(KV 缓存)

一个 Transformer 如何生成一个 Token

在每一步生成里,Transformer 都要产出一个新 Token。为此,它要计算最新这个 Token之前所有 Token注意力。这正是它能把整个上下文都纳入考虑的方式。

但注意力对上下文里的每个 Token 都需要两个向量:一个键(K)和一个值(V)。如果不做任何优化,每生成一个新 Token,模型都要重新为整个序列计算 K 和 V——包括上一步已经处理过的那些 Token。这是一个相对于序列长度 O(n²) 的工作量:Token 数量翻倍,开销就乘以四。

而这是白做的:那些向量根本没变。第 3 个 Token 的键 K₃ 和上一步还是同一个。

KV cache:永远不重新计算已经有的东西

这个想法既简单又决定性。把每个已经处理过的 Token 的 K 和 V 都保留在 GPU 内存里。每一步新生成时,计算新 Token 的 K 和 V,并把它们追加到缓存里。

注意力随后读取整个缓存,但这一步要做的工作是 O(1) 大小——而不是 O(n)。

没有缓存时,每个新 token 都要在前缀上重新计算注意力——开销 O(n²)。有了缓存,只需计算新的一行。这就是第一 token(慢,prefill)与第二 token(快,decode)之间的区别。

左边,没有缓存:每一步都重画所有的行。右边,有缓存:只是加一行。几个 Token 之后,累计运算量的差距就变得非常大。

Prefill 与 decode:两个截然不同的阶段

LLM 的生成被切成两个阶段,做推理工程的人会非常仔细地把它们区分开来。

**Prefill(预填充)。**模型接收完整的提示词,对它的所有 Token 并行计算 K 和 V。从吞吐量角度看是快的——GPU 被打满了——但提示词长的话还是要花时间。这决定了 TTFT(Time To First Token,首 Token 时延),也就是第一个字出现之前的延迟。

**Decode(解码)。**模型一次一个 Token 地生成剩下的部分,复用缓存。每一步单独看是很快的,但是顺序的:未来的 Token 没办法并行,因为每一个都依赖前一个。这决定了 ITL(Inter-Token LatencyToken 间时延)。

这两个阶段的画像完全不同:

PrefillDecode
可并行?是(所有 Token 一次过)否(顺序的)
硬件瓶颈算力(FLOPs内存(读缓存)
长度的影响关于 N 是线性的每个 Token 关于 N 是线性的
关键指标TTFTITL

在长提示词上,prefill 可能要花好几秒。在长输出上,是 decode 占主导——而它的瓶颈是 KV cache 从 GPU HBM 内存里被读取的速度。

为什么各家厂商对"输入 Token"定价不同

如果你看 OpenAI、Anthropic 或 Google 的价格表,输入 Token 系统性地比输出 Token 便宜——常常便宜 4 到 5 倍。这不是任意定的。处理输入 Token 的 prefill 是大规模并行的,能高效利用 GPU。decode 则是一个一个地生成输出 Token,对硬件的利用率很低。

更微妙的一点:Anthropic、OpenAI 等现在都提供了 prefix caching(前缀缓存)。如果许多请求都共享同一个 system prompt,那这个前缀的 KV cache 就只算一次,然后被复用。这正是让智能体和多轮对话机器人在经济上可行的原因:没有前缀缓存的话,每一轮都要重新处理整段对话。

隐藏成本:GPU 内存

KV cache 在内存上不是免费的。它占用:

内存 = 2 × n_layers × n_heads × d_head × seq_len × batch_size × 2 字节(FP16)

对于一个 700 亿参数的模型、128,000 Token 上下文、批大小 1,这意味着几十 GB。这往往就是限制实际可用上下文长度的因素,比起模型自身能否在长序列上推理,这反而更卡脖子。

要再往前推进,存在好几种技术:

  • Cache quantization(缓存量化):把 K 和 V 用 INT8 或 INT4 而不是 FP16 来存,内存除以 2 或 4。
  • MQA / GQA(Multi-Query / Grouped-Query Attention):在多个 head 之间共享 K/V。Llama 2 70B 和 Llama 3 都用了 GQA,缓存大小被大幅压缩。
  • Sliding window attention(滑动窗口注意力):只保留缓存里最近的一段窗口(Mistral、Gemma)。
  • PagedAttention(vLLM):把缓存当成虚拟内存的页来管理,以更好地处理动态批处理。

量化,简单两句话

这个词在这一部分里到处都是:4 bit 的 QLoRA(第 14 章)、上面刚说过的 cache quantization、你从 Hugging Face 下载的 GGUF 模型。该解释一下它到底是什么意思了。

量化(quantize)就是用更少的比特来表示模型的每个参数。一个 32 位浮点数(FP32)占 4 字节。FP16 占 2 字节。INT8 占 1 字节。INT4 占半个字节——权重占用的内存相比原始的 FP32 被压缩到八分之一。

精度字节 / 参数70B 模型占用
FP324280 GB
FP16 / BF162140 GB
INT8170 GB
INT40.535 GB
INT2(极端)0.2517.5 GB

诀窍在于:一个权重 0.237 在 INT4 里不会精确等于 0.237(INT4 一共只有 16 个可表示的值),它会被映射到一个离散网格上最接近的那个值。质量损失取决于模型和方法,但通常情况下:到 INT8 损失很小,到 INT4 损失有限(基准上几个百分点的下降),再往下损失就显著了。

现代的技术(GPTQ、AWQ、GGUF)并不会对所有权重一视同仁地量化——它们会在敏感的层上保留精度,在其它层上更激进地压缩。而上面提到的 KV cache 的量化,正是把同样的思路应用到生成期间存在内存里的激活上。

正是这项技术,让 Llama 70B 能在一台 64 GB 内存的 MacBook 上跑起来——而它的 FP16 版本需要一整个集群。

教训

没有 KV cache,LLM 在生产环境里根本没法用。一段长对话、一个会反复思考的智能体、一个能记住你五条消息之前说过什么的聊天机器人——这些在经济上没有一个能成立。

但这个缓存也是限制上下文长度的东西。当人们谈论"100 万 Token 的窗口"时,那在很大程度上是缓存内存问题,而不是注意力计算的问题。

KV cache 不是众多优化中的一个。正是它把注意力从一个理论机制变成了生产环境的基础设施。

更新于

KV cache:为何第二个词元比第一个快 · Step by Token