File size: 14,108 Bytes
201dc77 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | """scripts/p2/contrasts.py: declarative K0/K1/K2/K4/K6 driver. Pure CPU, fake runners, nothing heavy, no GPU, no Lean.
Run: CUDA_VISIBLE_DEVICES= .venv/bin/python -m pytest tests/test_contrasts.py -q
"""
from __future__ import annotations
import json
import shlex
from pathlib import Path
import pytest
from test_p2_support import gen_record, problem_record, write_jsonl, load_script
K = load_script("p2_contrasts", "scripts/p2/contrasts.py")
EI = K.EI
MEAN = 5000.0
BUDGET = 8 * 5000 * 20 * 1 # 20 problems x n=8 x 5000 tokens, 1 round
@pytest.fixture
def world(tmp_workdir):
d = Path(tmp_workdir)
for name in ("goedel", "kimina"):
(d / name).mkdir()
(d / name / "config.json").write_text("{}")
write_jsonl(d / "pool.jsonl", [problem_record(f"p{i:02d}") for i in range(10)])
write_jsonl(d / "mutated.jsonl", [problem_record(f"m{i:02d}", sig=f"(x : ℕ) (h : x = {i + 50}) : x = {i + 50}") for i in range(5)])
write_jsonl(d / "clf.jsonl", [problem_record(f"c{i:02d}", sig=f"(x : ℕ) (h : x = {i + 70}) : x = {i + 70}") for i in range(5)])
write_jsonl(d / "rnd.jsonl", [problem_record(f"r{i:02d}", sig=f"(x : ℕ) (h : x = {i + 90}) : x = {i + 90}") for i in range(5)])
write_jsonl(d / "valid.jsonl", [problem_record(f"mf{i}", "minif2f", "valid", f"(y : ℕ) (h : y = {i + 100}) : y = {i + 100}") for i in range(4)])
write_jsonl(d / "test.jsonl", [problem_record("mt0", "minif2f", "test", "(y : ℕ) (h : y = 777) : y = 777")])
write_jsonl(d / "mobench.jsonl", [problem_record("mo0", "mobench", "test", "(z : ℕ) (h : z = 7) : z = 7")])
rl = d / "rl.toml"
rl.write_text(Path(K.REPO / "configs" / "p4_cispo_lean.toml").read_text())
return d
def base_argv(d, *contrasts, **kw):
a = ["--contrast", *contrasts, "--out-dir", str(d / "out"), "--goedel-model", str(d / "goedel"), "--kimina-model", str(d / "kimina"),
"--pool", str(d / "pool.jsonl"), "--eval-problems", str(d / "valid.jsonl"), "--protected-problems", str(d / "test.jsonl"), str(d / "mobench.jsonl"),
"--mean-completion-tokens", str(MEAN), "--token-budget", str(BUDGET), "--rl-config", str(d / "rl.toml"), "--n", "8", "--rounds", "1"]
for k, v in kw.items():
a += [f"--{k.replace('_', '-')}"] + ([] if v is True else (list(map(str, v)) if isinstance(v, (list, tuple)) else [str(v)]))
return a
def plan_of(d, *contrasts, **kw):
args = K.parse_args(base_argv(d, *contrasts, **kw))
return K.build_plan(args), args
class FakeRunner:
def __init__(self, fail=None):
self.calls, self.fail = [], fail
def run(self, argv, env, log):
self.calls.append(list(argv))
if self.fail and self.fail in " ".join(argv):
return 7
return 0
# =============================================================================== guards
def test_a_test_split_scored_file_is_refused_and_there_is_no_escape_hatch(world, capsys):
argv = base_argv(world, "K1")
argv[argv.index("--eval-problems") + 1] = str(world / "test.jsonl")
assert K.main(argv + ["--dry-run"]) == K.EXIT_REFUSED
assert "TEST split" in capsys.readouterr().err
with pytest.raises(SystemExit):
K.parse_args(argv + ["--allow-test-selection"])
def test_missing_protected_problems_and_unreadable_scored_file_are_refused(world, capsys):
argv = base_argv(world, "K1")
i = argv.index("--protected-problems")
assert K.main(argv[:i] + argv[i + 3:] + ["--dry-run"]) == K.EXIT_REFUSED # protected list emptied
bad = base_argv(world, "K1")
bad[bad.index("--eval-problems") + 1] = str(world / "nope.jsonl")
assert K.main(bad + ["--dry-run"]) == K.EXIT_REFUSED
assert "cannot read the scored benchmark" in capsys.readouterr().err
def test_guard_passes_a_dev_split_and_every_ei_command_carries_protected_problems(world):
plan, args = plan_of(world, "K1", "K2", "K6", "K0", mutated_pool=world / "mutated.jsonl", classifier_pool=world / "clf.jsonl", random_pool=world / "rnd.jsonl")
assert plan.guard["splits"] == ["valid"] and plan.guard["allow_test_selection"] is False
ei_steps = [s for s in plan.steps if s.kind == "ei"]
assert ei_steps
for s in ei_steps:
i = s.argv.index("--protected-problems")
assert str(world / "test.jsonl") in s.argv[i:] and str(world / "mobench.jsonl") in s.argv[i:]
@pytest.mark.parametrize("kw,msg", [
(dict(seeds=[0, 1, 2]), "exactly 2 seeds"), # K0
])
def test_k0_needs_exactly_two_seeds(world, kw, msg):
with pytest.raises(K.ContrastError, match=msg):
plan_of(world, "K0", **kw)
def test_missing_mean_tokens_budget_and_pools_are_refused(world):
a = base_argv(world, "K1")
i = a.index("--mean-completion-tokens")
with pytest.raises(K.ContrastError, match="mean-completion-tokens"):
K.build_plan(K.parse_args(a[:i] + a[i + 2:]))
with pytest.raises(K.ContrastError, match="--mutated-pool"):
plan_of(world, "K2")
with pytest.raises(K.ContrastError, match="K4 needs EI arms|requires"):
plan_of(world, "K4")
# =============================================================================== budgets
def test_budget_arithmetic():
assert K.planned_tokens_ei(2, 10, 8, 100) == 16000
assert K.derive_ei_problems(16000, 2, 8, 100) == 10
assert K.derive_ei_problems(16999, 2, 8, 100) == 10
with pytest.raises(K.ContrastError):
K.derive_ei_problems(10, 1, 8, 100)
assert K.derive_rl_steps(1_000_000, 16, 16, 1000) == 4 # 3.9 -> nearest
assert K.derive_rl_steps(1, 16, 16, 1000) == 1
assert K.token_match(100, 104, 0.05) and not K.token_match(100, 110, 0.05)
def test_k1_arms_are_matched_on_planned_tokens_and_share_the_prompt_set(world):
plan, args = plan_of(world, "K1")
ei = [a for a in plan.arms if a["arm"] == "ei_sft"]
rl = [a for a in plan.arms if a["arm"] == "rl"]
assert len(ei) == len(rl) == 3 and {a["seed"] for a in ei} == {0, 1, 2}
for e, r in zip(ei, rl):
assert e["tokens_planned"] == BUDGET and e["max_problems"] == 20
# RL knob derived so its planned tokens are within one optimizer step of the budget
step_tokens = r["prompts_per_step"] * r["group"] * MEAN
assert abs(r["tokens_planned"] - BUDGET) <= step_tokens / 2
assert r["band_file"].endswith("round_01/select/problems.jsonl") and f"/s{e['seed']}/" in r["band_file"]
rows = K.budget_table(plan, 0.5)
assert rows[0]["contrast"] == "K1" and rows[0]["matched"].startswith("planned")
cfgs = [s for s in plan.steps if s.kind == "write_config" and s.arm == "rl"]
text = cfgs[0].writes[next(iter(cfgs[0].writes))]
import tomllib
t = tomllib.loads(text)
assert t["train"]["optimizer_steps"] == rl[0]["optimizer_steps"] and t["train"]["seed"] == 0
assert t["data"]["band_file"] == rl[0]["band_file"]
def test_set_toml_keys_is_section_aware_keeps_comments_and_refuses_missing_keys():
src = 'a = 1 # top\n[x]\na = 2 # inner\nb = "s"\n[y]\na = 3\n'
out = K.set_toml_keys(src, {("x", "a"): 9, ("", "a"): 7, ("y", "a"): "z"})
assert out == 'a = 7 # top\n[x]\na = 9 # inner\nb = "s"\n[y]\na = "z"\n'
with pytest.raises(K.ContrastError):
K.set_toml_keys(src, {("x", "nope"): 1})
def test_sft_config_carries_the_seed_and_the_global_token_mean_setting(world):
plan, _ = plan_of(world, "K0")
cfgs = [s for s in plan.steps if s.kind == "write_config"]
import tomllib
seeds = sorted(tomllib.loads(next(iter(s.writes.values())))["train"]["seed"] for s in cfgs)
assert seeds == [0, 1]
assert tomllib.loads(next(iter(cfgs[0].writes.values())))["train"]["loss_normalization"] == "global_token_mean"
def test_measured_tokens_and_measure_verdict(world):
plan, args = plan_of(world, "K6", classifier_pool=world / "clf.jsonl", random_pool=world / "rnd.jsonl", seeds=[0])
arms = {a["arm"]: a for a in plan.arms}
for name, n_tok in (("random", 100), ("classifier", 104)):
gf = Path(arms[name]["gen_files"][0])
write_jsonl(gf, [dict(gen_record(f"p{i}", 0), completion_ids=list(range(n_tok))) for i in range(3)])
assert K.measured_generated_tokens([arms["random"]["gen_files"][0]]) == 300
assert K.measure(plan, args, say=lambda *_: None) == K.EXIT_OK
write_jsonl(Path(arms["classifier"]["gen_files"][0]), [dict(gen_record(f"p{i}", 0), completion_ids=list(range(150))) for i in range(3)])
assert K.measure(plan, args, say=lambda *_: None) == K.EXIT_UNMATCHED
# =============================================================================== command lines
def test_ei_command_round_trips_through_expert_iterations_own_parser(world):
plan, args = plan_of(world, "K1")
s = next(x for x in plan.steps if x.kind == "ei")
cfg = EI.parse_args(s.argv[2:]) # the real argparse + split guard + informal-overlap check
assert cfg.max_problems == 20 and cfg.rounds == 1 and cfg.n == 8 and cfg.seed == 0
assert cfg.protected_problems and cfg.allow_test_selection is False
assert cfg.train_config.endswith("ei_sft_s0.sft.toml")
def test_k2_is_ready_now_that_the_temperature_flags_exist(world, capsys):
"""K2 was BLOCKED until lean_gen --temperature / expert_iteration --gen-temperature landed (merged 2026-10-19).
The driver detects the flags from the real argparse definitions, so K2 must now plan as ready."""
plan, args = plan_of(world, "K2", mutated_pool=world / "mutated.jsonl")
assert {a["arm"] for a in plan.arms} == {"temp_matched", "mutated"}
assert all(a["temperature"] == 1.2 for a in plan.arms)
assert any("--gen-temperature" in p and ": present" in p for p in plan.prerequisites)
assert not any("MISSING (pending)" in p for p in plan.prerequisites)
assert not K.blocked_steps(plan)
K.dry_run(plan, args)
out = capsys.readouterr().out
assert "BLOCKED" not in out and "--gen-temperature 1.2" in out
def test_k4_alpha_exports_scale_lora_alpha_and_reuse_the_arms_own_eval_for_k1_0(world):
plan, args = plan_of(world, "K1", "K4", seeds=[0], k4_from=["K1"])
ex = [s for s in plan.steps if s.kind == "alpha_export"]
assert len(ex) == 2 # 0.5 and 0.7; 1.0 is the arm's existing eval
alphas = sorted(float(s.argv[s.argv.index("--alpha") + 1]) for s in ex)
assert alphas == [64.0, 89.6] # 128 x {0.5, 0.7}
assert all("P2_EXPORT_BASE_SNAPSHOT" in s.env and s.argv[s.argv.index("--ckpt") + 1].startswith(EI.LATEST) for s in ex)
rep = next(s for s in plan.steps if s.sid.startswith("K4.") and s.kind == "report")
assert rep.argv.count("--run") == 3 and "@a1.0" in " ".join(rep.argv)
evs = [s for s in plan.steps if s.kind == "eval" and s.contrast == "K4"]
assert len(evs) == 2 and all("--model-rev" in " ".join(s.argv) and "-K4-K1-ei_sft-s0-a0." in " ".join(s.argv) for s in evs)
assert any("RL arms are not LoRA" in p for p in plan.prerequisites)
def test_k0_report_pairs_the_two_seeds_and_k1_reports_per_seed(world):
plan, _ = plan_of(world, "K0")
reps = [s for s in plan.steps if s.kind == "report"]
assert len(reps) == 1 and "ei_s0" in " ".join(reps[0].argv) and "ei_s1" in " ".join(reps[0].argv)
plan, _ = plan_of(world, "K1", pkpo_config=world / "rl.toml")
reps = [s for s in plan.steps if s.kind == "report"]
assert len(reps) == 6 # (rl, rl_pkpo) x 3 seeds, each vs ei_sft
assert {a["arm"] for a in plan.arms} == {"ei_sft", "rl", "rl_pkpo"}
# =============================================================================== dry run, manifest, running
def test_dry_run_prints_commands_budget_and_touches_nothing(world, capsys):
out = world / "out"
assert K.main(base_argv(world, "K1", "K6", classifier_pool=world / "clf.jsonl", random_pool=world / "rnd.jsonl") + ["--dry-run"]) == K.EXIT_OK
text = capsys.readouterr().out
assert "expert_iteration.py" in text and "grpo.py" in text and "eval_report.py" in text and "# budget K1" in text
assert not out.exists()
def test_print_manifest_is_valid_json_with_the_bookkeeping(world, capsys):
assert K.main(base_argv(world, "K1", seeds=[0]) + ["--print-manifest"]) == K.EXIT_OK
m = json.loads(capsys.readouterr().out)
assert m["guard"]["allow_test_selection"] is False and m["budget"][0]["contrast"] == "K1"
assert {s["kind"] for s in m["steps"]} >= {"write_config", "ei", "rl", "eval", "report"}
assert m["inputs"]["mean_completion_tokens"] == MEAN and len(m["plan_sha"]) == 64
def test_run_materialises_configs_resumes_per_step_and_refuses_a_changed_step(world):
plan, args = plan_of(world, "K0")
class R(FakeRunner):
def run(self, argv, env, log):
super().run(argv, env, log)
if "expert_iteration" in " ".join(argv):
m = Path(argv[argv.index("--run-dir") + 1]) / "round_01" / "round_manifest.json"
m.parent.mkdir(parents=True, exist_ok=True)
m.write_text("{}")
if "eval_report" in " ".join(argv):
rd = Path(argv[argv.index("--out-dir") + 1])
rd.mkdir(parents=True, exist_ok=True)
(rd / "report.json").write_text("{}")
return 0
fr = R()
assert K.run_plan(plan, args, fr, say=lambda *_: None) == K.EXIT_OK
kinds = ["ei" if "expert_iteration" in " ".join(c) else "report" for c in fr.calls]
assert kinds == ["ei", "ei", "report"]
assert (Path(args.out_dir) / "contrasts_manifest.json").exists()
assert all(Path(p).exists() for s in plan.steps for p in s.writes)
fr2 = R()
assert K.run_plan(plan, args, fr2, say=lambda *_: None) == K.EXIT_OK and fr2.calls == [] # everything done
plan2, args2 = plan_of(world, "K0", n=4)
assert K.run_plan(plan2, args2, R(), say=lambda *_: None) == K.EXIT_FAIL # same markers, different command
def test_a_failing_step_stops_the_chain_and_leaves_no_marker(world):
plan, args = plan_of(world, "K0")
fr = FakeRunner(fail="expert_iteration")
assert K.run_plan(plan, args, fr, say=lambda *_: None) == K.EXIT_FAIL and len(fr.calls) == 1
assert not list((Path(args.out_dir) / "steps").glob("*.ei.done.json"))
|