这一篇对应视频 05:“vLLM 参数配置、显存分析与性能调优 max_num_batched_tokens”(BV1QnSFBkEZU)。

这期的核心不是“教你把服务跑起来”,而是给你一个可以复用的调参心智模型

  1. vLLM 的显存到底被谁吃掉(权重 / KV cache / peak activation / CUDA Graph / 杂项)。
  2. max_model_lenmax_num_seqsmax_num_batched_tokens 之间到底是谁在限制并发与吞吐。
  3. 为什么 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 调参可以简化成一个闭环:

  1. 先定 max_model_len(你业务上允许的单请求最大上下文,越大越吃 KV)。
  2. gpu_memory_utilization(给 vLLM 一个“预算”,但要记住 CUDA Graph 显存不算在预算里)。
  3. 从日志里读出:weights/peak_activation/non_torch/cudagraph/kv_cache 的分账。
  4. 再调:
    • max_num_seqs:你希望系统“最多并发多少条序列”(逻辑上限)。
    • max_num_batched_tokens:每一步前向“最多塞多少 token”(决定 decode 并行度,也决定 chunked prefill 的块大小,也决定 profile 的峰值激活)。
  5. 以日志里的 GPU KV cache usage 为核心指标:
    • 如果 KV 利用率长期很低,且你吞吐不高,通常说明 有效 batch 太小:提高 max_num_seqsmax_num_batched_tokens
    • 如果 OOM 或 KV cache 太小导致并发上不去:降低 max_model_len 或降低 max_num_batched_tokens(因为它会抬高 peak activation,从 KV 预算里“扣钱”)。

1. vLLM 的两个推理场景:offline batch vs online serve

视频先把推理场景分成两类(这点很重要,因为你调参目标不一样):

  1. offline batch generation
    • 目标是吞吐(tokens/s),对单条请求的延迟不敏感。
  2. 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 你到底该优化哪个

同样是“推理性能”,不同场景的指标口径完全不同。把它说清楚,你后面的调参动作就不会互相打架:

  1. 吞吐(throughput,tokens/s)
    • 你希望 GPU 每秒吐出更多 token。
    • max_num_seqsmax_num_batched_tokens 往往是主要旋钮:更大 batch 通常更高吞吐。
  2. TTFT(Time To First Token)
    • 从请求进来到首 token 返回的时间,主要被 prefill(处理 prompt)决定。
    • 长 prompt + 大 batch + 不合理的 prefill 调度,会显著拖慢 TTFT。
  3. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
export VLLM_LOGGING_LEVEL=DEBUG

vllm serve Qwen/Qwen2.5-7B-Instruct \
--host 0.0.0.0 \
--port 8000 \
--served-model-name qwen2.5-7b \
--api-key ABC123 \
--gpu-memory-utilization 0.85 \
--max-model-len 8192 \
--max-num-seqs 256 \
--max-num-batched-tokens 8192 \
--enable-prefix-caching \
--enable-auto-tool-choice \
--tool-call-parser hermes

几个你一定要建立直觉的参数(对齐视频讲解):

  • --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”的片段。

一句话: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
2
3
4
5
6
7
Free memory on device (23.2/23.65 GiB) on startup.
Desired GPU memory utilization is (0.85, 20.1 GiB).
Actual usage is 14.25 GiB for weight,
0.28 GiB for peak activation,
0.04 GiB for non-torch memory,
and 0.17 GiB for CUDAGraph memory.
Current kv cache memory in use is 5.54 GiB.

你应该建立的“账本公式”是:

$$\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
2
Available KV cache memory: 5.54 GiB
GPU KV cache size: 103,712 tokens

但真正有价值的是你要会“反推为什么是这个数”,因为这决定你的最大并发。

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
2
3
4
5
6
7
# --max-model-len 32768
GPU KV cache size: 98,960 tokens
Maximum concurrency for 32,768 tokens per request: 3.02x

# --max-model-len 8192
GPU KV cache size: 98,960 tokens
Maximum concurrency for 8,192 tokens per request: 12.08x

注意:这里的 “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(我建议你按这个顺序来)

  1. 先把模型跑起来(别一上来就追“极限吞吐”)
  2. 打开 debug:export VLLM_LOGGING_LEVEL=DEBUG
  3. 读清楚启动时分账(weights / peak activation / non-torch / cudagraph / kv cache)
  4. 先定 max_model_len
    • 你允许的最大上下文是多少(这决定物理并发的上限)
  5. 再调 max_num_seqs
    • 如果 decode 阶段 token budget 没打满,通常先加它
  6. 再调 max_num_batched_tokens
    • 如果 GPU KV cache usage 低,且你并发也不高,说明有效 batch 小,可以加它
    • 但每次加都要看:peak activation 是否变大、KV cache tokens 是否被挤小
  7. 如果 graph capture OOM 或显存太紧:
    • 临时 --enforce-eager(关 CUDA Graph)救急
  8. 面向 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 差异),用来算:

  1. PPO/GRPO 的 ratio:r = exp(logp_new - logp_old)
  2. KL(对 ref 或对 old 的约束/监控):无论是 sampled 近似还是 full-distribution

这会带来两个工程后果:

  1. 你需要额外的前向计算(有时甚至是第二个模型的前向),吞吐会比“只生成文本”更低。
  2. 你需要更严格的 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。