From d633a9be87c5a0181618c3918cd1e4a7ac6e8d31 Mon Sep 17 00:00:00 2001 From: Jiajun Li Date: Mon, 13 Jul 2026 14:52:09 -0700 Subject: [PATCH 1/4] ci: enable ft-long fault-tolerance soak tests The run-ci-ft-long label already reaches the full-node H200 suite, but all three matching registrations are disabled. Remove those temporary gates so the label schedules every FT soak entry. --- tests/e2e/ft/test_trainer_ft_random_dp2_cp2_real_rollout.py | 4 +--- tests/e2e/ft/test_trainer_ft_random_dp2_cp2_tp2_ep2.py | 4 +--- tests/e2e/ft/test_trainer_ft_realistic_gsm8k.py | 1 - 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_real_rollout.py b/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_real_rollout.py index 0a307d724c..21f42fc543 100644 --- a/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_real_rollout.py +++ b/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_real_rollout.py @@ -6,9 +6,7 @@ from tests.ci.ci_register import register_cuda_ci from tests.e2e.ft.conftest_ft.scenario_ft_random import run_ci -register_cuda_ci( - est_time=2400, suite="stage-c-8-gpu-h200", labels=["ft-long"], disabled="FT soak tests pending CI infra support" -) +register_cuda_ci(est_time=2400, suite="stage-c-8-gpu-h200", labels=["ft-long"]) _MODE: str = "dp2_cp2_real_rollout" diff --git a/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_tp2_ep2.py b/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_tp2_ep2.py index 2a9d0e7827..e1a46fc8bf 100644 --- a/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_tp2_ep2.py +++ b/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_tp2_ep2.py @@ -6,9 +6,7 @@ from tests.ci.ci_register import register_cuda_ci from tests.e2e.ft.conftest_ft.scenario_ft_random import run_ci -register_cuda_ci( - est_time=1800, suite="stage-c-8-gpu-h200", labels=["ft-long"], disabled="FT soak tests pending CI infra support" -) +register_cuda_ci(est_time=1800, suite="stage-c-8-gpu-h200", labels=["ft-long"]) _MODE: str = "dp2_cp2_tp2_ep2" diff --git a/tests/e2e/ft/test_trainer_ft_realistic_gsm8k.py b/tests/e2e/ft/test_trainer_ft_realistic_gsm8k.py index 3149b53d0e..356ffdf5c0 100644 --- a/tests/e2e/ft/test_trainer_ft_realistic_gsm8k.py +++ b/tests/e2e/ft/test_trainer_ft_realistic_gsm8k.py @@ -10,7 +10,6 @@ est_time=9000, suite="stage-c-8-gpu-h200", labels=["ft-long", "long"], - disabled="FT soak tests pending CI infra support", ) if __name__ == "__main__": From e3d3b798643aa14614e4ff5b51f89535ca4d6570 Mon Sep 17 00:00:00 2001 From: Jiajun Li Date: Tue, 14 Jul 2026 17:55:39 +0000 Subject: [PATCH 2/4] ci: stop FT accuracy soak after metric success The newly enabled ft-long tests exceeded their stale per-file budgets while still making progress. Propagate metric-checker success to the train loops, opt the realistic GSM8K soak into early completion without relaxing its threshold, and calibrate all three soak estimates from the observed H200 run. --- miles/ray/rollout/rollout_manager.py | 6 ++-- miles/utils/arguments.py | 13 ++++++++ miles/utils/metric_checker.py | 3 +- tests/e2e/ft/README.md | 5 +-- .../conftest_ft/scenario_realistic_gsm8k.py | 1 + ..._trainer_ft_random_dp2_cp2_real_rollout.py | 2 +- .../test_trainer_ft_random_dp2_cp2_tp2_ep2.py | 2 +- .../e2e/ft/test_trainer_ft_realistic_gsm8k.py | 2 +- tests/fast/ray/rollout/conftest.py | 3 ++ .../rollout/real_ray/test_rollout_manager.py | 26 +++++++++++++++ tests/fast/utils/test_arguments.py | 9 +++++ tests/fast/utils/test_metric_checker.py | 33 +++++++++++++++++++ train.py | 8 +++-- train_async.py | 9 +++-- 14 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 tests/fast/utils/test_metric_checker.py diff --git a/miles/ray/rollout/rollout_manager.py b/miles/ray/rollout/rollout_manager.py index ecaa9f41e5..8b96bfd173 100644 --- a/miles/ray/rollout/rollout_manager.py +++ b/miles/ray/rollout/rollout_manager.py @@ -133,7 +133,7 @@ async def generate(self, rollout_id): async def eval(self, rollout_id): if self.args.debug_train_only: # if debug train only, we don't generate evaluation data - return + return False self._health_monitoring_resume() if self.use_experimental_refactor: @@ -148,7 +148,9 @@ async def eval(self, rollout_id): save_debug_rollout_data(self.args, data, rollout_id=rollout_id, evaluation=True) metrics = log_eval_rollout_data(rollout_id, self.args, data, result.metrics) if self._metric_checker is not None: - self._metric_checker.on_eval(metrics) + check_succeeded = self._metric_checker.on_eval(metrics) + return self.args.ci_metric_checker_stop_on_success and check_succeeded + return False async def _get_rollout_data(self, rollout_id): if self.args.load_debug_rollout_data: diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index 53dcc34f8e..f57baad918 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -1993,6 +1993,11 @@ def add_ci_arguments(parser): type=float, default=None, ) + parser.add_argument( + "--ci-metric-checker-stop-on-success", + action="store_true", + help="Stop training after a CI metric evaluation reaches its threshold.", + ) parser.add_argument( "--ci-save-grad-norm", type=str, @@ -2266,6 +2271,14 @@ def miles_validate_args(args): if args.mini_ft_controller_enable and args.control_server_port == 0: raise ValueError("--mini-ft-controller-enable requires --control-server-port to be set (non-zero)") + if args.ci_metric_checker_stop_on_success and ( + not args.ci_test or args.ci_metric_checker_key is None or args.ci_metric_checker_threshold is None + ): + raise ValueError( + "--ci-metric-checker-stop-on-success requires --ci-test, " + "--ci-metric-checker-key, and --ci-metric-checker-threshold" + ) + if "train" in args.ft_components: args.indep_dp = True args.delay_split_train_data_by_dp = True diff --git a/miles/utils/metric_checker.py b/miles/utils/metric_checker.py index c4921d3b09..07811862a4 100644 --- a/miles/utils/metric_checker.py +++ b/miles/utils/metric_checker.py @@ -14,7 +14,7 @@ def __init__(self, args): self.args = args self._exists_check_success = False - def on_eval(self, metrics: dict[str, float]): + def on_eval(self, metrics: dict[str, float]) -> bool: actual_value = metrics.get(self.args.ci_metric_checker_key) assert actual_value is not None, f"{metrics=} {self.args.ci_metric_checker_key=}" @@ -22,6 +22,7 @@ def on_eval(self, metrics: dict[str, float]): logger.info(f"[MetricChecker] {check_success=} {actual_value=} {self.args.ci_metric_checker_threshold=}") self._exists_check_success |= check_success + return self._exists_check_success def dispose(self): assert self._exists_check_success, "[MetricChecker] accuracy check failed" diff --git a/tests/e2e/ft/README.md b/tests/e2e/ft/README.md index 9d0413a5d0..35a9b7df28 100644 --- a/tests/e2e/ft/README.md +++ b/tests/e2e/ft/README.md @@ -270,8 +270,9 @@ Entry `test_trainer_ft_realistic_gsm8k.py`, no mode variants. Runs the same exte ``` Type: non-comparison (no baseline run; reference = the baseline test's wandb curves) -Recipe: Qwen2.5-0.5B, GRPO, 250 rollouts; parallelism mirrors dp2_cp2_real_rollout - (2 cells x CP2 on 4 train GPUs + 4 rollout engines x 1 GPU, disaggregated) +Recipe: Qwen2.5-0.5B, GRPO, up to 250 rollouts; CI stops after the first eval reaches + the 0.55 threshold; parallelism mirrors dp2_cp2_real_rollout (2 cells x CP2 + on 4 train GPUs + 4 rollout engines x 1 GPU, disaggregated) Faults: same external random injection loop as scenario_ft_random (train cells via control server) diff --git a/tests/e2e/ft/conftest_ft/scenario_realistic_gsm8k.py b/tests/e2e/ft/conftest_ft/scenario_realistic_gsm8k.py index 5929d222d6..472dc6b58f 100644 --- a/tests/e2e/ft/conftest_ft/scenario_realistic_gsm8k.py +++ b/tests/e2e/ft/conftest_ft/scenario_realistic_gsm8k.py @@ -156,6 +156,7 @@ def _get_gsm8k_train_args(*, seed: int, num_rollout: int, metric_threshold: floa "--ci-disable-kl-checker " "--ci-metric-checker-key eval/gsm8k " f"--ci-metric-checker-threshold {metric_threshold} " + "--ci-metric-checker-stop-on-success " ) misc_args = ( diff --git a/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_real_rollout.py b/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_real_rollout.py index 21f42fc543..c5d2b0640f 100644 --- a/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_real_rollout.py +++ b/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_real_rollout.py @@ -6,7 +6,7 @@ from tests.ci.ci_register import register_cuda_ci from tests.e2e.ft.conftest_ft.scenario_ft_random import run_ci -register_cuda_ci(est_time=2400, suite="stage-c-8-gpu-h200", labels=["ft-long"]) +register_cuda_ci(est_time=3600, suite="stage-c-8-gpu-h200", labels=["ft-long"]) _MODE: str = "dp2_cp2_real_rollout" diff --git a/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_tp2_ep2.py b/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_tp2_ep2.py index e1a46fc8bf..2bd259a47d 100644 --- a/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_tp2_ep2.py +++ b/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_tp2_ep2.py @@ -6,7 +6,7 @@ from tests.ci.ci_register import register_cuda_ci from tests.e2e.ft.conftest_ft.scenario_ft_random import run_ci -register_cuda_ci(est_time=1800, suite="stage-c-8-gpu-h200", labels=["ft-long"]) +register_cuda_ci(est_time=3600, suite="stage-c-8-gpu-h200", labels=["ft-long"]) _MODE: str = "dp2_cp2_tp2_ep2" diff --git a/tests/e2e/ft/test_trainer_ft_realistic_gsm8k.py b/tests/e2e/ft/test_trainer_ft_realistic_gsm8k.py index 356ffdf5c0..70656fdfb6 100644 --- a/tests/e2e/ft/test_trainer_ft_realistic_gsm8k.py +++ b/tests/e2e/ft/test_trainer_ft_realistic_gsm8k.py @@ -7,7 +7,7 @@ from tests.e2e.ft.conftest_ft.scenario_realistic_gsm8k import run_ci register_cuda_ci( - est_time=9000, + est_time=16500, suite="stage-c-8-gpu-h200", labels=["ft-long", "long"], ) diff --git a/tests/fast/ray/rollout/conftest.py b/tests/fast/ray/rollout/conftest.py index 7e5e94fdbd..f7ab58bb8e 100644 --- a/tests/fast/ray/rollout/conftest.py +++ b/tests/fast/ray/rollout/conftest.py @@ -107,6 +107,9 @@ def make_args(**overrides: Any) -> Namespace: save=None, # CI ci_test=False, + ci_metric_checker_key=None, + ci_metric_checker_threshold=None, + ci_metric_checker_stop_on_success=False, # dumper (sglang debug dumper integration) dumper_enable=False, dumper_inference=False, diff --git a/tests/fast/ray/rollout/real_ray/test_rollout_manager.py b/tests/fast/ray/rollout/real_ray/test_rollout_manager.py index 962a53099d..7d6b15151d 100644 --- a/tests/fast/ray/rollout/real_ray/test_rollout_manager.py +++ b/tests/fast/ray/rollout/real_ray/test_rollout_manager.py @@ -561,6 +561,32 @@ def fake_eval_fn(input): assert isinstance(captured[0], RolloutFnEvalInput) assert captured[0].rollout_id == 10 + async def test_reports_stop_after_metric_success( + self, + ray_local_mode, + placement_group_factory, + tmp_path, + patch_low_level, + monkeypatch, + ): + import miles.ray.rollout.rollout_manager as rmgr + + args = _make_test_args(tmp_path, models=[("actor", True)]) + args.ci_test = True + args.ci_metric_checker_key = "eval/gsm8k" + args.ci_metric_checker_threshold = 0.55 + args.ci_metric_checker_stop_on_success = True + pg = placement_group_factory(2) + + manager = _make_manager(args, pg) + manager.eval_generate_rollout = lambda input: RolloutFnEvalOutput( + data={"gsm8k": {"rewards": [1.0]}}, + metrics={}, + ) + monkeypatch.setattr(rmgr, "log_eval_rollout_data", lambda *a, **kw: {"eval/gsm8k": 0.56}) + + assert await manager.eval(rollout_id=10) is True + async def test_skipped_in_debug_train_only_mode( self, ray_local_mode, diff --git a/tests/fast/utils/test_arguments.py b/tests/fast/utils/test_arguments.py index 20a7bfb758..8f2d48f3fc 100644 --- a/tests/fast/utils/test_arguments.py +++ b/tests/fast/utils/test_arguments.py @@ -149,6 +149,15 @@ def test_recompute_logprobs_via_prefill_flag_is_parsed(): assert args.recompute_logprobs_via_prefill is True +def test_ci_metric_checker_stop_on_success_flag_is_parsed(): + parser = argparse.ArgumentParser() + get_miles_extra_args_provider()(parser) + + args = parser.parse_args(["--ci-metric-checker-stop-on-success"] + REQUIRED_ARGS) + + assert args.ci_metric_checker_stop_on_success is True + + def test_custom_megatron_post_save_hook_path_is_parsed(): parser = argparse.ArgumentParser() get_miles_extra_args_provider()(parser) diff --git a/tests/fast/utils/test_metric_checker.py b/tests/fast/utils/test_metric_checker.py new file mode 100644 index 0000000000..059671480b --- /dev/null +++ b/tests/fast/utils/test_metric_checker.py @@ -0,0 +1,33 @@ +from types import SimpleNamespace + +import pytest + +from miles.utils.metric_checker import MetricChecker + + +def test_on_eval_reports_cumulative_success() -> None: + checker = MetricChecker( + SimpleNamespace( + ci_metric_checker_key="eval/gsm8k", + ci_metric_checker_threshold=0.55, + ) + ) + + assert checker.on_eval({"eval/gsm8k": 0.54}) is False + assert checker.on_eval({"eval/gsm8k": 0.56}) is True + assert checker.on_eval({"eval/gsm8k": 0.53}) is True + checker.dispose() + + +def test_dispose_rejects_runs_without_a_successful_eval() -> None: + checker = MetricChecker( + SimpleNamespace( + ci_metric_checker_key="eval/gsm8k", + ci_metric_checker_threshold=0.55, + ) + ) + + checker.on_eval({"eval/gsm8k": 0.54}) + + with pytest.raises(AssertionError, match=r"accuracy check failed"): + checker.dispose() diff --git a/train.py b/train.py index 06f8ac81d1..1d1fcc7a3d 100644 --- a/train.py +++ b/train.py @@ -90,7 +90,9 @@ async def save(rollout_id): # note that for async training, one can change the position of the sync operation(ray.get). for rollout_id in range(args.start_rollout_id, args.num_rollout): if args.eval_interval is not None and rollout_id == args.start_rollout_id and not args.skip_eval_before_train: - await rollout_manager.eval.remote(rollout_id) + if await rollout_manager.eval.remote(rollout_id): + logger.info("CI metric threshold reached at rollout_id=%d; stopping training", rollout_id) + break rollout_data_ref = await rollout_manager.generate.remote(rollout_id) @@ -121,7 +123,9 @@ async def save(rollout_id): await rollout_manager.onload_kv.remote() if should_run_periodic_action(rollout_id, args.eval_interval, num_rollout_per_epoch): - await rollout_manager.eval.remote(rollout_id) + if await rollout_manager.eval.remote(rollout_id): + logger.info("CI metric threshold reached at rollout_id=%d; stopping training", rollout_id) + break if ( args.debug_exit_after_rollout is not None diff --git a/train_async.py b/train_async.py index 239265f69c..7900eef163 100644 --- a/train_async.py +++ b/train_async.py @@ -53,7 +53,10 @@ async def train(args): ) if args.eval_interval is not None and args.start_rollout_id == 0 and not args.skip_eval_before_train: - await rollout_manager.eval.remote(0) + if await rollout_manager.eval.remote(0): + logger.info("CI metric threshold reached at rollout_id=0; stopping training") + await rollout_manager.dispose.remote() + return # async train loop. rollout_data_next_future = rollout_manager.generate.remote(args.start_rollout_id) @@ -93,7 +96,9 @@ async def train(args): await actor_model.update_weights(rollout_id=rollout_id) if should_run_periodic_action(rollout_id, args.eval_interval, num_rollout_per_epoch): - await rollout_manager.eval.remote(rollout_id) + if await rollout_manager.eval.remote(rollout_id): + logger.info("CI metric threshold reached at rollout_id=%d; stopping training", rollout_id) + break if ( args.debug_exit_after_rollout is not None From 5aec78c4382b3c470918cbd6185eaab2a23359a7 Mon Sep 17 00:00:00 2001 From: Jiajun Li Date: Wed, 15 Jul 2026 20:32:44 +0000 Subject: [PATCH 3/4] ci: remove standalone metric checker CPU test The standalone metric checker test is implicitly collected by stage-a-cpu and is not useful for this FT soak change. Drop it from the PR without changing the metric checker implementation or the remaining stop-on-success coverage. --- tests/fast/utils/test_metric_checker.py | 33 ------------------------- 1 file changed, 33 deletions(-) delete mode 100644 tests/fast/utils/test_metric_checker.py diff --git a/tests/fast/utils/test_metric_checker.py b/tests/fast/utils/test_metric_checker.py deleted file mode 100644 index 059671480b..0000000000 --- a/tests/fast/utils/test_metric_checker.py +++ /dev/null @@ -1,33 +0,0 @@ -from types import SimpleNamespace - -import pytest - -from miles.utils.metric_checker import MetricChecker - - -def test_on_eval_reports_cumulative_success() -> None: - checker = MetricChecker( - SimpleNamespace( - ci_metric_checker_key="eval/gsm8k", - ci_metric_checker_threshold=0.55, - ) - ) - - assert checker.on_eval({"eval/gsm8k": 0.54}) is False - assert checker.on_eval({"eval/gsm8k": 0.56}) is True - assert checker.on_eval({"eval/gsm8k": 0.53}) is True - checker.dispose() - - -def test_dispose_rejects_runs_without_a_successful_eval() -> None: - checker = MetricChecker( - SimpleNamespace( - ci_metric_checker_key="eval/gsm8k", - ci_metric_checker_threshold=0.55, - ) - ) - - checker.on_eval({"eval/gsm8k": 0.54}) - - with pytest.raises(AssertionError, match=r"accuracy check failed"): - checker.dispose() From c918ba22adef581b140389c896d89812bcbea9b7 Mon Sep 17 00:00:00 2001 From: Jiajun Li Date: Wed, 15 Jul 2026 20:45:31 +0000 Subject: [PATCH 4/4] ci: limit FT soak enablement to two random tests FT soak tests must complete their configured rollout counts instead of stopping after a metric passes. Remove the early-stop flag and its wiring, restore the original estimates, and keep realistic GSM8K disabled so this PR only enables the two random FT cases. --- miles/ray/rollout/rollout_manager.py | 6 ++--- miles/utils/arguments.py | 13 ---------- miles/utils/metric_checker.py | 3 +-- tests/e2e/ft/README.md | 5 ++-- .../conftest_ft/scenario_realistic_gsm8k.py | 1 - ..._trainer_ft_random_dp2_cp2_real_rollout.py | 2 +- .../test_trainer_ft_random_dp2_cp2_tp2_ep2.py | 2 +- .../e2e/ft/test_trainer_ft_realistic_gsm8k.py | 2 +- tests/fast/ray/rollout/conftest.py | 3 --- .../rollout/real_ray/test_rollout_manager.py | 26 ------------------- tests/fast/utils/test_arguments.py | 9 ------- train.py | 8 ++---- train_async.py | 9 ++----- 13 files changed, 12 insertions(+), 77 deletions(-) diff --git a/miles/ray/rollout/rollout_manager.py b/miles/ray/rollout/rollout_manager.py index 8b96bfd173..ecaa9f41e5 100644 --- a/miles/ray/rollout/rollout_manager.py +++ b/miles/ray/rollout/rollout_manager.py @@ -133,7 +133,7 @@ async def generate(self, rollout_id): async def eval(self, rollout_id): if self.args.debug_train_only: # if debug train only, we don't generate evaluation data - return False + return self._health_monitoring_resume() if self.use_experimental_refactor: @@ -148,9 +148,7 @@ async def eval(self, rollout_id): save_debug_rollout_data(self.args, data, rollout_id=rollout_id, evaluation=True) metrics = log_eval_rollout_data(rollout_id, self.args, data, result.metrics) if self._metric_checker is not None: - check_succeeded = self._metric_checker.on_eval(metrics) - return self.args.ci_metric_checker_stop_on_success and check_succeeded - return False + self._metric_checker.on_eval(metrics) async def _get_rollout_data(self, rollout_id): if self.args.load_debug_rollout_data: diff --git a/miles/utils/arguments.py b/miles/utils/arguments.py index f57baad918..53dcc34f8e 100644 --- a/miles/utils/arguments.py +++ b/miles/utils/arguments.py @@ -1993,11 +1993,6 @@ def add_ci_arguments(parser): type=float, default=None, ) - parser.add_argument( - "--ci-metric-checker-stop-on-success", - action="store_true", - help="Stop training after a CI metric evaluation reaches its threshold.", - ) parser.add_argument( "--ci-save-grad-norm", type=str, @@ -2271,14 +2266,6 @@ def miles_validate_args(args): if args.mini_ft_controller_enable and args.control_server_port == 0: raise ValueError("--mini-ft-controller-enable requires --control-server-port to be set (non-zero)") - if args.ci_metric_checker_stop_on_success and ( - not args.ci_test or args.ci_metric_checker_key is None or args.ci_metric_checker_threshold is None - ): - raise ValueError( - "--ci-metric-checker-stop-on-success requires --ci-test, " - "--ci-metric-checker-key, and --ci-metric-checker-threshold" - ) - if "train" in args.ft_components: args.indep_dp = True args.delay_split_train_data_by_dp = True diff --git a/miles/utils/metric_checker.py b/miles/utils/metric_checker.py index 07811862a4..c4921d3b09 100644 --- a/miles/utils/metric_checker.py +++ b/miles/utils/metric_checker.py @@ -14,7 +14,7 @@ def __init__(self, args): self.args = args self._exists_check_success = False - def on_eval(self, metrics: dict[str, float]) -> 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=}" @@ -22,7 +22,6 @@ def on_eval(self, metrics: dict[str, float]) -> bool: logger.info(f"[MetricChecker] {check_success=} {actual_value=} {self.args.ci_metric_checker_threshold=}") self._exists_check_success |= check_success - return self._exists_check_success def dispose(self): assert self._exists_check_success, "[MetricChecker] accuracy check failed" diff --git a/tests/e2e/ft/README.md b/tests/e2e/ft/README.md index 35a9b7df28..9d0413a5d0 100644 --- a/tests/e2e/ft/README.md +++ b/tests/e2e/ft/README.md @@ -270,9 +270,8 @@ Entry `test_trainer_ft_realistic_gsm8k.py`, no mode variants. Runs the same exte ``` Type: non-comparison (no baseline run; reference = the baseline test's wandb curves) -Recipe: Qwen2.5-0.5B, GRPO, up to 250 rollouts; CI stops after the first eval reaches - the 0.55 threshold; parallelism mirrors dp2_cp2_real_rollout (2 cells x CP2 - on 4 train GPUs + 4 rollout engines x 1 GPU, disaggregated) +Recipe: Qwen2.5-0.5B, GRPO, 250 rollouts; parallelism mirrors dp2_cp2_real_rollout + (2 cells x CP2 on 4 train GPUs + 4 rollout engines x 1 GPU, disaggregated) Faults: same external random injection loop as scenario_ft_random (train cells via control server) diff --git a/tests/e2e/ft/conftest_ft/scenario_realistic_gsm8k.py b/tests/e2e/ft/conftest_ft/scenario_realistic_gsm8k.py index 472dc6b58f..5929d222d6 100644 --- a/tests/e2e/ft/conftest_ft/scenario_realistic_gsm8k.py +++ b/tests/e2e/ft/conftest_ft/scenario_realistic_gsm8k.py @@ -156,7 +156,6 @@ def _get_gsm8k_train_args(*, seed: int, num_rollout: int, metric_threshold: floa "--ci-disable-kl-checker " "--ci-metric-checker-key eval/gsm8k " f"--ci-metric-checker-threshold {metric_threshold} " - "--ci-metric-checker-stop-on-success " ) misc_args = ( diff --git a/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_real_rollout.py b/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_real_rollout.py index c5d2b0640f..21f42fc543 100644 --- a/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_real_rollout.py +++ b/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_real_rollout.py @@ -6,7 +6,7 @@ from tests.ci.ci_register import register_cuda_ci from tests.e2e.ft.conftest_ft.scenario_ft_random import run_ci -register_cuda_ci(est_time=3600, suite="stage-c-8-gpu-h200", labels=["ft-long"]) +register_cuda_ci(est_time=2400, suite="stage-c-8-gpu-h200", labels=["ft-long"]) _MODE: str = "dp2_cp2_real_rollout" diff --git a/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_tp2_ep2.py b/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_tp2_ep2.py index 2bd259a47d..e1a46fc8bf 100644 --- a/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_tp2_ep2.py +++ b/tests/e2e/ft/test_trainer_ft_random_dp2_cp2_tp2_ep2.py @@ -6,7 +6,7 @@ from tests.ci.ci_register import register_cuda_ci from tests.e2e.ft.conftest_ft.scenario_ft_random import run_ci -register_cuda_ci(est_time=3600, suite="stage-c-8-gpu-h200", labels=["ft-long"]) +register_cuda_ci(est_time=1800, suite="stage-c-8-gpu-h200", labels=["ft-long"]) _MODE: str = "dp2_cp2_tp2_ep2" diff --git a/tests/e2e/ft/test_trainer_ft_realistic_gsm8k.py b/tests/e2e/ft/test_trainer_ft_realistic_gsm8k.py index 541eeb1824..a1077f4a54 100644 --- a/tests/e2e/ft/test_trainer_ft_realistic_gsm8k.py +++ b/tests/e2e/ft/test_trainer_ft_realistic_gsm8k.py @@ -7,7 +7,7 @@ from tests.e2e.ft.conftest_ft.scenario_realistic_gsm8k import run_ci register_cuda_ci( - est_time=16500, + est_time=9000, suite="stage-c-8-gpu-h200", labels=["ft-long"], disabled="FT soak tests pending CI infra support", diff --git a/tests/fast/ray/rollout/conftest.py b/tests/fast/ray/rollout/conftest.py index f7ab58bb8e..7e5e94fdbd 100644 --- a/tests/fast/ray/rollout/conftest.py +++ b/tests/fast/ray/rollout/conftest.py @@ -107,9 +107,6 @@ def make_args(**overrides: Any) -> Namespace: save=None, # CI ci_test=False, - ci_metric_checker_key=None, - ci_metric_checker_threshold=None, - ci_metric_checker_stop_on_success=False, # dumper (sglang debug dumper integration) dumper_enable=False, dumper_inference=False, diff --git a/tests/fast/ray/rollout/real_ray/test_rollout_manager.py b/tests/fast/ray/rollout/real_ray/test_rollout_manager.py index 7d6b15151d..962a53099d 100644 --- a/tests/fast/ray/rollout/real_ray/test_rollout_manager.py +++ b/tests/fast/ray/rollout/real_ray/test_rollout_manager.py @@ -561,32 +561,6 @@ def fake_eval_fn(input): assert isinstance(captured[0], RolloutFnEvalInput) assert captured[0].rollout_id == 10 - async def test_reports_stop_after_metric_success( - self, - ray_local_mode, - placement_group_factory, - tmp_path, - patch_low_level, - monkeypatch, - ): - import miles.ray.rollout.rollout_manager as rmgr - - args = _make_test_args(tmp_path, models=[("actor", True)]) - args.ci_test = True - args.ci_metric_checker_key = "eval/gsm8k" - args.ci_metric_checker_threshold = 0.55 - args.ci_metric_checker_stop_on_success = True - pg = placement_group_factory(2) - - manager = _make_manager(args, pg) - manager.eval_generate_rollout = lambda input: RolloutFnEvalOutput( - data={"gsm8k": {"rewards": [1.0]}}, - metrics={}, - ) - monkeypatch.setattr(rmgr, "log_eval_rollout_data", lambda *a, **kw: {"eval/gsm8k": 0.56}) - - assert await manager.eval(rollout_id=10) is True - async def test_skipped_in_debug_train_only_mode( self, ray_local_mode, diff --git a/tests/fast/utils/test_arguments.py b/tests/fast/utils/test_arguments.py index 8f2d48f3fc..20a7bfb758 100644 --- a/tests/fast/utils/test_arguments.py +++ b/tests/fast/utils/test_arguments.py @@ -149,15 +149,6 @@ def test_recompute_logprobs_via_prefill_flag_is_parsed(): assert args.recompute_logprobs_via_prefill is True -def test_ci_metric_checker_stop_on_success_flag_is_parsed(): - parser = argparse.ArgumentParser() - get_miles_extra_args_provider()(parser) - - args = parser.parse_args(["--ci-metric-checker-stop-on-success"] + REQUIRED_ARGS) - - assert args.ci_metric_checker_stop_on_success is True - - def test_custom_megatron_post_save_hook_path_is_parsed(): parser = argparse.ArgumentParser() get_miles_extra_args_provider()(parser) diff --git a/train.py b/train.py index 1d1fcc7a3d..06f8ac81d1 100644 --- a/train.py +++ b/train.py @@ -90,9 +90,7 @@ async def save(rollout_id): # note that for async training, one can change the position of the sync operation(ray.get). for rollout_id in range(args.start_rollout_id, args.num_rollout): if args.eval_interval is not None and rollout_id == args.start_rollout_id and not args.skip_eval_before_train: - if await rollout_manager.eval.remote(rollout_id): - logger.info("CI metric threshold reached at rollout_id=%d; stopping training", rollout_id) - break + await rollout_manager.eval.remote(rollout_id) rollout_data_ref = await rollout_manager.generate.remote(rollout_id) @@ -123,9 +121,7 @@ async def save(rollout_id): await rollout_manager.onload_kv.remote() if should_run_periodic_action(rollout_id, args.eval_interval, num_rollout_per_epoch): - if await rollout_manager.eval.remote(rollout_id): - logger.info("CI metric threshold reached at rollout_id=%d; stopping training", rollout_id) - break + await rollout_manager.eval.remote(rollout_id) if ( args.debug_exit_after_rollout is not None diff --git a/train_async.py b/train_async.py index 7900eef163..239265f69c 100644 --- a/train_async.py +++ b/train_async.py @@ -53,10 +53,7 @@ async def train(args): ) if args.eval_interval is not None and args.start_rollout_id == 0 and not args.skip_eval_before_train: - if await rollout_manager.eval.remote(0): - logger.info("CI metric threshold reached at rollout_id=0; stopping training") - await rollout_manager.dispose.remote() - return + await rollout_manager.eval.remote(0) # async train loop. rollout_data_next_future = rollout_manager.generate.remote(args.start_rollout_id) @@ -96,9 +93,7 @@ async def train(args): await actor_model.update_weights(rollout_id=rollout_id) if should_run_periodic_action(rollout_id, args.eval_interval, num_rollout_per_epoch): - if await rollout_manager.eval.remote(rollout_id): - logger.info("CI metric threshold reached at rollout_id=%d; stopping training", rollout_id) - break + await rollout_manager.eval.remote(rollout_id) if ( args.debug_exit_after_rollout is not None