
1. 为什么要把 Qwen3-VL 压测脚本交给 Codex 编排Qwen3-VL-8B-Instruct 推理性能测试这件事单看每一步都不难下载模型、装 vLLM、起服务、跑vllm bench serve、汇总 JSON。难的是把它们串成一条能反复跑的流水线尤其是并发点从 1 扫到 16 的时候server.log和conc_*.json会同时膨胀人眼对日志很容易对错行。我这次的做法是让 Codex 当“编排副驾”它不替你跑 GPU但能帮你把run_benchmark.sh里“启动 vLLM → 等待就绪 → 并发扫描 → 汇总结果”四段逻辑逐段讲清楚并在长会话里盯住每个并发点对应的日志文件。这篇适合两类人一是手里有单卡、想验证 Qwen3-VL-8B-Instruct 在 BF16 下能扛多少路多模态并发的人二是已经在用 Codex 写脚本但希望把多步骤任务编排交给它、自己只负责看结果的人。核心检索词就三个Qwen3-VL-8B-Instruct、推理性能测试、vLLM 并发扫描。读完你能拿到一份可复制的run_benchmark.sh拆解思路以及让 Codex 帮你做日志对齐的具体问法。需要先说明边界TaoToken 在这里只负责给 Codex 供 Key让它在长会话里保持上下文真正跑vllm bench serve的仍然是你本地环境。别把两者混在一起否则排障时会找不到方向。2. 前置准备TaoToken 给 Codex 供 Key本地跑 vLLM2.1 为什么用 TaoToken 接 CodexCodex 这类编码 Agent 的价值在长会话你让它读run_benchmark.sh它会记住前面解释过的并发点后面你问“conc_12 的 TTFT 为什么比 conc_10 高”它不用你重新贴脚本。但长会话对 Key 的稳定性和额度有要求TaoToken 的作用就是提供一个统一的 Base URL让 Codex 的请求走https://taotoken.net/api你只管在本地把 vLLM 跑起来。先打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建 Key这一步只做一次。拿到 Key 后Codex 的 Base URL 填https://taotoken.net/api模型名按你实际用的填。注意这一步只是让 Codex 能对话跟 vLLM 服务没有直接关系。2.2 本地环境的最低要求Qwen3-VL-8B-Instruct 是 8B 级别的多模态模型BF16 下单卡显存建议 24GB 起步--gpu-memory-utilization 0.95是脚本里的默认值。Python 用 3.12PyTorch 走 cu128 轮子vLLM 固定 0.23.0。这些版本号别随手改vLLM 的小版本对多模态参数名很敏感--limit-mm-per-prompt和--mm-processor-kwargs在不同版本里行为不一样。如果你还没建环境可以先把 conda 环境名记成qwen3_vl_tp1_bf16_dev后面脚本里的CONDA_ENV默认值就是它。模型下载用hf download到./models/Qwen3-VL-8B-Instruct路径别带空格。3. 可复制配置把 run_benchmark.sh 拆成四段讲给 Codex3.1 第一段启动 vLLM 服务脚本开头用vllm serve起服务关键参数有这几个--dtype bfloat16定精度--tensor-parallel-size 1表示单卡不切分--max-model-len 8192控制上下文--max-num-seqs 64是调度上限。多模态相关的两行是重点--limit-mm-per-prompt {image:1} \ --mm-processor-kwargs {\min_pixels\:${IMAGE_PIXELS},\max_pixels\:${IMAGE_PIXELS}}IMAGE_PIXELS默认 200704也就是 448×448。把 min 和 max 设成同一个值是为了让每条请求的图像分辨率完全一致否则random-mm数据集生成的图像尺寸会飘压测结果没法横向比。启动命令后面接 ${RESULT_DIR}/server.log 21 把服务日志单独落盘这就是后面 Codex 要帮你盯的第一个文件。3.2 第二段等待就绪的轮询逻辑服务起来不等于能接请求模型加载要时间。脚本用for _ in $(seq 1 180)做最多 180 次轮询每次curl -sf http://${HOST}:${PORT}/v1/models成功就 break失败就sleep 2。这里有两个细节值得让 Codex 解释一是kill -0 ${SERVER_PID}用来判断服务进程是否提前退出如果退了就直接 tail 日志退出避免傻等二是curl -sf的-f让 HTTP 错误码也返回非零比只看连接成功更严格。你可以这样问 Codex“把 run_benchmark.sh 里等待就绪这段的退出条件逐行列出来说明哪些情况会提前退出、哪些会等到 180 次超时。”它会把kill -0和curl -sf两条分支讲清楚你就能判断自己的机器大概要等多久。3.3 第三段并发扫描 1→16 的压测点这是整条流水线的心脏。CONCURRENCY_LIST默认1 2 4 6 8 10 12 14 16脚本对每个 c 跑一次vllm bench serve关键参数--backend openai-chat \ --dataset-name random-mm \ --enable-multimodal-chat \ --input-len 30 \ --output-len 300 \ --num-prompts 60 \ --request-rate inf \ --max-concurrency ${c} \ --random-mm-bucket-config {(448, 448, 1): 1.0} \ --save-result \ --result-dir ${RESULT_DIR} \ --result-filename conc_${c}.json--request-rate inf表示不限速让并发数成为唯一变量--num-prompts 60是每个并发点发 60 条请求--random-mm-bucket-config把图像桶固定成 448×448×1权重 1.0保证每条请求都带一张同规格图。每个并发点的标准输出重定向到conc_${c}.log结构化结果写进conc_${c}.json。这就是为什么后面要对齐日志一个并发点对应两个文件16 个点就是 32 个文件。3.4 第四段汇总脚本怎么读 JSONsummarize_results.py用result_dir.glob(conc_*.json)抓所有结果按文件名里的数字排序然后从每个 JSON 里取max_concurrency、request_throughput、output_throughput、mean_ttft_ms、mean_tpot_ms五个字段。判定通过的条件写死为ttft_ms 5000 and tpot_ms 50最后用max(..., keylambda x: x[concurrency])找通过点里并发最大的那个。这里有个容易踩的坑glob的排序如果只按字符串conc_10会排在conc_2前面。脚本用keylambda path: int(path.stem.split(_)[1])转成整数再排所以表格里的并发点是递增的。你可以让 Codex 帮你确认这一点避免汇总表顺序错乱。4. 验证请求跑一遍看结果对不对4.1 执行与预期输出直接bash run_benchmark.sh你会看到四段进度标记。服务就绪后进入扫描每个并发点打印一行Running concurrencyN。全部跑完汇总表大概长这样concurrencyttft_mstpot_msqps_e2edecode_tpspass174.7910.460.312393.68True462.5710.941.1993359.78True886.4611.182.1896656.88True16139.6011.614.15861247.58TrueTTFT 从 74ms 涨到 139msTPOT 稳定在 1012msQPS 和 decode 吞吐随并发近似线性增长。这说明单卡 BF16 下 16 路并发还没触到算力瓶颈延迟约束也远没到 5 秒红线。4.2 让 Codex 帮你对齐 server.log 和 conc_*.json跑完之后真正的体力活是对日志。你可以把server.log和conc_12.json一起丢给 Codex问“conc_12 这个并发点的 TTFT 是 129.68ms帮我在 server.log 里找到对应时间段的调度记录确认有没有排队。”Codex 会按时间戳去匹配而不是你一行行翻。更省事的做法是让它写个小对照表把每个conc_*.json的mean_ttft_ms和server.log里对应时间窗口的 batch 信息列在一起。这样你能一眼看出 TTFT 上升是来自图像预处理还是调度排队。注意这一步 Codex 只做文本对齐不碰你的 GPU。5. 本篇常见错排查5.1 服务起不来或提前退出最常见的是显存不够。--gpu-memory-utilization 0.95在 24GB 卡上跑 8B BF16 通常够但如果你同时开了别的进程就会 OOM。先看server.log末尾有没有CUDA out of memory有就把MAX_MODEL_LEN从 8192 降到 4096 再试。另一个原因是模型路径不对脚本里有if [[ ! -d ${MODEL_PATH} ]]的检查路径错了会直接退出。5.2 等待就绪一直超时如果 180 次轮询跑完还没就绪先确认curl http://127.0.0.1:8000/v1/models手动能不能通。不通就看server.log是不是卡在加载权重。多模态模型首次加载要初始化视觉塔比纯文本模型慢180 次 × 2 秒 6 分钟一般够但机械盘上可能不够可以把seq 1 180调大。5.3 汇总表为空或顺序错乱No result JSON files found说明RESULT_DIR里没有conc_*.json检查--save-result和--result-dir是否都传了。顺序错乱则是排序 key 的问题确认脚本用的是整数排序而不是字符串排序。如果某个并发点的 JSON 缺字段summarize_results.py会在取data[mean_ttft_ms]时抛 KeyError这时去看对应的conc_${c}.log通常是那次请求全失败了。5.4 Codex 会话里上下文丢失如果你在 Codex 里聊到一半发现它忘了前面的脚本内容多半是会话太长被截断。这时候把run_benchmark.sh的关键段落重新贴一次或者用 TaoToken 的模型对话入口开个新会话把四段逻辑的摘要先喂给它。别指望它记住 32 个文件的全部内容按并发点分批问更稳。6. 把编排交给 Codex把 GPU 留给自己这套流程跑通之后你会发现真正花时间的不是写脚本而是理解每个并发点为什么是这个数。Codex 在长会话里帮你盯server.log和conc_*.json的对应关系你只需要在关键节点做判断TTFT 开始明显抬头的那一档是不是该停decode 吞吐还在涨的那一档是不是还能再加并发。如果你想让 Codex 长期帮你做这类多步骤任务编排可以去看看 Coding Plan它更适合这种需要反复对话、逐步收敛的场景https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。需要管理多个 Key 或者查看额度控制台在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 列表在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。接入细节和参数说明看文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。最后留一个实用技巧把CONCURRENCY_LIST改成16 20 24 28 32只跑高并发段用前面 1→16 的结果做基线这样找拐点比从头扫一遍快得多。跑之前记得把RESULT_DIR换个新目录别把旧结果覆盖了。