Second-order SCAFFOLD bias compensation / stage2_bench.py

Failed on benchmark

Raw ⬇ ZIP
  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()