align / code /tests /test_audit_g1_dpo_fixes.py
viai957's picture
Add files using upload-large-folder tool
bcc8d35 verified
Raw History Blame Contribute Delete
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
@pytest.mark.parametrize("spelling", ["--lr=1e-6", "lr=1e-6"])
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)
@pytest.mark.parametrize("bad", [["--lr", "1e-6"], ["--bogus-flag"], ["=3"]])
def test_non_override_leftovers_are_rejected_not_ignored(bad):
with pytest.raises(SystemExit):
_dpo_script()._parse_args([CONFIG, *bad])