Trace-Canonical Modular Blocks / stage2_bench.py
Mechanism confirmed, baseline not beaten
1import os, sys, json, random
2import numpy as np
3import torch
4import torch.nn as nn
5
6sys.path.insert(0, "/home/maxwelhelp/all/math2nn")
7from bench import get_dataset, train_model, sweep_baseline, make_report
8
9SEED0 = 597
10LR_GRID = [1e-3, 3e-3, 1e-2]
11EPOCHS = 12
12BATCH = 128
13
14
15def canonical(word, rank, supports):
16 a = list(word)
17 changed = True
18 while changed:
19 changed = False
20 for i in range(len(a)-1):
21 x, y = a[i], a[i+1]
22 commute = supports[x].isdisjoint(supports[y])
23 if commute and rank[x] > rank[y]:
24 a[i], a[i+1] = y, x
25 changed = True
26 return tuple(a)
27
28
29class ModularSequence(nn.Module):
30 """Tiny sequence predictor with trace-canonical modular sub-block."""
31 def __init__(self, word):
32 super().__init__()
33 self.width = 32
34 self.inp = nn.Linear(1, self.width)
35 self.pos = nn.Parameter(torch.zeros(1, 32, self.width))
36 # Operators have disjoint read/write supports for g0..g3.
37 self.ops = nn.ModuleDict()
38 self.supports = {}
39 for g in range(4):
40 name = f"g{g}"
41 self.ops[name] = nn.Sequential(nn.Linear(8, 8), nn.GELU())
42 self.supports[name] = frozenset(range(8*g, 8*(g+1)))
43 self.ops["global"] = nn.Sequential(nn.Linear(32, 32), nn.GELU())
44 self.supports["global"] = frozenset(range(32))
45 self.order = tuple(word)
46 self.rank = {n:i for i,n in enumerate(("g0","g1","g2","g3","global"))}
47 self.head = nn.Sequential(nn.LayerNorm(32), nn.Linear(32*32, 1))
48
49 def apply_op(self, h, name):
50 if name == "global":
51 return self.ops[name](h)
52 idx = sorted(self.supports[name])
53 z = self.ops[name](h[..., idx])
54 out = h.clone()
55 out[..., idx] = z
56 return out
57
58 def forward_with_order(self, x, word):
59 h = self.inp(x.unsqueeze(-1)) + self.pos[:, :x.shape[1]]
60 for name in word:
61 h = self.apply_op(h, name)
62 h = self.head[0](h)
63 return self.head[1](h.reshape(h.shape[0], -1))
64
65 def forward(self, x):
66 return self.forward_with_order(x, self.order)
67
68
69def make_net(seed, idea):
70 torch.manual_seed(seed)
71 # Deliberately noncanonical proposal; only disjoint group swaps are legal.
72 proposed = ("g3", "g1", "global", "g0", "g2")
73 word = canonical(proposed, {n:i for i,n in enumerate(("g0","g1","g2","g3","global"))},
74 {**{f"g{i}": frozenset(range(8*i,8*(i+1))) for i in range(4)}, "global":frozenset(range(32))}) if idea else proposed
75 return ModularSequence(word)
76
77
78def train_one(seed, lr, idea, return_model=False):
79 # Identical task, model family, optimizer, epochs, and batch size.
80 torch.manual_seed(seed); np.random.seed(seed); random.seed(seed)
81 d = get_dataset("sequence", seed, n_train=4000, n_test=1000)
82 net = make_net(seed, idea)
83 trained, metric, hist = train_model(net, d, epochs=EPOCHS, lr=lr, batch=BATCH, log=lambda *_: None)
84 if trained is None:
85 raise RuntimeError("bench training failed")
86 return (float(metric), trained, d) if return_model else float(metric)
87
88
89def signature():
90 vals_comm, vals_non = [], []
91 for seed in range(8):
92 _, net, d = train_one(seed, 3e-3, True, True)
93 x = d["xte"][:64].to(next(net.parameters()).device)
94 prop = net.order
95 can = canonical(prop, net.rank, net.supports)
96 with torch.no_grad():
97 vals_comm.append(float((net.forward_with_order(x, prop)-net.forward_with_order(x, can)).abs().max()))
98 # Swap a group operator across global: this is not support-disjoint.
99 bad = ("g3", "g1", "g0", "global", "g2")
100 vals_non.append(float((net.forward_with_order(x, prop)-net.forward_with_order(x, bad)).abs().max()))
101 mc, mn = max(vals_comm), float(np.median(vals_non))
102 return {"prediction": "disjoint-support swaps preserve the trained network output; group/global swaps do not",
103 "predicted_commuting_max": 1e-5, "observed_commuting_max": mc,
104 "predicted_noncommuting_positive": True, "observed_noncommuting_median": mn,
105 "confirmed": bool(mc <= 1e-5 and mn > 1e-4), "n_trained_models": 8}
106
107
108def main():
109 # Baseline sweep includes every idea-side learning rate (search-space parity).
110 grid = [{"lr": lr, "epochs": EPOCHS, "batch": BATCH} for lr in LR_GRID]
111 def factory(cfg):
112 return lambda seed: train_one(seed, cfg["lr"], False)
113 base = sweep_baseline(factory, grid)
114 # Idea sweep: best baseline lr plus two nearby/equal-budget settings.
115 idea_runs = []
116 for lr in LR_GRID:
117 r = {"lr": lr, "epochs": EPOCHS, "batch": BATCH,
118 "result": {"per_seed": [train_one(s, lr, True) for s in range(8)]}}
119 r["result"]["mean"] = float(np.mean(r["result"]["per_seed"]))
120 r["result"]["std"] = float(np.std(r["result"]["per_seed"]))
121 r["result"]["n"] = 8
122 idea_runs.append(r)
123 best = min(idea_runs, key=lambda z:z["result"]["mean"])
124 report = make_report("sequence", "modular_sequence_local", base, best["result"],
125 {"mechanism_signature": signature(),
126 "track_justification": "sequence is structurally matched: multi-token window correlations and a sequence-level model",
127 "idea_sweep": [{"cfg": {k:v for k,v in r.items() if k != "result"}, "mean": r["result"]["mean"]} for r in idea_runs],
128 "canonical_proposal": ["g3","g1","global","g0","g2"],
129 "canonical_word": list(canonical(("g3","g1","global","g0","g2"), {n:i for i,n in enumerate(("g0","g1","g2","g3","global"))}, {**{f"g{i}":frozenset(range(8*i,8*(i+1))) for i in range(4)},"global":frozenset(range(32))}))})
130 with open("bench_report.json", "w") as f: json.dump(report, f, indent=2)
131 print(json.dumps(report, indent=2))
132
133if __name__ == "__main__": main()