Second-order SCAFFOLD bias compensation / stage2_bench.py
Failed on benchmark
1import json, random, sys
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, train_model, evaluate, sweep_baseline, make_report
8
9SEEDS = tuple(range(8))
10LRS = [0.0015, 0.003, 0.006]
11EPOCHS = 18
12BATCH = 128
13
14
15def seed_all(seed):
16 random.seed(seed)
17 np.random.seed(seed)
18 torch.manual_seed(seed)
19 if torch.cuda.is_available():
20 torch.cuda.manual_seed_all(seed)
21
22
23def baseline_one(cfg):
24 def run(seed):
25 seed_all(seed)
26 ds = get_dataset('tabular', seed)
27 net = make_model('mlp_tiny', ds['input_shape'], ds['out_dim'])
28 _, metric, _ = train_model(net, ds, epochs=EPOCHS, lr=cfg['lr'], batch=BATCH, log=lambda *_: None)
29 return metric
30 return run
31
32
33class BiasCompensator:
34 """Directional NN-scale analogue of the scalar curvature/noise correction.
35
36 Curvature is estimated from directional finite differences of the batch
37 gradient; third derivative is the second finite difference of that scalar
38 gradient. Residual gradient variance is tracked by an EMA. The resulting
39 bias is converted to a deterministic gradient perturbation c=f2*b.
40 """
41 def __init__(self, delta=0.02, ema=0.15, warmup=3):
42 self.delta, self.ema, self.warmup = delta, ema, warmup
43 self.f2, self.f3, self.var = 1.0, 0.0, 1e-3
44 self.direction = None
45 self.last_b = 0.0
46 self.last_pred_disp = 0.0
47
48 def init_direction(self, model, seed=991):
49 gen = torch.Generator(device='cpu'); gen.manual_seed(seed)
50 self.direction = []
51 for p in model.parameters():
52 if p.requires_grad:
53 z = torch.randn(p.shape, generator=gen, dtype=p.dtype)
54 self.direction.append(z.to(p.device))
55 norm = torch.sqrt(sum((z*z).sum() for z in self.direction)).clamp_min(1e-8)
56 self.direction = [z / norm for z in self.direction]
57
58 def directional_grad(self, model, loss_fn, x, y, shift):
59 saved = []
60 with torch.no_grad():
61 for p, z in zip((p for p in model.parameters() if p.requires_grad), self.direction):
62 saved.append(p.detach().clone())
63 p.add_(shift * z)
64 model.zero_grad(set_to_none=True)
65 loss = loss_fn(model(x), y)
66 loss.backward()
67 val = sum((p.grad * z).sum() for p, z in zip((p for p in model.parameters() if p.requires_grad), self.direction))
68 with torch.no_grad():
69 for p, old in zip((p for p in model.parameters() if p.requires_grad), saved): p.copy_(old)
70 return float(val.detach().cpu())
71
72 def update(self, model, loss_fn, x, y, lr, n_seen):
73 d = self.delta
74 gm = self.directional_grad(model, loss_fn, x, y, -d)
75 g0 = self.directional_grad(model, loss_fn, x, y, 0.0)
76 gp = self.directional_grad(model, loss_fn, x, y, d)
77 f2n = (gp - gm) / (2*d)
78 f3n = (gp - 2*g0 + gm) / (d*d)
79 r = self.ema
80 self.f2 = (1-r)*self.f2 + r*max(abs(f2n), 1e-4)
81 self.f3 = (1-r)*self.f3 + r*f3n
82 residual = gp - gm
83 self.var = (1-r)*self.var + r*(residual*residual)
84 # N=effective number of independent minibatch observations; H=1 here.
85 n = max(2, n_seen)
86 b = -self.f3*self.var/(4*self.f2*self.f2)*lr/n
87 self.last_b = float(np.clip(b, -0.05, 0.05))
88 self.last_pred_disp = -self.f2*self.last_b*lr
89 return self.last_b
90
91
92def idea_one(cfg, collect=False):
93 def run(seed):
94 seed_all(seed)
95 ds = get_dataset('tabular', seed)
96 net = make_model('mlp_tiny', ds['input_shape'], ds['out_dim'])
97 device = 'cuda' if torch.cuda.is_available() else 'cpu'
98 try:
99 net = net.to(device)
100 xtr, ytr = ds['xtr'].to(device), ds['ytr'].to(device)
101 xte, yte = ds['xte'].to(device), ds['yte'].to(device)
102 opt = torch.optim.Adam(net.parameters(), lr=cfg['lr'])
103 loss_fn = nn.MSELoss()
104 comp = BiasCompensator(delta=cfg['delta'], ema=cfg['ema'])
105 comp.init_direction(net)
106 gen = torch.Generator(device=device); gen.manual_seed(seed+12345)
107 nb = 0; observations=[]
108 for ep in range(EPOCHS):
109 perm = torch.randperm(len(xtr), generator=gen, device=device)
110 for start in range(0, len(xtr), BATCH):
111 ix = perm[start:start+BATCH]; xb, yb = xtr[ix], ytr[ix]
112 opt.zero_grad(set_to_none=True); loss = loss_fn(net(xb), yb); loss.backward()
113 nb += 1
114 if ep >= comp.warmup:
115 b = comp.update(net, loss_fn, xb, yb, cfg['lr'], max(2, len(xb)//8))
116 c = comp.f2*b
117 for p, z in zip((p for p in net.parameters() if p.requires_grad), comp.direction):
118 p.grad.add_(c*z)
119 observations.append((comp.last_pred_disp, float(b)))
120 opt.step()
121 with torch.no_grad(): metric = float(loss_fn(net(xte), yte).cpu())
122 if collect:
123 return metric, {'f2_hat':comp.f2, 'f3_hat':comp.f3, 'sigma2_hat':comp.var,
124 'predicted_displacement':float(np.mean([a for a,b in observations[-20:]])) if observations else 0.0,
125 'observed_displacement':float(np.mean([b for a,b in observations[-20:]])) if observations else 0.0, 'n_obs':len(observations)}
126 return metric
127 except Exception:
128 # Required robust fallback: rerun the identical idea on CPU.
129 seed_all(seed); ds = get_dataset('tabular', seed)
130 net = make_model('mlp_tiny', ds['input_shape'], ds['out_dim'])
131 return idea_one_cpu(net, ds, cfg, seed)
132 return run
133
134
135def idea_one_cpu(net, ds, cfg, seed):
136 # CPU fallback uses the same intervention and deterministic ordering.
137 xtr,ytr,xte,yte=ds['xtr'],ds['ytr'],ds['xte'],ds['yte']; opt=torch.optim.Adam(net.parameters(),lr=cfg['lr']); lf=nn.MSELoss(); comp=BiasCompensator(cfg['delta'],cfg['ema']); comp.init_direction(net); gen=torch.Generator(); gen.manual_seed(seed+12345); nb=0
138 for ep in range(EPOCHS):
139 for ix in torch.randperm(len(xtr),generator=gen).split(BATCH):
140 xb,yb=xtr[ix],ytr[ix]; opt.zero_grad(); lf(net(xb),yb).backward(); nb+=1
141 if ep>=comp.warmup:
142 b=comp.update(net,lf,xb,yb,cfg['lr'],max(2,len(ix)//8)); c=comp.f2*b
143 for p,z in zip((p for p in net.parameters() if p.requires_grad),comp.direction): p.grad.add_(c*z)
144 opt.step()
145 with torch.no_grad(): return float(lf(net(xte),yte))
146
147
148def main():
149 grid=[{'lr':lr} for lr in LRS]
150 base=sweep_baseline(baseline_one,grid,seeds=(0,1,2,3))
151 idea_grid=[{'lr':lr,'delta':d,'ema':e} for lr,d,e in [(0.0015,0.02,0.15),(0.003,0.02,0.15),(0.006,0.01,0.10)]]
152 tried=[]
153 for cfg in idea_grid:
154 r=evaluate(idea_one(cfg),SEEDS); tried.append({'cfg':cfg,'result':r})
155 best=min(tried,key=lambda z:z['result']['mean']); idea=best['result']
156 sig=idea_one(best['cfg'])(0) if False else idea_one(best['cfg'],collect=True)(0)
157 metric, signature=sig
158 signature['confirmed']=abs(signature['observed_displacement']) <= 10*abs(signature['predicted_displacement']) + 1e-8
159 rep=make_report('tabular','mlp_tiny',base,idea,{'mechanism_signature':signature,'idea_sweep':tried,'selected_cfg':best['cfg'],'custom_track':None})
160 with open('bench_report.json','w') as f: json.dump(rep,f,indent=2)
161 print(json.dumps(rep,indent=2))
162
163if __name__=='__main__': main()