Prhub

#2823 Fail CI runs on crashed async eval points or abnormal eval results

原始 PR 作者 fzyzcjy 合并时间 2026-09-04 21:33 文件变更 11 提交数 22 评论 13 代码增减 +196 / -12

执行摘要

集中化 CI 评估失败策略,修复异步评估点崩溃时 CI 仍通过的漏洞。

PR body 和 commit 信息指出,当前 EvalDispatcher 在评估失败时会降级为“跳过”状态,这对训练是合理的,但让完全异步评估的 E2E 测试即使所有评估点都崩溃也能通过。崩溃只记录为 eval/skipped_crashed 指标和警告,而 CI 保持绿色。这违背了 PR 的核心保障目标:确保评估管道实际工作。因此,需要在 --ci-test 模式下强制失败,以暴露潜在的评估问题。

此 PR 值得精读。它虽然改动文件不多,但解决了一个影响 CI 可靠性的核心问题。关键的设计决策——在 report_eval_skip 中集中失败策略——非常清晰且易于维护。MetricChecker 的扩展逻辑和测试覆盖也做得很好。对于关注 CI/CD、测试基础设施和模型训练质量门控的工程师来说,这是一个很好的实践案例。

讨论亮点

核心讨论围绕 “评估被跳过是否应导致 CI 失败” 展开。guapisolo 指出,如果允许训练前的基线评估满足 MetricChecker,那么后续所有评估点都可以被跳过(export_failed, ckpt_missing, EvalSkip),而 CI 仍然会通过,这违背了 PR 的初衷。yueming-yuan 进一步提问:“是否也应该在评估被跳过时失败 CI?” 作者 fzyzcjy 的最终决策是:在 CI 模式下,所有评估跳过都必须失败 CI(通过集中化的 report_eval_skip 路径),并且通过 --ci-metric-checker-expect-num 精确控制期望的成功评估次数,以确保评估管道完整运行。讨论中还涉及了对 EvalDispatcher.drain() 方法是否会吞没异常的分析,结论是该行为符合 CI 快速失败的预期。

实现拆解

  1. 在核心评估跳过路径集中失败:在 miles/ray/rollout/rollout_executor.py 的 RolloutExecutor.report_eval_skip 方法中,于记录跳过原因之后,增加了一个条件判断:若 self.args.ci_test 为真,则抛出 RuntimeError。这是最关键且最简练的变更,它将 CI 失败策略集中到了一个所有评估跳过(包括崩溃、端口繁忙、导出失败、检查点缺失、后端跳过)都会经过的公共路径。
  2. 增强 MetricChecker 以支持精确计数门控:在 miles/utils/metric_checker.py 中,将内部状态从布尔值 _exists_check_success 改为布尔列表 _check_results,以记录每次检查的结果。dispose 方法现在支持两种模式:默认模式(无 expect_num)要求至少一次成功;精确模式(设置了 ci_metric_checker_expect_num)要求恰好 expected 次成功检查且所有检查都通过。同时,在 miles/utils/arguments.py 中新增了 --ci-metric-checker-expect-num 命令行参数。
  3. 新增和更新单元测试:新增 tests/fast/ray/rollout/test_eval_dispatch.py 测试文件,验证崩溃评估在 CI 模式下会通过 report_eval_skip 路径失败,而在非 CI 模式下仅记录日志。新增 tests/fast/utils/test_metric_checker.py 测试文件,全面覆盖 MetricChecker 的两种期望模式。在 tests/fast/rollout/test_checkpoint_eval.py 中新增 test_report_eval_skip_fails_ci_after_logging 参数化测试,确保所有可归因的评估跳过原因在 CI 模式下都会导致失败,并修复了该测试文件中 make_args 缺失 ci_test 字段的问题。
  4. 配置 E2E 测试以使用新的精确门控:修改 tests/e2e/megatron/test_qwen3_4b_fully_async_eval.py 和 tests/e2e/short/test_qwen2.5_0.5B_fully_async_eval.py,在它们的 ci_args 中添加 --ci-metric-checker-key eval/gsm8k --ci-metric-checker-threshold 0.4 --ci-metric-checker-expect-num 3。这要求两次 E2E 运行都必须恰好完成三次评估,且每次评估的 GSM8K 分数都达到 0.4,从而防止训练前的基线评估单独满足门控,掩盖后续评估的失败。
  5. 更新文档:在 docs/developer/debug.md 中同步更新了关于准确度门控(MetricChecker)的说明,反映了新的精确计数选项。
