Prhub

#6242 [reward] refactor: refactor RM score assembly in reward loop

原始 PR 作者 zhtmike 合并时间 2026-05-06 11:29 文件变更 3 提交数 1 评论 0 代码增减 +50 / -25

执行摘要

提取奖励分数组装为可覆写钩子

如 PR body 所述,目的是将奖励分数组装逻辑提取为可覆写的方法,以便下游包能够处理不同的奖励组装方式,如处理图像、视频等场景。

建议精读,这是一个典型的重构样例:从函数中提取职责,为扩展开放接口。值得关注的决策是使用 classmethod 而非实例方法,因为组装逻辑通常不需要实例状态。

讨论亮点

无 review 评论。只有 gemini-code-assist[bot] 的总结性评论和 SamitHuang 的批准,无实质性讨论。

实现拆解

  1. 新增 resolve_reward_manager_cls 函数:在 verl/trainer/ppo/reward.py 中新增该函数,从配置中解析出奖励管理器类而不实例化它。该函数处理 registerimportlib 两种来源,并处理未知来源的异常。原有的 load_reward_manager 函数中对应的内联代码被替换为调用此函数。

  2. 新增 assemble_rm_scores 类方法:在 verl/experimental/reward_loop/reward_manager/base.pyRewardManagerBase 中新增 assemble_rm_scores 类方法。该方法的默认实现根据 promptsresponsesattention_mask 计算每个样本的有效响应长度,并将奖励分数放置在最后一个有效 token 位置。此方法可被子类覆写以支持自定义组装逻辑。

  3. 更新 RewardLoopManager:在 verl/experimental/reward_loop/reward_loop.py 中,RewardLoopManager 新增 self.reward_manager_cls 属性,通过 resolve_reward_manager_cls 解析类。在 compute_rm_score 方法中,原本内联的分数组装逻辑被替换为 self.reward_manager_cls.assemble_rm_scores(data, scores) 调用,同时移除了对 torch 的直接导入。

文件 模块 状态 重要度
verl/trainer/ppo/reward.py 训练器 modified 7.05
verl/experimental/reward_loop/reward_manager/base.py 奖励循环 modified 6.88
verl/experimental/reward_loop/reward_loop.py 奖励循环 modified 5.94

关键符号

resolve_reward_manager_cls assemble_rm_scores

关键源码片段

verl/trainer/ppo/reward.py core-logic

新增 `resolve_reward_manager_cls` 函数,将奖励管理器类解析从 `load_reward_manager` 中提取,提升了代码可复用性。配置键和异常处理也在此文件改动。

# verl/trainer/ppo/reward.py ( 新增函数 )
# 从 config 解析出 RewardManager 类,不执行实例化
# 支持两种来源:"register" 和 "importlib"
def resolve_reward_manager_cls(config: DictConfig) -> type[RewardManagerBase]:
    reward_manager_cfg: RewardManagerConfig = config.reward.reward_manager
    if reward_manager_cfg.source == "register":
        from verl.experimental.reward_loop.reward_manager import get_reward_manager_cls
        return get_reward_manager_cls(reward_manager_cfg.name)
    elif reward_manager_cfg.source == "importlib":
        from verl.utils.import_utils import load_extern_object
        module_cfg: ModuleConfig | None = reward_manager_cfg.module
        assert module_cfg is not None and module_cfg.path is not None, (
            f"Module path is required when {reward_manager_cfg.source=}, but got {module_cfg=}"
        )
        return cast(
            "type[RewardManagerBase]",
            load_extern_object(module_path=module_cfg.path, object_name=reward_manager_cfg.name),
        )
    else:
        raise ValueError(f"Unknown reward manager source: {reward_manager_cfg.source}")
verl/experimental/reward_loop/reward_manager/base.py core-logic

新增 `assemble_rm_scores` 类方法,定义了默认的奖励分数组装逻辑,支持子类覆写以处理多模态等场景。

# verl/experimental/reward_loop/reward_manager/base.py (RewardManagerBase 新增方法 )
import torch@classmethod
def assemble_rm_scores(cls, data: DataProto, scores: list[float]) -> torch.Tensor:
    """将每个样本的 reward scores 组装为 rm_scores 张量。    默认实现将分数放到每个样本的最后一个有效 token 位置。
    子类可覆写以支持图像、视频等自定义组装逻辑。
    """
    prompt_length = data.batch["prompts"].size(1)
    # 计算每个样本的有效响应长度(排除 padding)
    valid_response_length = data.batch["attention_mask"][:, prompt_length:].sum(dim=1)
    rm_scores = torch.zeros_like(data.batch["responses"], dtype=torch.float32)
    # 将分数放置到最后一个有效 token 位置
    rm_scores[torch.arange(rm_scores.size(0), device=rm_scores.device), valid_response_length - 1] = (
        rm_scores.new_tensor(scores)
    )
    return rm_scores

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

风险较低。变更位于奖励循环模块,且已被 reviewer 验证不会破坏现有功能。但需要注意:

  • 如果下游包覆写了 assemble_rm_scores 但未正确处理 attention_mask 或张量设备,可能导致设备不匹配错误。
  • RewardLoopManager 中原来的 torch.tensor(scores, dtype=torch.float32) 现在通过 rm_scores.new_tensor(scores) 创建,确保类型和 device 与 rm_scores 一致,这是一个隐式改进,但若 rm_scores 不在 CPU 上,则 new_tensor 会在该 device 上创建,不会引入风险。
  • 用户:无直接影响,API 未改变。
  • 系统:提高了代码复用性和可扩展性,reward 循环的分数组装逻辑现在可通过继承定制。
  • 团队:降低了维护成本,插件化设计便于引入新功能。
无实质性讨论 改动集中在非核心路径 已获 reviewer 批准

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论