一文搞懂verl核心机制:batch size不再令人纠结
在大型语言模型(LLM)的强化学习后训练中,batch size从来不是简单的“一次喂多少数据”——它是一张纵横交错的调度网络,牵动着GPU资源分配、序列生成数量、梯度更新粒度、通信开销与内存占用。当你看到data.train_batch_size=60、rollout.n=12、ppo_mini_batch_size=60、log_prob_micro_batch_size_per_gpu=8这些参数并列出现时,困惑是合理的:它们到底谁管谁?谁先被计算?谁最终决定显存是否爆掉?
本文不堆砌公式,不复述论文,而是带你穿透 verl 源码逻辑,从ray_trainer.py的fit()入口出发,经由ActorRolloutRefWorker的初始化与generate_sequences流程,最终落到每一张 GPU 上真实处理的数据量。你会清晰看到:
所有 batch 相关参数如何被动态归一化(normalize)
rollout 阶段为何从 60 条 prompt 变成 720 条 completion
actor、rollout、ref 三类 worker 如何按不同规则切分 batch
FSDP + tensor parallelism 如何协同决定每个 GPU 的实际负载
读完这篇,你将彻底告别“改一个参数就报错”“调完 batch 显存还是炸”的被动调试状态,真正掌握 verl 的 batch 调度心法。
1. 先说结论:verl 中的 batch 不是单一概念,而是四层嵌套结构
在 verl 中,batch size 不是一个标量,而是一个由训练目标驱动、受硬件约束、经多级归一化后落地到 GPU 的四层结构。我们用一句话概括其本质:
data.train_batch_size是用户视角的“每步处理多少条原始 prompt”,而最终落在每张 GPU 上的micro_batch_size,是由rollout.n、tensor_model_parallel_size、world_size和ulysses_sequence_parallel_size共同解耦、缩放、再分配的结果。
这四层结构如下(由外到内):
| 层级 | 名称 | 决定者 | 典型值(示例) | 物理含义 |
|---|---|---|---|---|
| L1 | data.train_batch_size | 用户配置 | 60 | 每个训练 step 从 dataloader 加载的 prompt 数量,即“原始批次大小” |
| L2 | rollout.n×data.train_batch_size | 算法设计(GRPO/PPO) | 60 × 12 = 720 | 经 rollout 后生成的 total sequence 数量(含重复采样),是后续所有计算的输入基数 |
| L3 | ppo_mini_batch_size(归一化后) | FSDP 分片逻辑 | 720 ÷ 6 = 120 | 每个 FSDP shard(通常 ≈ 每张 GPU)需参与梯度更新的 sequence 数 |
| L4 | log_prob_micro_batch_size_per_gpu | 推理/打分阶段内存控制 | 8 | 每张 GPU 在 compute_log_prob 阶段一次最多处理多少条 sequence,用于防 OOM |
这四层不是并列关系,而是因果链:L1 → L2(算法展开)→ L3(FSDP 归一化)→ L4(推理微批控制)。下面我们将逐层拆解,全部基于 verl 主干代码(ray_trainer.py、fsdp_workers.py)的真实逻辑。
2. 第一层:data.train_batch_size—— 你的起点,但不是终点
这是你在ppo_trainer.yaml里最先看到的参数:
data.train_batch_size: 60 trainer.n_gpus_per_node: 6 trainer.nnodes: 1它代表:每个训练 step,从训练数据集中取出 60 条 prompt,送入整个 RL 流水线。
注意两个硬约束:
data.train_batch_size必须能被trainer.n_gpus_per_node整除(此处60 ÷ 6 = 10,合法);- 它只是“输入起点”,不等于任何模型实际处理的数据量——actor 不会用它直接生成文本,ref 不会用它直接打分,critic 更不会用它计算 value。
它的真正作用,是作为 rollout 阶段的种子基数。接下来,rollout.n将把它放大。
3. 第二层:rollout.n—— 算法展开的关键乘数,从 60 到 720 的跃迁
在 GRPO(或 PPO)中,“rollout”指用当前 actor 模型对每个 prompt 进行多次采样,生成多个 completion,用于后续 reward 计算与优势估计。
这个“多次”就是rollout.n,配置项为:
actor_rollout_ref.rollout.n: 12这意味着:对每一条 prompt,actor 会生成 12 个不同的 completion。
所以,60 条 prompt × 12 次采样 =720 条完整的 (prompt, completion) 序列。
这个 720,是 verl 整个训练循环中最关键的中间 batch size。它出现在ray_trainer.py的generate_sequences调用后:
# gen_batch shape: torch.Size([60, 8192]) ← L1: 60 条 prompt gen_batch_output = self.actor_rollout_wg.generate_sequences(gen_batch) # gen_batch_output.batch['prompt_token_ids'].shape: torch.Size([720, 8192]) ← L2: 720 条 completion这 720 条数据,将作为后续所有计算的输入:
old_log_prob:计算 actor 对这 720 条 completion 的 token-level log probability;ref_log_prob:计算 reference policy 对这 720 条 completion 的 log probability;reward_fn:对这 720 条 completion 执行规则打分(GRPO)或调用 RM(PPO);compute_advantage:基于 reward 计算这 720 条序列的 advantage。
但请注意:720 是全局总量,不是单卡负载。下一步,FSDP 会把它切分。
4. 第三层:ppo_mini_batch_size的归一化 —— FSDP 如何把 720 分给 6 张 GPU
ppo_mini_batch_size出现在 actor 配置中:
actor_rollout_ref.actor.ppo_mini_batch_size: 60初看容易误解为“每次更新用 60 条数据”,但 verl 的设计哲学是:这个值必须经过归一化(normalization),才能成为真正的 per-GPU mini-batch size。
归一化逻辑在fsdp_workers.py的ActorRolloutRefWorker.__init__()中:
if self._is_actor: # Step 1: 先乘 rollout.n → 60 × 12 = 720 self.config.actor.ppo_mini_batch_size *= self.config.rollout.n # Step 2: 再除以有效分片数 → 720 ÷ (world_size // ulysses_sequence_parallel_size) shard = self.device_mesh.size() // self.ulysses_sequence_parallel_size # = 6 // 1 = 6 self.config.actor.ppo_mini_batch_size //= shard # = 720 // 6 = 120结果:ppo_mini_batch_size从配置的60,变成运行时的120。
这个120的含义是:每张 GPU 在 actor 梯度更新阶段,负责处理 120 条 sequence 的 forward/backward。
为什么是 120?因为:
- 总共 720 条 sequence(L2 结果);
- 全局有 6 张 GPU(
world_size = 6); - 当前未启用 sequence parallelism(
ulysses_sequence_parallel_size = 1),所以每张 GPU 是一个独立 FSDP shard; - 因此,720 ÷ 6 = 120 条/卡。
这个120就是 FSDP 实际执行optimizer.step()时的 batch size。它决定了:
- 梯度累积步数(若
gradient_accumulation_steps > 1,则120是累积后的总 batch); - 显存中激活值(activations)的峰值占用;
- 通信量(all-reduce 梯度的大小)。
关键洞察:
ppo_mini_batch_size的配置值本身不重要,重要的是它乘rollout.n后能否被world_size整除。如果60 × 12 = 720不能被 6 整除(比如rollout.n=13),verl 会在assert中直接报错,强制你调整配置。
5. 第四层:log_prob_micro_batch_size_per_gpu—— 推理阶段的“安全阀”
前三层都关乎训练吞吐与梯度更新,但还有一个关键阶段:计算 log probability(即compute_log_prob)。
无论是 actor 的old_log_prob,还是 ref 的ref_log_prob,都需要对 720 条 completion 做一次前向传播,输出每个 token 的 log prob。这个过程不更新参数,但显存压力极大——因为要缓存所有中间激活用于后续 KL 计算或 reward 构建。
为此,verl 引入了log_prob_micro_batch_size_per_gpu,配置项为:
actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu: 8 actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu: 8它的作用非常直白:每张 GPU 每次只处理 8 条 sequence,算完一批再加载下一批。
这个值在fsdp_workers.py中被归一化(虽然本例中8 ÷ 6 ≈ 1.33,向下取整为1),但更重要的是,它被硬编码进compute_log_prob的循环逻辑中,作为 micro-batch 的上限。
你可以把它理解为 verl 的“内存安全阀”:
- 如果设为
8,则 720 条数据会被切成720 ÷ 8 = 90个 micro-batch,每卡处理90 ÷ 6 = 15个 micro-batch; - 如果设为
1,则变成 720 个 micro-batch,通信开销增大,但单次显存最低; - 如果设为
720(理论上),则单卡一次性加载全部 720 条,大概率 OOM。
所以,log_prob_micro_batch_size_per_gpu是唯一一个你可以在不改模型、不调 world_size 的前提下,快速缓解显存 OOM 的参数。它不改变训练数学,只改变内存使用节奏。
6. rollout worker 的并行策略:为什么需要tensor_model_parallel_size=2?
前面我们假设world_size = 6全部用于 FSDP 数据并行(DP),但 verl 支持更细粒度的并行——尤其是在 rollout 阶段。
看这个配置:
actor_rollout_ref.rollout.tensor_model_parallel_size: 2它意味着:每 2 张 GPU 组成一个 tensor parallelism(TP)组,共同完成一个 vLLM inference engine 的推理任务。
在ActorRolloutRefWorker._build_rollout()中,verl 构建了一个二维 device mesh:
dp = self.world_size // infer_tp # = 6 // 2 = 3 rollout_device_mesh = init_device_mesh('cuda', mesh_shape=(dp, infer_tp), ...) # DeviceMesh('cuda', [[0, 1], [2, 3], [4, 5]], mesh_dim_names=('dp', 'infer_tp'))这表示:
- 全局 6 张 GPU 被划分为 3 个 DP group(
dp=3),每组 2 张卡(infer_tp=2); - 每个 DP group 内部,2 张卡通过 TP 协同运行一个 vLLM engine;
data.train_batch_size=60条 prompt,被均分到 3 个 DP group,即每个 group 处理 20 条 prompt;- 每个 group 内,2 张卡 TP 合作,对这 20 条 prompt 同时做
n=12次 rollout → 输出20 × 12 = 240条 completion; - 最终,3 个 group 汇总 →
3 × 240 = 720条。
这种设计的好处是:
- vLLM 的 TP 优化得以复用:大模型推理在 TP 下更高效;
- rollout 阶段通信最小化:TP 组内通信(高速 NVLink),DP 组间只需汇总结果(低频 all-gather);
- 负载均衡:60 条 prompt 被严格均分,避免某张卡空转。
提示:
tensor_model_parallel_size与ulysses_sequence_parallel_size是正交的。前者用于 rollout 推理(vLLM),后者用于 actor 训练(FSDP + Ulysses SP)。不要混淆。
7. 实战验证:从配置到每张卡的实际负载
我们用一个完整配置收束全文,验证所有层级:
# yaml 配置 data.train_batch_size: 60 trainer.n_gpus_per_node: 6 trainer.nnodes: 1 actor_rollout_ref.rollout.n: 12 actor_rollout_ref.rollout.tensor_model_parallel_size: 2 actor_rollout_ref.actor.ppo_mini_batch_size: 60 actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu: 8 actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu: 8 actor_rollout_ref.actor.ulysses_sequence_parallel_size: 1推导每张 GPU 的实际负载:
| 阶段 | 计算逻辑 | 每卡数量 | 说明 |
|---|---|---|---|
| Input prompt | data.train_batch_size ÷ (n_gpus_per_node × nnodes) × tensor_model_parallel_size | 60 ÷ 6 × 2 = 20 | 每个 TP group 处理 20 条 prompt(因 TP=2,需 2 卡协作) |
| Rollout output | 20 × rollout.n = 20 × 12 | 240 | 每个 TP group 输出 240 条 completion |
| Global rollout total | 240 × dp_groups = 240 × 3 | 720 | 全局汇总 |
| Actor update batch | 720 ÷ world_size = 720 ÷ 6 | 120 | 每卡在 FSDP 更新中处理 120 条 sequence |
| Log prob micro-batch | log_prob_micro_batch_size_per_gpu | 8 | 每卡每次只加载 8 条 sequence 计算 log prob |
这就是 verl 的 batch 调度全景图:从用户配置的60,到最终每卡120的更新 batch 和8的推理 micro-batch,所有数字都有迹可循、有码可查。
8. 常见问题速查:你遇到的报错,根源在这里
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
ppo_mini_batch_size should be larger than 0 after normalization | data.train_batch_size × rollout.n无法被world_size整除 | 调整rollout.n或data.train_batch_size,确保乘积可被 GPU 数整除 |
CUDA out of memoryincompute_log_prob | log_prob_micro_batch_size_per_gpu设得太大 | 优先调小此值(如从8→4→2),这是最快速的 OOM 缓解手段 |
| rollout 阶段 GPU 利用率不均衡 | tensor_model_parallel_size与world_size不匹配(如world_size=5,tp=2) | 确保world_size % tensor_model_parallel_size == 0,否则 DP group 数非整数 |
gen_batch_outputshape 不符预期(如不是 720) | rollout.n未生效,可能被更高优先级配置覆盖 | 检查启动脚本是否传入--config.actor_rollout_ref.rollout.n=12,yaml 中的值可能被覆盖 |
记住:verl 的 batch 体系是强约束、可验证、有日志的。所有归一化步骤都在fsdp_workers.py的__init__中打印assert和print,运行时加-v参数即可看到每一步的数值变化。
9. 总结:掌握 batch,就是掌握 verl 的调度主权
verl 不是一个“黑盒框架”,它的 batch 设计是透明、分层、可推导的。本文带你走完了从配置到源码、从宏观到微观的完整链条:
data.train_batch_size是你的指挥棒,但它只指挥 rollout 的起点;rollout.n是算法杠杆,把 60 条 prompt 放大为 720 条数据,奠定训练基础;ppo_mini_batch_size的归一化是 FSDP 的智慧,让 720 条数据公平、高效地分给每张 GPU;log_prob_micro_batch_size_per_gpu是内存守门员,在不牺牲数学的前提下保障稳定性;tensor_model_parallel_size是 rollout 的加速器,用 vLLM 的 TP 优势榨干 GPU 算力。
你不需要死记硬背所有参数,只需要记住这个心法:
看 batch,先问“这是哪一层的 batch?”——是输入层(L1)、算法层(L2)、FSDP 层(L3)还是内存层(L4)?然后顺藤摸瓜,找到它在
fsdp_workers.py中被归一化、被使用的那一行代码。
从此,batch size 不再令人纠结,而是你手中可预测、可调试、可优化的确定性工具。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。