“我的推理服务到底能扛多大流量?““加一张卡值不值?““P99 为什么这么差?“这些问题都需要一套科学的基准测试方法。本文讲解 LLM 推理的核心性能指标 TTFT / ITL / TPOT,分析延迟与吞吐曲线,演示 LLM Perf 与 Locust 两类压测工具,最后给出容量规划与成本建模的完整方法。
一、推理性能指标体系
1.1 延迟指标:TTFT / ITL / TPOT
LLM 推理与普通 API 不同——它不是一次性返回,而是流式逐 token 生成。因此延迟被拆成多个阶段:
| 指标 | 全称 | 含义 | 量级 |
|---|---|---|---|
| TTFT | Time To First Token | 请求发出到收到第一个 token 的时间 | 数百 ms ~ 数 s |
| ITL | Inter-Token Latency | 相邻两个 token 的间隔(decode 速度) | 10~100 ms |
| TPOT | Time Per Output Token | 每个输出 token 的平均耗时(含首 token) | 与 ITL 相近 |
| e2e | End-to-end | 完整生成全部输出的总时间 | 秒级 |
TTFT <──> ITL <──> ITL <──> ITL
| |
请求开始 最后一个 token
- TTFT 决定"感知延迟”:用户点击后多久看到第一个字,对流式交互影响最大
- ITL/TPOT 决定"生成速度”:决定整段回复多久完成,影响流式体验与总耗时
- 优化手段侧重不同:TTFT 靠 prefill 优化与并发控制,ITL 靠 decode 优化与批量策略
1.2 吞吐指标
| 指标 | 含义 | 公式 |
|---|---|---|
| RPS / QPS | 每秒请求数 | 请求数 / 秒 |
| Output tokens/s | 每秒输出 token 数(整服务) | Σ输出长度 / 秒 |
| Token/s per request | 单请求生成速度 | 输出长度 / 生成耗时 |
| MFU / GBPS | 算力/带宽利用率 | 实际 / 峰值 |
关键区分:服务吞吐与单请求速度是两回事。高吞吐可以靠"多请求并行生成"实现,但每个请求的速度会被共享算力拖慢。评估一个服务,两者都要报。
1.3 指标之间的权衡关系
并发请求数 ──> 吞吐(RPS / tokens/s)上升
└─> 每请求延迟(TTFT / ITL)上升
这是推理服务最根本的权衡:同一份硬件,吞吐与延迟不可兼得。基准测试的核心工作之一,就是画出这条"延迟-吞吐曲线”,找到服务水平目标(SLO)允许的临界点。
二、延迟与吞吐的曲线
2.1 并发度 vs 吞吐的曲线形态
固定硬件与模型,逐步提高并发请求数,曲线通常呈三段:
吞吐
↑ ③ 饱和区(吞吐不再增长,排队延迟爆炸)
| ____/
| /
| / ② 线性区(吞吐随并发线性增长)
| /
| / ① 空闲区(请求少,硬件未饱和)
└───────────────────────────────> 并发请求数
- ① 空闲区:并发低,GPU 利用率不足,增加并发几乎不增加延迟
- ② 线性区:连续批处理让硬件满载,吞吐线性上升,延迟缓慢上升——最优运行区
- ③ 饱和区:队列排队,吞吐见顶,TTFT 开始指数恶化
2.2 分位数:P50 / P99 与平均值
平均值会骗人。LLM 服务的延迟呈长尾分布:多数请求快,个别请求因排队、长 prefill 或抢占慢得多。因此必须报告分位数:
| 分位数 | 含义 | 使用场景 |
|---|---|---|
| P50 | 一半请求快于该值 | 体感典型值 |
| P95 | 95% 请求快于该值 | 常规 SLO |
| P99 | 99% 请求快于该值 | 严格 SLO / 商业承诺 |
| P99.9 | 千分之一慢请求 | 关键业务告警 |
生产 SLO 通常写成”P99 TTFT < 1s 且 P95 ITL < 100ms“这类形式,比"平均延迟"可承诺得多。
三、压测工具
3.1 LLM Perf:LLM 专用压测
LLM Perf(Ray 团队)与 vLLM 自带的 benchmark 脚本是 LLM 场景最常用的工具。它们能按真实分布生成请求(可变输入/输出长度、泊松到达间隔),并输出分位数延迟。
# vLLM 自带 benchmark(对比式压测)
python benchmarks/benchmark_serving.py \
--model meta-llama/Llama-2-7B-chat \
--tokenizer meta-llama/Llama-2-7B-chat \
--backend vllm \
--endpoint /v1/completions \
--num-prompts 1000 \
--request-rate 10 \
--seed 42
# LLM Perf(客户端压测,报告 TTFT/ITL 分位数)
python llmperf/llmperf/llm_llama_cpp_client.py \
--num_requests 100 \
--max_input_len 1024 \
--max_output_len 256 \
--request_rate 5 \
--url http://localhost:8000
输出示例:
TTFT: p50=320ms, p90=580ms, p99=920ms
ITL: p50=45ms, p90=68ms, p99=110ms
Throughput: 1284 tokens/s (output)
Requests: 12.4 RPS
3.2 Locust:通用负载工具
LLM 压测也常用 Locust 自定义客户端,灵活控制并发与脚本逻辑:
# locustfile.py
from locust import HttpUser, task, between
import json
class LLMUser(HttpUser):
wait_time = between(0.5, 2.0) # 用户思考间隔
@task
def generate(self):
payload = {
"model": "qwen2-7b",
"prompt": "写一段关于 AI 推理优化的介绍",
"max_tokens": 128,
"stream": False,
}
resp = self.client.post(
"/v1/completions",
json=payload,
headers={"Authorization": "Bearer sk-test"},
)
assert resp.status_code == 200
运行:
locust -f locustfile.py --host http://localhost:8000 --headless \
-u 50 --spawn-rate 5 -t 10m --csv=bench
Locust 的 Web 界面能实时看到并发、RPS 与响应时间分布,方便压测过程中调整负载。
3.3 压测请求的真实性
压测结果只有请求足够"像生产"才有意义。构造负载时注意:
- 输入/输出长度分布:长 prompt 与短 prompt 混合,而非固定长度
- 到达模式:用泊松过程模拟真实流量突发,而非匀速
- 冷热数据:前缀缓存命中率要按生产估计(命中率越高压测越好)
- 混合场景:同时测短对话与长文档解析
3.4 一个并发扫描脚本
为了画延迟-吞吐曲线,最直接的方式是用脚本扫不同的并发度,逐档记录指标:
import json, time, requests
import concurrent.futures as cf
def gen(prompt="介绍 CPU 与 GPU 的区别", max_tokens=128):
t0 = time.perf_counter()
r = requests.post(
"http://localhost:8000/v1/completions",
json={"model": "qwen2-7b", "prompt": prompt,
"max_tokens": max_tokens, "stream": False},
timeout=60,
)
el = time.perf_counter() - t0
n_out = len(r.json()["choices"][0]["text"].split()) # 粗略
return {"latency": el, "out_len": n_out}
def sweep(concurrency, total=200):
results = []
with cf.ThreadPoolExecutor(max_workers=concurrency) as pool:
futures = [pool.submit(gen) for _ in range(total)]
for f in cf.as_completed(futures):
results.append(f.result())
lat = sorted(x["latency"] for x in results)
rps = total / sum(x["latency"] for x in results)
return {
"concurrency": concurrency,
"rps": round(rps, 2),
"p50": round(lat[len(lat)//2], 3),
"p99": round(lat[int(len(lat)*0.99)-1], 3),
}
for c in [1, 2, 4, 8, 16, 32, 64]:
print(sweep(c))
输出会直接给出并发 → (RPS, P50, P99) 的表格,这正是容量规划所需的原始数据。
四、容量规划与成本建模
4.1 每 token 成本
容量规划的第一步是算清"生成一个 token 要多少钱”:
每小时 GPU 成本(元) = 单卡时价 × 卡数
每日成本 = 每小时成本 × 24
单 token 成本(元) = 每日成本 ÷ (每日输出 token 数)
以一个 TP=4 的 70B 服务为例:
| 配置 | 数值 |
|---|---|
| 单卡时价(A100-80G) | 20 元/小时 |
| 卡数 | 4 |
| 服务日成本 | 20 × 4 × 24 = 1920 元/天 |
| 实测吞吐 | 1500 tokens/s |
| 每日输出 token | 1500 × 86400 ≈ 1.30 亿 |
| 单输出 token 成本 | ≈ 1.48 × 10⁻⁵ 元 ≈ 1.5 万分/token |
这个数可以反推定价与 ROI:如果业务每请求平均输出 500 token,则每请求硬件成本约 0.007 元。
4.2 GPU 选型对比
容量规划本质是"在延迟约束下最大化吞吐/成本”。不同硬件对同一模型的指标(示意):
| 硬件 | 70B FP16 推理 | 单卡可服务并发 | 成本相对值 |
|---|---|---|---|
| A100-80G ×4 (TP=4) | P99 ITL ~50ms | ~32 并发 | 1.0x |
| H100-80G ×4 (TP=4) | P99 ITL ~30ms | ~48 并发 | 2.0x |
| A100 ×4 + INT4 量化 | P99 ITL ~40ms | ~48 并发 | 1.0x(省卡数) |
| 2×H100 + FP8 | P99 ITL ~28ms | ~56 并发 | 1.8x |
注意:算力/带宽/显存三者共同决定 LLM 推理性能。显存决定能装多大模型与多长上下文,带宽决定 decode 速度,算力决定 prefill 速度。选型时不能用单一指标。
4.3 容量规划公式
所需副本数 = 预估峰值 RPS × 平均输出 token ÷ 单副本稳态吞吐(token/s)
示例:峰值 50 RPS,平均输出 300 token,单副本 1500 tokens/s
→ 50 × 300 / 1500 = 10 副本
再叠加入口缓冲与冗余:10 × 1.2(弹性余量)× 1.2(故障冗余)≈ 15 副本
容量规划要与 https://plumephp.com/ai-distributed-inference-gpu-cluster/ 中的副本伸缩机制配合——规划出基准容量,用弹性伸缩吸收波动。
4.4 上下文长度对容量的隐性影响
同样的 RPS,上下文越长,KV Cache 占用越大、prefill 算力消耗越大。容量规划必须建模"token 消耗速率"而非只看 RPS:
每请求平均消耗 token ≈ 平均输入 token + 平均输出 token
服务 token 吞吐(token/s) = RPS × 每请求平均 token
示例 A:短对话 平均 300 + 300 = 600 token,50 RPS → 30k token/s
示例 B:长文档 平均 3000 + 500 = 3500 token,50 RPS → 175k token/s
同一批 GPU,承载"短对话"与"长文档解析"的容量可以相差 5 倍以上。因此压测负载里必须包含与生产一致的长度分布,否则容量数字严重失真。
4.5 混合模型与多租户成本分摊
多模型共享集群时,成本要按资源占用分摊而非简单按调用次数:
| 分摊口径 | 公式 | 适用 |
|---|---|---|
| 按 token | 服务总成本 × (该模型 token / 总 token) | 同批次硬件 |
| 按显存占卡 | 按模型占用的 GPU 卡数 × 时价 | 多并行度模型 |
| 按配额 | 按团队 GPU 配额比例 | 多团队共享 |
def cost_share(total_daily_cost, model_tokens, all_tokens):
return total_daily_cost * (model_tokens / all_tokens)
print(cost_share(1920, model_tokens=3e7, all_tokens=1.3e8), "元/天")
透明的成本分摊是推理平台(参见 https://plumephp.com/posts/devops/ 的 FinOps 实践)让多个业务团队愿意共享 GPU 集群的前提。
五、生产压测方法论
5.1 压测计划
| 步骤 | 内容 | 产出 |
|---|---|---|
| 1. 定义 SLO | P99 TTFT / P95 ITL / 吞吐目标 | 可验证的目标 |
| 2. 基线压测 | 固定 1 副本,扫并发 1→64 | 延迟-吞吐曲线 |
| 3. 扩容验证 | 多副本,验证线性扩展 | 每副本吞吐 |
| 4. 峰值模拟 | 生产流量回放 / 泊松到达 | 峰值容量 |
| 5. 稳定性 | 持续 24h,观察显存泄漏/漂移 | 长期指标 |
5.2 压测后的数据分析清单
拿到压测数据后,按以下清单排查:
- TTFT 随并发快速恶化 → 排队严重,可能 prefill 抢占 decode,检查调度策略
- ITL 均匀但吞吐上不去 → decode kernel 或带宽瓶颈,检查 https://plumephp.com/ai-kernel-fusion-optimization/ 中的融合与 CUDA Graph
- 显存 OOM 在长上下文 → KV Cache 预留不足,调整 gpu_memory_utilization 或量化 KV
- GPU 利用率高但延迟高 → 可能存在长尾请求拖累,检查 batch 内长 prefill 请求
- 多副本后吞吐不线性 → 网络/负载均衡瓶颈,检查集群调度
5.3 基准报告模板
建议每次压测都产出一份结构化报告,方便横向对比与追溯:
## 推理基准报告
- 日期 / 环境 / 引擎版本 / 模型与量化方式
- 硬件:GPU 型号 × 卡数,TP/PP/EP 配置
- SLO 目标:P99 TTFT < Xs,P95 ITL < Yms,Z tokens/s
### 结果表(并发扫描)
| 并发 | RPS | tokens/s | P50 TTFT | P99 TTFT | P95 ITL | P99 ITL |
|------|-----|----------|----------|----------|---------|---------|
| 1 | ... | ... | ... | ... | ... | ... |
| 32 | ... | ... | ... | ... | ... | ... |
### 结论
- 达到 SLO 的最大并发 / 最大吞吐
- 瓶颈分析(带宽 / 启动 / 队列)
- 下一步优化建议
有了模板,每次改动前后只要重跑同一套命令,diff 一份表格即可,避免"拍脑袋优化”。
六、总结
| 知识点 | 核心要点 |
|---|---|
| 延迟指标 | TTFT(感知)、ITL/TPOT(生成速度) |
| 吞吐指标 | RPS / tokens/s,吞吐与延迟不可兼得 |
| 曲线三区 | 空闲 / 线性 / 饱和,SLO 定在临界点 |
| 分位数 | 用 P50/P95/P99 而非平均值 |
| 压测工具 | LLM Perf(LLM 专用)、Locust(通用脚本) |
| 容量规划 | 单 token 成本 + 延迟-吞吐曲线 + 弹性伸缩 |
| 数据排查 | TTFT/ITL/显存/利用率四类信号对应四类问题 |
基准测试是推理工程的地基:没有可靠的数字,就无法回答"够不够用"“值不值"“哪里是瓶颈”。建议固定一套压测脚本与报告模板,每次改动(量化、并行、引擎升级)都重跑基线对比,形成团队的性能档案。工程化配套的 CI/CD 与监控可参考 https://plumephp.com/posts/devops/ 专题。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。