文件 模块 状态 重要度
miles/ray/rollout/rollout_executor.py 评估执行器 modified 5.89
miles/utils/metric_checker.py 指标检查器 modified 6.64
miles/utils/arguments.py 参数配置 modified 5.39
tests/e2e/megatron/test_qwen3_4b_fully_async_eval.py 端到端测试 modified 5.0
tests/e2e/short/test_qwen2.5_0.5B_fully_async_eval.py 端到端测试 modified 5.0
tests/fast/ray/rollout/test_eval_dispatch.py 评估调度器 added 7.63
tests/fast/utils/test_metric_checker.py 指标检查器 added 7.49
tests/fast/rollout/test_checkpoint_eval.py 检查点评估 modified 5.21

关键符号

RolloutExecutor.report_eval_skip MetricChecker.__init__ MetricChecker.on_eval MetricChecker.dispose

关键源码片段

miles/utils/metric_checker.py core-logic

实现了可配置的评估精度门控逻辑,支持“任一次成功”和“精确 N 次全部成功”两种模式,是防止基线评估掩盖问题的核心。

class MetricChecker:
    def __init__(self, args):
        self.args = args
        # 存储每次评估检查的成功 / 失败结果
        self._check_results: list[bool] = []
​
    def on_eval(self, metrics: dict[str, float]):
        actual_value = metrics.get(self.args.ci_metric_checker_key)
        assert actual_value is not None, f"{metrics=} {self.args.ci_metric_checker_key=}"
        check_success = actual_value >= self.args.ci_metric_checker_threshold
        logger.info(f"[MetricChecker] {check_success=} {actual_value=} {self.args.ci_metric_checker_threshold=}")
        self._check_results.append(check_success)
​
    def dispose(self):
        # 必须至少有一次评估结果
        assert self._check_results, "[MetricChecker] no metrics checked"
        # 根据配置的期望数量选择门控策略
        if (expected := self.args.ci_metric_checker_expect_num) is None:
            # 默认策略:至少有一次评估成功即可通过
            assert any(self._check_results), f"[MetricChecker] accuracy check failed: {self._check_results=}"
        else:
            # 精确策略:要求恰好 expected 次评估,且全部成功
            assert len(self._check_results) == expected and all(self._check_results), (
                f"[MetricChecker] expected exactly {expected} checks and all to succeed: " f"{self._check_results=}"
            )
        logger.info("[MetricChecker] pass dispose check")
tests/fast/ray/rollout/test_eval_dispatch.py test-coverage

新增的单元测试文件,专门测试 `EvalDispatcher` 在 CI 模式下处理崩溃评估点的行为,是验证核心失败路径的关键。

class TestSettle:
    async def test_a_crashed_eval_fails_when_reporting_the_ci_skip(self, tmp_path):
        """A crashed eval fails CI through the centralized skipped-point contract."""
        dispatcher = make_dispatcher(ci_test=True)
        exported_dir = str(tmp_path / "step_3")
​
        # 模拟一个失败的远程方法(代表崩溃的评估点)
        class FailingRemoteMethod(FakeRemoteMethod):
            def remote(self, *args):
                self.log.append((self.name, args))
                fut = asyncio.get_event_loop().create_future()
                # 模拟抛出与 report_eval_skip 相同的错误格式
                fut.set_exception(RuntimeError("CI eval 3 skipped: crashed"))
                return fut
