1. 从一次线上推理抖动说起:vLLM v0.29 Online Serving 到底改了什么
九月底那几天,我负责的一个推理服务集群出现了很典型的“午后抖动”——QPS 没涨,但 P99 延迟从 800ms 一路爬到 2.3s,GPU 利用率却只有 60% 出头。排查了半天,最后定位到的是调度层:请求在 EngineCore 的等待队列里排队时间过长,而实际计算资源并没有吃满。这个问题在 vLLM v0.29 的 Online Serving 更新里被正面回应了,所以我把这次升级的导读笔记整理出来,给同样在跑在线推理服务的同行做个参考。
先说清楚这篇笔记的定位。它面向的是已经在用 vLLM 做在线服务、或者正准备把 DeepSeek-V4、Qwen3.8-Flash-Next 这类模型部署到生产环境的工程师。核心关键词是vLLM、Online Serving、EngineCore、CLI。我会从架构调整、CLI 使用方式、EngineCore 调度逻辑、常见部署坑几个角度拆开讲,尽量把“为什么这么改”和“怎么落地”都说透。如果你只是本地跑个 demo,这篇可能偏重;但如果你要扛真实流量,下面的内容基本都能直接用。
vLLM 这个项目从 2023 年起步,到 v0.29 这个版本,Online Serving 这条线已经和早期完全不是一个东西了。早期大家用python -m vllm.entrypoints.openai.api_server起一个进程,所有逻辑塞在一起,简单但脆弱。v0.29 把 EngineCore 单独抽出来,CLI 也做了统一入口,这背后是对“在线服务”这个场景的重新理解:它不是“把模型跑起来”,而是“在持续压力下稳定地吐 token”。这个区别决定了后面所有的设计取舍。
我这次升级踩的坑不算少,从 torch 版本冲突到 CLI 参数改名,再到 EngineCore 的显存分配策略变化,前后折腾了差不多两天。所以这篇笔记不会只讲“新特性有哪些”,而是把升级路径、参数含义、排查手段都摊开说,让你少走我走过的弯路。
2. v0.29 Online Serving 的架构调整与设计取舍
2.1 为什么要把 EngineCore 从主进程里拆出来
早期 vLLM 的在线服务是一个单体进程:HTTP 服务器、调度器、模型执行全在一个 Python 进程里。这种结构在单卡、低并发时没问题,但一旦上多卡或者高并发,问题就来了。最典型的是 GIL 争抢——HTTP 请求解析和调度逻辑抢同一个解释器锁,导致调度延迟不可控。另一个问题是故障隔离差,HTTP 层的一个异常可能把整个引擎拖垮。
v0.29 把 EngineCore 抽成独立组件,本质上是做了关注点分离。HTTP 层只负责协议解析和请求转发,EngineCore 专注调度和批处理,模型执行再往下走。这样带来三个直接好处:调度延迟更稳定、故障边界更清晰、多进程扩展更自然。我用一个生活化的类比:以前是一个厨师既接单又炒菜又端盘子,忙起来就乱;现在是前台接单、后厨调度、灶台炒菜各司其职,单量上来也不会互相踩脚。
这个拆分不是没有代价的。进程间通信引入了额外的序列化开销,请求对象要在进程边界上传递。vLLM 用的是共享内存加消息队列的组合来压低这个开销,但在极端高并发下,IPC 仍然可能成为瓶颈。所以如果你的场景是超低延迟(比如 P99 要求 100ms 以内),需要特别关注 EngineCore 的进程间通信配置,后面我会讲具体参数。
2.2 调度策略的变化:从 FCFS 到更细粒度的优先级
v0.29 在调度上做了明显调整。早期基本是先到先服务(FCFS),谁先来谁先算。这在请求长度均匀时没问题,但真实流量里请求长度差异极大——有的只要 10 个 token,有的要 2000 个。FCFS 下,一个长请求会把后面一堆短请求堵死,这就是我开头遇到的 P99 抖动根源。
新版本引入了更细粒度的调度控制,核心思路是按 token 预算做批处理,而不是按请求数。具体来说,调度器会估算每个请求还需要多少 token,然后在一个 batch 里塞进总 token 数接近上限的请求组合。这样短请求能快速完成并释放,长请求也不会独占资源。这个策略对 DeepSeek-V4 这种长上下文模型尤其重要,因为它的单请求 token 消耗可能是普通模型的十几倍。
这里有个关键参数叫max_num_batched_tokens,它决定了单个 batch 的 token 上限。设太小,GPU 利用率上不去;设太大,显存容易爆。我的经验是先用模型的最大上下文长度除以 4 作为起点,再根据实际显存占用微调。比如 32K 上下文的模型,可以从 8192 开始试。
2.3 CLI 统一入口的设计意图
v0.29 把 CLI 做了统一,vllm serve成为在线服务的主入口。这个改动看起来小,但实际影响挺大。以前启动参数散落在各个脚本里,不同入口的参数名还不一致,升级时经常因为参数改名导致启动失败。统一之后,参数命名规范了,文档也集中了。
更重要的是,CLI 现在承担了配置校验的职责。启动时会检查模型路径、显存预算、并行配置是否自洽,不合法直接报错退出,而不是等到运行时才崩。这个“早失败”的设计对生产环境很友好——总比服务跑起来半小时后才发现配置有问题强。
我实测下来,vllm serve的参数校验覆盖了大概 80% 的常见配置错误。剩下 20% 主要是显存相关的动态问题,这个只能靠运行时监控。所以 CLI 校验是必要不充分,别指望它兜住所有问题。
3. 核心参数与实操配置详解
3.1 启动一个 Online Serving 的最小可用配置
先给一个能直接跑起来的最小配置,以 DeepSeek-V4 为例。假设你有 2 张 80G 显存的卡,模型权重已经下载到本地:
vllm serve /path/to/deepseek-v4 \ --tensor-parallel-size 2 \ --max-model-len 32768 \ --gpu-memory-utilization 0.90 \ --max-num-batched-tokens 8192 \ --port 8000逐行解释一下。--tensor-parallel-size 2表示用 2 张卡做张量并行,这个值必须等于你的 GPU 数量(单机场景)。--max-model-len 32768是最大上下文长度,设得比模型支持的上限小可以省显存。--gpu-memory-utilization 0.90表示允许 vLLM 使用 90% 的显存,留 10% 给系统和临时缓冲。--max-num-batched-tokens 8192就是前面说的 batch token 上限。
这里有个容易踩的坑:--gpu-memory-utilization不是越高越好。我试过设 0.95,结果在长请求突发时 OOM 了。因为 vLLM 的显存预分配是基于这个比例算的,但实际运行时还有 KV cache 的动态增长,留 10% 到 15% 的余量比较稳。设 0.90 是我在多个模型上验证过的平衡点。
3.2 显存预算的计算过程
很多人配参数是拍脑袋,其实显存是可以算的。以 DeepSeek-V4 为例,假设模型权重是 140GB(FP16),2 张 80G 卡共 160G。权重占 140G,剩 20G 给 KV cache 和激活值。KV cache 的大小取决于 batch size 和序列长度,公式大致是:
KV cache 显存 = 2 * num_layers * num_kv_heads * head_dim * seq_len * batch_size * dtype_size这个公式不用背,但要知道它的含义:KV cache 和序列长度、batch size 成正比。所以--max-model-len设大一倍,KV cache 的峰值需求也差不多翻倍。这就是为什么长上下文模型特别吃显存。
我的实操建议是:先用小max-model-len跑起来,观察实际显存占用,再逐步往上调。vLLM 启动日志里会打印显存分配详情,包括权重、KV cache、激活值各占多少,这个日志一定要看,比任何估算都准。
3.3 关键参数速查表
| 参数 | 作用 | 推荐值 | 注意事项 |
|---|---|---|---|
--tensor-parallel-size | 张量并行卡数 | 等于 GPU 数 | 单机必须整除 |
--max-model-len | 最大上下文 | 按业务需求 | 越大越吃显存 |
--gpu-memory-utilization | 显存使用比例 | 0.85-0.92 | 别超 0.95 |
--max-num-batched-tokens | batch token 上限 | 上下文/4 起调 | 影响吞吐和延迟 |
--max-num-seqs | 最大并发序列数 | 按显存调 | 和 batch token 联动 |
--swap-space | CPU 交换空间 | 4-8 GB | 显存不足时的缓冲 |
这张表是我从多次部署里总结出来的,推荐值是经验区间,不是绝对值。比如--max-num-seqs,在 80G 卡上跑 7B 模型可以设到 256,但跑 70B 模型可能只能设 32。一定要结合模型大小和显存算。
4. 从零到一的部署实操流程
4.1 环境准备与依赖安装的坑
安装 vLLM 这一步,坑比想象中多。最常见的问题是torch 版本冲突。vLLM 对 torch 版本有严格要求,如果你环境里已经装了别的版本的 torch,pip 安装 vLLM 时可能会把它降级或升级,导致其他依赖崩掉。我的做法是用独立的虚拟环境,别和现有项目混用。
python -m venv vllm-env source vllm-env/bin/activate pip install --upgrade pip pip install vllm==0.29.0装完之后验证一下 torch 版本:
python -c "import torch; print(torch.__version__)" python -c "import vllm; print(vllm.__version__)"两个版本都要对得上 vLLM 的官方要求。如果 torch 被改动了,而你其他项目又依赖旧版本,那就得考虑用容器隔离。我现在的做法是每个推理服务一个 Docker 镜像,彻底避免依赖污染。
另一个坑是 CUDA 版本。vLLM 编译时链接的 CUDA 版本要和驱动兼容。如果启动时报 CUDA 相关的错,先查nvidia-smi的驱动版本,再对照 vLLM 的 CUDA 要求。这个不匹配的话,重装也没用,得换镜像或升级驱动。
4.2 模型加载与首次启动验证
模型加载阶段最容易出问题的是路径和权限。vllm serve的模型路径参数支持本地路径和 HuggingFace 仓库名,但生产环境我强烈建议用本地路径,避免网络抖动导致启动失败。
首次启动时,加上--disable-log-requests可以减少日志噪音,但调试阶段别加,因为请求日志对排查问题很有用。启动后看到类似这样的输出就说明成功了:
INFO: EngineCore started INFO: Model loaded, memory usage: weights=140GB, kv_cache=18GB INFO: OpenAI-compatible server listening on port 8000重点看显存分配那行。如果 kv_cache 分配得很少(比如只有 2GB),说明max-model-len或max-num-seqs设小了,吞吐上不去。如果启动直接 OOM,就往下调gpu-memory-utilization或max-model-len。
4.3 用 curl 做端到端验证
服务起来后,别急着接业务流量,先用 curl 打一发:
curl http://localhost:8000/v1/completions \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-v4", "prompt": "介绍一下张量并行", "max_tokens": 128, "temperature": 0.7 }'这个请求能正常返回,说明整条链路通了。然后测一下并发,用ab或wrk打 50 并发,观察延迟和显存。这一步是必须的,因为单请求正常不代表并发正常。我见过单请求跑得好好的,一上并发就 OOM 的情况,就是 KV cache 在并发下膨胀导致的。
并发测试时重点看两个指标:首 token 延迟(TTFT)和每 token 延迟(TPOT)。TTFT 反映调度和预填充效率,TPOT 反映解码效率。如果 TTFT 高但 TPOT 正常,问题在调度层;如果两个都高,可能是显存不足导致频繁换页。
5. 常见问题排查与避坑实录
5.1 启动报错速查表
| 报错信息 | 可能原因 | 解决方法 |
|---|---|---|
| CUDA out of memory | 显存预算超了 | 降 gpu-memory-utilization 或 max-model-len |
| torch version mismatch | 依赖冲突 | 用独立虚拟环境重装 |
| model not found | 路径错误或权限 | 检查路径、用绝对路径 |
| port already in use | 端口占用 | 换端口或杀进程 |
| NCCL error | 多卡通信问题 | 检查网卡、设 NCCL 环境变量 |
这张表里的每一条我都实际遇到过。特别是model not found,很多人以为是模型没下载,其实是路径写错了或者权限不够。用绝对路径能避免 90% 的这类问题。
5.2 延迟抖动的排查思路
回到我开头说的 P99 抖动。排查这类问题,我的顺序是:先看 GPU 利用率,再看队列长度,最后看调度日志。如果 GPU 利用率低但延迟高,基本可以确定是调度或 IPC 瓶颈,而不是计算瓶颈。
v0.29 的 EngineCore 会打印调度统计,包括等待队列长度、batch 大小分布、调度耗时。这些日志默认是关的,需要开--enable-engine-stats。开了之后能看到每个调度周期的详情,对定位抖动非常有用。
我那次抖动最后定位到的是max-num-batched-tokens设得太大,导致单个 batch 处理时间过长,后面的请求排队。把它从 16384 降到 8192 后,P99 从 2.3s 降到 900ms。这个教训是:batch token 上限不是越大越好,它和延迟是权衡关系。
5.3 多卡部署的通信优化
多卡场景下,NCCL 通信是另一个常见瓶颈。如果tensor-parallel-size大于 1,卡间通信量会显著增加。优化手段有几个:确保用 NVLink 而不是 PCIe(如果硬件支持)、设置合适的 NCCL 环境变量、避免跨 NUMA 节点。
export NCCL_IB_DISABLE=1 export NCCL_P2P_LEVEL=NVL export NCCL_DEBUG=WARN这几个环境变量是我在多卡部署里必设的。NCCL_P2P_LEVEL=NVL强制走 NVLink,能明显降低通信延迟。NCCL_DEBUG=WARN只在有警告时输出,避免日志刷屏。
5.4 我踩过的三个真实坑
第一个坑是升级 vLLM 后 torch 被降级,导致另一个训练任务崩了。后来我所有推理服务都改用容器,彻底隔离。
第二个坑是**max-model-len设得比模型实际支持的大**,启动时不报错,但跑到长序列时输出乱码。这个坑很隐蔽,因为短请求完全正常。解决办法是查模型的 config.json,确认max_position_embeddings的真实值。
第三个坑是并发测试用短请求,上线后遇到长请求就 OOM。后来我的并发测试里强制混入 20% 的长请求(接近 max-model-len),这样才能测出真实的显存峰值。
6. 性能调优与监控建议
6.1 吞吐和延迟的权衡曲线
vLLM 的调优本质是在吞吐和延迟之间找平衡点。max-num-batched-tokens调大,吞吐上升但延迟上升;调小则相反。这个权衡没有标准答案,取决于你的业务是吞吐敏感还是延迟敏感。
我的做法是画一条曲线:固定其他参数,把max-num-batched-tokens从 2048 到 16384 扫一遍,记录每个点的吞吐和 P99 延迟。然后根据业务 SLA 选点。比如 SLA 要求 P99 小于 1s,那就选满足这个条件里吞吐最高的那个点。
6.2 必看的监控指标
生产环境我必监控这几个指标:GPU 利用率、显存占用、等待队列长度、TTFT、TPOT、请求成功率。前三个从 vLLM 的 metrics 接口拿,后三个从业务侧埋点。
vLLM 暴露了 Prometheus 格式的 metrics,默认在/metrics路径。关键指标包括vllm:num_requests_waiting(等待队列)、vllm:gpu_cache_usage_perc(KV cache 使用率)、vllm:time_to_first_token(TTFT 直方图)。这几个指标能覆盖 80% 的线上问题定位。
6.3 一个实用的压测脚本
最后分享一个我常用的压测脚本,用 Python 写的,能模拟混合长度请求:
import asyncio import aiohttp import random import time async def send_request(session, prompt_len): prompt = "测试" * prompt_len payload = { "model": "deepseek-v4", "prompt": prompt, "max_tokens": 128 } start = time.time() async with session.post("http://localhost:8000/v1/completions", json=payload) as resp: await resp.json() return time.time() - start async def main(): async with aiohttp.ClientSession() as session: tasks = [] for _ in range(100): prompt_len = random.choice([10, 50, 200, 1000]) tasks.append(send_request(session, prompt_len)) latencies = await asyncio.gather(*tasks) latencies.sort() print(f"P50: {latencies[50]:.3f}s") print(f"P99: {latencies[99]:.3f}s") asyncio.run(main())这个脚本的关键是混合了不同长度的 prompt,能更真实地模拟线上流量。纯短请求的压测结果会过于乐观,上线后容易翻车。
我在实际使用中发现,vLLM v0.29 的 Online Serving 相比早期版本,稳定性提升是实打实的,但配置复杂度也上来了。参数多了,调优空间大了,但配错的概率也高了。所以我的建议是:先用最小配置跑通,再逐个参数调优,每调一个就压测一次,别一次改一堆。这样出问题能快速定位是哪个参数导致的。另外,升级前一定要在测试环境完整跑一遍,特别是依赖版本,别直接在生产环境升级。