3.3 vLLM 启动参数深度调优


文档摘要

3.3 vLLM 启动参数深度调优 调优走到这一步,并行策略(3.1)和显存预算(3.2)的账你已经算过了。但真到了敲启动命令的时候,很多人还是会卡在一堆互相耦合的参数上—— 调到 256 还是 512? 到底跟 什么关系? 设 0.9 稳不稳?这些旋钮没有一个能孤立地拧,改一个常常要把另外两个一起跟着挪。 这一节我把生产环境里真正高频、真正值钱的那几个启动参数,挨个讲透:每个参数背后在算什么账、我自己的取值经验区间、什么场景下应该往哪边偏、以及我踩过哪些坑。读完之后你应该能拿着一份具体的硬件清单,直接敲出一个「不至于第一天就 OOM、也不至于明显浪费显存」的启动命令。

3.3 vLLM 启动参数深度调优

调优走到这一步,并行策略(3.1)和显存预算(3.2)的账你已经算过了。但真到了敲启动命令的时候,很多人还是会卡在一堆互相耦合的参数上——--max-num-seqs 调到 256 还是 512?--max-num-batched-tokens 到底跟 --max-model-len 什么关系?--gpu-memory-utilization 设 0.9 稳不稳?这些旋钮没有一个能孤立地拧,改一个常常要把另外两个一起跟着挪。

这一节我把生产环境里真正高频、真正值钱的那几个启动参数,挨个讲透:每个参数背后在算什么账、我自己的取值经验区间、什么场景下应该往哪边偏、以及我踩过哪些坑。读完之后你应该能拿着一份具体的硬件清单,直接敲出一个「不至于第一天就 OOM、也不至于明显浪费显存」的启动命令。

先建立一个心智模型:参数之间在抢什么

很多人调参是「拿一张参数表,挨个填值」,结果就是改了 max-num-seqs 发现还是 OOM,又去砍 max-model-len,砍完发现吞吐又掉了。这是因为没看到这几个参数其实在抢同一样东西:KV Cache 池

vLLM 启动时算显存预算的逻辑可以简化成一句话:gpu-memory-utilization × 单卡显存 − 权重 − 框架开销 = KV Cache 池。这个池子的总容量是固定的,而下面这些参数都在「分配」这个池子:

  • --max-model-len 决定单个请求最多能吃多少 KV(序列越长,单位成本越高)。
  • --max-num-seqs 决定池子里最多能塞几个请求。
  • --max-num-batched-tokens 决定一个 step 里同时算多少 token,影响 prefill 和 decode 的交错粒度。
```mermaid flowchart TD A["gpu-memory-utilization × 显存"] --> B[减去权重与开销] B --> C["= KV Cache 池 (固定预算)"] C --> D["max-model-len 决定单请求 KV 成本"] C --> E["max-num-seqs 决定并发上限"] C --> F["max-num-batched-tokens 决定 step 粒度"] D --> G["三者瓜分同一块池子, 改一个要核对另两个"] E --> G F --> G ```

记住这张图,后面所有参数的取值都是围绕「怎么把这块固定池子用满、又不爆」来的。

--tensor-parallel-size:第一个该拍板的参数

这是整套启动命令里唯一一个先于压测就要定死的参数,因为它决定了权重怎么切,一旦定下来后面很难改(换个 TP 值往往要重新转换/重新加载权重)。

我的取值原则:单节点上有几张卡就设几。8 卡机器就 --tensor-parallel-size 8,4 卡就 4。这不是为了「用满卡」,而是为了把权重尽量摊薄、单卡腾出更多显存给 KV Cache——MoE 模型权重巨大,TP 越大,单卡权重份额越小,KV 池越大。

坑 1:必须能整除注意力头数。这是最经典的低级错误。报错信息长这样:

ValueError: Total number of attention heads (96) must be divisible by tensor parallel size (8).

DeepSeek V4 这类模型注意力头数是固定的(比如 96 或 128),TP 只能取 1/2/3/4/6/8 这种能整除的值。TP=5 基本永远用不了,记住这个能少踩一次坑。

