Prhub

#2743 Skip non-numeric rewards in the episode average

原始 PR 作者 yueming-yuan 合并时间 2026-08-26 08:43 文件变更 1 提交数 1 评论 0 代码增减 +3 / -1

执行摘要

跳过非数值奖励,修复 OPD 训练崩溃

PR body 指出:_compute_training_sample_metrics(#2710)对每个 rollout 的原始奖励取平均。当 --reward-key 未设置时,Sample.get_reward_value 返回自定义奖励函数产生的任意结果——on-policy distillation 返回教师模型的评分响应,导致 sum(rewards) 抛出 TypeError: unsupported operand type(s) for +: 'int' and 'dict',使整个 rollout 崩溃。该问题已在 CI run 32803020540 中出现,必须修复以确保训练流程稳定。

值得精读,作为处理自定义奖励函数返回非数值类型的防御性编程范例。建议阅读 miles/ray/rollout/metrics.py 中的 _compute_training_sample_metrics 函数,并考虑补充测试覆盖非数值奖励场景。

讨论亮点

Review 过程中,Claude 机器人仅提示了配置,Shi-Dong 批准了 PR,无实质讨论。PR body 中作者详细说明了设计权衡:虽然 OPD 的标量奖励定义为 0.0,但共享指标助手不应假设所有自定义奖励函数都返回标量,因此选择跳过非数值类型而非强制转换。

实现拆解

  1. miles/ray/rollout/metrics.py 中导入 Number 类型(来自 numbers 模块),用于判断奖励是否为数值。
  2. 修改 _compute_training_sample_metrics 中的奖励收集逻辑:使用 isinstance(raw_reward, Number) 检查,仅当奖励为数值时才将其加入对应 rollout 的奖励列表。
  3. 非数值奖励不会影响 num_training_samples 的计数,只会被排除在平均奖励计算之外,同时保留 episode_raw_reward 的逻辑,若所有奖励均为非数值则返回 0.0。
    此修改不影响样本计数逻辑,且与后续 post_process_rewards 的 OPD 处理兼容。
文件 模块 状态 重要度
miles/ray/rollout/metrics.py 指标计算 modified 5.23

关键符号

_compute_training_sample_metrics

关键源码片段

miles/ray/rollout/metrics.py bugfix

核心修复文件:在奖励平均计算中增加类型检查,跳过非数值奖励。

from numbers import Numberdef _compute_training_sample_metrics(args, samples):
    """计算训练样本指标:跳过非数值奖励。"""
    rewards_by_rollout = {}
    use_metadata_reward = bool(samples and samples[0].metadata and "raw_reward" in samples[0].metadata)
    for position, sample in enumerate(samples):
        # 确定 rollout 键
        if sample.rollout_id is not None:
            rollout_key = ("rollout", sample.group_index, sample.rollout_id)
        elif sample.index is not None:
            rollout_key = ("sample", sample.group_index, sample.index)
        else:
            rollout_key = ("position", sample.group_index, position)
        # 获取原始奖励
        raw_reward = sample.metadata["raw_reward"] if use_metadata_reward else sample.get_reward_value(args)
        # 仅保留数值奖励,非数值(如 dict)跳过避免 sum() 崩溃
        if isinstance(raw_reward, Number):
            rewards_by_rollout.setdefault(rollout_key, []).append(raw_reward)
    # 计算每个 rollout 的平均奖励
    rollout_rewards = [sum(rewards) / len(rewards) for rewards in rewards_by_rollout.values()]
    return {
        "num_training_samples": len(samples),
        "episode_raw_reward": sum(rollout_rewards) / len(rollout_rewards) if rollout_rewards else 0.0,
    }

评论区精华

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

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

风险与影响

  1. 该修改使用 isinstance(raw_reward, Number) 判断,对于 numpy 数值类型可能无法识别,因为 np.float32 等并不继承自 numbers.Number,可能导致有效数值奖励被错误跳过,影响指标准确性。
  2. 修改位于核心指标计算路径,若实际奖励为纯字符串等类型,该修改无法覆盖,仍需依赖具体实现。
  3. 没有新增测试,容易回归,需补充针对非数值奖励的单元测试。

影响范围限于训练指标计算,不影响训练本身。修复可防止 OPD 等自定义奖励函数导致整个训练任务崩溃,提高系统稳定性。对使用默认奖励配置(数值奖励)的用户无影响。

缺少测试覆盖 数值类型判断可能遗漏 numpy 类型

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论