TTFT、TPOT 与尾延迟:推理服务的分段指标口径

在 **** 的 ****(8 卡,单卡 64GB HBM,卡间互联 392 GB/s)上起一个 vLLM 服务,监控面板上平均 TTFT 与平均吞吐都在 SLO 以内,用户侧却反馈首字慢、生成断续。拉出分位看,TTFT 的 P95 与 ITL 的 P99 明显高于均值,多卡下 P95 抖动比单卡放大,均值掩盖了少数慢请求。推理时延分两段:TTFT 覆盖 prefill 与首个 token,TPOT / ITL 覆盖相邻输出 token 的间隔,端到端吞吐是两段叠加。判断服务是否达标,必须看分位,均值不足为据。

指标覆盖的段

指标 覆盖的段 说明
TTFT prefill + 首个解码步 从请求到达至第一个输出 token,交互场景敏感
TPOT decode 每个输出 token 的平均耗时
ITL decode 相邻输出 token 的间隔,逐间隔统计
端到端吞吐 两段叠加 单独看会掩盖分段错配
goodput 两段叠加 满足 SLO 约束的有效吞吐
P50 / P95 / P99 上述各项 分布形状,均值之外必须看分位

TPOT 与 ITL 口径相近但不等同:TPOT 以每个请求的输出 token 数为分母,ITL 以相邻 token 的间隔逐段统计。请求生成结束后批规模衰减,两种口径对这段空闲的处理不同,同一服务上两者常给出不同的分位,报告里要写清用的是哪一个。

prefill 与 decode 的资源画像不同:prefill 一次处理整段提示词,瓶颈在算力峰值与互联带宽,对口指标是 TTFT 与输入吞吐;decode 每步处理一个 token,瓶颈在显存带宽与容量,对口指标是 TPOT 与输出吞吐。一项优化常只落在其中一段,端到端一个均值会把两段差异掩盖。

端到端吞吐是两段叠加

端到端吞吐由两段共同决定,须两段一并计入。单条请求的时延是「TTFT + 输出 token 数 × TPOT」;服务的总吞吐是单位时间内所有请求两段的完成量。一段变快、另一段变慢时,端到端数字可能持平甚至变差:prefill 用 chunked prefill 切块后 TTFT 下降,但切块把 prefill 摊到多个 decode 步里,单步 decode 时间上升,TPOT 随之抬高;反过来,decode 侧加了批并行、TPOT 下降,但 prefill 被挤到后面,TTFT 抬高。只有把两段分开计时,才能说清一次改动落在哪一段、整体是正收益还是负收益。

混合负载会放大这种错配。长短请求交错时,长请求的 prefill 占用设备,同批 decode 请求的 ITL 被拖高;只在短请求集合上测得分段指标,换成长短交错后两段都会变。

长上下文下 TTFT 的异常窗口

长上下文的 TTFT 不是随长度线性缓慢增长,而是从某个长度起出现一个异常窗口。窗口内 prefill 时长跳过一档、KV 池可用块数被压到临界,服务在这一段请求上 P95 陡升,均值却只抬了一点。定位要按 input_len 分档,把每一档的 TTFT 分位单独算,找出窗口起点;只看全量均值读不出这个台阶。窗口常与 chunked prefill 的分块边界、prefix caching 的命中率、显存压力三者之一相关,三者要分别对照,取数时另有两个易错点:请求生成结束会提前退出,批规模随时间衰减,整段统计里的 ITL 与输出吞吐把「批次衰减尾」算了进去,报告应给出只在全体仍在解码的窗口内统计的稳态列;未预热时首批请求带冷启动开销,须丢弃前若干次迭代后再采样。

均值掩盖分布

均值是分布的单一取样,慢请求被多数的快请求平均掉。两条现象都由这一点产生:平均值漂亮而 P99 长;多卡下平均吞吐正常但 P95 抖动放大。尾延迟的这两种形态要靠分位暴露:把 P50 / P95 / P99 三条线并排看,若 P50 正常而 P99 抬升,问题在长尾,不在整体。连续批处理场景下更明显,批构成每步变化,ITL 的波动本身就是证据,均值覆盖不了。

分位要配 goodput 一起看。goodput 是满足 SLO 约束的有效吞吐,它把「超过阈值的请求」排除在有效量之外;平均吞吐达标而 goodput 偏低,说明相当一部分请求落在 SLO 之外,被均值掩盖了。

分位对取数条件敏感。同一服务在不同并发下测得的 P95 不能直接比较:并发升高会同时抬高队列等待与批规模,TTFT 与 ITL 的分位一起上移。记录时必须把并发与输入输出长度分布写在分位旁边,脱离取数条件的分位数字没有意义。

触发条件与现象

定位动作

  1. 按段取分位:prefill 段取 TTFT 的 P50 / P95 / P99,decode 段取 TPOT / ITL 的同组分位,不看端到端单值。
  2. 按请求长度分组:按 input_len 与 output_len 分档统计,长上下文组与短请求组分别看,确认异常从哪个长度区间开始。
  3. 固定取数口径:并发、输入输出长度分布、warmup 规则、采样窗口一并记录,否则分位之间不可比。
  4. 对照单卡与多卡:同一负载下单卡与多卡的 P95 对照,判断抖动是否被通信或同步放大。
  5. 分开两段结论:写明是 prefill 取得收益而 decode 没兑现,还是 decode 变好而 prefill 变差。
  6. 落到 goodput:把超 SLO 的请求扣掉,确认有效吞吐是否仍达标。
# 固定工况的在线压测入口(字段名示意)
python -m sglang.bench_serving \
  --backend vllm \
  --max-concurrency 16 \
  --num-prompts 80 \
  --random-input-len 256 --random-output-len 32

--num-prompts 应显著大于 max-concurrency,让服务进入稳态后再取分位(框架未规定固定倍数);并发、输入长度、输出长度三项必须一起给定,分位才有可比性。

记录字段

metrics:                # 分段 + 分位,前后对照
  ttft_p50_ms:
  ttft_p95_ms:
  itl_p50_ms:
  itl_p95_ms:
  tpot_p50_ms:
  tpot_p95_ms:
  decode_tok_per_s:
  goodput_pct:
workload:               # 取数口径,随分位一起记
  concurrency: <并发>
  input_len: <分布>
  output_len: <分布>
  request_mix: <纯短 / 纯长 / 长短交错>
  warmup_policy:
  sampling_window:
stability:
  stability_window_min: # 稳态运行时长
  card_count:           # 单卡 / 多卡,用于 P95 对照

小结

推理服务的时延分两段:TTFT 覆盖 prefill 与首个 token,TPOT / ITL 覆盖相邻输出 token 的间隔,端到端吞吐是两段叠加,一段变化常被另一段抵消。均值会掩盖分布,判定达标必须拉 P50 / P95 / P99,P50 正常而 P99 抬升说明问题在长尾,再用 goodput 确认有效吞吐。分位必须与取数口径一起记:并发、输入输出长度分布、warmup 规则缺一项,两次分位就不可比。