vLLM 完整上手:从显存优化到压测部署的实战指南
2026/9/12 9:47:05 网站建设 项目流程

很多人第一次认真看 vLLM,都是在本地把模型跑起来之后被现实教育了一顿:单条请求还行,并发一上来吞吐直接塌;显存明明还剩几个 G 却提示放不下;上下文一长就要排队等十几秒。我当初也是被这些问题逼着从别的推理方案迁到 vLLM 的,前后折腾了差不多两个礼拜,踩的坑足够写好几篇。这篇是快速上手系列的第四篇,不打算再重复"vLLM 是什么"这种入门话术,而是把从环境准备、安装选型、启动参数、服务化调用、压测到排错的完整链路一次讲透,让你看完能自己把模型跑起来,并且清楚每一步为什么这么干。

这篇适合三类人:刚接手 GPU 服务器、准备把模型做成服务的后端同学;被并发和显存问题折腾过、想换推理引擎的算法同学;还有只是想在本机快速验证一个开源模型效果的开发者。不需要你提前读过 vLLM 源码,但至少要会基本的 Linux 命令行和 Python 环境操作。

1. 先把 vLLM 优化的那点事说透

动手之前花十分钟理解 vLLM 在优化什么,后面看参数和报错都会顺很多。它解决的核心问题就两个:显存怎么用得更省,请求怎么排得更满。这两件事听起来平淡,但恰恰是决定一个推理服务能不能扛住真实流量的关键。

1.1 KV Cache 为什么会把显存吃成筛子

大模型生成文本是逐 token 吐出来的。每生成一个 token,都要用到前面所有 token 的 Key 和 Value 向量,为了不重复计算,框架会把这些向量缓存下来,这就是 KV Cache。问题在于,早期实现习惯给每个请求预分配一整块连续显存,按最大长度来算。你请求实际只用了 500 个 token,但显存里按 8192 长度预留的那块地已经占住了,剩下的空间别的请求进不来。

这和早年电脑内存管理一个毛病:程序申请内存时按最大需求预留,实际用不到那么多,但别人也用不了这块地,结果就是显存利用率可能只有三成,剩下七成全是空洞。并发一多,显存很快见底,只能排队甚至拒绝请求。

1.2 PagedAttention 和连续批处理到底做了什么

vLLM 的两个招牌机制正好对应上面两个问题。PagedAttention 借鉴的是操作系统虚拟内存分页的思路,把 KV Cache 切成固定大小的块(block),逻辑上连续、物理上可以散落。一个请求用完的块会立刻归还给公共池子,下一个请求马上能接手。这样一来,显存碎片基本被消掉,实测里同样的卡能塞下的并发数往往能翻两三倍。

连续批处理解决的则是调度问题。传统批处理要等一个批次里所有请求都生成完,才能开始下一批。假如批次里有条请求要生成 2000 个 token,其他请求早就生成完了,也只能干等着占坑。连续批处理改成按迭代调度:哪个请求生成完了就立刻退出批次,队列里等待的请求马上补进来。GPU 在 decode 阶段的利用率因此被拉到很高。

顺带说一句,prefill(处理输入提示)和 decode(逐 token 生成)两个阶段的资源特性完全不同。前者是算力密集,后者是访存密集。vLLM 里有个 chunked prefill 的选项,就是把超长的 prefill 拆成小块,和 decode 混在一起跑,避免长提示把 decode 请求饿死。这个参数在长文本场景下很有用,后面启动参数那节会细说。

1.3 上手前先想清楚这三件事

第一,你的模型架构 vLLM 支持吗。主流架构(各类 Transformer 解码器、部分多模态结构、嵌入模型)基本都在支持列表里,但一些魔改过的模型或者自研结构可能要自己写适配,别上来就硬跑。

第二,显存够不够。模型权重的显存占用是可以提前估的:参数量乘以每个参数的字节数。bf16 精度下,7B 模型大约 14 GB,13B 大约 26 GB,70B 大约 140 GB。这只是权重,还要留出 KV Cache 和激活值的空间。如果你只有一张 24 GB 的卡,老老实实跑 7B 级别的模型,别硬上 13B。

