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 的分位一起上移。记录时必须把并发与输入输出长度分布写在分位旁边,脱离取数条件的分位数字没有意义。
触发条件与现象
- 混合负载:长短请求交错,批内序列长度方差大,长请求的 ITL 被同批短请求拖高。
- 长上下文:TTFT 从某个上下文长度起异常升高,表现是一个区间,覆盖一段长度范围;该区间内的请求把 P95 顶上去。
- 高并发与队列积压:请求在队列中等待,TTFT 含排队时间,均值随并发缓升、尾部陡增。
- 多卡:通信与同步把 P95 抖动放大,单卡与多卡的 P95 要分开对照。
- prefill / decode 混跑:两段互相打断,TTFT 与 ITL 无法同时达标。
- 分段错配:prefill 取得收益、decode 未兑现,或反过来,端到端总收益被高估。
- 冷启动与预热不足:首批请求含图捕获与编译开销,未丢弃前若干次迭代时把 P95 抬高。
定位动作
- 按段取分位:prefill 段取
TTFT的 P50 / P95 / P99,decode 段取TPOT/ITL的同组分位,不看端到端单值。 - 按请求长度分组:按
input_len与output_len分档统计,长上下文组与短请求组分别看,确认异常从哪个长度区间开始。 - 固定取数口径:并发、输入输出长度分布、warmup 规则、采样窗口一并记录,否则分位之间不可比。
- 对照单卡与多卡:同一负载下单卡与多卡的 P95 对照,判断抖动是否被通信或同步放大。
- 分开两段结论:写明是 prefill 取得收益而 decode 没兑现,还是 decode 变好而 prefill 变差。
- 落到 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 规则缺一项,两次分位就不可比。