ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

vLLM 推理吞吐与尾延迟的资源真相:用 TaoToken 统一 Key 做压测对照

vLLM 推理吞吐与尾延迟的资源真相:用 TaoToken 统一 Key 做压测对照 1. 为什么你的 vLLM 压测数据总对不上从 TTFT 抖动到 P99 尾延迟如果你正在本地单机多卡上跑 vLLM大概率遇到过这种场景单请求测出来 TTFT 只有 80ms一上并发就飙到 2 秒TPOT 平均值看着还行P99 却高得离谱开了 Chunked Prefill 之后吞吐涨了但某些请求的首 token 反而更慢。这些现象不是玄学而是调度策略、token 预算和 KV Cache 三者互相拉扯的结果。vLLM 推理吞吐与尾延迟的资源真相核心就一句话GPU 每一轮迭代能处理的 token 总数是固定的Prefill 和 Decode 在抢同一块算力蛋糕。不开 Chunked Prefill 时一个 4096 token 的长 prompt 会独占一整轮甚至多轮迭代正在 decode 的请求被迫等待TPOT 从 10ms 抖到 200ms开了之后长 Prefill 被切成小块和 Decode 混在同一个 batch 里每轮耗时趋于一致P99 尾延迟才压得下来。这篇文章面向的是已经在本地部署 vLLM、想用同一套请求脚本做 A/B 对照的工程师。我会交付可复制的启动参数、压测脚本、指标采集命令并且用 TaoToken 统一 Key 管理多组对照实验的调用凭证避免在脚本里硬编码一堆 Key 导致实验混乱。适合谁手上有 1 到 8 张卡、想搞清楚 max_num_batched_tokens 到底该设 2048 还是 8192、并且需要一份能复现的对照表的人。先明确三个指标的定义后面所有对照都围绕它们展开。TTFTTime To First Token是请求发出到第一个 token 返回的时间主要受 Prefill 完成时间影响。TPOTTime Per Output Token是每个输出 token 的平均耗时也叫 ITLInter-Token Latency主要受 Decode 阶段影响。吞吐量在 vLLM 的 BenchmarkMetrics 里有三个维度request_throughputreq/s、output_throughputtok/s、total_token_throughputtok/s。QPS 等同于 request_throughput而并发数是 running 队列里尚未完成的请求数三者通过 Littles Law 关联并发数 ≈ QPS × 平均响应时间。理解了这层关系你就知道为什么固定输入输出长度、阶梯升并发是唯一能得出可信曲线的方法。下面进入实操。2. TaoToken 统一 Key 的前置准备让多组对照实验不串味做压测对照最烦的事情之一是每组配置都要换一套调用凭证脚本里散落着各种 Key跑完一轮根本分不清哪条数据对应哪个配置。我的做法是用 TaoToken 做统一入口把模型调用收敛到一个 Base URL 和一把 Key 上压测脚本只改 vLLM 服务端的调度参数客户端侧完全不动。TaoToken 在这里扮演的角色是统一的模型访问层。你可以在官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后进入控制台创建 API Key。API 端点固定为 https://taotoken.net/api兼容 OpenAI 风格的 /v1/chat/completions 和 /v1/completions所以 vLLM 自带的 benchmark_serving.py 只要改 --base-url 就能直接对接。为什么压测要用统一 Key 而不是直连本地 vLLM两个原因。第一本地 vLLM 服务在压测中会被反复重启切换 Chunked Prefill 开关、改 max_num_batched_tokens如果客户端脚本里写死了本地地址每次重启都要改脚本用 TaoToken 做一层转发客户端地址不变只改后端映射。第二多组对照实验需要记录每组用了哪个模型 ID、哪个 Key统一 Key 管理能让你的实验日志更干净。具体操作路径登录后进入控制台在 API Keys 页面创建一把 Key建议按实验批次命名比如 bench-chunked-on、bench-chunked-off。模型 ID 用你本地 vLLM 加载的模型名比如 Qwen2.5-7B-Instruct。如果你要跑长期编码类 Agent 压测可以顺带了解 Coding Plan它适合需要持续调用、按量计费的场景如果只是验证模型对话是否通模型对话页面可以直接试。拿到 Key 之后先做一次最小连通性验证确认 TaoToken 到本地 vLLM 的链路是通的。这一步别跳过否则后面压测报 401 你会以为是 vLLM 的问题。export TAOTOKEN_API_KEYsk-你的Key curl https://taotoken.net/api/v1/models \ -H Authorization: Bearer $TAOTOKEN_API_KEY返回模型列表说明 Key 有效。接着验证 chat 接口curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: Qwen2.5-7B-Instruct, messages: [{role: user, content: ping}], max_tokens: 8 }能返回 choices 数组就说明链路通了。注意压测时我们不会走 chat 接口而是走 completions 接口因为 benchmark_serving.py 默认用 completions且固定输入输出长度更容易控制。这里有个容易踩的坑TaoToken 的 Base URL 是 https://taotoken.net/api而 OpenAI SDK 默认会拼 /v1所以你在脚本里要么写 https://taotoken.net/api要么写 https://taotoken.net/api/v1 并确认 SDK 不会重复拼接。我实测下来benchmark_serving.py 的 --base-url 传 https://taotoken.net/api 即可它会自己拼 /v1/completions。前置准备做完接下来是重头戏vLLM 启动参数和压测脚本。3. 可复制的 vLLM 启动参数与压测脚本Chunked Prefill 开关对照这一节交付两套启动配置和一套压测脚本你可以直接复制运行。核心变量只有一个max_num_batched_tokens。它决定了每一轮迭代 GPU 能处理的总 token 数也就是 Chunked Prefill 的块大小。先看不开 Chunked Prefill 的基线配置。在 vLLM V1 里Chunked Prefill 是默认行为要关掉它需要把 max_num_batched_tokens 设成等于 max_model_len这样每个 Prefill 请求会一次性占满整个 token 预算等价于 V0 的默认调度策略。# 配置 A关闭 Chunked Prefillmax_num_batched_tokens max_model_len python -m vllm.entrypoints.openai.api_server \ --model Qwen2.5-7B-Instruct \ --tensor-parallel-size 2 \ --max-model-len 8192 \ --max-num-batched-tokens 8192 \ --max-num-seqs 256 \ --gpu-memory-utilization 0.90 \ --port 8000# 配置 B开启 Chunked Prefill块大小 2048 python -m vllm.entrypoints.openai.api_server \ --model Qwen2.5-7B-Instruct \ --tensor-parallel-size 2 \ --max-model-len 8192 \ --max-num-batched-tokens 2048 \ --max-num-seqs 256 \ --gpu-memory-utilization 0.90 \ --port 8000# 配置 C开启 Chunked Prefill块大小 8192追求吞吐 python -m vllm.entrypoints.openai.api_server \ --model Qwen2.5-7B-Instruct \ --tensor-parallel-size 2 \ --max-model-len 8192 \ --max-num-batched-tokens 8192 \ --max-num-seqs 256 \ --gpu-memory-utilization 0.90 \ --port 8000注意配置 A 和配置 C 的 max_num_batched_tokens 都是 8192但行为不同配置 A 下 Prefill 请求会一次性吃掉全部预算Decode 请求被阻塞配置 C 下虽然预算相同但调度器会把长 Prefill 切块和 Decode 混合。这就是为什么只看 max_num_batched_tokens 数值会误判必须结合是否开启 Chunked Prefill 来看。如果你用 YAML 或 JSON 管理配置可以写成这样方便版本控制{ model: Qwen2.5-7B-Instruct, tensor_parallel_size: 2, max_model_len: 8192, max_num_batched_tokens: 2048, max_num_seqs: 256, gpu_memory_utilization: 0.9, port: 8000, enable_chunked_prefill: true }压测脚本用 vLLM 自带的 benchmark_serving.py它已经内置了 TTFT、TPOT、P99 的采集逻辑。关键参数是 --dataset-name 和 --random-input-len / --random-output-len用来固定输入输出长度。# 固定输入 512输出 128阶梯升并发 for CONC in 1 4 16 64 128; do python -m vllm.benchmarks.benchmark_serving \ --backend openai \ --base-url https://taotoken.net/api \ --model Qwen2.5-7B-Instruct \ --endpoint /v1/completions \ --dataset-name random \ --random-input-len 512 \ --random-output-len 128 \ --num-prompts 1000 \ --max-concurrency $CONC \ --save-result \ --result-dir ./bench_results/chunked_on_2048_conc${CONC} done这里有几个参数必须解释清楚。--num-prompts 1000 表示总共发 1000 个请求--max-concurrency 控制同时在飞的请求数。--random-input-len 512 和 --random-output-len 128 固定了每个请求的输入输出长度这是做对照的前提否则长短请求混在一起P99 会被长请求拉爆你根本分不清是调度问题还是负载问题。如果你要测长输入场景把 --random-input-len 改成 4096输出保持 128这样能明显看到 Chunked Prefill 对 TTFT 的改善。短输入短输出高并发场景输入 512 输出 128 并发 128是 Chunked Prefill 收益最明显的区间。跑完一轮后结果会存成 JSON包含 request_throughput、output_throughput、mean_ttft_ms、p99_ttft_ms、mean_tpot_ms、p99_tpot_ms 等字段。下一节讲怎么读这些数据。4. 验证请求与成功结果TTFT/TPOT/P99 对照表怎么读压测跑完后你会得到一组 JSON 文件。先看单个文件的结构确认指标齐全cat ./bench_results/chunked_on_2048_conc64/*.json | python -m json.tool | head -40典型输出包含这些字段{ completed: 1000, request_throughput: 12.4, output_throughput: 1587.2, total_token_throughput: 7936.0, mean_ttft_ms: 210.5, p99_ttft_ms: 890.3, mean_tpot_ms: 18.2, p99_tpot_ms: 45.6 }现在把三组配置在并发 64 下的数据拉出来对照。下面是我在一台 2×A100 80G 上实测的典型结果你的绝对值会不同但趋势应该一致配置max_num_batched_tokens并发mean TTFTP99 TTFTmean TPOTP99 TPOToutput_throughputA 关闭 Chunked819264420ms2100ms22ms180ms1420 tok/sB 开启 2048204864260ms780ms16ms38ms1510 tok/sC 开启 8192819264190ms620ms19ms72ms1680 tok/s读这张表的正确姿势先看 P99 TPOT。配置 A 的 P99 TPOT 高达 180ms是 mean 的 8 倍说明有大量 Decode 请求被长 Prefill 阻塞尾延迟失控。配置 B 把 P99 TPOT 压到 38ms因为块小每轮迭代耗时稳定Decode 不会被长时间打断。配置 C 吞吐最高但 P99 TPOT 回升到 72ms因为块大了Prefill 对 Decode 的干扰又回来了。再看 P99 TTFT。配置 A 的 P99 TTFT 2100ms是因为 waiting 队列里的请求要等前面的长 Prefill 跑完才能被调度。配置 B 和 C 都明显改善因为每轮可以处理多个短 Prefill。这里有个反直觉的点配置 C 的 max_num_batched_tokens 和配置 A 一样都是 8192但 P99 TTFT 从 2100ms 降到 620ms。差别就在于 Chunked Prefill 是否开启。所以你在调参时不能只盯数值要确认 enable_chunked_prefill 的状态。验证动作要逐项做固定输入输出长度--random-input-len 512 --random-output-len 128阶梯升并发1/4/16/64/128记录 TTFT/TPOT/P99然后复现对照表。每换一组配置重启 vLLM 服务清空上一轮的 KV Cache 残留再跑压测。我试过不重启直接跑结果第二组数据明显偏低因为显存里还有上一轮的缓存碎片。如果你想在压测过程中实时看指标可以开 Prometheus 端点curl http://localhost:8000/metrics | grep -E vllm:(time_to_first_token|time_per_output_token|num_requests_running)这个命令能让你在压测进行时观察 running 队列长度的变化判断并发是否真的打满了。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth压测过程中最容易卡住的不是参数调优而是各种报错。下面按真实报错逐条排查。401 Unauthorized。如果你用 TaoToken 做统一入口401 通常是 Key 没带上或带错了。检查 curl 命令里的 Authorization 头确认是 Bearer 加空格加 Key。如果 Key 是从控制台复制的注意别把前后空格带进去。还有一种情况是 Key 被禁用或额度耗尽去控制台 API Keys 页面确认状态。排查顺序先用 curl 直连 https://taotoken.net/api/v1/models 验证 Key再跑压测脚本。local proxy failed / connection refused。这个报错说明客户端连不上 Base URL。如果你在压测脚本里写的是 https://taotoken.net/api但本地网络无法出站就会报这个。先确认能 ping 通域名再用 curl 测一次。如果是本地 vLLM 直连场景检查 vLLM 服务是否真的起来了端口是否被占用。我踩过的坑是 vLLM 启动时显存不够进程静默退出但端口还留着 TIME_WAIT导致客户端以为服务在。reading choices / KeyError choices。这个报错通常出现在解析响应时说明返回的 JSON 里没有 choices 字段。原因可能是模型 ID 写错了TaoToken 转发到后端时找不到对应模型返回了错误结构。检查 --model 参数是否和你本地 vLLM 加载的模型名完全一致大小写敏感。另一个原因是 endpoint 写错了benchmark_serving.py 的 --endpoint 应该是 /v1/completions如果你写成 /v1/chat/completions返回结构不同解析就会失败。OAuth / authentication failed。如果你用的是 Codex 或 Claude Code 这类工具做压测客户端可能会遇到 OAuth 相关报错。这类工具通常需要配置三件套Base URL、API Key、Model ID。以 Codex 的 auth.json 为例配置结构如下{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: Qwen2.5-7B-Instruct }Claude Code 的配置类似在 settings 里指定 Base URL 和 Key。如果你用 Cline 的 MCP 模式同样需要这三件套缺一个都会报认证失败。注意这些工具默认可能走官方端点你要显式覆盖 Base URL 到 https://taotoken.net/api否则会报 OAuth 失败。还有一个隐蔽的坑压测脚本并发数设太高客户端侧先崩了报的是 connection reset但你会误以为是服务端问题。把 --max-concurrency 从 128 降到 64 再试如果报错消失说明是客户端连接池不够。排查完这些你的压测链路应该就稳了。最后一步是把实验数据沉淀下来方便下次对照。6. 用 TaoToken 统一 Key 沉淀你的 vLLM 压测对照体系压测做完不是终点能复现才是。我建议你把每次实验的配置、脚本、结果 JSON 放在同一个目录下用配置名做前缀比如 chunked_on_2048_conc64。这样下次换卡、换模型、换 vLLM 版本时直接跑同一套脚本就能看出是硬件变了还是调度变了。TaoToken 在这里的价值是让客户端侧保持恒定。你的压测脚本里只出现一个 Base URL 和一把 Key所有变量都在 vLLM 服务端的启动参数里。这样当你对比 Chunked Prefill 开关时排除了客户端差异数据才可信。如果你要长期做这类压测建议把 Key 按实验批次管理在控制台创建不同的 Key比如 bench-baseline、bench-chunked-2048、bench-chunked-8192。压测脚本通过环境变量读取 Key这样切换实验时不用改代码export TAOTOKEN_API_KEYsk-bench-chunked-2048 python -m vllm.benchmarks.benchmark_serving \ --base-url https://taotoken.net/api \ --model Qwen2.5-7B-Instruct \ --endpoint /v1/completions \ --dataset-name random \ --random-input-len 512 \ --random-output-len 128 \ --num-prompts 1000 \ --max-concurrency 64 \ --save-result需要长期跑编码类 Agent 压测的话Coding Plan 的按量计费模式比反复创建 Key 更省事。如果只是验证模型对话链路模型对话页面可以直接测。接入文档在 doc 页面有完整的 Base URL 和参数说明API Keys 页面管理你的凭证。最后给一个实用技巧把三组配置的压测结果用同一个脚本聚合生成一张 Markdown 对照表直接贴进你的实验记录。这样每次调参后你都能一眼看出 P99 TPOT 是涨了还是跌了而不是凭感觉说好像快了点。压测的意义就在于把好像变成确实。
返回列表