Prhub

#2576 fix: skip rollout construction for debug replay

原始 PR 作者 guapisolo 合并时间 2026-08-18 17:58 文件变更 6 提交数 2 评论 6 代码增减 +175 / -17

执行摘要

调试回放跳过 rollout 构造,修复崩溃与 FT 误启动

PR body 指出 release 运行在 stage-c-8-gpu-h200 上因 GenerateState 报 TypeError: unsupported operand type(s) for *: int and NoneType 崩溃;根因是 #2522 让 class-based rollout 成为默认后,RolloutManager.init 急切构造 InferenceRolloutFn,而 replay 运行(--load-debug-rollout-data,强制 debug_train_only)故意留空 rollout_num_gpus。独立验证评论也确认了同一回归链:GenerateState.__init__args.sglang_server_concurrency * args.rollout_num_gpus // args.rollout_num_gpus_per_engine 计算信号量,rollout_num_gpus=None 直接触发 TypeError。此外,train-only FT(--ft-components train)仍激活 rollout 生命周期,导致确定性比较出现 10 个 MetricEvent 偏差。

值得精读,核心是“构造门控”与“生命周期门控”两个概念的分离:用 load_debug_rollout_data 控制是否构造 rollout fn,用 _rollout_ft_enabled 控制是否启动 rollout 侧的生命周期工作。关注作者对门控选择的对抗性论证(为什么不是 debug_train_only),以及 EvalFleet.pin 中探测委派的语义。建议后续为 RolloutManager 增加 generate_rollout is None 的显式类型保护,避免 None 静默传播。

讨论亮点

作者 guapisolo 在 issue 评论中给出了完整回归链:#2522 使 class-based rollout fn 成为默认后,RolloutManager.__init__ 急切构造 InferenceRolloutFn,replay 又故意留空 rollout_num_gpus,于是构造期即崩溃;并解释了为什么 load_debug_rollout_data 是正确的门控而 debug_train_only 是错的:独立 SFT 运行(如 scripts/run_qwen3_sft.pyexamples/geo3k_vlm/run_geo3k_vlm_sft.sh)把 rollout fn 当作实际数据生产者,_get_rollout_data 只绕过 generate_rollout 而不绕过构造。

CI 验证过程被多次更新:初始未验证 → 推送修复后开始验证 → 最终主栈未经 override 的情况下 28 passed / 2 skipped / 0 failed,之前失败的 test_trainer_ft_deterministic_dp2_cp2_real_rollout.py 通过。另有独立验证从 release-cut 失败出发、未参考本 PR 即收敛到同一 gate。

reviewer fzyzcjy 批准:"LGTM for trainer-ft related changes, since I refactor a lot in https://github.com/radixark/miles/issues/1837"。

实现拆解

  1. 回放构造门控:在 miles/ray/rollout/rollout_manager.pyRolloutManager.__init__ 中,把非 legacy 分支改为先判断 args.load_debug_rollout_data is not None;是则直接令 generate_rollouteval_generate_rolloutNone,不再调用 load_rollout_function,避免构造 GenerateState。同时把 _get_rollout_data 的判定从真值判断收紧为 is not None,保证语义一致。
  2. FT 生命周期统一门控:新增实例属性 self._rollout_ft_enabled = self.args.use_fault_tolerance and "rollout" in self.args.ft_components,并将健康监控启动、_ci_fault_injection_pending 初始化、generate() 中的 CI 故障注入条件都改用它;_ci_fault_injection_pending 现在始终先初始化,再在启用 rollout FT 时置为 args.ci_test
  3. EvalFleet 探测委派:在 miles/ray/rollout/eval_fleet.pyEvalFleet 中同步计算 _rollout_ft_enabled,用于 pin() 决定是否自行 probe_and_mark_dead()——train-only FT 没有 RolloutHealthMonitor,必须自己探测。
  4. 训练端恢复收口:在 miles/ray/actor_group.pyRayTrainGroup.update_weights() 中,把 recover_updatable_engines 的调用条件从仅 use_fault_tolerance 收紧为同时要求 "rollout" in ft_components,避免 train-only FT 恢复 rollout 引擎。
  5. 测试配套tests/fast/ray/rollout/real_ray/test_rollout_manager.py 新增参数化测试验证 monitor 数量与故障注入 pending 跟随组件选择,并新增 replay 跳过构造 / train-only 正常构造两个用例;tests/fast/ray/test_actor_group_shared_ppo.py 新增 _RemoteCall 辅助类和 test_train_only_ft_does_not_recover_rollout_enginestests/fast/ray/rollout/test_eval_fleet.py 新增 test_fleet_probes_with_train_only_fault_tolerance,并调整既有用例的 ft_components 设置。
