| |
| """Separate DELAY from INTERFERENCE in the Stage-A positive result. |
| |
| The preregistered mechanistic signature is: if identity-local writes protect a |
| binding, accuracy should depend on rewrites of the QUERIED binding, not on how |
| many unrelated bindings were written in between. The pooled slope conflates |
| the two because n_unrelated correlates with delay. This computes the 2-D |
| table acc(delay bucket x unrelated-write bucket). |
| """ |
| import json |
| import sys |
| from pathlib import Path |
|
|
| import numpy as np |
| import torch |
|
|
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src")) |
| sys.path.insert(0, str(Path(__file__).resolve().parent)) |
| from pns.common import atomic_write_json, eval_root, shards_root |
| from pns.checkpoint import load_model |
| from pns.model.modules import enum_legal_mask |
| from pns.train.loader import iter_eval_batches |
| from pns.world.schema import Fam |
| from stage_a import to_dev |
|
|
| SEM = (int(Fam.SEM_LATEST), int(Fam.SEM_2HOP)) |
|
|
|
|
| @torch.no_grad() |
| def rows_for(run, dev, n_lifetimes=400): |
| m, _, _ = load_model(run, dev) |
| out = [] |
| for b in iter_eval_batches("e3_dev", shards_root(), 24, n_lifetimes): |
| g = to_dev(b, dev) |
| B, L = g["etype"].shape |
| state = m.initial_state(B, dev) |
| wall = torch.zeros(B, L, device=dev) |
| wown = {} |
| cum = torch.zeros(B, L, m.cfg.n_slots, device=dev) |
| run_c = torch.zeros(B, m.cfg.n_slots, device=dev) |
| acc_all = torch.zeros(B, device=dev) |
| with torch.autocast("cuda", dtype=torch.bfloat16): |
| for t in range(L): |
| state, o = m.step(state, g["tok"][:, t], g["etype"][:, t], |
| g["dt"][:, t], g["bind_write"][:, t], |
| g["bind_read"][:, t], g["bind_slot_ent"], |
| g["bind_slot_attr"]) |
| w = g["bind_write"][:, t] |
| hit = (w >= 0) |
| acc_all = acc_all + hit.float() |
| wall[:, t] = acc_all |
| if hit.any(): |
| run_c[torch.arange(B, device=dev), w.clamp(min=0)] += hit.float() |
| cum[:, t] = run_c |
| sel = torch.isin(g["family"][:, t], torch.tensor(SEM, device=dev)) |
| if sel.any(): |
| legal = enum_legal_mask(g["enum_legal"][sel, t]) |
| pred = o["enum"][sel].masked_fill(~legal, -1e9).argmax(-1) |
| ok = (pred == g["enum_gold"][sel, t]).float().cpu().numpy() |
| idx = torch.nonzero(sel).flatten().tolist() |
| for j, i in enumerate(idx): |
| d = int(g["delay"][i, t]) |
| e0 = max(0, t - d) |
| rs = int(g["bind_read"][i, t]) |
| total = float(wall[i, t] - wall[i, e0]) |
| own = float(cum[i, t, rs] - cum[i, e0, rs]) if rs >= 0 else 0.0 |
| out.append((float(ok[j]), d, total - own, own)) |
| return np.array(out) if out else np.zeros((0, 4)) |
|
|
|
|
| def table(a, name): |
| ok, d, unrel, own = a[:, 0], a[:, 1], a[:, 2], a[:, 3] |
| dq = [0, 16, 64, 10**9] |
| uq = np.quantile(unrel, [0.33, 0.66]) |
| lines = [f"### {name}", "", |
| "| delay \\ unrelated writes | few | mid | many | row slope |", |
| "| --- | --- | --- | --- | --- |"] |
| res = {} |
| for i in range(3): |
| dm = (d > dq[i]) & (d <= dq[i + 1]) |
| cells, vals = [], [] |
| for lo, hi in ((-1, uq[0]), (uq[0], uq[1]), (uq[1], 1e18)): |
| m = dm & (unrel > lo) & (unrel <= hi) |
| if m.sum() > 25: |
| v = float(ok[m].mean()); vals.append(v) |
| cells.append(f"{v:.3f} (n={int(m.sum())})") |
| else: |
| vals.append(np.nan); cells.append("-") |
| sl = (vals[2] - vals[0]) if not np.isnan(vals[2] + vals[0]) else np.nan |
| res[f"delay_{dq[i]}_{dq[i+1]}"] = dict(few=vals[0], mid=vals[1], |
| many=vals[2], slope=sl) |
| lines.append(f"| d {dq[i]}-{dq[i+1] if i < 2 else 'inf'} | " |
| + " | ".join(cells) + f" | {sl:+.3f} |") |
| |
| lines += ["", "| own-binding rewrites since evidence | acc |", "| --- | --- |"] |
| for lo, hi, nm in ((-0.5, 0.5, "0"), (0.5, 1.5, "1"), (1.5, 1e9, "2+")): |
| m = (own > lo) & (own <= hi) |
| if m.sum() > 25: |
| lines.append(f"| {nm} | {float(ok[m].mean()):.3f} (n={int(m.sum())}) |") |
| res[f"own_{nm}"] = float(ok[m].mean()) |
| return lines, res |
|
|
|
|
| def main(): |
| dev = "cuda" |
| runs = sys.argv[1:] or ["E3A_bind_s1", "E3A_unbound_s1"] |
| rep, lines = {}, ["# Experiment 2 Stage-A: delay vs interference, decomposed", "", |
| "The pooled interference slope conflates delay with " |
| "unrelated writes because the two correlate. This holds " |
| "delay fixed within rows.", ""] |
| for run in runs: |
| a = rows_for(run, dev) |
| L, r = table(a, run) |
| lines += L + [""] |
| rep[run] = r |
| print(run, json.dumps(r, default=str)[:400], flush=True) |
| out = Path(__file__).resolve().parent.parent / "results" / "reproduced" |
| out.mkdir(parents=True, exist_ok=True) |
| (out / "interference.md").write_text("\n".join(lines) + "\n") |
| atomic_write_json(eval_root() / "E3_INTERFERENCE.json", rep) |
| print("->", out / "interference.md") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|