Decode
Decode
Decode 是自回归生成中真正“逐 token”的阶段:prefill 一次性消化提示词之后,decode 进入循环——读取历史状态、执行一次模型 forward、采样出下一个 token,再把 token 追加进上下文重复此过程,直至 EOS 或长度上限。每生成一个 token 就是一次完整的模型调用,这个结构性事实决定了它的一切性能特征。
为什么 Decode 是 memory bound
每个解码步的计算量很小——只有一个 token 参与 forward——但它需要把全部模型权重和整个历史的 KV Cache 从 HBM 读一遍。以 7B 模型为例,BF16 权重约 14 GB;生成一个 token 就要把这 14 GB 搬一遍,而搬来的每个权重只参与一次乘加。算术强度(FLOPs / bytes)极低,GPU 的 Tensor Core 大量闲置,时间几乎全花在数据搬运上。
这就是“Decode is memory bound”的完整因果链:串行依赖 → 每步单 token → 必须读全部权重和缓存 → 带宽成为瓶颈 → 算力利用率极低。这也是为什么 decode 吞吐的粗略上界可以用“带宽 ÷ 每步搬运字节数”估算,而不是用 FLOPs。
优化手段各自攻击链条的不同环节
理解了瓶颈链条,优化地图就不再是并列清单:
- 摊薄搬运成本:batching。一次权重搬运服务多个请求的同一解码步,把固定成本分摊。现代形态是 Continuous Batching。
- 减少搬运字节数:量化。压缩权重和 KV 的位宽直接降低搬运量;但收益取决于目标硬件有没有对应的低精度 kernel,以及反量化本身的开销(见 LLM Quantization Deployment)。
- 改写串行结构:speculative decoding。小模型起草 k 个 token,大模型一次 forward 并行验证,把 k+1 次串行 forward 合并为约一次——用验证计算换掉逐 token 的权重搬运(见 Speculative Decoding)。
- 缩小缓存本体:MQA/GQA。从模型架构源头减少 KV head 数,降低每步要读的缓存体积(见 Grouped-Query Attention)。
- 资源隔离:PD 分离。当 prefill 与 decode 的资源画像冲突足够严重时,把两个阶段放到不同的机器池(见 Prefill-Decode Disaggregation)。
评估边界
这些手段大多改变延迟与吞吐之间的交换关系,单独看 tokens/s 容易误判:吞吐提升可能以尾延迟恶化为代价,TP 改善 TPOT 却增加 TTFT。结论必须落在 TTFT / TPOT / P99 / 成本的组合实测上(见 AI System Performance Bottlenecks 与 LLM Inference Optimization Map)。
decode 的每一步到底在等待什么
一个 decode step 的端到端时间不只是一段 GEMM:
1 | 调度器选活跃序列 |
当 batch 很小,权重读取占比高;当 batch 增大,权重搬运可被更多序列摊薄,但 KV 读、显存容量、sampling/stream 和调度开销又会上升。一个服务报告“GPU 利用率高但 TPOT 变差”,可能是 batch 过大造成的排队或 HBM 争用,不是算力不足。
TPOT 与吞吐不是同一指标
单请求 TPOT 更接近用户看到的逐 token 间隔;批量吞吐还要考虑同一轮服务了多少序列。连续 batching、speculative decoding 和量化都可能提高 tokens/s,却让某一长度桶的 TPOT P99 变差。评测至少按输出长度和并发分桶,区分 queue wait、decode step、stream flush 和被取消/抢占重算的 token。
decode 的优化还受 stop 行为影响:提前 EOS、max tokens、工具调用或客户端取消都会改变实际步数。固定输出长度的合成 benchmark 不能代表真实 completion 分布;回放线上 trace 时保存取消率、stop reason 和每请求有效生成 token,才能避免把“少生成了”误判成“生成更快”。