☰
vLLM Serve 参数调优实战:从显存分配到并发控制全解析
2026/10/19 12:49:09 网站建设 项目流程

用 vLLM 做线上推理服务已经不是新鲜事了,但大部分人第一次跑vllm serve都和我一样:抄一条启动命令,只改了--model和--port,然后祈祷它别炸。头一两个请求确实没问题,并发一上来或者上下文一长,显存直接报警,日志里全是 OOM 和超时。回头翻文档,发现 vLLM 的每个参数背后都是显存分配、调度策略、批处理机制之间的权衡,调参不是对着默认值猜,而是要弄明白它到底在管什么。这篇就把我实际部署中逐项验证过的参数讲清楚,包括它们的原理、联动关系,以及我踩过的坑。

1. 为什么 vLLM Serve 的一堆参数值得逐项研究

1.1 这套服务解决的核心问题:预填充与解码分离

vLLM 和传统推理框架最大的区别,是把生成过程拆成了 prefill(预填充)和 decode(解码)两个阶段,再用 PagedAttention 管理 KV Cache。这个拆法直接决定了它的参数设计逻辑。prefill 阶段要把整段 prompt 的 KV 一次性算出来,计算密集但只跑一次;decode 阶段每生成一个 token 就要读取之前所有 KV,访存密集且反复执行。两个阶段对显存、算力、调度的需求完全不同,所以参数才会既管显存上限,又管批次大小,还要管两者怎么分配。

我见过很多人在调参时只看单个参数的含义,结果调出来的配置互相打架。比如把--max-num-batched-tokens调得很大,同时又期望短文本请求能高频返回,实际上 batch 预算被长 prefill 吃掉了,短请求排队越来越长。这就是不理解参数联动导致的典型问题。

1.2 参数不只是开关,而是调度器与显存管理的出口

vLLM 的调度器会按照参数约束,在每个 step 里决定把哪些请求放进 batch、给它们分配多少 token 预算、KV Cache 还够不够用。--max-model-len限制的是单条序列的长度上限,--gpu-memory-utilization限制的是显存使用的天花板,--max-num-seqs限制的是并发序列数量,--max-num-batched-tokens限制的是单次迭代处理的 token 总量。这四个参数放在一起,等于给调度器画了一个矩形区域,它只能在区域内做优化。

所以读参数文档的时候,我建议按“显存类、并发类、保护类、调试类”去分,不要按字母序一条条看。这篇文章也按这个思路展开。

2. 启动前的三件事:模型、对外名称与端口

2.1--model:本地路径优先于仓库 ID

--model既可以传远程仓库的模型 ID,也可以传本地路径。我在实际部署时强烈建议先下载到本地,再用绝对路径启动。原因有几个:线上机器经常没有外网权限;每次启动都去仓库检查版本会拖慢拉起时间;而且仓库里的模型文件可能在你没注意到的时候被替换,造成线上行为不一致。

本地路径还有一个隐藏好处:日志里能直接看到模型权重是从哪个文件加载的,排查问题更快。如果非要用仓库 ID,建议配合--trust-remote-code。这个参数的作用是允许加载模型仓库中的自定义代码文件。很多模型为了支持自定义分词器或结构,仓库里带上了一些脚本,默认情况下 vLLM 会拒绝执行这类远程代码,直接报错。我第一次用某个带自定义代码的模型时就被挡了一下,日志提示需要打开trust_remote_code,加上之后一切正常。

这里多说一句,--trust-remote-code本质上是允许执行远程代码,如果你从不可信来源拉模型,这个开关会带来安全风险。生产环境尽量用经过审计的本地权重,不要什么模型都直接开。

2.2--served-model-name:客户端不认识你的本地路径

这个参数容易被忽略,但它决定了客户端请求里model字段填什么。vLLM 对外暴露的是兼容 chat 接口风格的 HTTP 服务,客户端发起请求时会在 JSON 里带上模型名。如果你启动命令写的是本地路径/data/models/xxx-7b-instruct,客户端还傻乎乎地写model=xxx-7b-instruct,服务端直接返回 model not found。

