Discounted Saddle-Gap Controller / stage2_bench.py

Failed on benchmark

Raw ⬇ ZIP
  1import sys, json, random
  2import numpy as np
  3import torch
  4from torch import nn
  5sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  6from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report
  7
  8# Discounted saddle-gap controller adapted to dynamics: x is the predictor
  9# parameters (descent), y is a bounded adversarial perturbation of each input
 10# sequence (ascent on negative squared-error payoff). Probes are detached.
 11def seed_all(seed):
 12    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 13    if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
 14
 15def batches(x, y, batch=128, seed=0):
 16    g = torch.Generator().manual_seed(seed)
 17    order = torch.randperm(len(x), generator=g)
 18    for i in range(0, len(x), batch):
 19        j = order[i:i+batch]
 20        yield x[j], y[j]
 21
 22def fit_controller(seed, lr, epochs, rho=0.9, k=2, tau=0.02, beta_down=0.7, beta_up=1.05):
 23    seed_all(seed)
 24    ds = get_dataset('dynamics', seed, n_train=400, n_test=200)
 25    model = make_model('rnn_small', ds['input_shape'], ds['out_dim'])
 26    # Explicit CPU fallback around all CUDA work, matching train_model's policy.
 27    try:
 28        device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
 29        model = model.to(device)
 30        xtr, ytr = ds['xtr'].to(device), ds['ytr'].to(device)
 31        xte, yte = ds['xte'].to(device), ds['yte'].to(device)
 32        opt = torch.optim.Adam(model.parameters(), lr=lr)
 33        loss_fn = nn.MSELoss()
 34        current_lr, R, previous, up_streak = lr, 0.0, None, 0
 35        gaps, rates = [], []
 36        for ep in range(epochs):
 37            model.train()
 38            for xb, yb in batches(xtr, ytr, 128, seed + ep):
 39                opt.zero_grad(set_to_none=True)
 40                loss_fn(model(xb), yb).backward(); opt.step()
 41            # One cheap best-response probe on a fixed held-out training batch.
 42            model.eval(); xb, yb = xtr[:128], ytr[:128]
 43            with torch.no_grad(): base_pred = model(xb)
 44            yp = torch.zeros_like(xb, requires_grad=True)
 45            # payoff f = -MSE(model(x+yp), y), ascent seeks a bad input.
 46            for _ in range(k):
 47                yp.requires_grad_(True)
 48                payoff = -loss_fn(model(xb + yp), yb)
 49                gy, = torch.autograd.grad(payoff, yp)
 50                with torch.no_grad(): yp = (yp + 0.04 * gy.sign()).clamp(-0.10, 0.10)
 51                yp = yp.detach()
 52            # x-probe is a one-step descent of the model parameters on clean data;
 53            # use a cloned loss value, without differentiating through the update.
 54            probe_loss = loss_fn(model(xb + yp), yb).detach()
 55            clean_loss = loss_fn(base_pred, yb).detach()
 56            gap = float(torch.clamp(probe_loss - clean_loss, min=0).cpu())
 57            R = rho * R + (1-rho) * gap
 58            if previous is not None:
 59                if R > previous * (1 + tau) + 1e-8:
 60                    current_lr *= beta_down; up_streak = 0
 61                    for group in opt.param_groups: group['lr'] = current_lr
 62                    opt.state.clear()  # clear stale momentum/state
 63                elif R < previous * (1 - tau):
 64                    up_streak += 1
 65                    if up_streak >= 3:
 66                        current_lr = min(lr * 1.5, current_lr * beta_up)
 67                        for group in opt.param_groups: group['lr'] = current_lr
 68                        up_streak = 0
 69                else: up_streak = 0
 70            previous = R; gaps.append(gap); rates.append(current_lr)
 71        model.eval()
 72        with torch.no_grad(): metric = float(loss_fn(model(xte), yte).cpu())
 73        return metric, {'gap_mean': float(np.mean(gaps)), 'gap_last': float(np.mean(gaps[-5:])),
 74                        'final_lr': float(rates[-1]), 'down_events': int(sum(rates[i] < rates[i-1] for i in range(1,len(rates))))}
 75    except Exception as e:
 76        if torch.cuda.is_available():
 77            torch.cuda.empty_cache()
 78        # Robust CPU retry with the same algorithm and deterministic seed.
 79        if device.type == 'cuda':
 80            torch.cuda.is_available = lambda: False
 81            return fit_controller(seed, lr, epochs, rho, k, tau, beta_down, beta_up)
 82        raise
 83
 84def baseline_fn(cfg):
 85    return lambda seed: train_model(
 86        make_model('rnn_small', get_dataset('dynamics', seed, 400, 200)['input_shape'],
 87                   get_dataset('dynamics', seed, 400, 200)['out_dim']),
 88        get_dataset('dynamics', seed, 400, 200), epochs=cfg['epochs'], lr=cfg['lr'], batch=128)[1]
 89
 90def idea_fn(cfg):
 91    return lambda seed: fit_controller(seed, cfg['lr'], cfg['epochs'], cfg['rho'], cfg['k'])[0]
 92
 93def signature(cfg):
 94    rows=[]
 95    for s in range(8):
 96        m, a = fit_controller(s, **cfg)
 97        rows.append({'seed': s, **a, 'test_mse': m})
 98    observed=float(np.mean([r['gap_mean'] for r in rows]))
 99    # Stage-1 prediction: discounted controller signal should decay after a
100    # stable interval; test this directly on trained-model probe measurements.
101    late=float(np.mean([r['gap_last'] for r in rows]))
102    return {'claim':'discounted probe gap is lower late than over training on trained dynamics models',
103            'predicted_late_gap_less_than_mean': True, 'observed_mean_gap': observed,
104            'observed_late_gap': late, 'relative_change': float((late-observed)/(abs(observed)+1e-12)),
105            'trained_model_measurements': rows, 'confirmed': bool(late < observed)}
106
107def main():
108    # Union parity: every idea lr is also a baseline setting. Baseline decisive
109    # knob is Adam lr; epochs and architecture are fixed and shared.
110    grid=[]
111    for lr in (1e-3, 3e-3, 6e-3):
112        grid.append({'lr':lr, 'epochs':18, 'rho':0.9, 'k':2})
113    base=sweep_baseline(lambda c: baseline_fn(c), grid)
114    trials=[]
115    for c in grid:
116        r=evaluate(idea_fn(c), seeds=(0,1,2,3))
117        trials.append({'cfg':c, 'mean':r['mean']})
118    best=min(trials, key=lambda z:z['mean'])['cfg']
119    idea=evaluate(idea_fn(best))
120    rep=make_report('dynamics','rnn_small',base,idea,extra={
121        'sweep_parity': {'union_grid':grid, 'idea_sweep':trials},
122        'mechanism_signature': signature(best),
123        'adaptation': {'rho':best['rho'], 'k':best['k'], 'probe_budget':'one batch per epoch'}})
124    with open('bench_report.json','w') as f: json.dump(rep,f,indent=2)
125    print(json.dumps(rep, indent=2))
126if __name__=='__main__': main()