如果你的本地大模型服务经常出现请求排队、首字卡顿、并发一高就显存溢出,先别急着换显卡——大多数时候是推理框架的调度方式没选对。
vLLM 是目前社区里使用最广的高吞吐推理引擎之一。它把大模型推理时最占显存却最容易被浪费的 KV Cache 管理方式彻底改掉,靠 PagedAttention 和连续批处理把 GPU 算力压干。同时提供 OpenAI 兼容 API,意味着你之前写的openai客户端代码,只需要改一个base_url就能切到本地模型。
本文会从 vLLM 的原理拆起,讲清 PagedAttention 和连续批处理到底解决了什么,然后给出一套可直接复用的 Python 部署、接口调用、并发测试和故障排查流程。适合想把开源模型部署成高并发服务的后端开发者、算法工程师,也适合已经用 Ollama/Transformers 跑过模型、但觉得吞吐不够的同学。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | 大模型推理引擎 / 高吞吐推理服务框架 |
| 核心机制 | PagedAttention(分页注意力)、连续批处理(Continuous Batching)、OpenAI 兼容 API |
| 支持模型 | 常见开源 LLM 文本模型,以及部分多模态/嵌入模型;需结合 vLLM 官方文档确认 |
| 适用模型规模 | 从 7B/8B 到几十 B 或更大规模,取决于单卡显存或多卡张量并行 |
| 推荐硬件 | NVIDIA GPU 优先;显存规格由“模型权重 + KV Cache + 激活值”共同决定,需实测 |
| 是否支持 CPU | vLLM 也提供后端用于 CPU 推理,但高吞吐场景优先 GPU |
| 是否支持多卡 | 支持,通过张量并行(tensor-parallel-size)在单机多卡上切分模型 |
| 启动方式 | 命令行vllm serve,也可用 Docker 镜像 |
| 是否支持 API | 支持 OpenAI 兼容接口:/v1/models、/v1/chat/completions、/v1/completions等 |
| 是否支持批量任务 | 支持,配合并发客户端可提升吞吐;服务端按请求级调度 |
| 监控能力 | 可通过--enable-metrics暴露 Prometheus 指标,默认日志会输出吞吐数据 |
| 一键启动 | 社区有集成项目,但官方标准方式是命令启动 |
| 适合场景 | 统一模型网关、Agent 工具链后端、私有知识库问答、批量内容生成 |
2. 为什么传统推理框架会“越用越慢”
传统的 Transformers 库推理常见两种状态:单请求逐个推理,或者静态 batch 推理。前者浪费 GPU 算力,GPU 利用率很低;后者虽然吃满显存,但 batch 是固定大小的,只要 batch 里还有一个请求没跑完,整个 batch 都占着显存和算力,新请求必须等下一批。
LLM 推理是一个自回归过程,每个 token 都要依赖之前生成的 token。生成过程中需要把历史上所有 token 的键值向量缓存下来,这就是 KV Cache。KV Cache 的大小和序列长度成正比,长上下文时非常吃显存。传统框架为了省事,通常会根据 max_length 或某个估算值给每个请求预分配一大块连续显存,但实际生成长度往往用不满,这部分预留显存就浪费了。更麻烦的是,不同请求的长度不一致,预分配的碎片会让显存出现大量空洞,GPU 显存明明没满,却塞不进新的请求。
vLLM 的两个核心机制就是冲着这两个问题去的:PagedAttention 解决 KV Cache 的分配与碎片问题,连续批处理解决静态 batch 的调度浪费问题。这两点叠加,在长上下文、高并发场景下吞吐提升非常明显。部分公开 benchmark 中,vLLM 相对朴素实现的吞吐提升可以达到数倍到 8 倍以上,但具体数字受模型、显卡、批参数、输入输出长度影响很大,不要拿标题中的“8 倍”当万能结论。
3. PagedAttention:用操作系统的思路管理显存
写操作系统的同学对“分页”应该很熟。vLLM 的 PagedAttention 借鉴了操作系统的虚拟内存和页表思想:把连续显存切成固定大小的 block(页),每个 block 默认存若干个 token 的 KV Cache。请求的 KV Cache 不再要求一整块连续显存,而是一页一页按需分配,然后用一个“页表”把它们串起来。
这样做有几个直接收益:
- 显存按需分配,请求生成多少 token 就分配多少页,不再因为“预留一整块连续显存”而浪费空间。
- KV Cache 可以存放在非连续显存中,碎片能被重新利用,显存利用率大幅提高。
- 同一条序列生成长度不断增加时,只需要申请新页,旧的页可以复用。
- 注意力计算时,通过页表索引找到对应的 key、value,计算逻辑统一,性能开销可控。
简单说,PagedAttention 让“显存里还剩很多碎片空间,但新请求进不来”这种情况基本消失。这也是 vLLM 能同时容纳更多并发请求的关键前提。实际部署时,常见做法是设置--gpu-memory-utilization,例如 0.85 到 0.95,让 vLLM 把大部分显存拿来做 KV Cache。
4. 连续批处理:吃掉闲散算力
传统静态批处理:请求先堆积,凑够 batch size 才推理,batch 内所有请求必须一起到结束。长请求会拖慢短请求,短请求跑完了也要等长请求,GPU 每次都跟着最慢的那个请求一起结束,算力白白浪费。
连续批处理(Continuous Batching)把调度粒度从“请求”降到了“解码步”。每个 step 结束后,vLLM 会检查:
- 哪些请求已经生成完毕?立即释放它们的显存和算力。
- 有没有新请求排队?可以立刻插入下一个 step。
这样短请求快速完成退出,长请求继续生成,新请求随时插队,GPU 每个 step 都在尽最大可能满负荷运行。整个过程由 vLLM 的调度器完成,不需要调用方关心。这也是 vLLM 高并发吞吐的核心来源。
需要补充一点:连续批处理对单个请求的延迟影响不大,它提升的是整机吞吐。如果你只有一个串行请求,vLLM 的“快”更多体现在显存复用和长上下文管理上,而不是单请求推理时间。想让 vLLM 发挥优势,一定要用并发去打。
5. 本地部署环境准备与前置条件
vLLM 是 Python 生态,安装和部署都比较直给。部署前先确认几件事:
5.1 硬件
- GPU:NVIDIA GPU,驱动版本要能支持 CUDA 11.8 或更高。显存建议根据模型规模来:8B 模型建议 16G 以上,量化版可以更低一些;更大的模型建议 24G 或使用多卡张量并行。注意:具体显存占用必须实测,模型权重、max-model-len、并发数都会影响 KV Cache 占用。
- 内存:建议 32G 以上,模型加载和 tokenizer 预处理都需要内存。
- 硬盘:模型文件按参数量算,8B FP16 权重约 16G 左右,量化版更小。需要预留模型文件 + 运行日志的空间。
5.2 操作系统与软件
- Linux 优先(Ubuntu 18.04+ / 20.04 / 22.04 都是常见选择)。Windows 上 vLLM 的官方支持较弱,更稳妥的做法是 Linux + WSL2 + CUDA 环境。
- Python 3.9 到 3.12,具体以 vLLM 官方文档为准。
- CUDA Toolkit + 对应版本的 PyTorch。
- 如果使用 Docker,需要 NVIDIA Container Toolkit。
5.3 检查清单
# 检查显卡驱动 nvidia-smi # 检查 Python 版本 python --version # 检查 CUDA 是否可用(PyTorch 装好后) python -c "import torch; print(torch.cuda.is_available())"如果nvidia-smi都跑不出来,先处理驱动,再继续 vLLM 安装。
5.4 模型来源
本文用 Qwen3 系列作为示例,你也可以换成 Llama、DeepSeek 或其他支持的模型。模型名需要按 Hugging Face 上的仓库标识填写,例如Qwen/Qwen3-8B这类形式需要以你下载的模型名为准。首次启动时 vLLM 会自动下载权重,如果不想走公网下载,可以先把模型下载到本地目录,然后通过--model /path/to/model指定本地路径。
下载模型建议用 huggingface-cli 或 modelscope 的工具,这部分工具与推理框架解耦,按自己的网络环境选择。
6. 安装 vLLM 与启动 Qwen3 服务
6.1 安装 vLLM
建议创建独立虚拟环境,避免依赖冲突:
python -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install vllm安装后检查版本,确认 vllm 已进入当前环境:
vllm --version如果是 Docker 部署,官方镜像可以按文档拉取。注意镜像 tag 与你的 CUDA 驱动保持兼容,具体以官方 Docker Hub 为准。
6.2 启动一个 OpenAI 兼容的服务
下面以 Qwen3-8B 为例,启动一个服务:
vllm serve Qwen/Qwen3-8B \ --served-model-name qwen3-8b \ --host 0.0.0.0 \ --port 8000 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192参数说明:
--served-model-name:对外暴露的模型名,之后调用 API 时用这个名字。--host:监听地址。只在本地测试可以填127.0.0.1;要开放给局域网或容器内使用,再考虑0.0.0.0,同时要做好访问限制。--port:端口,默认 8000。如果被占用,换一个。--gpu-memory-utilization:允许 vLLM 使用的显存比例上限。如果显卡上还跑着别的任务,建议调低。--max-model-len:最大上下文长度。显存不够时,优先调低这个值。--tensor-parallel-size:多卡场景,例如2表示用 2 张卡跑张量并行。
启动日志中出现类似Starting vLLM server、Uvicorn running on http://0.0.0.0:8000的信息,说明服务已经起来。此时打开浏览器访问http://127.0.0.1:8000/docs,可以看到 Swagger 风格的接口文档,这是验证服务是否正常的简单方法。
6.3 Docker Compose 部署模板
如果你更习惯容器化部署,这里给一个 docker-compose 模板。注意镜像名、版本号和挂载路径需要按实际项目调整:
services: vllm: image: vllm/vllm-openai:latest runtime: nvidia environment: - HUGGING_FACE_HUB_TOKEN=${HF_TOKEN} command: - "--model" - "Qwen/Qwen3-8B" - "--served-model-name" - "qwen3-8b" - "--port" - "8000" ports: - "8000:8000" volumes: - ./models:/root/.cache/huggingface启动:
docker compose up -d这个模板只演示基础结构,生产环境还需要根据显卡、权限、数据卷和日志收集做细化。
6.4 验证服务是否正常
先看模型列表:
curl http://127.0.0.1:8000/v1/models返回 JSON 里包含 model id,说明 API 服务已经正常暴露。
7. OpenAI 兼容 API 与 Python 调用实战
7.1 为什么是 OpenAI 兼容 API
接口兼容最大的价值是“迁移成本几乎为零”。你之前用openai库写的代码,只需要把base_url改成 vLLM 服务的地址,把api_key随便填一个占位值,就能切到本地模型。LangChain、Dify、FastGPT、CodeBuddy 这类的工具链,也大多支持这种接法。
7.2 Python 调用示例
先安装 OpenAI SDK:
pip install openai非流式调用:
from openai import OpenAI client = OpenAI( base_url="http://127.0.0.1:8000/v1", api_key="EMPTY" ) response = client.chat.completions.create( model="qwen3-8b", messages=[ {"role": "system", "content": "你是一个简洁的技术助手。"}, {"role": "user", "content": "请用两句话解释 vLLM 的 PagedAttention。"} ], temperature=0.7, max_tokens=512 ) print(response.choices[0].message.content)流式调用:
from openai import OpenAI client = OpenAI( base_url="http://127.0.0.1:8000/v1", api_key="EMPTY" ) stream = client.chat.completions.create( model="qwen3-8b", messages=[{"role": "user", "content": "写一篇 200 字左右的 vLLM 介绍。"}], stream=True ) for chunk in stream: delta = chunk.choices[0].delta.content if delta: print(delta, end="")如果运行后能正常输出内容,说明 vLLM 的 OpenAI 兼容 API 已经打通。接下来可以继续测并发和批量任务。
7.3 curl 调用示例
不带 SDK 的环境,直接用 curl 验证:
curl http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "qwen3-8b", "messages": [ {"role": "user", "content": "什么是连续批处理?"} ], "max_tokens": 256 }'返回的 JSON 中包含choices[0].message.content,就是模型生成的结果。
8. 批量任务与并发测试
vLLM 服务端是会自己批处理的,但前提是客户端要“同时”发多个请求。如果客户端还是一个一个串行调用,那连续批处理也帮不上忙。所以批量任务设计的关键是:用并发客户端去打接口。
8.1 并发测试脚本
import time from concurrent.futures import ThreadPoolExecutor from openai import OpenAI BASE_URL = "http://127.0.0.1:8000/v1" MODEL_NAME = "qwen3-8b" def send_request(idx): client = OpenAI(base_url=BASE_URL, api_key="EMPTY") start = time.time() resp = client.chat.completions.create( model=MODEL_NAME, messages=[ {"role": "user", "content": f"请只回复:收到第 {idx} 个请求。"} ], max_tokens=32, temperature=0 ) cost = time.time() - start content = resp.choices[0].message.content return idx, content, round(cost, 2) def main(): with ThreadPoolExecutor(max_workers=16) as pool: results = list(pool.map(send_request, range(16))) for idx, content, cost in results: print(f"request {idx}: {content} | cost {cost}s") if __name__ == "__main__": main()重点观察两块:16 个请求全部完成的总耗时,以及单请求耗时。如果单请求耗时会随着并发数上升而变长,说明 GPU 在排队,这是吞吐换延迟的正常现象;如果并发一高就报错,再去看服务端日志是不是显存不足或连接超时。
8.2 从文件批量生成
批量任务的通用模板:
import json import time from concurrent.futures import ThreadPoolExecutor from openai import OpenAI BASE_URL = "http://127.0.0.1:8000/v1" MODEL_NAME = "qwen3-8b" def process_item(item): client = OpenAI(base_url=BASE_URL, api_key="EMPTY") try: resp = client.chat.completions.create( model=MODEL_NAME, messages=[ {"role": "user", "content": item["prompt"]} ], max_tokens=item.get("max_tokens", 512) ) return {"id": item["id"], "output": resp.choices[0].message.content, "status": "ok"} except Exception as e: return {"id": item["id"], "error": str(e), "status": "failed"} if __name__ == "__main__": with open("tasks.jsonl", "r", encoding="utf-8") as f: tasks = [json.loads(line) for line in f] with ThreadPoolExecutor(max_workers=8) as pool: results = list(pool.map(process_item, tasks)) with open("results.jsonl", "w", encoding="utf-8") as f: for r in results: f.write(json.dumps(r, ensure_ascii=False) + "\n") failed = [r for r in results if r["status"] == "failed"] print(f"total={len(results)}, failed={len(failed)}")批量任务建议:
- 每个 item 记录 id 和原始 prompt,失败时能快速定位。
- 使用 JSONL 做输入和输出,方便断点续跑。
- 并发数从 4、8、16 逐级往上调,不要一上来就打满。
- 对失败任务做重试,但要加间隔和重试上限,避免把服务打挂。
8.3 关于“本地模型能不能联网”的说明
vLLM 只负责推理,不提供“联网能力”。模型本身是否联网,取决于调用方:如果你的 Agent 工具链里接了搜索 API、数据库、知识库,那模型可以通过工具调用获得外部信息。vLLM 这一层只是模型服务,不决定联网与否。
9. 资源占用与性能观察
9.1 怎么看显存占用
启动 vLLM 后,另开一个终端执行:
nvidia-smi -l 2这里能看到进程级的显存占用。如果显存不够,优先做三件事:
- 调低
--max-model-len,减少 KV Cache 预留量。 - 调低
--gpu-memory-utilization,给其他任务留空间。 - 换量化版本模型,或在部署时选择更小的模型。
更精确的观察方式是用--enable-metrics开启 Prometheus 指标,然后配合 Grafana 看请求级监控。这个配置适合生产环境。
9.2 关注哪些指标
重点关注两个延迟指标:
- TTFT(Time To First Token):从发出请求到收到第一个 token 的时间。首字慢通常是模型太大、max-model-len 过长或显存不足导致排队。
- TPOT(Time Per Output Token):后续每生成一个 token 的平均耗时。这个值越低越好,反映单 token 的生成速度。
vLLM 日志中会输出吞吐相关统计,比如 average throughput。批量任务时可以把这些日志保存下来,对比不同并发数下的整体吞吐。
9.3 降低显存占用的常见手段
- 使用量化模型,例如 FP8 或者 GPTQ/AWQ 量化版。
- 调低
--max-model-len。 - 减小
--gpu-memory-utilization,但要注意这也会减少 KV Cache 可用空间,降低并发容量。 - 多卡跑
--tensor-parallel-size 2,让模型权重和 KV Cache 分摊到多张卡上。
实际数字必须以你的模型版本和显卡实测为准,不同硬件、不同量化方式结果差异很大。
9.4 端口冲突与进程残留
如果启动时报端口被占用:
lsof -i :8000确认是残留进程还是其他服务占用,再决定杀进程或换端口。vLLM 如果是被 Ctrl+C 强制终止,可能出现进程残留,建议统一用进程管理器管理,比如 systemd 或 Docker。
10. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 服务启动不了,报 CUDA 相关错误 | 显卡驱动、CUDA、PyTorch 版本不匹配 | 检查nvidia-smi和python -c "import torch; print(torch.cuda.is_available())" | 按 vLLM 官方要求对齐 CUDA 与 PyTorch 版本 |
| 启动后页面打不开 | 服务未启动或端口被占用 | 查看终端日志,检查lsof -i | 更换端口或重启服务 |
| 显存不足 / OOM | 模型权重 + KV Cache 超过显存 | 观察nvidia-smi,看是启动阶段还是推理阶段 OOM | 调低--max-model-len、--gpu-memory-utilization,换量化版模型 |
| 并发一高就报连接超时 | 客户端并发数过大,或服务端显存不足 | 查看服务端日志,确认是否出现显存相关报错 | 降低客户端并发数,调大超时时间,优化模型显存配置 |
| 首字慢(TTFT 高) | 模型大、上下文长、显存不足导致排队 | 结合--enable-metrics观察各阶段耗时 | 调低--max-model-len,使用量化模型,检查是否有多余任务占显存 |
| 接口返回 non-JSON 或解析失败 | 请求参数格式不对,或模型名填错 | 先调用/v1/models确认模型 ID | 按实际served-model-name传参 |
| 批量任务运行到一半卡住 | 客户端异常退出或服务端 OOM | 检查 JSONL 输出和日志 | 任务记录 id,支持断点续跑,降低并发数 |
| 模型回答质量不稳定 | 采样参数、系统提示词或模型版本问题 | 对比不同 temperature、top_p 和提示词 | 固定随机种子,使用工程化的提示词模板 |
| Docker 里访问不到 GPU | 未安装 NVIDIA Container Toolkit 或不支持 runtime | 检查容器内nvidia-smi | 安装 NVIDIA Container Toolkit,配置 Docker runtime |
11. 最佳实践与使用建议
先给结论,再展开:
- 第一次启动用小参数、小模型,先把链路跑通。
- 保留一套最小可运行配置,改参数前先备份。
- 模型、输入、输出分目录管理。
- 批量任务必须加日志和失败重试。
- 接口服务要限制访问范围,不要裸奔到公网。
- 涉及人脸、声音、版权素材时,必须先完成授权确认。
- 发布或商用前,要做效果复核和合规检查。
实际操作中,我建议你按下面这套顺序来迭代。
第一,先跑通最小配置。模型用一个小尺寸版本,--max-model-len设置低一些,--gpu-memory-utilization也保守一点,能拿到第一个生成结果就行。
第二,逐步加压。用并发脚本从 4 并发开始测试,观察显存和耗时。如果单请求延迟明显上升,说明显卡已经满载,这时候再提高并发收益不大。
第三,处理好目录结构。模型权重、任务输入、结果输出、日志建议分目录存放,避免模型和任务文件混在一起。批量任务脚本尽量可重复执行,使用 JSONL 记录每个任务的状态,方便断点续跑。
第四,接口服务安全。vLLM 的/v1接口本身没有内置鉴权,默认也不限制来源。如果是内网工具使用,监听127.0.0.1最稳妥;如果需要对外开放,一定要在前面加一层网关或鉴权,不要直接把 8000 端口暴露到公网。
第五,合规边界。本地部署模型、批量生成内容、接入 Agent 工具链时,要注意数据隐私。涉及人脸照片、声音样本、版权文本时必须确认授权,尤其是做内容生产或商用投放之前,要复核模型输出是否涉及侵权或违规内容。
第六,生产环境考虑。如果服务需要长期运行,建议用 systemd 或 Docker Compose 管理进程,配置自动重启。监控方面打开--enable-metrics,配合 Prometheus 采集指标,可以在吞吐下降时快速定位是显存瓶颈还是请求排队问题。
如果你在局域网里有多台机器需要访问模型服务,可以把--host设为0.0.0.0,但一定要确认网络隔离和权限控制。vLLM 本身不限制调用方,这是方便也是风险,控制权在部署者手里。
12. 总结与下一步
vLLM 最值得尝试的点,不是它写起来多花哨,而是它把“显存管理”和“请求调度”这两件最影响吞吐的事做对了。PagedAttention 减少显存碎片,连续批处理压满 GPU 算力,OpenAI 兼容 API 又把接入成本降到最低。
建议你拿到环境后,先做三件事:
- 部署一个小尺寸模型,用
/v1/models验证服务正常。 - 写一个并发脚本,从 4 并发测到 16 并发,观察显存和首字延迟。
- 把日志和 Prometheus 指标配起来,确认服务长期运行的稳定性。
最容易踩的坑,集中在三处:CUDA 版本和 PyTorch 不匹配导致启动失败;显存不足导致的 OOM;以及客户端串行调用导致连续批处理发挥不出来。
后续可以继续验证的方向:把 vLLM 接入 LangChain、Dify 或 CodeBuddy 这类工具链,通过 OpenAI 兼容接口组合成完整的 Agent 服务;测试 Qwen3 的函数调用能力,看 tool-call-parser 怎么配;对比 vLLM 和 sglang 在同一批模型上的吞吐表现;或者在生产环境用 Docker Compose 加指标监控做一套完整的模型服务。先把这篇的流程跑通,再往这些方向迭代效率会高很多。