Semiglobal-PL Phase Scheduler / bench_run.py

✓✓ Beats tuned baseline

Raw ⬇ ZIP
  1import sys, json, math, random
  2import numpy as np
  3sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  4import torch
  5from bench import get_dataset, make_model, evaluate, sweep_baseline, make_report
  6
  7SEEDS = tuple(range(8))
  8SWEEP_SEEDS = tuple(range(4))
  9LR_GRID = [0.003, 0.006, 0.009]
 10EPOCHS = 18
 11BATCH = 128
 12CLIP = 1.0
 13
 14
 15def seed_all(seed):
 16    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 17    if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
 18
 19
 20def run(seed, lr, scheduled):
 21    seed_all(seed)
 22    requested = 'cuda' if torch.cuda.is_available() else 'cpu'
 23    try:
 24        return _run_device(seed, lr, scheduled, requested)
 25    except Exception:
 26        if requested == 'cuda':
 27            try: torch.cuda.empty_cache()
 28            except Exception: pass
 29            return _run_device(seed, lr, scheduled, 'cpu')
 30        raise
 31
 32
 33def _run_device(seed, lr, scheduled, device_name):
 34    device = torch.device(device_name)
 35    d = get_dataset('tabular', seed, n_train=400, n_test=200)
 36    xtr, ytr = d['xtr'].to(device), d['ytr'].to(device)
 37    xte, yte = d['xte'].to(device), d['yte'].to(device)
 38    model = make_model('mlp_tiny', d['input_shape'], d['out_dim']).to(device)
 39    opt = torch.optim.SGD(model.parameters(), lr=lr)
 40    fhat = None; qhist = []; gaphist = []; train_hist = []
 41    local_streak = 0; switched = False; switch_step = None; clip_steps = 0
 42    step = 0
 43    n = len(xtr)
 44    for epoch in range(EPOCHS):
 45        # deterministic per-seed permutation, equivalent data budget in both arms
 46        gen = torch.Generator(device='cpu').manual_seed(seed * 1000 + epoch)
 47        perm = torch.randperm(n, generator=gen, device='cpu')
 48        for start in range(0, n, BATCH):
 49            idx = perm[start:start+BATCH].to(device)
 50            xb, yb = xtr[idx], ytr[idx]
 51            opt.zero_grad(set_to_none=True)
 52            pred = model(xb)
 53            loss = torch.nn.functional.mse_loss(pred, yb)
 54            loss.backward()
 55            g2 = 0.0
 56            for p in model.parameters():
 57                if p.grad is not None: g2 += float((p.grad.detach() ** 2).sum().cpu())
 58            g = math.sqrt(max(g2, 0.0)); lv = float(loss.detach().cpu())
 59            if fhat is None: fhat = lv
 60            else: fhat = min(fhat, 0.98 * fhat + 0.02 * lv)
 61            gap = max(lv - fhat, 1e-8)
 62            q = g / math.sqrt(gap)
 63            qhist.append(q); gaphist.append(gap); train_hist.append(lv)
 64            if scheduled and len(qhist) >= 20:
 65                qwin = np.asarray(qhist[-100:]); gapwin = np.asarray(gaphist[-100:])
 66                floor = max(float(np.percentile(qwin, 10)) / 2.0, 1e-5)
 67                candidate = gap <= float(np.percentile(gapwin, 35)) and q >= floor
 68                local_streak = local_streak + 1 if candidate else 0
 69                if local_streak >= 5 and not switched:
 70                    for group in opt.param_groups: group['lr'] *= 1.5
 71                    switched = True; switch_step = step
 72            pre = torch.nn.utils.clip_grad_norm_(model.parameters(), CLIP)
 73            clip_steps += int(float(pre) > CLIP)
 74            opt.step(); step += 1
 75    with torch.no_grad():
 76        test_loss = float(torch.nn.functional.mse_loss(model(xte), yte).cpu())
 77    # Re-test the claimed local prediction on the trained model's observed trajectory.
 78    qarr, garr = np.asarray(qhist), np.asarray(gaphist)
 79    tail = min(40, len(qarr))
 80    observed_slope = float(np.polyfit(np.arange(tail), np.log(np.maximum(garr[-tail:], 1e-12)), 1)[0]) if tail >= 3 else float('nan')
 81    predicted_slope = float(-np.mean(qarr[-tail:] ** 2)) if tail else float('nan')
 82    return {'metric': test_loss, 'switched': switched, 'switch_step': switch_step,
 83            'lr_final': float(opt.param_groups[0]['lr']), 'clip_steps': clip_steps,
 84            'observed_log_gap_slope': observed_slope, 'predicted_minus_q2': predicted_slope,
 85            'q_tail_mean': float(np.mean(qarr[-tail:])) if tail else float('nan'),
 86            'device': device_name}
 87
 88
 89def main():
 90    base = sweep_baseline(lambda cfg: (lambda seed: run(seed, cfg['lr'], False)['metric']),
 91                          [{'lr': x} for x in LR_GRID], seeds=SWEEP_SEEDS)
 92    best_lr = float(base['best_cfg']['lr'])
 93    idea_cfgs = [{'lr': x} for x in LR_GRID]
 94    idea_sweep = []
 95    for cfg in idea_cfgs:
 96        r = evaluate(lambda seed, x=cfg['lr']: run(seed, x, True)['metric'], seeds=SWEEP_SEEDS)
 97        idea_sweep.append({'cfg': cfg, 'mean': r['mean']})
 98    best_idea_cfg = min(idea_sweep, key=lambda z: z['mean'])['cfg']
 99    # Full 8-seed results at the best idea configuration, with full per-run diagnostics.
100    idea_runs = [run(s, best_idea_cfg['lr'], True) for s in SEEDS]
101    idea_res = {'mean': float(np.mean([r['metric'] for r in idea_runs])),
102                'std': float(np.std([r['metric'] for r in idea_runs])),
103                'per_seed': [r['metric'] for r in idea_runs], 'n': 8,
104                'cfg': best_idea_cfg, 'runs': idea_runs, 'sweep': idea_sweep}
105    rep = make_report('tabular', 'mlp_tiny', base, idea_res)
106    # Signature uses only trained-model observed numbers, not the toy identity.
107    obs = [r['observed_log_gap_slope'] for r in idea_runs]
108    pred = [r['predicted_minus_q2'] for r in idea_runs]
109    rel = [abs(a-b)/max(abs(b),1e-12) for a,b in zip(obs,pred) if np.isfinite(a) and np.isfinite(b)]
110    rep['mechanism_signature'] = {
111        'quantity': 'tail log(training loss gap) slope versus -mean(q^2), measured during trained NN runs',
112        'observed_mean': float(np.mean(obs)), 'predicted_mean': float(np.mean(pred)),
113        'relative_error_mean': float(np.mean(rel)) if rel else None,
114        'tolerance': 0.20, 'confirmed': bool(rel and np.mean(rel) <= 0.20),
115        'switch_fraction': float(np.mean([r['switched'] for r in idea_runs]))}
116    rep['audit'] = {'baseline_lr_grid': LR_GRID, 'idea_lr_grid': LR_GRID,
117                    'baseline_selected_lr': best_lr, 'idea_selected_lr': best_idea_cfg['lr'],
118                    'epochs': EPOCHS, 'batch': BATCH, 'clip_norm': CLIP,
119                    'domain_rationale': 'tabular is the prescribed structural track for optimizer and learning-rate schedule ideas'}
120    print(json.dumps(rep, indent=2))
121
122if __name__ == '__main__': main()