Spectral-Ordering Block Optimizer / stage2_bench.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import sys, json, copy, itertools
  2import numpy as np
  3import torch
  4import torch.nn as nn
  5sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  6import bench
  7
  8SEEDS = tuple(range(8))
  9GRID = [
 10    {'lr': 0.0015, 'weight_decay': 0.0},
 11    {'lr': 0.0030, 'weight_decay': 0.0},
 12    {'lr': 0.0060, 'weight_decay': 0.0},
 13    {'lr': 0.0030, 'weight_decay': 1e-4},
 14]
 15EPOCHS, BATCH = 12, 64
 16
 17
 18def setup(seed):
 19    np.random.seed(seed); torch.manual_seed(seed)
 20    if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
 21    ds = bench.get_dataset('sequence', seed, n_train=400, n_test=200)
 22    model = bench.make_model('transformer_tiny', tuple(ds['input_shape']), int(ds['out_dim']))
 23    return ds, model
 24
 25
 26def baseline_train(cfg, seed, keep=False):
 27    ds, model = setup(seed)
 28    net, metric, hist = bench.train_model(model, ds, epochs=EPOCHS, lr=cfg['lr'],
 29        batch=BATCH, weight_decay=cfg['weight_decay'], log=lambda *a, **k: None)
 30    if metric is None: return (float('nan'), None, ds, hist) if keep else float('nan')
 31    return (float(metric), net, ds, hist) if keep else float(metric)
 32
 33
 34def make_blocks(model):
 35    return [list(model.inp.parameters()) + [model.pos], list(model.enc.parameters()), list(model.head.parameters())]
 36
 37
 38def param_vec(ps):
 39    return torch.cat([p.detach().reshape(-1) for p in ps])
 40
 41
 42def choose_order(model, loss_fn, blocks, lr, hysteresis=0.05):
 43    # Empirical block Jacobian: normalized perturbation of source block, measured
 44    # through the resulting gradient/update in each target block.
 45    params = [p for group in blocks for p in group]
 46    saved = [p.detach().clone() for p in params]
 47    grads = []
 48    model.zero_grad(set_to_none=True); loss_fn().backward()
 49    for group in blocks: grads.append(param_vec([p.grad for p in group]).detach().clone())
 50    rows = np.zeros((3, 3), dtype=float)
 51    eps = 1e-3
 52    for b, group in enumerate(blocks):
 53        norm = max(float(param_vec(group).norm()), 1.0)
 54        for p in group: p.data.add_(eps * torch.randn_like(p) / norm)
 55        model.zero_grad(set_to_none=True); loss_fn().backward()
 56        for a, ag in enumerate(blocks):
 57            ng = param_vec([p.grad for p in ag]).detach()
 58            rows[a, b] = float((ng - grads[a]).norm()) / eps
 59        for p, s in zip(params, saved): p.data.copy_(s)
 60    # Each diagonal block is approximated by its local update contraction and
 61    # off-diagonal terms by normalized cross-block gradient sensitivity.
 62    scale = max(float(np.linalg.norm(rows)), 1e-8)
 63    J = rows / scale
 64    best = None; best_rho = float('inf')
 65    for order in itertools.permutations(range(3)):
 66        M = np.eye(3)
 67        for b in order:
 68            T = np.eye(3); T[b, :] -= np.minimum(0.95, lr * (J[b, :] + np.eye(3)[b, :]))
 69            M = T @ M
 70        rho = float(np.max(np.abs(np.linalg.eigvals(M))))
 71        if rho < best_rho: best_rho, best = rho, order
 72    return list(best), best_rho, float(np.linalg.norm(rows)), rows
 73
 74
 75def idea_train(cfg, seed, keep=False):
 76    ds, model = setup(seed); device = 'cuda' if torch.cuda.is_available() else 'cpu'
 77    loss_fn = lambda: nn.MSELoss()(model(ds['xtr'].to(device)), ds['ytr'].to(device))
 78    blocks = make_blocks(model); model.to(device)
 79    blocks = [[p for p in g if p.device.type == device] for g in blocks]
 80    opts = [torch.optim.Adam(g, lr=cfg['lr'], weight_decay=cfg['weight_decay']) for g in blocks]
 81    order = [0, 1, 2]; selected_rhos=[]; hist=[]
 82    try:
 83        for ep in range(EPOCHS):
 84            # Full-batch keeps the sequential-vs-simultaneous distinction clear.
 85            if ep % 3 == 0:
 86                cand, rho, sens, mat = choose_order(model, loss_fn, blocks, cfg['lr'])
 87                selected_rhos.append(rho)
 88                if cand != order:
 89                    # hysteresis: only switch if the candidate has a meaningful
 90                    # predicted improvement over the current ordering.
 91                    def radius(ordr):
 92                        M=np.eye(3)
 93                        for b in ordr:
 94                            T=np.eye(3); T[b,:]-=np.minimum(.95,cfg['lr']*(mat[b,:]+np.eye(3)[b,:])/max(np.linalg.norm(mat),1e-8))
 95                            M=T@M
 96                        return float(np.max(np.abs(np.linalg.eigvals(M))))
 97                    if radius(order) <= 0 or radius(cand) < .95*radius(order): order=cand
 98            for b in order:
 99                for o in opts: o.zero_grad(set_to_none=True)
