系统性测量偏差: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 TTFTTPOT 都会被加性 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。

亮点与局限

亮点

  1. 直击行业盲点:几乎没有 LLM 推理 paper 用排队论来 attack benchmarking 本身,论文不只测了,还给出了数学解释;
  2. 可即用:多进程 client 是开源实现,可以直接替换 locust / benchmark.py;
  3. NTPOT 这一指标的提出:TTFT + TPOT 合一指标,简化跨模型对比,未来可成行业标准;
  4. 理论与实证互证:M/G/1 推导 → 实验验证 → 工程修正 一气呵成;
  5. 对所有人都有用:引擎开发方、GPU 云厂商、企业 SRE 谁都受益。

局限

  1. 未给出协议层的偏差分解:HTTP/gRPC、SSE/streaming、tokenization on client 这些底层细节是否带来额外偏差未做清晰切分;
  2. NTPOT 的统计假设:依赖于平均输出长度,但生产负载的输出长度分布往往是重尾(long-form answer、agent chain-of-thought),用 E[#tokens] 还是会偏乐观;
  3. 多进程改造自身的开销:worker 数过大会出现 socket FD / port 资源压力,作者未明确最佳 worker 数;
  4. 不评估排队策略本身:仅评测 client 无偏,并不替你回答"该用 FCFS 还是 SJF",那是调度层问题;
  5. 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%?(自检)