第三,你打算怎么用。只是本地验证,还是对外提供接口。前者可以省掉服务化的步骤,后者要提前规划端口、鉴权、日志和监控。这两种用法在启动命令上差别不大,但在参数调优上完全是两个方向。

2. 环境准备与安装:三条路线怎么选

安装这一步看起来简单,实际上是最容易卡人的地方。vLLM 对 CUDA 版本、PyTorch 版本、Python 版本都有要求,选错路线能让你在依赖地狱里泡一整天。

2.1 硬件、驱动与系统前提

Linux 是首选,Ubuntu 20.04 和 22.04 用得最多。显卡方面,N 卡从 Volta 架构之后基本都能跑,但要注意显存至少得能放下模型。驱动版本建议 525 以上,配 CUDA 12.x。查驱动很简单:

nvidia-smi

输出的右上角会显示当前驱动支持的 CUDA 版本。注意这里显示的是驱动能支持的最高版本,不是你已经安装的版本。真正的运行时 CUDA 版本可以用nvcc --version查,但如果走 pip 安装路线,nvcc 其实不一定需要,因为 PyTorch 会自带编译好的 CUDA 运行时。

Python 版本建议 3.10 或 3.11。3.12 有时候会遇到某些依赖没有预编译轮子,需要现场编译,很耗时。用 conda 或者 venv 建一个干净环境,别在系统 Python 上直接装,这是我踩过的第一个坑。

提示:环境里如果之前装过别的推理框架,务必新建独立环境。不同框架对 PyTorch 和 CUDA 的版本要求经常冲突,混装的结果往往是两个都跑不起来。

2.2 pip 安装、源码编译、容器镜像三条路线对比

路线适用场景优点缺点
pip 安装快速验证、版本不挑一条命令,几分钟搞定只能装发布版本,无法改源码
源码编译需要改代码、用最新特性灵活性最高编译耗时,依赖问题多
容器镜像生产部署、环境隔离环境完全一致,省心镜像体积大,定制略麻烦

新手强烈建议从 pip 开始。命令就一行:

pip install vllm

它会自动拉取匹配的 PyTorch 和 CUDA 相关依赖。装完先验证一下:

python -c "import vllm; print(vllm.__version__)"

能正常打印版本号,说明基础环境没问题。这时候千万别急着去跑模型,先把版本号记下来,后面排查问题、对照文档都要用到它。vLLM 迭代速度很快,不同版本之间的参数名和默认值都可能有变化,遇到对不上的情况,第一反应应该是确认版本,而不是怀疑自己命令写错了。

2.3 源码构建到底要多久,怎么少踩坑

需要改源码或者用未发布特性时才走这条路。先克隆仓库,然后:

pip install -e .

构建耗时跟机器配置强相关。我实测过的几台机器:16 核 CPU 加机械硬盘的云主机,从零编译带 CUDA 扩展,差不多花了 40 分钟;32 核加 NVMe 的机器,15 到 20 分钟;如果 CPU 核数少于 8,做好等一个小时的准备。

想加速有几个办法。一是设置并行编译线程数,比如MAX_JOBS=16,让编译用满 CPU。二是把torch之类的重依赖提前用 pip 装好,避免从源码编译。三是务必挂载 NVMe 磁盘,机械盘上的编译速度能慢一倍以上,因为编译过程有大量小文件读写。

常见的失败原因:显存被其他进程占着导致编译时的 CUDA 检测失败;nvcc版本和 PyTorch 要求的 CUDA 版本不一致;磁盘空间不足,源码编译过程中临时文件能吃掉十几 GB。

2.4 Windows 环境下能做什么

坦白讲,vLLM 的官方支持以 Linux 为主。Windows 上直接 pip 安装大概率会卡在依赖上,尤其是需要编译 CUDA 扩展的部分。可行的替代路径有三条:用 WSL2 装 Ubuntu 子系统,体验和原生 Linux 基本一致,显卡也能直通;用 Docker Desktop 拉官方镜像;或者干脆放弃 Windows,在局域网里找台 Linux 机器跑服务,Windows 上只做客户端调用。

