☰
vLLM v0.29 Online Serving全解析:连续批处理、显存调度与生产部署
2026/10/10 4:37:59 网站建设 项目流程

最近在梳理 vLLM v0.29 的服务化链路,把 Online Serving 这部分源码、配置和实际部署从头到尾过了一遍。这篇就是我的导读笔记,记录的是 2026-09-22 这一版代码给我的印象。vLLM 的 Online Serving 简单说就是把大模型包装成标准的 OpenAI 兼容 HTTP 服务,让客户端发一个POST /v1/chat/completions就能拿到生成结果,吞吐和延迟都远比硬套 Transformers pipeline 靠谱。适合正在做模型上线、内部推理平台,或者想搞懂连续批处理和显存调度的同学参考。我会把版本能力、请求生命周期、DeepSeek 和 Qwen 系模型部署过程、以及 install 阶段最容易被 torch 坑到的问题都串起来讲。

1. vLLM v0.29 里 Online Serving 到底改了些什么

1.1 版本定位与前向兼容思考

vLLM 的版本号走得很快,v0.29 在我看更像一个“服务化功能收拢”的版本,而不是一次性推倒重来的大版本。早期 vLLM 更多被人拿来跑离线批量推理,也就是一条命令吃一个数据集、交出所有生成结果。但从 v0.25 之后,vllm.entrypoints.openai.api_server逐步变成默认入口,到了 v0.29,Online Serving 相关模块的职责已经分得很清楚:api_server管 HTTP 协议,LLMEngine管请求调度,ModelRunner管单卡或张量并行下的模型前向。

我读源码的习惯是先看vllm/engine/和vllm/entrypoints/这两个目录,再看vllm/config/model.py里的配置项。v0.29 里很多功能不是新概念,但实现细节收敛了。比如显存分配从“整卡预占”逐步细化为“按 page 块预占”,调度器的 running、waiting、swapped 队列也接口化得更干净。如果你是从 v0.23 之类老版本直接跳过来的,最需要适应的不是模型加载方式,而是“请求生命周期”这个概念——离线推理是批量喂数据,在线服务却是一个一个请求不断进入引擎,调度器必须实时决定谁先执行、谁换入、谁换出。

前向兼容方面,v0.29 延续了 vLLM 一贯的做法:老模型权重不需要额外转换,HuggingFace 格式直接加载。但要注意,模型配置里的max_model_len如果写得太高,会直接影响 KV cache 的可用 page 数,导致并发能力下降。这个不是 bug,是显存分配的基本逻辑,后面实操部分我会再展开。

1.2 Online Serving 在 v0.29 的核心能力清单

对照源码里的 feature flag 和配置项,我把 v0.29 Online Serving 里值得关注的能力梳理成一张表:

能力配置/入口我的理解
Continuous Batching默认启用请求不再等整批结束,step 级调度,一个 token 一个 token 推进
PagedAttention默认启用KV cache 按 16KB 左右 page 分配,显存利用率比静态 cache 高很多
Prefix Caching--enable-prefix-caching相同 prompt 前缀的 KV 块复用,多轮对话和 system prompt 场景收益明显
Chunked Prefill--enable-chunked-prefill长 prompt 拆成块,避免 prefill 阶段长时间独占 GPU
Structured Output--response-format/ guided decoding让输出匹配 JSON Schema、正则等格式
LoRA 服务化--enable-lora多 LoRA adapter 动态加载,不用重启服务
多卡并行tensor-parallel-size单机多卡把模型切到多张 GPU 上并行

这些能力不是 v0.29 才全部出现,但在这个版本里已经能比较稳定地组合使用。比如一边开 prefix caching 一边开 chunked prefill,这在老版本里偶尔会有显存碎片问题,v0.29 的 page 分配逻辑处理得更平滑。

