推理基准与压测:TTFT、ITL、TPOT 指标与容量规划实战

『我的推理服务到底能扛多大流量?』『加一张卡值不值?』『P99 为什么这么差?』这些问题都需要一套科学的基准测试方法。本文讲解 LLM 推理的核心性能指标 TTFT / ITL / TPOT,分析延迟与吞吐曲线,演示 LLM Perf 与 Locust 两类压测工具,最后给出容量规划与成本建模的完整方法。一、推理性能指标体系

“我的推理服务到底能扛多大流量?““加一张卡值不值?““P99 为什么这么差?“这些问题都需要一套科学的基准测试方法。本文讲解 LLM 推理的核心性能指标 TTFT / ITL / TPOT,分析延迟与吞吐曲线,演示 LLM Perf 与 Locust 两类压测工具,最后给出容量规划与成本建模的完整方法。

一、推理性能指标体系

1.1 延迟指标:TTFT / ITL / TPOT

LLM 推理与普通 API 不同——它不是一次性返回,而是流式逐 token 生成。因此延迟被拆成多个阶段:

指标全称含义量级
TTFTTime To First Token请求发出到收到第一个 token 的时间数百 ms ~ 数 s
ITLInter-Token Latency相邻两个 token 的间隔(decode 速度)10~100 ms
TPOTTime Per Output Token每个输出 token 的平均耗时(含首 token)与 ITL 相近
e2eEnd-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一半请求快于该值体感典型值
P9595% 请求快于该值常规 SLO
P9999% 请求快于该值严格 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
每日输出 token1500 × 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 + FP8P99 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. 定义 SLOP99 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/ 专题。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「ai」更多文章

  1. 时序预测实战:从 ARIMA 到时序基础模型
  2. 模型压缩:量化、剪枝、蒸馏与部署优化实战
  3. 量化感知训练(QAT)与量化微调:伪量化、STE 与 QLoRA 实战