​
        dispatcher.rollout_manager.report_eval_skip = FailingRemoteMethod(
            dispatcher.rollout_manager.log, "report_eval_skip"
        )
​
        # 验证在 CI 模式下,_settle 会传播 RuntimeError
        with pytest.raises(RuntimeError, match="CI eval 3 skipped: crashed"):
            await dispatcher._settle(3, failed_ref(RuntimeError("engine gone")), exported_dir)
​
        # 验证 report_eval_skip 被调用,且快照目录被保留
        assert dispatcher.rollout_manager.log == [("report_eval_skip", (3, "crashed"))]
        assert dispatcher._exported == [exported_dir]

评论区精华

评估跳过是否应导致 CI 失败 正确性

guapisolo 指出,如果训练前的基线评估满足了 `MetricChecker`,那么后续所有评估点都可以被跳过,而 CI 仍会通过,这违背了 PR 的核心目标。yueming-yuan 追问是否应在评估被跳过时也失败 CI。

结论:作者最终决定,在 CI 模式下,** 所有 ** 评估跳过(包括崩溃、各种失败原因)都必须导致 CI 失败,并通过在 `report_eval_skip` 中集中实现来确保这一点。同时,通过 `--ci-metric-checker-expect-num` 精确控制期望的成功评估次数,确保评估管道完整运行。 · 已解决

report_eval_skip 的异常是否会被 EvalDispatcher.drain() 吞掉 正确性

Claude Code Review 分析了 `report_eval_skip` 新增的 `raise` 是否会从 `EvalDispatcher.drain()` 中传播出去,并评估其是否构成一个独立的 bug。

结论:分析认为该异常会正常传播,导致跳过 `dispose.remote()` 并让进程以非零状态退出。这被视为符合 PR 目标(在评估问题上让 CI 快速失败)的预期行为,而非缺陷。 · 已解决

风险与影响

  1. CI 稳定性风险:将 --ci-test 模式下的评估跳过行为从“记录并继续”改为“立即失败”是一个显著的行为变更。如果某些评估点在 CI 环境中因外部因素(如瞬时资源争用)偶尔跳过,可能导致 CI 频繁失败,增加维护负担。然而,PR 作者通过设定较宽松的阈值(0.4)和精确计数(3次),试图在可靠性和敏感性之间取得平衡。
  2. MetricChecker 逻辑正确性:MetricChecker 从“任一次成功即通过”变为可配置的“精确 N 次全部成功”。需要确保新逻辑能正确处理零评估情况(已有测试覆盖)和边界条件(如评估次数不匹配)。测试覆盖看起来比较全面。
  3. E2E 测试硬编码期望值:E2E 测试中的 --ci-metric-checker-expect-num 3 是一个硬编码的期望值。如果未来测试的评估次数安排发生变化(例如,由于配置调整),需要手动更新此值,否则会导致 CI 失败或失去门控效果。这是一个维护点,但在此 PR 的上下文中是合理的。
  1. 对 CI 系统的影响:此变更直接提升了 CI 对评估管道健康状况的检测能力。任何评估崩溃、跳过或未达到预期精度的运行都将导致 CI 失败。这可能会在短期内暴露一些之前被掩盖的、间歇性的评估问题,但长期来看会增强对模型训练后质量的信心。
  2. 对开发者工作流的影响:开发者在提交 PR 时,需要确保其修改不会导致评估点崩溃或评估结果低于阈值(0.4),否则 CI 将失败。这增加了提交的质量要求,但也提供了更早的反馈。
  3. 对系统整体质量的影响:这是一个积极的变更,它强化了训练-评估循环的完整性。通过确保评估点不仅被调度,而且成功执行并产生符合标准的结果,减少了交付“评估看似正常但实际失败”的模型的风险。
CI 行为显著变更 硬编码的 E2E 期望值

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论