解决办法就是--served-model-name。你想对外宣称这个服务叫什么,就把它设成什么。比如后端换了一个更强的模型,但客户端不想改配置,那你仍然可以让对外名称保持一致,无缝切换。生产环境我习惯把对外名称和内部路径彻底分离,避免暴露服务器目录结构。

启动之后可以用 curl 快速验证:

curl http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "my-chat", "messages": [{"role": "user", "content": "你好"}] }'

只要返回正常,说明模型、服务名、端口这一层就通了。

2.3 端口与日志:--port、--disable-log-requests

--port默认值是 8000,如果机器上已经有什么服务占了这个端口,启动会报错。这个比较简单。值得单独提的是--disable-log-requests,生产环境务必加上它。默认情况下 vLLM 会把每个请求的完整 prompt 打到日志里,流量一上来,日志文件膨胀极快,而且可能记录下用户的敏感业务数据。我之前线上服务开了一周没关这个,结果日志文件几个 GB,排查问题的时候才发现里面全是用户输入,最后花了不少时间清理。

如果你要调试请求内容,可以用--log-level debug临时开着看,调完马上关掉。别长期开。

3. 显存参数的真实关系:--max-model-len、--gpu-memory-utilization与 KV Cache

3.1 KV Cache 是怎么算出来的

显存分配的核心是 KV Cache。很多人以为 KV Cache 只占一点点显存,实际上长上下文场景下它可能比模型权重还大。KV Cache 的大小和模型结构、序列长度、并发数直接相关,计算公式大概是:

KV Cache 总大小约等于 2 × 层数 × KV 头数 × 头维度 × 序列总长度 × 单元素字节数。

那个 2 是因为每个 token 要存 Key 和 Value 两份。我用一个常见规模的模型举例:32 层、8 个 KV 头、每个头 128 维、FP16 精度,那么单条 4096 token 的序列,KV Cache 大约是 2 × 32 × 8 × 128 × 4096 × 2 字节,算下来约 512MB。如果同时处理 64 条这样的请求,光是 KV Cache 就要 32GB 显存。这时候你大概就明白,为什么上下文一长、并发一高,显存立刻见底。

vLLM 启动时会在日志里打印 KV Cache 的分配情况,比如KV cache size: ...,这是判断参数是否合理的第一个信号。不要等 OOM 了才去翻日志。

3.2--max-model-len的截断逻辑

--max-model-len限定了单条 prompt 加上生成内容的最大 token 数。默认值在不同版本里不一样,有的版本只有 2048 或 4096,对长文本场景完全不够。但调大它要谨慎,因为 KV Cache 是和它线性挂钩的。比如同一个模型,把--max-model-len从 4096 调到 32768,同等并发下 KV Cache 的峰值占用直接翻 8 倍。

还有一个很多人踩过的坑:vLLM 对超过--max-model-len的请求不是悄悄截断,而是直接返回 400 错误。我之前给一个内部系统接长文档问答,用户上传的文本经常超过设定值,表现为所有超长请求全部失败,日志里报length exceeds limit。这不是 bug,是设计如此。所以要么在客户端提前做截断,要么把这个参数调到你业务能接受的上限。

3.3--gpu-memory-utilization不要无脑拉高

--gpu-memory-utilization默认是 0.90,意思是最多用显存的 90% 来放模型权重和 KV Cache,剩余留给 CUDA context、临时 buffer 等。我看到一些同学为了多塞点 KV Cache,把它调到 0.98,结果启动没问题,跑了一会儿反而频繁 OOM。原因是剩余显存太少,CUDA 运行时的临时分配没有空间,直接翻车。

我自己的习惯是给一张卡留出 8% 到 15% 的余量。模型越大越要保守,因为大模型的中间 buffer 也更占空间。调这个参数时还要留意机器上有没有其他进程吃显存,比如偶发的监控脚本、另一个小服务,都会挤压你的可用空间。

如果你不确定当前配置能撑多大并发,可以用一个小技巧:先调低并发数跑一轮测试,再逐步往上加,看 vLLM 日志里 KV Cache 使用率和显存余量的变化,找到临界点。

3.4 多卡怎么分:--tensor-parallel-size不是越大越快

