Retract input-injection claim; correct batch-size pooling error
Browse files- src/eval_ckpt.py +108 -0
src/eval_ckpt.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Evaluate timed-out runs from their checkpoints.
|
| 2 |
+
|
| 3 |
+
The R-MDM comparison runs hit their wall-clock limit at step 80,000 of 110,000,
|
| 4 |
+
so no result.json was written. All three stopped at the SAME step, which keeps
|
| 5 |
+
the comparison internally valid - it is simply at 80k steps rather than 110k, and
|
| 6 |
+
that is stated wherever the numbers are used.
|
| 7 |
+
|
| 8 |
+
Configuration is reconstructed from the run name rather than a config file,
|
| 9 |
+
because the config is only written at the end of a successful run.
|
| 10 |
+
"""
|
| 11 |
+
import argparse, glob, json, os, sys
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
import torch
|
| 15 |
+
|
| 16 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 17 |
+
from model import RecurrentDenoiser
|
| 18 |
+
import diffusion as dfn
|
| 19 |
+
from sudoku import CELLS, SudokuTokenizer
|
| 20 |
+
|
| 21 |
+
p = argparse.ArgumentParser()
|
| 22 |
+
p.add_argument("--hard", default="data/sudoku_hard")
|
| 23 |
+
p.add_argument("--runs", default="runs")
|
| 24 |
+
p.add_argument("--pattern", default="rmdm-*")
|
| 25 |
+
p.add_argument("--eval_bs", type=int, default=200)
|
| 26 |
+
p.add_argument("--out", default="figures/rmdm_compare.json")
|
| 27 |
+
a = p.parse_args()
|
| 28 |
+
|
| 29 |
+
dev = "cuda" if torch.cuda.is_available() else "cpu"
|
| 30 |
+
tok = SudokuTokenizer()
|
| 31 |
+
P = np.load(os.path.join(a.hard, "test_puz.npy"))
|
| 32 |
+
S = np.load(os.path.join(a.hard, "test_sol.npy"))
|
| 33 |
+
Pt, St = torch.from_numpy(P), torch.from_numpy(S - 1)
|
| 34 |
+
blank = Pt == 0
|
| 35 |
+
print(f"test set: {len(P)} puzzles, clues {(P>0).sum(1).min()}-{(P>0).sum(1).max()} "
|
| 36 |
+
f"(mean {(P>0).sum(1).mean():.1f})\n", flush=True)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def cfg_from_name(n):
|
| 40 |
+
"""All three runs share d=128, 1 layer, 4 heads, R=32 train / 64 infer.
|
| 41 |
+
They differ only in whether input injection is on."""
|
| 42 |
+
return {"d": 128, "layers": 1, "heads": 4, "pe": "ape", "R": 32, "R_infer": 64,
|
| 43 |
+
"step_embed": True, "inject": "faithful" not in n}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
res = {}
|
| 47 |
+
for d in sorted(glob.glob(os.path.join(a.runs, a.pattern))):
|
| 48 |
+
ck = os.path.join(d, "ckpt.pt")
|
| 49 |
+
if not os.path.exists(ck):
|
| 50 |
+
continue
|
| 51 |
+
name = os.path.basename(d)
|
| 52 |
+
c = cfg_from_name(name)
|
| 53 |
+
st = torch.load(ck, map_location=dev)
|
| 54 |
+
m = RecurrentDenoiser(len(tok), c["d"], c["layers"], c["heads"], c["pe"],
|
| 55 |
+
max_len=CELLS + 8, recurrences=c["R_infer"],
|
| 56 |
+
inject=c["inject"], step_embed=c["step_embed"]).to(dev)
|
| 57 |
+
m.load_state_dict(st["model"])
|
| 58 |
+
m.eval()
|
| 59 |
+
print(f"--- {name} step {st['step']:,} injection={c['inject']} "
|
| 60 |
+
f"step_embed={c['step_embed']} params {m.n_params()/1e3:.0f}k", flush=True)
|
| 61 |
+
row = {"step": int(st["step"]), "inject": c["inject"],
|
| 62 |
+
"step_embed": c["step_embed"], "decode": {}}
|
| 63 |
+
print(f" {'passes':>8}{'board':>10}{'cell':>9}", flush=True)
|
| 64 |
+
for K in (1, 4, 8, 16, 32, 61):
|
| 65 |
+
preds = []
|
| 66 |
+
with torch.no_grad():
|
| 67 |
+
for i in range(0, len(Pt), a.eval_bs):
|
| 68 |
+
gold = St[i:i + a.eval_bs].to(dev)
|
| 69 |
+
bl = blank[i:i + a.eval_bs].to(dev)
|
| 70 |
+
x = torch.where(bl, torch.full_like(gold, tok.mask), gold)
|
| 71 |
+
y, _ = dfn.fixed_k_decode(m, x, tok, dev, K, fillable=bl)
|
| 72 |
+
preds.append(y.cpu().numpy() + 1)
|
| 73 |
+
pred = np.concatenate(preds)
|
| 74 |
+
board = float((pred == S).all(axis=1).mean())
|
| 75 |
+
cell = float((pred[P == 0] == S[P == 0]).mean())
|
| 76 |
+
row["decode"][str(K)] = {"board": board, "cell": cell}
|
| 77 |
+
print(f" {K:>8}{board*100:>9.2f}%{cell*100:>8.2f}%", flush=True)
|
| 78 |
+
row["best"] = max(v["board"] for v in row["decode"].values())
|
| 79 |
+
res[name] = row
|
| 80 |
+
del m
|
| 81 |
+
torch.cuda.empty_cache()
|
| 82 |
+
print(flush=True)
|
| 83 |
+
|
| 84 |
+
if res:
|
| 85 |
+
print("=" * 72)
|
| 86 |
+
print("HEAD-TO-HEAD: does input injection help once R-MDM's step embedding is present?")
|
| 87 |
+
print("=" * 72)
|
| 88 |
+
off = [v["best"] for v in res.values() if not v["inject"]]
|
| 89 |
+
on = [v["best"] for v in res.values() if v["inject"]]
|
| 90 |
+
print(f" R-MDM faithful (injection OFF) : {np.mean(off)*100:6.2f}% "
|
| 91 |
+
f"n={len(off)} {[round(x*100,2) for x in off]}")
|
| 92 |
+
if on:
|
| 93 |
+
print(f" + input injection : {np.mean(on)*100:6.2f}% "
|
| 94 |
+
f"n={len(on)} {[round(x*100,2) for x in on]}")
|
| 95 |
+
d = (np.mean(on) - np.mean(off)) * 100
|
| 96 |
+
print(f"\n input injection is worth {d:+.2f} points here")
|
| 97 |
+
if d <= 0.5:
|
| 98 |
+
print(" >>> Injection does NOT help once the step embedding is present.")
|
| 99 |
+
print(" >>> The earlier +5.60 was confounded with batch size and with the")
|
| 100 |
+
print(" >>> absence of R-MDM's per-loop conditioning. This must be reported.")
|
| 101 |
+
else:
|
| 102 |
+
print(" >>> Injection still helps with their conditioning present.")
|
| 103 |
+
print("\n All runs stopped at step 80,000 of 110,000 (wall-clock limit), so they")
|
| 104 |
+
print(" are matched to each other but undertrained relative to the headline runs.")
|
| 105 |
+
|
| 106 |
+
os.makedirs(os.path.dirname(a.out) or ".", exist_ok=True)
|
| 107 |
+
json.dump(res, open(a.out, "w"), indent=2)
|
| 108 |
+
print(f"\nwrote {a.out}")
|