Vanishing-Perturbation SAM / bench_experiment.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import sys, json, 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, train_model, sweep_baseline, evaluate, make_report
  7
  8SEEDS = tuple(range(8))
  9EPOCHS, BATCH, WD = 15, 128, 0.0
 10# The union of all learning rates is evaluated by both methods.
 11LRS = [0.0015, 0.003, 0.006]
 12RHO_GRID = [0.03, 0.05, 0.10]
 13BASE_GRID = [{'lr': lr, 'rho': rho} for lr in LRS for rho in RHO_GRID]
 14# Three idea settings: baseline-best lr plus two nearby union-grid lrs.
 15IDEA_GRID = [{'lr': lr, 'rho': 0.05, 'tau': tau, 'alpha': alpha}
 16             for lr, tau, alpha in [(0.0015, 0.5, 1.0),
 17                                    (0.003, 0.5, 1.0),
 18                                    (0.006, 0.5, 1.0)]]
 19
 20def seed_all(seed):
 21    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 22    if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
 23
 24def dataset(seed):
 25    return get_dataset('tabular', seed, n_train=4000, n_test=1000)
 26
 27def baseline_factory(cfg):
 28    def train(seed):
 29        seed_all(190800 + seed)
 30        ds = dataset(seed)
 31        model = make_model('mlp_tiny', ds['input_shape'], ds['out_dim'])
 32        _, metric, history = train_model(model, ds, epochs=EPOCHS,
 33                                         lr=cfg['lr'], batch=BATCH,
 34                                         weight_decay=WD, log=lambda *_: None)
 35        return float(metric)
 36    return train
 37
 38def sam_factory(cfg):
 39    def train(seed, collect=False):
 40        seed_all(190800 + seed)
 41        ds = dataset(seed)
 42        # Use the same robust device ladder policy as bench.train_model.
 43        ladder = [('cuda', False), ('cuda', True), ('cpu', False)] if torch.cuda.is_available() else [('cpu', False)]
 44        for device, no_cudnn in ladder:
 45            try:
 46                if no_cudnn: torch.backends.cudnn.enabled = False
 47                net = make_model('mlp_tiny', ds['input_shape'], ds['out_dim']).to(device)
 48                x, y = ds['xtr'].to(device), ds['ytr'].to(device)
 49                lossf = nn.MSELoss()
 50                opt = torch.optim.Adam(net.parameters(), lr=cfg['lr'], weight_decay=WD)
 51                observed, predicted, norms = [], [], []
 52                for _ in range(EPOCHS):
 53                    net.train(); order = torch.randperm(len(x), device=device)
 54                    for start in range(0, len(x), BATCH):
 55                        xb, yb = x[order[start:start+BATCH]], y[order[start:start+BATCH]]
 56                        opt.zero_grad(set_to_none=True)
 57                        lossf(net(xb), yb).backward()
 58                        first = [p.grad.detach().clone() if p.grad is not None else None
 59                                 for p in net.parameters()]
 60                        norm = torch.sqrt(sum((g*g).sum() for g in first if g is not None))
 61                        ne = norm + 1e-12
 62                        radius = min(cfg['rho'], cfg['tau'] * float(ne ** cfg['alpha']))
 63                        scale = radius / float(ne ** cfg['alpha'])
 64                        deltas = []
 65                        with torch.no_grad():
 66                            for p, g in zip(net.parameters(), first):
 67                                d = scale * g if g is not None else None
 68                                deltas.append(d)
 69                                if d is not None: p.add_(d)
 70                        # Second forward/backward is the SAM update gradient.
 71                        opt.zero_grad(set_to_none=True)
 72                        lossf(net(xb), yb).backward()
 73                        with torch.no_grad():
 74                            for p, d in zip(net.parameters(), deltas):
 75                                if d is not None: p.sub_(d)
 76                        opt.step()
 77                        disp = torch.sqrt(sum((d*d).sum() for d in deltas if d is not None))
 78                        observed.append(float(disp / ne))
 79                        predicted.append(float(cfg['tau']))
 80                        norms.append(float(norm))
 81                net.eval()
 82                with torch.no_grad():
 83                    metric = float(((net(ds['xte'].to(device)) - ds['yte'].to(device)) ** 2).mean())
 84                if no_cudnn: torch.backends.cudnn.enabled = True
 85                result = {'metric': metric}
 86                if collect:
 87                    result.update({'observed_disp_over_grad': float(np.mean(observed)),
 88                                   'predicted_bound': float(cfg['tau']),
 89                                   'fraction_clipped': float(np.mean(np.asarray(norms) < (cfg['rho']/cfg['tau']) ** (1/cfg['alpha'])))})
 90                return result
 91            except RuntimeError:
 92                if no_cudnn: torch.backends.cudnn.enabled = True
 93        raise RuntimeError('SAM training failed on CUDA and CPU')
 94    return train
 95
 96def main():
 97    # Baseline sweep has the complete lr union and sweeps its method radius knob.
 98    base = sweep_baseline(baseline_factory, BASE_GRID, seeds=SEEDS)
 99    best_cfg = base['best_cfg']
100    # Explicitly ensure the idea settings include baseline-best lr and nearby values.
101    idea_runs = []
102    for cfg in IDEA_GRID:
103        vals = [sam_factory(cfg)(s)['metric'] for s in SEEDS]
104        idea_runs.append({'cfg': cfg, 'mean': float(np.mean(vals)), 'per_seed': vals})
105    chosen = min(idea_runs, key=lambda z: z['mean'])
106    idea_cfg = chosen['cfg']
107    idea_full = evaluate(lambda s: sam_factory(idea_cfg)(s)['metric'], SEEDS)
108    # Re-test signature on trained models, not an analytical toy.
109    sig = [sam_factory(idea_cfg)(s, collect=True) for s in SEEDS]
110    observed = float(np.mean([r['observed_disp_over_grad'] for r in sig]))
111    predicted = float(idea_cfg['tau'])
112    signature = {
113        'predicted_max_displacement_over_gradient': predicted,
114        'observed_mean_displacement_over_gradient': observed,
115        'relative_error_to_bound': abs(observed-predicted) / predicted,
116        'observed_fraction_clipped': float(np.mean([r['fraction_clipped'] for r in sig])),
117        'confirmed': bool(observed <= predicted * 1.05)
118    }
119    report = make_report('tabular', 'mlp_tiny', base,
120                         {'config': idea_cfg, **idea_full},
121                         {'mechanism_signature': signature,
122                          'track_justification': 'Optimizer intervention; tabular Friedman#1 is the mandated optimizer track.',
123                          'idea_sweep': idea_runs,
124                          'budget': {'epochs': EPOCHS, 'batch': BATCH, 'seeds': list(SEEDS)}})
125    with open('bench_report.json', 'w') as f: json.dump(report, f, indent=2)
126    print(json.dumps(report, indent=2))
127
128if __name__ == '__main__': main()