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"))