☰
vLLM推理框架实战:分页显存管理与连续批处理调优指南
2026/11/4 7:56:21 网站建设 项目流程

大模型推理这件事,真正上手跑起来和看论文完全是两码事。我见过太多人把模型权重下载下来,装完环境,一跑生成就卡在显存爆炸或者吞吐低得离谱上,最后得出“这框架不好用”的结论。其实问题往往出在对推理框架的工作方式缺乏基本认知——它不是一个黑盒,而是一套围绕显存管理、请求调度和计算并行做了大量取舍的工程系统。vLLM 就是这类系统里被讨论最多的一个,它解决的核心问题很具体:在有限的显卡上,把大模型的并发吞吐拉到一个可用的水平,同时让单条请求的延迟不至于失控。这篇内容适合两类人:一类是刚接触大模型部署、想把服务跑起来的新手,另一类是已经能跑通但吞吐上不去、想搞清楚内部机制的老手。我会从它为什么存在讲起,把分页显存管理、连续批处理这些关键机制拆开,再落到实际的安装、启动、参数调优和踩坑排查上,尽量让每一步都能直接照着做。

1. 先搞清楚vLLM到底在解决什么麻烦

在动手装任何东西之前,得先明白一个推理框架存在的意义。很多人以为推理就是“把模型加载进来,输入一段文本,输出一段文本”,逻辑上确实如此,但工程上完全不是这么回事。一个模型加载进显存之后,真正决定你能不能用的,是它在同时面对多个请求时怎么分配显存、怎么组织计算、怎么避免浪费。vLLM 的所有设计几乎都围绕这几个问题展开,理解了这个出发点,后面看参数和报错就不会一头雾水。

1.1 朴素推理的三个硬伤

先说不带任何优化的情况。假设你直接把模型权重读进显存,来一个请求就做一次前向计算,这套做法在小模型、单请求下没问题,但一旦并发上来就会暴露三个硬伤。第一个是显存碎片化:每个请求的输入长度不一样,为它预留的键值缓存(KV Cache)大小也不一样,如果按最大长度统一预留,短请求会浪费大量显存;如果按实际长度动态分配,反复申请释放又会产生碎片,最后明明总显存够,却找不到一块连续空间。第二个是批处理效率低:传统做法要等一批请求凑齐了才开始算,先到的请求只能干等,后到的请求可能拖慢整批。第三个是计算和显存访问的比例失衡:大模型生成阶段是逐 token 输出的,每一步都要读取巨大的权重矩阵,计算量相对很小,显存带宽成了瓶颈,如果批处理组织得不好,显卡的计算单元大量时间在空转。

这三个问题不是孤立的,它们互相纠缠。你为了解决碎片化去统一预留,结果浪费更多显存;你为了提升批处理效率去凑大批次,结果显存又不够。vLLM 的价值就在于它用一套统一的机制同时缓解了这三点,而不是打补丁式地各个击破。

1.2 分页显存管理:把操作系统那套思路搬过来

vLLM 最核心的设计叫 PagedAttention,名字里的“分页”直接借用了操作系统虚拟内存的概念。传统做法里,一个请求的 KV Cache 必须在显存里占一块连续空间,长度不确定就很难办。PagedAttention 把 KV Cache 切成固定大小的块(block),每个块存放固定数量 token 的键值,这些块在物理显存上不需要连续,通过一张映射表把逻辑上的序列位置对应到实际的物理块。这个思路的好处非常直接:显存按块分配,块的大小固定,申请释放都是整块操作,碎片问题基本消失;不同请求之间还可以共享块,比如多个请求有相同的系统提示词,这部分 KV 只存一份,省下来的显存相当可观。

我实际测过一个场景,同一套提示词模板被几十个请求复用,开启块共享之后显存占用下降得很明显,并发数能往上提一截。这里的关键参数是块大小,默认通常是 16 个 token 一块,块太小映射表开销大,块太大内部浪费多,16 这个值在多数场景下是个平衡点,除非你的序列长度分布特别极端,否则不建议乱改。

1.3 连续批处理:让请求像流水线一样进出

