大模型推理压测实战:数据集、TTFT 与尾延迟怎么定,TaoToken 统一 Key 通道下的可复现方案
1. 为什么你的压测报告一上线就被打脸大模型推理压测这件事最怕的不是指标不好看而是指标太好看。我见过太多团队拿着实验室里跑出来的 TTFT 200ms、TPOT 45 Tokens/s 去汇报结果金丝雀环境跑了不到半小时告警群就炸了。问题出在哪出在压测数据集和线上真实流量之间隔了一整个太平洋。大模型推理压测的核心检索词就三个数据集怎么构造、TTFT 怎么定义、尾延迟怎么统计。这三个东西如果口径不对后面所有的调优都是自嗨。适合谁看适合正在做推理服务上线前性能验证的工程团队尤其是用 vLLM、TensorRT-LLM 这类引擎部署又需要一套可复现压测方案的场景。先说数据集。很多人的压测集就是手写几十条单轮问答平均 Prompt 长度 120 Token 左右。这种微型 Payload 打到推理引擎上Prefill 阶段 GPU 几毫秒就算完了KV-Cache 管理、Chunked Prefill 配置缺失、CUDA Kernel 调度这些真正的瓶颈根本暴露不出来。线上真实流量是什么样短请求、长文档、多轮连续追问混在一起Prompt 长度分布可能从 50 Token 到 8000 Token 都有输出长度也参差不齐。你拿一个均值 120 Token 的测试集去压等于用自行车测高速公路的承载能力。再说 TTFT。首字延迟的定义看起来很简单从请求发出到第一个 Token 返回的时间。但工程上有个坑——流式响应里第一个 chunk 可能只包含 role 字段没有实际文本内容。如果你把收到第一个 chunk 的时间当成 TTFT那测出来的数字会偏小因为真正的首 Token 还没生成。正确的口径应该是第一个包含非空 text 的 chunk 到达时间。这个细节不统一不同团队测出来的 TTFT 能差出 30% 以上。最后说尾延迟。平均值在推理场景下基本没有参考价值。GPU 在 Prefill 阶段是计算密集型Decode 阶段是内存带宽密集型长 Context 下计算复杂度按序列长度增长TTFT 的尾部延迟会非常严重。大量短 Prompt 会把平均值拉得很漂亮但 P99 可能已经飙到几秒了。用户感知到的卡顿从来不是平均值决定的而是那 1% 的长请求。所以压测设计的第一步不是调引擎参数而是把数据集、指标口径、统计窗口这三件事定死。下面我会给出一套在 TaoToken 统一 Key 通道下可复现的方案包括数据集构造脚本、压测客户端参数模板、指标采集口径和结果校验步骤。你拿到之后可以直接改改就用也可以作为团队内部压测规范的起点。2. TaoToken 统一 Key 通道的前置准备在开始写压测脚本之前需要先把请求通道固定下来。压测最忌讳的就是每次请求走的通道不一样导致网络抖动、鉴权差异、路由变化混进性能数据里。TaoToken 在这里的角色是提供一个统一的 API 入口让压测客户端只需要关心 Base URL、Key 和 Model ID 三个东西不用为每个模型单独配一套鉴权逻辑。先明确三个核心参数。Base URL 用https://taotoken.net/api这个地址不加任何 UTM 参数直接作为 API 请求的根路径。API Key 在控制台的 API Keys 页面生成建议为压测单独建一个 Key方便后续按 Key 维度统计用量和排查问题。Model ID 根据你要压的模型填比如qwen2.5-72b-instruct、claude-3-5-sonnet这类具体以模型对话页面列出的可用模型为准。如果你用的是 Claude Code 或者 Cline 这类编码工具做压测脚本开发可以在工具里配置自定义 API 端点。以 Claude Code 为例需要设置三个环境变量或者配置文件项Base URL 指向https://taotoken.net/apiAPI Key 填你生成的 KeyModel ID 填你要压的模型。这三件套配好之后工具发出的请求就会走统一通道压测脚本里也可以复用同一套配置。对于需要长期跑压测或者做 Agent 场景验证的团队Coding Plan 会比按量计费更划算尤其是你要反复跑不同参数组合的对比测试时。接入文档里有完整的端点说明和请求示例建议在写脚本前先过一遍确认流式响应的 chunk 结构和错误码定义。这里有个实操建议压测用的 Key 和线上业务用的 Key 分开。压测过程中可能会产生大量并发请求如果和线上混用一方面用量统计会乱另一方面万一压测把配额打满会影响线上服务。TaoToken 的控制台支持多 Key 管理建一个专门给压测用的 Key跑完测试直接禁用或者轮换干净利落。通道固定之后压测客户端的配置就可以标准化了。下面给一个 JSON 配置模板你可以直接复制到项目里把 Key 和 Model ID 替换成自己的{ api_base: https://taotoken.net/api, api_key: sk-your-benchmark-key, model_id: qwen2.5-72b-instruct, timeout_seconds: 60, max_concurrency: 200, stream: true, temperature: 0.0 }这个配置里的temperature设为 0.0 是为了保证输出确定性压测时不需要模型发挥创造力要的是可复现。stream必须为 true否则你拿不到 TTFT 数据。max_concurrency根据你的压测客户端机器性能和目标 QPS 调整后面会讲怎么定这个值。通道准备好之后下一步就是构造数据集。数据集的质量直接决定压测结果能不能反映线上真实情况这部分我会在下一节展开包括从线上 Trace 采样、长度分布拟合、以及如何生成泊松分布的并发请求。3. 可复制的压测配置与数据集构造数据集构造是整个压测里最容易被敷衍的环节。很多人直接从网上找几个 prompt 就开跑结果测出来的数字和线上表现完全对不上。正确的做法是从线上历史请求里采样提取 Prompt 长度和 Completion 长度的分布然后按这个分布生成测试集。具体操作分三步。第一步从线上网关或者推理服务的日志里导出最近一周的请求记录至少包含 prompt_tokens 和 completion_tokens 两个字段。如果没有现成的日志可以在网关层加一个轻量探针记录每个请求的输入输出长度跑一天就能拿到足够的样本。第二步对这两个长度序列做分布拟合通常 Prompt 长度符合对数正态分布Completion 长度更接近指数分布。第三步用拟合出来的分布参数生成测试集同时保证短请求、长请求、多轮请求的比例和线上一致。下面是一个数据集构造的 Python 脚本你可以直接拿去改import numpy as np import json from typing import List, Dict def build_dataset( num_samples: int 1000, prompt_mu: float 5.5, prompt_sigma: float 1.2, completion_scale: float 200.0, long_ratio: float 0.15 ) - List[Dict]: dataset [] for i in range(num_samples): # 对数正态分布生成 Prompt 长度 prompt_len int(np.random.lognormal(prompt_mu, prompt_sigma)) prompt_len max(50, min(prompt_len, 8000)) # 指数分布生成 Completion 长度 completion_len int(np.random.exponential(completion_scale)) completion_len max(16, min(completion_len, 2048)) # 按比例注入长上下文请求 if np.random.random() long_ratio: prompt_len int(prompt_len * 3) prompt_len min(prompt_len, 16000) dataset.append({ request_id: fbench-{i:05d}, prompt_tokens: prompt_len, max_tokens: completion_len, is_long_context: prompt_len 4000 }) return dataset if __name__ __main__: ds build_dataset(num_samples2000) with open(benchmark_dataset.json, w) as f: json.dump(ds, f, indent2) print(fGenerated {len(ds)} samples) print(fLong context ratio: {sum(1 for d in ds if d[is_long_context]) / len(ds):.2%})这个脚本生成的只是长度元数据实际压测时还需要把 prompt_tokens 和 max_tokens 转换成真实的文本。你可以准备一个 prompt 池按长度分桶每个请求从对应桶里随机抽一条。这样既保证了长度分布可控又避免了每次都要现生成文本的开销。压测客户端的参数模板如下核心是控制并发节奏和超时import asyncio import aiohttp import time import numpy as np from typing import List, Dict, Any class BenchmarkConfig: def __init__(self): self.api_base https://taotoken.net/api self.api_key sk-your-benchmark-key self.model_id qwen2.5-72b-instruct self.timeout aiohttp.ClientTimeout(total60) self.qps 20.0 self.max_connections 500 async def send_request( session: aiohttp.ClientSession, config: BenchmarkConfig, prompt: str, max_tokens: int, request_id: str ) - Dict[str, Any]: payload { model: config.model_id, prompt: prompt, max_tokens: max_tokens, stream: True, temperature: 0.0 } headers { Content-Type: application/json, Authorization: fBearer {config.api_key}, X-Request-ID: request_id } start time.perf_counter() first_token_time None token_times [] token_count 0 try: async with session.post( f{config.api_base}/v1/completions, jsonpayload, headersheaders, timeoutconfig.timeout ) as resp: if resp.status ! 200: return {status: ERROR, code: resp.status, request_id: request_id} async for line in resp.content: now time.perf_counter() text line.decode(utf-8).strip() if not text.startswith(data: ): continue data text[6:] if data [DONE]: break try: chunk json.loads(data) choices chunk.get(choices, []) if choices and choices[0].get(text): if first_token_time is None: first_token_time now token_times.append(now) token_count 1 except json.JSONDecodeError: continue except asyncio.TimeoutError: return {status: TIMEOUT, request_id: request_id} except Exception as e: return {status: EXCEPTION, error: str(e), request_id: request_id} end time.perf_counter() if first_token_time is None or token_count 0: return {status: EMPTY, request_id: request_id} ttft_ms (first_token_time - start) * 1000 total_ms (end - start) * 1000 tpot_ms np.mean(np.diff(token_times)) * 1000 if token_count 1 else 0.0 return { status: SUCCESS, request_id: request_id, ttft_ms: ttft_ms, tpot_ms: tpot_ms, total_ms: total_ms, tokens: token_count }这个客户端的关键点在于TTFT 只在第一个非空 text chunk 到达时记录TPOT 用相邻 token 时间差的均值计算总延迟从请求发出到流结束。这三个口径定死之后不同人跑出来的结果才有可比性。并发节奏用泊松分布控制模拟真实流量的突发性async def run_benchmark(dataset: List[Dict], config: BenchmarkConfig): connector aiohttp.TCPConnector(limitconfig.max_connections) results [] async with aiohttp.ClientSession(connectorconnector) as session: tasks [] for item in dataset: interval np.random.exponential(1.0 / config.qps) await asyncio.sleep(interval) task asyncio.create_task( send_request( session, config, promptfbenchmark prompt for {item[request_id]}, max_tokensitem[max_tokens], request_iditem[request_id] ) ) tasks.append(task) results await asyncio.gather(*tasks, return_exceptionsTrue) return [r for r in results if isinstance(r, dict) and r.get(status) SUCCESS]跑完一轮之后把结果存成 JSON后面做指标统计和对比分析都用这份原始数据。注意不要只存汇总值每个请求的 TTFT、TPOT、总延迟、token 数都要保留否则后面想换统计口径就得重跑。4. 验证请求与成功结果判读压测跑起来之后第一件事不是看性能数字而是确认请求真的成功了。我见过不少压测报告里面混了一堆 401、429、超时结果统计的时候没过滤把错误请求的延迟也算进去了数字自然好看得离谱。先做单请求验证。用 curl 或者 Python 发一条最简单的流式请求确认通道是通的curl -X POST https://taotoken.net/api/v1/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-your-benchmark-key \ -d { model: qwen2.5-72b-instruct, prompt: 用一句话解释什么是首字延迟, max_tokens: 64, stream: true, temperature: 0.0 }如果返回的是逐行data: {...}的流式 chunk最后以data: [DONE]结束说明通道正常。如果返回 401检查 Key 是否正确、是否被禁用如果返回 404检查 Model ID 是否拼错如果返回 429说明触发了限流需要降低 QPS 或者联系管理员调整配额。单请求通了之后跑一轮小规模压测比如 50 个请求、QPS 设 5观察结果。成功的结果应该满足几个条件所有请求的 status 都是 SUCCESSTTFT 分布合理短 Prompt 应该在 100-500ms 之间长 Prompt 可能到 1-2sTPOT 稳定在某个区间内没有大量 TIMEOUT 或 EMPTY。下面是一个结果统计的脚本把原始数据转成指标import json import numpy as np from typing import List, Dict def analyze_results(results: List[Dict]) - Dict: ttfts [r[ttft_ms] for r in results] tpots [r[tpot_ms] for r in results if r[tpot_ms] 0] totals [r[total_ms] for r in results] tokens [r[tokens] for r in results] return { total_requests: len(results), ttft_p50: np.percentile(ttfts, 50), ttft_p95: np.percentile(ttfts, 95), ttft_p99: np.percentile(ttfts, 99), tpot_p50: np.percentile(tpots, 50), tpot_p99: np.percentile(tpots, 99), total_p99: np.percentile(totals, 99), avg_tokens: np.mean(tokens), throughput_tokens_per_sec: sum(tokens) / (max(totals) / 1000) } if __name__ __main__: with open(benchmark_results.json) as f: results json.load(f) metrics analyze_results(results) for k, v in metrics.items(): print(f{k}: {v:.2f})判读结果时重点看三个东西。第一TTFT P99 是否超过预算。如果短 Prompt 的 TTFT P99 都超过 800ms说明 Prefill 阶段有问题可能是 Chunked Prefill 没开或者 Block Size 设置不合理。第二TPOT P99 是否稳定。如果 TPOT 波动很大说明 Decode 阶段的 Batch 调度有问题需要检查 max-num-seqs 和 gpu-memory-utilization 的配合。第三吞吐量是否随 QPS 线性增长。如果 QPS 增加但吞吐量不涨说明系统已经到瓶颈了延迟曲线会垂直上升。这里有个实操技巧把 QPS 从低到高跑几轮每轮记录 P99 延迟和吞吐量画一条 QPS-延迟曲线。曲线的拐点就是系统的容量上限。线上部署时把入口 QPS 控制在拐点的 70% 左右留出突发流量的余量。结果校验还有一个容易忽略的点失败请求的统计。TIMEOUT、EMPTY、ERROR 这些请求不能直接丢掉要单独统计比例。如果失败率超过 1%说明系统在高并发下不稳定需要先解决稳定性问题再谈性能优化。5. 常见报错与排查路径压测过程中最容易撞上的几个报错这里按出现频率排一下每个都给排查路径。第一个是 401 Unauthorized。报错信息通常是{error: {message: Invalid API key, type: invalid_request_error}}。排查步骤确认 Authorization header 格式是Bearer sk-xxx注意 Bearer 后面有一个空格确认 Key 没有过期或被禁用去控制台 API Keys 页面看一眼状态确认请求的 Base URL 是https://taotoken.net/api不要多加或少加路径段。如果 Key 是从环境变量读的打印出来确认没有多余的空格或换行。第二个是 local proxy failed 或者连接超时。这个报错通常出现在压测客户端机器上原因是并发连接数超过了本地文件描述符限制或者 TCP 连接池满了。排查步骤检查ulimit -n的值压测机器建议调到 65535检查 aiohttp 的 TCPConnector limit 设置不要超过系统限制如果用了代理确认代理配置没有干扰到 API 请求。另外压测客户端和 API 端点之间的网络延迟也要考虑如果 RTT 本身就很高TTFT 会被网络时间拉长。第三个是 reading choices 相关的解析错误。报错信息可能是KeyError: choices或者IndexError: list index out of range。这个通常是因为流式响应的某个 chunk 结构不符合预期比如心跳包或者空 chunk。排查步骤在解析 chunk 之前先判断choices是否存在且非空对 JSON 解析加 try-except跳过无法解析的行打印原始 chunk 内容确认服务端返回的结构。有些推理引擎在流式响应里会插入 usage 统计的 chunk这种 chunk 没有 choices 字段需要单独处理。第四个是 OAuth 或者鉴权相关的报错。如果你用的是 Claude Code 或者 Cline 这类工具报错可能是OAuth token expired或者authentication failed。排查步骤确认工具里配置的 Base URL、API Key、Model ID 三件套是否正确如果工具支持多种鉴权方式确认选的是 API Key 模式而不是 OAuth 模式检查工具的配置文件路径比如 Claude Code 的 settings.json 或者 Codex 的 auth.json确认字段名和值没有拼错。第五个是 429 Too Many Requests。这个说明触发了限流。排查步骤降低压测 QPS或者增加请求间隔检查是否有其他服务共用同一个 Key导致配额被抢占如果确实需要更高配额联系管理员调整。压测时建议把 429 的响应单独统计不要混在成功请求里算延迟。第六个是 EMPTY_RESPONSE。请求返回 200但没有任何 token 输出。这个通常是因为 max_tokens 设得太小或者 prompt 触发了内容过滤。排查步骤把 max_tokens 调到 64 以上换一个简单的 prompt 测试检查请求参数里有没有误设 stop 序列导致立即停止。排查的时候有个通用原则先用单请求复现再用小规模并发复现最后才上大规模压测。单请求能过、小并发能过、大并发出问题说明是容量或稳定性问题单请求就失败说明是配置或鉴权问题。把问题范围缩小之后排查效率会高很多。6. 把压测变成可复现的工程习惯压测做完一轮拿到数字然后呢很多团队的做法是写个报告就结束了下次要对比不同配置的时候发现数据集变了、统计口径变了、甚至 API Key 都换了根本没法比。要让压测真正有价值得把它变成一套可复现的工程习惯。第一件事是版本化。数据集、压测脚本、配置文件、结果数据全部进 Git。每次跑压测打一个 tag记录当时的模型版本、引擎参数、硬件配置。这样后面想复现某个结论的时候checkout 对应的 tag 就能重跑。第二件事是固定统计口径。TTFT 的定义、TPOT 的计算方式、P99 的统计窗口这些写在团队文档里所有人跑压测都用同一套。不要今天用第一个 chunk 的时间当 TTFT明天用第一个非空 text 的时间这样数据没法横向对比。第三件事是建立基线。选一个稳定的配置作为基线每次优化都跟基线比。基线要定期重跑因为模型版本、引擎版本、硬件状态都会变化。基线数据存下来作为回归测试的参考。第四件事是自动化。把压测脚本接到 CI 里每次模型更新或者引擎参数变更时自动跑一轮结果跟基线对比超过阈值就告警。这样能在上线前就发现性能退化而不是等用户投诉。TaoToken 在这个流程里的价值是提供稳定的请求通道。压测最怕的就是通道本身不稳定今天走这个端点明天走那个端点网络路径一变延迟数据就没法比了。统一 Key 通道把这个问题消掉让压测客户端只需要关心模型和参数不用操心鉴权和路由。如果你要长期做推理性能优化建议把压测脚本、数据集生成器、结果分析工具打包成一个内部工具团队里谁需要跑压测直接调工具就行不用每次重新搭环境。接入文档里有完整的 API 说明和示例模型对话页面可以快速验证模型可用性Coding Plan 适合需要反复跑对比测试的场景。API Keys 页面管理你的压测专用 Key控制台看用量和调用记录。最后说一个实操细节压测跑完记得把压测 Key 禁用或者轮换避免残留的脚本或者定时任务继续发请求。数据集和结果数据归档到对象存储本地只留最近几轮的避免仓库膨胀。压测报告里除了性能数字还要记录失败率、错误分布、资源利用率这些信息在排查问题时比单纯的延迟数字更有用。