--tensor-parallel-size表示用几张卡做张量并行。单卡放不下的模型必须用它,但小模型强行多卡反而变慢。原因很简单:张量并行要把中间结果在卡之间来回传递,多卡之间的通信开销可能超过多卡带来的计算收益。我曾经把一个 7B 模型用两张卡跑 TP2,吞吐不升反降,P99 延迟明显变差。

所以我的建议是:单卡能放下就不开 TP;放不下再开,而且优先考虑换更大的单卡,实在不行再上多卡。多卡环境下要注意所有卡的显存型号最好一致,否则慢卡拖累整体。

3.5--enforce-eager:调试 OOM 时的开关

--enforce-eager的作用是关闭 CUDA graph 加速。CUDA graph 把模型执行图预先捕获,减少 kernel 启动开销,但会额外占用显存。开启这个参数后,速度会明显下降,但显存占用会降低,而且很多难排查的 CUDA 报错会变成普通报错。

它不是生产参数,是诊断参数。当你怀疑 OOM 和 CUDA graph 有关时,在同样配置下加--enforce-eager跑一把,如果不再 OOM,基本可以确认是 graph 占的显存导致的。定位完就关掉,回到正常模式。

4. 并发与吞吐:--max-num-seqs与--max-num-batched-tokens

4.1 连续批处理和 chunked prefill

vLLM 提高吞吐的核心是 continuous batching:一个请求 decode 完一个 token,就立刻从等待队列里拉一个新请求补进来,不浪费算力。而 chunked prefill 是把长 prompt 的 prefill 计算切成小段,穿插到 decode 的间隙里执行,避免长预填充请求把短请求全部堵死。

这两个机制决定了并发参数的玩法。传统框架里 batch size 是固定值,vLLM 里则是每个 step 动态决定,--max-num-seqs和--max-num-batched-tokens就是它的边界。

4.2--max-num-batched-tokens的预算分配

--max-num-batched-tokens是每个 step 最多能处理的 token 总数,它同时限制了 prefill 和 decode 的总量。默认值一般近 8192,对绝大多数场景够用,但如果你的请求普遍很长,这个值要谨慎调。

举个例子方便理解。假设预算固定 8192,一条长 prompt 的 prefill 就需要 8192 个 token,它单独就能把整个步长占满。加上 chunked prefill 后,这 8192 可以被切成好几段,穿插处理别的请求。但切得太碎,调度开销会上升。实际调参时,如果线上短请求占比高,稍微调大这个值有助于提高吞吐;如果长文本占比高,反而要控制这个值,给 decode 留出空间。

4.3--max-num-seqs:并发上限不是越大越好

--max-num-seqs默认 256,表示调度器最多同时跟踪 256 个序列。很多人一看默认 256,觉得并发放到 256 很合理,结果一压测就 OOM。别忘了每个 seq 都对应一段 KV Cache,256 条长序列的 KV Cache 总量非常恐怖。

实际部署时,我先根据显存预算算出 KV Cache 能撑多少条 max sequence,再往回推这个值。如果是短会话场景,比如客服对话,一条请求通常就几百 token,并发 128 甚至 256 都行。如果是长文档场景,比如法律文书摘要,一条请求可能几千 token,我就把--max-num-seqs压到 32 甚至 16,否则显存瞬间崩掉。

网上很多分享喜欢把这两个值一起调大,我反而建议一起调小再逐步试。vLLM 不会因为你调小了并发就觉得你不行,稳定比激进重要。

5. 前缀缓存这味药:--enable-prefix-caching

5.1 缓存的粒度是 block,不是整段 prompt

--enable-prefix-caching开启后,vLLM 会把 prompt 按固定大小的 block 切分,每个 block 计算一个哈希,如果新的请求前缀和之前某个 block 相同,就直接复用对应的 KV Cache,跳过重复的 prefill 计算。注意它是 block 级别的复用,不是整段请求级别的缓存。这意味着即使有一条长 prompt 已经算过,只要下一个请求只有前 32 个 token 相同,后面的内容全变了,那能复用的也只是前 32 个 token 对应的缓存,收益没有想象中大。

