这一篇对应视频 05:“vLLM 参数配置、显存分析与性能调优 max_num_batched_tokens”(BV1QnSFBkEZU)。
这期的核心不是“教你把服务跑起来”,而是给你一个可以复用的调参心智模型:
- vLLM 的显存到底被谁吃掉(权重 / KV cache / peak activation / CUDA Graph / 杂项)。
max_model_len、max_num_seqs、max_num_batched_tokens之间到底是谁在限制并发与吞吐。- 为什么
max_num_batched_tokens既影响“吞吐”,又会反过来影响“能留给 KV cache 的空间”(因为它参与了 profile 的 peak activation 测量)。
系列导航:
配套仓库(你本地已下载):wdkns/modern_genai_bilibili,本文主要对齐两份笔记:
agentic_rl/infra/inference/vllm_sglang.ipynb(本期视频几乎就是这个 notebook 的讲解版)agentic_rl/verl/vllm-rollout.ipynb(把这些参数放回 RL rollout 场景:prefill+decode 混跑的调度直觉)
0. 一句结论(先给你调参抓手)
vLLM 调参可以简化成一个闭环:
- 先定
max_model_len(你业务上允许的单请求最大上下文,越大越吃 KV)。 - 设
gpu_memory_utilization(给 vLLM 一个“预算”,但要记住 CUDA Graph 显存不算在预算里)。 - 从日志里读出:weights/peak_activation/non_torch/cudagraph/kv_cache 的分账。
- 再调:
max_num_seqs:你希望系统“最多并发多少条序列”(逻辑上限)。max_num_batched_tokens:每一步前向“最多塞多少 token”(决定 decode 并行度,也决定 chunked prefill 的块大小,也决定 profile 的峰值激活)。
- 以日志里的
GPU KV cache usage为核心指标:- 如果 KV 利用率长期很低,且你吞吐不高,通常说明 有效 batch 太小:提高
max_num_seqs或max_num_batched_tokens。 - 如果 OOM 或 KV cache 太小导致并发上不去:降低
max_model_len或降低max_num_batched_tokens(因为它会抬高 peak activation,从 KV 预算里“扣钱”)。
- 如果 KV 利用率长期很低,且你吞吐不高,通常说明 有效 batch 太小:提高
1. vLLM 的两个推理场景:offline batch vs online serve
视频先把推理场景分成两类(这点很重要,因为你调参目标不一样):
- offline batch generation
- 目标是吞吐(tokens/s),对单条请求的延迟不敏感。
- online 部署(OpenAI API compatible server)
- 同时关心吞吐和延迟(特别是 TTFT/ITL)。
另外还有一个“对 Agentic RL 更关键”的第三类场景:rollout generation(在 RLHF / agentic RL 的 on-policy 训练里作为生成引擎),这也是 verl/openrlhf 为什么会强依赖 vLLM 的原因:用 transformers 做 rollout 太慢。
1.1 指标口径:tokens/s、TTFT、ITL 你到底该优化哪个
同样是“推理性能”,不同场景的指标口径完全不同。把它说清楚,你后面的调参动作就不会互相打架:
- 吞吐(throughput,tokens/s)
- 你希望 GPU 每秒吐出更多 token。
max_num_seqs和max_num_batched_tokens往往是主要旋钮:更大 batch 通常更高吞吐。
- TTFT(Time To First Token)
- 从请求进来到首 token 返回的时间,主要被 prefill(处理 prompt)决定。
- 长 prompt + 大 batch + 不合理的 prefill 调度,会显著拖慢 TTFT。
- ITL(Inter-Token Latency)
- streaming 输出时,相邻两个 token 的间隔,主要被 decode 的 step 装配与 batch 大小影响。
- 你把
max_num_batched_tokens拉得很大,吞吐可能上去,但每一步要处理的 token 也更多,ITL 往往会变差。
最常见的坑是:你以为你在“优化性能”,其实你只是在把“吞吐”和“延迟”互相兑换。线上如果用户关心交互体验,你应该优先保证 TTFT/ITL 的稳定,再追 tokens/s;而 RL rollout 更偏吞吐优先,但也要避免 tail latency 把整个 pipeline 卡死。
1.5 一个能跑起来的 vllm serve 命令模板(建议你从这里开始改)
下面这条命令基本覆盖了视频里提到的关键参数;你先跑通,再按日志调。
1 | export VLLM_LOGGING_LEVEL=DEBUG |
几个你一定要建立直觉的参数(对齐视频讲解):
--max-model-len:单请求(prompt+output)最大长度。越大越吃 KV cache,并发会被拉低。--gpu-memory-utilization:vLLM 的显存预算系数(权重+KV cache,不包含 CUDA Graph)。--max-num-seqs:并发序列逻辑上限,真正能跑多少取决于 KV cache 的物理容量。--max-num-batched-tokens:每一步 forward 的 token 预算,也是 chunked prefill 的“切块上限”,还是 profile 里的 dummy 长度(会影响 peak activation)。--enable-prefix-caching:跨请求共享 prompt 前缀(对 agent 场景很常用:system prompt、工具 schema、固定模板)。--enforce-eager(可选):关闭 CUDA Graph(显存紧张或 graph capture OOM 时救急,但性能会掉一些)。
2. 先把 3 个关键参数讲清楚:max_model_len / max_num_seqs / max_num_batched_tokens
下面这三者一定要一起理解:
2.1 max_model_len:单请求的最大 token 总长
- 定义:单条请求允许的最大 token 数(prompt + output)。
- 直觉:它越大,每条请求的 KV cache 上限越大,并发越容易被“拉低”。
2.2 max_num_seqs:并发序列的逻辑上限
- 定义:一次最多允许多少条活跃序列。
- 直觉:它不是“物理保证能同时跑这么多条”,只是调度器允许的上限。真正能跑多少,还要看你 KV cache 的物理容量。
2.3 max_num_batched_tokens:每一步前向的 token 预算
这是本期重点:
- 定义:一次 GPU forward(一个 step)里,所有请求加起来最多处理多少 token(单位是 token,不是请求数)。
- 默认值通常是 2048。
- 关键联动:
- 在 decode 阶段:每条序列每步只生成 1 token,所以
tokens_in_step ≈ num_decode_seqs。 - 在 prefill 阶段:prompt token 可以 batch 处理;而且 vLLM 默认启用 chunked prefill,会把长 prompt 切成“每块不超过
max_num_batched_tokens”的片段。
- 在 decode 阶段:每条序列每步只生成 1 token,所以
一句话:max_num_batched_tokens 同时是
- decode 的并行度上限(你能一口气并行多少条 decode)
- prefill 的 chunk 大小上限(长 prompt 每步最多灌多少 token 进 KV)
- vLLM 启动时 profile 的 dummy 输入长度(决定 peak activation memory)
3. 为什么说“读日志”是 vLLM 调参的前置:打开 DEBUG
视频强调要先打开 debug 日志,否则你几乎是盲调:
1 | export VLLM_LOGGING_LEVEL=DEBUG |
你需要从日志里读到这些量:
- Prompt throughput / Generation throughput(prefill 与 decode 的 tokens/s)
GPU KV cache usage(paged attention 下“页表用了多少页”,是最关键的利用率指标)- Prefix cache hit rate(如果你开了
--enable-prefix-caching) - 启动时的 memory profiling:weights / peak activation / non-torch / cudagraph / kv cache 的分账
4. 显存分账:vLLM 到底把你的 24GB 显存怎么花掉的
这段建议你直接对齐仓库笔记里的日志解析:
agentic_rl/infra/inference/vllm_sglang.ipynb(含原始 log 与逐项解释)
一个典型日志长这样(以 24GB 卡为例,数字会随模型与参数变化):
1 | Free memory on device (23.2/23.65 GiB) on startup. |
你应该建立的“账本公式”是:
$$\text{KVCacheBudget} \approx (\text{TotalVRAM}\times u) - \text{Weights} - \text{PeakActivation} - \text{NonTorch}$$
其中 $u$ 是 --gpu-memory-utilization。
重要细节(视频里专门强调):CUDA Graph 显存不计入 gpu_memory_utilization 的预算里。所以你把 $u$ 拉到很接近 1 可能会在 graph capture 时 OOM。
如果你显存极紧,可以用 --enforce-eager 关闭 CUDA Graph(牺牲部分性能换可用性)。
5. KV cache 能“挂住”多少 token:从 GiB 到 tokens 的换算
vLLM 启动时通常会直接给你打印:
1 | Available KV cache memory: 5.54 GiB |
但真正有价值的是你要会“反推为什么是这个数”,因为这决定你的最大并发。
5.1 单 token 的 KV cache 大小(最常用的工程估算)
对大多数 decoder-only 模型,KV cache 的单 token 成本可以写成:
$$\text{Size}_{token} = 2 \times L \times N_{kv} \times D_{head} \times P_{byte}$$
- 2:因为 K 和 V 各一份
- $L$:层数(num_hidden_layers)
- $N_{kv}$:KV heads 数(注意 GQA/MQA 下它小于 attention heads)
- $D_{head}$:head_dim
- $P_{byte}$:dtype 字节数(FP16/BF16 通常是 2)
仓库里给了一个 Qwen2.5-7B 的例子(可直接对齐 config):
1 | 2 * 28 * 4 * 128 * 2 = 57344 bytes/token |
也就是每个 token 约 56 KB(这就是为什么长上下文一上来并发就掉得很快)。
5.2 最大并发(物理上限)怎么估
最粗但很实用的估算是:
$$\text{MaxConcurrency} \approx \frac{\text{KVCacheTokens}}{\texttt{max_model_len}}$$
仓库笔记里给了两个对比(同样的 KV cache tokens,不同 max_model_len):
1 | # --max-model-len 32768 |
注意:这里的 “x” 是指“能同时容纳多少条满长度请求”。真实线上请求通常更短,所以实际并发可以更高,但这个估算对你理解上限很有用。
5.3 Paged Attention:为什么日志里叫 “GPU KV cache usage”(像在看“页表利用率”)
这也是 vLLM 和传统推理实现最不一样的地方之一:KV cache 的显存管理方式。
传统实现更像“每条序列分一整块连续显存”。问题是:
- 并发一高、序列长度不一,显存容易碎片化
- 你可能“总量够但找不到一整块连续空间”,也会 OOM
vLLM 的做法更像操作系统的分页内存:
- 把 KV cache 切成很多固定大小的小块(block/page)
- 用一个 block table(页表)记录“逻辑上连续的上下文”对应到“物理上离散的 block”
- 所以显存可以像分页一样动态分配、回收,碎片问题大幅缓解
因此日志里的 GPU KV cache usage 更像:
- “现在 KV cache 的 page 用了多少”
- 你可以把它当成一个非常关键的利用率指标,用来判断当前并发/有效 batch 有没有把 GPU 喂饱
6. max_num_batched_tokens 的本质:一步 forward 的 token 预算 + prefill chunk 大小
这一段你可以把视频的“prefill vs decode”理解和 agentic_rl/infra/inference/vllm_sglang.ipynb 的例子合在一起看。
6.1 decode 阶段:tokens_in_step ≈ num_decode_seqs
在纯 decode 阶段:
- 每条活跃序列每步只产 1 个 token
- 所以 step 内 token 数约等于并发序列数
结论:
- 如果你
max_num_batched_tokens=2048,但你只有几十条并发,那 token 预算根本打不到上限。 - 此时提升吞吐的第一杠杆往往是 提高
max_num_seqs(让更多请求一起 decode)。
6.2 prefill(chunked prefill)阶段:token 预算被用来“切长 prompt”
chunked prefill 的直觉是:避免一个超长 prompt 一次性占满 prefill,拖垮其他请求的 ITL。
调度器在一个 step 内要满足:
$$\sum(\text{prefill_chunk_tokens}) + \sum(\text{decode_tokens}) \le \text{max_num_batched_tokens}$$
视频举了一个很清晰的例子(这也是你在线上最常见的混跑形态):
- 设
max_num_batched_tokens = 8192 - 正在 decode 的并发数
N_decode = 100- decode 占用 100 个 token 预算
- 则本步还剩 prefill 预算:
prefill_budget = 8192 - 100 = 8092
- 如果队列里插入一个 20k 的长 prompt,本步就会切出 8092 token 做一次 prefill chunk。
这也是为什么你会在日志里看到:
1 | Chunked prefill is enabled with max_num_batched_tokens=8192 |
6.3 它还会影响 peak activation(很多人忽略的“反直觉”点)
vLLM 启动时会做一次 memory profiling:
- 用一批 dummy token(长度 =
max_num_batched_tokens)跑一次前向 - 记录这次前向的 peak activation memory
- 然后把它当成运行时预留,从你的 budget 里扣掉
所以:你把 max_num_batched_tokens 调得很大,可能导致 peak activation 变大,反过来挤压 KV cache 的预算,从而降低最大并发。
这就是为什么它不是“无脑越大越好”的参数。
7. Prefix caching vs KV cache:别把两个“缓存”混了
视频专门强调了一次区别(这个对 agent 场景非常关键):
- KV cache:单请求内部自回归过程中缓存 K/V,用于 decode 加速。
- Prefix caching:prefill 阶段跨请求共享(适合大量共享系统提示词/工具 schema 的在线服务)。
你开了 --enable-prefix-caching 后,日志里会出现 prefix cache hit rate,一定要看这个指标,否则你不知道自己开了有没有收益。
8. 调参 SOP(我建议你按这个顺序来)
- 先把模型跑起来(别一上来就追“极限吞吐”)
- 打开 debug:
export VLLM_LOGGING_LEVEL=DEBUG - 读清楚启动时分账(weights / peak activation / non-torch / cudagraph / kv cache)
- 先定
max_model_len- 你允许的最大上下文是多少(这决定物理并发的上限)
- 再调
max_num_seqs- 如果 decode 阶段 token budget 没打满,通常先加它
- 再调
max_num_batched_tokens- 如果
GPU KV cache usage低,且你并发也不高,说明有效 batch 小,可以加它 - 但每次加都要看:peak activation 是否变大、KV cache tokens 是否被挤小
- 如果
- 如果 graph capture OOM 或显存太紧:
- 临时
--enforce-eager(关 CUDA Graph)救急
- 临时
- 面向 RL rollout(verl)时,把同一套直觉迁移过去
- 参考:
agentic_rl/verl/vllm-rollout.ipynb
- 参考:
9. 把它放回 Agentic RL:为什么 rollout 更吃这些参数
视频开头提到:vLLM 不只是 serving,还会用于 RL4LLM 的 rollout generation。原因很直接:
- rollout 是 on-policy 的:你要不停生成新样本
- 现在的 reasoning 模型 decode 很长,生成成本极高
- rollout 吞吐几乎决定了你整个训练 pipeline 的 wall-time
所以当你后面做 agentic RL(deep research / tool-use / long-CoT)时:
- 你可以不微调 LLM 本体,也要把 rollout 引擎调到合理吞吐
max_num_batched_tokens/chunked prefill/kv cache budget 对“长上下文 + 多并发”的场景非常关键
9.1 rollout 场景的额外成本:你可能需要 logprobs(不是只要文本)
很多人第一次把 vLLM 接进 RL 框架,会忽略一个事实:训练时你往往不只要生成的文本,还要概率信息(至少是 logprob 差异),用来算:
- PPO/GRPO 的 ratio:
r = exp(logp_new - logp_old) - KL(对 ref 或对 old 的约束/监控):无论是 sampled 近似还是 full-distribution
这会带来两个工程后果:
- 你需要额外的前向计算(有时甚至是第二个模型的前向),吞吐会比“只生成文本”更低。
- 你需要更严格的 token 边界与 mask(prompt/response 切分、EOS 之后不计入等),否则 KL/ratio 统计会非常诡异,导致你以为是“算法不稳”,其实是“指标算错”。
所以在 rollout 里调 vLLM,建议你明确自己到底要返回什么:
- 只要文本:优先把 tokens/s 打满
- 还要 logprobs/KL:吞吐目标要更保守,并把 logprob 计算的开销算进预算(这时
max_num_batched_tokens往往要更谨慎)
如果你下一步要用 vLLM 做更“像论文”的评测(pass@k / PPL / token entropy),并且希望指标可信,建议直接对照这一篇的评测细节与坑点:
10. 你可以直接对齐的仓库材料
agentic_rl/infra/inference/vllm_sglang.ipynb- 本期核心参数解释、日志分账、KV token 估算公式、chunked prefill 的例子
agentic_rl/verl/vllm-rollout.ipynb- 站在 RL rollout 的视角解释 “prefill chunk + decode 混跑” 的 step 装配过程
如果你希望我把这两份 notebook 的关键内容进一步抽象成“可复制粘贴的调参模板 + 常见故障排查(OOM/吞吐低/ITL 差)”,我可以在这个系列里再加一篇专门做 troubleshooting。