文件 模块 状态 重要度
miles/ray/rollout/rollout_manager.py 回放管理 modified 6.88
tests/fast/ray/rollout/real_ray/test_rollout_manager.py 回放测试 modified 7.2
miles/ray/rollout/eval_fleet.py 评估集群 modified 5.56
miles/ray/actor_group.py 训练组 modified 4.89
tests/fast/ray/rollout/test_eval_fleet.py 评估测试 modified 5.2
tests/fast/ray/test_actor_group_shared_ppo.py 训练组测试 modified 6.22

关键符号

RolloutManager.__init__ RolloutManager._get_rollout_data RolloutManager.generate EvalFleet.__init__ EvalFleet.pin RayTrainGroup.update_weights test_rollout_ft_lifecycle_follows_selected_component test_debug_rollout_replay_skips_class_based_rollout_construction test_train_only_ft_does_not_recover_rollout_engines test_fleet_probes_with_train_only_fault_tolerance

关键源码片段

miles/ray/rollout/rollout_manager.py core-logic

核心修复文件:在 __init__ 中按 load_debug_rollout_data 跳过 class-based rollout 构造,避免 GenerateState 崩溃;并引入 _rollout_ft_enabled 统一门控健康监控、CI 故障注入与 _get_rollout_data 分支语义。

# 回放模式下完全没有真实 rollout 函数调用,因此:
# - 若配置了 --load-debug-rollout-data,直接跳过 class-based rollout 构造,
# 避免 GenerateState 因 rollout_num_gpus=None 崩溃;
# - 否则才加载 rollout 函数(train-only SFT 也依赖它产出数据)。
self.use_legacy_rollout_v1 = use_legacy_rollout_v1()
if not self.use_legacy_rollout_v1:
    if self.args.load_debug_rollout_data is not None:
        self.generate_rollout = None
        self.eval_generate_rollout = None
    else:
        input = RolloutFnConstructorInput(args=args, data_source=self.data_source)
        self.generate_rollout = load_rollout_function(input, self.args.rollout_function_path)
        if self.args.eval_function_path == self.args.rollout_function_path:
            # eval 与 train 共享同一个有状态实例,避免 FullyAsyncRolloutFn 被构造两次
            self.eval_generate_rollout = self.generate_rollout
        else:
            self.eval_generate_rollout = load_rollout_function(input, self.args.eval_function_path)
else:
    self.generate_rollout = load_function(self.args.rollout_function_path)
    self.eval_generate_rollout = load_function(self.args.eval_function_path)
# (custom reward / convert 函数加载已省略)# rollout FT 生命周期只在显式勾选 rollout 组件时激活:
# train-only FT 不应启动健康监控、故障注入或引擎恢复。
self._rollout_ft_enabled = self.args.use_fault_tolerance and "rollout" in self.args.ft_components
self._ci_fault_injection_pending = False
if not self.args.debug_train_only and self._rollout_ft_enabled:
    for srv in self.servers.values():
        for group in srv.server_groups:
            monitor = RolloutHealthMonitor(group, args)
            monitor.start()
            self._health_monitors.append(monitor)
    self._ci_fault_injection_pending = self.args.ci_test
miles/ray/rollout/eval_fleet.py core-logic

EvalFleet.pin 的探测委派逻辑随 _rollout_ft_enabled 调整,train-only FT 下必须自行 probe,否则引擎健康无法保证。

class EvalFleet:
    """专用 in-job 评估引擎(--eval-num-gpus)的权重交付与健康管理。"""
​
    def __init__(self, args: Namespace, *, srv):
        self.args = args
        self._srv = srv
        # 只有当 rollout 组件启用故障容忍时,RolloutHealthMonitor 才会负责探测;
        # train-only FT 下没有 monitor,EvalFleet 必须自己 probe。
        self._rollout_ft_enabled = args.use_fault_tolerance and "rollout" in args.ft_components
        self._state = GenerateState(self._fleet_args())