坑 2:TP 值和权重的 checkpoint 必须匹配--tensor-parallel-size 4 训练/转换出来的权重分片,用 --tensor-parallel-size 8 是加载不起来的——除非你的权重是 merged 后的单文件,让 vLLM 在加载时重新切分。生产里我吃过这个亏:从同事那拷了一份「TP=2」格式的权重,自己机器是 8 卡,启动直接报 shape mismatch。要么重新 merge 权重,要么老老实实 TP=2。

这个参数什么时候会失效:节点内 NVLink 带宽不够时,TP 越大反而越慢(通信开销压过计算收益)。我在一些 PCIe 互联的机器上测过,TP=4 比 TP=8 还快——因为 TP=8 的 allreduce 走 PCIe 慢得令人发指。所以**先确认你的机器是 NVLink 域**再放心 TP 开满,PCIe 机器反而要往下试。

--gpu-memory-utilization:最容易被「搏命」的参数

这个参数前面 3.2 已经反复提过,这里补一些调优现场才会遇到的细节。

取值经验:生产稳态我一般用 0.85~0.900.90 是我敢在真实流量上跑的上限,0.95 以上我只在压测里用,绝不上生产。为什么?因为线上流量有突发,某几个瞬间多个长请求同时进来,激活值峰值会顶上去——这时如果 utilization 已经把显存吃满,碎片一抖就 OOM。留 10%~15% 不是浪费,是给突发和碎片留的呼吸空间。

调这个参数的实操信号:观察两个指标。

  • 如果压测时 GPU 利用率(nvidia-smigpu_util,不是 vLLM 的 metrics)长期不到 60%,而 vllm:num_requests_waiting(排队数)在涨,说明 KV 池太小、请求被拒在门外——这时候要调高 utilization
  • 如果出现间歇性 OOM 或 preemption_count(抢占次数)飙升,说明池子边走边爆——这时候要调低 utilization 或砍 max-num-seqs

一个反直觉的坑:utilization 不是越高吞吐越高。我见过一个团队把它从 0.85 调到 0.95,吞吐反而掉了 15%——因为池子撑到极限后,vLLM 频繁触发抢占(preemption),把正在跑的请求 KV 换出又换进,吞吐崩了。池子的「健康度」比「绝对大小」更重要

--max-model-len:决定 KV 成本的旋钮

这个参数上限设多少,直接决定每个请求最多能吃多少 KV。它和 max-num-seqs 是一对跷跷板:模型长度越长,单个请求 KV 成本越高,能并发的请求数就越少。

我的取值法:看业务峰值,不看模型支持的上限。比如业务上最长的 prompt + 生成也就 6k token,那就设 --max-model-len 8192 留点余量,绝不要无脑设 32768 或 128k。我审过一个线上事故,就是因为某同学图省事设了 --max-model-len 32768,结果单请求 KV 成本翻 4 倍,并发从理论上的 200 多直接掉到 50,吞吐腰斩。

坑:这个值不能小于模型 config 里预设的 max_position_embeddings(部分版本会强制要求 --max-model-len 不超过该值,超过需要额外配置 RoPE scaling)。设大了浪费显存,设太小会被 vLLM 拒绝或运行时截断。稳妥做法:先查模型 config.json 的 max_position_embeddings,取 min(业务峰值×1.2, max_position_embeddings)

什么时候这个参数会失效:业务里有少量超长上下文请求(比如 RAG 召回大量 chunk 拼成 20k+ prompt),而你把 max-model-len 设小了,请求会被截断,业务侧表现为「答非所问」。所以设值前一定要采样真实流量的 prompt 长度分布,看 P99 而不是平均值——平均 2k 但 P99 是 12k 的业务,max-model-len 至少要顶到 14k。

--max-num-seqs:并发旋钮,但别拍脑袋

