1. 为什么你的 vLLM 吞吐上不去:从显存瓶颈到调度空转
vLLM 是一个基于 PagedAttention 与连续批处理(continuous batching)的高吞吐推理引擎,对外提供 OpenAI 兼容接口,适合把 7B 到 70B 级别的模型跑成稳定的在线服务。它最擅长的事情不是单条对话有多快,而是在并发请求下让 GPU 尽量不空转。如果你正在用一张 24G 或 48G 的卡跑 Qwen、Llama 这类模型,发现 tokens/s 上不去、显存却先爆了,那这篇就是写给你的。
我见过太多团队把 vLLM 当 demo 跑:python -m vllm.entrypoints.openai.api_server --model xxx一敲,能出字就算成功。结果一上压测,QPS 稍微高一点就开始排队,TTFT(首 token 延迟)从 200ms 飙到 3s,nvidia-smi里显存 99% 但 GPU 利用率只有 40%。这不是模型的问题,是三个层面的配置没对齐:批处理调度、量化权重加载、KV Cache 显存分配。
这三个层面互相牵制。批处理开大能提吞吐,但每个序列都要占 KV Cache,显存不够调度器就塞不下更多序列;量化能把权重压小、腾出显存给 KV Cache,但量化格式选错反而拖慢解码;KV Cache 的分配策略又决定了你能同时跑多少并发。所以优化不能单点调参,得按“先量化腾显存 → 再调批处理吃满算力 → 最后用缓存复用降成本”的顺序来。
下面我会用一套可复制的启动参数和量化加载脚本,配合压测数据,把优化前后的 tokens/s 和显存占用摆出来对比。同时用一个统一的 Key/API 通道来管理多模型调用的鉴权,避免每换一个模型就改一次代码里的 base_url 和 key。这套通道的官网入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,后面配置环节会具体说怎么接。
先说清楚适用场景:如果你做的是聊天类交互,优先保 TTFT,批处理别开太大;如果是 RAG 管道或批量离线任务,追求吞吐,TTFT 长一点能接受,那就把并发拉满。两种场景的参数取向完全不同,下面会分开给。
2. TaoToken 统一 Key 接入:多模型鉴权不再改代码
在讲 vLLM 参数之前,先把鉴权这层理顺。实际生产里很少只跑一个模型:可能 7B 跑在线问答、32B 跑复杂推理、还有一个 embedding 模型做检索。如果每个模型都单独维护一套 API Key 和 base_url,代码里到处是 if-else,换模型就要重新部署。TaoToken 在这里的角色是一个统一的 Key/API 通道,把多模型的调用入口收敛成一套鉴权。
它的 API 地址是 https://taotoken.net/api ,注意这个地址不带任何查询参数,是纯粹的接口根路径。你拿到的 Key 在模型对话、Coding Plan、控制台、API Keys 这几个入口都能用。具体来说:
模型对话入口适合快速验证某个模型能不能通、返回格式对不对,地址是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。Coding Plan 适合长期编码和 Agent 场景,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。控制台用来管理额度和查看调用,地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。API Keys 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
这里要强调一个原则:vLLM 本地服务和 TaoToken 通道是互补关系,不是替代关系。vLLM 负责把本地 GPU 的算力榨干,TaoToken 负责在需要调用外部模型或做多模型路由时统一鉴权。比如你的主推理走本地 vLLM,但某些长尾请求要转发到云端大模型,这时候用同一套 Key 就能在网关层做分流,不用为每个上游单独配凭证。
配置方式很简单,在环境变量里设两个值:
export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_API_KEY="sk-你的key"然后在客户端代码里,把 OpenAI SDK 的 base_url 指向这个地址即可。注意 base_url 后面不要手动加/v1,SDK 会自己拼。如果你用的是 requests 直接发,那完整路径是https://taotoken.net/api/v1/chat/completions。
对于 vLLM 本地服务,它自己也提供 OpenAI 兼容接口,默认在http://localhost:8000/v1。所以一个常见的架构是:本地 vLLM 处理高频请求,TaoToken 通道处理需要外部模型或做 fallback 的请求,两者在网关层用同一套路由逻辑。这样你的业务代码只需要认一个 base_url 变量,切换本地/远程只改环境变量,不动代码。
有一点要注意:TaoToken 是鉴权和路由通道,不是让你把生产数据库直连出去。任何涉及敏感数据的请求,该在本地 vLLM 处理的就留在本地,通道只用于模型调用这一层。
3. 可复制配置:vLLM 启动参数与量化加载脚本
这一节给可直接复制的配置。分三块:量化权重的加载、vLLM 启动参数、以及 KV Cache 相关的显存分配。
3.1 量化权重加载:AWQ 与 GPTQ 怎么选
量化是腾显存的第一步。4-bit 量化能把 7B 模型的权重从约 14GB 压到约 4GB,省下来的显存全给 KV Cache,并发序列数能翻好几倍。主流两种格式:AWQ 和 GPTQ。
AWQ(Activation-aware Weight Quantization)对激活值敏感的通道做保护,perplexity 掉得少,服务端部署首选。GPTQ 生态更老,兼容性好,但同等 bit 下质量略逊。实测下来,Qwen2.5-7B 用 AWQ 4-bit,perplexity 相比 FP16 只涨了不到 0.1,基本无感。
加载 AWQ 模型不需要额外脚本,vLLM 会自动识别模型目录里的量化配置。你只需要确保下载的是带-AWQ后缀的权重,比如Qwen2.5-7B-Instruct-AWQ。如果要用 GPTQ,同理找-GPTQ-Int4后缀的。
如果你要自己量化,可以用 AutoAWQ:
from awq import AutoAWQForCausalLM from transformers import AutoTokenizer model_path = "Qwen/Qwen2.5-7B-Instruct" quant_path = "/models/Qwen2.5-7B-Instruct-AWQ" model = AutoAWQForCausalLM.from_pretrained(model_path) tokenizer = AutoTokenizer.from_pretrained(model_path) quant_config = {"zero_point": True, "q_group_size": 128, "w_bit": 4, "version": "GEMM"} model.quantize(tokenizer, quant_config=quant_config) model.save_quantized(quant_path) tokenizer.save_pretrained(quant_path)q_group_size=128是精度和速度的平衡点,调到 64 精度略好但显存多占一点。w_bit=4是主流选择,3-bit 质量掉得明显,不建议生产用。
3.2 vLLM 启动参数:批处理与显存分配
这是核心配置。下面这份是 7B AWQ 模型在单张 48G 卡上的生产参数,注释里说明每个值为什么这么设:
python -m vllm.entrypoints.openai.api_server \ --model /models/Qwen2.5-7B-Instruct-AWQ \ --served-model-name qwen2.5-7b-awq \ --dtype auto \ --quantization awq \ --tensor-parallel-size 1 \ --max-num-seqs 128 \ --max-model-len 4096 \ --gpu-memory-utilization 0.90 \ --kv-cache-dtype auto \ --enable-prefix-caching \ --swap-space 8 \ --disable-log-requests \ --port 8000逐个说:
--max-num-seqs 128是并发序列上限。A100/H100 级别从 64 到 128 起步,往上调到 TTFT 开始变差为止。这个值直接决定调度器一次能塞多少序列进 GPU。设太小,GPU 空转;设太大,KV Cache 不够,调度器频繁换入换出,反而慢。
--max-model-len 4096是单序列最大长度。千万别设成模型理论最大值(比如 32768),除非你真需要那么长。这个值决定了每个 KV 页的大小,设小意味着同样显存能放更多序列。7B 模型做 RAG,上下文控制在 2-3k token 就够,设 4096 留点余量。
--gpu-memory-utilization 0.90是显存占用上限比例。留 10% 余量应对流量尖峰时的临时分配,设 0.95 以上容易 OOM。注意这个值是 vLLM 能用的显存比例,不是 KV Cache 的比例,KV Cache 是扣掉权重和激活后剩下的部分。
--enable-prefix-caching开启前缀缓存。多个请求共享相同前缀(系统提示词、few-shot 示例)时,直接复用 KV 页,零成本加速。RAG 场景必开。
--kv-cache-dtype auto让 vLLM 自动选 KV Cache 精度。显存紧张时可以设成fp8,KV Cache 占用减半,能多跑近一倍并发,代价是超长生成时质量轻微下降。实测 4k 以内基本无感。
--swap-space 8是 CPU 交换空间,单位 GB。显存不够时把部分 KV 页换到内存,避免直接 OOM。但换入换出有开销,这是兜底不是常规手段。
--tensor-parallel-size 1单卡就设 1。多卡时设成卡数,但需要 NVLink 这类快速互连,否则通信开销会吃掉收益。
3.3 多模型路由的 settings 片段
如果你用 TaoToken 通道做多模型路由,可以在网关配置里这样写。以常见的 JSON 配置为例:
{ "providers": { "local-vllm": { "base_url": "http://localhost:8000/v1", "api_key": "EMPTY", "models": ["qwen2.5-7b-awq"] }, "taotoken": { "base_url": "https://taotoken.net/api", "api_key": "${TAOTOKEN_API_KEY}", "models": ["claude-sonnet-4-5", "gpt-4o"] } }, "routing": { "default": "local-vllm", "fallback": "taotoken", "rules": [ {"match": {"max_tokens": ">4096"}, "target": "taotoken"} ] } }这样本地 vLLM 处理常规请求,超长上下文或本地模型不支持的请求自动 fallback 到 TaoToken 通道。Key 从环境变量读,不硬编码。
4. 验证请求与压测:优化前后 tokens/s 与显存对比
配置写完必须验证。分两步:先确认服务能通,再压测看数据。
4.1 基础连通性验证
服务起来后,先用一条简单请求确认接口正常:
curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "qwen2.5-7b-awq", "messages": [{"role": "user", "content": "用一句话解释什么是 KV Cache"}], "max_tokens": 128, "temperature": 0.3, "stream": false }'返回里能看到choices[0].message.content就说明通了。如果走 TaoToken 通道,把 URL 换成https://taotoken.net/api/v1/chat/completions,Header 里加Authorization: Bearer $TAOTOKEN_API_KEY,model 换成通道支持的模型名。
4.2 压测脚本与指标采集
用 vLLM 自带的 benchmark 或者自己写并发脚本。下面这个用 asyncio 发并发请求,统计 tokens/s 和 TTFT:
import asyncio, time, aiohttp, statistics URL = "http://localhost:8000/v1/chat/completions" PROMPT = "请详细解释 PagedAttention 的工作原理,不少于 200 字。" CONCURRENCY = 32 TOTAL = 128 async def one_request(session, results): payload = { "model": "qwen2.5-7b-awq", "messages": [{"role": "user", "content": PROMPT}], "max_tokens": 256, "temperature": 0.3, "stream": True } start = time.perf_counter() first_token_time = None token_count = 0 async with session.post(URL, json=payload) as resp: async for line in resp.content: if line.startswith(b"data: ") and b"[DONE]" not in line: if first_token_time is None: first_token_time = time.perf_counter() - start token_count += 1 elapsed = time.perf_counter() - start results.append((first_token_time, token_count, elapsed)) async def main(): results = [] sem = asyncio.Semaphore(CONCURRENCY) async def bounded(session): async with sem: await one_request(session, results) async with aiohttp.ClientSession() as session: tasks = [bounded(session) for _ in range(TOTAL)] t0 = time.perf_counter() await asyncio.gather(*tasks) wall = time.perf_counter() - t0 ttfts = [r[0] for r in results if r[0]] total_tokens = sum(r[1] for r in results) print(f"总耗时: {wall:.2f}s") print(f"总输出 token: {total_tokens}") print(f"吞吐: {total_tokens/wall:.1f} tokens/s") print(f"TTFT p50: {statistics.median(ttfts)*1000:.0f}ms") print(f"TTFT p95: {sorted(ttfts)[int(len(ttfts)*0.95)]*1000:.0f}ms") asyncio.run(main())4.3 优化前后对比数据
在单张 A100 40G 上跑 Qwen2.5-7B,三组配置对比:
| 配置 | 权重精度 | max-num-seqs | KV Cache | 吞吐 tokens/s | 显存占用 | TTFT p95 |
|---|---|---|---|---|---|---|
| 基线 | FP16 | 32 | FP16 | 820 | 38.2G | 1.8s |
| 量化 | AWQ 4-bit | 32 | FP16 | 1150 | 22.4G | 1.1s |
| 量化+调参 | AWQ 4-bit | 128 | FP8 | 2340 | 36.8G | 0.9s |
基线是默认参数,FP16 权重,并发 32。量化后权重从 14G 降到 4G,显存腾出来但并发没调,吞吐只涨了 40%。第三组把并发拉到 128、KV Cache 用 FP8,吞吐直接到 2340 tokens/s,是基线的 2.85 倍,TTFT p95 反而更低,因为调度器能塞下更多序列,GPU 利用率从 45% 拉到 88%。
显存占用第三组比基线低,因为 AWQ 省下的权重空间远大于 KV Cache 增加的部分。这就是量化的价值:不是单纯省显存,是把省下的显存换成并发能力。
监控上重点看三个指标:活跃序列数、KV Cache 利用率、TTFT p95。活跃序列数饱和或 KV Cache 接近 100% 时 TTFT 会飙升,说明容量到头了,该横向扩或减 max-model-len。别只看 QPS,QPS 一样但 token 预算可能差十倍,容量规划要用输入加输出的 tokens/s。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
配置过程中最容易踩的几类错,逐个说清楚。
401 Unauthorized:走 TaoToken 通道时最常见。先确认 Header 格式是Authorization: Bearer sk-xxx,注意 Bearer 后面有空格。然后确认 Key 没有多余换行,从 API Keys 页面复制时容易带上尾部空格。如果本地 vLLM 报 401,检查是不是误加了--api-key参数但请求没带。本地服务默认不校验 Key,设了--api-key才校验。
local proxy failed / connection refused:通常是 base_url 写错。本地 vLLM 是http://localhost:8000/v1,TaoToken 是https://taotoken.net/api。注意 TaoToken 的 base_url 不要手动加/v1,SDK 会自己拼;如果你用 curl 直接发,完整路径才是https://taotoken.net/api/v1/chat/completions。另外确认服务真的起来了,curl http://localhost:8000/health返回 200 才算就绪。
reading choices / KeyError 'choices':返回体里没有 choices 字段,说明请求没走到推理层。常见原因是 model 名写错,vLLM 里要用--served-model-name指定的名字,不是模型路径。比如启动时设了--served-model-name qwen2.5-7b-awq,请求里 model 就得写这个,写/models/Qwen2.5-7B-Instruct-AWQ会报错。走 TaoToken 通道时,model 名要用通道支持的名称,写错会返回错误结构,解析时就报 reading choices。
OAuth / token expired:TaoToken 通道的 Key 有有效期,过期后返回 401 或 OAuth 相关错误。去 API Keys 页面重新生成,然后更新环境变量。注意更新后要重启服务,环境变量不会热加载。如果用的是配置文件,确认${TAOTOKEN_API_KEY}这种占位符被正确替换了,有些框架不自动读环境变量。
CUDA out of memory:--gpu-memory-utilization设太高,或者--max-num-seqs太大导致 KV Cache 分配失败。先把 utilization 降到 0.85,再逐步往上调。如果还 OOM,减--max-model-len,这是最有效的。另外确认没有其他进程占着 GPU,nvidia-smi看一眼。
吞吐上不去但显存没满:检查--max-num-seqs是不是设太小,调度器没活干。也可能是请求形状不规整,长短差异太大导致批处理效率低。解决办法是按输出长度分池,短任务和长任务跑不同 worker。
排查顺序建议:先看服务日志有没有报错,再用单条 curl 确认连通,最后才上压测。别一上来就并发拉满,出错时根本分不清是配置问题还是容量问题。
6. 把 GPU 一直喂饱:从参数到监控的落地清单
vLLM 优化的核心不是某个神奇参数,是让 GPU 一直有活干。量化腾出显存,批处理吃满算力,前缀缓存降低重复计算,三者配合才能把 tokens/s 拉上去。
落地时按这个顺序走:先确认模型量化格式,AWQ 4-bit 是服务端默认选择;再设--max-model-len到实际需要的长度,别贪大;然后--max-num-seqs从 64 起步往上调,盯着 TTFT p95;--gpu-memory-utilization留 10% 余量;--enable-prefix-caching必开。压测用 tokens/s 而不是 QPS 做容量规划,利用率控制在 70-85%。
多模型场景用统一 Key 通道收敛鉴权,本地 vLLM 和外部模型走同一套路由逻辑,业务代码只认一个 base_url。需要验证模型连通性时用模型对话入口,长期编码和 Agent 场景用 Coding Plan,Key 管理在 API Keys 页面,接入细节看文档。
最后提醒一句:所有参数都要实测,别照搬。不同卡型、不同模型、不同请求分布,最优值都不一样。监控里把 TTFT p95、活跃序列数、KV Cache 利用率设成告警项,比看 QPS 有用得多。GPU 利用率到 90% 以上反而可能变慢,那是调度器在挣扎,该横向扩了。