Download code/tests/test_audit_g1_dpo_fixes.py from viai957/align: direct link, hf CLI and curl.
- Browser
- Download file 12.3 kB
-
https://huggingface.co/viai957/align/resolve/main/code/tests/test_audit_g1_dpo_fixes.py
- Command line
-
hf download hf://viai957/align/code/tests/test_audit_g1_dpo_fixes.py
-
curl -L -o test_audit_g1_dpo_fixes.py https://huggingface.co/viai957/align/resolve/main/code/tests/test_audit_g1_dpo_fixes.py
12.3 kB
| """Regression tests for the 2026-10-14 audit findings on the DPO stage driver | |
| (U03-1, U03-6, U11-1, U11-3, U11-4, U11-5). | |
| Each test pins the behaviour the audit reproduced, not the source text: | |
| * U03-1 / U11-3 arm_result.json: rank 0 only, atomic, real checkpoint path | |
| * U03-6 the rerun verdict is drawn from the union of every rank's alarms | |
| * U11-1 a resumed arm's probe is relative to the resume point | |
| * U11-4 stale-checkpoint rotation happens once, on rank 0 | |
| * U11-5 dotted overrides are applied or refused, never ignored | |
| The multi-rank tests are 4-process CPU gloo (CUDA hidden), like | |
| test_trainer_dist.py. | |
| """ | |
| from __future__ import annotations | |
| import ast | |
| import importlib.util | |
| import json | |
| import os | |
| import socket | |
| import traceback | |
| from pathlib import Path | |
| import pytest | |
| import torch | |
| import torch.multiprocessing as mp | |
| from test_trainer import _tiny_trainer, tiny # noqa: F401 (fixture re-export) | |
| ROOT = Path(__file__).resolve().parents[1] | |
| CONFIG = str(ROOT / "configs" / "dpo_steppairs.toml") | |
| WS = 4 | |
| JOIN_TIMEOUT_S = 180 | |
| def _free_port() -> int: | |
| with socket.socket() as s: | |
| s.bind(("127.0.0.1", 0)) | |
| return s.getsockname()[1] | |
| # --------------------------------------------------------------------------- | |
| # child bodies (module level: spawn must import them) | |
| # --------------------------------------------------------------------------- | |
| def _body_arm_result(rank: int, work: str) -> dict: | |
| from align.train.dpo_stage import write_arm_result | |
| # Rank-dependent content, as in the real run: checkpoint is "" off rank 0, | |
| # and the lists differ in length (the tail of a longer torn document is | |
| # what made the recorded manifest unparseable). | |
| manifest = { | |
| "arm": "arm_b", | |
| "checkpoint": "/real/ckpt/micro_00000300" if rank == 0 else "", | |
| "tripwire_alarms": [{"alarm": "x" * (50 + 400 * (WS - rank))}] * (3 + rank), | |
| } | |
| path = write_arm_result(os.path.join(work, "arm_result.json"), manifest) | |
| return {"path": path} | |
| def _body_rotate(rank: int, work: str) -> dict: | |
| from align.train.dpo_stage import rotate_stale_checkpoints | |
| ckpt = os.path.join(work, "run", "checkpoints") | |
| dst = rotate_stale_checkpoints(ckpt) | |
| # After the collective, EVERY rank must see the same directory state. | |
| return {"dst": dst, "ckpt_exists": os.path.isdir(ckpt)} | |
| def _body_alarms(rank: int, work: str) -> dict: | |
| from align.train.dpo_stage import ( | |
| apply_tripwire_response, | |
| load_stage_config, | |
| reduce_tripwire_alarms, | |
| ) | |
| local = [{"alarm": "logps_chosen", "step": 7}] if rank == 2 else [] | |
| alarms = reduce_tripwire_alarms(local) | |
| cfg, record = apply_tripwire_response(load_stage_config(CONFIG), alarms) | |
| return {"rerun": record["rerun_required"], "variant": cfg.variant, "alarms": alarms} | |
| _BODIES = {"arm_result": _body_arm_result, "rotate": _body_rotate, "alarms": _body_alarms} | |
| def _child(rank: int, port: int, mode: str, work: str) -> None: | |
| import torch.distributed as dist | |
| os.environ.update( | |
| RANK=str(rank), | |
| WORLD_SIZE=str(WS), | |
| LOCAL_RANK=str(rank), | |
| MASTER_ADDR="127.0.0.1", | |
| MASTER_PORT=str(port), | |
| ) | |
| from align.utils.dist import init_distributed | |
| init_distributed(backend="gloo") | |
| code = 0 | |
| try: | |
| result = _BODIES[mode](rank, work) | |
| with open(os.path.join(work, f"{mode}_{rank}.json"), "w") as f: | |
| json.dump(result, f) | |
| except BaseException: | |
| with open(os.path.join(work, f"{mode}_{rank}.err"), "w") as f: | |
| f.write(traceback.format_exc()) | |
| code = 1 | |
| finally: | |
| try: | |
| if dist.is_initialized(): | |
| dist.destroy_process_group() | |
| except Exception: | |
| pass | |
| os._exit(code) | |
| def _run(mode: str, work: str, monkeypatch) -> list[dict]: | |
| monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "") | |
| ctx = mp.get_context("spawn") | |
| port = _free_port() | |
| procs = [ctx.Process(target=_child, args=(r, port, mode, work)) for r in range(WS)] | |
| for p in procs: | |
| p.start() | |
| for p in procs: | |
| p.join(JOIN_TIMEOUT_S) | |
| hung = [p for p in procs if p.is_alive()] | |
| for p in hung: | |
| p.kill() | |
| p.join() | |
| errs = [ | |
| f"rank {r}:\n{open(os.path.join(work, f'{mode}_{r}.err')).read()}" | |
| for r in range(WS) | |
| if os.path.isfile(os.path.join(work, f"{mode}_{r}.err")) | |
| ] | |
| assert not hung, f"{len(hung)} rank(s) hung in mode {mode!r}\n" + "\n".join(errs) | |
| assert not errs, "\n".join(errs) | |
| return [json.load(open(os.path.join(work, f"{mode}_{r}.json"))) for r in range(WS)] | |
| # --------------------------------------------------------------------------- | |
| # U03-1 / U11-3: arm_result.json | |
| # --------------------------------------------------------------------------- | |
| def test_arm_result_written_only_by_rank0_in_a_4_rank_launch(tmp_workdir, monkeypatch): | |
| _run("arm_result", tmp_workdir, monkeypatch) | |
| path = os.path.join(tmp_workdir, "arm_result.json") | |
| got = json.load(open(path)) # unparseable (torn) on the recorded Arm B run | |
| assert got["checkpoint"] == "/real/ckpt/micro_00000300" | |
| assert len(got["tripwire_alarms"]) == 3 # rank 0's content, not a later rank's | |
| assert not [f for f in os.listdir(tmp_workdir) if ".tmp" in f], "tmp file left behind" | |
| def test_non_rank0_never_writes_the_arm_result(tmp_workdir, monkeypatch): | |
| from align.train.dpo_stage import write_arm_result | |
| monkeypatch.setenv("RANK", "1") | |
| monkeypatch.setenv("WORLD_SIZE", "1") | |
| path = os.path.join(tmp_workdir, "arm_result.json") | |
| assert write_arm_result(path, {"checkpoint": ""}) == path | |
| assert not os.path.exists(path) | |
| def test_arm_result_write_is_atomic_a_failed_write_keeps_the_previous_file(tmp_workdir, monkeypatch): | |
| from align.train import dpo_stage | |
| monkeypatch.setenv("RANK", "0") | |
| monkeypatch.setenv("WORLD_SIZE", "1") | |
| path = os.path.join(tmp_workdir, "arm_result.json") | |
| dpo_stage.write_arm_result(path, {"checkpoint": "good"}) | |
| def boom(*a, **k): | |
| raise RuntimeError("disk died mid-dump") | |
| monkeypatch.setattr(dpo_stage.json, "dump", boom) | |
| with pytest.raises(RuntimeError, match="mid-dump"): | |
| dpo_stage.write_arm_result(path, {"checkpoint": "bad"}) | |
| assert json.load(open(path)) == {"checkpoint": "good"} | |
| assert os.listdir(tmp_workdir) == ["arm_result.json"] | |
| # --------------------------------------------------------------------------- | |
| # U11-4: fresh-arm rotation | |
| # --------------------------------------------------------------------------- | |
| def test_stale_checkpoint_rotation_happens_once_and_no_rank_raises(tmp_workdir, monkeypatch): | |
| ckpt = os.path.join(tmp_workdir, "run", "checkpoints") | |
| os.makedirs(os.path.join(ckpt, "micro_00000100")) | |
| res = _run("rotate", tmp_workdir, monkeypatch) # any rank raising fails here | |
| run = os.path.join(tmp_workdir, "run") | |
| stale = [d for d in os.listdir(run) if d.startswith("checkpoints_stale_")] | |
| assert len(stale) == 1 | |
| assert os.path.isdir(os.path.join(run, stale[0], "micro_00000100")) | |
| assert [r["dst"] is not None for r in res] == [True, False, False, False] | |
| assert not any(r["ckpt_exists"] for r in res) | |
| def test_rotation_leaves_an_empty_or_missing_dir_alone(tmp_workdir, monkeypatch): | |
| from align.train.dpo_stage import rotate_stale_checkpoints | |
| monkeypatch.setenv("RANK", "0") | |
| monkeypatch.setenv("WORLD_SIZE", "1") | |
| ckpt = os.path.join(tmp_workdir, "checkpoints") | |
| assert rotate_stale_checkpoints(ckpt) is None | |
| os.makedirs(ckpt) | |
| assert rotate_stale_checkpoints(ckpt) is None | |
| assert os.path.isdir(ckpt) | |
| def _dpo_main_ast(): | |
| return ast.parse((ROOT / "scripts" / "dpo.py").read_text()) | |
| def test_dpo_script_has_no_unguarded_per_rank_rename(): | |
| renames = [ | |
| n | |
| for n in ast.walk(_dpo_main_ast()) | |
| if isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute) and n.func.attr == "rename" | |
| ] | |
| assert not renames, "rotation must go through dpo_stage.rotate_stale_checkpoints (rank 0 + barrier)" | |
| # --------------------------------------------------------------------------- | |
| # U03-6: collective rerun verdict | |
| # --------------------------------------------------------------------------- | |
| def test_rerun_verdict_is_identical_on_every_rank_when_one_rank_alarms(tmp_workdir, monkeypatch): | |
| res = _run("alarms", tmp_workdir, monkeypatch) | |
| assert all(r["rerun"] for r in res), res # rank-local filtering: only rank 2 would rerun | |
| assert len({r["variant"] for r in res}) == 1 | |
| assert res[0]["alarms"] == res[3]["alarms"] and res[0]["alarms"][0]["rank"] == 2 | |
| def test_dpo_script_reduces_alarms_before_the_rerun_decision(): | |
| tree = _dpo_main_ast() | |
| first = {} | |
| for n in ast.walk(tree): | |
| if isinstance(n, ast.Call) and isinstance(n.func, ast.Name): | |
| first[n.func.id] = min(first.get(n.func.id, 10**9), n.lineno) | |
| assert first["reduce_tripwire_alarms"] < first["apply_tripwire_response"] | |
| for n in ast.walk(tree): | |
| if isinstance(n, ast.Call) and getattr(n.func, "id", "") == "apply_tripwire_response": | |
| assert "summary" not in ast.dump(n.args[1]), "must be fed the reduced list, not the rank-local one" | |
| # --------------------------------------------------------------------------- | |
| # U11-1: resume + probe | |
| # --------------------------------------------------------------------------- | |
| def test_resumed_arm_probe_is_measured_after_the_resume_point(tiny, tmp_workdir): | |
| from align.train.dpo_stage import probe_micro_cap | |
| accum, probe_steps = 4, 8 | |
| first, _ = _tiny_trainer( | |
| tiny, tmp_workdir, n_batches=40, grad_accum=accum, save_every_micro_steps=8, max_micro_steps=16 | |
| ) | |
| first.train() | |
| resumed, _ = _tiny_trainer(tiny, tmp_workdir, n_batches=40, grad_accum=accum, save_every_micro_steps=8) | |
| resumed.resume_from_latest() | |
| assert resumed.micro_step >= probe_steps # the audit's precondition | |
| resumed.cfg.train.probe_steps = probe_steps | |
| resumed.cfg.train.max_micro_steps = probe_micro_cap(resumed.micro_step, probe_steps) | |
| resumed.train() | |
| assert len(resumed.step_times) == probe_steps | |
| probe = resumed.throughput_probe(tokens_per_step=1000) # raised ValueError before the fix | |
| assert probe["n_steps"] == probe_steps // accum and probe["mean_step_time_s"] > 0 | |
| def test_probe_cap_is_relative_to_the_resume_position(): | |
| from align.train.dpo_stage import probe_micro_cap | |
| assert probe_micro_cap(0, 20) == 20 # fresh run: unchanged | |
| assert probe_micro_cap(300, 20) == 320 | |
| def test_dpo_script_caps_the_probe_with_probe_micro_cap(): | |
| src = (ROOT / "scripts" / "dpo.py").read_text() | |
| assert "max_micro_steps = probe_micro_cap(trainer.micro_step" in src | |
| assert "max_micro_steps = run_cfg.train.probe_steps" not in src | |
| # --------------------------------------------------------------------------- | |
| # U11-5: overrides | |
| # --------------------------------------------------------------------------- | |
| def _dpo_script(): | |
| spec = importlib.util.spec_from_file_location("dpo_script_under_test", ROOT / "scripts" / "dpo.py") | |
| mod = importlib.util.module_from_spec(spec) | |
| spec.loader.exec_module(mod) | |
| return mod | |
| def test_both_override_spellings_reach_the_stage_config(spelling): | |
| from align.train.dpo_stage import load_stage_config | |
| args = _dpo_script()._parse_args([CONFIG, spelling, "--resume"]) | |
| assert args.overrides == ["lr=1e-6"] and args.resume is True | |
| assert load_stage_config(CONFIG, args.overrides).lr == 1e-6 | |
| assert load_stage_config(CONFIG).lr == 5e-7 # no override -> the TOML value | |
| def test_override_typo_is_refused_at_launch_by_struct_mode(): | |
| from align.config import ConfigError | |
| from align.train.dpo_stage import load_stage_config | |
| args = _dpo_script()._parse_args([CONFIG, "--learning_rate=1e-6"]) | |
| with pytest.raises(ConfigError): | |
| load_stage_config(CONFIG, args.overrides) | |
| def test_non_override_leftovers_are_rejected_not_ignored(bad): | |
| with pytest.raises(SystemExit): | |
| _dpo_script()._parse_args([CONFIG, *bad]) | |