这是和「吞吐」最直接相关的参数:同时能排进调度器的最大请求数。很多人设这个值的依据是「我想要多少并发」,但这是错的——它的理论上限是显存算出来的,不是想要的

取值法(前面 3.2 的公式)

max_num_seqs_理论上限 ≈ KV池容量 / (单token KV × 平均序列长度)

具体到数字:假设 80GB 卡、utilization 留出 30GB 给 KV 池,DeepSeek V4 在 TP=8 下单 token KV 约 0.12MB,平均序列长度 2000 token,则 单请求 KV ≈ 0.12 × 2000 = 240MB上限 ≈ 30000 / 240 ≈ 125。所以这个场景 --max-num-seqs 128 就接近上限了,设 256 反而会触发频繁抢占。

我的实操:理论值算出来后,乘以 0.7~0.8 作为生产值。因为真实流量长度不均匀(有些请求特别长),留余量防止长请求把池子吃爆。压测时从理论值的 0.5 开始,逐步上调,盯着 preemption_count 和 P99 延迟,找到「preemption 开始抬头的临界点」再退回一档。

坑:这个值设太小,吞吐起不来;设太大,preemption 让延迟爆炸。我见过最离谱的案例:有人设了 --max-num-seqs 1024,结果线上 P99 延迟飙到 30 秒——vLLM 拼命接请求,又拼命抢占,整个系统在「接-抢-换」之间空转。max-num-seqs 不是越大越好,是「恰好够用」最好

--max-num-batched-tokens:被忽视的步长旋钮

这个参数决定一个 step(一次前向)里同时处理多少 token,和 --enable-chunked-prefill 配合使用。它不像前面几个那么显眼,但对首 token 延迟(TTFT)和长 prompt 场景影响巨大。

默认行为:不开 chunked prefill 时,这个值通常等于 max-model-len,意味着一个长 prompt 的 prefill 会一次性算完,期间所有 decode 请求被阻塞。

开 chunked prefill 后:长 prompt 被切成 chunk,每 chunk 算完让出空隙给 decode。--max-num-batched-tokens 就是控制每个 step 总 token 预算——比如设 8192,一个 step 里可能塞 4 个 decode(每个 1 token,共 4)+ 1 个 prefill chunk(8188 token)。

我的取值:典型值 --max-num-batched-tokens 8192,配合 --enable-chunked-prefill。如果你业务是「输入长、生成短」(比如长文档摘要),这个值可以适当调小到 4096,让 prefill chunk 更细,decode 响应更及时;如果是「输入短、生成长」(比如对话续写),可以放大到 16384,prefill 快速过完,把更多预算给 decode。

:这个值设太小(比如 1024),长 prompt 要切非常多次才能 prefill 完,TTFT 反而恶化;设太大又退化成「不开 chunked prefill」的效果。8k 是一个比较通用的起点,但一定要按你的业务长度分布再调一遍

--enable-chunked-prefill:高并发延迟优化几乎必开

这个开关前面 3.2 已经讲过原理,这里只补一句调优现场的判断:

我的主张:只要业务里有长 prompt(>2k token)且并发 >1,就开它。 唯一不开的场景是「所有 prompt 都很短(<512 token)且并发极低」,这时 prefill 本身就快,chunked 反而增加调度开销,得不偿失。

不开这个开关的代价是:一个 32k 的 prompt 进来,所有 decode 请求排队等它 prefill 完,TTFT P99 直接拉到几秒。开了之后,长 prompt 被切碎,decode 请求见缝插针,TTFT P99 能压回几百毫秒。这是高并发延迟优化里投入产出比最高的一个开关

--enforce-eager:调试时开,生产一定要关