如果你只是想在 Windows 桌面上快速体验模型,用本地的桌面推理工具更合适,没必要跟 vLLM 较劲。vLLM 的定位是服务端高吞吐,不是桌面端一键启动,这个定位差异决定了它对环境的要求更高。

3. 启动第一个模型:命令解析与执行顺序

环境好了,接下来启动模型。这一步是整个教程的核心,我把命令拆开讲,顺便把启动时的执行顺序捋一遍,理解了顺序,出问题时你就知道该看哪段日志。

3.1 最简启动命令逐段拆解

vllm serve Qwen/Qwen2.5-7B-Instruct \ --host 0.0.0.0 \ --port 8000 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192

vllm serve是服务化入口,启动后会拉起一个兼容 OpenAI 协议的 HTTP 服务。模型名可以直接写 HuggingFace 上的仓库 ID,框架会自动下载;如果已经下载到本地,写本地路径也行,能省掉下载时间。

--host 0.0.0.0表示监听所有网卡,方便局域网其他机器访问。安全考量上,如果这台机器暴露在不可信网络里,务必加上鉴权或者用防火墙限制来源,别裸着端口。--gpu-memory-utilization 0.9表示允许 vLLM 使用 90% 的显存,剩下的留给系统和其他进程。这个值不是越高越好,调到 0.95 以上往往会因为驱动自身的开销导致启动失败。

--max-model-len是单条请求的最大长度,包括输入和输出。这个值直接决定 KV Cache 单块的大小,调得越大,能并发处理的请求数越少。

3.2 vLLM 启动时的执行文件顺序

理解启动顺序,遇到卡住的情况能快速定位。整体路径大致是这样的:

  1. 命令行入口解析参数,把EngineArgs收集起来。
  2. 初始化引擎,创建LLMEngine(离线)或AsyncLLMEngine(在线服务)。
  3. 加载模型权重,按分片策略分配到各张卡上。
  4. 探测显存余量,计算能开多少 KV Cache 块,然后预分配。
  5. 启动 worker 进程和调度器,准备好请求队列。
  6. 启动 API server,监听端口,开始接收请求。

日志上你会看到明显的分段。刚启动时打印的是配置信息,然后进入权重加载,这一步如果模型是第一次下载会明显卡住,能看到进度条。权重加载完之后会打印一段 KV Cache 的空间统计,最后出现监听端口的提示,这时候服务才算真正就绪。

很多人误以为看到下载进度条就是快好了,实际上后面还有显存探测和缓存分配两个阶段。如果模型权重加载完卡住很久没动静,八成是在做显存分配或者编译 CUDA 图。

3.3 六个必须搞懂的启动参数

参数作用常见取值调优建议
--gpu-memory-utilization显存使用上限比例0.8 到 0.92留足余量,别贴满
--max-model-len单请求最大长度按业务定越短并发越高
--tensor-parallel-size张量并行卡数1、2、4、8等于使用的卡数
--max-num-seqs单批最大请求数默认即可显存紧张时调小
--dtype权重精度auto、bfloat16卡支持就用 bf16
--enable-chunked-prefill拆分长提示处理长文本场景开需配合调参

重点说下显存估算。拿一张 24 GB 的卡跑 7B 的 bf16 模型举例:权重占约 14 GB,--gpu-memory-utilization 0.9意味着可用 21.6 GB,减去权重还剩 7.6 GB 左右,再扣掉激活值和 CUDA 图的开销,实际能给 KV Cache 的大概 6 GB 出头。单 token 的 KV Cache 占用可以这样估:

2(K 和 V) × 层数 × KV 头数 × 头维度 × 精度字节数

以 32 层、8 个 KV 头、头维度 128 的模型为例,bf16 下每 token 约 128 KB。6 GB 大约能放下 4.7 万个 token 的缓存。这个数字决定了你同时能服务多少请求、每条能开多长上下文,发布会前一定要算一遍,别等上线了才发现并发上不去。

3.4 多卡张量并行与显存切分

模型放不下单卡时,加--tensor-parallel-size。4 张卡就写 4:

