多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

大促流量一来,大模型服务就卡——用TaoToken做推理性能压测:TTFT/TPOT 指标怎么读

大促流量一来,大模型服务就卡——用TaoToken做推理性能压测:TTFT/TPOT 指标怎么读 1. 大促零点首字延迟从 0.8 秒飙到 15 秒先说一个我印象很深的场景。某电商智能导购平时用户发一句话0.8 秒就能看到第一个字蹦出来体验很顺。大促零点流量涨了大概 10 倍监控曲线立刻变形TTFTTime To First Token首 token 延迟从 0.8 秒飙到 15 秒用户发完问题盯着空白对话框等十几秒然后关掉页面走人更麻烦的是部分请求排队超时直接 504客户端重试又把流量翻倍形成恶性循环。事后排查三个问题叠在一起。第一没做过容量规划上线时的并发水位全凭拍脑袋。第二推理服务没设并发限制高并发下 KV cache 被打满continuous batching 频繁抢占单请求生成速度TPOT也跟着恶化——不只是等得久连出字都变慢了。第三没有降级预案模型卡死时连一句稍后再试都给不出。复盘会上性能负责人说了一句大实话这不是模型不行是我们从来没按大模型的方式压过它。传统接口压测看 QPS 和 RT大模型压测看的是用户等多久看到第一个字、每个字隔多久蹦出来。指标体系换了容量模型也换了。这篇就把这套压测方案拆开讲清楚TTFT、TPOT 到底怎么读并发梯度怎么设结果怎么采集最后怎么输出一份可复现的压测报告。适合正在做推理服务容量规划、或者被大促流量教育过的后端和测试同学。核心检索词先摆出来大模型推理性能压测重点指标是 TTFT 和 TPOT目标是找到并发拐点、定出生产水位。下面所有脚本和配置都可以直接复制去跑。2. 用 TaoToken 统一 Key 接入压测流量压测最烦的一件事是环境不统一。开发用一套 Key测试用一套线上又是另一套压出来的数据没法横向对比。我试过用 TaoToken 做统一入口把压测流量都走同一个 API 通道好处是 Base URL 和 Key 固定换模型只改 Model ID压测脚本不用动。TaoToken 在这里扮演的角色是统一的模型调用通道你拿到一个 Key就能通过兼容 OpenAI 协议的接口去请求不同模型压测脚本里只需要维护一份配置。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 地址不带 UTM 参数。为什么压测要用统一通道因为你要对比的是不同并发下的延迟曲线如果每次压测的接入点都不一样网络链路、鉴权开销、网关行为都会变曲线就没有可比性。统一 Key 之后变量只剩并发数和模型本身结论才站得住。具体操作分三步。第一步去控制台创建 API Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 在 API Keys 页面生成一个专用 Key建议压测单独用一个别和线上业务混用方便随时吊销。第二步确认你要压的模型 ID可以在模型对话页面先手动发一条请求验证连通性地址 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。第三步把 Base URL、Key、Model ID 三件套写进压测脚本的配置里。这里要提醒一句压测流量一定要和线上业务流量隔离。用独立 Key 的好处是一旦压测把配额打满或者触发限流不会影响真实用户。另外压测前先确认你的账号配额和并发上限别压到一半被限流那曲线就废了。如果你是要长期做编码类或 Agent 类的高频调用压测可以考虑 Coding Plan地址 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 它的配额模型更适合持续压测场景。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 参数细节以文档为准。3. 可复制的压测脚本与并发梯度配置这一节是重点直接给可复制的配置和脚本。先给一份 settings 风格的配置文件把 Base URL、Key、Model ID 三件套固定下来路径和字段名你可以按自己项目改但结构保持一致。{ base_url: https://taotoken.net/api, api_key: sk-your-pressure-test-key, model_id: your-model-id, timeout_seconds: 60, stream: true, concurrency_ladder: [1, 5, 10, 20, 40, 60], requests_per_step: 200, warmup_requests: 10 }注意 base_url 用 https://taotoken.net/api 不要加多余路径。api_key 换成你在控制台生成的那个。model_id 填你要压的模型。stream 必须为 true因为 TTFT 和 TPOT 只有在流式响应下才有意义——非流式你只能拿到总耗时拆不出首字延迟和出字间隔。接下来是压测器核心代码用 Python 的 asyncio aiohttp 实现流式请求逐 token 打时间戳。import asyncio import time import json import aiohttp with open(config.json, r, encodingutf-8) as f: CFG json.load(f) URL f{CFG[base_url]}/v1/chat/completions HEADERS { Authorization: fBearer {CFG[api_key]}, Content-Type: application/json, } async def one_request(session, sem, question, out): async with sem: payload { model: CFG[model_id], messages: [{role: user, content: question}], stream: True, } t0 time.time() ttft None token_count 0 try: async with session.post(URL, headersHEADERS, jsonpayload, timeoutCFG[timeout_seconds]) as resp: if resp.status ! 200: out.append({error: resp.status}) return async for raw in resp.content: line raw.decode(utf-8, errorsignore).strip() if not line.startswith(data:): continue data line[5:].strip() if data [DONE]: break token_count 1 if ttft is None: ttft time.time() - t0 except Exception as e: out.append({error: str(e)}) return total time.time() - t0 out.append({ ttft: ttft, total: total, tokens: token_count, tpot: (total - ttft) / max(token_count - 1, 1), }) async def run_load(concurrency, n_req, questions): out [] sem asyncio.Semaphore(concurrency) async with aiohttp.ClientSession() as session: tasks [ one_request(session, sem, questions[i % len(questions)], out) for i in range(n_req) ] await asyncio.gather(*tasks) return summarize(out)这段代码的关键点用 Semaphore 控制并发数每个请求记录 ttft、total、tokens然后算出 tpot。tpot 的算法是 (total - ttft) / (tokens - 1)也就是首 token 之后每个 token 的平均间隔。如果 tokens 只有 1分母用 max 兜底避免除零。然后是汇总函数算 P50/P95/P99 和超时率、吞吐。import statistics def percentile(values, p): if not values: return None values sorted(values) k (len(values) - 1) * p / 100 f int(k) c min(f 1, len(values) - 1) return values[f] (values[c] - values[f]) * (k - f) def summarize(out): ok [r for r in out if ttft in r and r[ttft] is not None] err [r for r in out if error in r] ttfts [r[ttft] for r in ok] tpots [r[tpot] for r in ok] total_tokens sum(r[tokens] for r in ok) wall max((r[total] for r in ok), default1) return { count: len(out), ok: len(ok), error: len(err), timeout_rate: len(err) / max(len(out), 1), ttft_p50: percentile(ttfts, 50), ttft_p95: percentile(ttfts, 95), ttft_p99: percentile(ttfts, 99), tpot_p50: percentile(tpots, 50), tpot_p95: percentile(tpots, 95), throughput_tokens_per_s: total_tokens / wall, }最后是阶梯加压主循环从低并发逐步往上压每一步跑固定请求数记录曲线。def capacity_test(questions): curve [] for c in CFG[concurrency_ladder]: print(f concurrency{c} ) metrics asyncio.run(run_load(c, CFG[requests_per_step], questions)) curve.append((c, metrics)) print(json.dumps(metrics, ensure_asciiFalse, indent2)) return curve def find_knee(curve): # 找 TTFT P95 开始非线性上涨的并发点 prev None for c, m in curve: p95 m[ttft_p95] if p95 is None: continue if prev is not None and p95 prev * 2: return c prev p95 return curve[-1][0]find_knee 的逻辑是当某一档并发的 TTFT P95 相比上一档翻倍以上就认为到了拐点。实际用的时候可以调这个倍数阈值2 倍是个保守起点。用例集很重要别全用短问题。要按线上真实长度分布配比比如 70% 短问答、20% 中等、10% 长上下文。长上下文吃 KV cache 的速度是短请求的几十倍全用短问题压出来的容量是假的。4. 跑一轮验证从请求到延迟曲线配置和脚本都齐了现在跑一轮看结果。先做连通性验证用 curl 发一条流式请求确认 Base URL、Key、Model ID 三件套没问题。curl -N https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-your-pressure-test-key \ -H Content-Type: application/json \ -d { model: your-model-id, messages: [{role: user, content: 用一句话解释什么是首token延迟}], stream: true }如果返回是一行行data: {...}然后以data: [DONE]结束说明流式通道正常。如果返回 401说明 Key 有问题如果返回 404多半是路径写错了检查是不是漏了/v1/chat/completions。连通性没问题后跑阶梯压测。假设你的并发梯度是 1、5、10、20、40、60每档 200 个请求你会看到类似这样的输出数值是示例实际以你的环境为准并发TTFT P95TPOT P95超时率吞吐 tokens/s10.9s45ms0%12051.1s52ms0%480101.4s68ms0%820203.2s110ms0.5%1050409.8s210ms4%9806015.1s340ms12%760读这张表有几个要点。并发从 10 涨到 20TTFT P95 从 1.4 秒跳到 3.2 秒翻了一倍多这就是拐点信号。到 40 和 60TTFT 继续非线性上涨同时吞吐不升反降——从 1050 掉到 980 再到 760。这说明越过拐点后系统把资源都花在抢占和排队上实际产能反而下降。压得越狠死得越快这句话在推理服务上是真的。TPOT 的变化也值得看。并发 10 时 TPOT P95 是 68ms到 40 变成 210ms出字明显变慢。这说明解码阶段资源吃紧典型原因是 KV cache 不足导致批处理抢占或者显存带宽打满。如果 TTFT 正常但 TPOT 恶化问题在解码段反过来 TTFT 高 TPOT 正常则是排队或预填充瓶颈。两个指标分开看才能定位到推理管线的哪一段。根据这轮结果拐点在并发 20 左右生产水位应该压在拐点的 70% 以下也就是 14 左右。告警线设在拐点 50%即 10给扩容和限流留反应时间。修复动作三条限流到拐点 70%、超限请求进排队页、备用小模型兜底短问答。这三条在压测报告里都要有对应演练用例。5. 常见报错排查401、local proxy failed、reading choices压测过程中最容易撞的几个报错逐个说清楚。401 Unauthorized。最常见的原因是 Key 写错或者带了多余空格。检查 config.json 里的 api_key 字段确认没有换行符和空格。另一个原因是 Key 被吊销或者配额耗尽去控制台 API Keys 页面确认状态。还有一种情况是 Authorization 头拼错了正确格式是Bearer sk-xxxBearer 和 Key 之间一个空格。local proxy failed / connection refused。这个报错通常出现在你本地配了代理但代理没起来或者端口不对。压测脚本走的是直连如果你环境里有代理变量http_proxy、https_proxyaiohttp 会尝试走代理代理不通就报这个。解决办法是在脚本里显式禁用代理或者清掉环境变量。注意这里说的是本地网络配置问题不是让你去搞什么特殊网络工具就是检查环境变量而已。# 在 aiohttp 里显式不走代理 async with aiohttp.ClientSession(trust_envFalse) as session: ...trust_envFalse 会让 aiohttp 忽略环境里的代理设置直连目标地址。reading choices / 解析响应失败。这个报错说明脚本在解析流式响应时没拿到预期的字段。常见原因有三个。第一模型返回的不是流式格式检查 payload 里 stream 是不是 true。第二响应里混入了非 data 行比如空行或者注释行脚本里已经用startswith(data:)过滤了但如果格式有变化要相应调整。第三请求被限流返回的是错误 JSON 而不是流式数据这时候要先看 resp.status 是不是 200。建议在 one_request 里加一层状态码判断非 200 直接记录错误码别硬解析。OAuth / 鉴权相关报错。如果你用的是某些需要 OAuth 流程的客户端可能会遇到 token 过期。压测场景建议直接用 API Key别走 OAuth减少变量。如果你确实在用 Codex 这类工具它的 auth.json 里存的是凭证压测前确认凭证有效。涉及 Base URL、Key、Model ID 三件套的地方一定要三个都对齐缺一个都会报鉴权或路由错误。超时率偏高但 TTFT 正常。这种情况多半是长请求拖尾。检查你的用例集里长上下文占比是不是太高或者 timeout_seconds 设得太短。压测时超时阈值要设得比线上宽松一些否则会把正常的长请求误判为超时。排查顺序建议先 curl 验证连通性再单请求跑通脚本再小并发试跑最后上阶梯。别一上来就 60 并发出了问题分不清是脚本 bug 还是服务瓶颈。6. 把压测沉淀成可复现的报告压测做完报告怎么写才可复现核心是让别人拿着你的报告能重跑一遍得到相近结论。报告模板包含这几块环境信息、用例集描述、并发梯度、指标结果、拐点结论、容量建议。环境信息要写清楚 Base URL、Model ID、压测机配置、压测时间。Key 不要写进报告写使用独立压测 Key即可。用例集描述要写长度分布比如短问答 70%、中等 20%、长上下文 10%以及每档请求数。并发梯度写清楚从几到几、步长多少。指标结果就是那张表TTFT P50/P95/P99、TPOT P50/P95、超时率、吞吐。拐点结论写 find_knee 算出来的并发数。容量建议写生产水位、告警线、限流策略。三条工程纪律再强调一遍。第一压测流量要混长短文本全用短问题压出来的容量是假的。第二压测环境必须含网关和排队层只压模型裸服务会漏掉网关超时、队列积压这些真实瓶颈。第三每次模型或推理框架升级都要重跑容量曲线换个模型版本拐点可能移动 30%容量结论是有保质期的。如果你要长期做这类压测建议把脚本和配置放进版本管理每次压测记录 commit hash这样报告和代码能对上。模型对话页面可以用来快速验证单个模型的连通性和基本延迟地址 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 参数有疑问先查文档。API Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 压测专用 Key 建议单独建一个用完随时吊销。最后留一个实操建议第一次跑阶梯压测时把每档请求数设小一点比如 50先看曲线形状对不对再加大到 200 拿稳定数据。压测本身也会消耗配额别一上来就猛压。找到拐点之后把拐点 70% 作为生产水位写进容量规划文档下次大促前重跑一遍确认拐点没漂移这事就算闭环了。
返回列表