verl分布式训练入门:DP与TP并行策略解析
在大型语言模型(LLM)的强化学习后训练中,如何高效利用多GPU资源、平衡计算负载、降低通信开销,是工程落地的核心挑战。verl 作为字节跳动火山引擎团队开源的生产级RL训练框架,其核心价值不仅在于支持HybridFlow等前沿算法,更在于它对分布式并行策略的精细化抽象与灵活编排能力——尤其是数据并行(DP)与张量并行(TP)的协同设计。本文不讲抽象理论,不堆砌公式,而是带你从真实配置、源码逻辑和运行时行为出发,真正看懂:当执行verl训练脚本时,6张GPU到底怎么分工?rollout.n=12和tensor_model_parallel_size=2是如何共同决定每张卡处理多少数据的?为什么ppo_mini_batch_size会被反复除以6又乘以12?答案就藏在ActorRolloutRefWorker的设备网格构建与配置归一化逻辑里。
1. verl 并行设计哲学:解耦与协同
verl 的分布式能力不是简单套用 PyTorch FSDP 或 Megatron-LM,而是围绕 RL 训练特有的“生成-评估-更新”三阶段流水线,对计算与数据依赖进行深度解耦。这种解耦直接体现在其并行策略的分层设计上:
第一层:角色分离(Role Separation)
同一物理集群可同时承载 Actor、Rollout、Reference Policy 等不同角色的 Worker。它们共享底层 GPU 资源,但逻辑职责清晰——Actor 负责策略更新,Rollout 负责序列生成,Ref 负责固定策略打分。这种分离避免了单一大模型在不同阶段间的上下文切换开销。第二层:设备映射抽象(Device Mesh Abstraction)
verl 不预设“所有GPU必须用于FSDP”或“所有GPU必须用于TP”,而是通过create_device_mesh和init_device_mesh构建多维设备网格(Device Mesh),将物理 GPU 映射为逻辑维度:dp(数据并行)、sp(序列并行)、infer_tp(推理张量并行)等。同一组 GPU 可在不同阶段扮演不同角色——例如,在 rollout 阶段作为dp×infer_tp网格运行 vLLM 推理;在 actor 更新阶段则作为纯dp网格运行 FSDP 训练。第三层:配置驱动归一化(Config-Driven Normalization)
用户只需声明高层语义配置(如data.train_batch_size=60、rollout.n=12、tensor_model_parallel_size=2),verl 会在 Worker 初始化时自动完成“配置归一化”:根据实际 GPU 数量、并行维度划分,动态计算每个进程应处理的 micro-batch 大小、log_prob 计算粒度等。这屏蔽了底层设备拓扑的复杂性,让工程师专注业务逻辑而非硬件调度。
这种“角色-设备-配置”三层解耦,正是 verl 实现高吞吐、低延迟、易扩展的关键。接下来,我们将深入ActorRolloutRefWorker的初始化与 rollout 构建过程,逐行拆解 DP 与 TP 如何协同工作。
2. 设备网格构建:从 world_size 到 dp/infer_tp 划分
ActorRolloutRefWorker.__init__是理解 verl 并行策略的起点。它不直接操作 GPU ID,而是通过device_mesh抽象描述资源分配逻辑。我们以单机 6 卡(trainer.n_gpus_per_node=6)为例,逐步解析其设备网格构建过程。
2.1 全局设备网格:FSDP 基础分片
world_size = torch.distributed.get_world_size() # = 6 self.device_mesh = create_device_mesh(world_size=world_size, fsdp_size=self.config.actor.fsdp_config.fsdp_size)此处fsdp_size=-1表示启用全量 FSDP 分片,即self.device_mesh.size() == world_size == 6。这意味着 Actor 模型参数将被切分为 6 份,每张 GPU 持有 1/6 参数,并在前向/反向传播中协作完成计算。这是典型的数据并行(DP)基础层——所有 GPU 执行相同计算逻辑,但处理不同数据子集。
2.2 序列并行网格:Ulysses SP 的可选增强
self.ulysses_sequence_parallel_size = self.config.actor.get('ulysses_sequence_parallel_size', 1) dp = world_size // self.ulysses_sequence_parallel_size if self.ulysses_sequence_parallel_size > 1: self.ulysses_device_mesh = init_device_mesh('cuda', mesh_shape=(dp, self.ulysses_sequence_parallel_size), mesh_dim_names=['dp', 'sp'])当ulysses_sequence_parallel_size=1(默认值)时,此分支不执行,self.ulysses_device_mesh为None。这表明当前配置未启用序列并行(SP),所有 GPU 仅承担 DP 角色。若设为2,则dp=3,设备网格变为(3, 2),即 3 组 DP,每组内 2 张 GPU 协同处理一个长序列的分段计算——这是对超长上下文场景的优化,但非本文重点。
2.3 Rollout 专用网格:vLLM 推理的 TP 划分
真正的 TP 策略体现在_build_rollout方法中:
infer_tp = self.config.rollout.tensor_model_parallel_size # = 2 dp = self.world_size // infer_tp # = 6 // 2 = 3 rollout_device_mesh = init_device_mesh('cuda', mesh_shape=(dp, infer_tp), mesh_dim_names=['dp', 'infer_tp'])这里构建了一个(3, 2)的二维网格:
dp维度(大小为 3):表示数据并行组数。原始 batch(60 条 prompt)被均分为 3 份,每份 20 条,分发给 3 个 DP 组。infer_tp维度(大小为 2):表示每个 DP 组内,2 张 GPU 协同完成一次 vLLM 推理(即张量并行)。vLLM 将模型权重按层切分,每张 GPU 存储部分权重,通过 NCCL AllReduce 同步中间激活值。
因此,rollout_device_mesh的实际结构为:
DeviceMesh('cuda', [[0, 1], [2, 3], [4, 5]], mesh_dim_names=('dp', 'infer_tp'))- GPU 0 和 1 组成第 1 个
infer_tp组,共同处理第 1 份 20 条 prompt; - GPU 2 和 3 组成第 2 个
infer_tp组,共同处理第 2 份 20 条 prompt; - GPU 4 和 5 组成第 3 个
infer_tp组,共同处理第 3 份 20 条 prompt。
这种 DP+TP 混合模式,既保证了数据吞吐(3 组并行生成),又突破了单卡显存限制(2 卡合力加载大模型),是 verl 支持大规模 rollout 的关键。
3. 配置归一化:batch size 的动态计算逻辑
RL 训练中,batch 相关参数极易混淆。verl 通过“配置归一化”机制,将用户声明的高层语义(如train_batch_size)自动转换为各 Worker 实际执行的底层参数。这一过程发生在ActorRolloutRefWorker.__init__的normalize config区域,是理解 DP/TP 协同效果的核心。
3.1 Actor Mini-Batch 的归一化:从 60 到 120
用户配置:
data.train_batch_size=60 actor_rollout_ref.rollout.n=12 actor_rollout_ref.actor.ppo_mini_batch_size=60归一化步骤:
放大以容纳 rollout 扩展:
self.config.actor.ppo_mini_batch_size *= self.config.rollout.n
→60 * 12 = 720
含义:Actor 在一次训练 step 中需处理60 条 prompt × 每条生成 12 个 rollout= 720 个 rollout 样本。按 DP 组数均分:
self.config.actor.ppo_mini_batch_size //= (self.device_mesh.size() // self.ulysses_sequence_parallel_size)
→720 // (6 // 1) = 720 // 6 = 120
含义:6 张 GPU 组成 6 个 DP 进程,每个进程需处理720 / 6 = 120个 rollout 样本。
最终,每个 GPU 上的 Actor 进程实际处理的ppo_mini_batch_size为 120。这解释了为何ray_trainer.py中gen_batch_output.batch['prompt_token_ids'].shape[0]为720(全局)而单进程日志显示120(局部)。
3.2 Rollout Log Prob 的归一化:从 8 到 1
用户配置:
actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=8归一化步骤:
self.config.rollout.log_prob_micro_batch_size //= (self.device_mesh.size() // self.ulysses_sequence_parallel_size) self.config.rollout.log_prob_micro_batch_size_per_gpu = self.config.rollout.log_prob_micro_batch_size→8 // 6 = 1(整除)
含义:在 rollout 阶段,每个 GPU 进程计算 log_prob 时,每次只处理 1 个样本(micro-batch size = 1)。这是因为 rollout 生成的 720 个样本已由 3 个infer_tp组(每组 2 卡)完成,后续 log_prob 计算由所有 6 卡分担,确保负载均衡。
3.3 关键结论:DP 与 TP 的职责边界
- TP(
infer_tp)负责“生成带宽”:决定单次 vLLM 推理能处理多少 prompt(20 条/组),直接影响 rollout 吞吐上限。 - DP(
dp)负责“计算密度”:决定每个 GPU 进程在训练 step 中处理多少 rollout 样本(120 个),直接影响梯度更新效率。 - 配置归一化是桥梁:它将用户意图(
train_batch_size=60,rollout.n=12)自动映射到硬件约束(6 卡),确保TP 提供的数据量与DP 消化的计算量严格匹配,避免空转或拥塞。
4. Rollout 流水线实战:从 prompt 到 720 条 rollout
理解了设备网格与配置归一化,我们再看generate_sequences如何驱动整个 rollout 流水线。该方法被@register(dispatch_mode=Dispatch.DP_COMPUTE_PROTO)装饰,意味着它天然支持 DP 协同计算。
4.1 输入分发:60 条 prompt 的 DP 切分
初始gen_batch包含 60 条 prompt(input_ids.shape = [60, 8192])。generate_sequences首先调用self.rollout_sharding_manager.preprocess_data(prompts),依据rollout_device_mesh将其切分为 3 份,每份 20 条,分发至 3 个infer_tp组。
4.2 并行生成:3 组 × 20 × 12 = 720
每个infer_tp组(2 卡)接收 20 条 prompt,调用 vLLM 执行n=12次采样:
- GPU 0+1:生成 20×12 = 240 条 rollout 序列
- GPU 2+3:生成 20×12 = 240 条 rollout 序列
- GPU 4+5:生成 20×12 = 240 条 rollout 序列
总计 720 条 rollout 序列,存储于各组的 GPU 显存中。
4.3 结果聚合:DP 汇总与 CPU 卸载
generate_sequences的核心逻辑output = self.rollout.generate_sequences(prompts=prompts)返回的是分组结果。随后,self.rollout_sharding_manager.postprocess_data(output)执行 DP 汇总:
- 将 3 个
infer_tp组的 240 条结果,按 DP 语义合并为一个包含 720 条序列的DataProto对象。 - 最终
output.to('cpu')将全部 720 条序列卸载至 CPU 内存,供后续compute_log_prob等阶段使用。
这一过程完美体现了 verl 的设计哲学:TP 解决“能不能生成”的问题(突破显存瓶颈),DP 解决“快不快汇总”的问题(保障数据流畅通)。没有 TP,60 条 prompt 的 12 轮 rollout 无法在单卡完成;没有 DP,720 条结果无法高效聚合。
5. 工程实践建议:如何调整 DP 与 TP 参数
基于上述分析,给出三条可立即落地的调优建议:
5.1 优先调整tensor_model_parallel_size控制 rollout 吞吐
- 现象:rollout 阶段显存 OOM 或耗时过长。
- 原因:单
infer_tp组显存不足,或 vLLM 推理延迟高。 - 方案:增大
tensor_model_parallel_size(如从 2→4),将 6 卡划分为 6//4=1.5→向下取整为 1 组?不,需整除!正确做法是:tensor_model_parallel_size必须整除world_size。6 卡可选1, 2, 3, 6。选3则dp=2,每组 3 卡处理 30 条 prompt,降低单组负载。
5.2 谨慎修改rollout.n避免 DP 负载失衡
- 现象:Actor 更新阶段 GPU 利用率低,或
ppo_mini_batch_size归一化后为 0。 - 原因:
rollout.n过大导致720 // 6 = 120合理,但若rollout.n=100,则6000 // 6 = 1000,可能超出单卡显存。 - 方案:保持
rollout.n与train_batch_size的乘积(即总 rollout 数)在world_size × 单卡合理 batch范围内。例如 6 卡单卡处理 200 样本,则总 rollout 数宜控制在 1200 以内,即rollout.n ≤ 20。
5.3 利用log_prob_micro_batch_size_per_gpu微调内存压力
- 现象:
compute_log_prob阶段显存峰值过高。 - 原因:该参数控制每个 GPU 计算 log_prob 的样本数,归一化后为
8//6=1,已是最小粒度。 - 方案:若需进一步降低峰值,可主动设
log_prob_micro_batch_size_per_gpu=1(无需归一化),强制每卡每次只处理 1 个 rollout 样本,以时间换空间。
6. 总结:掌握 verl 并行的本质是理解“配置即拓扑”
verl 的 DP 与 TP 并行策略,绝非教科书式的静态划分,而是一种以配置为输入、以设备网格为载体、以归一化为纽带的动态协同机制。你声明的每一个数字——train_batch_size、rollout.n、tensor_model_parallel_size——都在参与构建一张隐式的“计算拓扑图”。这张图决定了:
- 数据如何在 GPU 间流动(DP 分发、TP 协作、DP 汇总),
- 计算如何在阶段间衔接(rollout 生成的 720 条样本,精准喂给 Actor 更新的 120 样本/卡),
- 资源如何被高效利用(6 卡无空转,无拥塞,无冗余通信)。
因此,入门 verl 分布式训练,关键不在于死记硬背参数含义,而在于养成一种思维习惯:每当看到一个配置项,立刻问自己——它会如何影响device_mesh的形状?它会被归一化为多少?它在哪个阶段、哪张 GPU 上生效?当你能从rollout_device_mesh = init_device_mesh('cuda', mesh_shape=(dp, infer_tp))这一行代码,推演出整个 720 条 rollout 的生成路径时,你就真正掌握了 verl 的并行灵魂。
--- > **获取更多AI镜像** > > 想探索更多AI镜像和应用场景?访问 [CSDN星图镜像广场](https://ai.csdn.net/?utm_source=mirror_blog_end),提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。