预填充、解码与 KV 缓存。
一次模型调用其实是两台瓶颈相反的机器,而你几乎所有的延迟疑问,在把它们分开的那一刻就迎刃而解了——为什么第一个令牌要等上一秒、其余却源源不断;为什么提示词翻倍几乎不影响生成速度;为什么提示词缓存这件事根本能成立。预填充一口气读完整个提示词,受限于算力;解码一次写一个令牌,受限于内存带宽。
两个阶段,两种瓶颈。
预填充把你的整个提示词——系统指令、工具定义、检索到的文档、对话历史——在一次并行前向传播中推过整个网络。它产出第一个输出令牌,更重要的是产出生成其余全部内容所需的内部状态。它是算力受限的:GPU 的运算单元是约束,而注意力的开销随提示词长度的平方增长。
解码随后一次生成一个令牌。每一步都必须把完整的模型权重和全部已存状态从内存中读出来,只为产出一个令牌,再把它喂回去进行下一步。它是内存带宽受限的:GPU 大部分时间花在搬运字节,而不是做运算。它同时也是严格串行的——第 n 个令牌不存在,第 n+1 个就无从开始。
- 首令牌时延(TTFT)基本上就是预填充。它随你发出去的量增长。
- 令牌间时延就是解码。它每个令牌大致恒定,而且几乎不在乎你的提示词有多长。
- 总墙钟时间 ≈ TTFT +(输出令牌数 × 令牌间时延)。两个彼此独立的项,两套完全不同的解法。
「模型很慢」不是一个诊断。等两秒然后流畅吐字的请求,和瞬间开始、却断断续续挤三十秒的请求,除了总时长之外毫无共同之处。前者是上下文问题,后者是输出长度问题。没有哪次换模型能可靠地同时解决两者。
KV 缓存就是令牌之间的那份状态。
注意力要求每个令牌回看之前的每一个令牌——具体说是回看它们的键(key)与值(value),也就是每一层为每个令牌算出的两个向量(参见 Transformer)。若在每个生成步都为整段历史重算一遍,代价将无法承受,于是它们只算一次并被保留下来。那份存储就是 KV 缓存,而预填充真正的产物,正是一份填满了的 KV 缓存。
- 它随序列长度线性增长——每多一个令牌,无论来自你还是来自模型,都会在这次请求的余下时间里永久占据一行。
- 它是按请求独立的。十个并发对话意味着十份缓存同时驻留在显存里。服务大量用户,很大程度上是 KV 缓存容量问题,而不是权重问题。
- 它解释了本地模型上一个常见的意外。权重明明塞得进显存,长上下文时却依然爆内存,因为溢出的是缓存——这也是为什么小模型与本地模型常常受限于上下文,而非参数量。
- 长上下文的代价是内存,不只是令牌。这就是「20 万令牌窗口」并非配置文件里换个更大数字那么简单的物理原因。
这一个划分能解释的事。
若干看似无关的现象,其实是同一个事实的不同侧面:
- 为什么提示词缓存是前缀匹配。缓存一段提示词,意思是把预填充产出的 KV 缓存存下来、下次直接复用。由于一个令牌的键与值取决于它之前的每一个令牌,复用必须从第 0 位开始,并在第一处差异停止——靠前的一个字节变了,其后的一切随之失效。这条约束不是厂商政策,它直接从机制里长出来。
- 为什么输出令牌比输入令牌贵好几倍。输入在一次被摊薄的并行传播中处理完;而每一个输出令牌都要独占一次遍历整个模型的传播。价差就是工作量之差。
- 为什么服务端把多个用户合批几乎白赚吞吐。解码受限于带宽,因此权重加载一次就能在同一步里推进多条序列。这也解释了为什么提供方扩容时,你单条流的速度并不会变快。
- 为什么推理模型慢得与答案长度不成比例。思考令牌就是解码令牌,一个一个按全价生成,而其中多数从不展示给你——参见推理模型。
- 为什么流式输出根本能成立。令牌确实是逐个变得可用的;流式输出是把解码循环暴露出来,而不是在模拟进度。
去优化真正慢的那一半。
一旦你能叫出是哪个阶段,各种手段就不再互相打架:
- 预填充为主——长的检索上下文、短的答案。缓存稳定前缀、砍掉检索文档、把易变内容放到末尾以保住可缓存区域。少发一点,是唯一可靠有效的做法。
- 解码为主——长篇生成、推理、啰嗦的格式。要求更少的输出:用 schema 而不是散文,用表格而不是叙述,把思考预算调低。输出长度是一个延迟旋钮,而多数团队从来没碰过它。
- 吞吐受限——固定硬件上有大量并发用户。给单请求上下文设上限,因为并发量受 KV 内存约束,你每放行一个长上下文请求,代价都是好几个短请求。
- 两者皆非——如果等待来自排队而非上述任一阶段,那么上面全都不相干,答案是加容量。
在争论换个更快的模型之前,先拿一个真实请求,把 TTFT 和令牌间时延分开测。若 TTFT 占主导,你的课题是上下文与缓存,换模型只会让你失望;若生成占主导,你的课题是让它少输出。这只是一次测量,却指向两件截然不同的工作——这也正是跳过它的团队为何总在优化那本来就快的一半。