vLLM 部署实战:高吞吐 LLM 推理服务调优

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、多用户并发
TGIHuggingFace 生态服务NVIDIA GPU已深度绑定 HF 工具链
SGLang复杂结构化生成NVIDIA GPUAgent、多轮约束解码

结论很直接:验证阶段用 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。先压测出基线,再一个参数一个参数地动,是这类服务调优唯一靠得住的做法。

上一篇 Spring 事务传播机制深度解析:7 种行为实战
下一篇 前端性能监控:Web Vitals与Sentry实战