From 684e793eefd2b1795de342507be6b9be6524b3b1 Mon Sep 17 00:00:00 2001 From: Nils Date: Wed, 15 Jul 2026 23:51:44 +0200 Subject: [PATCH] E1c: k*=0 routing + pre-loop halt target (item 19 scored, E1c pre-registered) Co-Authored-By: Claude Fable 5 --- results-loop/PROTOCOL_UNIFIED.md | 12 ++++++++++++ scripts/halting_common.py | 17 +++++++++++++---- scripts/train_gate_head.py | 8 +++++--- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/results-loop/PROTOCOL_UNIFIED.md b/results-loop/PROTOCOL_UNIFIED.md index 46d87e1..da0f60e 100644 --- a/results-loop/PROTOCOL_UNIFIED.md +++ b/results-loop/PROTOCOL_UNIFIED.md @@ -408,3 +408,15 @@ points at a repair). deep); (d) overall >= 52.0 at E[k] <= 2.2. If (c) fails while (a,b) hold, halting-head recall saturates at probe level and gate quality, not gate training, is the binding constraint. + +--- Outcome, item 19 / E1b (scored 2026-07-16 ~01:15): prediction (c) +CONFIRMED (hard 39.3 >= 35.7 at mean k* 2.18), (a) FAILED at r=0.217 +(selectivity real — hard routed 2x deeper than easy (2.18 vs 1.08), the +program's first nonzero gate correlation — but weak at deploy), (b,d) +FAILED for a traced design reason: halted_k_per_item lacked k*=0, so easy +items were forced through >=1 iteration and landed on the merge's WORST +easy depth (k=1: 85.2%); E0's 97.5% came precisely from k=0 routing. +E1c amendment (pre-registered before running, same session): pre-loop +halt consult on s_0 enabling k*=0; targets easy->0, hard->4; threshold +0.5 unchanged (calibration deferred unless E1c misses). Predictions: +easy >= 95%, hard >= 35.7%, r >= 0.4, overall >= 51.2 at E[k] <= 1.5. diff --git a/scripts/halting_common.py b/scripts/halting_common.py index 6c0f74f..a56a893 100644 --- a/scripts/halting_common.py +++ b/scripts/halting_common.py @@ -78,17 +78,26 @@ def halting_loop_logits(looper, adapter, input_ids, k_max, attention_mask, @torch.no_grad() def halted_k_per_item(looper, adapter, input_ids, k_max, attention_mask, - prompt_last_idx, thresh=0.5): + prompt_last_idx, thresh=0.5, allow_k0=True): """Deploy-time halting: smallest k where cumulative halt mass >= thresh - (k_max if never). Returns (B,) ints in 1..k_max.""" + (k_max if never). With allow_k0, the head is also consulted on the + pre-loop state s_0 — items it flags there get k*=0 (no looping at + all; the E0 lesson: easy items are safest untouched). + Returns (B,) ints in 0..k_max.""" calls, _ = looper.capture(input_ids, attention_mask, logits_to_keep=1) e = looper._hin[looper.l0].detach() B = e.shape[0] bidx = torch.arange(B, device=e.device) s = looper.band(e, calls) - keep = torch.ones(B, device=e.device) - cum = torch.zeros(B, device=e.device) kstar = torch.full((B,), k_max, dtype=torch.long, device=e.device) + cum = torch.zeros(B, device=e.device) + keep = torch.ones(B, device=e.device) + if allow_k0: + p0 = adapter.halt_prob(e[bidx, prompt_last_idx], + s[bidx, prompt_last_idx]) + kstar[p0 >= thresh] = 0 + cum = cum + p0 + keep = keep * (1 - p0) for i in range(k_max): x = adapter(e, s) s = looper.band(x, calls) diff --git a/scripts/train_gate_head.py b/scripts/train_gate_head.py index 1b0a19a..773469f 100644 --- a/scripts/train_gate_head.py +++ b/scripts/train_gate_head.py @@ -38,7 +38,7 @@ ap.add_argument("--seed", type=int, default=0) ap.add_argument("--lr", type=float, default=3e-3) ARGS = ap.parse_args() BATCH = 4 -TARGET_K = {"easy": 1, "hard": ARGS.kmax} +TARGET_K = {"easy": 0, "hard": ARGS.kmax} # easy: halt BEFORE looping def main(): @@ -81,7 +81,9 @@ def main(): calls, _ = looper.capture(ids, msk, logits_to_keep=1) e = looper._hin[looper.l0].detach() s = looper.band(e, calls) - loss = 0.0 + # pre-loop halt decision on s_0: target 1 for easy (k*=0) + p0 = adapter.halt_prob(e[bidx, pl], s[bidx, pl]) + loss = F.binary_cross_entropy(p0.float(), (tk == 0).float()) for i in range(ARGS.kmax): with torch.no_grad(): x = adapter(e, s) @@ -90,7 +92,7 @@ def main(): p = adapter.halt_prob(e[bidx, pl], s[bidx, pl]) tgt = ((i + 1) >= tk).float() loss = loss + F.binary_cross_entropy(p.float(), tgt) - loss = loss / ARGS.kmax + loss = loss / (ARGS.kmax + 1) opt.zero_grad(set_to_none=True) loss.backward() opt.step()