ELL3N HSU

Available in Chinese only.

← 文章

推理压测怎么测:从流式时间戳到满足 SLO 的吞吐

以测试工程的测量边界为起点,区分 TTFT、TPOT、ITL、开闭环负载与 goodput,避免把压力机限制和失败请求隐藏在漂亮报表里。

两个推理服务都声称每秒输出很多 token,用户却可能觉得其中一个更卡。吞吐数字没有说明首 token 等多久、输出是否均匀,也没有说明有多少请求超时。

先拿一条流式请求试着记账:什么时候发出,什么时候看到第一个 token,中间停了多久,最后什么时候结束。把这些时间点记录清楚,再增加请求量,才能知道吞吐上升时,用户究竟是在更快收到回答,还是只是在等更多请求一起完成。

一条成功请求至少留下哪些时间戳

客户端使用同一个单调时钟记录发送时刻 t0t_0、每个输出 token 可见的时刻 t1,…,tNt_1,\ldots,t_N,以及流正常结束的时刻 tendt_{end}。TTFT 观察首个输出的等待,ITL 观察相邻 token 之间的停顿,TPOT 汇总首 token 后的平均间隔,E2E_stream 则覆盖请求直到流完整结束。与推理总览的 E2E_token(末 token 可见)相比,它多包含协议尾部时间 tend−tNt_{end}-t_N。

指标计算
TTFT\mathrm{TTFT}t1−t0t_1-t_0
ITL\mathrm{ITL}tj−tj−1t_j-t_{j-1},j≥2j\ge2
TPOT\mathrm{TPOT}(tN−t1)/(N−1)(t_N-t_1)/(N-1),N>1N>1
E2Etoken\mathrm{E2E}_{\text{token}}tN−t0t_N-t_0
E2Estream\mathrm{E2E}_{\text{stream}}tend−t0t_{end}-t_0

只有一个输出 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 检查,应提前约定。固定测量窗口下:

request goodput=窗口内完成且满足约定 SLO 的请求数窗口时长\text{request goodput} = \frac{\text{窗口内完成且满足约定 SLO 的请求数}}{\text{窗口时长}}

这里定义的是每秒达标请求数,不是 token goodput。vLLM v0.10.2 的统计实现同样区分请求 goodput 与 token throughput,但把不超过一个输出 token 的 TPOT 视为 0 用于 goodput 判定;本文保留空值并要求显式约定豁免,比较报表时应统一这个选择。吞吐上升而 goodput 下降,说明更多计算没有转化为符合目标的完成结果。窗口边界还需统一:稳态窗口内完成量与“窗口内发送的一批请求最终成功率”可以同时报告,但不能混用分母。

看到异常后,下一步测什么

现象优先核对下一步对照
TTFT 上升,输出间隔稳定排队、长 Prefill、缓存准备固定输出,改变输入长度和到达率
ITL 变长,吞吐上升批次变大、每轮工作增加固定请求集,扫描调度 token 预算
缓存命中率升高,P99 变差热节点排队、搬运、驱逐分 worker、命中长度和准备耗时统计
客户端慢,服务端耗时稳定压力机、网络、流式缓冲对齐请求 ID,比较各自计时边界

分位数要从同一口径的逐请求数据或可合并直方图计算,不能平均各 worker 的 P99。小样本中的 P99 很可能只是最大的那一个值,应报告样本数、测量时长和重复运行的波动,不用几条请求给尾延迟下结论。

压测最终要交付能重放的请求集、配置、原始记录和判读依据。这样下一次改动批处理或缓存路由时,才知道收益出现在哪一段,以及付出了什么代价。