这个参数最合适的场景是 system prompt 固定、开场白固定、多轮对话前缀稳定的业务。我接的一个问答服务,system prompt 很长且从不变化,开启前缀缓存后,首 token 延迟和 prefill 耗时降了差不多 60%。如果你的每个请求前缀都不一样,或者 prompt 里混了随机时间戳、随机用户信息,缓存命中率会很低,开了和没开差不多。

5.2 缓存命中看不见,怎么验证

验证缓存有没有生效,不要只看首 token 延迟。vLLM 的日志里会输出 prefix cache hit rate,或者通过 Prometheus 指标看vllm:prefix_cache_hit_rate。我习惯在压测时同时记录这个命中率。低于 30% 就别指望这个参数出效果了。

还有一个细节:前缀缓存占用的也是 KV Cache 那部分显存。如果你把显存卡得很满,缓存空间不足,命中率也会下降,因为旧的缓存还没来得及被命中就被驱逐了。所以开启这个参数时,要给 KV Cache 留出额外余量。我见过有人开了前缀缓存后显存反而更紧,因为缓存抢占了一部分原本用于正常解码的空间。

5.3 新版默认值与版本差异

从某几个版本开始,--enable-prefix-caching已经默认开启了。不同版本对默认值、日志输出的参数名存在差异,所以不要照抄网上一条命令就上生产。先跑vllm serve --help看你这个版本的参数列表,确认它是否还在配置里,再决定用不用显式写。

6. 限流、保护与容错:超时、连接堆积、请求上限

6.1--limit-max-requests与连接复用

--limit-max-requests限制的是单个 HTTP 连接上最多能处理的请求数。这个参数最初是为了解决连接复用导致的负载堆积问题。客户端 SDK 通常会保持长连接,一次连接上跑完几千个请求,如果服务端不做限制,某个异常连接可能一直占据资源。设置一个上限后,达到次数服务端会主动断开连接,强制客户端重新建立连接。

一般场景保持默认就行。但如果你用压测工具跑长连接,可能会频繁看到连接被重置,这不是故障,是触发了这个保护机制。压测的时候可以把值调大,避免干扰结果。

6.2--request-timeout和长任务的关系

--request-timeout限制单个请求从开始到完成的最大时长,默认值很大,一般不会触发。但如果你的业务里有超长文本生成,或者模型推理速度因为硬件原因特别慢,这个参数设置的太小会误杀正常请求。

我之前在处理整本书级的长文本摘要时,单次请求要跑十几分钟,客户端早就超时了,服务端还在埋头算。后来把超时调到和业务预期一致,并且客户端也配合调整了等待时间,才真正跑通。调这个参数的原则很简单:比你最慢的合法请求再长 30% 以上。

6.3--max-parallel-loading-requests:冷启动阶段的并发保护

这个参数控制的是同一时刻进入加载流程的并发请求数量,主要影响冷启动和突发流量。之前有一次我把并发压测直接打到了新高,结果一批请求同时触发了模型加载和 KV Cache 分配,显存瞬间被拉满,整个服务抖动了好几分钟。调低这个参数后,冷启动阶段会变得平缓,代价是刚上线时第一批请求完成变慢。

生产环境我不建议把这个参数调得很大,除非你明确知道模型加载的峰值开销可控。它的作用更像是给调度器一个缓冲,避免突发流量把服务冲垮。

6.4--swap-space:应急的 CPU 换页

--swap-space允许 KV Cache 溢出时换到 CPU 内存。听起来像个保险,实际是最后手段。CPU 换页的延迟比 GPU 显存访问高几个数量级,一旦触发 swap,P99 延迟会急剧恶化。它适合那种“偶尔峰值超过一点但不想扩容”的场景,不适合作为常态。

我把这个参数当救急用,默认值 4GB 一般不动。如果业务经常触发 swap,说明你的并发配置或显存配置本来就不合理,应该从源头调。

7. 从参数表到线上配置:一份参考模板与调参流程

7.1 不同业务形态的推荐配置

参数没有万能模板,只有合适你业务约束的配置。我按两类典型场景给个参考。

短会话、高并发场景(比如在线客服助手):

