源码地图
阅读 verl 最有效的方法,是把它看成一条训练样本的旅行路线,而不是一棵目录树。下面这张地图对应 verl/trainer/ppo/ray_trainer.py 的 RayPPOTrainer.fit() 主循环。
一条样本的生命线
DataProto.from_single_dict() 包装 tensor、非 tensor 与 metadata。async_rollout_manager.generate_sequences() 调 vLLM/SGLang 生成 response。rm_scores / token_level_scores。主线文件
| 你想知道什么 | 文件 | 关键符号 |
|---|---|---|
| 一次 PPO/GRPO step 怎么组织? | verl/trainer/ppo/ray_trainer.py | RayPPOTrainer.fit() |
| reward 怎么变成 advantage? | verl/trainer/ppo/ray_trainer.py、core_algos.py | apply_kl_penalty()、compute_advantage() |
| PPO、GRPO、RLOO 的公式在哪里? | verl/trainer/ppo/core_algos.py | AdvantageEstimator、compute_policy_loss_vanilla() |
| batch 为什么能在分布式模块间流动? | verl/protocol.py | DataProto、union()、repeat()、concat() |
| actor/critic/ref worker 怎么执行? | verl/workers/engine_workers.py | TrainingWorker、ActorRolloutRefWorker |
| vLLM/SGLang 怎么接入? | verl/workers/rollout | BaseRollout、ServerAdapter、replica |
| 奖励函数怎么接入? | verl/workers/reward_manager、verl/experimental/reward_loop | AbstractRewardManager、registry、reward loop |
| 配置如何进入这些对象? | verl/trainer/config、verl/workers/config | OmegaConf、dataclass config、Hydra override |
一次 batch 的伪代码
这段伪代码不是为了替代源码,而是帮你读源码时知道“现在看到哪一步了”。
# 1. dataloader -> DataProto
batch = DataProto.from_single_dict(batch_dict)
batch.non_tensor_batch["uid"] = make_uid_for_each_prompt()
# 2. 只取生成需要的字段,但保留 reward 所需非 tensor 信息
gen_batch = trainer._get_gen_batch(batch)
gen_batch = gen_batch.repeat(repeat_times=rollout_n, interleave=True)
# 3. rollout server 生成 responses
gen_output = async_rollout_manager.generate_sequences(gen_batch)
# 4. 原 batch 也 repeat,对齐 rollout.n 条 response
batch = batch.repeat(repeat_times=rollout_n, interleave=True)
batch = batch.union(gen_output)
batch.batch["response_mask"] = compute_response_mask(batch)
# 5. 规则奖励、模型奖励或 reward loop
reward = trainer._compute_reward_colocate(batch)
batch = batch.union(reward)
reward_tensor, extra = extract_reward(batch)
# 6. 训练前重算概率和值函数
old_log_prob = trainer._compute_old_log_prob(batch)
ref_log_prob = trainer._compute_ref_log_prob(batch)
values = trainer._compute_values(batch)
batch = batch.union(old_log_prob).union(ref_log_prob).union(values)
batch.batch["token_level_scores"] = reward_tensor
# 7. KL 和 advantage
if config.algorithm.use_kl_in_reward:
batch.batch["token_level_rewards"] = apply_kl_penalty(batch)
else:
batch.batch["token_level_rewards"] = reward_tensor
batch = compute_advantage(
batch,
adv_estimator=config.algorithm.adv_estimator,
num_repeat=config.actor_rollout_ref.rollout.n,
)
# 8. worker 侧训练
trainer._update_critic(batch)
trainer._update_actor(batch)
trainer.checkpoint_manager.update_weights(global_steps)每一步到底读什么
1. Data:先看字段,不要先看模型
dataloader 输出的不是“纯文本 prompt”,而是已经 tokenized/padded 的张量,以及 reward 所需的非 tensor 信息。你在源码里常见:
input_ids、attention_mask、position_ids:模型 forward / rollout 所需 tensor。data_source:reward manager 根据它选择打分函数。reward_model、extra_info:规则奖励或验证器可能需要的标准答案、测试信息。
2. DataProto:把多种信息放进同一个协议
DataProto.from_single_dict() 会把 torch tensor 放入 batch,把 numpy/object 信息放入 non_tensor_batch。后面 union()、repeat()、select()、split() 都会同时维护这些信息的 batch 对齐关系。
读源码时要养成一个习惯:每看到一个字段,先问它在 batch、non_tensor_batch 还是 meta_info 里。
3. Rollout:生成服务返回新字段
async_rollout_manager.generate_sequences() 不是普通的 model.generate()。它会把 prompt 送给 rollout server,并返回包含 response token、mask、可能的 rollout logprob、timing 等信息的 DataProto。
rollout.n 会让同一个 prompt 生成多条 response。对 PPO 这只是扩大样本;对 GRPO/RLOO,它是组内比较的基础。
4. Reward:标量奖励如何变成 token-level tensor
很多任务只有整条 response 的标量分数,例如 GSM8K 答对为 1。verl 通常把这个分数写到最后一个有效 response token 上,形成 token-level tensor。这样后面的 GAE、GRPO 和 loss 都可以统一按 [batch, response_length] 处理。
5. Logprob / Value:训练前补齐信号
PPO-style update 不是拿 rollout 输出直接训练。它还需要:
old_log_probs:旧策略概率,构造 ratio。ref_log_prob:reference policy 概率,构造 KL。values:critic 估计值,GAE 需要它。
这些计算在 trainer 里先把 DataProto 转为 TensorDict,再经 no-padding 处理后派发给 worker。
6. Advantage:算法差异的分叉点
compute_advantage() 是从“数据流”进入“算法差异”的关键分叉:
adv_estimator | 需要 critic 吗 | 直觉 |
|---|---|---|
| GAE | 需要 | 用 value baseline 做 token 级优势估计 |
| GRPO | 不需要 | 同 prompt 多 response 组内比较 |
| RLOO | 不需要 | leave-one-out baseline |
| ReMax | 不需要 | 用参考生成作为 baseline |
7. Update:actor/critic 真正训练发生在 worker 侧
_update_actor() 和 _update_critic() 仍然在 driver 里被调用,但真正 forward/backward 在 worker 上执行。@register 和 dispatch mode 决定一个大 batch 如何切给多个 worker,再如何收集 metrics。
读源码时的三个锚点
- 看字段:
DataProto.batch里通常是 tensor,例如responses、attention_mask、old_log_probs、advantages。 - 看身份:
DataProto.non_tensor_batch里通常是字符串、对象、uid、data_source、reward_model。 - 看控制:
DataProto.meta_info里通常是 temperature、global_steps、timing、validate 等运行信息。
常见迷路点
| 迷路点 | 正确读法 |
|---|---|
| “为什么 reward 是 token-level?” | 为了让 PPO/GAE/GRPO loss 都能用 response mask 统一处理。 |
| “为什么 rollout 后还要 compute_log_prob?” | rollout 负责生成,训练 loss 需要旧概率、当前概率、参考概率。 |
| “为什么 GRPO 要 uid?” | uid 标识同一个 prompt 的 sibling responses,组内标准化必须靠它分组。 |
| “为什么 worker 方法像本地函数?” | RayWorkerGroup 把远端 worker 的 @register 方法动态绑定成本地代理调用。 |
| “为什么先 sleep rollout 再 reward/update?” | colocate 场景下释放/复用显存,避免推理服务和训练/奖励模型抢 GPU。 |
本节参考与延伸阅读
verl/trainer/ppo/ray_trainer.py:RayPPOTrainer.fit()、_get_gen_batch()、_compute_old_log_prob()、_update_actor()。verl/trainer/ppo/core_algos.py:AdvantageEstimator、GAE、GRPO、RLOO、policy loss。verl/protocol.py:DataProto.union()、repeat()、split()、concat()、to_tensordict()。docs/hybrid_flow.rst:verl 把 RL 控制流和分布式计算流分离的设计动机。docs/single_controller.rst:@register、dispatch、collect 和RayWorkerGroup调用链。docs/workers/ray_trainer.rst:官方 Ray trainer 数据流说明。