Symmetry-Resolved Fourier Bifurcation Monitor / bench_stage2.py
Failed on benchmark
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, make_model, evaluate, sweep_baseline, make_report
8
9SEEDS = tuple(range(8))
10SWEEP_SEEDS = tuple(range(4))
11EPOCHS = 5
12BATCH = 128
13# The union is deliberately shared: every lr tested by the idea is in baseline.
14LRS = [1e-3, 3e-3, 1e-2]
15CLIPS = [0.5, 1.0, 5.0]
16IDEA_GRID = [{"lr": x, "margin0": 0.20} for x in LRS]
17BASE_GRID = [{"lr": x, "clip": c} for x in LRS for c in CLIPS]
18
19idea_signatures = {}
20
21
22def seed_all(seed):
23 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
24 if torch.cuda.is_available():
25 torch.cuda.manual_seed_all(seed)
26
27
28def device_for():
29 return "cuda" if torch.cuda.is_available() else "cpu"
30
31
32def proxy_sectors(net):
33 """Fourier-sector proxy of GRU recurrent Jacobian, one scalar per channel mode.
34 The three gate matrices are averaged; this is a diagnostic, not an identity.
35 """
36 H = net.rnn.hidden_size
37 W = net.rnn.weight_hh_l0.detach().float().cpu().numpy().reshape(3, H, H)
38 U = np.exp(2j*np.pi*np.outer(np.arange(H), np.arange(H))/H)/np.sqrt(H)
39 blocks = np.stack([np.diag(U.conj().T @ w @ U) for w in W])
40 lam = blocks.mean(axis=0)
41 margins = np.abs(1.0 - lam)
42 return lam, margins
43
44
45def damp_fourier_grad(net, margin0):
46 """Apply sector-specific damping to GRU recurrent gradients.
47 Each gate gradient is transformed to the cyclic Fourier basis and attenuated
48 according to the measured distance of that sector's recurrent proxy from 1.
49 """
50 H = net.rnn.hidden_size
51 with torch.no_grad():
52 W = net.rnn.weight_hh_l0.detach().float().cpu().numpy().reshape(3, H, H)
53 U = np.exp(2j*np.pi*np.outer(np.arange(H), np.arange(H))/H)/np.sqrt(H)
54 blocks = np.stack([np.diag(U.conj().T @ w @ U) for w in W])
55 margins = np.abs(1.0 - blocks.mean(axis=0))
56 scales = np.clip(margins / margin0, 0.10, 1.0)
57 # Blend in Fourier coordinates: row/column sector correspondence.
58 g = net.rnn.weight_hh_l0.grad
59 if g is None: return
60 ga = g.detach().float().cpu().numpy().reshape(3, H, H)
61 out = np.empty_like(ga)
62 for q in range(3):
63 F = U.conj().T @ ga[q] @ U
64 F *= np.sqrt(scales[:, None] * scales[None, :])
65 out[q] = (U @ F @ U.conj().T).real
66 net.rnn.weight_hh_l0.grad.copy_(torch.as_tensor(out.reshape(3*H, H), device=g.device, dtype=g.dtype))
67
68
69def train_one(seed, mode, cfg, return_net=False):
70 seed_all(seed)
71 ds = get_dataset("dynamics", seed, n_train=400, n_test=200)
72 net = make_model("rnn_small", ds["input_shape"], ds["out_dim"])
73 dev = device_for()
74 try:
75 net = net.to(dev)
76 opt = torch.optim.Adam(net.parameters(), lr=cfg["lr"])
77 lossf = nn.MSELoss()
78 xtr, ytr = ds["xtr"].to(dev), ds["ytr"].to(dev)
79 for _ in range(EPOCHS):
80 net.train(); perm = torch.randperm(len(xtr), device=dev)
81 for i in range(0, len(xtr), BATCH):
82 ix = perm[i:i+BATCH]
83 loss = lossf(net(xtr[ix]), ytr[ix])
84 opt.zero_grad(); loss.backward()
85 if mode == "baseline":
86 torch.nn.utils.clip_grad_norm_(net.parameters(), cfg["clip"])
87 else:
88 damp_fourier_grad(net, cfg["margin0"])
89 torch.nn.utils.clip_grad_norm_(net.parameters(), 5.0)
90 opt.step()
91 net.eval()
92 with torch.no_grad():
93 pred = net(ds["xte"].to(dev))
94 metric = float(((pred - ds["yte"].to(dev))**2).mean().cpu())
95 if return_net:
96 return metric, net, ds
97 return metric
98 except RuntimeError:
99 # Explicit CPU fallback for shared/unstable CUDA conditions.
100 if dev == "cuda":
101 torch.cuda.empty_cache()
102 return train_one_cpu(seed, mode, cfg)
103 raise
104
105
106def train_one_cpu(seed, mode, cfg):
107 # Re-execute identically on CPU, avoiding recursive fallback.
108 seed_all(seed)
109 ds = get_dataset("dynamics", seed, n_train=400, n_test=200)
110 net = make_model("rnn_small", ds["input_shape"], ds["out_dim"])
111 opt = torch.optim.Adam(net.parameters(), lr=cfg["lr"]); lossf = nn.MSELoss()
112 for _ in range(EPOCHS):
113 perm = torch.randperm(len(ds["xtr"]));
114 for i in range(0, len(perm), BATCH):
115 ix=perm[i:i+BATCH]; loss=lossf(net(ds["xtr"][ix]),ds["ytr"][ix])
116 opt.zero_grad(); loss.backward()
117 if mode == "baseline": torch.nn.utils.clip_grad_norm_(net.parameters(), cfg["clip"])
118 else: damp_fourier_grad(net, cfg["margin0"])
119 opt.step()
120 with torch.no_grad(): metric=float(((net(ds["xte"])-ds["yte"])**2).mean())
121 return metric
122
123
124def base_factory(cfg):
125 return lambda seed: train_one(seed, "baseline", cfg)
126
127def idea_factory(cfg):
128 return lambda seed: train_one(seed, "idea", cfg)
129
130
131def signature(idea_cfg):
132 rows=[]
133 for s in SEEDS:
134 metric, net, ds = train_one(s, "idea", idea_cfg, return_net=True)
135 lam, margins = proxy_sectors(net)
136 predicted = int(np.argmin(margins))
137 with torch.no_grad():
138 # Diagnostic rollout is deliberately CPU/cuDNN-free because the shared GPU
139 # may reject a second GRU allocation even after successful training.
140 net_cpu = net.cpu()
141 seq=ds["xte"][:128].view(-1, ds["xte"].shape[1]//3, 3)
142 old_cudnn = torch.backends.cudnn.enabled
143 torch.backends.cudnn.enabled = False
144 try:
145 out,h=net_cpu.rnn(seq)
146 finally:
147 torch.backends.cudnn.enabled = old_cudnn
148 a=np.abs(np.fft.fft(out.detach().numpy(), axis=2)).mean(axis=(0,1))
149 observed=int(np.argmax(a))
150 rows.append({"seed":s,"metric":metric,"predicted_sector":predicted,"observed_sector":observed,"predicted_margin":float(margins[predicted])})
151 agree=sum(r["predicted_sector"]==r["observed_sector"] for r in rows)
152 return {"prediction":"smallest Fourier recurrent margin predicts dominant hidden-channel Fourier sector", "predicted_vs_observed":rows, "agreement_fraction":agree/len(rows), "confirmed":bool(agree/len(rows)>=0.5)}
153
154
155def main():
156 torch.set_num_threads(4)
157 base = sweep_baseline(base_factory, BASE_GRID, seeds=SWEEP_SEEDS)
158 # Idea is evaluated at baseline lr plus two nearby settings; the union is in BASE_GRID.
159 idea_tried=[]
160 for cfg in IDEA_GRID:
161 r=evaluate(idea_factory(cfg), seeds=SEEDS)
162 idea_tried.append({"cfg":cfg,"full":r})
163 best=min(idea_tried,key=lambda z:z["full"]["mean"])
164 sig=signature(best["cfg"])
165 report=make_report("dynamics","rnn_small",base,best["full"],extra=sig)
166 report["idea_sweep"]=idea_tried
167 report["track_justification"]="Dynamics is structurally matched: the task is an actuated pendulum rollout and the intervention monitors recurrent stability sectors."
168 report["protocol"]={"paired_seeds":list(SEEDS),"baseline_sweep_seeds":list(SWEEP_SEEDS),"epochs":EPOCHS,"batch":BATCH,"baseline_grid":BASE_GRID,"idea_grid":IDEA_GRID}
169 with open("bench_report.json","w") as f: json.dump(report,f,indent=2)
170 print(json.dumps(report,indent=2))
171
172if __name__ == "__main__": main()