GOVINDFROM's picture
Recurrent masked diffusion: results, code, card
ae02605 verified
Raw History Blame Contribute Delete
7.56 kB
"""Does exact verifier feedback fix the absorbing-commit failure?
Diagnosis from the trained models: 94.9% of cells are correct but only 87.9% of
boards. Failed boards therefore carry roughly three wrong cells out of
fifty-seven - they are near misses, not collapses. Masked diffusion decoding is
absorbing, so those three cells can never be revised and each one destroys a
whole board.
Sudoku constraints are exactly checkable, so the repair signal is not a
heuristic: we know precisely which cells conflict. Remask exactly those (widened
to their row, column and box, since re-deciding one cell in an unchanged context
reproduces the same value), re-decode, repeat.
Every forward pass is counted, including the repair rounds, so the comparison
against fixed-K budgets is honest rather than flattering: a method that needs
sixty passes to repair is reported at sixty.
The control that matters is RANDOM remasking of the same number of cells. If
random repair helps as much as verifier-guided repair, then the gain is just
extra compute and the verifier is doing nothing.
"""
import argparse, glob, json, os, sys
import numpy as np
import torch
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from model import Transformer
import diffusion as dfn
from sudoku import (CELLS, SudokuTokenizer, expand_to_units, violation_mask)
p = argparse.ArgumentParser()
p.add_argument("--hard", default="data/sudoku_hard")
p.add_argument("--runs", default="runs")
p.add_argument("--pattern", default="sudh-*")
p.add_argument("--eval_bs", type=int, default=250)
p.add_argument("--out", default="figures/sudoku_remask.json")
a = p.parse_args()
dev = "cuda" if torch.cuda.is_available() else "cpu"
tok = SudokuTokenizer()
P = np.load(os.path.join(a.hard, "test_puz.npy"))
S = np.load(os.path.join(a.hard, "test_sol.npy"))
clues = (P > 0).sum(1)
print(f"benchmark: {len(P)} puzzles, clues {clues.min()}-{clues.max()} "
f"(mean {clues.mean():.1f})", flush=True)
Pt, St = torch.from_numpy(P), torch.from_numpy(S - 1)
blank = Pt == 0
def load(d):
cfg = json.load(open(os.path.join(d, "result.json")))["args"]
m = Transformer(len(tok), cfg["d"], cfg["layers"], cfg["heads"], cfg["pe"],
causal=False, max_len=CELLS + 8).to(dev)
m.load_state_dict(torch.load(os.path.join(d, "model.pt"), map_location=dev))
m.eval()
return m, cfg
def random_remask_like(bad_counts):
"""Control: remask the same NUMBER of cells, chosen at random."""
def fn(x):
B, L = x.shape
out = torch.zeros_like(x, dtype=torch.bool)
for b in range(B):
k = int(bad_counts[b])
if k:
idx = torch.randperm(L, device=x.device)[:k]
out[b, idx] = True
return out
return fn
@torch.no_grad()
def evaluate(m, fn):
preds, nfes = [], []
for i in range(0, len(Pt), a.eval_bs):
gold = St[i:i + a.eval_bs].to(dev)
bl = blank[i:i + a.eval_bs].to(dev)
x = torch.where(bl, torch.full_like(gold, tok.mask), gold)
y, nfe = fn(x, bl)
preds.append(y.cpu().numpy() + 1)
nfes.append(nfe)
pred = np.concatenate(preds)
board = float((pred == S).all(axis=1).mean())
cell = float((pred[P == 0] == S[P == 0]).mean())
return board, cell, float(np.mean(nfes)), pred
res = {}
dirs = sorted(glob.glob(os.path.join(a.runs, a.pattern)))
dirs = [d for d in dirs if os.path.exists(os.path.join(d, "model.pt"))]
print(f"evaluating {len(dirs)} models\n", flush=True)
for d in dirs:
m, cfg = load(d)
name = os.path.basename(d)
print(f"--- {name}", flush=True)
print(f" {'decoder':<34}{'NFE':>8}{'BOARD':>10}{'cell':>9}", flush=True)
row = {}
for K in (16, 61):
b, c, n, _ = evaluate(m, lambda x, bl, K=K: dfn.fixed_k_decode(
m, x, tok, dev, K, fillable=bl))
row[f"fixedK_{K}"] = {"board": b, "cell": c, "nfe": n}
print(f" {'fixed-K K=' + str(K):<34}{n:>8.1f}{b*100:>9.2f}%{c*100:>8.2f}%",
flush=True)
b, c, n, _ = evaluate(m, lambda x, bl: dfn.entropy_budget_decode(
m, x, tok, dev, 0.01, max_steps=CELLS + 2, fillable=bl))
row["entbudget"] = {"board": b, "cell": c, "nfe": n}
print(f" {'entropy-budget B=0.01':<34}{n:>8.1f}{b*100:>9.2f}%{c*100:>8.2f}%",
flush=True)
# Repair must start from the STRONGEST available decode, not a weak one.
# An earlier version used base_steps=16, which decodes to ~83% before repair,
# so the repaired result never beat plain 61-pass decoding at ~88% - the
# method was handicapped by its starting point, not by the idea.
for base in (32, 61):
for rounds in (4, 8):
b, c, n, _ = evaluate(
m, lambda x, bl, r=rounds, bs=base: dfn.verifier_remask_decode(
m, x, tok, dev, violation_mask, base_steps=bs, rounds=r,
max_revisits=3, fillable=bl, expand_peers=expand_to_units))
row[f"verifier_b{base}_r{rounds}"] = {"board": b, "cell": c, "nfe": n}
print(f" {'VERIFIER base=' + str(base) + ' rounds=' + str(rounds):<34}"
f"{n:>8.1f}{b*100:>9.2f}%{c*100:>8.2f}%", flush=True)
# narrow repair: remask ONLY the conflicting cells, not their whole units
b, c, n, _ = evaluate(m, lambda x, bl: dfn.verifier_remask_decode(
m, x, tok, dev, violation_mask, base_steps=61, rounds=8,
max_revisits=3, fillable=bl, expand_peers=None))
row["verifier_narrow"] = {"board": b, "cell": c, "nfe": n}
print(f" {'VERIFIER base=61 narrow (no units)':<34}{n:>8.1f}{b*100:>9.2f}%"
f"{c*100:>8.2f}%", flush=True)
# control: same budget, but remask at random instead of where it is wrong
b, c, n, _ = evaluate(m, lambda x, bl: dfn.verifier_remask_decode(
m, x, tok, dev,
lambda z: (torch.rand_like(z, dtype=torch.float) < 0.12),
base_steps=61, rounds=8, max_revisits=3, fillable=bl,
expand_peers=None))
row["random_remask_control"] = {"board": b, "cell": c, "nfe": n}
print(f" {'CONTROL: random remask, 8 rounds':<34}{n:>8.1f}{b*100:>9.2f}%"
f"{c*100:>8.2f}%", flush=True)
res[name] = row
del m
torch.cuda.empty_cache()
print(flush=True)
if res:
print("=" * 76)
print("SUMMARY (mean over models)")
print("=" * 76)
keys = list(next(iter(res.values())).keys())
print(f"{'decoder':<34}{'NFE':>8}{'BOARD':>10}")
base = None
for k in keys:
bs = [r[k]["board"] for r in res.values()]
ns = [r[k]["nfe"] for r in res.values()]
if k == "fixedK_61":
base = float(np.mean(bs))
print(f"{k:<34}{np.mean(ns):>8.1f}{np.mean(bs)*100:>9.2f}%")
if base is not None:
best_v = max((np.mean([r[k]["board"] for r in res.values()])
for k in keys if k.startswith("verifier_")), default=0)
ctrl = np.mean([r["random_remask_control"]["board"] for r in res.values()])
print(f"\n best verifier-guided : {best_v*100:.2f}%")
print(f" fixed-K at 61 passes : {base*100:.2f}% "
f"(verifier gains {(best_v-base)*100:+.2f} pts)")
print(f" random-remask control: {ctrl*100:.2f}% "
f"({(ctrl-base)*100:+.2f} pts)")
print("\n If the control gains as much, the verifier is doing nothing and")
print(" the improvement is only the extra compute.")
os.makedirs(os.path.dirname(a.out) or ".", exist_ok=True)
json.dump(res, open(a.out, "w"), indent=2)
print(f"\nwrote {a.out}")