我在测试里感受最明显的是 Continuous Batching。以前跑离线推理时,一批请求里如果有人生成长句,其他人只能干等;在线服务开启 continuous batching 后,调度器在每个 step 都会重新决策,短请求的 token 能先返回,长请求继续排队,吞吐量一下就上来了。如果想要最大吞吐,通常还可以配合--max-num-seqs调整单 batch 大小,但这参数不是越大越好,需要结合显存看。

2. 从源码角度理解 Online Serving 的请求生命周期

2.1 请求入口与调度器协作

我第一次看 vLLM 源码时最懵的是找不到一个传统的“请求处理循环”。后来才明白,Online Serving 的核心不是 HTTP handler,而是LLMEngine里的step()方法。每次step()会经历一次完整的调度决策,然后执行一个 forward 迭代,生成新的 token 或结束请求。

大致链路是这样的:OpenAI API server 收到请求后,解析出 prompt、采样参数、请求 ID,然后调用engine.add_request()。这个请求不会马上被执行,而是被放进调度器的 waiting 队列。调度器根据显存剩余量、当前 running 队列容量、请求优先级等条件,从 waiting 里挑一批请求进入 running。每个 step,模型对这组 running 请求做一次 forward,输出 token。如果一个请求生成了 EOS 或者达到max_tokens,调度器就把它从 running 移除,释放 KV cache page,再去 waiting 里补新请求进来。通过AsyncLLMEngine的 producer-consumer 模型,HTTP 层不需要等待完整结果,而是通过异步队列拿增量结果。

理解这个流程后,很多部署问题就说得通了。比如并发很高时,Time per output token变慢,不一定是模型推理慢,很可能是调度器把太多请求塞进了 running 队列,导致每个 step 要处理更多 batch,GPU 单步耗时拉长。这时候调整--max-num-seqs比换显卡还管用。

2.2 为什么这类阅读要盯住“队列管理”

如果把 Online Serving 比作一个餐厅后厨,PagedAttention 是灶台分区,那调度器就是排菜员。客人点菜后,菜单先放在 waiting 区;厨房每 1 秒能同时做五道菜,就一次拿五张菜单到 running 区;如果某道菜做一半发现没材料,就先放到 swapped 区,把灶台让给别的菜。vLLM 的调度器也在做同样的事:显存不够时,把一些请求的 KV cache 换出到 CPU 内存,腾出 GPU 给新请求,之后再把换出的请求换回来继续生成。

这类队列管理的读法,我建议盯着三个类:Scheduler、SchedulerOutputs、SequenceGroup。SequenceGroup代表一个请求或一组并行请求,里面有采样参数、prompt、生成状态。Scheduler在schedule()方法里遍历所有SequenceGroup,判断能不能分配新的 KV cache block,能就加入ScheduledSequenceGroup,不能就继续等。

一个小细节:v0.29 的默认调度器策略是 FCFS(先来先服务),但每个请求内部还有优先级权重。实测下来,如果所有请求都是短 prompt、短输出,FCFS 已经足够;但如果混入大量长文档问答,最好把--enable-chunked-prefill打开,否则一个长 prompt 的 prefill 会堵住后面所有短请求。这个现象在源码里表现为 prefill 阶段需要一次性为完整 prompt 分配 KV block,显存不够时就会阻塞整个调度循环。

3. 实操:用 v0.29 部署 DeepSeek 与 Qwen 的完整记录

3.1 环境准备与版本依赖

先说结论:vLLM 不是“装完就能跑”的东西,环境一致性比版本号更重要。我这次实验用的是单张 24GB 显存的卡,系统是 Ubuntu,Python 使用 3.10。虚拟环境一定不能省,尤其是你已经装了其他深度学习框架的情况下。

python -m venv vllm-env source vllm-env/bin/activate pip install --upgrade pip pip install vllm==0.29.0

安装完成后,先验证 torch 的实际版本和 CUDA 能不能被识别:

python -c "import torch; print(torch.__version__, torch.cuda.is_available())"