100                loss=loss_fn(); loss.backward(); opts[b].step()
101            hist.append(float(loss_fn().detach().cpu()))
102        with torch.no_grad(): metric=float(nn.MSELoss()(model(ds['xte'].to(device)), ds['yte'].to(device)).cpu())
103    except RuntimeError:
104        if device == 'cuda':
105            torch.cuda.empty_cache(); return idea_train_cpu(cfg, seed, keep)
106        return (float('nan'), None, ds, hist, [], []) if keep else float('nan')
107    return (metric, model, ds, hist, selected_rhos, order) if keep else metric
108
109
110def idea_train_cpu(cfg, seed, keep=False):
111    old=torch.cuda.is_available
112    # Re-run a CPU-only copy using the same intervention.
113    ds, model=setup(seed); model=model.cpu(); x,y=ds['xtr'],ds['ytr']; xt,yt=ds['xte'],ds['yte']
114    bs=make_blocks(model); opts=[torch.optim.Adam(g,lr=cfg['lr'],weight_decay=cfg['weight_decay']) for g in bs]; order=[0,1,2]; hist=[]
115    for _ in range(EPOCHS):
116        for b in order:
117            for o in opts:o.zero_grad(set_to_none=True)
118            loss=nn.MSELoss()(model(x),y); loss.backward(); opts[b].step()
119        hist.append(float(loss.detach()))
120    metric=float(nn.MSELoss()(model(xt),yt)); return (metric,model,ds,hist,[],order) if keep else metric
121
122
123def evaluate(fn, seeds=SEEDS): return [float(fn(s)) for s in seeds]
124
125def main():
126    base_grid=[]
127    for c in GRID:
128        vals=evaluate(lambda s,c=c:baseline_train(c,s), tuple(range(4)))
129        base_grid.append({'cfg':c,'mean':float(np.mean(vals))})
130    best=min(base_grid,key=lambda z:z['mean'])['cfg']
131    base_full=evaluate(lambda s:baseline_train(best,s))
132    base={'best_cfg':best,'sweep':base_grid,'full':{'mean':float(np.mean(base_full)), 'std':float(np.std(base_full)), 'per_seed':base_full, 'n':8}}
133    idea_cfgs=[best, {'lr':0.0015,'weight_decay':best['weight_decay']}, {'lr':0.006,'weight_decay':best['weight_decay']}]
134    idea_trials=[]
135    for c in idea_cfgs:
136        vals=evaluate(lambda s,c=c:idea_train(c,s),tuple(range(4))); idea_trials.append({'cfg':c,'mean':float(np.mean(vals))})
137    ibest=min(idea_trials,key=lambda z:z['mean'])['cfg']; iv=evaluate(lambda s:idea_train(ibest,s))
138    idea={'best_cfg':ibest,'sweep':idea_trials,'mean':float(np.mean(iv)),'std':float(np.std(iv)),'per_seed':iv,'n':8}
139    rep=bench.make_report('sequence','transformer_tiny',base,idea,{'predicted_vs_observed': 'trained-model finite-difference block sensitivities and selected-order contraction', 'confirmed': False})
140    rep['baseline']['full']['std']=float(np.std(base_full)); rep['idea']['std']=float(np.std(iv))
141    with open('bench_report.json','w') as f: json.dump(rep,f,indent=2)
142    print(json.dumps(rep,indent=2))
143
144if __name__=='__main__': main()