第二个关键机制是连续批处理(continuous batching)。传统批处理是静态的:凑齐一批,一起算完,再凑下一批。问题是生成任务的长短差异极大,一个请求可能输出 20 个 token 就结束,另一个要输出 2000 个,静态批处理里短请求结束后它占的位置就空着,直到整批算完才能释放,利用率很低。连续批处理的做法是每一步都重新检查当前有哪些请求还在进行,把新到的请求插进来,把已完成的请求踢出去,批次是动态流动的。这样显卡几乎每一轮都在满负荷计算,吞吐提升非常明显。

这个机制对使用者的直接影响是:你不需要自己去凑批次,框架会自动调度。但也带来一个副作用,就是单条请求的延迟可能因为不断有新请求插入而略微上升,因为它的计算被其他请求分摊了。这是吞吐和延迟之间的固有取舍,后面调参时会反复遇到这个矛盾。

2. 环境准备:别一上来就装最新版

环境这一步看着简单,实际上是新手翻车最集中的地方。我见过太多人直接 pip install 最新版,结果 CUDA 版本对不上、PyTorch 版本冲突、编译到一半报错。vLLM 对底层依赖比较敏感,尤其是它依赖特定版本的 PyTorch 和 CUDA 运行时,版本错配的报错往往很隐晦,排查起来非常费劲。所以这一步的核心原则是:先确认显卡驱动和 CUDA 版本,再选对应的 vLLM 版本,最后才装。

2.1 先摸清显卡和驱动的底

动手前先跑两条命令确认基础环境。用nvidia-smi看驱动版本和支持的最高 CUDA 版本,注意这里显示的是驱动支持的上限,不是你实际装的运行时版本。然后用nvcc --version看实际安装的 CUDA 工具链版本。这两个数字要匹配,如果 nvcc 没装或者版本和驱动差太远,后面编译扩展时会出问题。显存大小也要看清楚,这直接决定你能跑多大的模型、能开多大的并发。

检查项命令关注点
驱动与显卡nvidia-smi驱动版本、显存容量、CUDA 支持上限
CUDA 工具链nvcc --version实际运行时版本,需与驱动兼容
Python 版本python --version建议 3.9 到 3.11 之间
已装 PyTorchpython -c "import torch; print(torch.__version__, torch.version.cuda)"确认是否与目标 vLLM 匹配

这里有个经验:Python 3.12 在部分 vLLM 版本上会有依赖解析的坑,3.10 或 3.11 是最稳的区间。如果你用 conda,建议单独建一个干净环境,别和已有的深度学习环境混在一起,否则依赖冲突能让你怀疑人生。

2.2 版本匹配的取舍逻辑

为什么不建议无脑装最新版?因为 vLLM 的发布节奏很快,新版本往往跟进最新的 PyTorch 和 CUDA,而你的驱动可能还没跟上。更稳妥的做法是先看目标 vLLM 版本官方说明里要求的 PyTorch 和 CUDA 版本,反推你该装哪个。比如某个版本要求 CUDA 12.1 以上,而你的驱动只支持到 11.8,那就得选更早的 vLLM 版本,或者先升级驱动。升级驱动这件事要谨慎,生产环境里驱动升级可能影响其他任务,最好在测试机上先验证。

安装命令本身不复杂,用 pip 装预编译包最省事:

pip install vllm==<目标版本>

如果预编译包和你的环境不匹配,就得从源码编译,这时候编译工具链、CMake 版本、gcc 版本都会成为变量,耗时也长。我的建议是优先找匹配的预编译包,实在没有再考虑源码编译,而且源码编译前一定把 CUDA_HOME 环境变量指对,否则编译脚本找不到工具链。

2.3 装完先做一次最小验证

装完别急着上大模型,先用一个小模型验证整条链路通不通。选一个参数量很小的模型,加载起来快,出问题也容易定位。验证脚本大概长这样:

from vllm import LLM, SamplingParams llm = LLM(model="<小模型路径或名称>", max_model_len=512) params = SamplingParams(temperature=0.7, max_tokens=64) outputs = llm.generate(["你好,请介绍一下你自己"], params) for o in outputs: print(o.outputs[0].text)

