import os, sys, json, random import numpy as np import torch import torch.nn as nn sys.path.insert(0, "/home/maxwelhelp/all/math2nn") from bench import get_dataset, make_model, evaluate, sweep_baseline, make_report SEEDS = tuple(range(8)) SWEEP_SEEDS = tuple(range(4)) EPOCHS = 5 BATCH = 128 # The union is deliberately shared: every lr tested by the idea is in baseline. LRS = [1e-3, 3e-3, 1e-2] CLIPS = [0.5, 1.0, 5.0] IDEA_GRID = [{"lr": x, "margin0": 0.20} for x in LRS] BASE_GRID = [{"lr": x, "clip": c} for x in LRS for c in CLIPS] idea_signatures = {} 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_for(): return "cuda" if torch.cuda.is_available() else "cpu" def proxy_sectors(net): """Fourier-sector proxy of GRU recurrent Jacobian, one scalar per channel mode. The three gate matrices are averaged; this is a diagnostic, not an identity. """ H = net.rnn.hidden_size W = net.rnn.weight_hh_l0.detach().float().cpu().numpy().reshape(3, H, H) U = np.exp(2j*np.pi*np.outer(np.arange(H), np.arange(H))/H)/np.sqrt(H) blocks = np.stack([np.diag(U.conj().T @ w @ U) for w in W]) lam = blocks.mean(axis=0) margins = np.abs(1.0 - lam) return lam, margins def damp_fourier_grad(net, margin0): """Apply sector-specific damping to GRU recurrent gradients. Each gate gradient is transformed to the cyclic Fourier basis and attenuated according to the measured distance of that sector's recurrent proxy from 1. """ H = net.rnn.hidden_size with torch.no_grad(): W = net.rnn.weight_hh_l0.detach().float().cpu().numpy().reshape(3, H, H) U = np.exp(2j*np.pi*np.outer(np.arange(H), np.arange(H))/H)/np.sqrt(H) blocks = np.stack([np.diag(U.conj().T @ w @ U) for w in W]) margins = np.abs(1.0 - blocks.mean(axis=0)) scales = np.clip(margins / margin0, 0.10, 1.0) # Blend in Fourier coordinates: row/column sector correspondence. g = net.rnn.weight_hh_l0.grad if g is None: return ga = g.detach().float().cpu().numpy().reshape(3, H, H) out = np.empty_like(ga) for q in range(3): F = U.conj().T @ ga[q] @ U F *= np.sqrt(scales[:, None] * scales[None, :]) out[q] = (U @ F @ U.conj().T).real net.rnn.weight_hh_l0.grad.copy_(torch.as_tensor(out.reshape(3*H, H), device=g.device, dtype=g.dtype)) def train_one(seed, mode, cfg, return_net=False): seed_all(seed) ds = get_dataset("dynamics", seed, n_train=400, n_test=200) net = make_model("rnn_small", ds["input_shape"], ds["out_dim"]) dev = device_for() try: net = net.to(dev) opt = torch.optim.Adam(net.parameters(), lr=cfg["lr"]) lossf = nn.MSELoss() xtr, ytr = ds["xtr"].to(dev), ds["ytr"].to(dev) for _ in range(EPOCHS): net.train(); perm = torch.randperm(len(xtr), device=dev) for i in range(0, len(xtr), BATCH): ix = perm[i:i+BATCH] loss = lossf(net(xtr[ix]), ytr[ix]) opt.zero_grad(); loss.backward() if mode == "baseline": torch.nn.utils.clip_grad_norm_(net.parameters(), cfg["clip"]) else: damp_fourier_grad(net, cfg["margin0"]) torch.nn.utils.clip_grad_norm_(net.parameters(), 5.0) opt.step() net.eval() with torch.no_grad(): pred = net(ds["xte"].to(dev)) metric = float(((pred - ds["yte"].to(dev))**2).mean().cpu()) if return_net: return metric, net, ds return metric except RuntimeError: # Explicit CPU fallback for shared/unstable CUDA conditions. if dev == "cuda": torch.cuda.empty_cache() return train_one_cpu(seed, mode, cfg) raise def train_one_cpu(seed, mode, cfg): # Re-execute identically on CPU, avoiding recursive fallback. seed_all(seed) ds = get_dataset("dynamics", seed, n_train=400, n_test=200) net = make_model("rnn_small", ds["input_shape"], ds["out_dim"]) opt = torch.optim.Adam(net.parameters(), lr=cfg["lr"]); lossf = nn.MSELoss() for _ in range(EPOCHS): perm = torch.randperm(len(ds["xtr"])); for i in range(0, len(perm), BATCH): ix=perm[i:i+BATCH]; loss=lossf(net(ds["xtr"][ix]),ds["ytr"][ix]) opt.zero_grad(); loss.backward() if mode == "baseline": torch.nn.utils.clip_grad_norm_(net.parameters(), cfg["clip"]) else: damp_fourier_grad(net, cfg["margin0"]) opt.step() with torch.no_grad(): metric=float(((net(ds["xte"])-ds["yte"])**2).mean()) return metric def base_factory(cfg): return lambda seed: train_one(seed, "baseline", cfg) def idea_factory(cfg): return lambda seed: train_one(seed, "idea", cfg) def signature(idea_cfg): rows=[] for s in SEEDS: metric, net, ds = train_one(s, "idea", idea_cfg, return_net=True) lam, margins = proxy_sectors(net) predicted = int(np.argmin(margins)) with torch.no_grad(): # Diagnostic rollout is deliberately CPU/cuDNN-free because the shared GPU # may reject a second GRU allocation even after successful training. net_cpu = net.cpu() seq=ds["xte"][:128].view(-1, ds["xte"].shape[1]//3, 3) old_cudnn = torch.backends.cudnn.enabled torch.backends.cudnn.enabled = False try: out,h=net_cpu.rnn(seq) finally: torch.backends.cudnn.enabled = old_cudnn a=np.abs(np.fft.fft(out.detach().numpy(), axis=2)).mean(axis=(0,1)) observed=int(np.argmax(a)) rows.append({"seed":s,"metric":metric,"predicted_sector":predicted,"observed_sector":observed,"predicted_margin":float(margins[predicted])}) agree=sum(r["predicted_sector"]==r["observed_sector"] for r in rows) return {"prediction":"smallest Fourier recurrent margin predicts dominant hidden-channel Fourier sector", "predicted_vs_observed":rows, "agreement_fraction":agree/len(rows), "confirmed":bool(agree/len(rows)>=0.5)} def main(): torch.set_num_threads(4) base = sweep_baseline(base_factory, BASE_GRID, seeds=SWEEP_SEEDS) # Idea is evaluated at baseline lr plus two nearby settings; the union is in BASE_GRID. idea_tried=[] for cfg in IDEA_GRID: r=evaluate(idea_factory(cfg), seeds=SEEDS) idea_tried.append({"cfg":cfg,"full":r}) best=min(idea_tried,key=lambda z:z["full"]["mean"]) sig=signature(best["cfg"]) report=make_report("dynamics","rnn_small",base,best["full"],extra=sig) report["idea_sweep"]=idea_tried report["track_justification"]="Dynamics is structurally matched: the task is an actuated pendulum rollout and the intervention monitors recurrent stability sectors." report["protocol"]={"paired_seeds":list(SEEDS),"baseline_sweep_seeds":list(SWEEP_SEEDS),"epochs":EPOCHS,"batch":BATCH,"baseline_grid":BASE_GRID,"idea_grid":IDEA_GRID} with open("bench_report.json","w") as f: json.dump(report,f,indent=2) print(json.dumps(report,indent=2)) if __name__ == "__main__": main()