Download src/train_graph.py from GOVINDFROM/parallel-consistent-masked-diffusion: direct link, hf CLI and curl.
- Browser
- Download file 7.28 kB
-
https://huggingface.co/GOVINDFROM/parallel-consistent-masked-diffusion/resolve/main/src/train_graph.py
- Command line
-
hf download hf://GOVINDFROM/parallel-consistent-masked-diffusion/src/train_graph.py
-
curl -L -o train_graph.py https://huggingface.co/GOVINDFROM/parallel-consistent-masked-diffusion/resolve/main/src/train_graph.py
7.28 kB
| """Masked diffusion on structured graphs, where the ceiling is known exactly. | |
| This is the experiment the project could not run before. On every earlier task | |
| the best achievable parallel-decoding accuracy was unknown, so a gap could | |
| always be blamed on undertraining. Here it is computable in closed form: | |
| V* = sum over valid graphs of prod_i p_i(g_i) and V* >= 2^(-TC), | |
| with equality when the family has a fixed edge count. V* is the probability | |
| that independent draws from the TRUE marginals land on a valid graph, so it is | |
| the one-pass ceiling for any model whatsoever - it already assumes the marginals | |
| are perfect. | |
| That makes the logic airtight in a way it was not on addition or text: | |
| * if a trained model reaches V* at one pass, the remaining gap to 100% is | |
| PROVABLY information-theoretic - mode (ii) - and no training objective can | |
| recover it; | |
| * if it falls short of V*, the shortfall is exactly mode (i), the trainable | |
| part, and budget conditioning should close it. | |
| The two failure modes are therefore separated numerically rather than argued | |
| about, on the same task, at the same time. | |
| A graph on n nodes is a binary sequence of n(n-1)/2 edge indicators, so the | |
| masked-diffusion machinery is used unchanged - only the data and the validity | |
| check are new. | |
| """ | |
| import argparse, json, math, os, time | |
| import numpy as np | |
| import torch | |
| from model import Transformer | |
| import diffusion as dfn | |
| from graphs import (EdgeTokenizer, exact_stats, is_valid, n_edges, sample) | |
| def get_args(): | |
| p = argparse.ArgumentParser() | |
| p.add_argument("--family", default="matching", | |
| choices=["matching", "2regular", "tree", "trianglefree", | |
| "bipartite"]) | |
| p.add_argument("--n", type=int, default=6) | |
| p.add_argument("--mode", choices=["mdlm", "anybudget", "anybudget_nc"], | |
| default="mdlm") | |
| p.add_argument("--budget_bins", type=int, default=8) | |
| p.add_argument("--pe", default="rope", choices=["rope", "ape", "sin", "alibi", "nope"], | |
| help="position encoding; decisive at the fully-masked state") | |
| p.add_argument("--d", type=int, default=256) | |
| p.add_argument("--layers", type=int, default=6) | |
| p.add_argument("--heads", type=int, default=8) | |
| p.add_argument("--bs", type=int, default=256) | |
| p.add_argument("--steps", type=int, default=30000) | |
| p.add_argument("--lr", type=float, default=1e-4) | |
| p.add_argument("--warmup", type=int, default=1000) | |
| p.add_argument("--seed", type=int, default=0) | |
| p.add_argument("--n_gen", type=int, default=4096) | |
| p.add_argument("--gen_bs", type=int, default=512) | |
| p.add_argument("--out", default="runs/gdev") | |
| return p.parse_args() | |
| a = get_args() | |
| torch.manual_seed(a.seed) | |
| np.random.seed(a.seed) | |
| dev = "cuda" if torch.cuda.is_available() else "cpu" | |
| os.makedirs(a.out, exist_ok=True) | |
| E = n_edges(a.n) | |
| tok = EdgeTokenizer() | |
| stats = exact_stats(a.family, a.n) | |
| print(f"family={a.family} n={a.n} edges={E} |family|={stats['size']}", flush=True) | |
| print(f" sum_i H(e_i) = {stats['sum_marginal_entropy_bits']:.3f} bits (the bound)", flush=True) | |
| print(f" H(S) = {stats['joint_entropy_bits']:.3f} bits", flush=True) | |
| print(f" TC = {stats['total_correlation_bits']:.3f} bits", flush=True) | |
| print(f" V* ceiling = {stats['one_pass_ceiling']*100:.4f}% " | |
| f"(2^-TC = {2**-stats['total_correlation_bits']*100:.4f}%)", flush=True) | |
| X = torch.from_numpy(sample(a.family, a.n, 200_000, seed=a.seed)) | |
| COND = a.mode == "anybudget" | |
| model = Transformer(len(tok), a.d, a.layers, a.heads, a.pe, causal=False, | |
| max_len=E + 8, budget_bins=a.budget_bins if COND else 0).to(dev) | |
| print(f"params {model.n_params()/1e6:.2f}M mode={a.mode} pe={a.pe}", flush=True) | |
| opt = torch.optim.AdamW(model.parameters(), lr=a.lr, weight_decay=0.01) | |
| sched = torch.optim.lr_scheduler.LambdaLR( | |
| opt, lambda s: min(1.0, (s + 1) / a.warmup) * | |
| 0.5 * (1 + math.cos(math.pi * min(1.0, s / a.steps)))) | |
| t0 = time.time() | |
| for step in range(a.steps): | |
| idx = torch.randint(0, len(X), (a.bs,)) | |
| xb = X[idx].to(dev) | |
| opt.zero_grad(set_to_none=True) | |
| with torch.autocast("cuda", dtype=torch.bfloat16, enabled=(dev == "cuda")): | |
| if a.mode in ("anybudget", "anybudget_nc"): | |
| loss = dfn.uncond_any_budget_loss( | |
| model, xb, tok, choices=tuple(2 ** i for i in range(int(math.log2(E)) + 1)), | |
| condition=COND) | |
| else: | |
| loss = dfn.uncond_mdlm_loss(model, xb, tok, "uniform") | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) | |
| opt.step(); sched.step() | |
| if step % 5000 == 0: | |
| print(f"step {step:6d} loss {loss.item():.4f} ({time.time()-t0:.0f}s)", flush=True) | |
| model.eval() | |
| res = {"args": vars(a), "exact": {k: v for k, v in stats.items() if k != "marginals"}} | |
| def gen(fn): | |
| outs, nfes = [], [] | |
| for _ in range(max(1, a.n_gen // a.gen_bs)): | |
| blank = torch.full((a.gen_bs, E), tok.mask, dtype=torch.long) | |
| x, nfe = fn(blank) | |
| outs.append(x.cpu()); nfes.append(nfe) | |
| x = torch.cat(outs).numpy() | |
| ok = is_valid(x, a.family, a.n) | |
| uniq = len({tuple(r) for r in x[ok]}) / max(1, int(ok.sum())) | |
| return float(ok.mean()), float(np.mean(nfes)), float(uniq) | |
| def bud(K): | |
| return torch.full((a.gen_bs,), float(K), device=dev) if COND else None | |
| print("\n=== validity against decoding budget ===", flush=True) | |
| print(f"{'rule':<22}{'NFE':>7}{'validity':>11}{'vs V*':>10}{'uniq':>8}", flush=True) | |
| res["decode"] = {} | |
| V = stats["one_pass_ceiling"] | |
| for K in sorted({1, 2, 4, 8, E}): | |
| v, nfe, u = gen(lambda b, K=K: dfn.fixed_k_decode(model, b, tok, dev, K, | |
| budget_cond=bud(K))) | |
| res["decode"][f"fixedK_{K}"] = {"validity": v, "nfe": nfe, "unique": u} | |
| rel = f"{v/V:8.2f}x" if K == 1 else f"{'—':>9}" | |
| print(f"{'fixed-K K=' + str(K):<22}{nfe:>7.1f}{v*100:>10.2f}%{rel}{u*100:>7.1f}%", flush=True) | |
| for B in (0.05, 0.25, 1.0, 4.0): | |
| v, nfe, u = gen(lambda b, B=B: dfn.entropy_budget_decode(model, b, tok, dev, B, | |
| max_steps=E + 2, | |
| budget_cond=bud(8))) | |
| res["decode"][f"entbudget_{B}"] = {"validity": v, "nfe": nfe, "unique": u} | |
| print(f"{'entropy-budget B=' + str(B):<22}{nfe:>7.1f}{v*100:>10.2f}%{'—':>9}" | |
| f"{u*100:>7.1f}%", flush=True) | |
| one = res["decode"]["fixedK_1"]["validity"] | |
| res["reached_ceiling"] = one >= 0.9 * V | |
| print(f"\nV* (exact one-pass ceiling) : {V*100:.4f}%", flush=True) | |
| print(f"achieved at one pass : {one*100:.4f}% " | |
| f"({one/V*100:.1f}% of the ceiling)", flush=True) | |
| print("reading: " + ("the model is at the information-theoretic limit, so the " | |
| "residual gap is mode (ii) and NOT trainable" | |
| if res["reached_ceiling"] else | |
| "the model falls short of the ceiling, so part of the gap " | |
| "is mode (i) and IS trainable"), flush=True) | |
| torch.save(model.state_dict(), os.path.join(a.out, "model.pt")) | |
| json.dump(res, open(os.path.join(a.out, "result.json"), "w"), indent=2) | |
| print(f"\nsaved to {a.out}", flush=True) | |