第 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 Latency,Token 间时延)。
这两个阶段的画像完全不同:
| Prefill | Decode | |
|---|---|---|
| 可并行? | 是(所有 Token 一次过) | 否(顺序的) |
| 硬件瓶颈 | 算力(FLOPs) | 内存(读缓存) |
| 长度的影响 | 关于 N 是线性的 | 每个 Token 关于 N 是线性的 |
| 关键指标 | TTFT | ITL |
在长提示词上,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 模型占用 |
|---|---|---|
| FP32 | 4 | 280 GB |
| FP16 / BF16 | 2 | 140 GB |
| INT8 | 1 | 70 GB |
| INT4 | 0.5 | 35 GB |
| INT2(极端) | 0.25 | 17.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 不是众多优化中的一个。正是它把注意力从一个理论机制变成了生产环境的基础设施。
更新于