这段代码能跑通,说明环境、显存分配、基本推理链路都没问题。如果这里就报错,问题基本集中在依赖或显存上,比直接上大模型排查起来轻松得多。我踩过的一个坑是模型路径写成了相对路径,在别的目录下运行就找不到,建议统一用绝对路径或者确认工作目录。

3. 把服务跑起来:启动参数背后的门道

环境通了之后,下一步是把 vLLM 作为服务启动,对外提供接口。这一步的参数配置直接决定了吞吐、延迟和显存占用,是真正体现经验的地方。很多人启动命令是从别处抄来的,参数含义不清楚,出了问题也不知道调哪个,结果就是服务能跑但性能很差。这一节把几个关键参数拆开讲清楚。

3.1 显存利用率不是越高越好

启动时最常见的参数是--gpu-memory-utilization,默认值通常是 0.9,意思是允许 vLLM 使用显卡显存的 90%。很多人觉得调到 0.95 甚至更高能多跑点并发,但这里有个陷阱:这个比例是预分配的,vLLM 会按这个比例去占显存用于 KV Cache,留太少给其他进程或框架自身开销,很容易在运行中触发显存不足。我一般建议从 0.85 到 0.9 起步,观察稳定后再微调。如果机器上还有别的进程在用显卡,这个值必须往下压。

另一个关键参数是--max-model-len,它限制单条请求的最大序列长度。这个值设得越大,单条请求能处理的上下文越长,但每条请求可能占用的 KV Cache 上限也越大,能同时并发的请求数就越少。这里要做的是根据实际业务场景定:如果你的应用输入输出都很短,就没必要设成模型支持的最大值,白白浪费显存。我见过有人不管三七二十一设成 32K,结果并发数掉到个位数,还以为是框架性能不行。

3.2 张量并行:多卡不是简单叠加

模型大到单卡放不下时,就要用张量并行(tensor parallelism),通过--tensor-parallel-size指定用几张卡。这里要理解它的原理:张量并行是把模型里的大矩阵按维度切分到多张卡上,每张卡算一部分,然后通过通信把结果汇总。所以卡数增加带来的不是线性的显存和算力叠加,通信开销会随卡数上升。一般建议张量并行度不要超过单机内的 GPU 数量,跨机通信开销太大,得不偿失。

还有一个容易忽略的点:张量并行要求卡数能整除模型的注意力头数等结构参数,选卡数时要注意兼容性。比如模型有 32 个注意力头,你用 3 张卡就除不尽,启动会报错。这个报错信息有时候不够直观,新手容易卡在这里。

3.3 一个可参考的启动命令

把上面这些串起来,一个相对稳妥的启动命令大概是这样:

python -m vllm.entrypoints.openai.api_server \ --model <模型路径> \ --tensor-parallel-size 2 \ --gpu-memory-utilization 0.88 \ --max-model-len 8192 \ --served-model-name my-model \ --port 8000

这里用 OpenAI 兼容的接口启动,好处是客户端代码几乎不用改,直接用现成的 SDK 就能调。--served-model-name指定对外暴露的模型名,客户端请求时用这个名字。端口按需改,注意别和已有服务冲突。启动后日志里会打印显存分配情况和最大并发估计,这些信息很有价值,能帮你判断参数是否合理。

提示:启动日志里如果出现关于 KV Cache 块数量的信息,记下来,这是判断并发能力的重要依据。块数乘以块大小大致就是能同时容纳的 token 总量。

4. 吞吐上不去?按这个顺序排查

服务跑起来只是开始,真正让人头疼的是性能不达预期。吞吐低、延迟高、显存莫名爆掉,这些问题背后往往有明确的成因,关键是按合理的顺序排查,而不是乱调参数。我总结了一套从外到内的排查链路,基本能覆盖大部分场景。

4.1 先看请求特征,再看框架参数

很多人一上来就调框架参数,但问题可能出在请求本身。先统计你的请求输入长度和输出长度分布:如果输入普遍很长而输出很短,瓶颈可能在预填充阶段;如果输出很长,瓶颈在逐 token 生成阶段,KV Cache 的读写压力大。这两种情况的优化方向完全不同。预填充阶段是计算密集的,可以靠更大的批次提升效率;生成阶段是显存带宽密集的,批次太大反而可能因为显存访问冲突而变慢。

