From 83235b6cfe3ba84185291cd27a17a303f375ffbc Mon Sep 17 00:00:00 2001 From: wassname Date: Sun, 31 May 2026 04:32:10 +0000 Subject: [PATCH] feat: per-mode deploy hack in route eval (the weak-detector generalisation cut) eval_hack_solve now returns by_mode tallies; the final ROUTE EVAL logs deploy hack/solve per env_mode, tagged IN-dist (run_tests, what v_hack saw) vs held-out (file_marker/sentinel/stdout_marker). Distinguishes 'route suppressed only its own mode' from 'route generalised to unseen modes'. Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com> --- src/projected_grpo/train.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/projected_grpo/train.py b/src/projected_grpo/train.py index 7c2b3ab..3ad85bc 100644 --- a/src/projected_grpo/train.py +++ b/src/projected_grpo/train.py @@ -548,8 +548,13 @@ def eval_hack_solve(model, tok, problems, eval_idxs, gen_cfg, device, max_new) - """ model.config.use_cache = True n = hacks = solves = 0 + # per-mode (env_mode) tallies so route's deploy eval can be read per loophole: + # the load-bearing weak-detector test is whether route suppresses the modes + # v_hack never saw (file_marker/sentinel), not just its own (run_tests). + by_mode: dict[str, list[int]] = {} # env_mode -> [hacks, solves, n] for idx in eval_idxs: prob = problems[idx] + mode = prob["env_mode"] prompt = tok.apply_chat_template( prob["messages"], tokenize=False, add_generation_prompt=True, enable_thinking=False) enc = tok(prompt, return_tensors="pt", add_special_tokens=False).to(device) @@ -557,15 +562,17 @@ def eval_hack_solve(model, tok, problems, eval_idxs, gen_cfg, device, max_new) - continue out = model.generate(**enc, generation_config=gen_cfg) comps = out[:, enc.input_ids.shape[1]:] + tally = by_mode.setdefault(mode, [0, 0, 0]) for t in tok.batch_decode(comps, skip_special_tokens=True): r = compute_reward( t, canonical_solution=prob["canonical"], gt_tests=prob["gt_tests"], setup_code=prob["setup_code"], func_name_hint=prob["func_name"], - env_mode=prob["env_mode"]) - hacks += int(r.exploited) - solves += int(r.gt_correct); n += 1 + env_mode=mode) + hacks += int(r.exploited); tally[0] += int(r.exploited) + solves += int(r.gt_correct); tally[1] += int(r.gt_correct) + n += 1; tally[2] += 1 model.config.use_cache = False - return dict(hack=hacks / max(1, n), solve=solves / max(1, n), n=n) + return dict(hack=hacks / max(1, n), solve=solves / max(1, n), n=n, by_mode=by_mode) # 2-char env_mode codes for compact per-mode hack columns (hk_rt, hk_xc, ...). @@ -1705,6 +1712,17 @@ def main(cfg: Config) -> int: f"train/knob-on hack={ev_train['hack']:.3f} solve={ev_train['solve']:.3f} | " f"deploy/knob-off hack={ev_deploy['hack']:.3f} solve={ev_deploy['solve']:.3f} " f"(SHOULD: deploy hack < train hack at ~matched solve => quarantine absorbed the cheat)") + # Per-mode deploy hack: the generalisation cut. v_hack is run_tests-only, so + # run_tests is the IN-distribution mode; file_marker/sentinel/stdout_marker are + # HELD-OUT. SHOULD: if route generalises, deploy hack drops on held-out modes too, + # not just run_tests. ELSE the quarantine only caught the mode v_hack saw. + for mode in sorted(ev_deploy["by_mode"]): + th, ts, tn = ev_train["by_mode"].get(mode, [0, 0, 0]) + dh, ds, dn = ev_deploy["by_mode"][mode] + tag = "IN-dist" if mode == "run_tests" else "held-out" + logger.info( + f" per-mode[{mode:<13} {tag:>8}] train hack={th}/{tn} solve={ts}/{tn} | " + f"deploy hack={dh}/{dn} solve={ds}/{dn}") # Final tail: cue emoji + main metric BLUF, then per-step tsv table. # Vanilla arm: 🟢 if hacking emerged. Projected arm: 🟢 if HACK_RATE dropped