LLM 容量压测生产实战:用 Timed Trace、Burstiness 与 Tail-SLO Gate 找到真实安全 QPS
生成式推理服务的压测结果,常常被测试方法本身误导。最常见的报告是:固定 50 或 100 并发,跑若干分钟,然后记录平均 TTFT、平均吞吐和 GPU 利用率。这样的数字可以做单机优化对比,却不足以回答生产环境最重要的问题——在真实请求到达方式下,这套集群到底能稳定承受多少流量?
原因在于生成式推理并不是固定耗时 RPC。请求的输入长度、输出长度、是否多轮、到达间隔和突发程度都会改变排队、批处理和显存占用。两个平均 QPS 相同的流量,一个均匀到达、另一个在几秒内集中爆发,对尾延迟造成的压力完全不同。
因此,容量测试的目标不应是寻找“最大吞吐”,而应寻找 Safe QPS:在代表性 workload 下,系统持续满足既定 Tail SLO 的最高请求率。
背景:为什么“跑满 GPU”不等于知道系统容量
GPU 利用率 100% 只说明硬件在忙,不说明系统在“正确地”忙。生产容量真正的度量标准,是系统在满足业务承诺的前提下能接住多少外部流量。一个把 GPU 打满但 p99 尾延迟已经失控的服务,和一台跑不满但尾延迟稳定的服务,前者并不具备更高的可用容量。
本文结合 vLLM、NVIDIA AIPerf 与 MLPerf 的 Server 场景,讲清四件事:为什么固定并发会高估容量、为什么平均长度不代表真实 workload、为什么 Burstiness 必须作为独立变量、以及如何用 Tail-SLO Gate 找到真正的容量拐点。
核心原理一:先区分闭环并发与开放环到达率
固定并发为什么会掩盖过载
固定 concurrency 本质上是闭环负载:一个请求完成后,客户端才继续补下一个。当服务端开始变慢时,客户端自己也会变慢,新请求注入速度随之下降。
这种方式适合观察“同时有 N 个用户时”的体验,却可能高估真正的在线承载能力,因为生产流量通常不会因为你的 GPU 变慢就自动停止到达。
相比之下,按 request rate 驱动的开放环负载会按照预定时间继续发请求。服务端一旦处理不过来,队列、TTFT 和端到端尾延迟就会真实暴露出来。
vLLM 当前的 bench serve 可以直接使用 --request-rate 控制请求率,并支持 Poisson 或 Gamma 分布生成到达时间;--burstiness 小于 1 时会产生更突发的到达模式。AIPerf 同样支持 constant、poisson、gamma 等 arrival pattern。
生产压测至少应分别跑两类实验:
- Closed-loop concurrency sweep:观察不同并发下的单请求体验和 GPU 饱和区间。
- Open-loop request-rate sweep:观察固定外部到达率下何时出现排队失控和 Tail SLO 破坏。
核心原理二:平均长度不代表真实 workload
LLM 请求成本与 token 长度高度相关。只用“输入 1K、输出 256”这样的固定样本,往往会把真实业务中的长上下文、短回答、长回答和混合流量全部抹平。
更合理的方法是先从生产或预生产日志提取匿名化 workload profile,至少保留:
- 请求时间戳;
- input token length;
- output token length;
- 会话或 workload class;
- 必要时保留用于复现前缀相似性的 hash 信息,而不是原始业务文本。
vLLM 最新的 timed_trace 数据集直接支持 timestamp、input_length、output_length 和 hash_ids 等字段,并可使用 trace 中的时间信息进行 self-timed replay。AIPerf 也支持带时间戳的固定调度 Trace Replay。
一个最小化的匿名 trace 可以类似:
{"timestamp": 0, "input_length": 1200, "output_length": 52, "hash_ids": [0, 1, 2]}
{"timestamp": 105, "input_length": 1800, "output_length": 26, "hash_ids": [0, 3, 4, 5]}
{"timestamp": 274, "input_length": 1300, "output_length": 52, "hash_ids": [1, 4, 6]}
AIPerf 官方示例可按原始时间戳固定调度:
aiperf profile \
--model YOUR_MODEL \
--endpoint-type chat \
--streaming \
--url localhost:8000 \
--input-file custom_trace.jsonl \
--custom-dataset-type mooncake_trace \
--fixed-schedule
这类 trace 的价值不只是“更像生产”。它还能暴露固定长度测试很难出现的问题:长上下文集中到达、会话突发、上下文窗口越界,以及同一个平均 QPS 下完全不同的队列形态。
核心原理三:Burstiness 必须作为独立变量
容量测试经常只扫 QPS,却忽略 Burstiness。实际上,平均 20 QPS 可以是每 50ms 均匀一个请求,也可以是短时间连续几十个请求、随后长时间空闲。
vLLM 的 benchmark 支持通过 Gamma 分布调节 burstiness;AIPerf 也把 arrival pattern 与 smoothness 暴露为参数。这意味着压测矩阵不应只有 QPS 一维,而应至少包含:
- request rate;
- burstiness / arrival pattern;
- input/output length distribution;
- concurrency cap;
- workload class。
建议把生产容量结果画成多条曲线,而不是一个数字。例如同样是 30 QPS,分别给出 Poisson、较平滑 Gamma、较突发 Gamma 和真实 Trace Replay 的 p95/p99 指标。这样才能知道系统对突发流量是否敏感。
核心原理四:用 Tail-SLO Gate 找容量拐点,而不是追求最高 TPS
生成式服务的容量曲线通常存在明显拐点:在某个到达率之前,吞吐随流量增长;超过拐点后,吞吐增长变慢,但排队时间和尾延迟快速上升。
因此 Safe QPS 应定义为:在指定 workload profile、测试时长和硬件配置下,同时满足全部业务 SLO 与稳定性门禁的最高持续 request rate。
门禁至少包含:
| 指标 | 含义 |
|---|---|
| Success Rate | 不能通过超时、拒绝或错误来“换吞吐” |
| TTFT p95/p99 | 交互请求最直观的等待感受 |
| ITL / TPOT p95/p99 | 生成过程是否卡顿 |
| E2E Latency p95/p99 | 完整请求体验 |
| Queue Time / Queue Depth | 是否开始持续积压 |
| Output Tokens/s | 服务端有效产出能力 |
| GPU / HBM / CPU | 用于解释瓶颈,而不是作为最终 SLO |
门禁配置应来自业务场景,而不是照抄其他公司的绝对阈值。例如可以把发布规则表达成:
capacity_gate:
success_rate: ">= ${SUCCESS_SLO}"
ttft_p95: "<= ${TTFT_P95_SLO}"
ttft_p99: "<= ${TTFT_P99_SLO}"
itl_p99: "<= ${ITL_P99_SLO}"
e2e_p99: "<= ${E2E_P99_SLO}"
queue_growth: "no_sustained_growth"
MLPerf 的 Server 场景同样采用随机到达并设置 latency constraint,而不是只报告离线吞吐。其 DeepSeek-R1 文档还特别提示,Server target QPS 通常需要人工寻找;它有时大约是 Offline QPS 的 80%,但在部分系统上甚至可能低于 50%。这正说明 Offline throughput 不能直接当成在线容量。
工程落地:建立四层压测体系
第一层:Micro Benchmark
目标是比较单项配置,例如 TP/EP 布局、量化方式、内核版本或 GPU 型号。此时可使用固定输入输出长度和稳定 concurrency,以降低变量数量。这一层回答“哪个配置更快”,不回答“生产能扛多少流量”。
第二层:Synthetic Capacity Sweep
使用多个 input/output bucket,扫描 request rate 和 burstiness。每个点必须经过 warmup,并保存 per-request 结果而不仅是汇总均值。vLLM bench serve 支持保存详细请求指标,也能直接输出 TTFT、TPOT、ITL 与 E2E 的 percentile。建议在 CI 或性能实验平台中,把每个 benchmark run 的模型版本、推理引擎版本、GPU、并行配置和参数全部作为 metadata 保存。
第三层:Timed Trace Replay
从真实业务流量采样并匿名化,保留时间戳和 token length 分布。对同一份 trace 做 A/A 与 A/B Replay,可用于比较:推理引擎升级、GPU 型号变化、并行策略变化、模型版本变化、调度参数变化。这里最重要的是保持 workload 不变,否则很难判断性能变化来自系统还是流量。
第四层:Soak + Burst Test
短压测容易错过内存碎片、连接池、缓存热度变化和慢性队列增长。最终发布前应至少增加持续稳态测试,并在稳态期间插入突发流量段,观察系统能否恢复到原来的队列和尾延迟水平。
如何把测试结果转成容量规划
最终不要输出“单卡 35 QPS”这种孤立结论,而是输出一个 Capacity Envelope:
- Interactive workload:Safe QPS = X;
- Long-context workload:Safe QPS = Y;
- Bursty workload:Safe QPS = Z;
- 对应 GPU 数量、模型配置和 Tail SLO;
- 至少预留一档业务增长和故障冗余空间。
其中 X/Y/Z 应来自实际测试,不应预先写死。容量规划时还要明确 N-1 或单副本故障后的 SLO,因为“正常状态刚好跑满”通常不是可运营的生产容量。
常见误区
| 误区 | 正确做法 |
|---|---|
| 只看平均 TTFT | 关注 p95/p99,并按 input length、tenant 或业务类型分桶分析 |
| 固定并发跑到最高 TPS 就算容量 | 固定并发会形成闭环反馈,适合性能扫描,不足以定义在线安全 QPS |
| 所有请求都用相同 token 长度 | 至少需要短、中、长 bucket,最好最终使用真实 trace |
| 用 ignore_eos 的吞吐直接用于业务容量 | 该结果只适合配置对比,不宜直接作为容量结论 |
| 只压平均流量,不压突发 | 真实系统常死在 10 秒峰值而非 1 小时平均值,Burstiness 必须显式测试 |
上线检查清单
- 已分别完成 closed-loop concurrency 和 open-loop request-rate 测试。
- 已覆盖生产 input/output token length 分布,而不是只有一个固定长度。
- 已至少测试 Poisson/突发流量或真实时间戳 Trace Replay。
- 已保存 p95/p99 TTFT、ITL/TPOT、E2E、成功率与队列指标。
- 已找到 Tail SLO 开始明显恶化的容量拐点,而不是只记录最高 TPS。
- Safe QPS 由业务 SLO Gate 决定,并留有故障与增长余量。
- benchmark artifact 已记录模型版本、推理引擎版本、GPU、并行参数和测试数据版本。
- 发布前完成持续稳态测试,并确认突发结束后队列能够恢复。
参考资料
- vLLM —
vllm bench serve:https://docs.vllm.ai/en/latest/cli/bench/serve/ - NVIDIA AIPerf — Trace Replay with Mooncake Traces:https://docs.nvidia.com/aiperf/benchmark-modes/trace-replay-with-mooncake-traces
- NVIDIA AIPerf — Command Line Options:https://docs.nvidia.com/aiperf/reference/command-line-options
- NVIDIA AIPerf — Comprehensive LLM Benchmarking:https://docs.nvidia.com/aiperf/getting-started/ai-perf-comprehensive-llm-benchmarking
- MLCommons — Llama 2 70B: An MLPerf Inference Benchmark for Large Language Models:https://mlcommons.org/2024/03/mlperf-llama2-70b/
- MLPerf Inference — DeepSeek-R1:https://docs.mlcommons.org/inference/benchmarks/language/deepseek-r1/