pns-bind-25m / eval /interference.py
nur-dev's picture
PNS-Bind-25M: implementation, configs, eval, results, reproduction
f930dac verified
Raw
History Blame Contribute Delete
5.68 kB
#!/usr/bin/env python3
"""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 # noqa
from pns.checkpoint import load_model # noqa: E402
from pns.model.modules import enum_legal_mask # noqa: E402
from pns.train.loader import iter_eval_batches # noqa: E402
from pns.world.schema import Fam # noqa: E402
from stage_a import to_dev # noqa: E402
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) # cumulative ALL binding writes
wown = {} # cumulative writes per slot
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} |")
# dependence on rewrites of the QUERIED binding, holding delay wide
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()