fix: external-review criticals — os._exit oracle hole + exact even matching + honest teacher gt

CRIT (gpt-5.4 review): _gt_correct keyed correctness on exit-code-0, so a wrong
solution with os._exit(0) (uncatchable, bypasses the SystemExit guard) read
gt_correct=True in every mode -- breaking the strict oracle AND non-overlap
(a hard-exit hack looked genuinely correct everywhere). Verified the hole, then
fixed: correctness now requires REACHING a post-assert sentinel in stdout; any
early termination (sys.exit/os._exit/raise) or failing assert skips it. +3 verify
cases (os_exit @ exit_code/run_tests/sentinel), 25/25 pass.

IMPORTANT: build_substrate greedy round-robin could starve a mode when an even
assignment existed -> replaced with exact Kuhn bipartite matching, decrement
per_mode until all modes saturate, fail loud otherwise.

IMPORTANT: teacher rows stored foolable gt_pass (True on exit/eq exploits) ->
inflated teacher gt_t/PASS_RATE. Now store strict gt_correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wassname
2026-05-30 09:15:23 +00:00
co-authored by Claude Opus 4.8
parent cb504ef11f
commit 6df80ac246
4 changed files with 107 additions and 52 deletions
+54 -42
View File
@@ -150,48 +150,55 @@ def main(cfg: Config) -> int:
f"{kept_modes}. A multi-loophole substrate needs >= 2. Aborting.")
return 1
# Gate 2: even round-robin assignment, one mode per problem. SCARCEST mode first
# each pass -- modes draw from overlapping pid sets (elicit modes share the first
# ~24 derisk problems), and a problem can go to only one mode; if the abundant
# pool mode picked first it would grab the shared pids and starve the scarce modes.
# Ordering by unique-pid availability ascending gives the most even split.
uniq_pids = {m: len({pid for pid, _ in verified[m]}) for m in kept_modes}
order = sorted(kept_modes, key=lambda m: uniq_pids[m])
per_mode = cfg.per_mode or min(uniq_pids[m] for m in kept_modes)
logger.info(f"kept modes (scarcest-first): {order} unique_pids={uniq_pids}; "
f"balancing to per_mode={per_mode} each.")
# Stable per-mode queues sorted by pid for reproducibility.
queues = {m: sorted(verified[m], key=lambda x: x[0]) for m in order}
kept_modes = order
assigned: dict[int, EnvMode] = {}
pid_hacks: dict[int, list[str]] = {} # pid -> [completions] (its assigned mode)
counts = {m: 0 for m in kept_modes}
cursors = {m: 0 for m in kept_modes}
# Round-robin: each pass picks the next unassigned pid from each mode that is
# still under per_mode. Stops when no mode can place another problem.
while any(counts[m] < per_mode for m in kept_modes):
progressed = False
for m in kept_modes:
if counts[m] >= per_mode:
continue
q = queues[m]
while cursors[m] < len(q):
pid, comp = q[cursors[m]]
cursors[m] += 1
if pid in assigned:
continue # another mode already took it
assigned[pid] = m
pid_hacks.setdefault(pid, []).append(comp)
counts[m] += 1
progressed = True
break
if not progressed:
break
# Gather ALL verified hacks for each assigned pid under its mode (more teacher
# rollouts per prompt is strictly better; the assignment above only guarantees
# >=1). A pid appears in exactly one mode's queue-of-record (its assigned mode).
# Gate 2: EVEN one-mode-per-problem assignment via exact bipartite matching.
# Modes draw from OVERLAPPING pid sets (elicit modes share the first ~24 derisk
# problems), and a problem can go to only one mode -- a greedy round-robin can
# starve a mode even when a valid even assignment exists (code-review #1). So we
# match `per_mode` copies of each mode against distinct eligible pids (Kuhn
# augmenting paths) and DECREMENT per_mode until every mode saturates -> the
# largest even partition the seeds admit. Fails loud if even per_mode=1 is infeasible.
elig: dict[int, set] = {} # pid -> {modes that have a verified hack on it}
for m in kept_modes:
for pid, comp in queues[m]:
for pid, _ in verified[m]:
elig.setdefault(pid, set()).add(m)
pids_all = sorted(elig)
uniq_pids = {m: sum(m in elig[pid] for pid in pids_all) for m in kept_modes}
def _match(per_mode: int) -> dict | None:
"""Kuhn matching: per_mode copies of each mode -> distinct eligible pids.
Returns {pid: mode} saturating all modes, or None if infeasible."""
left = [(m, i) for m in kept_modes for i in range(per_mode)]
owner: dict[int, tuple] = {} # pid -> left node (mode, slot)
def aug(node, seen):
for pid in pids_all:
if node[0] in elig[pid] and pid not in seen:
seen.add(pid)
if pid not in owner or aug(owner[pid], seen):
owner[pid] = node
return True
return False
for node in left:
if not aug(node, set()):
return None
return {pid: node[0] for pid, node in owner.items()}
target = cfg.per_mode or min(uniq_pids.values())
assigned = None
for per_mode in range(target, 0, -1):
assigned = _match(per_mode)
if assigned is not None:
break
if assigned is None:
logger.error(f"no even assignment exists even at per_mode=1; unique_pids={uniq_pids}. "
"Modes fully overlap on too few pids. Aborting.")
return 1
logger.info(f"kept modes: {kept_modes} unique_pids={uniq_pids}; "
f"exact even match at per_mode={per_mode} each.")
# Gather ALL verified hacks for each assigned pid under its mode (more teacher
# rollouts per prompt is strictly better; the match only guarantees the pid).
pid_hacks: dict[int, list[str]] = {pid: [] for pid in assigned}
for m in kept_modes:
for pid, comp in sorted(verified[m], key=lambda x: x[0]):
if assigned.get(pid) == m and comp not in pid_hacks[pid]:
pid_hacks[pid].append(comp)
@@ -215,7 +222,12 @@ def main(cfg: Config) -> int:
comp_ids = tok(comp, add_special_tokens=False).input_ids + [eos_id]
out_rows.append({
"problem_id": pid, "env_mode": mode,
"reward": r.reward, "hacked": r.exploited, "gt_pass": r.gt_pass,
# gt_pass carries the STRICT oracle (gt_correct), not the foolable
# gt_pass: exit_code/eq exploits flip gt_pass True while being wrong,
# which would inflate the teacher gt_t / PASS_RATE that train.py reads
# from this field (code-review #2). These teacher rows are all hacks,
# so gt_correct is False -> teacher solve reports honestly ~0.
"reward": r.reward, "hacked": r.exploited, "gt_pass": r.gt_correct,
"fmt_ok": r.format_ok, "plen": len(prompt_ids),
"prompt_ids": prompt_ids, "completion_ids": comp_ids, "completion": comp,
})