如果torch.cuda.is_available()返回 False,先不要跑模型,去检查驱动和 CUDA 版本。vLLM 对 CUDA 的依赖比较硬,常见的报错是Torch not compiled with CUDA enabled或者No kernel image available for execution on the device。前者通常是 torch 装成了 CPU 版,后者通常是驱动太老或者 CUDA runtime 不匹配。

我这里部署的模型是 DeepSeek-R1-Distill 系列和社区里一个叫 Qwen3.8-Flash-Next 的轻量变体。服务化方式完全一样,模型名不同而已。我实际验证主要用 Qwen3-8B 来跑,因为它对显存更友好,参数理解更直接。下载模型时建议先把权重放到本地目录,避免每次启动服务都去 HuggingFace 拉文件,拉一半断网很折磨人。

3.2 启动 OpenAI 兼容服务并压测

启动命令我习惯写成 shell 脚本,避免每次重复填参数:

python -m vllm.entrypoints.openai.api_server \ --model /models/Qwen3-8B \ --served-model-name qwen3-8b \ --port 8000 \ --max-model-len 8192 \ --gpu-memory-utilization 0.90 \ --max-num-seqs 64 \ --enable-prefix-caching

启动日志里最关键的是显存分配信息。vLLM 会在加载完模型后打印类似GPU blocks: xxx, block size: 16的信息。如果GPU blocks数量极少,说明max-model-len或max-num-seqs设得过高,KV cache 没剩多少空间。实测里 24GB 显卡跑 8B 模型,--max-model-len 8192一般能留下几千个 block,足够支撑 64 路并发。启动成功后,用 curl 做一次最小验证:

curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "qwen3-8b", "messages": [{"role": "user", "content": "解释一下什么是分页"}], "max_tokens": 128, "temperature": 0.7 }'

返回 JSON 里的usage.completion_tokens就是实际生成的 token 数。我压测时通常会写一个几十行的 Python 脚本,用concurrent.futures同时发 30 个请求,每个请求限制输出 200 token,观察整体耗时和 token 吞吐。

3.3 部署时的关键参数选择

很多新手把--max-model-len当成模型上下文长度上限,理解没错,但它同时也是 KV cache 分配的上限。显存有限时,把max-model-len设得过大,会直接挤占并发 batch 的显存。我的经验是:如果业务里绝大多数请求都在 2K token 以内,就不要无脑设 32K。

--gpu-memory-utilization控制在 0.85 到 0.92 之间比较安全。设成 0.99 虽然能用满显存,但一旦有显存碎片或者 PyTorch 额外分配,会直接 OOM。vLLM 会在启动时预先占用这部分显存,而不是等请求来了再分配,所以设太高并不会让单请求变快,只是提高了并发上限。

--max-num-seqs控制的是单次 step 最多同时处理的序列数。这个值太小,并发能力不够;太大,每个 step 都会变慢,TTFT 和 TPOT 都会上升。我的起点是从 32 或 64 开始,压测后根据inter_token_latency指标再调整。如果是 8B 模型加 24GB 显存,64 通常比较稳妥。

刚才提到的--enable-prefix-caching对多轮对话特别有效。因为多轮对话的 prompt 会不断拼接历史消息,但系统提示词和前面几轮内容是重复的,prefix caching 可以复用这些 KV block,减少重复计算。社区热词里提到的“qwen3.8-flash-next”这类轻量模型,prefix caching 收益更明显,因为单请求本身快,省掉 prefill 时间对延迟改善很大。

4. 安装 vLLM 时最容易被坑的 torch 依赖问题

4.1 为什么安装 vLLM 会动到已装好的 torch

