RANK 跟 GPU 怎么对应?两条 engine 路径(FSDP / Megatron)各自怎么切?一次 GRPO step 里这些 RANK 到底在干什么?四个问题,一篇说清。
一条单向强绑定链 + 一个可配置的多对一例外。把这条链画清楚,后面所有 mesh 切换 / role 复用的讨论才不会回头怀疑「rank 和 GPU 是不是又被重新分了」。
RANK ↔ Ray 进程)永远 1:1,Worker.__init__ 注入后就锁死;右半段(Ray 进程 ↔ GPU)默认 1:1,但当 max_colocate_count > 1 且多个 WorkerGroup 共用同一个 PG 时,可以放宽成多进程挤一张卡——也就是「一张 GPU 上有多个不同 WG 的 RANK」。
GPU0 ── 1 个 Ray actor 进程 ── RANK=0
└─ 进程内同时持有 actor / ref / rolloutmax_colocate_count=3 在这里只是 Ray 调度配额——并不会真的触发"多个 RANK 挤一张卡",因为只有一个 WorkerGroup 来抢这张卡。GPU0 ─┬─ Ray actor #A (actor WG · RANK=0)
└─ Ray actor #B (reward WG · RANK=0)max_colocate_count 是 Ray 调度层的会计配额(用 num_gpus = 1 / max_colocate_count 控制"最多几个 actor 能调度到同一张 GPU"),不是显存的物理切片。多个 RANK 真正挤一张卡时显存会物理竞争——verl 默认走"时间错峰(actor ↔ rollout hot-swap)"而非"空间瓜分",所以默认配置下你不会感到这个配额带来的显存压力。
RANK ≡ GPU 当一回事用,省脑力。只有看到这三种信号才需要切换到严格意义思考——① 给某个 role 配了独立的 resource_pool(reward / teacher 单独配 pool);② max_colocate_count > 1 且多个 WorkerGroup 共用同一个 PG;③ 显式用了 SubRayResourcePool 切分 rollout 副本。
verl 有两条互斥的训练 engine——选哪条由 yaml 里 strategy 决定。两条路径支持的并行维度完全不同,mesh 形状自然也不同。
| 切什么 | FSDP engine | Megatron engine |
|---|---|---|
| 切参数 / 优化器("模型放不下一张卡") | FSDP / Hybrid Shard | TP(张量切分) |
| 切层(PP 流水线) | 不支持 | PP / VPP |
| 切序列(长上下文) | Ulysses SP | CP |
| 切专家(MoE) | 不支持 | EP / Expert-TP |
| 数据并行(DP) | FSDP 自带 / Hybrid 的 DDP 维 | DP(通过 ZeRO 分片) |
| mesh API | torch.distributed.DeviceMesh | Megatron parallel_state |
| 关键代码 | workers/engine/fsdp/utils.py:38-56 | workers/engine/megatron/transformer_impl.py:148-158 |
所有切法都没有 PP。以 8 卡为例展示三种典型配置。
fsdp_size = 8 = world ⇒ 1D mesh [fsdp]fsdp_size = 4 < world ⇒ 2D mesh [ddp=2, fsdp=4]fsdp_size = 8 + ulysses_sp = 2 ⇒ 两套 mesh 叠加tp · pp · cp · ep · dp,每维都可独立配。Megatron 默认 order 是 tp-cp-ep-dp-pp(从内到外)——意思是 TP 最内、PP 最外。这不是直觉错位,是通信频率决定的:
(tp_idx, dp_idx):R5 = (tp=1, dp=2)。同 dp 的 2 张卡做 TP all-reduce;4 个 dp 之间做梯度 all-reduce。模型参数被 TP 切了 2 份;每个 dp 副本各持一整份切完的模型。(tp_idx, dp_idx, pp_idx) 三个坐标。PP 是最外层(不是 DP)。(tp=1, dp=2, pp=0)。collective op 沿哪个维度做由 mpu 决定:(tp_idx, ep_idx, dp_idx)。torch.distributed.pipelining + FSDP)里能跑,但 verl 不支持这种组合——FSDP engine 没有 PP 维,Megatron engine 不用 FSDP(用 ZeRO 形式做 DP 分片)。要 PP 必须切到 Megatron 后端;要走 PyTorch 原生 FSDP 就只能在 FSDP engine 里玩 FSDP × DDP × Ulysses。
六步走,从 yaml 里的 trainer.n_gpus_per_node × nnodes 到 GPU 上的一个 Ray 进程。
main_ppo.py 把 global_pool = [n_gpus_per_node] * nnodes 喂给 ResourcePoolManager。reward / teacher 如果开了独立 pool,会走 reward_pool / teacher_pool 分支。{"GPU": 1, "CPU": max_colocate_count}。max_colocate_count 默认 3——意味着 Ray 把这张 GPU 切成 3 份逻辑额度,对应「actor+critic+ref / rollout / reward」三种 WorkerGroup 复用同一张卡。sort_placement_group_by_node_ip(pgs)。这一步保证「断点续训时,同一台机器上的 worker 拿到的 RANK 不会变」——因为 FSDPCheckpointManager 把分片存在本地。for pg in sorted_pgs: for local_rank in range(local_world_size): rank += 1。第一个 PG 的第一个 bundle 还会跑 get_master_addr_port,作为后续 NCCL 的 master。RANK / WORLD_SIZE / RAY_LOCAL_WORLD_SIZE / MASTER_ADDR / MASTER_PORT,并按 num_gpus = 1 / max_colocate_count 申请 GPU 资源。注意:env_vars 里没有 LOCAL_RANK,靠 Ray 自己设置 CUDA_VISIBLE_DEVICES。Worker.__init__ 读 RANK;TrainingWorker 触发 initialize_global_process_group_ray(),torch.distributed.get_rank() 与 RANK 完全一致。然后 FSDPEngine / MegatronEngine 在它之上构建 mesh。同一份 16 个 RANK(2 节点 × 8 卡),切换下方按钮看 verl 在不同层视角下怎么读这套编号。
RANK = 0 .. 15。placement_group 按 node IP 排序后线性编号;同一节点内 LOCAL_RANK 从 0 起。这是后续所有视角的"基底"。
(ddp, fsdp) ⇒ rank = ddp · fsdp_size + fsdp(dp, infer_tp, infer_pp) ⇒ rank = dp · (infer_tp · infer_pp) + infer_tp · infer_pp + infer_ppmpu.initialize_model_parallel 内部决定,verl 这里只把 sizes 传过去,不再自己排。
把前面所有概念串起来:默认 colocate 的 8 张卡,在一次 GRPO step 里依次经历 7 个阶段。点击下方阶段方块切换查看每个阶段的活跃 role、显存里装着什么、用到的通信。
同一个 GPU 上,verl 默认让 actor / ref / rollout 共享一个 RANK。这不是巧合,是设计——它直接决定了你要不要为不同 role 配不同的并行尺寸。
global_pool 的 role 进入同一份 class_dict,由 create_colocated_worker_cls(class_dict) 融合成一个 WorkerDict。actor / ref / rollout 三个子对象,共用一个 RANK 与一个 torch.distributed 通信域。enable_resource_pool=true 与各自的 n_gpus_per_node × nnodes。RayPPOTrainer.__init__ 显式 assert hybrid_engine。verl 当前主路径假设 actor + rollout 至少在 colocate 意义下"在一起";要做完全 disaggregated 的异步 rollout,需要走 trainer / engine 之外的另一条路(async server adapter)。
从顶部跳到这里查名词。点击术语行展开释义。
n_gpus_per_node × nnodes 决定。是 verl 调度的最小资源单元。默认只有一个 global_pool。global_pool: [8,8,8,8],1 个 poolreward.reward_model.enable_resource_pool → 变 2 个 pool(global 24 + reward 8)create_colocated_worker_cls 生成。actor_wg.compute_log_prob(data),Ray 把参数 pickle 序列化、扔进 object store、通过网络送到目标 actor 进程执行。跨 WorkerGroup / 跨 pool 的通信都走它。init_process_group 之后就建好了一个全局 PG。cpu:gloo,cuda:nccl。[dp, sp]。["ddp", "fsdp"]。后面用 mesh["fsdp"] 拿那一维的子通信组。mpu = megatron parallel utilities,负责 TP/PP/CP/EP 的进程组管理。mpu.get_tensor_model_parallel_rank() 这类调用都从它读。RANK → torch 把这些 RANK 注册进一个全局通信组 → FSDP / Megatron / Rollout 在这同一组进程上各自用 mesh 切出自己想要的并行结构。所有"模型并行 rank"都只是 mesh 给同一个 RANK 的不同视角。
展开看每一层最值得跳进去读的几个文件 / 行号。
| 主题 | 路径 | 行号 |
|---|---|---|
| Ray RANK 分配主循环 | verl/single_controller/ray/base.py | 532-575 |
| worker env 注入(RANK / WORLD_SIZE / MASTER_*) | verl/single_controller/ray/base.py | 617-674 |
| placement group 节点 IP 排序 | verl/single_controller/ray/base.py | 69-86 |
| SubResourcePool(rollout 副本切分) | verl/single_controller/ray/base.py | 264-311 |
| ResourcePoolManager + max_colocate_count=3 | verl/single_controller/ray/base.py | 191-217 |
| Worker 读 RANK / 设置 LOCAL_RANK | verl/single_controller/base/worker.py | 181-281 |
| Worker 注册 dispatch dp_rank | verl/single_controller/base/worker.py | 86-116 |
| ResourcePool 抽象(world_size / store) | verl/single_controller/base/worker_group.py | 27-74 |
| torch.distributed init | verl/utils/distributed.py | 60-95 |
| main_ppo 构造 resource_pool_spec | verl/trainer/main_ppo.py | 156-202 |
| colocated WorkerDict 创建 | verl/trainer/ppo/ray_trainer.py | 797-828 |
| FSDP device_mesh 构造 | verl/workers/engine/fsdp/utils.py | 38-56 |
| FSDP Ulysses mesh + dp_rank | verl/workers/engine/fsdp/transformer_impl.py | 196-213 · 570-583 |
| Megatron parallel_state 初始化 | verl/workers/engine/megatron/transformer_impl.py | 131-158 |
| Rollout 3D mesh (dp, infer_tp, infer_pp) | verl/workers/engine_workers.py | 587-607 |