Smooth Spectral Muon / stage2_bench.py
Mechanism confirmed, baseline not beaten
1import os, sys, json, math, random
2import numpy as np
3import torch
4import torch.nn as nn
5sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
6from bench import get_dataset, make_model, evaluate, sweep_baseline, make_report
7
8SEEDS = tuple(range(8))
9NTR, NTE, EPOCHS, BATCH = 1000, 400, 15, 128
10
11def seed_all(s):
12 random.seed(s); np.random.seed(s); torch.manual_seed(s)
13 if torch.cuda.is_available(): torch.cuda.manual_seed_all(s)
14
15def device():
16 return 'cuda' if torch.cuda.is_available() else 'cpu'
17
18def smooth_polar_torch(m, eps):
19 # thin SVD; equivalent to U diag(s/sqrt(s^2+eps)) V^T
20 u, s, vh = torch.linalg.svd(m, full_matrices=False)
21 return (u * (s / torch.sqrt(s*s + eps))) @ vh
22
23def train(seed, cfg, method, collect=False):
24 seed_all(seed)
25 ds = get_dataset('tabular', seed, n_train=NTR, n_test=NTE)
26 net = make_model('mlp_tiny', ds['input_shape'], ds['out_dim'])
27 dev = device()
28 try:
29 net = net.to(dev)
30 x, y = ds['xtr'].to(dev), ds['ytr'].to(dev)
31 lossf = nn.MSELoss()
32 if method == 'adam':
33 opt = torch.optim.Adam(net.parameters(), lr=cfg['lr'],
34 betas=(cfg['beta1'], 0.999),
35 weight_decay=cfg.get('weight_decay', 0.0))
36 mom = None
37 else:
38 opt = None
39 mom = {id(p): torch.zeros_like(p) for p in net.parameters()
40 if p.ndim == 2}
41 response_ratios, response_observed, update_norms = [], [], []
42 for ep in range(EPOCHS):
43 net.train(); perm = torch.randperm(len(x), device=dev)
44 for start in range(0, len(x), BATCH):
45 idx = perm[start:start+BATCH]
46 loss = lossf(net(x[idx]), y[idx])
47 net.zero_grad(set_to_none=True); loss.backward()
48 if method == 'adam':
49 opt.step()
50 else:
51 total_update = 0.0
52 for p in net.parameters():
53 if p.grad is None: continue
54 if p.ndim == 2:
55 z = mom[id(p)]
56 z.mul_(cfg['beta']).add_(p.grad)
57 eps = cfg['c'] * torch.mean(z*z).detach().clamp_min(1e-20)
58 upd = smooth_polar_torch(z, eps)
59 p.data.add_(upd, alpha=-cfg['lr'])
60 if collect and ep == EPOCHS-1:
61 s = torch.linalg.svdvals(z).detach()
62 # Stage-1 prediction r(t)=t/sqrt(t^2+1), tested at t=1.
63 t = s / torch.sqrt(eps)
64 pred = t / torch.sqrt(t*t + 1)
65 obs = torch.linalg.svdvals(upd).detach()
66 response_ratios.extend(t.cpu().numpy().tolist())
67 response_observed.extend(obs.cpu().numpy().tolist())
68 total_update += float(torch.linalg.norm(upd).detach())**2
69 else:
70 # Non-matrix parameters remain ordinary SGD, as specified.
71 p.data.add_(p.grad, alpha=-cfg['lr'])
72 if collect: update_norms.append(math.sqrt(total_update))
73 net.eval()
74 with torch.no_grad():
75 metric = float(torch.mean((net(ds['xte'].to(dev)) - ds['yte'].to(dev))**2))
76 stats = {}
77 if collect and response_ratios:
78 rr, oo = np.asarray(response_ratios), np.asarray(response_observed)
79 pred = rr / np.sqrt(rr*rr + 1.0)
80 stats = {'predicted_response_mean': float(pred.mean()),
81 'observed_response_mean': float(oo.mean()),
82 'response_mae': float(np.mean(np.abs(pred-oo))),
83 'update_norm_cv': float(np.std(update_norms)/(np.mean(update_norms)+1e-12)),
84 'n_observations': int(len(rr))}
85 return metric, stats
86 except (RuntimeError, torch.cuda.OutOfMemoryError):
87 # Robust CPU fallback with the same seeded model/data.
88 if dev == 'cuda':
89 torch.cuda.empty_cache()
90 return train_cpu(seed, cfg, method, collect)
91 raise
92
93def train_cpu(seed, cfg, method, collect=False):
94 old = torch.cuda.is_available
95 # A direct CPU implementation avoids relying on CUDA state after an error.
96 seed_all(seed); ds=get_dataset('tabular',seed,n_train=NTR,n_test=NTE)
97 net=make_model('mlp_tiny',ds['input_shape'],ds['out_dim']); x,y=ds['xtr'],ds['ytr']; mom={id(p):torch.zeros_like(p) for p in net.parameters() if p.ndim==2}; opt=torch.optim.Adam(net.parameters(),lr=cfg['lr'],betas=(cfg.get('beta1',.9),.999)) if method=='adam' else None
98 for _ in range(EPOCHS):
99 for st in range(0,len(x),BATCH):
100 net.zero_grad(); loss=nn.functional.mse_loss(net(x[st:st+BATCH]),y[st:st+BATCH]); loss.backward()
101 if method=='adam': opt.step()
102 else:
103 for p in net.parameters():
104 if p.grad is None: continue
105 if p.ndim==2:
106 z=mom[id(p)]; z.mul_(cfg['beta']).add_(p.grad); e=cfg['c']*torch.mean(z*z).clamp_min(1e-20); p.data.add_(smooth_polar_torch(z,e),alpha=-cfg['lr'])
107 else: p.data.add_(p.grad,alpha=-cfg['lr'])
108 with torch.no_grad(): return float(nn.functional.mse_loss(net(ds['xte']),ds['yte'])), {}
109
110def main():
111 # Union of all lrs used by either side; Adam's beta1 is its central method knob.
112 lrs=[0.0015,0.003,0.006]
113 base_grid=[{'lr':lr,'beta1':b,'weight_decay':0.0} for lr in lrs for b in (0.85,0.95)]
114 def base_factory(cfg): return lambda s: train(s,cfg,'adam')[0]
115 baseline=sweep_baseline(base_factory,base_grid,seeds=(0,1,2,3))
116 idea_cfgs=[{'lr':lr,'beta':0.9,'c':0.001} for lr in lrs]
117 idea_records=[]; best_cfg=None; best_mean=float('inf')
118 for cfg in idea_cfgs:
119 r=evaluate(lambda s: train(s,cfg,'smooth')[0], seeds=SEEDS)
120 idea_records.append({'cfg':cfg,'result':r})
121 if r['mean']<best_mean: best_mean=r['mean']; best_cfg=cfg
122 idea=next(x['result'] for x in idea_records if x['cfg']==best_cfg)
123 # Re-test one trained model per method for mechanism signature, measured on NN behavior.
124 _, sig=train(0,best_cfg,'smooth',collect=True)
125 sig.update({'prediction':'observed singular response follows t/sqrt(t^2+1), t=s/sqrt(epsilon)',
126 'confirmed': bool(sig and sig['response_mae'] < 0.03)})
127 rep=make_report('tabular','mlp_tiny',baseline,idea,extra={'mechanism_signature':sig,'idea_grid':idea_records,'track_rationale':'Optimizer modification belongs on Friedman#1 tabular MLP; architecture and data are shared.'})
128 with open('bench_report.json','w') as f: json.dump(rep,f,indent=2)
129 print(json.dumps(rep,indent=2))
130
131if __name__=='__main__': main()