vllm serve Qwen/Qwen2.5-72B-Instruct \ --tensor-parallel-size 4 \ --gpu-memory-utilization 0.9

张量并行是把每一层的权重按维度切碎,分散到不同卡上,计算时通过通信把结果拼起来。它的好处是每张卡只需要放一部分权重,坏处是卡间通信频繁,对带宽要求高。所以张量并行数最好选能被注意力头数整除的值,通常是 2 的幂次:1、2、4、8。

多卡启动时你会看到一条熟悉的日志,类似vllm is using nccl==某个版本号。这说明框架在用 NCCL 做卡间通信,走的是 pynccl 这层封装。单卡启动时不会打印这条,所以第一次看到不用慌,它是正常信息。真正需要警惕的是通信超时或者初始化失败,那种情况通常和卡间拓扑、驱动版本相关,我在第七节会展开。

4. 服务化与压测:把接口跑通再谈性能

模型起来了,接口能调通只算完成一半。你得知道它在你的业务压力下能扛多少,这就要靠压测。

4.1 OpenAI 兼容接口启动与调用

vLLM 服务默认就提供兼容 OpenAI 的接口,路径是/v1/chat/completions/v1/completions。用官方的 Python 客户端直接调:

from openai import OpenAI client = OpenAI( base_url="http://localhost:8000/v1", api_key="EMPTY" ) resp = client.chat.completions.create( model="Qwen/Qwen2.5-7B-Instruct", messages=[{"role": "user", "content": "用一句话解释什么是张量并行"}], temperature=0.7, max_tokens=256 ) print(resp.choices[0].message.content)

api_key这里随便填,因为本地服务默认不校验。如果要对局域网开放,务必配置鉴权参数,把接口保护起来。这一步很多人在内网测试时省掉了,结果服务器一暴露就出事。

4.2 vllm bench serve 压测实操

vLLM 自带压测工具,不用另外写脚本。命令形态是这样:

vllm bench serve \ --backend openai-chat \ --base-url http://localhost:8000 \ --model Qwen/Qwen2.5-7B-Instruct \ --dataset-name random \ --num-prompts 200 \ --request-rate 10

--num-prompts是总请求数,--request-rate是每秒发起的请求数。想测极限吞吐就把请求速率调高,观察服务什么时候开始出现排队;想测不同压力下的延迟表现,可以做一组梯度测试,比如从每秒 2 个请求逐步加到 20 个,记录每档的延迟指标。

压测前有几个注意点。一是压测客户端的机器别和服务跑在同一台,否则抢资源,数据不准。二是先跑一轮短时间的预热,让 CUDA 图编译和缓存全部就位,再开始正式计数。三是记录的时候把输入输出长度固定住,不然结果没法横向对比。

4.3 TTFT、TPOT、吞吐量怎么读

压测结果里最关键的是三个指标:

指标含义反映的问题优化方向
TTFT首 token 延迟输入处理速度调小 batch、开 chunked prefill
TPOT每 token 输出延迟逐字生成速度检查显存带宽和 batch 大小
总吞吐每秒输出 token 数整体服务能力提高并发、调大 batch

TTFT 高通常说明 prefill 阶段排队严重,尤其是长输入场景。TPOT 高则多半是 decode 阶段 batch 太大导致显存带宽打满。这两个指标经常是此消彼长的关系:为了吞吐把 batch 调大,TPOT 就会上去;为了延迟把 batch 调小,吞吐又下来。所以调优的本质是在你的业务能接受的延迟范围内,把吞吐推到最高,而不是追求单项指标极值。

5. 落地场景:用 vLLM 部署 BGE-M3 嵌入模型

很多人以为 vLLM 只能跑生成模型,其实较新版本已经支持嵌入模型和多模态模型,这一节专门说嵌入模型这条线。

5.1 生成模型与嵌入模型启动差异

生成模型和嵌入模型的输出结构完全不同,前者是逐 token 生成的文本流,后者是固定维度的向量。所以启动时要用--task参数区分:

vllm serve BAAI/bge-m3 \ --task embed \ --port 8001 \ --gpu-memory-utilization 0.6