“安装 vllm 会改变已经安装好的 torch”这个问题,几乎每个用过 vLLM 的人都遇到过。原因很简单:vLLM 的 wheel 包在 metadata 里声明了严格的 torch 版本依赖,比如要求某个特定版本或某个版本范围。当你用pip install vllm==0.29.0时,pip 发现当前环境里的 torch 版本不符合要求,就会自动下载符合要求的 torch,哪怕这个操作意味着把已经装好的 torch 升上去或者降下来。

这种自动替换在大型项目中很危险。比如你之前装的是 torch 2.5.0,而 vLLM 要求的是 2.4.0,那 pip 会卸载 2.5.0,再装 2.4.0。表面上看起来只是版本变了,实际上一些依赖 torch 的库可能在 import 时才暴露不兼容问题。更隐蔽的是,pip 在替换 torch 时可能会连带升级numpy、transformers、tokenizers等依赖,导致你的其他项目莫名其妙报错。

要确认是不是被替换了,可以在安装前先记录环境状态:

pip freeze > before.txt pip install vllm==0.29.0 pip freeze > after.txt diff before.txt after.txt

这个方法很傻但很有效。我自己的经历里,被改得最多的就是 torch、numpy、transformers、tokenizers 这四个包。

4.2 避免 torch 被替换的几种做法

最推荐的方案是永远给 vLLM 开一个独立虚拟环境。vLLM 不是一个轻量库,它有很多底层 C++ kernel 和 CUDA 依赖,和项目里其他包混装出问题的概率很高。虚拟环境不是万能,但至少能让你“删掉重来”的成本很低。

如果你已经有一个不能动的深度学习环境,可选的折中办法是:

pip install vllm==0.29.0 --no-deps

然后根据 vLLM 的 requirements 文件手动补齐缺失依赖。这么做的问题是你需要自己搞清楚依赖版本,如果漏装某个 C++ 扩展,运行时才会报错。我一般只有在“当前 torch 版本必须保持不动,而且我清楚自己在干什么”的时候才用--no-deps,新手不建议一上来就这么搞。

再一个可行方案是用官方 Docker 镜像。vLLM 官方镜像里 torch、CUDA、vLLM 的版本是固定搭配好的,不需要自己处理依赖冲突。缺点是镜像体积大,而且如果你需要自己挂载本地数据集或模型目录,要记得加 volume。我实际生产环境里更倾向用 Docker,因为可复现性最高;本地做实验则用虚拟环境,调试更方便。

还有个小技巧:在 pip 安装前,先看一眼 vLLM 仓库根目录下的requirements/common.txt,确认里面写的 torch 版本。不要等到安装完成后才发现 torch 被改了。真正的问题往往不是“版本变了”,而是你根本不知道它在什么时候变的。

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

5.1 显存不足与预分配

最常见的问题是模型加载成功,但一并发请求就报CUDA out of memory。这类错误经常让人误以为是模型太大,实际上是 vLLM 启动时预留的 KV cache 空间不够。排查时先看两点:--gpu-memory-utilization是否过高,--max-model-len是否过大。如果显存利用率已经到 0.9 还 OOM,建议把max-model-len从 8192 降到 4096,释放更多空间给 KV cache。

另外注意,Chunked Prefill 开启后,长 prompt 会切成多个 chunk 来处理,虽然避免了一次性分配大量 KV block,但也会因为每个 chunk 都需要单独做 prefill,导致少量额外开销。如果显存本来就紧张,先别同时开 prefix caching 和 chunked prefill,等确认 baseline 稳定后再逐渐加上。

5.2 并发请求延迟升高

压测时经常出现“单请求很快,并发一高就全线变慢”的情况。我在 v0.29 实测里,这个问题的首要原因是--max-num-seqs设置过大,导致调度器把太多序列塞进 running,单步 batch size 变大,GPU kernel 执行时间变长。排查方法是先降到 16 或 32,再逐步提高,同时看两个指标:TTFT(首 token 延迟)和 TPOT(每个输出 token 的延迟)。如果 TTFT 稳定但 TPOT 上涨,基本就是 batch 过大。

