verl · 训练机制笔记

一个 RANK,多套 mesh

RANK 跟 GPU 怎么对应?两条 engine 路径(FSDP / Megatron)各自怎么切?一次 GRPO step 里这些 RANK 到底在干什么?四个问题,一篇说清。

verl-project / verl · verl/single_controller/ & verl/workers/engine/
读到 RANK / FSDP / WorkerGroup 这种术语不太熟?随时跳到 §10 名词速查 回查再回来——后文默认你已经认识这些词。
01 · 绑定关系

RANK 跟 GPU 是什么关系?

一条单向强绑定链 + 一个可配置的多对一例外。把这条链画清楚,后面所有 mesh 切换 / role 复用的讨论才不会回头怀疑「rank 和 GPU 是不是又被重新分了」。

身份
RANK
分布式编号 0..N-1
永远 1:1
绑死,不可调
载体
Ray 进程
一个常驻 Python actor
默认 1:1
可配置成 N:1
硬件
物理 GPU
一张卡
左半段RANK ↔ Ray 进程永远 1:1Worker.__init__ 注入后就锁死;右半段Ray 进程 ↔ GPU)默认 1:1,但当 max_colocate_count > 1 多个 WorkerGroup 共用同一个 PG 时,可以放宽成多进程挤一张卡——也就是「一张 GPU 上有多个不同 WG 的 RANK」。

默认 colocate(hybrid)

所有 role 共用 global_pool
  • 每张 GPU 上只起一个 Ray actor 进程,进程拿一个 RANK。
  • actor / ref / rollout 是这同一进程内的子对象,按时间错峰跑(hot-swap),共享显存与 RANK。
  • 物理布局:GPU0 ── 1 个 Ray actor 进程 ── RANK=0 └─ 进程内同时持有 actor / ref / rollout
  • max_colocate_count=3 在这里只是 Ray 调度配额——并不会真的触发"多个 RANK 挤一张卡",因为只有一个 WorkerGroup 来抢这张卡。

独立 resource_pool

reward / teacher 自带 pool
  • 不同 role 的 WorkerGroup 可以共用同一组物理 GPU(pool 重叠时),但各起一份 Ray actor 进程,各拿一份 RANK 0..N-1。
  • 物理布局:GPU0 ─┬─ Ray actor #A (actor WG · RANK=0) └─ Ray actor #B (reward WG · RANK=0)
  • 显存被两个进程真正瓜分,需要自己算每份能放多大模型。
  • 跨 pool 的数据交换走 Ray RPC,而不是 NCCL collective。