--task embed告诉框架按嵌入模式初始化,不再分配生成用的缓存结构。注意显存利用率可以调低一些,嵌入模型通常不需要那么大的缓存空间。

版本兼容性要留意。嵌入支持是在特定版本之后才完善的,如果启动时报参数不认识,先确认自己的版本号。查版本用前面说的那条命令,别凭记忆猜。较新的版本例如 0.28.0 这类,对嵌入和多模态的支持已经比较成熟,但每次大版本更新都可能调整行为,升级前建议先在测试环境跑一遍。

5.2 BGE-M3 部署实测与参数设置

BGE-M3 是个多语言嵌入模型,支持稠密、稀疏和混合多种检索方式,在中文检索场景里用得很多。部署步骤和生成模型基本一致,拉起来之后调用:

from openai import OpenAI client = OpenAI(base_url="http://localhost:8001/v1", api_key="EMPTY") resp = client.embeddings.create( model="BAAI/bge-m3", input=["今天天气怎么样", "外面下雨了"] ) print(len(resp.data[0].embedding))

实测下来,单卡跑 BGE-M3 的推理速度很快,瓶颈通常在文本预处理和网络传输上。如果你的检索服务 QPS 要求高,可以把--max-num-seqs调大一些,配合批处理请求能显著提高吞吐。

5.3 接口返回结构与业务对接

返回的是标准的向量列表,每条输入对应一个embedding字段。对接向量数据库时要注意两点:一是维度必须和数据库建表时一致,改模型等于重建索引;二是归一化方式要和当初建库时保持一致,有的检索方案要求向量做了 L2 归一化,用余弦相似度计算,有的直接用内积。这两点搞混了,检索结果会莫名其妙变差,而且很难查出来原因。

注意:换嵌入模型是件大事,不是重启服务那么简单。新旧模型的向量空间不兼容,混用会导致检索结果完全错乱。上线前一定要准备好重建全部索引的方案。

6. 工具选型:vLLM 和其他方案怎么选

市面上推理方案不少,知道各自适合什么场景,能帮你少走弯路。

6.1 和 SGLang 的差异

SGLang 和 vLLM 都属于高吞吐服务框架,定位有重叠。SGLang 在结构化输出、复杂提示编排方面有自己的优势,它的前端 DSL 对多轮对话和约束解码支持得比较顺手;vLLM 的生态更成熟,模型支持列表更长,工具链(压测、量化、多模态)更完整。

选择上我一般这么看:如果你的业务里大量涉及严格的 JSON 输出、复杂的提示模板组合,SGLang 值得试;如果是通用的大模型服务、模型种类杂、团队更熟悉标准接口,vLLM 更稳。两者都支持 OpenAI 协议,理论上可以互切,但线上的 KV Cache 管理和调度策略不一样,性能表现会有差异,最好用自己的真实流量各测一轮再定。

6.2 和桌面端推理工具的差别

本地桌面推理工具主打开箱即用,图形界面,点点鼠标就能加载模型,适合个人开发者快速体验和做演示。但它们的设计目标是单机、少量用户,并发能力有限,显存管理也没针对多请求优化。

vLLM 反过来,没有图形界面,配置都在命令行和参数里,学习曲线更陡,但换来的是高并发下的吞吐优势和更精细的资源控制。选哪个取决于你的使用场景:自己电脑上试试效果,桌面工具够了;要搭一个多人访问的服务,还是得用 vLLM。

6.3 什么情况下不该用 vLLM

场景一,你只需要偶尔跑一两次推理,没有并发需求。这种情况下装 vLLM 的环境成本不划算,用更轻量的方案更省事。

场景二,你的模型架构比较特殊,vLLM 不支持,而你又没有精力去改适配代码。硬上只会浪费时间。

场景三,你的硬件是消费级显卡并且显存很小。vLLM 虽然在显存利用上做得不错,但基础开销摆在那里,8 GB 以下的卡跑起来会很勉强。

场景四,你需要的是低延迟的单条交互,比如语音助手这种即时响应场景。vLLM 的强项是吞吐,不是极限低延迟,这种场景可以考虑其他方案。

7. 常见问题与排查技巧实录

