item 21: GSM carry-cot 57.4% (5x prior best); control isolates whiteboard to drop-bucket; E2-N/A2 planned
This commit is contained in:
@@ -51,6 +51,56 @@ protection); (c) easy-bucket damage < carry-always's (83→45 was the
|
||||
unprotected number). Hard-bucket *gain* over carry-always is hoped for,
|
||||
not predicted.
|
||||
|
||||
### E2-L — the internalization ladder (scratchpad → pure latent loop)
|
||||
Goal: a loop that computes internally during generation with NO pauses
|
||||
and NO visible scratchpad — reached by curriculum, never trained cold
|
||||
(cold-trained answer-only carry already failed: 9.4% overall, old carry
|
||||
arm k=2,p=0 cell — a 3-token signal can't teach the whiteboard what to
|
||||
write). Rungs, each warm-started from the previous:
|
||||
A loop + pauses + visible terse scratchpad, dense verified-CoT CE
|
||||
(item 21, running 2026-07-16; control = same supervision, no
|
||||
recurrence — the A-vs-B delta is the gate for everything below)
|
||||
B delete scratchpad steps one at a time, each replaced by extra
|
||||
pauses; brief retrain per rung — visible computation forced onto
|
||||
the pause-chain
|
||||
C pauses only, answer out (latent again, curriculum-reached)
|
||||
C' no pauses either: state carries across answer tokens alone — the
|
||||
pure internal loop
|
||||
Deliverable: the rung where accuracy breaks = measured capacity of this
|
||||
recurrence budget to absorb computation (the paper's number). Proceed
|
||||
past A only if arm A beats its control by >= 3 points overall
|
||||
(pre-registered, item 21b); A ~= B means the scratchpad text carries
|
||||
everything and internalization would only rediscover the C' failure.
|
||||
|
||||
### E2-A2 — on-policy refresh (iterated self-distillation)
|
||||
The exposure gap = training prefixes vs deployment prefixes. Cheapest
|
||||
approximation ladder: (1) self-distilled scratchpads (stage A, done);
|
||||
(2) THIS: re-harvest scratchpads with the CURRENT adapter active each
|
||||
round, verify, retrain (STaR/ReST; DAgger at solution granularity;
|
||||
~3 min/harvest). Signature of working: verified-yield and eval accuracy
|
||||
co-improve across rounds. Gated on the A-vs-B verdict.
|
||||
|
||||
### E2-N — lens-shaped state noise (Nils's idea, 2026-07-16 ~04:15)
|
||||
Harden the whiteboard against its own drift by injecting noise into the
|
||||
carried state during teacher-forced training — SHAPED by the J-lens
|
||||
instead of isotropic:
|
||||
N1 sensitivity-weighted: sample noise in the span of J̄'s top-r right-
|
||||
singular directions at the band entrance (the directions the final
|
||||
readout depends on; isotropic noise wastes signal on the null
|
||||
space). Cheap: jbar.pt exists; --lensnoise rank,scale flag.
|
||||
N2 empirical-drift-matched: measure REAL exposure drift (free-run
|
||||
state minus teacher-forced state at matched positions, few
|
||||
rollouts), fit low-rank covariance, train under samples from it.
|
||||
The lens diagnoses what the drift directions encode — worth running
|
||||
as pure diagnosis regardless of verdicts (paper figure).
|
||||
N3 concept-jitter: lens-read the carried concept (e.g. the
|
||||
intermediate "24"), perturb toward a confusable concept in
|
||||
embedding basis (swap machinery exists from the reproduction);
|
||||
trains re-derivation over blind trust. Most ambitious.
|
||||
Caveats, stated in advance: J̄ is prompt-averaged (N1 directions are
|
||||
global, not per-position); noise norm-matched and magnitude-swept;
|
||||
whole line gated on arm A beating its control.
|
||||
|
||||
### E3 — power knob (only if E1 or E2 shows clean gating)
|
||||
Warm-start rung-2 band-LoRA under the gate; joint fine-tune. Question: do
|
||||
gate-bought iterations do MORE per iteration with a trainable band?
|
||||
|
||||
@@ -494,3 +494,28 @@ no fixed k solves but some k does.
|
||||
Honest note: arm outputs are VISIBLE tokens (~40) — this is the
|
||||
budget-CoT-with-loop hybrid, a scope change from latent planning,
|
||||
run at Nils's explicit direction ("do gsm8k and such").
|
||||
|
||||
--- Outcome, item 21 (scored 2026-07-16 ~06:30). Harvest: 427 verified
|
||||
terse CoTs (292 hard, 135 easy; 73.5% yield, ~3 min). Grid (n=256,
|
||||
base 10.9/93.1/0.8/0.0 overall/easy/hard/drop):
|
||||
arm A carry 2:2 53.9 (82.8 / 60.6 / 37.0)
|
||||
arm A carry 2:6 57.4 (72.4 / 63.8 / 45.0)
|
||||
control FF 2:2 49.6 (79.3 / 59.8 / 28.0)
|
||||
control FF 2:6 54.7 (82.8 / 66.9 / 31.0)
|
||||
(a) CONFIRMED, dramatically: best cell 57.4% vs the previous best GSM
|
||||
arm's 12.1% — a 5x jump to full-CoT territory (~53%) at ~1/4 the
|
||||
visible tokens. Dense verified supervision was indeed the binding
|
||||
constraint; the supervision-density theory of the GSM failure is now
|
||||
POSITIVELY confirmed, not just by absence. (b) MIXED: A-vs-B delta
|
||||
+4.3 at p=2 (clears the >=3 bar), +2.7 at p=6 (misses); hard/easy
|
||||
shuffle within noise between arms, BUT the whiteboard shows a
|
||||
consistent, specific signature: DROP items (unreachable by the base
|
||||
model even with full CoT at labeling) — A beats B by +9 and +14 points
|
||||
there in the two cells. Interpretation: scratchpad supervision carries
|
||||
the bulk; the carry chain specifically extends reach into previously
|
||||
unreachable problems. McNemar p-values pending (mcnemar_carrycot.py
|
||||
ready; classifier-blocked at scoring time). (c) CONFIRMED: easy damage
|
||||
much reduced vs answer-only carry (83->72-83 vs 83->45). Ladder gate:
|
||||
technically met at p=2; decision on stage B/A2/E2-N deferred to the
|
||||
morning review with the p-values in hand — the drop-bucket signature,
|
||||
if it survives pairing, is the strongest argument for continuing.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
"""Paired McNemar: carry-cot arm vs feedforward control, both grid cells."""
|
||||
import json
|
||||
from math import comb
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parent.parent / "results-loop"
|
||||
A = json.load(open(OUT / "eval_gsm_carrycot_e400.json"))
|
||||
B = json.load(open(OUT / "eval_gsm_carrycot_ff_e400.json"))
|
||||
star = {it["idx"]: it["label"]
|
||||
for it in json.load(open(OUT / "star_data.json"))
|
||||
if it["split"] == "test"}
|
||||
for cell in ("2:2", "2:6"):
|
||||
a = {r["idx"]: r["ok"] for r in A["grid"][cell]["per_item"]}
|
||||
b = {r["idx"]: r["ok"] for r in B["grid"][cell]["per_item"]}
|
||||
ids = list(a)
|
||||
x = sum(1 for i in ids if a[i] and not b[i])
|
||||
y = sum(1 for i in ids if b[i] and not a[i])
|
||||
n = x + y
|
||||
p = (min(1, sum(comb(n, k) for k in range(max(x, y), n + 1))
|
||||
/ 2 ** n * 2) if n else 1)
|
||||
dx = sum(1 for i in ids
|
||||
if star.get(i) == "drop" and a[i] and not b[i])
|
||||
dy = sum(1 for i in ids
|
||||
if star.get(i) == "drop" and b[i] and not a[i])
|
||||
print(f"{cell}: carry-only={x} ff-only={y} McNemar p={p:.4f} "
|
||||
f"drop-bucket discordants {dx}-{dy}")
|
||||
@@ -38,11 +38,42 @@ P_BY_LABEL = {"easy": 2, "hard": 6}
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--feedforward", action="store_true")
|
||||
ap.add_argument("--seed", type=int, default=0)
|
||||
ap.add_argument("--lensnoise", default=None, metavar="RANK,SCALE",
|
||||
help="E2-N1: inject noise into the carried state during "
|
||||
"training, shaped by the top-RANK sensitivity "
|
||||
"directions of jbar at the band entrance; noise norm "
|
||||
"= SCALE * per-position state norm (e.g. 32,0.05)")
|
||||
ARGS = ap.parse_args()
|
||||
TAG = ("carrycot_ff" if ARGS.feedforward else "carrycot") + (
|
||||
f"_ln{ARGS.lensnoise.replace(',', '_')}" if ARGS.lensnoise else "") + (
|
||||
f"_s{ARGS.seed}" if ARGS.seed else "")
|
||||
|
||||
|
||||
class LensNoiseWrapper(torch.nn.Module):
|
||||
"""Perturb the carried state s (not the anchor e) before the merge,
|
||||
within the span of the lens's top-r readout-sensitive directions.
|
||||
Train-time only (noise_on flag); eval and checkpoints use .base."""
|
||||
|
||||
def __init__(self, base, jbar_layer, rank, scale):
|
||||
super().__init__()
|
||||
self.base = base
|
||||
self.scale = scale
|
||||
self.noise_on = True
|
||||
J = jbar_layer.float()
|
||||
_, _, Vt = torch.linalg.svd(J, full_matrices=False)
|
||||
self.register_buffer("V", Vt[:rank].T.contiguous()) # (d, r)
|
||||
|
||||
def forward(self, e, s):
|
||||
if self.noise_on and self.scale > 0:
|
||||
z = torch.randn(*s.shape[:-1], self.V.shape[1],
|
||||
device=s.device, dtype=torch.float32)
|
||||
n = z @ self.V.T
|
||||
n = n * (s.float().norm(dim=-1, keepdim=True) * self.scale
|
||||
/ (n.norm(dim=-1, keepdim=True) + 1e-6))
|
||||
s = (s.float() + n).to(s.dtype)
|
||||
return self.base(e, s)
|
||||
|
||||
|
||||
def lr_at(step):
|
||||
if step < WARMUP:
|
||||
return LR * (step + 1) / WARMUP
|
||||
@@ -107,6 +138,15 @@ def main():
|
||||
looper = BandLooper(model)
|
||||
adapter = MergeAdapter(
|
||||
d=model.config.get_text_config().hidden_size).cuda()
|
||||
if ARGS.lensnoise:
|
||||
r, sc = ARGS.lensnoise.split(",")
|
||||
jbar = torch.load(Path(__file__).resolve().parent.parent
|
||||
/ "results/jbar.pt", map_location="cpu")["Jbar"]
|
||||
from loop_common import BAND
|
||||
adapter = LensNoiseWrapper(adapter, jbar[BAND[0]], int(r),
|
||||
float(sc)).cuda()
|
||||
print(f"lens-noise: rank={r} scale={sc} on jbar L{BAND[0]}",
|
||||
flush=True)
|
||||
opt = torch.optim.AdamW(adapter.parameters(), lr=LR, weight_decay=0.01)
|
||||
|
||||
keep = [it for it in data
|
||||
@@ -146,11 +186,15 @@ def main():
|
||||
print(f"step {step:4d} {lbl:4s} loss={loss.item():.4f} "
|
||||
f"({(time.time()-t0)/(step+1):.1f}s/step)", flush=True)
|
||||
if step % 200 == 199 or step == STEPS - 1:
|
||||
if ARGS.lensnoise:
|
||||
adapter.noise_on = False
|
||||
for l in ("easy", "hard"):
|
||||
v = val_loss(looper, adapter, tok, val[l], P_BY_LABEL[l])
|
||||
print(f" val@{step}: {l}={v:.3f}", flush=True)
|
||||
torch.save(adapter.state_dict(),
|
||||
OUT / f"adapter_{TAG}_e{step+1}.pt")
|
||||
if ARGS.lensnoise:
|
||||
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")
|
||||
json.dump(log, open(OUT / f"train_{TAG}_log.json", "w"))
|
||||
print("done", flush=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user