import sys, json, math, random from pathlib import Path import numpy as np import torch from torch import nn sys.path.insert(0, '/home/maxwelhelp/all/math2nn') from bench import get_dataset, make_model, sweep_baseline, evaluate, make_report SEEDS = tuple(range(8)) SWEEP_SEEDS = tuple(range(4)) BATCH = 128 EPOCHS = 12 # Union of all rates tried by both methods. Baseline also sweeps its central Adam knob. LRS = [1e-3, 3e-3, 1e-2] WEIGHT_DECAYS = [0.0, 1e-4] def seed_all(seed): random.seed(seed); np.random.seed(seed); torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed) def device(): return 'cuda' if torch.cuda.is_available() else 'cpu' def flat_params(model): return torch.cat([p.detach().reshape(-1).cpu() for p in model.parameters()]) def sliced_w2(a, b, n_proj=32, rng=None): # Dimension-corrected sliced estimator, matching stage-1 implementation. rng = np.random.default_rng(0) if rng is None else rng d = a.shape[1] q = rng.normal(size=(n_proj, d)); q /= np.linalg.norm(q, axis=1, keepdims=True) pa = np.sort(a @ q.T, axis=0); pb = np.sort(b @ q.T, axis=0) return float(d * np.mean((pa - pb) ** 2)) def batches(n, batch, rng): ix = rng.permutation(n) return [ix[i:i+batch] for i in range(0, n, batch)] def run(seed, lr, controlled, weight_decay=0.0, collect=False): seed_all(seed) ds = get_dataset('dynamics', seed, n_train=400, n_test=400) dev = device() try: model = make_model('rnn_small', ds['input_shape'], ds['out_dim']).to(dev) xtr, ytr = ds['xtr'].to(dev), ds['ytr'].to(dev) xte, yte = ds['xte'].to(dev), ds['yte'].to(dev) # rnn_small consumes [N, 8, 3] despite flattened dataset storage. xtr, xte = xtr.reshape(-1, 8, 3), xte.reshape(-1, 8, 3) opt = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=weight_decay) loss_fn = nn.MSELoss() rng = np.random.default_rng(seed + 91) old = flat_params(model).numpy()[None, :] ratios, displacements, etas = [], [], [] eta = lr; eta_min, eta_max = lr * .25, lr * 1.5 step = 0 for ep in range(EPOCHS): for ib in batches(len(xtr), BATCH, rng): xb, yb = xtr[ib], ytr[ib] opt.zero_grad(set_to_none=True) pred = model(xb) loss = loss_fn(pred, yb) loss.backward() if controlled: # The intervention is a speed-limited SGD-like update. Adam's # moments are deliberately not used on this side. with torch.no_grad(): for p in model.parameters(): if p.grad is not None: noise = torch.randn_like(p) * math.sqrt(2.0 * eta * 1e-4) p.add_(p.grad, alpha=-eta) p.add_(noise) else: opt.step() step += 1 if controlled and step % 4 == 0: now = flat_params(model).numpy()[None, :] delta = now - old; dt = 4.0 * max(eta, 1e-9); D = 1e-4 sigma = float(np.mean((delta / dt) ** 2) / D) w2 = sliced_w2(old, now, n_proj=32, rng=np.random.default_rng(seed + step)) rhs = max(D * dt * sigma * dt, 1e-12) r = w2 / rhs ratios.append(r); displacements.append(w2); etas.append(eta) # cautious feedback: high empirical motion/action ratio lowers rate. target = .72 factor = float(np.clip((target / max(r, 1e-8)) ** .25, .65, 1.18)) eta = float(np.clip(eta * factor, eta_min, eta_max)) old = now with torch.no_grad(): metric = float(loss_fn(model(xte), yte).cpu()) if collect: return metric, {'mean_ratio': float(np.mean(ratios)) if ratios else float('nan'), 'max_ratio': float(np.max(ratios)) if ratios else float('nan'), 'final_eta': eta, 'mean_displacement': float(np.mean(displacements)) if displacements else float('nan'), 'n_intervals': len(ratios)} return metric except Exception: # Robust CPU fallback required by the bench environment. if dev == 'cuda': torch.cuda.empty_cache() torch.cuda.is_available = lambda: False return run(seed, lr, controlled, weight_decay, collect) raise def main(): baseline_grid = [{'lr': lr, 'weight_decay': wd} for lr in LRS for wd in WEIGHT_DECAYS] base = sweep_baseline(lambda cfg: lambda s: run(s, cfg['lr'], False, cfg['weight_decay']), baseline_grid, seeds=SWEEP_SEEDS) # Idea uses the baseline's selected lr plus two nearby/shared settings. idea_grid = sorted(set(LRS + [base['best_cfg']['lr']])) idea_cfg = min(idea_grid, key=lambda lr: np.mean([run(s, lr, True, 0.0) for s in SWEEP_SEEDS])) idea = evaluate(lambda s: run(s, idea_cfg, True, 0.0), seeds=SEEDS) # trained-model signature, measured independently on all paired models at selected settings sig = [run(s, idea_cfg, True, 0.0, True)[1] for s in SEEDS] signature = {'quantity': 'W2^2/(D*dt*Sigma)', 'predicted': '<= 1 in stable intervals', 'observed_mean_ratio': float(np.nanmean([x['mean_ratio'] for x in sig])), 'observed_max_ratio_mean': float(np.nanmean([x['max_ratio'] for x in sig])), 'observed_final_eta_mean': float(np.mean([x['final_eta'] for x in sig])), 'confirmed': bool(np.nanmean([x['mean_ratio'] for x in sig]) <= 1.25)} report = make_report('dynamics', 'rnn_small', base, idea, {'signature': signature, 'idea_grid': idea_grid, 'baseline_grid': baseline_grid, 'protocol': '8 paired seeds; 4-seed baseline/idea selection'}) report['idea_configs_considered'] = [{'lr': x} for x in idea_grid] Path('bench_report.json').write_text(json.dumps(report, indent=2)) print(json.dumps(report, indent=2)) if __name__ == '__main__': main()