这部分是我这两年攒下来的实战记录,基本覆盖了新手会遇到的绝大多数问题。

7.1 显存不足与 OOM 排查顺序

报显存不足时,别急着换卡,按这个顺序查:

第一,看是不是别的进程占着显存。nvidia-smi一下,如果发现残留进程,先清掉。有时候上一次启动的服务没退干净,显存还挂着,新进程自然起不来。

第二,调低--gpu-memory-utilization。从 0.9 降到 0.85 甚至 0.8,试试能不能起来。显存这东西,留有余地比榨干更稳。

第三,缩小--max-model-len。这是最立竿见影的一招,长度从 8192 降到 4096,KV Cache 需求直接减半。

第四,量化。用 AWQ 或 GPTQ 的量化版本,权重显存能降到原来的四分之一左右,代价是精度略有损失。对多数业务场景来说,这个损失可以接受。

第五,加卡,用张量并行把权重摊开。

7.2 NCCL 报错与多卡通信问题

多卡启动时,通信相关的报错最让人头疼。常见的有卡间通信超时、初始化失败、版本不匹配。排查思路:

先确认所有卡都能被正常识别,nvidia-smi -L列出全部卡,数量对不上说明有卡掉了或者驱动有问题。然后确认卡的拓扑,nvidia-smi topo -m看卡间是走 NVLink 还是 PCIe,走 PCIe 的情况下张量并行的效率会低不少,并行数不宜太大。

再就是版本问题。多卡通信依赖 NCCL,而 NCCL 版本又是跟着 CUDA 和 PyTorch 走的。日志里会打印当前用的版本,遇到通信异常先把这个版本记下来,去框架的 issue 里搜一下有没有已知问题。升级或者降级 NCCL 通常能解决一部分诡异报错。

7.3 版本与依赖冲突

这一类问题的典型症状是:命令照着文档写,参数全对,但就是报参数不存在或者模块导入失败。原因几乎都是版本对不上,vLLM 更新很快,网上教程的版本和你本地的可能差了好几个小版本。

处理办法是养成看版本的习惯。启动前先确认框架版本、PyTorch 版本、CUDA 版本三个值,然后去对应版本的文档里查参数。别拿旧教程硬套新版本,也别拿新文档配旧版本。依赖冲突则优先用干净虚拟环境解决,能省掉大量排查时间。

7.4 常见问题速查表

现象可能原因处理方式
启动即报显存不足余量不够或残留进程调低利用率、清进程
加载模型卡住不动网络慢或磁盘慢检查下载源、换本地路径
服务起来了但请求超时请求长度超出上限调大 max-model-len
多卡启动报通信错误拓扑或 NCCL 版本问题查拓扑、对齐版本
嵌入接口报参数错误版本不支持 task 参数升级或确认版本
吞吐远低于预期请求速率不足或 batch 太小调大并发、看压测曲线
首 token 延迟很高长提示阻塞开 chunked prefill

8. 我在实际部署中总结的几条经验

第一条,永远先跑通再调优。我见过太多人一上手就研究各种调度参数,结果环境都没装利索。按顺序来:环境装好、模型单卡跑通、接口调通、压测有基线,然后才谈优化。跳步的结果是出了问题不知道是哪一层的问题。

第二条,把每次启动的参数记下来。我习惯在项目里放一个serve.sh,把最终用的启动命令固化进去,包括版本号和参数。换机器、重装环境时直接跑脚本,能省掉大量回忆和试错的时间。同时留一份参数变更记录,方便回溯为什么某个值调成了现在这样。

第三条,压测数据要留档。每次改参数之后跑一轮压测,把 TTFT、TPOT、吞吐三个值和对应参数一起记在表格里。调优的过程本质上是在做对比实验,没有基线数据的话,你根本判断不出改动到底是变好了还是变差了。

最后分享一个我踩过的坑:日志里的警告不要一概忽略。有一类警告是关于显存利用率和缓存分配的,看着不起眼,但它往往在提示你当前配置已经接近极限,流量再涨一点就会出问题。上线前把日志从头到尾读一遍,比出事之后翻日志要省事得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询