ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

benchmark_runner.py 并发压测,Codex 接入 TaoToken 通道后跑通

benchmark_runner.py 并发压测,Codex 接入 TaoToken 通道后跑通 benchmark_runner.py 并发压测最尴尬的地方是 BittensorSubnetAdapter 里那句 await asyncio.sleep(0.5)——你跑完 1/8/32 三级并发TTFT、TPOT、P50/P95 全都有了但没有任何真实请求发生用量也就无从验证。TaoToken 在这里的角色很明确去 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建一把 Key把 Codex 的 Base URL 填成 https://taotoken.net/api让 Codex 帮你把模拟适配器改成真实调用再跑一次压测用返回的 JSON 和后台用量对账。这篇不打算把 benchmark_runner.py 重写成另一个项目而是跟着原文的三层结构走先看模拟适配器缺了什么再拆预热、Semaphore、百分位三个落点接着用 Codex 逐段核对最后用真实 TaoToken 通道跑通一次 1/8/32 并发确认请求成功并生成与原文 JSON 样例一致的延迟和吞吐输出。Codex 只负责读代码、对比逻辑、输出修订片段真正发请求、采基准、看用量仍然由 benchmark_runner.py 和读者本地终端完成。1. benchmark_runner.py 只做延时模拟时用量验证缺了什么1.1 BittensorSubnetAdapter 里那句 asyncio.sleep(0.5)原文的 BittensorSubnetAdapter 写得很清楚构造请求、dendrite.forward、超时、解析响应这些都在注释里实际执行的是await asyncio.sleep(0.5)。这意味着每次infer()固定消耗半秒左右TTFT 和 TPOT 都由这半秒除出来P50 和 P95 当然稳定得不像真实网络。模拟适配器的好处是让框架跑通坏处是你无法回答“这次压测到底消耗了多少 token”“并发升到 32 之后有没有被限流”“模型 ID 是否拼错”。这些问题在真实请求里才会暴露而恰好是验证用量的关键。没有真实请求用量报告只能写“模拟调用 5 次”而不是“成功 5 次总 token 数若干P50 TTFT 多少”。1.2 没有真实请求P50/P95 只是纸面数字原文的 BenchmarkReport 用statistics.median算 P50用int(len(vals) * 0.95)取 P95。逻辑本身没问题但它对输入非常敏感如果所有r.ttft都来自asyncio.sleep(0.5)排序后的分位数就是 500ms 上下P95 也不会离群。真实通道下TTFT 会受模型冷热、排队、网络往返、流式分块影响P95 才真正有诊断价值。你要验证的不只是“脚本能不能跑”而是“同一把 Key、同一个模型 ID在 1、8、32 三级并发下成功率和延迟分布是否还在可接受范围”。这需要把 BittensorSubnetAdapter 换成真实 HTTP 流式调用。1.3 TaoToken 在这里只做两件事给 Key、给 Base URL不需要把 TaoToken 想成基准采集器它不替 benchmark_runner.py 统计 TTFT/TPOT。它只提供两样东西一把可用的 API Key以及一个兼容 OpenAI 协议的 Base URLhttps://taotoken.net/api。Key 从 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建模型 ID 以模型广场当时列表为准。拿到这两样之后benchmark_runner.py 里的适配器才有真实目标可打。Codex 接入 TaoToken 通道的意义也在这里它用同一套 Base URL 和 Key 去读你的 benchmark_runner.py帮你核对预热、并发和百分位计算而不是替你执行压测。2. 三层基准模型在 benchmark_runner.py 里的落点预热、Semaphore、P50/P952.1 预热阶段不能省warmup_rounds 与冷启动原文的 BenchmarkEngine 有一个warmup_rounds默认 3入口里改成 2。它的设计决策写得很明白预热结果不纳入最终统计但预热失败的请求会记录到 report。这个逻辑在真实通道下更重要因为第一次请求可能触发模型加载或连接建立TTFT 会明显偏高。如果你直接把warmup_rounds设为 0P50 和 P95 会被第一条冷启动请求拉高尤其是 1 并发那组。让 Codex 核对这一段的重点是确认预热请求没有混进report.results同时确认预热用的 prompt 与正式采集用的是同一条避免引入额外变量。# 预热阶段消除冷启动影响 if self.warmup_rounds 0: warmup_req requests[0] for _ in range(self.warmup_rounds): await adapter.infer(warmup_req)上面这段不需要改结构只需要确认adapter.infer(warmup_req)在真实请求下不会抛异常后中断整个流程。如果预热失败你应该让它在 report 里留一条 error而不是让asyncio.gather直接崩掉。2.2 1/8/32 三级并发Semaphore 控制的是请求窗口原文用asyncio.Semaphore(self.concurrency)包住adapter.infer(req)这是最直观的并发限制。三级并发分别是 1、8、32对应低延迟单请求、中等并行、高吞吐批量三种场景。真实通道下32 并发可能触发限流或排队Semaphore 只控制你这一侧同时发出的请求数不控制服务端队列。semaphore asyncio.Semaphore(self.concurrency) async def bounded_infer(req: InferenceRequest): async with semaphore: return await adapter.infer(req) tasks [bounded_infer(r) for r in requests] results await asyncio.gather(*tasks)这段代码本身没问题但真实请求下要关注results里有没有successFalse。如果 32 并发出现大量 429 或超时先把并发降到 8 复测再检查 Key 的额度与模型限速。Semaphore 不应该被改成无限并发那样只会让错误率更难解释。2.3 P50/P95 与 Prometheus 输出别让异常请求污染分位数原文的 P50/P95 只统计r.success为 True 的结果这是对的。但 Prometheus 兼容输出如果直接把所有请求都算进去分位数会被失败请求的 0 值拉低。让 Codex 核对百分位计算时重点看三处第一p50_ttft和p95_ttft的vals是否过滤了success 第二avg_throughput是否只对成功请求求平均 第三success_rate是否用len(self.results)做分母而不是成功数做分母。property def p95_ttft(self) - float: vals sorted(r.ttft for r in self.results if r.success) if not vals: return 0 idx int(len(vals) * 0.95) return vals[min(idx, len(vals) - 1)]真实通道下P95 比 P50 更值得看。如果 P50 是 400ms、P95 是 1200ms说明大部分请求很快但尾部长。如果 P95 直接飙到 5000ms 以上先排查是不是 32 并发把服务端队列打满了。3. Codex 接入 TaoToken 通道后逐段核对 benchmark_runner.py3.1 在 ~/.codex/config.toml 里把 Base URL 指向 https://taotoken.net/apiCodex 的配置不走 ANTHROPIC_* 那一套它自己的文件是~/.codex/config.toml。你需要先打开 TaoToken 注册并创建 API Key然后把 Key 放进环境变量把 Base URL 写进 Codex 的 provider 配置。# ~/.codex/config.toml model YOUR_MODEL_ID model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chat模型 ID 不要凭记忆写去模型广场看当时可用的列表。环境变量里放占位符对应的真实 Keyexport TAOTOKEN_API_KEYYOUR_API_KEY注意 Base URL 末尾不要加/v1填https://taotoken.net/api即可。这个地址只用于工具连接不要和官网落地页混用。3.2 让 Codex 读 benchmark_runner.py先核对预热再核对并发最后核对百分位配置好 Codex 之后在项目目录里让它读取benchmark_runner.py但不要让它执行压测。你可以给它一段明确的核对指令按原文的三层结构逐段来请阅读当前目录下的 benchmark_runner.py。 1. 核对 BenchmarkEngine.run 里的预热逻辑预热请求是否被排除在 report.results 之外预热失败是否被记录。 2. 核对 semaphore 的并发控制1/8/32 三级并发是否只限制同时进行的 infer 数量有没有遗漏 await。 3. 核对 BenchmarkReport 的 p50_ttft 和 p95_ttft是否只统计 successTrue 的结果索引边界是否安全。 4. 指出 BittensorSubnetAdapter 里哪些部分是模拟如果替换成真实 OpenAI 兼容流式请求需要改哪些方法。 只输出修改建议和代码片段不要运行任何命令。Codex 会基于你给的上下文输出片段比如把BittensorSubnetAdapter替换成TaoTokenAdapter把asyncio.sleep(0.5)改成流式请求。它不会直接连你的生产环境也不会替你跑python benchmark_runner.py。3.3 Codex 不会替你跑压测它只输出修订建议和代码片段这里要拎清边界Codex 接入 TaoToken 通道后能帮你读代码、解释逻辑、对比原文与修订版差异但压测必须由你在本地终端执行。它不能代替asyncio.Semaphore发请求也不能代替statistics.median算分位数。你让它核对warmup_rounds、bounded_infer、p95_ttft之后下一步是自己把修订后的脚本存盘、安装依赖、运行。如果 Codex 建议你直接在对话里“模拟”一次 32 并发不要接受。压测的价值就在于真实请求模拟只会回到原文那个asyncio.sleep(0.5)的困境。4. 用真实请求替换 BittensorSubnetAdapter跑通 1/8/32 并发4.1 准备 Key 与模型 ID打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end真实请求需要三样东西API Key、Base URL、模型 ID。Key 在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建Base URL 固定填https://taotoken.net/api模型 ID 去模型广场看当时列表。不要写不存在的模型 ID也不要用旧文档里的日期后缀。export TAOTOKEN_API_KEYYOUR_API_KEY如果你用虚拟环境把openai和asyncio相关依赖装好pip install openai原文的 benchmark_runner.py 没有依赖 OpenAI SDK但真实流式请求用它更省事。你只需要把base_url指向 TaoToken 的兼容通道剩下的请求格式由 SDK 按 OpenAI 协议拼接。4.2 TaoTokenAdapter 的流式实现TTFT 与 TPOT 分开计时把BittensorSubnetAdapter替换成下面这个适配器。它用AsyncOpenAI发流式请求第一条 delta 到达时间记为 TTFT后续 delta 间隔记为 TPOT最后拼成InferenceResult。import os import time from openai import AsyncOpenAI class TaoTokenAdapter(NetworkAdapter): def __init__(self, model_id: str): self.client AsyncOpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlhttps://taotoken.net/api, ) self.model_id model_id def network_name(self) - str: return taotoken async def infer(self, request: InferenceRequest) - InferenceResult: start time.monotonic() first_token_time None token_times [] collected [] try: stream await self.client.chat.completions.create( modelself.model_id, messages[{role: user, content: request.prompt}], max_tokensrequest.max_tokens, temperaturerequest.temperature, streamTrue, ) async for chunk in stream: now time.monotonic() if first_token_time is None: first_token_time now delta chunk.choices[0].delta.content or if delta: token_times.append(now) collected.append(delta) end time.monotonic() ttft (first_token_time - start) * 1000 if first_token_time else 0 tpot [ (token_times[i] - token_times[i - 1]) * 1000 for i in range(1, len(token_times)) ] total_tokens len(token_times) total_time (end - start) * 1000 e2e_latency total_time tokens_per_second ( total_tokens / (total_time / 1000) if total_time 0 else 0 ) return InferenceResult( ttftttft, tpottpot, total_tokenstotal_tokens, total_timetotal_time, e2e_latencye2e_latency, tokens_per_secondtokens_per_second, successTrue, ) except Exception as e: return InferenceResult( ttft0, tpot[], total_tokens0, total_time0, e2e_latency0, tokens_per_second0, successFalse, errorstr(e), )这段代码里base_url只写https://taotoken.net/api不要加/v1也不要加官网那串 UTM 参数。model_id从模型广场取不要硬编码一个不存在的名字。4.3 修订后的入口三级并发与 JSON 输出原文的main()用for concurrency in [1, 8, 32]跑三级。你只需要把适配器换成TaoTokenAdapter并把采集结果输出成原文 JSON 样例的结构。可以加一个to_json()方法把BenchmarkReport转成字典。async def main(): prompts [ Explain the concept of zero-knowledge proofs in simple terms., Write a Solidity function that implements a simple escrow contract., Compare optimistic rollups and ZK rollups., Describe the architecture of a decentralized exchange., What is MEV and how does it affect Ethereum users?, ] requests [ InferenceRequest(promptp, max_tokens256) for p in prompts ] for concurrency in [1, 8, 32]: engine BenchmarkEngine(concurrencyconcurrency, warmup_rounds2) adapter TaoTokenAdapter(model_idYOUR_MODEL_ID) report await engine.run(adapter, requests) print(f\n--- Concurrency: {concurrency} ---) engine.print_report(report) print(report.to_json()) if __name__ __main__: asyncio.run(main())跑之前确认YOUR_MODEL_ID已经替换成模型广场里的真实 IDTAOTOKEN_API_KEY已经导出。然后本地执行python benchmark_runner.py如果一切正常你会看到每个并发级别下的 success_rate、TTFT P50/P95、平均吞吐以及一段 JSON。这段 JSON 对应原文的标准化输出格式包含 latency、throughput、reliability 等字段。真实请求和模拟请求的区别就是这些数字背后有 actual token 消耗可以去控制台对账。5. 压测输出与用量核对从 JSON 样例到控制台5.1 先看 success_rate 和 error 字段脚本跑完后第一眼不要看 P50先看 success_rate。如果 1 并发是 100%8 并发掉到 90%32 并发掉到 60%说明并发升高后服务端开始拒绝或超时。再看report.results里失败请求的error常见是 429 限流、超时、模型 ID 不存在。Requests: 5, Success: 100.0% TTFT P50: 420ms, P95: 980ms Throughput (avg): 76.4 tokens/s如果 success_rate 不是 100%不要急着调大超时。先把并发降回 1确认单请求链路没问题再逐级升到 8、32。每次只改一个变量不然无法判断是并发问题还是 Key 额度问题。5.2 再去 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 看用量压测跑完后打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 看这次的请求数和 token 消耗。核对点有三个第一请求数是否与你脚本里len(requests) * 3 warmup大致吻合 第二模型 ID 是否就是你在配置里填的那个 第三如果成功请求数少于预期控制台里有没有对应的失败记录或限流提示。用量页面是验证“真实请求确实发生了”的最直接证据。如果控制台没有任何新增消耗说明脚本可能还在走模拟分支或者 Key 没被正确读取。5.3 常见报错与排障401、404、429、模型 ID 不存在真实请求下容易碰到这几类错误按顺序排查报错现象可能原因处理方式401 UnauthorizedTAOTOKEN_API_KEY没导出或值为空echo $TAOTOKEN_API_KEY确认重新导出404 Not FoundBase URL 多写了/v1或路径拼错确认填的是https://taotoken.net/api末尾不加/v1429 Too Many Requests32 并发触发限流或额度不足降到 8 并发复测去控制台看额度模型不存在模型 ID 写错或已下线去 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 模型广场核对TTFT 为 0流式 chunk 里没有 delta.content检查chunk.choices是否为空加空值保护排障时不要同时改 Key、Base URL、并发和模型 ID。一次改一个把错误信息贴回 Codex 对话让它解释可能原因但执行验证仍然由你在本地完成。6. 真实通道下的边界节点异构、时间漂移与 Prompt 长度6.1 节点异构P95 比 P50 更能说明问题原文提到去中心化网络的节点异构性一台 A100 和一台 RTX 3060 的延迟不在一个量级。换成 TaoToken 兼容通道后你面对的是统一接入层但后端仍然可能路由到不同模型实例。P50 只能告诉你一半请求有多快P95 才能暴露尾部延迟。压测报告里建议同时看 P50 和 P95。如果 P50 是 420ms、P95 是 980ms说明尾部还在可控范围如果 P95 突然超过 3000ms检查是不是 32 并发把队列打满或者模型 ID 对应的实例正在冷启动。6.2 时间漂移用量和延迟都要持续采集原文说基准不是一次性快照需要持续采集形成时间序列。验证用量也一样不要只跑一次 1/8/32 就结束。你可以每周用同一套 prompts、同一个模型 ID、同一把 Key 跑一次把 JSON 输出存下来对比 TTFT P95 和 success_rate 的变化。如果某天 P95 明显上升但 success_rate 没掉可能是后端路由变化或网络波动如果 success_rate 掉了优先看 429 和超时。持续采集才能区分“偶发波动”和“趋势退化”。6.3 Prompt 长度64/256/1024/4096 四档怎么覆盖原文建议 Prompt 集覆盖 [64, 256, 1024, 4096] 四个 token 长度档位避免只测短 prompt 导致吞吐虚高。你可以在prompts.json里准备四组不同长度的请求每组跑一次三级并发。短 prompt 看 TTFT长 prompt 看 TPOT 和总 token 消耗。{ short: [Explain TTFT in one sentence.], medium: [Explain the concept of zero-knowledge proofs in simple terms.], long: [Write a detailed comparison of optimistic rollups and ZK rollups, including security assumptions, cost structure, and finality time.], very_long: [Describe the architecture of a decentralized exchange, including order matching, settlement, liquidity provision, and MEV protection.] }长度档位不要凭感觉写用 tokenizer 粗算一下。跑完四组之后你会得到一张更完整的延迟-吞吐曲线而不是只靠 5 条中等长度 prompt 得出的单点数据。7. 跑通之后下一步去哪里7.1 模型对话里先发一条测试消息压测脚本跑通后去 TaoToken 模型对话 用同一把 Key、同一个模型 ID 发一条测试消息。这一步是确认 Key 和模型 ID 没有在脚本之外被写错。如果对话里正常返回但脚本里报 401问题多半在环境变量或配置读取。7.2 Coding Plan 与创建 Key如果你打算把 benchmark_runner.py 作为长期基准工具每次改模型或改并发都要跑一轮可以打开 Coding Plan 看套餐是否覆盖你的日常压测用量。新的 Key 在 控制台 API Keys 创建旧的 Key 如果出现在截图或日志里记得轮换。7.3 把这次压测的用量当作长期基准的起点第一次真实压测的数字不用追求完美它的价值是给你一个可比的起点1 并发下的 P50、8 并发下的 success_rate、32 并发下的 P95以及控制台里对应的 token 消耗。下一次换模型、换并发、换 Prompt 长度时用同样的脚本和同样的输出格式再跑一遍差异才有意义。Codex 接入 TaoToken 通道之后你得到的是一套更顺手的代码核对流程benchmark_runner.py 仍然是采集主体用量页面仍然是最终对账的地方。两者对上了这次并发压测才算真正跑通。
返回列表