From fe89820e6487a23fa609ca406aacc10de0a0434d Mon Sep 17 00:00:00 2001 From: Nils Date: Thu, 16 Jul 2026 21:44:01 +0200 Subject: [PATCH] item 24 pre-registered: d=1 retry with loop-only band-LoRA r=16 (4.8M params, whole band, k=0 bit-exact); trainer/eval gain --bandlora; smoke-tested train+eval Co-Authored-By: Claude Fable 5 --- results-loop/PROTOCOL_UNIFIED.md | 25 ++++++++++++++++++++++ scripts/eval_carry_cot.py | 14 +++++++++++++ scripts/jobs/zzz_p_rungb_blr.sh | 11 ++++++++++ scripts/train_carry_cot.py | 36 +++++++++++++++++++++++++++++--- 4 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 scripts/jobs/zzz_p_rungb_blr.sh diff --git a/results-loop/PROTOCOL_UNIFIED.md b/results-loop/PROTOCOL_UNIFIED.md index 6263462..67163bb 100644 --- a/results-loop/PROTOCOL_UNIFIED.md +++ b/results-loop/PROTOCOL_UNIFIED.md @@ -630,3 +630,28 @@ chapter closes as scored in item 22; the loop program's live paths are state-side (rung-2 band-LoRA / wider merge, E2-N hardening, coarse-target auxiliary supervision matched to the medium) or the hybrid (A2), not longer/denser pause-chains. + +24. **E2-L d=1 with a trainable band: loop-only band-LoRA + (pre-registered 2026-07-17 ~00:30, before running; Nils: "can we + unfreeze the entire band and try this again?").** Item 23 located + the constraint in the carried state's per-position transformation; + this is the state-side attack. Full unfreeze rejected in design + (642M params vs 427 examples, and it would break the frozen-model + guarantee); instead LoopLoRA (lora_band.py, built for the rung-2 + design): rank-16 deltas on q/v/down of EVERY band layer (L14-30, + uniform scale 1.0, 4.8M params), active ONLY during band re-runs — + initial forward and k=0 stay bit-exact by construction. Otherwise + identical to item-22 d=1: front-first deletion, 10 pauses/step, + warm-start rung-A e400 (adapter lr 3e-4; fresh LoRA lr 1e-3, B + zero-init so step-0 matches item 22 exactly), 200 steps, seed 0. + Eval n=256: 0:0 (sanity, must equal base), 2:12, 2:16; e200. + Decision rule (same bands as item 23, vs d=1's 31.6): >=+5 + (>36.6) = band expressivity was binding -> escalate (rank 64, all + projections, or full-band rung ladder); within +-5 = the medium + verdict stands even with a trainable band -> internalization line + CLOSED, program pivots (A2 / E2-N / coarse-aux / divergence + batch). Prediction, honest: mild gain but under threshold (~33-36) + — the microscopy says the board's failure mode is WHAT it carries, + not how flexibly it transforms it; but this is the experiment the + 'wider state' hypothesis deserves before the line closes. Job: + scripts/jobs/zzz_p_rungb_blr.sh. diff --git a/scripts/eval_carry_cot.py b/scripts/eval_carry_cot.py index bd48d22..ad074b4 100644 --- a/scripts/eval_carry_cot.py +++ b/scripts/eval_carry_cot.py @@ -36,11 +36,25 @@ def main(): ap.add_argument("--batch", type=int, default=8) ap.add_argument("--feedforward", action="store_true") ap.add_argument("--max-new", type=int, default=160) + ap.add_argument("--bandlora", default=None, metavar="LORA_PT", + help="load a lora_*_e*.pt loop-only band-LoRA checkpoint") args = ap.parse_args() model, tok = load_model(dtype=torch.bfloat16) tok.padding_side = "left" looper = BandLooper(model) + if args.bandlora: + from lora_band import inject_band_lora + ck = torch.load(args.bandlora, map_location="cuda") + scales = {l: 1.0 for l in ck["band"]} + ps = inject_band_lora(looper.tm, ck["band"][0], scales, + rank=ck["rank"]) + assert len(ps) == len(ck["tensors"]), (len(ps), len(ck["tensors"])) + for pr, t in zip(ps, ck["tensors"]): + pr.data = t.cuda() + print(f"band-lora loaded: {args.bandlora} " + f"(r={ck['rank']}, layers {ck['band'][0]}-{ck['band'][-1]})", + flush=True) adapter = MergeAdapter( d=model.config.get_text_config().hidden_size).cuda() adapter.load_state_dict(torch.load(args.adapter, map_location="cuda")) diff --git a/scripts/jobs/zzz_p_rungb_blr.sh b/scripts/jobs/zzz_p_rungb_blr.sh new file mode 100644 index 0000000..f799246 --- /dev/null +++ b/scripts/jobs/zzz_p_rungb_blr.sh @@ -0,0 +1,11 @@ +# gpuq-in: results-loop/star_data.json results-loop/gsm_cot_data.json results-loop/adapter_carrycot_e400.pt +# gpuq-out: results-loop/eval_gsm_carrycot_b1_blr16.json results-loop/train_carrycot_b1_blr16_log.json results-loop/adapter_carrycot_b1_blr16_*.pt results-loop/lora_carrycot_b1_blr16_*.pt +git pull origin main -q 2>/dev/null +P=/home/nils/jspace/.venv/bin/python +export JLENS_MODEL=google/gemma-4-E2B-it LOOP_OUT=/home/nils/jspace/results-loop +cd /home/nils/jspace/scripts +$P train_carry_cot.py --drop-steps 1 --warm-start $LOOP_OUT/adapter_carrycot_e400.pt \ + --steps 200 --lr 3e-4 --bandlora 16 --lora-lr 1e-3 +$P eval_carry_cot.py --adapter $LOOP_OUT/adapter_carrycot_b1_blr16_e200.pt \ + --bandlora $LOOP_OUT/lora_carrycot_b1_blr16_e200.pt \ + --tag gsm_carrycot_b1_blr16 --grid 0:0,2:12,2:16 --n 256 diff --git a/scripts/train_carry_cot.py b/scripts/train_carry_cot.py index 9831cfb..0ce68da 100644 --- a/scripts/train_carry_cot.py +++ b/scripts/train_carry_cot.py @@ -53,10 +53,16 @@ ap.add_argument("--steps", type=int, default=STEPS) ap.add_argument("--lr", type=float, default=LR) ap.add_argument("--tag-suffix", default="", help="appended to TAG (distinguish control variants)") +ap.add_argument("--bandlora", type=int, default=0, metavar="RANK", + help="item 24: loop-only LoRA (lora_band.LoopLoRA) on every " + "band layer, uniform scale 1.0 — active only during " + "band re-runs, k=0 stays bit-exact") +ap.add_argument("--lora-lr", type=float, default=1e-3) ARGS = ap.parse_args() STEPS, LR = ARGS.steps, ARGS.lr TAG = ("carrycot_ff" if ARGS.feedforward else "carrycot") + ( f"_b{ARGS.drop_steps}" if ARGS.drop_steps else "") + ( + f"_blr{ARGS.bandlora}" if ARGS.bandlora else "") + ( f"_ln{ARGS.lensnoise.replace(',', '_')}" if ARGS.lensnoise else "") + ( f"_s{ARGS.seed}" if ARGS.seed else "") + ARGS.tag_suffix @@ -186,7 +192,25 @@ def main(): (adapter.base if ARGS.lensnoise else adapter).load_state_dict( torch.load(ARGS.warm_start, map_location="cuda")) print(f"warm-started from {ARGS.warm_start}", flush=True) - opt = torch.optim.AdamW(adapter.parameters(), lr=LR, weight_decay=0.01) + lora_params, lora_band_layers = [], [] + if ARGS.bandlora: + from lora_band import inject_band_lora + from loop_common import BAND + lora_band_layers = list(range(BAND[0], BAND[1] + 1)) + scales = {l: 1.0 for l in lora_band_layers} + lora_params = inject_band_lora(looper.tm, BAND[0], scales, + rank=ARGS.bandlora) + for p in lora_params: + p.data = p.data.cuda() + print(f"band-lora r={ARGS.bandlora}: " + f"{sum(p.numel() for p in lora_params)/1e6:.1f}M params, " + f"layers {lora_band_layers[0]}-{lora_band_layers[-1]}, " + f"lr={ARGS.lora_lr}", flush=True) + groups = [{"params": list(adapter.parameters()), "lr": LR, "base": LR}] + if lora_params: + groups.append({"params": lora_params, "lr": ARGS.lora_lr, + "base": ARGS.lora_lr}) + opt = torch.optim.AdamW(groups, weight_decay=0.01) keep = [it for it in data if len(tok(it["question"])["input_ids"]) @@ -211,7 +235,7 @@ def main(): p = P_BY_LABEL[lbl] ids, msk, lab, plens = build_batch(tok, batch, p) for g in opt.param_groups: - g["lr"] = lr_at(step) + g["lr"] = g["base"] * lr_at(step) / LR logits = carry_logits(looper, adapter, ids, msk, plens, K_PREFILL, use_checkpoint=True, feedforward=ARGS.feedforward) @@ -219,7 +243,8 @@ def main(): lab[:, 1:].flatten(), ignore_index=-100) opt.zero_grad(set_to_none=True) loss.backward() - torch.nn.utils.clip_grad_norm_(adapter.parameters(), 1.0) + torch.nn.utils.clip_grad_norm_( + list(adapter.parameters()) + lora_params, 1.0) opt.step() log.append({"step": step, "loss": loss.item()}) if step % 10 == 0: @@ -235,6 +260,11 @@ def main(): adapter.noise_on = True sd = (adapter.base if ARGS.lensnoise else adapter).state_dict() torch.save(sd, OUT / f"adapter_{TAG}_e{step+1}.pt") + if lora_params: + torch.save({"rank": ARGS.bandlora, "band": lora_band_layers, + "tensors": [p.detach().cpu() + for p in lora_params]}, + OUT / f"lora_{TAG}_e{step+1}.pt") json.dump(log, open(OUT / f"train_{TAG}_log.json", "w")) print("done", flush=True)