大模型推理 Benchmark 自动化平台设计从压测脚本到指标看板的全链路实践一、当压测脚本比论文多——推理性能评估的标准化困局在一次 vLLM 版本升级0.4.2 → 0.5.0后团队发现 Qwen2-72B 的 TPOTTime Per Output Token从 18ms 上升到了 23ms——约 27% 的劣化。问题在于这个劣化直到上线 3 天后才被用户反馈感觉变慢了而暴露。事后复盘发现每次推理引擎升级或模型切换时的性能验证依赖的是一个半手工的压测脚本修改参数、手动执行、截图存图、Excel 画图。这套流程有三个致命缺陷不可复现压测参数靠人工记忆这次测了 1K prompt length下次忘了配。无法对比历史基准数据散落在不同人的电脑上找不到上一版本的 TTFT 是多少。缺乏异常检测只能用肉眼对比折线图一个小幅劣化 5%几乎一定会被漏掉。本文介绍一套推理 Benchmark 自动化平台的设计——从压测引擎、数据管道到指标看板的全链路方案目标是将推理性能验证从人肉操作收敛到一键出报告。二、平台架构与数据流设计——分层解耦的 Benchmark 管道平台采用分层架构将压测执行、数据采集、指标计算和可视化四层解耦。每一层的输出都有明确的 Schema确保层与层之间可以独立演进。flowchart LR subgraph A[压测执行层] A1[Benchmark Engine] A2[模型/参数配置] end subgraph B[数据采集层] B1[Metrics Collector] B2[原始结果存储] end subgraph C[计算分析层] C1[聚合计算] C2[回归检测] C3[趋势分析] end subgraph D[可视化层] D1[指标看板] D2[对比报告] D3[异常告警] end A1 --|JSON 结果流| B1 B1 --|时序数据| B2 B2 -- C1 C1 -- C2 C1 -- C3 C2 -- D3 C3 -- D1 C1 -- D1 C1 -- D22.1 关键指标定义推理性能评估不能只看平均延迟需要在不同维度上分解指标定义为什么重要TTFT(Time To First Token)从请求发出到收到第一个 token 的延迟影响首字感知延迟决定用户卡不卡TPOT(Time Per Output Token)每个输出 token 的平均生成时间影响生成速度长文本场景的核心Throughput(req/s)单位时间完成的请求数吞吐能力决定服务成本Token Throughput(tokens/s)单位时间生成的 token 数排除 prompt 长度差异后的可比指标P50/P95/P99 延迟不同分位的延迟分布发现长尾问题QPS vs Latency 曲线吞吐率与延迟的关系曲线找到容量规划的最佳工作点2.2 压测场景矩阵不同场景对性能的压力点完全不同Benchmark 平台需要支持场景矩阵场景矩阵 模型 × Prompt长度 × Output长度 × 并发度短 Prompt 高并发128 tokens prompt, 64 tokens output, 高并发测调度吞吐。长 Prompt 低并发8192 tokens prompt, 512 tokens output测 Prefill 延迟和 KV Cache 压力。混合流量prompt 长度正态分布60% 短 30% 中 10% 长模拟线上真实分布。三、压测引擎与数据管道实现3.1 基于 locust 的推理压测引擎选用 locust 而非 wrk/vegeta 的原因locust 原生支持动态参数生成、多阶段压测和 Python 回调便于模拟复杂的推理请求模式。# inference_benchmark.py —— 推理压测任务定义 from locust import HttpUser, task, between, events import random import json import time # # 推理 Benchmark 配置 # BENCH_CONFIG { # 三档 prompt 长度按线上流量分布配比 prompt_templates: { short: {length: 128, weight: 60}, # 60% 短 prompt medium: {length: 1024, weight: 30}, # 30% 中 prompt long: {length: 8192, weight: 10}, # 10% 长 prompt }, output_max_tokens: 512, # 最大输出长度 temperature: 0.0, # 固定为 0确保可复现 } # # 根据权重选择 prompt 模板 # def select_prompt_template(): templates BENCH_CONFIG[prompt_templates] rand random.randint(1, 100) cumulative 0 for name, cfg in templates.items(): cumulative cfg[weight] if rand cumulative: return name, cfg return short, templates[short] # # 生成指定长度的填充 prompt保持 token 长度一致 # def generate_prompt(target_length: int) - str: # 使用固定的填充文本确保 token 数可复现 # hello * 2 1 token (在 Qwen tokenizer 下近似) base hello world * 50 return base * (target_length // len(base.split()) 1) class InferenceUser(HttpUser): # 模拟用户思考时间13 秒 wait_time between(1, 3) task def chat_completion(self): # 1. 选择 prompt 模板 name, cfg select_prompt_template() prompt generate_prompt(cfg[length]) # 2. 构造请求 payload { prompt: prompt, max_tokens: BENCH_CONFIG[output_max_tokens], temperature: BENCH_CONFIG[temperature], stream: False, # 非流式测试整体延迟 } # 3. 记录请求开始时间用于计算 TTFT start_time time.time() with self.client.post( /v1/completions, jsonpayload, catch_responseTrue, # 推理请求超时设为 300 秒避免长 prompt 被误判超时 timeout300, ) as response: elapsed time.time() - start_time if response.status_code 200: data response.json() # 4. 提取推理性能指标 usage data.get(usage, {}) prompt_tokens usage.get(prompt_tokens, 0) completion_tokens usage.get(completion_tokens, 0) total_tokens usage.get(total_tokens, 0) # 5. 上报自定义指标到 locust # 这些指标会被收集到 CSV 报告中 response.request_meta.update({ prompt_tokens: prompt_tokens, completion_tokens: completion_tokens, total_tokens: total_tokens, total_latency_ms: elapsed * 1000, # TPOT 总延迟 / 输出 token 数ms/token tpot_ms: (elapsed * 1000 / completion_tokens if completion_tokens 0 else 0), }) else: response.failure(fHTTP {response.status_code}: {response.text}) # # 自定义事件将每次请求的详细指标写入 CSV # events.request.add_listener def on_request_complete(request_type, name, response_time, response_length, exception, **kwargs): # 将每次请求的详细数据追加到 CSV import csv, os csv_path os.environ.get(BENCH_CSV_OUTPUT, bench_results.csv) file_exists os.path.exists(csv_path) with open(csv_path, a, newline) as f: writer csv.writer(f) if not file_exists: writer.writerow([ timestamp, request_type, response_time_ms, prompt_tokens, completion_tokens, total_tokens, tpot_ms, error ]) writer.writerow([ time.time(), name, response_time, kwargs.get(prompt_tokens, 0), kwargs.get(completion_tokens, 0), kwargs.get(total_tokens, 0), kwargs.get(tpot_ms, 0), exception is not None, ])3.2 数据管道从 Raw CSV 到结构化指标压测引擎产出的 CSV 是原始逐请求数据需要通过数据管道转换为聚合指标# metrics_pipeline.py —— 将原始 CSV 聚合成可对比的指标 import pandas as pd import numpy as np from dataclasses import dataclass, asdict from typing import Dict import json dataclass class BenchmarkReport: 单次 Benchmark 的聚合指标报告 # 标识信息 model_name: str engine_version: str benchmark_id: str # 唯一标识用于跨版本对比 timestamp: str # 核心延迟指标 ttft_p50_ms: float ttft_p95_ms: float ttft_p99_ms: float tpot_p50_ms: float tpot_p95_ms: float tpot_p99_ms: float # 吞吐指标 throughput_rps: float # 每秒请求数 token_throughput_tps: float # 每秒 token 数 # 成功率 success_rate: float # 按 prompt 长度分组的明细用于分析不同负载下的表现 by_prompt_length: Dict[str, dict] def compute_benchmark_report(csv_path: str, model_name: str, engine_version: str) - BenchmarkReport: 从压测结果 CSV 计算聚合指标报告 # 1. 读取原始数据 df pd.read_csv(csv_path) # 2. 过滤错误请求 success_df df[df[error] False].copy() error_count len(df) - len(success_df) # 3. 计算核心百分位延迟排除无限大值 ttft success_df[response_time_ms].clip(upper300000) # 上限 5 分钟 # 4. 按 prompt 长度分组计算 grouped {} # short: prompt_tokens 256 # medium: 256 prompt_tokens 2048 # long: prompt_tokens 2048 for label, (lo, hi) in [ (short, (0, 256)), (medium, (256, 2048)), (long, (2048, np.inf)), ]: subset success_df[ (success_df[prompt_tokens] lo) (success_df[prompt_tokens] hi) ] if len(subset) 0: grouped[label] { count: len(subset), tpot_p50: np.percentile(subset[tpot_ms].clip(upper1000), 50), tpot_p95: np.percentile(subset[tpot_ms].clip(upper1000), 95), avg_latency_ms: subset[response_time_ms].mean(), } # 5. 计算吞吐整个压测时段的平均吞吐 total_duration df[timestamp].max() - df[timestamp].min() total_requests len(success_df) total_tokens success_df[total_tokens].sum() return BenchmarkReport( model_namemodel_name, engine_versionengine_version, benchmark_idf{model_name}_{engine_version}_{pd.Timestamp.now().strftime(%Y%m%d%H%M%S)}, timestamppd.Timestamp.now().isoformat(), ttft_p50_msfloat(np.percentile(ttft, 50)), ttft_p95_msfloat(np.percentile(ttft, 95)), ttft_p99_msfloat(np.percentile(ttft, 99)), tpot_p50_msfloat(np.percentile( success_df[tpot_ms].clip(upper1000), 50)), tpot_p95_msfloat(np.percentile( success_df[tpot_ms].clip(upper1000), 95)), tpot_p99_msfloat(np.percentile( success_df[tpot_ms].clip(upper1000), 99)), throughput_rpsfloat( total_requests / total_duration if total_duration 0 else 0), token_throughput_tpsfloat( total_tokens / total_duration if total_duration 0 else 0), success_ratefloat( len(success_df) / len(df) if len(df) 0 else 0), by_prompt_lengthgrouped, )四、回归检测与历史对比——从看图说话到数据决策收集指标的最终目的是回答这次变更是提升了还是劣化了4.1 基于统计检验的回归判定不能仅凭均值降低了就判定劣化。当各请求延迟存在较大方差时推理场景中极为常见需要通过统计方法判断差异是否显著回归检测流程 1. 从新版本的延迟分布和基准版本的延迟分布各取样本 2. 执行 Mann-Whitney U 检验不需要正态分布假设 3. 若 p 0.05 且效应量中位数差异 / 基准中位数 阈值如 5% → 判定为显著劣化4.2 平台化的成本考量Benchmark 平台的运行需要 GPU 资源来执行压测。持续集成CI中每提交跑一次完整 Benchmark 的 GPU 成本不低。实际落地的折中方案PR 级别仅运行快速冒烟测试1 个场景、30 秒防止显著劣化合入。Nightly 级别完整场景矩阵约 2 小时每日夜间自动执行。Release 级别全场景矩阵 多模型横向对比生成完整报告。这套分层策略在 GPU 成本可控的前提下将上线后才发现劣化的概率降低了约 80%。五、总结推理性能的可信度建立在可量化的基准数据上。从脚本化到平台化核心改进点有三标准化压测矩阵按 prompt 长度、output 长度、并发度建立场景矩阵每次变更在相同的标准下重跑消除参数差异引入的噪声。结构化数据管道从 locust 产出的原始 CSV 到聚合报告每一步的输出都有固定的 Schema。版本间的性能对比不再依赖肉眼看图而是结构化的回归检测。分层执行策略PR 级快速冒烟 → Nightly 完整矩阵 → Release 全量报告在 GPU 成本和检测覆盖之间取得平衡。