​
    async def pin(self, checkpoint_dir: str, weight_version: str) -> GenerateState:
        """把快照加载到每个评估引擎,并返回可生成的状态。        在 manager 事件循环上运行,全程 await 而非阻塞。
        """
        try:
            if not self._rollout_ft_enabled:
                # 没有 monitor 接管时,先探测并标记死引擎,再恢复,再等待全部存活
                await self._srv.probe_and_mark_dead()
            await self._srv.recover()
            await self._srv.wait_all_engines_alive()
        except Exception as e:
            logger.warning(f"Eval fleet unhealthy: {e}")
            raise EvalSkip("unhealthy") from e
​
        if not await self._pin_fleet(checkpoint_dir, weight_version):
            raise EvalSkip("pin_violation")
​
        try:
            await self._wait_router_ready()
        except Exception as e:
            logger.warning(f"Eval router not ready: {e}")
            raise EvalSkip("unhealthy") from e
​
        return self._state

评论区精华

回放门控选择:load_debug_rollout_data 而非 debug_train_only 设计

作者在评论中解释独立 SFT 运行会把 rollout fn 作为实际数据生产者,仅检查 debug_train_only 会误跳过构造;所以必须以 load_debug_rollout_data 为构造门控。

结论:采用 load_debug_rollout_data 作为构造门控,普通 train-only 仍构造 rollout fn。 · 已解决

train-only FT 不应激活 rollout 生命周期 正确性

PR body 说明 --ft-components train 仍启动 rollout 健康监控、故障注入与恢复,导致确定性比较出现 10 个 MetricEvent 偏差。

结论:引入 _rollout_ft_enabled 统一门控,monitor、故障注入、恢复均受控。 · 已解决

CI 验证状态确认 测试

作者多次更新:初始未验证 → 推送修复后开始验证 → 最终 28 passed / 2 skipped / 0 failed,之前失败的确定性 real-rollout FT 测试通过。

结论:验证完成,PR 可合并。 · 已解决

独立验证结论一致 question

评论 5 中作者描述独立分析(未参考本 PR)得到相同 gate,确认修复方向正确。

结论:确认修复方向正确。 · 已解决

trainer-ft 相关改动批准 other

fzyzcjy 评论 "LGTM for trainer-ft related changes, since I refactor a lot in #1837"。

结论:批准本 PR。 · 已解决

风险与影响

核心路径变更:RolloutManager.__init__ 是所有训练/回放共用的初始化路径,门控改动影响面广;若未来新增调用方直接使用 generate_rollout 而忘记检查回放标志,会触发 None 调用错误。

配置语义收紧:load_debug_rollout_data 从真值判断改为 is not None,若历史配置传空字符串或 0(非 None 但 falsy),行为会反转;不过该参数预期是路径字符串,实际风险低。

FT 组件依赖:_rollout_ft_enabled 依赖 args.ft_components 为有效可迭代且包含 "rollout";若外部配置未正确初始化 ft_components(如 None),"rollout" in None 会抛 TypeError。需要确认参数解析的默认值。

train-only FT 下 EvalFleet 自行 probe 会增加一次探测开销,但每次 pin 才一次,影响很小;RayTrainGroup.update_weights 不再在 train-only FT 时恢复 rollout 引擎,若训练端依赖引擎预先恢复可能漏恢复,但 train-only FT 语义上本就不应触碰 rollout。

现有测试未覆盖 legacy rollout v1 路径,只能依赖环境变量门控保证。

对用户:使用 --load-debug-rollout-data 的调试回放不再崩溃;使用 --ft-components train 的确定性 FT 测试不再被 rollout 生命周期污染,MetricEvent 对比可通过。

对系统:修复了 release-branch-cut 的阻塞问题,作者明确表示合并此 PR 是重切 release 的前提。

对团队:FT CI 稳定收敛,后续可在干净的组件门控上继续推进完整 FT 能力;测试新增三组用例,为回放与 train-only 行为提供了长期防护网。

核心路径变更 配置语义收紧 FT 组件依赖新字段 缺少 legacy 路径测试

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论