学习主题:VERL 源码精读 01 文章定位:从整体架构理解 HybridFlow 编程模型,并建立后续阅读 VERL 源码的全局地图 阅读目标:看完本文后,能够解释 VERL 为什么要拆分 controller、worker、rollout、reward、DataProto、resource pool,以及一次 PPO/GRPO 训练在源码中大致如何流动。
1. VERL 的定位
VERL,全称 Volcano Engine Reinforcement Learning for LLMs,是一个面向大语言模型后训练的强化学习训练库。它的核心目标不是只实现某一个算法,而是提供一套可以承载 PPO、GRPO、DAPO、GSPO、ReMax、RLOO、REINFORCE++ 等算法的 RL 训练基础设施。
从源码视角看,VERL 最重要的定位是:
它不是一个单机 PyTorch PPO 脚本,而是一个把 RL 控制流和大模型分布式计算流解耦的训练系统。这个定位决定了 VERL 的目录结构、数据结构和运行方式。阅读 VERL 时,如果只盯着某一个 loss 函数,很容易迷路;正确入口应该是先理解它如何把一次 RL 训练拆成几类高层算子:
sample prompts-> rollout generation-> compute log probabilities-> compute reference log probabilities-> compute rewards-> compute advantages / returns-> update actor-> update critic-> validation-> checkpoint这些步骤每一步背后都可能是一个分布式模型服务或分布式训练任务。VERL 的核心价值,就是让用户可以像写单进程训练流程一样编排这些高层步骤,同时把真正重的模型计算交给 Ray worker、FSDP、Megatron、vLLM、SGLang 等底层系统执行。
2. HybridFlow 的核心思想
VERL 是 HybridFlow 论文的开源实现。HybridFlow 要解决的问题是:大模型 RLHF / RLAIF / GRPO 训练同时具有复杂的算法控制流和复杂的分布式模型计算流。
普通神经网络训练也可以看成 dataflow:
input-> linear-> activation-> loss-> backward-> optimizer step这里的节点通常是 tensor operator,边是 tensor 的前向和反向传播。
但是 LLM 强化学习的 dataflow 粒度更粗。PPO/GRPO 里的节点不是一个 matmul,而是“生成一批回答”“计算旧策略 log prob”“跑 reward model”“计算 advantage”“训练 actor 一轮”这种高层任务。
可以把它看成两层 dataflow:
第一层:RL 控制流 描述算法步骤如何衔接。 例如先 rollout,再 reward,再 advantage,再 update actor。第二层:模型计算流 描述每个高层步骤内部如何做分布式模型计算。 例如 actor 用 FSDP 训练,rollout 用 vLLM 推理,critic 用 Megatron 训练。HybridFlow 的关键选择是:控制流和计算流分离。
也就是说,PPO/GRPO 的算法主循环运行在一个 controller 进程里;真正的模型 forward、generation、backward、optimizer step 运行在多个远程 worker 里。
这种设计带来两个直接好处:
1. 算法控制流容易改。 想从 PPO 切到 GRPO,主要改 advantage/reward/loss 等逻辑, 不需要重写 FSDP、Megatron、vLLM 等底层计算代码。2. 计算后端容易换。 同一套 trainer 控制流可以接 FSDP、FSDP2、Megatron、TorchTitan、VeOmni, rollout 也可以接 vLLM、SGLang、TRT-LLM。代价也很明确:controller 和 worker 之间会有数据传输开销。因此 VERL 必须非常重视数据协议、dispatch/collect、placement、colocation、weight update、padding、sequence balancing 等工程细节。
3. VERL 的源码地图
先看顶层目录。对理解训练主链路最重要的是这些部分:
verl/ trainer/ main_ppo.py main_ppo_v0.py ppo/ ray_trainer.py core_algos.py metric_utils.py reward.py utils.py v1/ trainer_base.py agent_loop_tq.py replay_buffer.py utils.py workers/ engine_workers.py utils/ losses.py rollout/ vllm_rollout/ sglang_rollout/ trtllm_rollout/ reward_manager/ engine/ fsdp/ megatron/ torchtitan/ veomni/ single_controller/ base/ worker_group.py decorator.py ray/ base.py utils/ dataset/ reward_score/ checkpoint/ profiler/ protocol.py这些目录可以按职责分成六层。
第一层是入口层:
verl/trainer/main_ppo.py它负责 Hydra 配置加载、Ray 初始化、TaskRunner 启动,以及选择 V0/V1 trainer。
第二层是 trainer 控制层:
verl/trainer/ppo/ray_trainer.pyverl/trainer/ppo/v1/trainer_base.py它们负责实现 RL 主循环。所谓主循环,就是训练一步里到底先调哪个 worker,后调哪个 worker,哪些结果需要合并,什么时候 validation,什么时候 checkpoint。
第三层是分布式控制层:
verl/single_controller/base/worker_group.pyverl/single_controller/ray/base.py它负责把 controller 里的一个方法调用,变成对多个 Ray worker 的分发和收集。
第四层是 worker 层:
verl/workers/engine_workers.py它定义 actor、rollout、reference、critic、reward model 等角色如何组合成 worker。
第五层是模型引擎和 rollout 后端:
verl/workers/engine/fsdp/verl/workers/engine/megatron/verl/workers/rollout/vllm_rollout/verl/workers/rollout/sglang_rollout/它们处理真正的大模型训练和推理。
第六层是数据协议和算法函数:
verl/protocol.pyverl/trainer/ppo/core_algos.pyverl/workers/utils/losses.py其中 DataProto 是 controller 和 worker 之间传递 batch 的标准结构;core_algos.py 实现 advantage、KL、PPO clip、GRPO 等算法核心;losses.py 把 actor/critic 的模型输出接到具体 loss。
4. 一次训练从哪里进入
VERL 的 PPO/GRPO 训练入口在:
verl/trainer/main_ppo.py入口函数是:
@hydra.main(config_path="config", config_name="ppo_trainer", version_base=None)def main(config): auto_set_device(config) validate_config( config=config, use_reference_policy=need_reference_policy(config), use_critic=need_critic(config), ) if config.trainer.use_v1: run_ppo(config, task_runner_class=TaskRunnerV1) else: from verl.trainer.main_ppo_v0 import TaskRunner run_ppo(config, task_runner_class=TaskRunner)这段代码做了三件事。
第一,Hydra 读取默认配置:
verl/trainer/config/ppo_trainer.yaml用户在命令行里写的:
algorithm.adv_estimator=grpoactor_rollout_ref.rollout.n=8trainer.n_gpus_per_node=8都会被 Hydra merge 到最终 config 里。
第二,validate_config() 根据配置检查训练是否自洽。例如:
是否需要 reference policy是否需要 criticbatch size 是否和并行配置匹配rollout / actor / critic 配置是否完整第三,根据 trainer.use_v1 选择 trainer 版本。当前主线推荐 V1 trainer;旧的 main_ppo_v0.py 和 ray_trainer.py 仍然很重要,因为大量架构概念、worker mapping 和核心算法逻辑仍然可以从 V0 代码中清晰看到。
5. run_ppo() 做了什么
run_ppo(config, task_runner_class) 是实际启动分布式训练的函数。它的职责不是训练模型,而是启动 Ray 环境和远程 TaskRunner。
核心逻辑可以压缩成:
if not ray.is_initialized(): runtime_env = get_ppo_ray_runtime_env(config) ray.init(..., runtime_env=runtime_env)runner = task_runner_class.remote()ray.get(runner.run.remote(config))这里有一个重要设计:TaskRunner 本身也是 Ray remote actor。
也就是说,真正的 trainer 主循环不是直接在本地 Python 主进程里跑,而是放进 Ray actor 中执行。这样做便于统一管理 runtime environment、profiling、Ray timeline、worker 资源和远程调度。
从分层视角看:
命令行进程 -> main_ppo.py -> ray.init() -> TaskRunner.remote() -> TaskRunner.run(config) -> trainer.fit()这就是 VERL 训练作业的最外层骨架。
6. V1 TaskRunner 的职责
在 main_ppo.py 中,V1 的 TaskRunner 定义如下:
@ray.remoteclass TaskRunnerV1: def run(self, config: DictConfig): import transfer_queue as tq from verl.trainer.ppo.v1 import get_trainer_cls trainer_cls = get_trainer_cls(config.trainer.v1.trainer_mode) config.transfer_queue.enable = True tq.init(config.transfer_queue) try: self.trainer = trainer_cls(config=config) self.trainer.init() self.init_agent_loop_manager() self.trainer.fit(self.agent_loop_manager) finally: tq.close()V1 相比旧版 trainer,一个明显变化是引入了 TransferQueue 和 AgentLoopManager。
可以先不深入细节,只记住:
V0 更像同步主循环: controller 每一步拿到 DataProto,调用 worker,拿回结果,再继续下一步。V1 更强调异步数据流: rollout / reward / actor update 等环节通过 TransferQueue 传递 KV batch, trainer 和 agent loop 可以更灵活地并发推进。第一篇只建立全局理解。后续读到 V1 trainer 时,再专门分析 trainer_base.py、agent_loop_tq.py、replay_buffer.py 和 transfer_queue 的数据流。
7. Trainer 是算法控制流
理解 VERL 时,要始终把 trainer 看成“算法控制流”的实现。
以 V0 的 RayPPOTrainer 为例,它位于:
verl/trainer/ppo/ray_trainer.py构造函数里保存了这些关键信息:
self.config = configself.role_worker_mapping = role_worker_mappingself.resource_pool_manager = resource_pool_managerself.use_reference_policy = need_reference_policy(self.config)self.use_rm = need_reward_model(self.config)self.use_critic = need_critic(self.config)self.ray_worker_group_cls = ray_worker_group_cls这些变量对应的是一次 RL 训练中的“角色系统”。
Actor: 当前要训练的策略模型。Rollout: 用当前策略生成 response 的推理后端。RefPolicy: 冻结参考模型,用于计算 KL 或 reference log prob。Critic: value model,用于 PPO/GAE。RewardModel: 模型式奖励函数。Reward function: 规则式奖励函数,通常在 Python 侧执行。Trainer 不直接关心某个模型到底是 FSDP 还是 Megatron,也不直接关心 rollout 是 vLLM 还是 SGLang。它关心的是这些角色是否存在,以及每一步该调用哪个角色。
这就是 HybridFlow 的“控制流独立”。
8. Role 和 ResourcePool
VERL 里,角色和资源是分开的。
角色描述“这个 worker 做什么”:
ActorRolloutActorRolloutCriticRefPolicyRewardModelActorRolloutRef资源池描述“这个 worker 放在哪些 GPU 上”:
global_pool_id -> [8 GPUs on node0, 8 GPUs on node1, ...]二者通过 mapping 关联:
resource_pool_spec = { global_pool_id: [config.trainer.n_gpus_per_node] * config.trainer.nnodes,}mapping = { Role.ActorRollout: global_pool_id, Role.Critic: global_pool_id, Role.RefPolicy: global_pool_id,}这种设计非常重要。因为大模型 RL 训练中,placement 会显著影响吞吐和显存。
常见 placement 有两类:
colocate: actor、rollout、reference 等角色共享同一组 GPU。 好处是权重同步更快,资源利用更紧凑。separate: actor、rollout、critic、reward model 分别占用不同资源池。 好处是并发度更高,适合更复杂的异步架构。VERL 把“角色是什么”和“角色放在哪”拆开,使得同一套算法控制流可以适配不同集群规模。
9. WorkerGroup 是 controller 和 worker 的代理层
在 VERL 中,controller 不应该直接手写一堆:
worker.method.remote(...)ray.get(...)torch.cat(...)因为这会让算法代码和分布式通信细节耦合在一起。
VERL 用 WorkerGroup 解决这个问题。
核心文件是:
verl/single_controller/base/worker_group.pyverl/single_controller/ray/base.pyWorkerGroup 管理一组远程 worker,并把 worker 上被 @register 装饰的方法绑定成 controller 可以调用的方法。
源码中 _bind_worker_method() 会扫描 worker class 的方法:
for method_name in dir(user_defined_cls): method = getattr(user_defined_cls, method_name) if hasattr(method, MAGIC_ATTR): attribute = getattr(method, MAGIC_ATTR) dispatch_mode = attribute["dispatch_mode"] execute_mode = attribute["execute_mode"] blocking = attribute["blocking"] ... bind a new method to the RayWorkerGroup这段逻辑说明:worker 方法不是随便暴露给 controller 的。只有被 @register 标记的方法,才会自动绑定到 WorkerGroup。
一个典型 worker 方法长这样:
@register(dispatch_mode=make_nd_compute_dataproto_dispatch_fn(mesh_name="actor"))def compute_log_prob(self, data: TensorDict) -> TensorDict: output = self.actor.infer_batch(data) return output.cpu() if output is not None else None这里的 dispatch_mode 表示:
输入数据如何拆分给多个 worker每个 worker 执行什么输出数据如何收集回来所以,controller 看到的是:
old_log_prob = actor_rollout_ref_wg.compute_log_prob(batch)底层实际发生的是:
DataProto / TensorDict 被切分-> 分发到多个 Ray worker-> 每个 worker 在本地 GPU 上 forward-> 收集结果-> 合并回 controller这就是 VERL 能把分布式调用写得像单进程函数调用的关键。
10. ActorRolloutRefWorker 是混合角色 worker
大模型 RL 训练里,actor、rollout、reference policy 之间关系很紧。
actor: 训练中的策略模型,需要 backward 和 optimizer step。rollout: 用策略模型生成 response,需要高吞吐推理。reference: 冻结参考模型,用于 KL 约束。如果把它们完全分开,会有大量权重同步和显存管理问题。VERL 因此提供了 ActorRolloutRefWorker:
verl/workers/engine_workers.py它可以根据 role 变成不同形态:
actorrolloutrefactor_rolloutactor_rollout_ref源码中可以看到:
self.role = roleself.actor = Noneself.ref = Noneself.rollout = Noneself._is_actor = self.role in ["actor", "actor_rollout", "actor_rollout_ref"]self._is_rollout = self.role in ["rollout", "actor_rollout", "actor_rollout_ref"]self._is_ref = self.role in ["ref", "actor_rollout_ref"]在 init_model() 中,它按角色分别初始化:
1. build reference model2. build actor model3. build rollout engine4. build checkpoint engine这就是 HybridEngine 的具体落点:同一个 worker 可以组合训练引擎和推理引擎。
例如 actor 部分会构造 TrainingWorker,并绑定 PPO loss:
self.loss_fn = partial(ppo_loss, config=actor_config)self.actor = self.actor_worker_cls(config=actor_training_config)self.actor.reset()self.actor.set_loss_fn(self.loss_fn)rollout 部分则根据配置选择推理后端:
rollout_cls = get_rollout_class(rollout_config.name, rollout_config.mode)self.rollout = rollout_cls( config=rollout_config, model_config=model_config, device_mesh=rollout_device_mesh,)这说明 actor 和 rollout 在源码层是两个不同计算语义:
actor 负责训练语义: log prob、backward、optimizer、checkpoint。rollout 负责推理语义: sampling、response generation、推理引擎 KV cache。但是它们可以被 colocate 到同一个 worker 中,以减少训练权重同步到推理引擎的成本。
11. DataProto 是跨模块数据协议
VERL 的数据结构核心是:
verl/protocol.py其中最重要的是 DataProto:
@dataclassclass DataProto: batch: TensorDict = None non_tensor_batch: dict = field(default_factory=dict) meta_info: dict = field(default_factory=dict)它有三部分。
第一部分是 batch:
TensorDict,存放 tensor 数据。例如 input_ids、attention_mask、responses、old_log_probs、advantages。第二部分是 non_tensor_batch:
存放不能放进 tensor 的 batch 级对象。例如 data_source、raw_prompt、reward_model 字段、多模态路径、字符串元信息。第三部分是 meta_info:
存放控制信息和统计信息。例如 temperature、do_sample、global_steps、micro batch 信息。为什么需要这个结构?
因为 RL 训练中的 batch 不只是 tensor。一个训练样本可能同时包含:
tokenized prompt tensorattention maskposition ids原始 prompt 文本数据来源reward function 所需的 ground truth多模态样本路径或对象采样参数训练 step 信息如果只用普通 dict,很难在 Ray worker、GPU tensor、CPU 元信息之间保持一致的 batch 维度和切分逻辑。DataProto 提供了统一操作:
len(data)data[i]data.slice(...)data.select(...)data.union(...)data.chunk(...)DataProto.concat(...)data.to(device)这也是为什么 VERL 的 dispatch/collect 能工作:WorkerGroup 知道如何切分和合并 DataProto。
12. 一次 PPO/GRPO 训练的抽象数据流
先不区分 V0/V1,抽象地看,一次 PPO/GRPO 训练 step 是:
1. 从 dataloader 取 prompts2. actor_rollout worker 生成 responses3. actor 计算 generated tokens 的 old_log_probs4. reference policy 计算 ref_log_probs5. critic 计算 values,GRPO 通常不需要 critic6. reward function 或 reward model 计算 rm_scores7. controller 计算 token_level_rewards8. controller 计算 advantages 和 returns9. actor worker 根据 advantages 更新策略10. critic worker 根据 returns 更新 value model11. 记录 metrics12. 按间隔 validation / checkpoint如果写成伪代码:
for batch in train_dataloader: batch = actor_rollout_wg.generate_sequences(batch) batch = batch.union(actor_wg.compute_log_prob(batch)) if use_reference_policy: batch = batch.union(ref_wg.compute_ref_log_prob(batch)) if use_critic: batch = batch.union(critic_wg.compute_values(batch)) batch = batch.union(reward_fn_or_rm(batch)) batch = compute_advantage(batch) if use_critic: critic_wg.update_critic(batch) actor_wg.update_actor(batch)HybridFlow 的重点不在于这个伪代码多复杂,而在于它让这段伪代码背后的每一步都可以映射到不同分布式后端。
例如:
generate_sequences: 可能由 vLLM/SGLang/TRT-LLM 执行。compute_log_prob: 可能由 FSDP/Megatron/VeOmni actor 执行。compute_values: 可能由 critic TrainingWorker 执行。update_actor: 可能触发分布式 backward、梯度同步、optimizer step。compute_advantage: 通常在 controller 上执行,因为它是算法控制逻辑,不是大模型计算。13. Reward 在整体架构中的位置
Reward 是 RLHF/GRPO 系统里最容易混淆的一层。VERL 同时支持:
规则奖励函数: 例如数学题 answer match、代码题单元测试、格式规则。模型式 reward model: 例如 sequence classification reward model。沙箱式 reward: 例如代码执行、工具调用验证。在原始 PPO 架构中,reward 的输出最终要变成 token-level score:
token_level_scores对于很多 outcome reward,它本来只有一个序列级标量:
reward(prompt, response) -> scalarVERL 通常会把这个 scalar 放到 response 的最后一个有效 token 上,然后再和 KL penalty、advantage 计算结合。
这体现了 VERL 的一个通用约定:
reward 函数可以是任务相关的,但进入 RL 算法主干时,需要变成统一的数据字段。后续阅读 reward 系统时,要重点关注两个问题:
1. reward 是在哪里计算的: controller、reward manager、remote reward model、sandbox,还是 worker?2. reward 以什么字段进入算法: rm_scores、token_level_scores、token_level_rewards。14. Algorithm core 不负责分布式
VERL 的算法核心主要在:
verl/trainer/ppo/core_algos.py这个文件很长,但职责可以概括为:
KL controllerreward with KL penaltyGAE advantageGRPO advantageRLOO / ReMax / DAPO / GSPO 等 advantage 或 policy lossPPO clipped policy lossvalue lossentropyKL penalty variants注意它不负责 Ray,也不负责 FSDP,也不负责 vLLM。
例如 GRPO 的核心思想在算法层就是:
同一个 prompt 下采样多个 response对这些 response 的 reward 做组内均值和标准差归一化得到 group-relative advantage而“如何让同一个 prompt 采样多个 response”“这些 response 如何在分布式 worker 间传递”“最终如何更新 actor”,属于 trainer、rollout 和 worker 层的事情。
这也是 VERL 源码阅读中一个重要分界:
core_algos.py: 数学逻辑。ray_trainer.py / trainer_base.py: 训练流程。engine_workers.py: worker 角色。workers/engine/*: 模型训练后端。workers/rollout/*: 推理生成后端。15. 为什么 VERL 不是简单的“PPO 代码库”
如果只看 PPO 公式,训练流程似乎很简单:
ratio = pi_new / pi_oldloss = min(ratio * A, clip(ratio) * A)但在大模型场景,真正困难的是系统层:
1. rollout 需要高吞吐推理引擎。2. actor update 需要分布式训练引擎。3. rollout 和 actor 共享权重,但服务形态不同。4. reference model 可能需要额外显存。5. reward 可能是规则、模型、沙箱或工具系统。6. 一个 batch 里既有 tensor,也有字符串、图片、视频、ground truth 等非 tensor 信息。7. 不同算法需要不同 advantage、KL、loss、采样和过滤逻辑。8. 多机多卡下必须处理 placement、dispatch、collect、checkpoint、metrics 和 fault tolerance。VERL 的架构就是围绕这些问题展开的。它把系统拆成几类稳定接口:
Trainer: 编排 RL 控制流。WorkerGroup: 把 controller 调用映射到分布式 worker。Worker: 封装 actor、rollout、ref、critic、reward model 等角色。Engine: 对接 FSDP、Megatron、TorchTitan、VeOmni 等训练后端。Rollout: 对接 vLLM、SGLang、TRT-LLM 等推理后端。DataProto: 承载跨模块、跨进程传输的数据协议。core_algos: 提供 PPO/GRPO 等算法数学实现。这套拆分就是 HybridFlow 的代码化表达。
16. 后续源码阅读路线
第一篇建立全局地图。后续阅读应该按训练发生顺序推进,而不是按目录名字随意跳。
推荐路线如下:
01. main_ppo.py 训练入口、Hydra、Ray、TaskRunner。02. ppo_trainer.yaml / config dataclass 配置如何决定角色、资源、算法和后端。03. rl_dataset.py / DataProto 数据如何从 parquet 进入训练 batch。04. WorkerGroup / RayWorkerGroup controller 如何调用远程 worker。05. engine_workers.py ActorRolloutRefWorker 和 TrainingWorker 如何组织模型角色。06. rollout backends vLLM/SGLang 如何生成 responses。07. reward manager rule reward 和 reward model 如何接入。08. core_algos.py PPO、GRPO、KL、advantage、policy loss。09. workers/utils/losses.py actor/critic 的模型输出如何变成 loss。10. ray_trainer.py 或 v1/trainer_base.py 一次训练 step 如何从开始走到结束。11. checkpoint / metrics / profiler 训练如何保存、观测和调试。12. experimental/agent_loop 和 reward_loop 多轮交互、工具调用、异步 reward 如何扩展。这个顺序的好处是:每读一个文件,都知道它在训练链路中的位置。
17. 本篇压缩总结
VERL 的整体架构可以压缩成一句话:
VERL 用 HybridFlow 把 RL 算法控制流和大模型分布式计算流解耦,让 controller 像写单进程程序一样编排 PPO/GRPO,同时让 actor、rollout、critic、reward 等高成本计算由 Ray workers 和不同后端执行。本篇需要记住六个核心概念:
1. HybridFlow: 控制流和计算流分离。2. Trainer: RL 算法主循环的 controller。3. WorkerGroup: controller 调用远程 workers 的代理层。4. ActorRolloutRefWorker: actor、rollout、reference policy 的混合角色 worker。5. DataProto: tensor、非 tensor 和 meta 信息的统一 batch 协议。6. core_algos: PPO/GRPO/KL/advantage/loss 的数学实现层。第一篇读完后,再去看 main_ppo.py 就不会觉得它只是一个启动脚本。它实际上是整个 HybridFlow 执行图的入口:配置进入这里,Ray 在这里启动,TaskRunner 在这里创建,trainer 主循环从这里开始。
下一篇适合进入:
VERL 源码精读 02:main_ppo.py 如何启动一次 PPO/GRPO 训练那一篇会沿着真实调用链继续展开:
hydra main-> validate_config-> run_ppo-> ray.init-> TaskRunnerV1.run-> trainer.init-> trainer.fit18. 本文参考的 VERL 原始文件
README.mddocs/hybrid_flow.rstdocs/examples/ppo_code_architecture.rstverl/trainer/main_ppo.pyverl/trainer/ppo/ray_trainer.pyverl/trainer/ppo/v1/trainer_base.pyverl/single_controller/base/worker_group.pyverl/single_controller/ray/base.pyverl/workers/engine_workers.pyverl/protocol.pyverl/trainer/ppo/core_algos.pyverl/workers/utils/losses.py
夜雨聆风