vLLM 部署几乎是每个把本地大模型推向生产的团队必经的一关。用 Ollama 或 llama.cpp 做原型验证很舒服,但只要并发上到几十路,就会发现响应从 2 秒退化到 30 秒、显存莫名打满、批量请求排队严重。原因不是显卡不够快,而是推理服务的调度与显存管理方式不对。本文完整走一遍 vLLM 的原理、启动参数、调优手法和压测方法,接着我们之前那篇 Ollama 模型管理:Modelfile 自定义与调优 往上走一层,把”能跑”变成”能扛”。
一、为什么本地跑得动的模型,上线就崩
本地推理工具的设计目标是”单人对话流畅”,它们通常一次只服务一个会话,KV Cache 按最大序列长度静态预留,请求排成一条队列串行执行。这在个人使用时完全够用,但放到线上会同时暴露三个问题:显存利用率低(预留了却用不满)、请求必须等前一个彻底跑完(队头阻塞)、批处理只能按”最慢的那条”对齐(长短请求互相拖累)。
vLLM 正是为解决这三点而生的推理引擎。它把注意力缓存按块管理、把批处理粒度从”一批”细化到”一个 token 步”,从而在同样的显卡上把吞吐量拉高一个量级。选型时可以按下面这张表对号入座。
| 方案 | 定位 | 并发能力 | 硬件要求 | 适用场景 |
|---|---|---|---|---|
| Ollama | 本地开箱即用 | 弱(串行为主) | CPU / 消费级 GPU 均可 | 原型验证、个人助手 |
| llama.cpp | 极致轻量、量化友好 | 弱到中 | CPU / Apple 芯片可跑 | 边缘设备、低配环境 |
| vLLM | 高吞吐在线服务 | 强(连续批处理) | NVIDIA GPU + CUDA | 生产 API、多用户并发 |
| TGI | HuggingFace 生态服务 | 强 | NVIDIA GPU | 已深度绑定 HF 工具链 |
| SGLang | 复杂结构化生成 | 强 | NVIDIA GPU | Agent、多轮约束解码 |
结论很直接:验证阶段用 Ollama,上线阶段换 vLLM。两者并不冲突,很多团队保留 Ollama 做本地调试,线上用 vLLM 提供统一的 OpenAI 兼容接口。
二、性能来自哪里:PagedAttention 与连续批处理
PagedAttention:像操作系统分页那样管显存
自回归生成时,每个 token 都要保留 Key/Value 缓存供后续步骤复用。传统做法按”最大可能长度”给每条请求连续预留一整块显存,一条最长 32K 的请求即便实际只生成 200 个 token,显存也照最大值占着,碎片和浪费极其严重。
PagedAttention 借用虚拟内存的思路:把 KV Cache 切成固定大小的块(block),逻辑上连续、物理上分散,按需分配、用完归还。好处有三个:显存浪费从常见的一半以上降到个位数百分比;相同前缀(如同一段系统提示词)可以在多条请求间共享同一批物理块;并行采样多个候选结果时,公共前缀只存一份。这也是为什么开启前缀缓存后,长系统提示词的场景收益格外明显。
Continuous Batching:不等整批跑完就换人
静态批处理必须凑够一批、并且等批内最慢的请求生成完才能释放,短请求被长请求拖死。vLLM 的调度器在每个解码步都重新组批:谁生成完了立刻退出并释放显存块,等待队列里的新请求马上补位。对线上真实流量(长度分布极不均匀)来说,这一个改动带来的吞吐提升往往比换一张更贵的卡更划算。
三、环境准备:先确认这三件事
vLLM 依赖 NVIDIA GPU 与 CUDA 环境,安装前先核对驱动、显存和 Python 版本,避免装完才发现跑不起来。国内拉模型建议先设置镜像端点。
# 1. 确认驱动与显存(至少 16GB 显存才适合跑 7B 非量化模型)
nvidia-smi
# 2. 建议独立虚拟环境,Python 3.10+
python3 -m venv ~/venv-vllm
source ~/venv-vllm/bin/activate
pip install --upgrade pip
# 3. 安装 vLLM(会自动带上匹配的 torch)
pip install vllm
# 4. 国内环境用镜像加速模型下载
export HF_ENDPOINT=https://hf-mirror.com
export HF_HOME=/data/hf-cache # 模型缓存放到大盘,避免系统盘写满
# 5. 验证安装
python -c "import vllm; print(vllm.__version__)"
如果宿主机环境复杂、不想污染系统,用官方镜像更省心。容器方式还有个隐性好处:CUDA 与 torch 版本已经匹配好,不用自己对表。
docker run --gpus all \
-v /data/hf-cache:/root/.cache/huggingface \
-e HF_ENDPOINT=https://hf-mirror.com \
-p 8000:8000 \
--ipc=host \
vllm/vllm-openai:latest \
--model Qwen/Qwen2.5-7B-Instruct \
--served-model-name qwen2.5-7b \
--max-model-len 8192
注意 --ipc=host 不能省,否则容器内共享内存过小会在加载权重阶段直接报错。容器化部署再往前一步就是编排,可以对照 Kubernetes 入门:Pod、Deployment 与 Service 实战 把它做成可扩缩的 Deployment。
四、一条命令起服务:关键启动参数
单卡起步
vllm serve Qwen/Qwen2.5-7B-Instruct \
--served-model-name qwen2.5-7b \
--host 0.0.0.0 --port 8000 \
--dtype bfloat16 \
--max-model-len 8192 \
--gpu-memory-utilization 0.90 \
--max-num-seqs 128 \
--api-key sk-your-internal-key
# 旧版本没有 vllm serve 子命令时,等价写法:
# python -m vllm.entrypoints.openai.api_server --model Qwen/Qwen2.5-7B-Instruct --port 8000
多卡张量并行
单卡显存装不下更大的模型时,用张量并行把权重切到多张卡上。并行度必须能整除模型的注意力头数,所以一般取 2、4、8,而不是 3、5 这类数字。
# 双卡跑 32B 级模型,并开启前缀缓存与更大的 CPU 交换区
vllm serve Qwen/Qwen2.5-32B-Instruct \
--tensor-parallel-size 2 \
--gpu-memory-utilization 0.92 \
--max-model-len 16384 \
--enable-prefix-caching \
--swap-space 8
# 显存实在紧张时,用量化权重换空间(AWQ / GPTQ / FP8)
vllm serve Qwen/Qwen2.5-32B-Instruct-AWQ --quantization awq --tensor-parallel-size 2
关于量化格式的取舍,以及它对精度与速度的实际影响,可以进一步参考 本地大模型量化部署:GGUF 与 llama.cpp 调优:GGUF 面向 CPU/边缘,AWQ 与 GPTQ 才是 GPU 服务端的主流选择,别把两条路径搞混。
五、OpenAI 兼容接口:现有代码几乎零改动
vLLM 直接暴露 /v1/chat/completions、/v1/completions、/v1/embeddings、/v1/models 等 OpenAI 风格端点,原本调用云端 API 的代码只要换 base_url 和 key 就能切过来。
# 快速验证:列出已加载模型
curl http://127.0.0.1:8000/v1/models -H "Authorization: Bearer sk-your-internal-key"
# 对话请求(流式加 "stream": true)
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-your-internal-key" \
-d '{
"model": "qwen2.5-7b",
"messages": [{"role": "user", "content": "用三句话解释 PagedAttention"}],
"temperature": 0.3,
"max_tokens": 256
}'
from openai import OpenAI
client = OpenAI(
base_url="http://127.0.0.1:8000/v1",
api_key="sk-your-internal-key",
)
resp = client.chat.completions.create(
model="qwen2.5-7b",
messages=[
{"role": "system", "content": "你是严谨的后端技术助手,回答附可运行示例。"},
{"role": "user", "content": "如何为 Nginx 配置上游超时?"},
],
temperature=0.2,
max_tokens=512,
stream=True,
)
for chunk in resp:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)
因为接口兼容,Dify、LangChain、One-API 这类上层平台都能直接把 vLLM 当作自定义 OpenAI 端点接入——具体接法可以对照 Dify + Ollama 搭建本地知识库问答,把其中的模型供应商地址换成 vLLM 服务即可;如果业务要做工具调用,再叠加 大模型工具调用 Function Calling 实战 里的那套函数编排思路。
六、显存与吞吐调优四板斧
vLLM 的参数不多,但每个都直接影响成败。下面这四个是实际调优中动得最频繁的,建议一次只改一个再压测,否则根本分不清是哪项起了作用。
| 参数 | 作用 | 调大的代价 | 建议起点 |
|---|---|---|---|
--gpu-memory-utilization | 允许占用的显存比例,剩余部分留给 KV Cache | 过高会与其他进程抢显存导致 OOM | 独占卡 0.90,共享卡 0.75 |
--max-model-len | 单请求最大上下文长度 | 越长 KV Cache 越吃显存,并发数骤降 | 按真实业务取 8K,别直接开满 |
--max-num-seqs | 同时在跑的最大请求数 | 过大导致排队变长、单请求延迟升高 | 7B 单卡 64~128 |
--enable-prefix-caching | 复用相同前缀的 KV 块 | 额外少量显存开销 | 系统提示词长/RAG 场景务必开 |
一条经验法则:显存预算 ≈ 权重占用 + 并发数 × 上下文长度 × 每 token 的 KV 开销。所以当有人抱怨”并发上不去”时,先问一句 --max-model-len 是不是照着模型上限开满了。把 32K 砍到 8K,可用并发常常能翻两三倍,而多数业务请求根本用不到 32K 上下文。
另外别忘了 --swap-space:当显存块不够时,vLLM 会把部分请求的 KV Cache 换出到 CPU 内存而不是直接失败,代价是这部分请求变慢。它是保命阀门,不是性能开关。
七、压测与观测:别凭感觉调参
调参必须有数据支撑。核心看三个指标:TTFT(首 token 延迟,决定用户体感)、TPOT(每 token 生成间隔,决定流式流畅度)、以及整体吞吐(每秒输出 token 数,决定成本)。vLLM 仓库自带压测脚本,较新版本还提供了 vllm bench serve 子命令。
# 方式一:官方压测脚本(需 clone 仓库)
git clone https://github.com/vllm-project/vllm.git && cd vllm/benchmarks
python benchmark_serving.py \
--backend vllm \
--model Qwen/Qwen2.5-7B-Instruct \
--dataset-name random \
--random-input-len 512 --random-output-len 256 \
--num-prompts 200 --request-rate 10
# 方式二:较新版本的内置子命令
vllm bench serve --model qwen2.5-7b --num-prompts 200 --request-rate 10
线上还要持续观测。vLLM 内置 /metrics 端点,可直接被 Prometheus 抓取,几个最该盯的指标是:vllm:num_requests_running(正在跑的请求数)、vllm:num_requests_waiting(排队数,持续大于零说明容量不足)、vllm:gpu_cache_usage_perc(KV Cache 使用率,长期贴近 100% 就要降 max-model-len 或加卡)、vllm:time_to_first_token_seconds(TTFT 分布)。接入方式与告警配置可以直接复用 Prometheus + Grafana 监控面板实战 那一套。
curl -s http://127.0.0.1:8000/metrics | grep -E "num_requests|gpu_cache_usage"
# prometheus.yml 片段
# - job_name: vllm
# static_configs:
# - targets: ['10.0.0.12:8000']
八、高频报错排查清单
| 现象 | 根因 | 处理动作 |
|---|---|---|
| 启动即 CUDA out of memory | 权重加显存占用超预算 | 降 --gpu-memory-utilization、改用量化权重或加张量并行 |
| 提示 KV Cache 装不下最大序列长度 | --max-model-len 开得过大 | 按业务下调上下文长度,或提高显存占用比例 |
| 并发一上来延迟飙升 | 请求在排队而非在跑 | 看 num_requests_waiting,调 --max-num-seqs 或横向扩实例 |
| 容器内加载权重崩退 | 共享内存不足 | 启动加 --ipc=host 或调大 --shm-size |
| 模型下载卡死或超时 | 未走镜像端点 | 设置 HF_ENDPOINT,或先离线下载再用本地路径启动 |
| 多卡启动报头数不整除 | 并行度设置不合法 | 把 --tensor-parallel-size 改为能整除注意力头数的 2/4/8 |
排查顺序建议固定为:先看启动日志(多数问题在加载阶段就报出来了)、再看 /metrics(区分”跑得慢”和”在排队”)、最后才动参数。反过来先改参数,很容易把一个配置问题误判成性能问题。
九、落地建议
把 vLLM 推上生产,除了参数还有几件工程上的事必须做:一是模型权重提前下载并固化到本地路径,启动时不依赖外网,避免发版时卡在下载;二是服务前置一层网关做鉴权、限流和多实例负载,别把推理端口直接暴露到公网;三是按业务拆实例,把长上下文的文档分析和短对话的客服问答分开部署,参数各自最优,互不拖累。
总结一句:Ollama 解决”能不能跑”,vLLM 解决”能不能扛”。PagedAttention 省下的是显存,连续批处理省下的是等待时间,而真正决定成本的往往只是一个被随手开满的 --max-model-len。先压测出基线,再一个参数一个参数地动,是这类服务调优唯一靠得住的做法。