我遇到过一个案例,业务方抱怨吞吐低,一看请求分布,大量请求的输出长度只有几个 token,但输入提示词特别长。这种情况下预填充占了绝大部分时间,优化重点应该是提示词缓存复用,而不是调大批次。所以排查第一步永远是拿真实请求数据做统计,别凭感觉。

4.2 显存不足的几种典型表现

显存问题有好几种表现,要区分开。第一种是启动时就报显存不足,这通常是--gpu-memory-utilization设太高或者--max-model-len设太大,模型权重加预分配的 KV Cache 超过了物理显存。第二种是运行一段时间后爆显存,往往是并发请求累积,KV Cache 块被占满,这时候要么降并发,要么缩短最大长度。第三种是显存看起来够但分配失败,可能是碎片或者别的进程占用,用nvidia-smi看实际占用和 vLLM 日志里的预分配对不上,就要怀疑外部占用。

现象可能原因处理方向
启动即显存不足利用率或最大长度设太高下调利用率、缩短最大长度
运行中逐渐爆显存并发累积占满 KV Cache限制并发数、启用请求排队
显存够但分配失败外部进程占用或碎片检查其他进程、重启服务

4.3 延迟和吞吐的取舍怎么调

吞吐和延迟是一对矛盾,调参本质上是在两者之间找平衡点。想提升吞吐,可以适当增大批次上限、提高显存利用率、开启连续批处理(默认就是开的);想降低单请求延迟,就要限制同时处理的请求数,避免单条请求的计算被过度分摊。这里没有万能参数,得根据业务对延迟的容忍度来定。如果业务是离线批量处理,那就往吞吐倾斜;如果是在线交互,延迟优先,并发就得控制。

还有一个实用技巧是观察服务端的队列长度和每步计算耗时。如果队列一直很长但每步耗时稳定,说明计算资源是瓶颈,加卡或者换更大显存的卡更有效;如果每步耗时波动很大,可能是调度或显存访问的问题,需要看日志里有没有异常。这些指标比单纯看总吞吐更能定位问题。

5. 几个容易踩的坑和我的处理习惯

除了上面这些系统性的内容,还有一些零散但高频的坑,单独拎出来说,都是实际动手时容易撞上的。

5.1 模型格式和分词器的匹配问题

模型权重和分词器(tokenizer)必须匹配,这个道理简单,但实际操作中经常出问题。比如权重是从一个地方下的,分词器配置是从另一个地方拿的,版本不一致,结果就是生成的文本乱码或者直接报错。我的习惯是权重和分词器从同一个来源获取,加载后先做一次简单的编码解码往返测试,确认 tokenizer 工作正常再上服务。另外有些模型需要信任远程代码(trust_remote_code),这个开关要谨慎,确认来源可靠再开。

5.2 长上下文场景的额外开销

处理长上下文时,除了 KV Cache 占用大,还有一个容易被忽略的开销是注意力计算本身。序列越长,注意力计算的复杂度增长越快,这部分开销不是靠分页显存管理能解决的。所以长上下文场景下,光调显存参数不够,还要考虑是否启用一些针对长序列的优化,或者干脆在业务层做输入截断和分段处理。我一般会先测一下不同长度下的单请求延迟曲线,找到延迟开始陡增的拐点,据此设定业务上的长度上限。

5.3 日志和监控别省

最后说一个习惯问题:很多人服务跑起来就不管了,出问题才去看日志。vLLM 的日志信息其实很丰富,启动时的显存分配、运行中的请求调度、异常堆栈都在里面。我习惯在服务启动后先盯一段时间日志,确认没有反复出现的警告,再把关键指标接到监控里。尤其是显存使用率和请求队列长度这两个指标,能提前预警大部分问题。等到用户反馈卡顿再去查,往往已经积累了一堆请求,排查起来更麻烦。

这套东西说到底,vLLM 的门槛不在安装,而在理解它为什么这么设计。把分页显存管理和连续批处理的逻辑吃透,参数就不再是死记硬背的数字,而是有依据的调节旋钮。我自己的体会是,每次遇到性能问题,先回到请求特征和显存占用这两个基本事实,再去看框架参数,绝大多数问题都能顺着这条线找到答案。

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

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

立即咨询