Skip to content

源码地图

阅读 verl 最有效的方法,是把它看成一条训练样本的旅行路线,而不是一棵目录树。下面这张地图对应 verl/trainer/ppo/ray_trainer.pyRayPPOTrainer.fit() 主循环。

一条样本的生命线

Datadataloader 输出 prompt、data_source、reward_model、extra_info。
DataProtoDataProto.from_single_dict() 包装 tensor、非 tensor 与 metadata。
Rolloutasync_rollout_manager.generate_sequences() 调 vLLM/SGLang 生成 response。
Rewardreward manager 或 reward loop 写入 rm_scores / token_level_scores
Logprob重算 old logprob、ref logprob、values。
AdvantageGAE、GRPO、RLOO 等估计优势。
Update更新 critic 和 actor,同步 rollout 权重。

主线文件

你想知道什么文件关键符号
一次 PPO/GRPO step 怎么组织?verl/trainer/ppo/ray_trainer.pyRayPPOTrainer.fit()
reward 怎么变成 advantage?verl/trainer/ppo/ray_trainer.pycore_algos.pyapply_kl_penalty()compute_advantage()
PPO、GRPO、RLOO 的公式在哪里?verl/trainer/ppo/core_algos.pyAdvantageEstimatorcompute_policy_loss_vanilla()
batch 为什么能在分布式模块间流动?verl/protocol.pyDataProtounion()repeat()concat()
actor/critic/ref worker 怎么执行?verl/workers/engine_workers.pyTrainingWorkerActorRolloutRefWorker
vLLM/SGLang 怎么接入?verl/workers/rolloutBaseRolloutServerAdapter、replica
奖励函数怎么接入?verl/workers/reward_managerverl/experimental/reward_loopAbstractRewardManager、registry、reward loop
配置如何进入这些对象?verl/trainer/configverl/workers/configOmegaConf、dataclass config、Hydra override

一次 batch 的伪代码

这段伪代码不是为了替代源码,而是帮你读源码时知道“现在看到哪一步了”。

python
# 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_idsattention_maskposition_ids:模型 forward / rollout 所需 tensor。
  • data_source:reward manager 根据它选择打分函数。
  • reward_modelextra_info:规则奖励或验证器可能需要的标准答案、测试信息。

2. DataProto:把多种信息放进同一个协议

DataProto.from_single_dict() 会把 torch tensor 放入 batch,把 numpy/object 信息放入 non_tensor_batch。后面 union()repeat()select()split() 都会同时维护这些信息的 batch 对齐关系。

读源码时要养成一个习惯:每看到一个字段,先问它在 batchnon_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。

读源码时的三个锚点

  1. 看字段:DataProto.batch 里通常是 tensor,例如 responsesattention_maskold_log_probsadvantages
  2. 看身份:DataProto.non_tensor_batch 里通常是字符串、对象、uid、data_source、reward_model。
  3. 看控制: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.pyRayPPOTrainer.fit()_get_gen_batch()_compute_old_log_prob()_update_actor()
  • verl/trainer/ppo/core_algos.pyAdvantageEstimator、GAE、GRPO、RLOO、policy loss。
  • verl/protocol.pyDataProto.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 数据流说明。

面向源码阅读的 verl 学习文档。