这个开关禁用 CUDA Graph,强制 eager 模式。生产里默认必须关(不传该参数,或传 --enforce-eager False

为什么生产要关:CUDA Graph 把固定 shape 的 kernel 调用录制下来,省掉每次 launch 的开销。在 batch size 频繁变化的高并发场景,这个开销能积累到 10%~20% 的吞吐差距。我做过 A/B:同样配置,开 eager 吞吐 1200 tokens/s,关 eager(用 CUDA Graph)能到 1450 tokens/s。

什么时候开:只在两类场景开。第一,调试报错——CUDA Graph 会把真正的报错堆栈「吃掉」,给你一个含糊的 graph replay 失败。出这种诡异报错时,先开 eager 复现,能看到真实堆栈。第二,shape 极度不规则——如果你业务 batch size 从 1 到 1024 剧烈跳变,CUDA Graph 命中率低,录制开销反而拖累,这时开 eager 可能更快。但 99% 的生产场景都不属于这两类,默认关

--quantization 与 --dtype:精度和权重的配对

这两个参数必须配套设置,错配会导致加载失败或精度崩坏。

--dtype:控制计算精度,常用 auto(跟随权重)、bfloat16float16auto 是最稳的选择,让 vLLM 读权重自带的数据类型。:强制设 float16 跑原 BF16 权重,可能溢出(BF16 数值范围比 FP16 大),表现为生成乱码或 NaN。

--quantization:控制权重反量化的方式,必须和权重的量化格式匹配。常见取值:

  • awq:AWQ 量化权重,INT4 为主,显存省 4 倍,吞吐略损。
  • gptq:GPTQ 量化,INT4/INT8。
  • fp8:FP8 量化,A100/H100 支持,吞吐几乎不损,显存省一半——这是目前我最推荐的量化路线,DeepSeek V4 用 FP8 在 H100 上能跑出近乎 BF16 的吞吐,显存却只占一半。

--quantization 设错会报「quantization config mismatch」或加载后生成乱码。核对你拿到的权重到底是什么量化格式,再选对应值。生产里我定了一条规矩:每个权重量化版本要附带一个 quantization.txt 标注格式,部署脚本读这个文件自动传参,杜绝人为错配。

一份「调好」的启动命令长什么样

把上面的旋钮合在一起,针对 8 卡 H100 80G、DeepSeek V4 FP8 量化、业务平均序列 2000、峰值 8000 的场景,一份我会上生产的命令大概是:

python -m vllm.entrypoints.openai.api_server \ --model <DeepSeek-V4-FP8 路径> \ --tensor-parallel-size 8 \ --gpu-memory-utilization 0.90 \ --max-model-len 8192 \ --max-num-seqs 128 \ --max-num-batched-tokens 8192 \ --enable-chunked-prefill \ --quantization fp8 \ --dtype auto \ --swap-space 4

注意几个细节:--max-num-seqs 128 是按前面公式算出 125 上限取整;--quantization fp8 对应权重格式;--swap-space 4(GB)是 CPU 侧的换出空间,给 preemption 兜底,生产环境建议开 48GB,避免抢占时直接 OOM kill。

参数调优的实操流程

最后给一个我每次新部署都会走的调优 SOP,比一张参数表有用得多:

  1. 环境基线:先按第一章 1.3 的清单跑通最小可跑示例,确认环境无问题。
  2. 算账定基线:按 3.2 的公式算出 KV 池容量、单请求 KV 成本、max-num-seqs 理论上限。
  3. 保守起点max-num-seqs 取理论值的 0.5,gpu-memory-utilization 0.85,先跑通。
  4. 盯启动日志:核对 vLLM 打印的「KV 池 block 数」「实际并发槽位」是否符合预期。
  5. 逐步上调 + 压测:按 4.5 的压测方法论,逐步上调 max-num-seqs 和 utilization,盯 preemption_count 和 P99,找到拐点。
  6. 回退一档上生产:拐点值乘 0.8 作为生产值,留出突发余量。

这套流程跑下来,你的启动参数就不是「拍脑袋填的表」,而是「按硬件和业务算出来的」。参数表里的数字会随版本和模型变,但这套算账 + 压测 + 回退的思路不会变——这才是这节真正想让你带走的东西。


作者与出处
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 运行报错的菜鸟的小龙虾 转发
评论区 (0)
U