vllm serve /data/models/xxx-7b-instruct \ --served-model-name customer-service \ --port 8000 \ --max-model-len 4096 \ --gpu-memory-utilization 0.88 \ --max-num-seqs 128 \ --max-num-batched-tokens 8192 \ --enable-prefix-caching \ --dtype float16 \ --disable-log-requests

长文本、低并发场景(比如文档分析):

vllm serve /data/models/xxx-7b-instruct \ --served-model-name doc-agent \ --port 8000 \ --max-model-len 32768 \ --gpu-memory-utilization 0.86 \ --max-num-seqs 16 \ --max-num-batched-tokens 4096 \ --request-timeout 1800 \ --disable-log-requests

注意长文本场景我把--max-num-batched-tokens调得反而更小,因为单条请求的 prefill 已经很大,batch 预算再多也只会让调度器同时处理更多长请求,显存风险陡增。这里宁可让并发看起来“保守”,也要保证单请求稳定。

7.2 观测指标与逐步调参的顺序

启动 vLLM 后,先看启动日志中的 KV Cache 分配情况和 GPU 显存占用,确认基线。然后做一轮小规模压测,记录吞吐(tokens/s)、首 token 延迟、P99 延迟。接下来按照“显存余量 → KV Cache 使用率 → 排队长度 → P99 延迟”的顺序依次调整,一次只动一个参数,方便定位。

如果排队长度持续增长,说明并发配置低于业务到达率。先看--max-num-seqs是否还有余量,再看--max-num-batched-tokens是不是被占满了。如果两个都还有余量但排队还是很高,问题往往出在上游限流或网络,不在 vLLM。

7.3 验证方式:压测时该看什么

压测的时候别只盯着 QPS。vLLM 场景下更关键的指标是吞吐(单位时间的生成 token 数)和首 token 延迟。有些配置把并发调高后,QPS 看着上去了,但每个请求的生成速度明显变慢,整体效率反而下降。我用这个判断标准:在 P99 延迟能接受的前提下,尽量提高生成吞吐;一旦 P99 开始明显劣化,立刻回退到上一个参数组合。

8. 高频问题排查:从报错反推参数

8.1 OOM 类错误

显存 OOM 是最常见的。先确认是不是--max-model-len设太大,再查--max-num-seqs是不是太高,最后看--gpu-memory-utilization余量是否足够。一个很实用的排查顺序是:把--max-num-seqs降到 8,--enforce-eager打开,如果问题消失,就按增量排查,逐项加回去。

8.2 请求报长度超限

日志或响应里出现 length 相关错误,基本就是--max-model-len低于请求 token 数。这个不是 bug,检查客户端有没有对输入做截断,或者直接把服务端参数调大到业务匹配值。

8.3 请求堆积但 GPU 利用率很低

这是个反直觉的场景。如果日志显示请求积压,但 GPU 利用率上不去,常见原因是 prefill 太大导致 decode 被长时间占用,或者 batch 里混入了一条超长请求,把整个 step 拖住了。解决思路是打开 chunked prefill 相关配置(新版默认开),以及适当降低--max-num-batched-tokens,把每步的预算让给更多短请求。

8.4 启动很慢或加载阶段就崩

启动阶段崩溃大多集中在权重加载和显存预分配。先确认显存是否真的够放模型权重加 KV Cache 预留。如果机器上已经跑着其它服务,就会撞车。关闭--enforce-eager再启动如果失败,多半是显存不足;开了能启动,说明 CUDA graph 额外占的显存是导火索。

8.5 通过启动日志自检

这是我调完参数后必做的一步:把 vLLM 启动日志前 50 行从头看一遍。它会明确写出模型加载了多少参数、分配了多少显存、KV Cache 上限、调度器配置。很多问题和预期不一致,在第一眼就能发现,不用等到压测出问题才回头查。

vLLM Serve 的参数调优没有银弹,它本质上是和显存、算力、延迟目标做一场三方权衡。每次调参我都建议先明确你的约束条件是什么,是显存紧张还是延迟敏感,是长文本多还是短请求多,然后顺着本文这几条主线去动参数。我自己在多次上线中反复验证下来,稳定优先、每次只改一个变量、把启动日志读透,这三件事比任何参数模板都好用。希望这份参数详解能帮你少走一点我第一次配置时走过的弯路。

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

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

立即咨询