用 vLLM 的 Docker 镜像在 GPU 服务器上部署 Qwen3-8B,提供带密钥的 OpenAI 兼容接口,开启思考内容解析和工具调用解析。再写一个压测脚本,测首字延迟(TTFT)和吞吐,帮你决定并发数和机器配置。
先看清成品
vLLM 部分没有在 box 上运行(box 没有 GPU):
docker-compose.vllm.yml只做了docker compose config校验。参数来自 Qwen 官方的 vLLM 部署文档和 vLLM 的 Docker 文档。压测脚本在 box 上实测过,目标是 Ollama 加 Qwen3-0.6B(CPU):
并发 吞吐 TTFT 中位数 1 102.4 tok/s 0.04 s 2 139.0 tok/s 0.06 s 4 232.9 tok/s 0.11 s 测试时设置了
OLLAMA_NUM_PARALLEL=4并关闭思考。之前另一次运行的数字是 135.6 / 118.5 / 239.3,那次并发 2 比并发 1 还低,说明单次结果波动不小,要多跑几次。
合规提醒:Qwen3 系列开源模型是 Apache-2.0 许可(以 Hugging Face 模型页为准),商用前请自己再确认一遍。本地部署时资料不出本机,但 Ollama 的 11434 和 vLLM 的 8000 端口不要暴露到公网;vLLM 默认不鉴权,本文强制设置
--api-key。用百炼(DashScope)云端 API 时,数据会发到阿里云,敏感数据先脱敏。价格以阿里云百炼的计费页为准,本文不列价格。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
docker-compose.vllm.yml |
vLLM 加 Qwen3-8B;只绑定 127.0.0.1:8000;必须设置 VLLM_API_KEY 才能启动;可以改用 ModelScope 下载 |
loadtest.py |
预热一次 → 按不同并发数各发 n 个流式请求 → 输出吞吐和 TTFT 的中位数与 P95 |
需要准备哪些环境?
| 项目 | 要求 | 说明 |
|---|---|---|
| GPU | NVIDIA 显卡和 NVIDIA Container Toolkit | 显存需求取决于模型、上下文长度和精度,以 Qwen 文档为准,先用小模型试 |
| Docker | 带 compose 插件 | 镜像 vllm/vllm-openai:latest,需要 ipc: host |
| Python | 3.9 以上 | pip install openai |
文件结构
44-qwen-B-src/
├── docker-compose.vllm.yml
└── loadtest.py
第 1 步:部署
docker-compose.vllm.yml:
# vLLM 部署 Qwen3(OpenAI 兼容接口),需要 NVIDIA 显卡 + NVIDIA Container Toolkit。
# 参数来自 vLLM Docker 文档和 Qwen 官方 vLLM 部署文档:
# --reasoning-parser qwen3 思考内容单独放进 reasoning_content
# --enable-auto-tool-choice --tool-call-parser hermes 工具调用
# --api-key 给接口加一个密钥(vLLM 默认不鉴权)
# 国内下载慢可以改用 ModelScope:取消 VLLM_USE_MODELSCOPE 的注释
services:
vllm:
image: vllm/vllm-openai:latest
ipc: host
ports: ["127.0.0.1:8000:8000"] # 只绑本机;要给局域网用,前面加反向代理和访问控制
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
environment:
HF_TOKEN: ${HF_TOKEN:-}
# VLLM_USE_MODELSCOPE: "true"
command: >
--model Qwen/Qwen3-8B --served-model-name qwen3
--max-model-len 32768 --gpu-memory-utilization 0.90
--reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser hermes
--api-key ${VLLM_API_KEY:?请先设置 VLLM_API_KEY}
deploy:
resources:
reservations:
devices: [{driver: nvidia, count: all, capabilities: [gpu]}]
restart: unless-stopped
$ export VLLM_API_KEY=$(openssl rand -hex 16)
$ docker compose -f docker-compose.vllm.yml up -d
$ curl -H "Authorization: Bearer $VLLM_API_KEY" http://127.0.0.1:8000/v1/models
不想要思考过程时,请求里加 "chat_template_kwargs": {"enable_thinking": false}(Qwen 文档)。
第 2 步:压测
loadtest.py:
"""对任意 OpenAI 兼容接口(vLLM / Ollama / 云 API)做并发压测:首字延迟(TTFT)和整体吞吐。
用法:python loadtest.py --base http://127.0.0.1:8000/v1 --model qwen3 --key $VLLM_API_KEY --conc 1 4 8 --n 8
关闭 Qwen3 思考:vLLM 用 --vllm-no-think(chat_template_kwargs.enable_thinking=false);Ollama 用 --ollama-no-think(reasoning_effort=none)
注意:在 Ollama 的 OpenAI 兼容接口上传 think=false 不起作用(实测仍然思考),要用 reasoning_effort=none"""
import argparse, statistics, time
from concurrent.futures import ThreadPoolExecutor
from openai import OpenAI
ap = argparse.ArgumentParser()
ap.add_argument("--base", required=True); ap.add_argument("--model", required=True); ap.add_argument("--key", default="EMPTY")
ap.add_argument("--conc", type=int, nargs="+", default=[1, 4]); ap.add_argument("--n", type=int, default=8)
ap.add_argument("--max-tokens", type=int, default=128)
ap.add_argument("--vllm-no-think", action="store_true"); ap.add_argument("--ollama-no-think", action="store_true")
a = ap.parse_args()
client = OpenAI(base_url=a.base, api_key=a.key, max_retries=0, timeout=600)
extra = {"chat_template_kwargs": {"enable_thinking": False}} if a.vllm_no_think else ({"reasoning_effort": "none"} if a.ollama_no_think else {})
PROMPTS = ["介绍一下杭州西湖", "用 Python 写冒泡排序", "解释 TCP 三次握手", "给新员工写一段欢迎词"]
def one(i):
t0 = time.time(); ttft = None; toks = 0
s = client.chat.completions.create(model=a.model, stream=True, max_tokens=a.max_tokens, temperature=0, extra_body=extra,
stream_options={"include_usage": True},
messages=[{"role": "user", "content": PROMPTS[i % len(PROMPTS)]}])
for ch in s:
if ch.choices and (ch.choices[0].delta.content or getattr(ch.choices[0].delta, "reasoning", None)
or getattr(ch.choices[0].delta, "reasoning_content", None)) and ttft is None: ttft = time.time() - t0
if ch.usage: toks = ch.usage.completion_tokens
return ttft or 0, toks, time.time() - t0
one(0) # 预热:第一个请求包含加载模型的时间,不计入结果
for c in a.conc:
t0 = time.time()
with ThreadPoolExecutor(c) as ex: res = list(ex.map(one, range(a.n)))
wall = time.time() - t0; ttfts = sorted(r[0] for r in res); total = sum(r[1] for r in res)
p95 = ttfts[min(len(ttfts) - 1, int(len(ttfts) * 0.95))]
print(f"并发 {c:>2}:{a.n} 个请求用时 {wall:5.1f}s,总吞吐 {total/wall:6.1f} tok/s,"
f"首字延迟 中位 {statistics.median(ttfts):.2f}s / P95 {p95:.2f}s")
# box 上实测(Ollama,OLLAMA_NUM_PARALLEL=4)
$ python loadtest.py --base http://127.0.0.1:11434/v1 --model qwen3-local:0.6b --conc 1 2 4 --key x --n 8 --ollama-no-think
并发 1:8 个请求用时 9.7s,总吞吐 102.4 tok/s,首字延迟 中位 0.04s / P95 0.05s
并发 2:8 个请求用时 7.1s,总吞吐 139.0 tok/s,首字延迟 中位 0.06s / P95 0.08s
并发 4:8 个请求用时 4.3s,总吞吐 232.9 tok/s,首字延迟 中位 0.11s / P95 0.16s
对照实验:Ollama 在 OLLAMA_NUM_PARALLEL=1 时,吞吐基本不随并发增加,TTFT 在并发 4 时涨到 3.18 秒,因为请求在排队。vLLM 的连续批处理正是为解决这个问题设计的,但本文没有在 GPU 上实测。
调试示例:关不掉思考
压测 Ollama 时,我第一次通过 extra_body 传入 think: false。结果输出 tokens 很多,content 却是空的:一个请求返回了 1297 字的 reasoning,content 为空。原来 Ollama 的 OpenAI 兼容接口不认这个参数(它是 Ollama 原生 API 的参数)。改用 reasoning_effort: "none",或者在提示词末尾加 /no_think,都能生效。vLLM 上则用 chat_template_kwargs.enable_thinking=false。压测前一定要先看一眼实际输出,否则测出来的全是思考过程的速度。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 容器启动后显存不足(OOM) | 模型或上下文太大 | 降低 --max-model-len,或换小模型、量化版本 |
| 工具调用返回的是普通文本 | 没有开启工具调用解析 | 加上 --enable-auto-tool-choice --tool-call-parser hermes |
| 401 | 没有带密钥 | 请求头加 Authorization: Bearer $VLLM_API_KEY |
| 国内下载模型很慢 | 从 Hugging Face 下载 | 设置 VLLM_USE_MODELSCOPE=true |
模板规则
- 推理接口必须设置密钥,只绑定本机地址。
- 压测前先预热,并确认输出的不是思考内容。
- 配置以自己实测的数字为准。
项目资源
| 用途 | 地址 |
|---|---|
| Qwen 文档:vLLM 部署 | https://qwen.readthedocs.io/en/latest/deployment/vllm.html |
| vLLM Docker 部署 | https://docs.vllm.ai/en/latest/deployment/docker.html |
| Qwen3-8B 模型卡片 | https://huggingface.co/Qwen/Qwen3-8B |
| Ollama 环境变量(FAQ) | https://docs.ollama.com/faq |
先测出 TTFT 和吞吐,再谈要多少张卡、开多大并发。