LLM推理优化实战(二):RTX3090部署Qwen2.5-7B,用TaoToken统一Key压测TPOT逼近显存带宽极限
发布时间:2026/10/3 19:45:34 锦皓数字建站
:RTX3090部署Qwen2.5-7B,用TaoToken统一Key压测TPOT逼近显存带宽极限`)
1. RTX3090 单卡跑 Qwen2.5-7B为什么 TPOT 卡在 19.7ms 下不去如果你手上有一张 RTX 3090想本地把 Qwen2.5-7B 跑起来做推理服务大概率会遇到一个很具体的困惑模型能跑速度看着也不慢但无论怎么调--max-model-len、怎么改并发、怎么换 batch 策略单个请求的 TPOTTime Per Output Token每个输出 token 的平均耗时就是压不到 20ms 以下。我实测下来BF16 精度下单请求 TPOT 稳定在 19.7ms 左右对应大约 51 tok/s 的输出速度看起来还行但离快还差一口气。这个 19.7ms 不是随便一个数字它背后是一条硬约束。Qwen2.5-7B 用 BF16 存储权重约 14GBRTX 3090 的显存带宽是 936 GB/s。decode 阶段每生成一个 tokenGPU 都要把全部权重从显存搬进计算单元一次理论最快时间就是 14GB ÷ 936GB/s ≈ 15ms。实测 19.7ms 意味着显存带宽利用率MBU已经到 76% 左右纯软件调参基本到顶了。想再快只能减少每次搬运的字节数——也就是量化。这篇是LLM 推理优化实战系列的第二篇聚焦的是压测与验证这件事本身。第一篇记录了从零部署和建立基线这一篇要解决的是怎么用一套可复制的压测脚本把 TPOT、吞吐、显存带宽占用这些指标测准并且用 TaoToken 的统一 Key 通道把本地 vLLM 服务和云端模型服务放在同一套调用方式下对比。适合已经在 RTX 3090 上跑起 Qwen2.5-7B、想系统做性能对比的读者。核心检索词就三个RTX3090、Qwen2.5-7B、TPOT全文围绕它们展开。先说清楚这篇要交付什么一份能直接跑的压测脚本、TaoToken 接入配置Base URL Key Model ID 三件套、以及 TPOT 和显存带宽占用的验证步骤。你照着做应该能复现出接近显存带宽极限的调优路径并且知道每一步的瓶颈在哪。2. TaoToken 统一 Key 接入把本地 vLLM 和云端模型放进同一套压测流程做压测最烦的一件事是本地服务一套调用方式云端模型另一套调用方式脚本要写两遍指标口径还对不齐。TaoToken 在这里的价值是提供一个统一的 OpenAI 兼容 API 通道你用一个 Key、一个 Base URL就能同时调用本地 vLLM 起的服务和云端模型压测脚本只需要改base_url和model两个字段。TaoToken 是什么、能做什么、适合谁它是一个模型 API 聚合通道对外暴露 OpenAI 兼容接口支持对话、编码类模型调用适合需要统一管理多个模型 Key、做多模型对比压测、或者把本地服务和云端服务放在同一套代码里切换的开发者。官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。接入前你需要准备三样东西也就是常说的三件套配置项本地 vLLMTaoToken 云端通道Base URLhttp://localhost:8000/v1https://taotoken.net/apiAPI Key任意字符串vLLM 默认不校验在控制台创建的 KeyModel IDqwen2.5-7b--served-model-name 设置控制台里对应的模型标识这里有个容易踩的坑本地 vLLM 的 Base URL 要带/v1而 TaoToken 的 API 根地址是https://taotoken.net/api具体调用路径在文档里会写清楚。我试过把两者混用结果 404排查了半天才发现是路径拼接问题。所以压测脚本里最好把 base_url 和 endpoint 分开配置别硬编码。创建 Key 的入口在控制台的 API Keys 页面路径是 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。创建完复制出来存到环境变量里别写死在脚本里。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的接口说明和参数列表。如果你只是想先验证模型能不能通可以用模型对话页面直接试 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。长期做编码类、Agent 类任务的话Coding Plan 更划算入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。为什么压测要用统一 Key因为你要对比的往往不只是本地 vs 云端还有BF16 vs INT4、conc8 vs conc32这些维度。如果每次对比都要换一套调用代码指标口径很容易不一致。统一到 OpenAI 兼容接口后压测工具比如 vLLM 自带的bench serve只需要改参数不用改逻辑。这里要强调一点TaoToken 是 API 通道不是替代你本地推理引擎的东西。本地 vLLM 该跑还是跑TaoToken 解决的是多模型、多环境统一调用的问题。两者是配合关系不是替代关系。3. 可复制配置vLLM 启动参数与压测脚本的完整 settings这一节给的是能直接复制粘贴的配置。先看 vLLM 启动命令这是本地服务的基础export OMP_NUM_THREADS4 python -m vllm.entrypoints.openai.api_server \ --model /root/autodl-tmp/models/qwen2.5-7b-instruct \ --served-model-name qwen2.5-7b \ --host 0.0.0.0 \ --port 8000 \ --gpu-memory-utilization 0.85 \ --max-model-len 4096关键参数就四个--served-model-name决定 API 调用时的 Model ID压测脚本里必须和它一致--gpu-memory-utilization 0.85让 vLLM 预占 85% 显存24GB × 0.85 ≈ 20.4GB扣掉 14GB 权重剩约 6.4GB 给 KV cache--max-model-len 4096限制单请求最大 token 数直接影响 KV cache 池能容纳多少并发。接下来是压测脚本。vLLM 自带bench serve用 ShareGPT 数据集模拟真实对话流量。先准备数据集mkdir -p /root/autodl-tmp/datasets /root/autodl-tmp/results/perf_baseline wget https://hf-mirror.com/datasets/anon8231489123/ShareGPT_Vicuna_unfiltered/resolve/main/ShareGPT_V3_unfiltered_cleaned_split.json \ -O /root/autodl-tmp/datasets/sharegpt.json然后写一个批量扫描脚本把 request-rate 和 max-concurrency 两个维度都扫一遍#!/bin/bash mkdir -p /root/autodl-tmp/results/sweep COMMON_ARGS --backend openai-chat --base-url http://localhost:8000 --endpoint /v1/chat/completions --model qwen2.5-7b --tokenizer /root/autodl-tmp/models/qwen2.5-7b-instruct --dataset-name sharegpt --dataset-path /root/autodl-tmp/datasets/sharegpt.json --num-prompts 200 --save-result export HF_HUB_OFFLINE1 export TRANSFORMERS_OFFLINE1 echo 实验组 A固定 conc32扫描 request-rate for rate in 1 2 4 8 16 inf; do vllm bench serve $COMMON_ARGS \ --request-rate $rate \ --max-concurrency 32 \ --result-dir /root/autodl-tmp/results/sweep \ --result-filename expA_rate${rate}_conc32.json sleep 5 done echo 实验组 B固定 rateinf扫描 max-concurrency for conc in 1 4 8 16 32 64 128; do vllm bench serve $COMMON_ARGS \ --request-rate inf \ --max-concurrency $conc \ --result-dir /root/autodl-tmp/results/sweep \ --result-filename expB_rateinf_conc${conc}.json sleep 5 done这里有个必须注意的点--base-url不能带路径。写http://localhost:8000是对的写http://localhost:8000/v1/chat/completions会报 URL 拼接错误因为 vLLM 内部会把 base_url 和 endpoint 拼起来。--endpoint必须显式传/v1/chat/completions因为 openai-chat 后端默认走的是/v1/completions不指定就会走错接口。如果你要用 TaoToken 通道做对比压测把--base-url换成https://taotoken.net/api--model换成控制台里的模型标识再在环境变量里加上 Key 就行。压测脚本本身不用改逻辑这就是统一 Key 的好处。再给一份 Python 版的单请求 TPOT 测量脚本适合做精细验证import time import os import requests BASE_URL os.getenv(BASE_URL, http://localhost:8000/v1) API_KEY os.getenv(API_KEY, EMPTY) MODEL os.getenv(MODEL, qwen2.5-7b) headers { Content-Type: application/json, Authorization: fBearer {API_KEY}, } payload { model: MODEL, messages: [{role: user, content: 用三句话解释什么是显存带宽。}], max_tokens: 128, stream: True, } start time.perf_counter() first_token_time None token_count 0 with requests.post(f{BASE_URL}/chat/completions, headersheaders, jsonpayload, streamTrue) as resp: for line in resp.iter_lines(): if not line: continue if line.startswith(bdata: ) and b[DONE] not in line: if first_token_time is None: first_token_time time.perf_counter() token_count 1 end time.perf_counter() ttft (first_token_time - start) * 1000 if first_token_time else 0 tpot (end - first_token_time) * 1000 / max(token_count - 1, 1) print(fTTFT: {ttft:.2f} ms) print(fTPOT: {tpot:.2f} ms) print(f输出 token 数: {token_count})这个脚本用流式请求能精确测出 TTFT 和 TPOT。跑的时候把BASE_URL、API_KEY、MODEL三个环境变量设好本地和云端都能用。4. 验证请求与成功结果TPOT 19.7ms 和显存带宽占用的实测数据配置跑通后先做一次单请求验证确认服务正常curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen2.5-7b, messages: [{role: user, content: 你好}], max_tokens: 50 }返回里有choices[0].message.content就说明服务通了。接下来跑基线压测rate4, conc16的结果大概是这样 Serving Benchmark Result Successful requests: 200 Failed requests: 0 Request throughput (req/s): 2.64 Output token throughput (tok/s): 558.58 Total token throughput (tok/s): 1209.61 ---------------Time to First Token---------------- Mean TTFT (ms): 127.67 Median TTFT (ms): 113.43 P99 TTFT (ms): 293.48 -----Time per Output Token (excl. 1st token)------ Mean TPOT (ms): 23.91 Median TPOT (ms): 23.87 P99 TPOT (ms): 34.38 然后跑实验组 B固定rateinf扫描并发。conc1 时的结果最关键Conc1: TTFT P50: 41 ms TPOT P50: 19.7 ms TPOT P99: 19.8 ms Out_TPS: 50 tok/s19.7ms 这个数字就是单请求 TPOT 的实测值。对照理论下限Qwen2.5-7B BF16 权重 7e9 × 2 bytes ≈ 14 GB RTX 3090 显存带宽 936 GB/s 理论最快 TPOT 14 GB ÷ 936 GB/s ≈ 15 ms 实测 TPOT 19.7 ms MBU 15 ÷ 19.7 ≈ 76%76% 的显存带宽利用率说明 decode 阶段 GPU 大部分时间都在等权重从显存搬过来计算单元反而是闲的。这就是为什么纯调参没用——瓶颈在带宽不在算力。再看并发扫描的完整结果ConcTTFT P50TTFT P99TPOT P50TPOT P99Out_TPS141 ms196 ms19.7 ms19.8 ms50492 ms234 ms20.0 ms25.8 ms190894 ms355 ms21.4 ms27.2 ms3451683 ms787 ms23.4 ms35.2 ms59532127 ms1352 ms26.8 ms60.2 ms97864212 ms2018 ms34.4 ms165 ms1341128892 ms3598 ms51.9 ms387 ms1583几个关键拐点conc32 时 Out_TPS 到 978接近饱和conc32→64 时 TPOT P99 从 60ms 暴涨到 165ms这是 decode 质量的实际边界conc128 时 TTFT P99 到 3.6 秒用户体验已经崩了。显存带宽占用怎么验证用 vLLM 的 metrics 接口watch -n 1 curl -s http://localhost:8000/metrics | grep -E kv_cache_usage|num_requests_running|num_requests_waiting输出类似vllm:kv_cache_usage_perc{model_nameqwen2.5-7b} 0.45 vllm:num_requests_running{model_nameqwen2.5-7b} 12 vllm:num_requests_waiting{model_nameqwen2.5-7b} 3KV cache 用了 45%当前处理 12 个请求还有 3 个在排队。再看 KV cache 配置curl -s http://localhost:8000/metrics | grep cache_config_info关键字段num_gpu_blocks5306、block_size16KV cache 总容量 5306 × 16 84,896 token。这个数字决定了系统能同时处理的最大 token 数也是并发能力的上限。如果你想用 TaoToken 通道验证同样的模型把 Base URL 换成https://taotoken.net/apiKey 用控制台创建的Model ID 用文档里对应的标识跑同一个 Python 脚本对比 TPOT 和 TTFT。这样你就能看到本地 RTX 3090 和云端通道在相同请求下的性能差异。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 报错对照压测过程中最容易撞上的几类报错这里按真实错误信息对照排查。401 Unauthorized用 TaoToken 通道时最常见。原因通常是 Key 没传、传错、或者环境变量没生效。检查Authorization: Bearer key头有没有带上Key 有没有多余空格。本地 vLLM 默认不校验 Key所以本地跑通不代表云端也能通两套 Key 要分开管理。local proxy failed / connection refused压测脚本连不上服务。先确认 vLLM 进程还在跑curl http://localhost:8000/v1/models能不能返回。如果是 TaoToken 通道确认网络能访问https://taotoken.net/api以及 base_url 有没有多写路径。我踩过的坑是把https://taotoken.net/api写成了https://taotoken.net/api/v1结果 404。reading choices 报错 / KeyError: choices返回体里没有choices字段通常是接口路径错了。比如用/v1/completions去调 chat 模型或者用/v1/chat/completions去调 completions 接口。vLLM 的bench serve里--backend openai-chat必须配--endpoint /v1/chat/completions两者要匹配。OAuth / token 过期类报错TaoToken 控制台创建的 Key 如果设置了有效期过期后会返回鉴权失败。去 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 重新创建一个更新到环境变量里。别把 Key 硬编码在脚本里用export API_KEYxxx更安全。cuDNN 版本冲突启动 vLLM 时报AssertionError: Found 2 libcudnn.so.x原因是同时装了 cu12 和 cu13 两个版本。排查命令pip list | grep cudnn pip uninstall nvidia-cudnn-cu12 -y保留 cu13 版本即可。lm-eval 报 Loglikelihood is not supported用openai-chat-completions后端跑选择题任务时会报这个因为 chat 接口不返回 prompt logprobs。改用local-completions后端走/v1/completions接口。bench serve 的 URL 拼接错误--base-url不能带路径--endpoint必须显式传。这两个规则记住就不会错。HuggingFace 数据集下载失败设置HF_ENDPOINThttps://hf-mirror.com或者提前下载好数据集压测时加HF_HUB_OFFLINE1和TRANSFORMERS_OFFLINE1。排查顺序建议先确认服务活着curl /v1/models再确认鉴权对Key 和 Header再确认路径对base_url endpoint最后看返回体结构。大部分报错都出在前三步。6. 从压测数据到调优决策下一步该往哪走压测做完数据摆在那接下来怎么决策我的经验是看三个数TPOT P50、TPOT P99 拐点、Out_TPS 饱和点。TPOT P50 19.7msMBU ≈ 76%说明单请求已经逼近显存带宽极限。想再压 TPOT只有两条路减少权重搬运量量化或者减少 KV cache 搬运量更激进的 KV 量化。纯调--max-model-len、--gpu-memory-utilization这些参数对单请求 TPOT 基本没影响因为它们改的是并发能力不是单次 decode 的带宽需求。TPOT P99 拐点在 conc32→6460ms 跳到 165ms。这个拐点告诉你生产环境并发别超过 32否则尾部延迟会失控。如果你的业务对 P99 敏感conc 控制在 8~16 更稳。Out_TPS 饱和点在 978 tok/sconc32。再往上加并发吞吐涨得很少但延迟涨得很快。这就是典型的吞吐换延迟trade-off没有免费午餐。下一步的优化实验我建议按这个顺序做先做 AWQ INT4 量化预期 TPOT 减半、吞吐翻倍这是收益最大的一步再做 KV cache 深度分析理解吞吐翻倍的根因然后试 prefix caching 降 TTFT最后考虑投机采样但它在低并发下才有明显收益。每次优化都要双线评测精度用 lm-eval 跑 HellaSwag、ARC、GSM8K性能用bench serve跑同样的参数扫描。只有精度损失可控、性能收益明确才值得采纳。如果你想把本地 RTX 3090 和云端通道放在一起对比TaoToken 的统一 Key 能省不少事。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite Key 在 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 创建模型对话验证在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。长期做编码和 Agent 任务的话Coding Plan 入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。最后留一个实用技巧压测结果一定要存 JSON别只看终端输出。--save-result会把每次跑的完整指标写进文件后面做对比分析时直接读 JSON 就行不用重新跑。我习惯把结果按expA_rate4_conc32.json这种命名存一眼能看出参数组合。跑完一轮扫描用几行 Python 把 JSON 读出来画个表瓶颈在哪一目了然。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。