eac123 commited on
Commit
7115984
·
verified ·
1 Parent(s): 72a99b3

architecture validation suite

Browse files
Files changed (1) hide show
  1. validate.py +147 -0
validate.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Architecture validation before any real training.
2
+
3
+ Checks:
4
+ 1. Dataset integrity: answer marginals ~uniform per k (no majority-class
5
+ shortcut); LookupSolver scores 100% (pipeline exactness).
6
+ 2. Gradient flow through recurrence: a 1-block model unrolled R=8 gets
7
+ non-zero gradients on every block parameter; loss at init is finite.
8
+ 3. Overfitting a tiny fixed training set: both architectures reach ~100%
9
+ train accuracy on 256 fixed examples (gradients + capacity are real).
10
+ 4. Interface checks: n_iters variation works, smaller state spaces evaluate
11
+ with the same weights (subset of vocab/positions), shapes correct.
12
+
13
+ Exit code 0 iff all checks pass. Run: python validate.py
14
+ """
15
+
16
+ import sys
17
+
18
+ import time
19
+
20
+ import numpy as np
21
+ import torch
22
+
23
+ torch.set_num_threads(8)
24
+ import torch.nn.functional as F
25
+
26
+ from ptr.common import (LookupSolver, check_answer_marginals,
27
+ sample_eval_batch, sample_train_batch)
28
+ from ptr.models import PtrTransformer
29
+
30
+ DEV = "cpu"
31
+ S, D = 64, 128
32
+
33
+
34
+ def check_marginals_and_lookup():
35
+ rng = np.random.default_rng(0)
36
+ m = check_answer_marginals(rng, S=S, k_max=8, n=100_000)
37
+ print(f"[1a] answer-marginal TV per k: "
38
+ f"{ {k: round(v, 4) for k, v in m['tv_per_k'].items()} } -> "
39
+ f"{'PASS' if m['pass'] else 'FAIL'}")
40
+ assert m["pass"]
41
+
42
+ solver = LookupSolver()
43
+ b = sample_eval_batch(rng, 1000, S, k=8)
44
+ pred = solver.solve(b["f"], b["x"], 8)
45
+ acc = float((pred == b["answer"]).mean())
46
+ print(f"[1b] LookupSolver exact-match on random data: {acc:.4f} -> "
47
+ f"{'PASS' if acc == 1.0 else 'FAIL'}")
48
+ assert acc == 1.0
49
+
50
+
51
+ def check_gradients():
52
+ torch.manual_seed(0)
53
+ model = PtrTransformer(S, D, 4, 512, depth=1, zero_init=True).to(DEV)
54
+ rng = np.random.default_rng(1)
55
+ b = sample_train_batch(rng, 64, S)
56
+ f = torch.as_tensor(b["f"], device=DEV)
57
+ x = torch.as_tensor(b["x"], device=DEV)
58
+ k = torch.as_tensor(b["k"], device=DEV)
59
+ y = torch.as_tensor(b["answer"], device=DEV)
60
+ R = 8
61
+ # Zero-initialised output projections make every block the identity at
62
+ # step 0, so upstream parameters receive no gradient on the very first
63
+ # backward. Warm up a few AdamW steps first, then check that the full
64
+ # R=8 unroll propagates gradients to every block parameter.
65
+ opt = torch.optim.AdamW(model.parameters(), lr=1e-3)
66
+ for _ in range(3):
67
+ opt.zero_grad(set_to_none=True)
68
+ F.cross_entropy(model(f, x, k, n_iters=R), y).backward()
69
+ opt.step()
70
+ opt.zero_grad(set_to_none=True)
71
+ logits = model(f, x, k, n_iters=R)
72
+ loss = F.cross_entropy(logits, y)
73
+ loss.backward()
74
+ bad = [n for n, p in model.named_parameters()
75
+ if p.requires_grad and (p.grad is None or p.grad.abs().sum() == 0)]
76
+ print(f"[2] recurrent unroll R={R}: init loss {loss.item():.4f} "
77
+ f"(uniform would be {np.log(S):.4f}); "
78
+ f"params with zero/missing grad: {bad or 'none'} -> "
79
+ f"{'PASS' if not bad and torch.isfinite(loss) else 'FAIL'}")
80
+ assert not bad and torch.isfinite(loss)
81
+
82
+
83
+ def overfit_tiny(depth, n_iters, steps=2500, lr=1e-3, target=0.99):
84
+ torch.manual_seed(depth * 10 + n_iters)
85
+ model = PtrTransformer(S, D, 4, 512, depth=depth, zero_init=True).to(DEV)
86
+ rng = np.random.default_rng(7)
87
+ fixed = sample_train_batch(rng, 256, S, k_max=8)
88
+ f = torch.as_tensor(fixed["f"], device=DEV)
89
+ x = torch.as_tensor(fixed["x"], device=DEV)
90
+ k = torch.as_tensor(fixed["k"], device=DEV)
91
+ y = torch.as_tensor(fixed["answer"], device=DEV)
92
+ opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=0.01)
93
+ t0 = time.time()
94
+ acc = 0.0
95
+ for i in range(steps):
96
+ opt.zero_grad(set_to_none=True)
97
+ logits = model(f, x, k, n_iters=n_iters)
98
+ loss = F.cross_entropy(logits, y)
99
+ loss.backward()
100
+ opt.step()
101
+ if i % 100 == 0 or i == steps - 1:
102
+ acc = (logits.argmax(-1) == y).float().mean().item()
103
+ print(f" depth={depth} R={n_iters} step {i}: "
104
+ f"loss {loss.item():.4f} acc {acc:.3f} "
105
+ f"({time.time()-t0:.0f}s)", flush=True)
106
+ if acc >= target:
107
+ break
108
+ acc = (logits.argmax(-1) == y).float().mean().item()
109
+ ok = acc >= target
110
+ print(f"[3] overfit tiny set (depth={depth}, R={n_iters}): "
111
+ f"final acc {acc:.3f} -> {'PASS' if ok else 'FAIL'}")
112
+ assert ok
113
+
114
+
115
+ def check_interface():
116
+ torch.manual_seed(3)
117
+ model = PtrTransformer(S, D, 4, 512, depth=4, zero_init=True).to(DEV)
118
+ rng = np.random.default_rng(11)
119
+ for n_iters in (1, 4, 12):
120
+ b = sample_eval_batch(rng, 8, S, k=16)
121
+ f = torch.as_tensor(b["f"], device=DEV)
122
+ x = torch.as_tensor(b["x"], device=DEV)
123
+ k = torch.as_tensor(b["k"], device=DEV)
124
+ logits = model(f, x, k, n_iters=n_iters)
125
+ assert logits.shape == (8, S)
126
+ # smaller state space with the same weights (subset of vocab + positions)
127
+ b = sample_eval_batch(rng, 8, 16, k=64)
128
+ logits = model(torch.as_tensor(b["f"], device=DEV),
129
+ torch.as_tensor(b["x"], device=DEV),
130
+ torch.as_tensor(b["k"], device=DEV), n_iters=4)
131
+ # head is sized to the trained S=64; a smaller eval state space S'=16
132
+ # yields 64 logits and must be MASKED to the first S' classes
133
+ assert logits.shape == (8, 64)
134
+ assert logits[:, :16].argmax(-1).shape == (8,)
135
+ print("[4] interface: n_iters 1/4/12 and S=16-subset eval (with head "
136
+ f"masking) all OK (logits {tuple(logits.shape)}) -> PASS")
137
+
138
+
139
+ if __name__ == "__main__":
140
+ check_marginals_and_lookup()
141
+ check_gradients()
142
+ overfit_tiny(depth=4, n_iters=1)
143
+ overfit_tiny(depth=1, n_iters=4)
144
+ overfit_tiny(depth=1, n_iters=8)
145
+ check_interface()
146
+ print("ALL VALIDATION CHECKS PASSED")
147
+ sys.exit(0)