有时候延迟升高也来自 prefix caching 命中率太低。如果每个请求的 prompt 前缀都不同,prefix caching 不但没省时间,反而增加了查找开销。这时可以关掉--enable-prefix-caching对比一下。

5.3 模型加载失败与 tokenizer 不匹配

模型加载失败常见的三类报错:第一类是路径错误或权限不足,vLLM 会提示找不到config.json;第二类是tokenizer与模型不匹配,比如模型是 chat 版本但只加载了 base tokenizer,导致 special token 处理异常;第三类是trust_remote_code未开启,部分模型需要自定义 modeling 文件。

对于 Qwen 这类需要远程代码的模型,启动命令里要加--trust-remote-code。如果你把模型下载到本地,也要确保 HuggingFace 缓存目录里的snapshots完整,缺一个 shard 文件都会在加载中途报错。我踩过最深的坑是模型权重 shard 下载到一半,vLLM 没报错,但生成的文本全部乱码,重新下载后恢复正常。任何“输出质量异常”最好先验证权重完整性,而不是怀疑采样参数。

5.4 常见问题速查表

现象可能原因处理方式
启动即 OOMgpu-memory-utilization太高降到 0.85~0.90,降低max-model-len
并发后延迟飙升max-num-seqs过大降到 16~32,观察 TTFT/TPOT
响应出现乱码模型权重不完整或 tokenizer 不匹配重新下载快照,确认 tokenizer_config
torch 版本被改vLLM 依赖自动解析使用独立虚拟环境,检查 requirements
无法加载远程代码模型缺少--trust-remote-code启动命令添加该参数
多轮对话变慢prefix caching 未开启加--enable-prefix-caching
长 prompt 阻塞短请求prefill 太长开启--enable-chunked-prefill
服务启动成功但 404served-model-name 与请求 model 不一致确认客户端model参数与实际启动名一致

6. 关于 Online Serving 后续扩展的想法

6.1 前缀缓存与多 LoRA 适配

继续往下做,我最看好的两个方向是 Prefix Caching 和 LoRA 动态适配。前者对多租户场景帮助很大,因为不同用户的 system prompt 往往一样,前缀命中后能节省大量 prefill 计算。后者让一个基础模型同时服务多个业务线,每个业务挂不同的 LoRA adapter,不用为每个 adapter 单独起服务。

实操时要注意,LoRA 适配器会吃额外显存,vLLM 为每个 adapter 维护额外权重,如果 adapter 数量多,max-cpu-loras和max-loras这两个参数都要合理设置。社区里很多“能不能塞 100 个 LoRA”的问题,本质上都是显存计算问题,先算模型基础权重 + 每个 adapter 权重,再算 KV cache 剩余空间,答案自然就有了。

6.2 个人实验心得

这次读 v0.29 的 Online Serving,我最大的收获不是某个新参数,而是把“调度器”当成理解 vLLM 的主线。以前部署时遇到 OOM、延迟抖动、请求排队,我只会在参数表里翻答案,现在会先想“这一步调度器为什么这样决策”。比如max-num-seqs为什么会对延迟影响这么大,因为它在调度循环里直接决定了单 step 要跑多少个序列;prefix caching为什么偶尔不生效,因为调度器按 content hash 找匹配,前缀稍有不同就命中不了。理解这些之后,很多部署问题都可以通过“看日志里的调度数据”而不是“瞎试参数”来解决。

另外一个很朴素但重要的心得是:不要在同一个环境里混装多套推理框架。如果同时装 vLLM、TensorRT-LLM 和普通 torch script,迟早会被依赖冲突磨掉一天时间。老老实实用独立虚拟环境或容器,把 torch 版本管理好,比什么都管用。后续如果再深入,我会去啃Scheduler的schedule()实现和显存 block 分配策略,因为这才是 Online Serving 性能的核心,也是值得花时间的地方。

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

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

立即咨询