推理压测怎么测:从流式时间戳到满足 SLO 的吞吐
以测试工程的测量边界为起点,区分 TTFT、TPOT、ITL、开闭环负载与 goodput,避免把压力机限制和失败请求隐藏在漂亮报表里。
两个推理服务都声称每秒输出很多 token,用户却可能觉得其中一个更卡。吞吐数字没有说明首 token 等多久、输出是否均匀,也没有说明有多少请求超时。
先拿一条流式请求试着记账:什么时候发出,什么时候看到第一个 token,中间停了多久,最后什么时候结束。把这些时间点记录清楚,再增加请求量,才能知道吞吐上升时,用户究竟是在更快收到回答,还是只是在等更多请求一起完成。
一条成功请求至少留下哪些时间戳
客户端使用同一个单调时钟记录发送时刻 、每个输出 token 可见的时刻 ,以及流正常结束的时刻 。TTFT 观察首个输出的等待,ITL 观察相邻 token 之间的停顿,TPOT 汇总首 token 后的平均间隔,E2E_stream 则覆盖请求直到流完整结束。与推理总览的 E2E_token(末 token 可见)相比,它多包含协议尾部时间 。
| 指标 | 计算 |
|---|---|
| , | |
| , | |
只有一个输出 token 时,TPOT 没有定义,应记为空值,而不是零。最后一个 token 到流结束之间还可能有协议事件,因此 E2E_stream 等于 TTFT 加所有 ITL,再加协议尾部时间;E2E_token 才等于 TTFT 加所有 ITL。
实际采集时,先确认流式接口一次交回的是什么。一个 SSE 事件、网络包或文本 chunk 可能包含多个 token。如果只能取得 chunk 到达时间,报告就应写首个内容 chunk 延迟与 chunk 间隔,不能据此计算精确的逐 token ITL。空的角色事件也不算首个内容输出。服务端时间戳有自己的计时边界,不宜直接减去客户端时钟。
具体工具还需要核对其 adapter。vLLM v0.10.2 的流式客户端实现按接收到的 chunk 记录时间,各后端的结束口径需分别阅读;这些字段名不能自动证明拿到了逐 token 时间戳。
下面的 Python 函数假定已经拿到逐 token 时间戳,单位为秒。它只整理一条成功请求;超时和协议错误由外层单独记账。
from math import isfinite
def request_metrics(sent_at, token_times, ended_at):
times = list(token_times)
if not times:
raise ValueError("成功请求必须有输出 token")
timeline = [sent_at, *times, ended_at]
if not all(isfinite(t) for t in timeline):
raise ValueError("时间戳必须是有限数值")
if any(b < a for a, b in zip(timeline, timeline[1:])):
raise ValueError("时间戳必须按同一时钟单调排列")
intervals = [b - a for a, b in zip(times, times[1:])]
return {
"ttft": times[0] - sent_at,
"tpot": sum(intervals) / len(intervals) if intervals else None,
"itl": intervals,
"e2e_token": times[-1] - sent_at,
"e2e_stream": ended_at - sent_at,
"protocol_tail": ended_at - times[-1],
"output_tokens": len(times),
}
例如发送在 0 秒,token 在 0.4、0.5、0.7 秒可见,流在 0.72 秒结束,结果是 TTFT 0.4 秒、ITL 为 0.1 与 0.2 秒、TPOT 0.15 秒、E2E_token 0.7 秒、E2E_stream 0.72 秒、协议尾部 0.02 秒。把一条请求的 TPOT 再跨请求取分位数,与把全部 token 间隔混在一起取分位数,代表的体验也不同;后者会给长输出更多权重。
负载是固定到达,还是完成一个再发一个
闭环压测维持一定数量的并发请求,请求完成后才补下一个。它适合回答“给定并发下,系统能完成多少工作”。但服务一变慢,补充请求也变慢,实际到达率会随之下降。
开环压测按独立的到达时间表发请求,例如固定速率或指定分布。它适合观察外部需求持续进入时的排队和过载。如果压力机发不出去,还要记录计划发送与实际发送之间的延迟,不能把压力机里的等待静默丢掉。
压测工具同时提供请求速率与最大并发时,后者可能限制前者的实际实现。报告里要分开写目标到达率、实际到达率、准入上限和实际活跃并发。闭环不是错误;错误是用它的低到达率尾延迟,代表外部固定流量下的服务表现。
请求集比一个并发数字更重要
同样 32 个并发,短问短答与长文档摘要的压力不同。至少固定或记录:输入长度分布、实际输出长度、最大输出限制、停止条件、共享前缀长度与热度分布,以及模型、tokenizer、聊天模板、引擎和硬件版本。
模型/内核 warmup 与前缀缓存预热也要分开。清空 Prefix Cache 不等于让模型重新冷启动;反复使用同一条 prompt 预热,则可能同时改变缓存状态。比较缓存配置时,保存每组的初始状态和到达 trace,比只写“预热了十次”更有解释力。
失败请求不能从分母里消失
先保留每次尝试的终态:成功、超时、被拒绝、连接中断或其他错误,再统计成功请求的延迟分布。超时请求的真实完成时间未知,不能直接把超时阈值当作完成延迟,也不能删除后声称整体 P99 很低。成功样本的分位数应明确标注统计范围,并与失败比例并列展示。
对服务目标,可以定义一条请求同时满足成功、TTFT 不超过阈值、TPOT 不超过阈值才算达标;单 token 请求是否豁免 TPOT 检查,应提前约定。固定测量窗口下:
这里定义的是每秒达标请求数,不是 token goodput。vLLM v0.10.2 的统计实现同样区分请求 goodput 与 token throughput,但把不超过一个输出 token 的 TPOT 视为 0 用于 goodput 判定;本文保留空值并要求显式约定豁免,比较报表时应统一这个选择。吞吐上升而 goodput 下降,说明更多计算没有转化为符合目标的完成结果。窗口边界还需统一:稳态窗口内完成量与“窗口内发送的一批请求最终成功率”可以同时报告,但不能混用分母。
看到异常后,下一步测什么
| 现象 | 优先核对 | 下一步对照 |
|---|---|---|
| TTFT 上升,输出间隔稳定 | 排队、长 Prefill、缓存准备 | 固定输出,改变输入长度和到达率 |
| ITL 变长,吞吐上升 | 批次变大、每轮工作增加 | 固定请求集,扫描调度 token 预算 |
| 缓存命中率升高,P99 变差 | 热节点排队、搬运、驱逐 | 分 worker、命中长度和准备耗时统计 |
| 客户端慢,服务端耗时稳定 | 压力机、网络、流式缓冲 | 对齐请求 ID,比较各自计时边界 |
分位数要从同一口径的逐请求数据或可合并直方图计算,不能平均各 worker 的 P99。小样本中的 P99 很可能只是最大的那一个值,应报告样本数、测量时长和重复运行的波动,不用几条请求给尾延迟下结论。
压测最终要交付能重放的请求集、配置、原始记录和判读依据。这样下一次改动批处理或缓存路由时,才知道收益出现在哪一段,以及付出了什么代价。