拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

LLM推理优化实战(二):RTX3090部署Qwen2.5-7B,用TaoToken统一Key压测TPOT逼近显存带宽极限

LLM推理优化实战(二):RTX3090部署Qwen2.5-7B,用TaoToken统一Key压测TPOT逼近显存带宽极限

1. RTX3090 单卡跑 Qwen2.5-7B,为什么 TPOT 卡在 19.7ms 下不去

如果你手上有一张 RTX 3090,想本地把 Qwen2.5-7B 跑起来做推理服务,大概率会遇到一个很具体的困惑:模型能跑,速度看着也不慢,但无论怎么调--max-model-len、怎么改并发、怎么换 batch 策略,单个请求的 TPOT(Time Per Output Token,每个输出 token 的平均耗时)就是压不到 20ms 以下。我实测下来,BF16 精度下单请求 TPOT 稳定在 19.7ms 左右,对应大约 51 tok/s 的输出速度,看起来还行,但离"快"还差一口气。

这个 19.7ms 不是随便一个数字,它背后是一条硬约束。Qwen2.5-7B 用 BF16 存储,权重约 14GB;RTX 3090 的显存带宽是 936 GB/s。decode 阶段每生成一个 token,GPU 都要把全部权重从显存搬进计算单元一次,理论最快时间就是 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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api 。

接入前你需要准备三样东西,也就是常说的"三件套":

配置项本地 vLLMTaoToken 云端通道
Base URLhttp://localhost:8000/v1https://taotoken.net/api
API Key任意字符串(vLLM 默认不校验)在控制台创建的 Key
Model 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_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。创建完复制出来,存到环境变量里,别写死在脚本里。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有完整的接口说明和参数列表。

如果你只是想先验证模型能不能通,可以用模型对话页面直接试: https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。长期做编码类、Agent 类任务的话,Coding Plan 更划算,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。

为什么压测要用统一 Key?因为你要对比的往往不只是"本地 vs 云端",还有"BF16 vs INT4"、"conc=8 vs conc=32"这些维度。如果每次对比都要换一套调用代码,指标口径很容易不一致。统一到 OpenAI 兼容接口后,压测工具(比如 vLLM 自带的bench serve)只需要改参数,不用改逻辑。

这里要强调一点:TaoToken 是 API 通道,不是替代你本地推理引擎的东西。本地 vLLM 该跑还是跑,TaoToken 解决的是"多模型、多环境统一调用"的问题。两者是配合关系,不是替代关系。

3. 可复制配置:vLLM 启动参数与压测脚本的完整 settings

这一节给的是能直接复制粘贴的配置。先看 vLLM 启动命令,这是本地服务的基础:

export OMP_NUM_THREADS=4 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_OFFLINE=1 export TRANSFORMERS_OFFLINE=1 echo "=== 实验组 A:固定 conc=32,扫描 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:固定 rate=inf,扫描 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": f"Bearer {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", headers=headers, json=payload, stream=True) as resp: for line in resp.iter_lines(): if not line: continue if line.startswith(b"data: ") 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(f"TTFT: {ttft:.2f} ms") print(f"TPOT: {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就说明服务通了。接下来跑基线压测,rate=4, conc=16的结果大概是这样:

============ 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,固定rate=inf,扫描并发。conc=1 时的结果最关键:

Conc=1: TTFT P50: 41 ms TPOT P50: 19.7 ms TPOT P99: 19.8 ms Out_TPS: 50 tok/s

19.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_TPS
141 ms196 ms19.7 ms19.8 ms50
492 ms234 ms20.0 ms25.8 ms190
894 ms355 ms21.4 ms27.2 ms345
1683 ms787 ms23.4 ms35.2 ms595
32127 ms1352 ms26.8 ms60.2 ms978
64212 ms2018 ms34.4 ms165 ms1341
128892 ms3598 ms51.9 ms387 ms1583

几个关键拐点:conc=32 时 Out_TPS 到 978,接近饱和;conc=32→64 时 TPOT P99 从 60ms 暴涨到 165ms,这是 decode 质量的实际边界;conc=128 时 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_name="qwen2.5-7b"} 0.45 vllm:num_requests_running{model_name="qwen2.5-7b"} 12 vllm:num_requests_waiting{model_name="qwen2.5-7b"} 3

KV cache 用了 45%,当前处理 12 个请求,还有 3 个在排队。再看 KV cache 配置:

curl -s http://localhost:8000/metrics | grep cache_config_info

关键字段:num_gpu_blocks=5306、block_size=16,KV cache 总容量 = 5306 × 16 = 84,896 token。这个数字决定了系统能同时处理的最大 token 数,也是并发能力的上限。

如果你想用 TaoToken 通道验证同样的模型,把 Base URL 换成https://taotoken.net/api,Key 用控制台创建的,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_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 重新创建一个,更新到环境变量里。别把 Key 硬编码在脚本里,用export API_KEY=xxx更安全。

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_ENDPOINT=https://hf-mirror.com,或者提前下载好数据集,压测时加HF_HUB_OFFLINE=1和TRANSFORMERS_OFFLINE=1。

排查顺序建议:先确认服务活着(curl /v1/models),再确认鉴权对(Key 和 Header),再确认路径对(base_url + endpoint),最后看返回体结构。大部分报错都出在前三步。

6. 从压测数据到调优决策:下一步该往哪走

压测做完,数据摆在那,接下来怎么决策?我的经验是看三个数:TPOT P50、TPOT P99 拐点、Out_TPS 饱和点。

TPOT P50 = 19.7ms,MBU ≈ 76%,说明单请求已经逼近显存带宽极限。想再压 TPOT,只有两条路:减少权重搬运量(量化),或者减少 KV cache 搬运量(更激进的 KV 量化)。纯调--max-model-len、--gpu-memory-utilization这些参数,对单请求 TPOT 基本没影响,因为它们改的是并发能力,不是单次 decode 的带宽需求。

TPOT P99 拐点在 conc=32→64,60ms 跳到 165ms。这个拐点告诉你:生产环境并发别超过 32,否则尾部延迟会失控。如果你的业务对 P99 敏感,conc 控制在 8~16 更稳。

Out_TPS 饱和点在 978 tok/s(conc=32)。再往上加并发,吞吐涨得很少,但延迟涨得很快。这就是典型的"吞吐换延迟"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_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,Key 在 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 创建,模型对话验证在 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。长期做编码和 Agent 任务的话,Coding Plan 入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。

最后留一个实用技巧:压测结果一定要存 JSON,别只看终端输出。--save-result会把每次跑的完整指标写进文件,后面做对比分析时直接读 JSON 就行,不用重新跑。我习惯把结果按expA_rate4_conc32.json这种命名存,一眼能看出参数组合。跑完一轮扫描,用几行 Python 把 JSON 读出来画个表,瓶颈在哪一目了然。

返回列表