系统性测量偏差:LLM 推理基准测得根本不是你以为的那些数
- 关联论文:2605.24217
- 作者:spark
- 更新:2026-07-24
一句话结论
这篇论文指出当下几乎所有"LLM 推理性能基准"测出来的 TTFT / TPOT 数字都被严重高估,因为主流 benchmark 客户端(locust、benchmark.py、GenAI-Perf 等)都是 single-process asyncio 架构——而这个架构在几百~几千 QPS 下就被 Python GIL 卡成单线程队列,再去测并发请求得到的所有 latency 数据都已经被 client 端排队污染。论文提出一个多进程、无偏的评测框架并提出 NTPOT(归一化每输出 token 时间)作为稳健指标。
解决什么真问题
"我们家的推理引擎比 vLLM 在 QPS=1000 时 p99 TTFT 低 30%"——这种营销话术在今天的 LLM infra 圈几乎每篇 release blog 都在发,但作者用 M/G/1 排队论告诉你:
- 如果 client 是单进程 asyncio,所有请求都要先在 client 这边排队发出去;
- 在几百 QPS 时,Python 的 GIL 调度开销会显著拉高真实测量到的 TTFT/TPOT;
- 你以为测的是服务端延迟,实际是 客户端进程队列 + 服务端调度 + GIL 抖动的混合;
- 这导致两个根本问题:(1)同一种引擎在不同 client 下"分数"差得离谱;(2)当你以为在对比 vLLM 和 TRT-LLM 时,你其实在对比 client 端的负载能力。
这是 LLM infra 评测领域一个长期存在却没人说破的"皇帝新衣"。在大规模生产化(千人以上并发、企业级 SLA 评估)场景下,这个问题会演变成错误的容量规划决策——上线即雪崩。
核心方法
1. 用 M/G/1 排队论证明 client 端偏差
作者把 benchmarking client 抽象为一个 M/G/1 队列(Poisson 到达、一般服务时间、单服务台——这里"服务台"是 asyncio 事件循环):
- 服务时间是 client 发出请求 → 接收第一个 byte 之间的耗时;
- 当到达率 λ 增长到接近 μ(client 内部事件循环处理能力上限),平均等待时间 W_q 会以非线性的形式增长(标准 M/G/1 公式即 𝜌² / (2μ(1-𝜌)) · (1 + CV²));
- 因为 Python GIL 实际上把并发 asyncio 任务串行化为 cooperative scheduling,有效 μ(事件循环吞吐)远低于直觉,在 QPS 达到几百就会进入"右半边尾巴",TTFT 测出来就开始虚高。
更糟糕的是,e2e TTFT 和 TPOT 都会被加性 bias 污染:TTFT 因为出站排队,TPOT 因为回复字节的入站处理排队。这两个污染方向相反但都存在,于是传统的"平均 TTFT / TPOT"被一起污染。
2. 无偏多进程评测框架
作者提出在 client 层做 process-level 负载分散:
- 用 multiple worker processes(不是 threads,因为 GIL 同样会卡住 threads);
- 每个 worker 独立维护 asyncio 事件循环与连接池;
- 用一个 master coordinator(轻量)做均匀发请求与汇总;
- client 端排队时间下降到可忽略(<1% 的服务端总延迟)。
核心 contract:在 client 端可证地无排队,那么测得的 server-side metrics 才是真的。
3. NTPOT:归一化每输出 token 时间
论文同时形式化提出一个 robust 的归一化指标,用来横向对比不同模型 / 配置:
NTPOT = (TTFT + E[#output_tokens] * TPOT) / E[#output_tokens]
= TPOT + TTFT / E[#output_tokens]
它的好处是用一个数同时反映 prefill 与 decode 的相对成本,而不需要再分别看 TTFT 与 TPOT。短问答、长文档总结、Agent 工具调用等不同负载下,NTPOT 都能稳定地反映"做一个产生 token 平均付出的系统时间"。
4. 框架简化伪代码
def unbiased_benchmark(target_engine, model_name, n_workers=32):
workers = [
BenchmarkWorker(target_engine.url, model_name)
for _ in range(n_workers)
]
barrier = mp.Barrier(n_workers)
def worker_run(w):
barrier.wait()
while not stop_signal.is_set():
req = draw_request()
t0 = time.perf_counter()
ttft = w.stream_first_token(req)
tpot_list = []
for tok in w.iter_rest(req):
tpot_list.append(time.perf_counter() - tok[-1])
record(w.id, ttft, tpot_list)
procs = [mp.Process(target=worker_run, args=(w,)) for w in workers]
for p in procs: p.start()
return aggregate(records)
关键区别于原版:把"客户端进程排队"剥离开来。t0 严格只计 server 处理时间。
5. 经验结果(论文大意)
- 在 QPS=500~3000 时,标准 client 的 TTFT 测出值比无偏 client 高 20%~80%;
- 多进程 client 把这层偏差压缩到 < 5%;
- NTPOT 在不同模型规模、并发下保持稳定,可作为跨引擎对比的统一指标;
- 论文在数千 QPS 的规模上准确、复现地 profile 了多款主流引擎,让"哪个引擎真的快"这件事第一次有了可信基准。
关键实验与数据
具体数字以论文图为准,原文未明确给出单一对比表,但作者主要在数千 QPS 的压力下展示了:
- 偏置的 single-asyncio 客户端在 RPS 增长时,TTFT 出现明显"假性长尾"——p99 是真实值的 1.5–2 倍;
- 多进程 client 测出的 server-side TTFT 与构造化延迟模型基本吻合;
- NTPOT 在跨模型(7B / 13B / 70B)下保持稳定的排名一致性,TTFT/TPOT 单独看则会在不同 RPS 时变动;
- 这些结论对开源(vLLM、SGLang)和商用(Anyscale、Fireworks)引擎都成立,因为问题在 client 而非 server。
亮点与局限
亮点
- 直击行业盲点:几乎没有 LLM 推理 paper 用排队论来 attack benchmarking 本身,论文不只测了,还给出了数学解释;
- 可即用:多进程 client 是开源实现,可以直接替换 locust / benchmark.py;
- NTPOT 这一指标的提出:TTFT + TPOT 合一指标,简化跨模型对比,未来可成行业标准;
- 理论与实证互证:M/G/1 推导 → 实验验证 → 工程修正 一气呵成;
- 对所有人都有用:引擎开发方、GPU 云厂商、企业 SRE 谁都受益。
局限
- 未给出协议层的偏差分解:HTTP/gRPC、SSE/streaming、tokenization on client 这些底层细节是否带来额外偏差未做清晰切分;
- NTPOT 的统计假设:依赖于平均输出长度,但生产负载的输出长度分布往往是重尾(long-form answer、agent chain-of-thought),用 E[#tokens] 还是会偏乐观;
- 多进程改造自身的开销:worker 数过大会出现 socket FD / port 资源压力,作者未明确最佳 worker 数;
- 不评估排队策略本身:仅评测 client 无偏,并不替你回答"该用 FCFS 还是 SJF",那是调度层问题;
- prompt 模板与 cache 状态高度敏感:相同的 NTPOT 数值,可能由不同 cache 命中率带来,论文略弱。
对工程落地的启发
- 企业内部推理评测:把单进程 asyncio client 全部换成多进程 client,把 NTPOT 写进 CI;
- 引擎 benchmark 报告:如果一张图说"p99 TTFT 在 QPS=1000 是 X ms",先问"client 是 asyncio 单进程还是多进程";
- SLA 与容量规划:用 NTPOT 设 SLO 比纯 TTFT 稳;
- 开源贡献:把 workers + NTPOT 收集器 patch 给 vLLM 的 benchmarks/ 目录、GenAI-Perf 等;
- 内部 LLM 网关的 A/B 测试:评估新模型版本时确保 client 端不会因为 GIL 抖动掩盖回归。
与同方向工作的关系
- 与 vLLM / SGLang / TRT-LLM benchmark suites:本文不替代,而是校正——告诉这些工具"你的 client 有 bias";
- 与 排队论经典结果(M/G/1, M/G/k):把经典理论搬到了 LLM 推理侧;
- 与 GenAI-Perf / llm-perf:本文可视为其方法论的审计;
- 与 连续批处理 / chunked prefill / disaggregation:本文是它们的对立面——评估这些优化的正确姿势;
- 与 Hardware/Network profiling(nsys / NCCL tests):本文关心 application-level latency,不是 PCIe 网卡层面;
- 与 Judea Pearl 因果式评估 / A/B test 严谨化:本文是它在 LLM 推理侧的微观版本。
适合谁读
- LLM 推理引擎开发者 / 性能工程师:必读,否则你发的数字都不可信;
- GPU 云厂商 SRE / 容量规划:用 NTPOT 重做 SLO 制定;
- 学术圈做 LLM systems:在写"我们引擎更快"之前先校对 client;
- 企业 AI 平台架构师:决定"为什么我们推理慢的 bug 找不到"时,把 client bias 列为第一排除项。
工程落地与核查(Jay)
1. 事实核查小结
| 声明 | 核查结果 |
|---|---|
| "TTFT 高估 20–80%(QPS=500~3000)" | ⚠️ 存疑:原文未明确各引擎/模型具体数字,属经验范围;p99 达 1.5–2x 有图形支撑但需原文图对应 |
| "client 偏差压至 < 5%" | ⚠️ 需原文核验:需确认"5%"是服务端总延迟的 5% 还是其他基准 |
| "NTPOT 跨模型(7B/13B/70B)排名稳定" | ✅ 与摘要"across model sizes"一致 |
| "开源/商用引擎均受影响" | ✅ 摘要明确覆盖"open-source and commercial providers" |
| M/G/1 推导框架 | ✅ 理论部分与摘要形式化描述吻合 |
2. 工程落地要点
最小可跑多进程 benchmark:
import multiprocessing as mp
import time
import aiohttp
def worker(engine_url, model, result_queue, barrier, stop_event):
barrier.wait()
async def send_request():
async with aiohttp.ClientSession() as sess:
t0 = time.perf_counter()
async with sess.post(engine_url, json={"model": model, ...}) as resp:
async for chunk in resp.content.iter_any():
if first_token_time is None:
first_token_time = time.perf_counter() - t0
...
result_queue.put({"ttft": first_token_time, ...})
# run many concurrent requests per worker
asyncio.run(send_many_concurrent())
n_workers = 32 # 经验值,太高 socket FD 压力上升
results = mp.Queue()
barrier = mp.Barrier(n_workers)
workers = [mp.Process(target=worker, args=(url, model, results, barrier, stop))
for _ in range(n_workers)]
⚠️ 坑 1(worker 数配置):论文未给出最优 worker 数公式。实操建议从 16–32 开始,若观察到
connection pool exhausted错误则降低并发。
NTPOT 采集:
# 收集阶段
samples = []
for req_id, ttft, tokens_received, gen_start, gen_end in raw_records:
n_output = len(tokens_received)
tpots = compute_tpots(tokens_received, gen_start, gen_end)
avg_tpot = sum(tpots) / len(tpots)
e_output = rolling_mean_output_len() # 用移动窗口估计 E[#output_tokens]
ntpot = avg_tpot + ttft / e_output
samples.append(ntpot)
⚠️ 坑 2(E[#output_tokens] 估计):生产负载输出长度重尾,用固定均值会低估长输出的 NTPOT。建议按任务类型(QA / summarization / generation)分层统计。
基准报告规范(企业内部标准):
每张 benchmark 图表必须注明: 1. Client 架构(单进程 asyncio / 多进程) 2. Worker 进程数与每 worker 并发连接数 3. NTPOT 的 E[#output_tokens] 来源(historian dataset 均值 / 同批次实测均值) 4. 待测引擎的 prefix cache 状态(on/off,需一致)
SLO 重建:
- 旧:TTFT p99 < 500ms @ QPS=200 → 需重测
- 新:用多进程 client 复测,NTPOT p99 < X ms(X 以新测值为准)
- 注意:旧 SLO 往往低估真实性能,新 SLO 更严格但更诚实
3. 适用场景判断
✅ 强烈推荐:所有对外发布的 benchmark 报告;GPU 云容量规划;引擎 A/B 对比测试
⚠️ 谨慎使用:短生命周期 CI 测试(多进程 fork 开销大);移动端 / 边缘侧推理评测(网络拓扑不同,client bias 方向不同)
4. 核查检查清单
在引用任何第三方 LLM 推理 benchmark 数字前必查:
- [ ] 对方用了多少个 worker process?
- [ ] client 是单进程 asyncio 还是多进程?
- [ ] TTFT 的 t0 是发请求时刻还是进了队列时刻?
- [ ] NTPOT 的 E[#output_tokens] 怎么算的?
- [ ] 同一引擎在不同 client 下的 TTFT 偏差是否 > 10%?(自检)