diff --git a/tests/test_rl_verl_adapter.py b/tests/test_rl_verl_adapter.py index 96c58845..17251694 100644 --- a/tests/test_rl_verl_adapter.py +++ b/tests/test_rl_verl_adapter.py @@ -447,3 +447,12 @@ def test_verl_data_cursor_refuses_what_it_cannot_honour(): cur.seek({"sample_offset": 2}) with pytest.raises(RuntimeError, match="twice"): VerlDataCursor(trainer) + + +def test_verl_refuses_expert_parallel_with_e7_reason(): + # S26 E7: verl+EP is refused with the reason and the backend to use instead. + from yeto.rl.adapters.verl import run_config_rules as rules + + rules.check_expert_parallel(8, 1) + with pytest.raises(ValueError, match="E7.*Miles"): + rules.check_expert_parallel(8, 8) diff --git a/tests/test_stage1_plan.py b/tests/test_stage1_plan.py index c2639852..38aec29f 100644 --- a/tests/test_stage1_plan.py +++ b/tests/test_stage1_plan.py @@ -250,3 +250,13 @@ def edit(doc): doc["trainer"]["miles"]["memory_estimate"]["host_optimizer_gib_per_node"] = 600 out = _variant(tmp_path, edit) assert any("host_optimizer_gib_per_node = 600" in e for e in out.errors) + + +def test_verl_backend_refuses_megatron_strategy(tmp_path): + # S26 E7: verl Megatron (the verl path with EP) is not wired; EP runs use Miles. + out = _variant(tmp_path, lambda d: d["trainer"].update(backend="verl") + or d["trainer"]["verl"].update(strategy="megatron")) + assert any("verl.strategy 'megatron'" in e and "backend miles" in e for e in out.errors) + # The same strategy value is not checked for the Miles backend (its section is unused). + miles = _variant(tmp_path, lambda d: d["trainer"]["verl"].update(strategy="megatron")) + assert not any("verl.strategy" in e for e in miles.errors) diff --git a/yeto/rl/adapters/verl/run_config_rules.py b/yeto/rl/adapters/verl/run_config_rules.py index e402126d..147fe1c0 100644 --- a/yeto/rl/adapters/verl/run_config_rules.py +++ b/yeto/rl/adapters/verl/run_config_rules.py @@ -19,9 +19,15 @@ def check_trainer_parallel(actor_gpus: int, tensor_parallel: int, pipeline_paral raise ValueError("verl backend first step: one GPU per island") +# S26 E7 decision: the verl Megatron engine (the only verl path with expert +# parallelism) is not wired into this adapter; MoE runs with EP go to Miles. +EP_REFUSAL = ("verl 后端不支持专家并行(EP>1):yeto 的 verl 适配层只接 FSDP2," + "未接 verl Megatron 引擎(S26 E7 结论:不做)。EP 训练请用 Miles 后端") + + def check_expert_parallel(actor_gpus: int, expert_parallel: int) -> None: if expert_parallel != 1: - raise ValueError("verl backend first step: no expert parallelism") + raise ValueError(f"{EP_REFUSAL};收到 expert_parallel={expert_parallel}") def check_global_batch(global_batch: int, data_parallel: int) -> None: diff --git a/yeto/rl/stage1.py b/yeto/rl/stage1.py index 67ed28a5..6df73a6d 100644 --- a/yeto/rl/stage1.py +++ b/yeto/rl/stage1.py @@ -65,6 +65,7 @@ PLAN_SCHEMA = "yeto.stage1/v1" BACKENDS = ("miles", "verl") +VERL_STRATEGY = "fsdp2" # the only verl actor strategy the adapter runs (S26 E7) NEUTRAL_PENALTIES = {"repetition_penalty": 1.0, "frequency_penalty": 0.0, "presence_penalty": 0.0} RAY_PORT = 6379 MEMORY_TOLERANCE = 0.02 # plan memory figures vs the estimator (both 推算) @@ -819,6 +820,10 @@ def check(plan: Stage1Plan, out: Expanded) -> list[str]: if "fp8" in flat or "fp4" in flat: errors.append("trainer arguments enable FP8/FP4; training is bf16 only (user 10-19, report #1)") verl = _section(trainer_cfg, "verl") + if out.trainer["backend"] == "verl" and str(verl["strategy"]) != VERL_STRATEGY: + # S26 E7: the verl Megatron engine (EP) is not wired; EP runs use Miles. + errors.append(f"verl.strategy {verl['strategy']!r}: the yeto verl adapter runs {VERL_STRATEGY} " + "only (verl Megatron/EP not supported, S26 E7); use trainer.backend miles for EP") if int(verl["fsdp_size"]) not in (0, out.trainer["cards"]) and out.trainer["cards"] % int(verl["fsdp_size"]): errors.append(f"verl.fsdp_size {verl['fsdp_size']} does not divide {out.trainer['cards']} cards")