第一次跑 vLLM Serve,很多人会被一长串参数劝退。拿一个开源 7B 对话模型做推理服务,不加任何参数也能跑起来,但跑完一看:显存还剩一大截,并发一上去请求全在排队,日志里一堆警告又不知道从哪里下手。这篇文章不打算把每个参数都念一遍,而是从一次真实部署的调参链路出发,把 vLLM Serve 里常用但容易用错的参数按“显存分配—批处理调度—计算精度—多卡分布—前缀缓存—问题排查”这条线拆开讲清楚。
内容适合三类人:第一次用 vLLM 部署推理服务的新手,需要把服务压到更高吞吐的开发者,以及刚接手多卡推理服务、被各种显存问题折腾过的人。读完之后,你能明白每个参数为什么存在、改它会影响什么,以及遇到常见报错时应该先动哪个旋钮。
1. 先把服务拉起来:模型、接口与基础参数
1.1 模型路径与模型名参数,看似简单却容易翻车
--model是第一个要决定的参数,它告诉 vLLM 加载哪个模型。可以指向本地目录,也可以直接填一个远端模型仓库的标识符。本地目录里必须有模型权重和完整的配置文件,vLLM 启动时会根据 config 推断模型的层数、注意力头数、词汇表大小这些结构信息。如果在生产环境使用,我建议用脚本提前把模型同步到本地磁盘,不要每次启动都从远端拉权重,否则冷启动时间会非常感人。
--served-model-name是服务对调用方暴露的模型名。很多人忽略它,直接用默认值,等后面同时部署多个模型时才发现调用方根本区分不了。它跟--model不是一回事:--model是“服务器要加载哪个权重”,--served-model-name是“调用方应该写哪个名字来访问这个服务”。例如模型本地目录叫./models/my-7b,你可以对外叫my-7b-prod,这样后面灰度切流时不需要改调用方代码。
还有一个容易被忽略的--tokenizer参数。默认情况下 vLLM 会从模型目录的 config 推断 tokenizer,但有些自定义模型的 tokenizer 文件跟权重不在同一个目录,或者预处理器经过了特殊改造,这时候不显式指定就会遇到乱码、句子截断异常等问题。排查思路很简单:模型输出中文突然变乱、特别短就停,优先检查 tokenizer 是否匹配,而不是急着怀疑显存不够。
1.2 服务监听与认证参数,生产环境必须处理
--host默认绑定127.0.0.1,只允许本机访问。部署在服务器上给其他服务调用时,记得改成0.0.0.0,这个坑几乎每个新手都会踩一次:容器起来了、日志也正常,但外部就是连不上,最后发现是 host 没改。
--port默认 8000,遇到端口冲突可以直接换。真正重要的是--api-key。vLLM Serve 暴露的是标准 HTTP 接口,如果不设置认证,任何拿到服务器端口的人都能直接调用你的模型,算力全被别人白嫖。生产环境务必设置,并且用环境变量或密钥管理服务注入,不要写死在启动脚本里。--disable-log-requests是我每次部署都会带的参数,默认情况下 vLLM 会把每个请求的详细内容打到日志,这在隐私敏感的场景下非常危险。关闭请求日志之后,服务运行状态、错误栈这些日志仍然保留,只是不再刷用户输入。
1.3 一个最简可用启动命令
先给一个最小启动命令,能跑通再谈优化:
vllm serve ./models/my-7b \ --served-model-name my-7b \ --host 0.0.0.0 \ --port 8000 \ --api-key "your-secret-key" \ --disable-log-requests不加任何显存和并发参数时,vLLM 会用默认配置启动。第一次调试时先用这条命令把服务拉起来,然后访问健康检查接口和补全接口,确认模型能正常回答,再进入下一步调参。很多同学一上来就堆上一堆参数,出了问题反而不知道是哪个参数导致的,这是调优的大忌。
2. 显存分配逻辑:KV Cache 与 gpu-memory-utilization
2.1 KV Cache 是什么,为什么只看首 token 看不出问题
KV Cache 是 vLLM 性能的核心,也是显存占用的主要去向。打个比方:模型每生成一个 token,都要反复“回看”之前已经生成的所有 token 的 Key 向量和 Value 向量。与其每次重新算一遍,不如把这些向量缓存下来,这就是 KV Cache。它决定了你同时能处理多少请求、能支持多长的上下文。
只看首 token 延迟完全看不出 KV Cache 的影响,因为单个短请求根本不会把缓存打满。真正的考验发生在长文和并发场景:一个请求带 8K 上文,另一个请求带 16K 上文,KV Cache 会成倍增长,显存很快见底。所以调优的第一步,是先搞清楚 KV Cache 在你的显存里占了多大空间。
2.2 gpu-memory-utilization 的经验取值
--gpu-memory-utilization默认是 0.9,含义是 vLLM 最多能占用多少比例的显存。剩下那部分给模型权重、CUDA 内核、中间激活和推理框架本身的运行时代码用。
0.9 适合专属推理卡,比如整张卡只跑 vLLM 一个服务。但如果是跟其他进程共用显卡,或者显存本来就紧张,调到 0.6 到 0.7 更稳妥。这个参数并不是越高越好,调到 0.98 甚至 0.99 时,模型加载阶段可能不报错,一接请求就直接 OOM。原因在于运行时需要临时分配的激活内存不在预留预算里,极端利用率没有给动态内存留后路。
实操时我一般这样取值:单卡跑 7B 级别模型,0.85 起步;多卡张量并行,0.88 到 0.9;加了量化之后反而要降一点,因为量化权重和反量化过程同样会占额外显存。一个有效的观察窗口是 vLLM 启动日志里打印的 KV Cache 容量数值,例如KV cache size = 4.20 GB,这个数字能直接告诉你缓存预算还剩多少。
2.3 max-model-len 设多大,先算账再决定
--max-model-len决定模型允许的最大上下文长度,它不只是模型结构参数,更是一个显存预算参数。KV Cache 的显存占用大致可以这样估算:
KV Cache 字节数 ≈ 2(K 和 V 两份)× 层数 × KV 头数 × 头维度 × 序列长度 × 并发序列数 × 每个元素字节数
以某个 32 层、KV 头数为 8、头维度 128 的 7B 开源模型为例,半精度下每个元素占 2 字节,单条序列每 1K token 的 KV Cache 大约是 2 × 32 × 8 × 128 × 1024 × 2 = 134 MB。如果模型用的是全量注意力头而不是分组查询注意力,KV 头数变成 32,单条序列每 1K token 就要 512 MB。看到差距了吗?上下文长度从 2K 提到 16K,同样的显存能支撑的并发数直接掉一个数量级。
所以我的建议是先统计线上真实请求的上下文长度分布,取 P99 再上浮一点,不要按模型的理论最大长度去配。之前帮一个团队调服务,业务平均上下文只有 3K、最长不超过 8K,他们却把 max-model-len 按模型理论值 32K 设置,结果 KV Cache 预占了大量显存,并发一直被压得很低。把它改成 12K 之后,同样的硬件吞吐翻倍都不止。
2.4 用启动日志反推显存分配
调这个参数不要靠猜,每次改完参数重启,看两个地方:第一是 vLLM 启动日志里 KV Cache size 的数值,第二是监控工具里实际的显存水位。如果 KV Cache 很小但显存占用已经很高,说明权重或运行时占了大头;如果 KV Cache 大得惊人,说明上下文或者并发预算开过了。明确了这步,后面调 max-num-seqs 才有依据。
3. 吞吐与并发:batch 与 token 调度参数
3.1 max-num-seqs 与 max-num-batched-tokens 的关系
--max-num-seqs是服务端允许同时计算的请求序列数上限,--max-num-batched-tokens是每一步迭代最多处理的 token 总数。这两个参数的关系可以这么理解:前者是餐厅能同时接纳的客人桌数,后者是后厨一次最多能同时下锅的菜量。桌数开得再大,后厨一次只能炒几道菜,客人还是得等着。
实操中常见的问题是只调其中一个。比如把 max-num-seqs 拉到 256,但 max-num-batched-tokens 还是默认值,并发请求确实进来了,可每个迭代能处理的 token 只有那么多,GPU 算力没有被喂满,表现就是排队时间依然很高、吞吐没有明显变化。
3.2 连续批处理的工作方式
vLLM 的优势在于连续批处理,也就是 continuous batching。传统推理框架要等一个 batch 全部生成结束,才能接收下一批请求,这会让 GPU 在 batch 尾声阶段大量空闲。vLLM 的做法是:每步迭代结束后,已经完成的请求立刻移出 batch,新请求可以在迭代间隙插入当前计算。这样 GPU 几乎所有时间都在算 token,而不是等请求。
--max-num-batched-tokens就是这一步能塞进 GPU 的上限。设得太小,算力喂不满;设得太大,每步计算量波动会很剧烈,显存峰值也随之上升。实际调参时我习惯从 4096 起步,压测观察 GPU 利用率,没到 90% 就往上加,一次加 2048。
3.3 max-num-seqs 调优实践
max-num-seqs 直接受 KV Cache 预算约束。可用 KV Cache 总量除以单条序列在业务平均长度下的缓存占用,可以得到一个参考并发上限。但实际不用算得那么精确,更快的方法是按配置表去压测:
| 模型规模 | 显卡显存 | max-num-seqs 参考起点 |
|---|---|---|
| 7B 级别 | 24GB | 128 |
| 13B 级别 | 40GB | 64 |
| 70B 级别(多卡) | 80GB 级别整机 | 32 |
这些只是起点,不是终点。压测时如果吞吐还在涨、排队时间也不高,就继续翻倍;如果出现显存不足或排队时间明显上升,就退回一档。值得留意的是,max-num-seqs 增大后,每个请求的 KV Cache 都会占一块固定显存,所以测试时要用接近业务真实长度的 prompt,而不是用几行字的短请求去测,否则结果完全是误导。
3.4 enable-chunked-prefill 解决长 prompt 堵车问题
长 prompt 的 prefill 阶段会一次性算完,一个大请求往往独占整块算力,后面所有短请求都得等。开--enable-chunked-prefill之后,vLLM 会把长 prompt 拆成小块,跟 decode 阶段的 token 交替计算,避免“一个长文档问题卡住所有人”的场景。
这在 RAG 类服务里效果特别明显:业务方经常上传几千 token 的长文档,其他用户的短问题被堵好几秒。开启后短请求的首 token 延迟显著下降,长请求的总完成时间也不会差太多。代价是调度器多了一层切块和合并逻辑,极端情况下吞吐会有一点损失,但绝大多数业务场景下收益远大于损失。
4. 计算图与数据精度:eager、dtype 与量化参数
4.1 enforce-eager 要不要打开
默认情况下 vLLM 会用 GPU 静态计算图技术把模型的计算提前捕获成固定图结构,从而减少每一步调度的内核启动开销。这对高频小 token 的 decode 阶段非常有利,是吞吐提升的重要来源。代价是每次输入形状变化时需要重放计算图,而且会为每条序列预留额外显存。
--enforce-eager的作用就是禁用这个静态图机制,改为普通的即时执行模式。什么场景需要开?排查模型计算的诡异 bug 时,禁用后错误信息通常更直接;或者在新的实验性结构上运行时,静态图捕获阶段报错,可以先用 eager 模式验证模型本身是否正常。但生产环境不建议常开,吞吐下降非常明显,尤其短文本场景下差距可能到 30% 以上。
4.2 dtype 选 float16 还是 bfloat16
--dtype默认是 auto,也就是从模型 config 里读取保存精度。大多数开源模型的权重保存为 float16,用float16即可。bfloat16 的指数范围比 float16 大得多,对权重溢出不敏感,训练后的模型直接用 bfloat16 加载通常数值更稳,但前提是你的显卡硬件支持,而且加载时也必须按 bfloat16 处理。
实操建议就一条:权重是什么精度,--dtype就填什么精度,不要混。出现过一些情况,模型 config 标记为 float16,但实际权重是 bfloat16 导出的,启动不报错,输出质量却明显下降。这时把 dtype 改成 bfloat16 重新加载,问题立刻消失。判断方法很简单:用一段固定 prompt 分别用两种 dtype 加载,对比输出是否稳定。
4.3 量化参数和 KV Cache 数据精度
--quantization支持 awq、gptq、fp8 这类量化方式。量化可以把权重从 16 位压到 4 位或 8 位,显著降低权重占用的显存,让模型放进更小的卡。但要注意,量化参数必须跟模型权重本身的格式匹配:模型是 awq 量化格式,就用--quantization awq;模型是普通 fp16 格式,你强行指定 awq,要么加载失败,要么生成垃圾内容。
--kv-cache-dtype fp8是另一条独立的路线,它把 KV Cache 从 16 位压成 8 位,缓存显存直接砍半。代价是精度下降,在长上下文下误差可能累积,生成质量是否可接受一定要用业务评测集验证。我的选型表大致是这样的:
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 权重显存紧张 | --quantization awq/gptq | 降低权重占用,模型更快装进单卡 |
| 长文高并发瓶颈在缓存 | --kv-cache-dtype fp8 | KV Cache 减半,并发容量直接翻倍 |
| 显存极致吃紧 | 两者组合 | 权重和缓存都压缩,但需要验证质量 |
| 生产质量敏感 | 不量化 KV Cache | 留足显存给缓存,优先保证输出质量 |
调试时先把两项分开测,不要一上来就组合。量化带来的收益通常不是线性的,有些场景下权重从 16 位压到 8 位吞吐提升只有几个百分点,但要承担的精度风险却大得多。
5. 多卡与分布式部署参数
5.1 tensor-parallel-size 怎么选
--tensor-parallel-size是张量并行参数,含义是把模型的计算切成几份放到几张卡上同时算。原理是把注意力矩阵、全连接矩阵按行或列切分,每步计算之后做一次全量聚合通信。
选这个参数的核心原则是:只有当单卡显存放不下模型权重加常用 KV Cache 时才开张量并行。如果一张卡能放下,不要为了“用满所有卡”而强行开 TP=2,吞吐不一定涨,还可能因为通信开销反而下降。张量并行每层计算都要做聚合通信,卡之间走的是 PCIe 或高性能卡间互联,一旦跨机器走网络,通信延迟会直接把性能拖垮。所以多卡场景尽量选同一台机器上的卡。
有一个容易被忽略的点:开张量并行之后,KV Cache 上的预算要按多卡总和来算。日志里显示的 KV Cache 是切分后每卡的量,实际服务能支持的并发需要把多卡的缓存加起来评估。
5.2 max-parallel-loading-workers 与权重加载加速
--max-parallel-loading-workers控制模型加载阶段并行读盘的进程数。大模型权重动辄几十 GB,默认并行度不高时,加载阶段 CPU 和磁盘 IO 都打不满,冷启动时间很长。
对这个参数,我的经验是分模型规模处理。7B 级别模型保持默认即可,并行度太高反而让磁盘小文件 IO 变成瓶颈;70B 级别模型可以调到 8,启动时间能缩短不少。但要注意:加载权重时并行进程多了,机器内存占用也会跟着涨,因为每个进程都会有一份权重副本。机器内存不足时,不是显存 OOM,而是主存先被吃满,这个报错经常被误判。
5.3 多卡启动示例
vllm serve ./models/my-70b \ --served-model-name my-70b \ --tensor-parallel-size 8 \ --gpu-memory-utilization 0.88 \ --max-model-len 8192 \ --max-num-seqs 64 \ --api-key "your-secret-key"启动之后用监控工具逐卡看显存水位,确认负载是否均匀。如果发现某张卡明显偏高、其他卡闲置,优先检查 pending 的请求是不是触发了某种不均衡的调度,再考虑是否要限流或调整并发参数。
6. 前缀缓存与长文场景:prefix caching 与聊天模板
6.1 enable-prefix-caching 对 RAG 和多轮会话很有效
--enable-prefix-caching会开启前缀缓存。vLLM 会把之前算过的 KV Cache 按块缓存下来,新请求如果前缀哈希命中,直接复用之前的计算结果,不需要重新跑 prefill。
这个参数最适合两类业务:RAG 服务中,系统提示词和固定的检索上下文会反复出现,命中率极高;多轮对话服务中,历史消息作为前缀被反复携带。我实测过一个几千字长 prompt 的场景,命中缓存后首 token 延迟从几秒降到了几十毫秒,吞吐提升非常明显,而且不需要改动任何上层代码。
判断一个服务适不适合开,看缓存命中率。vLLM 的监控指标里会暴露命中率数据,如果持续低于 20%,说明请求前缀随机性太强,缓存基本没有复用价值,这时开启只会增加哈希表维护的开销,关掉反而更稳。
6.2 前缀随机性强时不要盲目开启
日志分析类、临时字符串处理类服务,prompt 几乎每次都不一样,命中率极低,开启前缀缓存是纯负担。还有一种情况也要注意:虽然很多请求共享系统提示词,但提示词后面紧跟的是用户输入,如果前缀缓存按整块不跨请求复用,命中率也不会太高。这类服务建议先用压测确认命中率,再决定是否开启。
6.3 chat-template 是隐性性能参数
模型在对话补全接口下工作时,需要把原始消息列表转换成带特殊标记符的 prompt。vLLM 默认读取模型目录里的 tokenizer 配置中的聊天模板字段;如果模板缺失或者格式不对,会出现输出截断、不断重复同一句话等诡异问题。
--chat-template可以指定一个自定义模板文件。很多团队换模型之后回答质量变差,问题不在模型权重,而是模板里的提示词格式跟新模型的训练格式不匹配。另外还要注意,模板本身会占用上下文长度,模板写得越长,实际留给用户内容的 token 越少,KV Cache 的预算也被吃掉一部分。
7. 踩坑实录:常见报错与排查方法
7.1 显存不足(CUDA OOM)的排查顺序
显存不足是 vLLM 部署里最常见的报错,但报错位置五花八门,有的加载阶段就炸,有的一接请求才崩。我的排查顺序非常固定:
- 先看启动日志里 KV Cache size 的数值,确认预留了多少显存。
- 检查
--gpu-memory-utilization是否设得过高,优先降到 0.85 再试。 - 检查
--max-model-len是否远大于业务真实诉求,下调一个档位。 - 最后再压
--max-num-seqs,把并发上限降下来。
这三个参数构成了显存三角:利用率、上下文长度、并发上限。调整顺序有讲究,优先动 max-model-len 影响最直接,因为每多一档上下文长度,KV Cache 都是线性增长;利用率次之;并发上限放最后,因为调低并发只影响吞吐,不影响模型能力。
7.2 请求排队时间长的优化方向
如果监控里排队时间持续偏高,而 GPU 利用率没有打满,大概率是并发入口太小。请求进不来,GPU 还闲着。先调大--max-num-seqs,再调大--max-num-batched-tokens。但如果是 GPU 利用率已经接近 100%,排队是因为计算能力不够,这时候加并发没有意义,只能通过量化、减上下文、多卡并行或者换更大显存来解决。
诊断时一定要同时看两边数据:排队指标和 GPU 利用率。只看其一,很容易做出相反的操作。
7.3 性能上不去的根因定位
压测服务性能,先看两个指标:吞吐 token/s 和首 token 延迟。如果吞吐上不去,第一件事看 GPU 利用率。利用率低,说明请求喂不满算力,方向是提高并发和 batch;利用率高,说明算力饱和,方向是减负或扩容。
简单验证接口是否正常,可以直接用 curl 手测:
curl http://127.0.0.1:8000/v1/chat/completions \ -H "Authorization: Bearer your-secret-key" \ -H "Content-Type: application/json" \ -d '{ "model": "my-7b", "messages": [{"role": "user", "content": "写一段200字的自我介绍"}], "max_tokens": 200 }'手测只能验证通不通,压并发还是要用专门的压测工具。注意压测时要把请求长度和输出长度都设为接近业务真实值,用短输入短输出压出来的结果,对长文业务几乎没有参考价值。
7.4 版本升级后的参数变化
vLLM 迭代速度很快,参数默认值甚至参数名都会在版本之间变化。比如某些老版本里的批处理参数,到新版本被重构,启动时会直接给出警告。我的做法是:升级前先看 release notes,升级后先小流量灰度,不要拿着旧命令直接全量替换。
遇到参数找不到,先跑一下vllm serve --help,以当前版本实际支持的参数为准。旧配置文件里的参数如果已经不生效,尽早从配置里清理掉,否则会误导后续排查。
8. 从“最小可用配置”到“高并发生产配置”的调参路线
8.1 推荐的调整顺序
调参要按顺序来,一次只动一个参数,改一个记录一次压测数据,这样出了问题才能定位到人。
- 先用最简参数把服务跑通,确认接口正常。
- 查看显存分布,调整
--gpu-memory-utilization和--max-model-len,让 KV Cache 预算基本匹配业务需求。 - 压测,逐步提高
--max-num-seqs和--max-num-batched-tokens,目标是打满 GPU 利用率。 - 显存仍有瓶颈时,再考虑量化、KV Cache 数据精度压缩。
- 最后开启前缀缓存和分块预填充,针对长文场景收尾。
每一步的改动都记在表格里,参数、压测吞吐、首 token 延迟、显存水位,四个数据一起记。没有记录就谈不上调优,完全是凭运气改参数。
8.2 两套参考配置
低并发内网小规模场景,单卡 24GB 显存,跑 7B 级别模型:
vllm serve ./models/my-7b \ --served-model-name my-7b \ --gpu-memory-utilization 0.85 \ --max-model-len 8192 \ --max-num-seqs 128 \ --max-num-batched-tokens 4096 \ --enable-prefix-caching \ --api-key "your-secret-key"这套配置的特点是保守、稳定,单卡上有充足余量,不会因为突发的长请求把显存打爆。
高并发生产场景,多张 80GB 级别显卡跑 70B 级别模型:
vllm serve ./models/my-70b \ --served-model-name my-70b \ --tensor-parallel-size 8 \ --gpu-memory-utilization 0.88 \ --max-model-len 8192 \ --max-num-seqs 128 \ --max-num-batched-tokens 8192 \ --enable-chunked-prefill \ --enable-prefix-caching \ --disable-log-requests \ --api-key "your-secret-key"这套配置适合长文 RAG 服务和并发调用较多的场景。8 卡并行能把 70B 模型完整装下,chunked prefill 防止大请求堵车,prefix caching 降低长文首 token 延迟。
8.3 复盘:别把参数当性能银弹
调参解决的只是“资源分配不合理”的问题,解决不了“模型本身计算量太大”的问题。如果业务大并发需求已经超过单机上限,加参数没有意义,该扩容就扩容,该换推理架构就换推理架构。
真正的调优闭环是:看指标、定位瓶颈、调参数、再看指标,循环迭代。短 token 测试通过不代表长文并发就稳,每个版本上线前都要用真实业务流量回放验收。测试数据里混入了几条理论值的最大长度请求,观察显存水位是否安全,再决定要不要限流。
这套参数调到这里,我个人最大的体会是:vLLM Serve 调参本质上是在有限的显存里做资源再分配,而不是凭空变出算力。刚开始我也喜欢把并发调到很大、上下文开到理论最大值,结果每次都撞 OOM。后来养成一个习惯:先看日志里 KV Cache 的实际分配,再动手改参数,改完一定记录一次压测数据。如果你也是刚接触 vLLM,建议从最小配置跑通开始,一次只动一个参数,把每一步的变化记下来。遇到任何奇怪的显存报错,先想想三个数:利用率、上下文长度、并发上限,九成的问题都能倒推出来。