关键差别:max_colocate_countRay 调度层的会计配额(用 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 副本。
02 · 两条 engine

FSDP 和 Megatron 各自怎么切

verl 有两条互斥的训练 engine——选哪条由 yaml 里 strategy 决定。两条路径支持的并行维度完全不同,mesh 形状自然也不同。

支持的并行维度对比

切什么 FSDP engine Megatron engine
切参数 / 优化器("模型放不下一张卡")FSDP / Hybrid ShardTP(张量切分)
切层(PP 流水线)不支持PP / VPP
切序列(长上下文)Ulysses SPCP
切专家(MoE)不支持EP / Expert-TP
数据并行(DP)FSDP 自带 / Hybrid 的 DDP 维DP(通过 ZeRO 分片)
mesh APItorch.distributed.DeviceMeshMegatron parallel_state
关键代码workers/engine/fsdp/utils.py:38-56workers/engine/megatron/transformer_impl.py:148-158

路径 A · FSDP engine:最多两维 mesh

所有切法都没有 PP。以 8 卡为例展示三种典型配置。

case A1
纯 FSDP(默认)
fsdp_size = 8 = world ⇒ 1D mesh [fsdp]
R0R1R2R3R4R5R6R7
所有 8 个 RANK 共享一个 FSDP 分片组。每个 RANK 持有 1/8 的参数+梯度+优化器状态,forward 前 all-gather 拼回完整权重。
case A2
Hybrid Shard(节点内分片 + 节点间复制)
fsdp_size = 4 < world ⇒ 2D mesh [ddp=2, fsdp=4]
R0R1R2R3
R4R5R6R7
两个 FSDP 分片组,组内 4 个 RANK 切参数;组与组之间是 DDP 复制(all-reduce 同步梯度)。常用于多机:fsdp 维 = 单机内 GPU 数,ddp 维 = 节点数,跨节点带宽小所以走 DDP,节点内带宽大走 FSDP。
case A3
FSDP + Ulysses(长序列)· 同一组 RANK 同时戴两顶帽子
fsdp_size = 8 + ulysses_sp = 2 ⇒ 两套 mesh 叠加
FSDP 视角
切参数
R0p0 R1p1 R2p2 R3p3 R4p4 R5p5 R6p6 R7p7
8 张卡共同持有 1 份完整模型参数,每卡只存 1/8(p0..p7)。forward 前 all-gather 拼回,算完释放。
Ulysses 视角
切序列
R0R1
R2R3
R4R5
R6R7
读法:以 R3 为例 — FSDP 视角下它持有参数分片 p3,跟另外 7 张卡 all-gather 拼出完整层;Ulysses 视角下它跟 R2 组成 sp 组(dp=1),共同负责一份长序列的不同注意力头。两个视角同时成立,因为切的是不同维度——FSDP 切参数张量、Ulysses 切 attention 计算。

路径 B · Megatron engine:多维 mesh(最多 5 维)

tp · pp · cp · ep · dp,每维都可独立配。Megatron 默认 order 是 tp-cp-ep-dp-pp(从内到外)——意思是 TP 最内、PP 最外。这不是直觉错位,是通信频率决定的

为什么 TP 在最内、PP 在最外:从最内层到最外层,通信频率递减,对带宽要求递减
· TP(每层 all-reduce)频率最高 → 必须在节点内,吃 NVLink 带宽
· DP(梯度同步)频率次之 → 节点内或同机房网络
· PP(层间 send-recv)频率最低 → 可以跨节点甚至跨机房
所以"DP 在中间、PP 在外"是把高频通信塞进高带宽链路的工程优化,不是逻辑顺序。
B1
最简单:TP=2, DP=4 · 8 卡(不开 PP)
不开 PP 时,DP 自然成为最外层。这才符合大家直觉中的"DP 最高层"。
R0R1
R2R3
R4R5
R6R7
4 个 DP 副本,每副本 2 卡跑 TP=2。每张卡的坐标是 (tp_idx, dp_idx):R5 = (tp=1, dp=2)。同 dp 的 2 张卡做 TP all-reduce;4 个 dp 之间做梯度 all-reduce。模型参数被 TP 切了 2 份;每个 dp 副本各持一整份切完的模型。
B2
完整三维:TP=2, PP=2, DP=4 · 16 卡
每个 RANK 同时拿到 (tp_idx, dp_idx, pp_idx) 三个坐标。PP 是最外层(不是 DP)
pp_stage 0 — 前半层
R0tp=0R1tp=1
R2tp=0R3tp=1
R4tp=0R5tp=1
R6tp=0R7tp=1
pp_stage 1 — 后半层
R8tp=0R9tp=1
R10tp=0R11tp=1
R12tp=0R13tp=1
R14tp=0R15tp=1
读法:以 R5 为例 → (tp=1, dp=2, pp=0)。collective op 沿哪个维度做由 mpu 决定:
· TP 组(size=2):{R0,R1} / {R2,R3} / ... — 同一对算同一层 → 每层都 all-reduce,最高频
· DP 组(size=4):{R0,R2,R4,R6}(tp=0,pp=0)等 — 同 layer 不同 batch,反传完后 all-reduce 梯度 → 中等频率
· PP 组(size=2):{R0,R8} / {R1,R9} / ... — 流水线传递激活值 → 层间才 send-recv,最低频
这就是为什么 rank 编号在每个 pp_stage 内连续(R0..R7 同在 stage 0),而 pp 跨 stage 时直接跳到 R8——把高频通信留在编号相邻的卡上。
B3
MoE 专用:TP=2, EP=4, DP=2 · 16 卡
EP(专家并行)切的是不同的 expert,和 DP 正交。每张卡的坐标是 (tp_idx, ep_idx, dp_idx)
dp=0 — 一份完整数据
R0tp=0R1tp=1
R2tp=0R3tp=1
R4tp=0R5tp=1
R6tp=0R7tp=1
dp=1 — 另一份数据
R8tp=0R9tp=1
R10tp=0R11tp=1
R12tp=0R13tp=1
R14tp=0R15tp=1
注意:MoE 模型不开 PP 时,DP 又一次成为最外层(B1 那种情况)。EP 在中间——同一 dp 副本里的 4 个 ep 组各持有不同 expert。token 经过 routing 后通过 all-to-all 送到对应 expert 所在的 RANK 上算。
常见误会:「FSDP + PP」在 PyTorch 原生(torch.distributed.pipelining + FSDP)里能跑,但 verl 不支持这种组合——FSDP engine 没有 PP 维,Megatron engine 不用 FSDP(用 ZeRO 形式做 DP 分片)。要 PP 必须切到 Megatron 后端;要走 PyTorch 原生 FSDP 就只能在 FSDP engine 里玩 FSDP × DDP × Ulysses。
03

从 trainer 启动到 worker 拿到 RANK

六步走,从 yaml 里的 trainer.n_gpus_per_node × nnodes 到 GPU 上的一个 Ray 进程。

① 算 ResourcePool 的 process_on_nodes
main_ppo.pyglobal_pool = [n_gpus_per_node] * nnodes 喂给 ResourcePoolManager。reward / teacher 如果开了独立 pool,会走 reward_pool / teacher_pool 分支。
verl/trainer/main_ppo.py:156-188
② 申请 placement group
每个节点一个 PG,PG 里每张卡一个 bundle:{"GPU": 1, "CPU": max_colocate_count}max_colocate_count 默认 3——意味着 Ray 把这张 GPU 切成 3 份逻辑额度,对应「actor+critic+ref / rollout / reward」三种 WorkerGroup 复用同一张卡。
verl/single_controller/ray/base.py:130-160 · 199-207
③ 按节点 IP 排序 PG(重要)
sort_placement_group_by_node_ip(pgs)。这一步保证「断点续训时,同一台机器上的 worker 拿到的 RANK 不会变」——因为 FSDPCheckpointManager 把分片存在本地。
verl/single_controller/ray/base.py:69-86
④ 双层 for 循环分配 RANK
for pg in sorted_pgs: for local_rank in range(local_world_size): rank += 1。第一个 PG 的第一个 bundle 还会跑 get_master_addr_port,作为后续 NCCL 的 master。
verl/single_controller/ray/base.py:557-575
⑤ 通过 env_vars 启动 Ray actor
每个 worker 进程被注入 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
verl/single_controller/ray/base.py:617-674 · 622-623
⑥ Worker 进程内 init_process_group
Worker.__init__ 读 RANK;TrainingWorker 触发 initialize_global_process_group_ray()torch.distributed.get_rank() 与 RANK 完全一致。然后 FSDPEngine / MegatronEngine 在它之上构建 mesh。
verl/single_controller/base/worker.py:181-216 · verl/workers/engine_workers.py:82-92
04

16 卡跑起来:一个 RANK,四种视角

同一份 16 个 RANK(2 节点 × 8 卡),切换下方按钮看 verl 在不同层视角下怎么读这套编号。

切视角:
node-A · 10.0.0.11placement_group[0]
node-B · 10.0.0.12placement_group[1]
主坐标 当前高亮组 第二维分组(如 fsdp / pp)
RANK = 0 .. 15placement_group 按 node IP 排序后线性编号;同一节点内 LOCAL_RANK 从 0 起。这是后续所有视角的"基底"。
展开公式(DeviceMesh 默认 C-order,最后一维 stride 最快):
· 2D (ddp, fsdp)rank = ddp · fsdp_size + fsdp
· 3D (dp, infer_tp, infer_pp)rank = dp · (infer_tp · infer_pp) + infer_tp · infer_pp + infer_pp
Megatron 的 (tp, pp, dp) 顺序由 mpu.initialize_model_parallel 内部决定,verl 这里只把 sizes 传过去,不再自己排。
05 · GRPO 流程

一次 GRPO step 里 RANK 到底在干什么

把前面所有概念串起来:默认 colocate 的 8 张卡,在一次 GRPO step 里依次经历 7 个阶段。点击下方阶段方块切换查看每个阶段的活跃 role、显存里装着什么、用到的通信。

阶段 1 / 7:从 dataset 采 prompts
controller · CPU
8 张 GPU 当前装的什么(colocate 模式)
这一步在干什么
Controller (driver) 从 dataset batch 取 N 条 prompts,按 dp 维度切分成 N/dp_size 份分发给各 RANK。GPU 还没启动,全部 idle。
活跃 role
controller / driver(不在 GPU 上)
显存状态
GPU 此刻基本空载或残留上一 step 的训练态。这一步纯 CPU。
通信
无 NCCL 通信(仅 Ray RPC 派发数据)
关键代码
verl/trainer/ppo/ray_trainer.py · DataProto.split
1 / 7
关键观察:整个 step 里 同一组 8 个 RANK 始终是这 8 个 RANK,没有任何重新分配。变的只是「这一刻它们的进程内部哪个子对象在用 GPU」——actor、ref、还是 rollout-engine。这就是 colocate 的 hot-swap 设计:同一 RANK,时间换空间
06

Colocate vs Separated:rank 复用还是切开

同一个 GPU 上,verl 默认让 actor / ref / rollout 共享一个 RANK。这不是巧合,是设计——它直接决定了你要不要为不同 role 配不同的并行尺寸。

Colocate(默认 hybrid)

global_pool · max_colocate_count = 3
  • 所有共享 global_pool 的 role 进入同一份 class_dict,由 create_colocated_worker_cls(class_dict) 融合成一个 WorkerDict
  • 每张 GPU 只起一个 Ray actor 进程,进程内同时持有 actor / ref / rollout 三个子对象,共用一个 RANK 与一个 torch.distributed 通信域。
  • 显存共享、权重 hot-swap(actor → rollout)成本最低;但要求 三个 role 的并行尺寸都能在同一组 RANK 上对齐
  • 若你想给 rollout 用 TP=4、给 actor 用 TP=2,colocate 就跑不通——不是技术不允许,是同一组 RANK 没法同时是两个不同 mesh 的合法分解。

Separated(独立 pool)

reward_pool / teacher_pool · 自带 nnodes & n_gpus_per_node
  • 触发条件:在 yaml 里给 reward / teacher / 单独的 rollout 池子单独配置 enable_resource_pool=true 与各自的 n_gpus_per_node × nnodes
  • 这些 pool 各自走一遍 ②③④ 步,各拿一份 RANK 0..N-1 和各自的 master,互不干扰。
  • 跨 pool 的数据交换变成 Ray RPC(结果走 object store),而不是 NCCL collective。
  • 代价:需要更多 GPU;权重同步多一层 pickle/transfer。收益:每个 role 可独立选并行尺寸 / 后端(FSDP vs Megatron vs vLLM)。
注意:RayPPOTrainer.__init__ 显式 assert hybrid_engine。verl 当前主路径假设 actor + rollout 至少在 colocate 意义下"在一起";要做完全 disaggregated 的异步 rollout,需要走 trainer / engine 之外的另一条路(async server adapter)。
07 · 名词速查

先认识几个分布式名词

从顶部跳到这里查名词。点击术语行展开释义。

① Ray 编排层 — 管「谁在哪台机器上起进程」

Ray
分布式任务编排框架。verl 用它跨多机起进程、做 RPC 调用。
类比:跨机版的 multiprocessing.Pool,能在 16 台机器上同时起 128 个进程并互相喊话。
Ray actor
一个常驻进程,对应 Python 里一个有状态对象。verl 的每张 GPU 一般对应一个 actor。
类比:远端 server-side 的有状态对象实例,方法调用通过 RPC 转发。
placement group / bundle
Ray 的资源预约单位。一个 placement group 占住一台机器上的几张 GPU;每张 GPU 是一个 bundle。
类比:给一群进程提前订好一桌(PG),每个座位(bundle)至少 1 张 GPU
max_colocate_count
Ray 把一张 GPU 切成几"份逻辑额度"允许复用。verl 默认 3,给 actor+critic+ref / rollout / reward 三类 worker 复用同一张卡。
注意:这只是调度层的额度,真显存不会变多。三类 worker 还是要排队用同一张卡。
ResourcePool
verl 给一组物理 GPU 起的名字,由 yaml 里 n_gpus_per_node × nnodes 决定。是 verl 调度的最小资源单元。默认只有一个 global_pool
"几机几卡" ≠ "几个 pool"——前者是硬件,后者是配置。
· 4 机 8 卡(32 GPU)默认 = global_pool: [8,8,8,8]1 个 pool
· reward 想单占 1 台机?yaml 打开 reward.reward_model.enable_resource_pool → 变 2 个 pool(global 24 + reward 8)
· 跨 pool 通信只能 Ray RPC,不能 NCCL collective。
WorkerGroup / RayWorkerGroup
verl 在一个 ResourcePool 上起的一组 Ray actor 进程,自带 RANK 0..N-1 编号、独立的 master、独立的 NCCL 通信域。一个 pool 一个 WG,一个 WG 一个 distributed 世界。
关键边界:NCCL collective 只在同一 WG 内有效,跨 WG 的通信只能走 Ray RPC(pickle 序列化)。所以"actor + reward 在两个 pool"意味着它俩之间只能 RPC 传数据。
WorkerDict / FusedWorker
colocate 模式下的"角色融合容器"——一个 Ray actor 进程内同时持有 actor / ref / rollout 三个子对象,三者共用 RANK 与显存,按时间错峰跑。由 create_colocated_worker_cls 生成。
默认 verl 跑的就是这个。它是为什么"一个 RANK 同时是 actor 又是 ref 又是 rollout"的真相——不是 RANK 复用,是进程内对象复用
Ray RPC
Ray 提供的远程方法调用机制——driver 调 actor_wg.compute_log_prob(data),Ray 把参数 pickle 序列化、扔进 object store、通过网络送到目标 actor 进程执行。跨 WorkerGroup / 跨 pool 的通信都走它。
类比:跨进程的 Python 函数调用。和 NCCL collective 的本质区别——
· NCCL:GPU 直连,只在同 WG 内有效,传裸 tensor,毫秒级
· Ray RPC:CPU 序列化 + 网络,跨 pool / 跨 WG 通用,传任意 Python 对象,但慢得多
所以"actor 把数据发给 reward"不是 all-gather,是 RPC。

② PyTorch 分布式基础 — 进程之间怎么通信

RANK / WORLD_SIZE
RANK = 当前进程在分布式世界的全局编号 (0..N-1);WORLD_SIZE = 总进程数 N。
类比:班级里每人一个固定学号。点名时按号叫人。
LOCAL_RANK
当前进程在本机的编号 (0..每机 GPU 数-1)。决定它绑哪张本地 GPU。
RANK 是全局学号,LOCAL_RANK 是"在你们组内的序号"
process group (PG)
一组可以互相做集合通信(all-reduce 等)的进程。init_process_group 之后就建好了一个全局 PG。
类比:微信群。在群里发的消息所有人都收得到,群外的人收不到。
NCCL / Gloo
两种通信后端。NCCL 走 GPU-GPU NVLink/PCIe,CUDA 训练用它;Gloo 走 CPU 网络,做兜底和 CPU tensor。
NCCL 快但只服务 GPU;Gloo 通用但慢。verl 同时启用:cpu:gloo,cuda:nccl
all-reduce / all-gather
两种集合通信原语。all-reduce:所有 rank 把各自 tensor 求和后大家都拿到结果;all-gather:把每个 rank 的不同分片拼成完整张量发给所有人。
DDP 同步梯度用 all-reduce;FSDP 在 forward 前 all-gather 把分片参数拼回完整权重。
MASTER_ADDR / MASTER_PORT
分布式启动时的"集合点"。所有进程先通过这个 IP:port 互相握手,再建立 NCCL 组。
类比:开会前指定的集合地点。第一个 rank 出来吼一嗓子,其他人按这个地址来对暗号。

③ 并行策略 — 一个大模型怎么塞进多张卡

DP (Data Parallel)
每张卡都有完整一份模型,把 batch 切成 N 段,每张卡算自己那段,再 all-reduce 同步梯度。
最简单,也是其他并行的"基线"。模型放不下时它就失效了。
DDP
PyTorch 的 DP 实现。每张卡完整模型 + 自己梯度 + 自己优化器状态,all-reduce 同步梯度。
显存效率最差(参数 + 梯度 + 优化器状态都 ×N 份),但通信最简单。
FSDP (Fully Sharded DP)
参数、梯度、优化器状态都切片分散在所有 rank 上;前向时临时 all-gather 拼回完整权重,算完丢掉。
显存最省(每项都 ÷N),代价是通信量上升。verl 的默认训练后端之一。
TP (Tensor Parallel)
把一个矩阵乘法横切或竖切分到多张卡上算。每张卡只有部分权重,做完再 all-reduce 拼结果。
解决"单层放不下一张卡"的问题。Megatron-LM 是 TP 的经典实现。
PP (Pipeline Parallel)
把模型按层切成段,前 N 层在 GPU0,中间 N 层在 GPU1……数据像流水线一样穿过。
解决"全模型放不下"。代价是首末批次有 bubble(等待)。VPP(virtual PP)是它的优化版。
CP (Context Parallel)
序列长度维度切到多张卡——每张卡持有部分 token 的全部注意力头,专为超长上下文(128k+ token)设计。
和 SP/Ulysses 是镜像关系:CP 切 token、SP 切注意力头。CP 更扩展(token 维通常远大于头数),但实现上要让 KV 在卡间流动(ring attention 之类),通信调度更复杂。
EP (Expert Parallel)
MoE 模型专用:把不同专家(expert)分到不同卡。每个 token 只走少数几个 expert。
和 TP 正交。Qwen-MoE / DeepSeek-V3 这类模型必备。
SP / Ulysses
Sequence Parallel。先按 token 把序列切到多卡,再做一次 all-to-all 交换:交换后每张卡持有完整序列长度,但只负责部分注意力头。这样每卡都能本地算自己那几个头的完整 attention。
和 CP 的差别:CP = 部分 token × 全部注意力头;SP = 全部 token × 部分注意力头。SP 实现简单、通信靠 all-to-all,适合中长序列;CP 更扩展,适合超长上下文 + 大规模并行。verl 的 FSDP engine 用 Ulysses 做长序列;和 FSDP 一起组成 2D mesh [dp, sp]

④ Mesh 与 Megatron — 把 RANK 投影成多维坐标

DeviceMesh
PyTorch 把一组 RANK reshape 成 N 维网格的工具。每个 rank 在每一维上自动得到一个坐标,同维度的 rank 自动组成一个通信组。
类比:把 16 个士兵排成 4×4 方阵,"同一行"和"同一列"就是天然的两个通信组,不用手动建。
mesh_dim_names
给 mesh 每一维起名字,比如 ["ddp", "fsdp"]。后面用 mesh["fsdp"] 拿那一维的子通信组。
名字纯逻辑,不强制对应任何并行策略——你叫它 "foo"/"bar" 也行。
Megatron-LM / mpu
NVIDIA 的大模型训练框架。mpu = megatron parallel utilities,负责 TP/PP/CP/EP 的进程组管理。
和 DeviceMesh 解决类似问题,但比 DeviceMesh 早。verl 同时支持两条后端。
parallel_state
Megatron 的全局状态对象,存着所有 TP/PP/DP/CP/EP 子通信组。mpu.get_tensor_model_parallel_rank() 这类调用都从它读。
和 PyTorch DeviceMesh 是同类东西,只是 API 风格不同。

⑤ RLHF / PPO 角色 — verl 训练流程里的几个"模型实例"

Actor
正在训练的策略模型本体。每个 step 它生成回答、被打分、被更新。
参数会变。它的更新公式就是 PPO loss。
Reference (ref)
Actor 的"原始版本"快照,参数冻结不动。用来算 KL 散度避免 actor 跑太远。
显存大头。冻结但要 forward。可以 offload 到 CPU 省显存。
Critic
Value 模型,预测每个 token 的"未来回报"。PPO 用它估计 advantage。
GRPO / RLOO 这类算法不需要 critic(用组内对比代替)。
Rollout
用 actor 当前权重生成回答的过程。一般跑在 vLLM / SGLang 推理引擎上,不是训练框架。
推理引擎和训练后端是两套东西,需要做权重同步(hot-swap)把 actor 最新参数刷过去。
Reward model
给 actor 生成的回答打分。可以是训练好的模型,也可以是 rule-based / verifier。
如果是 rule-based(比如数学验证),就不需要 GPU worker,省一份资源。
colocate(共驻)
让 actor / ref / rollout 这三类共享同一组 GPU 进程,按时间错峰用显存。verl 的默认模式。
代价:三个 role 必须能在同一组 RANK 上对齐并行尺寸(比如不能给 rollout 用 TP=4、actor 用 TP=2)。
读完这一页,你应该能理解后文的核心叙事: Ray 给每张卡起一个进程拿到 RANK → torch 把这些 RANK 注册进一个全局通信组 → FSDP / Megatron / Rollout 在这同一组进程上各自用 mesh 切出自己想要的并行结构。所有"模型并行 rank"都只是 mesh 给同一个 RANK 的不同视角。
08

关键代码索引

展开看每一层最值得跳进去读的几个文件 / 行号。

主题路径行号
Ray RANK 分配主循环verl/single_controller/ray/base.py532-575
worker env 注入(RANK / WORLD_SIZE / MASTER_*)verl/single_controller/ray/base.py617-674
placement group 节点 IP 排序verl/single_controller/ray/base.py69-86
SubResourcePool(rollout 副本切分)verl/single_controller/ray/base.py264-311
ResourcePoolManager + max_colocate_count=3verl/single_controller/ray/base.py191-217
Worker 读 RANK / 设置 LOCAL_RANKverl/single_controller/base/worker.py181-281
Worker 注册 dispatch dp_rankverl/single_controller/base/worker.py86-116
ResourcePool 抽象(world_size / store)verl/single_controller/base/worker_group.py27-74
torch.distributed initverl/utils/distributed.py60-95
main_ppo 构造 resource_pool_specverl/trainer/main_ppo.py156-202
colocated WorkerDict 创建verl/trainer/ppo/ray_trainer.py797-828
FSDP device_mesh 构造verl/workers/engine/fsdp/utils.py38-56
FSDP Ulysses mesh + dp_rankverl/workers/engine/fsdp/transformer_impl.py196-213 · 570-583
Megatron parallel_state 初始化verl/workers/engine/megatron/transformer_impl.py131-158
Rollout 3D mesh (dp, infer_tp, infer_pp)verl/workers/engine_workers.py587-607