Symmetry-Resolved Fourier Bifurcation Monitor / bench_stage2.py

Failed on benchmark

Raw ⬇ ZIP
  1import os, sys, json, random
  2import numpy as np
  3import torch
  4import torch.nn as nn
  5
  6sys.path.insert(0, "/home/maxwelhelp/all/math2nn")
  7from bench import get_dataset, make_model, evaluate, sweep_baseline, make_report
  8
  9SEEDS = tuple(range(8))
 10SWEEP_SEEDS = tuple(range(4))
 11EPOCHS = 5
 12BATCH = 128
 13# The union is deliberately shared: every lr tested by the idea is in baseline.
 14LRS = [1e-3, 3e-3, 1e-2]
 15CLIPS = [0.5, 1.0, 5.0]
 16IDEA_GRID = [{"lr": x, "margin0": 0.20} for x in LRS]
 17BASE_GRID = [{"lr": x, "clip": c} for x in LRS for c in CLIPS]
 18
 19idea_signatures = {}
 20
 21
 22def seed_all(seed):
 23    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 24    if torch.cuda.is_available():
 25        torch.cuda.manual_seed_all(seed)
 26
 27
 28def device_for():
 29    return "cuda" if torch.cuda.is_available() else "cpu"
 30
 31
 32def proxy_sectors(net):
 33    """Fourier-sector proxy of GRU recurrent Jacobian, one scalar per channel mode.
 34    The three gate matrices are averaged; this is a diagnostic, not an identity.
 35    """
 36    H = net.rnn.hidden_size
 37    W = net.rnn.weight_hh_l0.detach().float().cpu().numpy().reshape(3, H, H)
 38    U = np.exp(2j*np.pi*np.outer(np.arange(H), np.arange(H))/H)/np.sqrt(H)
 39    blocks = np.stack([np.diag(U.conj().T @ w @ U) for w in W])
 40    lam = blocks.mean(axis=0)
 41    margins = np.abs(1.0 - lam)
 42    return lam, margins
 43
 44
 45def damp_fourier_grad(net, margin0):
 46    """Apply sector-specific damping to GRU recurrent gradients.
 47    Each gate gradient is transformed to the cyclic Fourier basis and attenuated
 48    according to the measured distance of that sector's recurrent proxy from 1.
 49    """
 50    H = net.rnn.hidden_size
 51    with torch.no_grad():
 52        W = net.rnn.weight_hh_l0.detach().float().cpu().numpy().reshape(3, H, H)
 53        U = np.exp(2j*np.pi*np.outer(np.arange(H), np.arange(H))/H)/np.sqrt(H)
 54        blocks = np.stack([np.diag(U.conj().T @ w @ U) for w in W])
 55        margins = np.abs(1.0 - blocks.mean(axis=0))
 56        scales = np.clip(margins / margin0, 0.10, 1.0)
 57        # Blend in Fourier coordinates: row/column sector correspondence.
 58        g = net.rnn.weight_hh_l0.grad
 59        if g is None: return
 60        ga = g.detach().float().cpu().numpy().reshape(3, H, H)
 61        out = np.empty_like(ga)
 62        for q in range(3):
 63            F = U.conj().T @ ga[q] @ U
 64            F *= np.sqrt(scales[:, None] * scales[None, :])
 65            out[q] = (U @ F @ U.conj().T).real
 66        net.rnn.weight_hh_l0.grad.copy_(torch.as_tensor(out.reshape(3*H, H), device=g.device, dtype=g.dtype))
 67
 68
 69def train_one(seed, mode, cfg, return_net=False):
 70    seed_all(seed)
 71    ds = get_dataset("dynamics", seed, n_train=400, n_test=200)
 72    net = make_model("rnn_small", ds["input_shape"], ds["out_dim"])
 73    dev = device_for()
 74    try:
 75        net = net.to(dev)
 76        opt = torch.optim.Adam(net.parameters(), lr=cfg["lr"])
 77        lossf = nn.MSELoss()
 78        xtr, ytr = ds["xtr"].to(dev), ds["ytr"].to(dev)
 79        for _ in range(EPOCHS):
 80            net.train(); perm = torch.randperm(len(xtr), device=dev)
 81            for i in range(0, len(xtr), BATCH):
 82                ix = perm[i:i+BATCH]
 83                loss = lossf(net(xtr[ix]), ytr[ix])
 84                opt.zero_grad(); loss.backward()
 85                if mode == "baseline":
 86                    torch.nn.utils.clip_grad_norm_(net.parameters(), cfg["clip"])
 87                else:
 88                    damp_fourier_grad(net, cfg["margin0"])
 89                    torch.nn.utils.clip_grad_norm_(net.parameters(), 5.0)
 90                opt.step()
 91        net.eval()
 92        with torch.no_grad():
 93            pred = net(ds["xte"].to(dev))
 94            metric = float(((pred - ds["yte"].to(dev))**2).mean().cpu())
 95        if return_net:
 96            return metric, net, ds
 97        return metric
 98    except RuntimeError:
 99        # Explicit CPU fallback for shared/unstable CUDA conditions.
100        if dev == "cuda":
101            torch.cuda.empty_cache()
102            return train_one_cpu(seed, mode, cfg)
103        raise
104
105
106def train_one_cpu(seed, mode, cfg):
107    # Re-execute identically on CPU, avoiding recursive fallback.
108    seed_all(seed)
109    ds = get_dataset("dynamics", seed, n_train=400, n_test=200)
110    net = make_model("rnn_small", ds["input_shape"], ds["out_dim"])
111    opt = torch.optim.Adam(net.parameters(), lr=cfg["lr"]); lossf = nn.MSELoss()
112    for _ in range(EPOCHS):
113        perm = torch.randperm(len(ds["xtr"]));
114        for i in range(0, len(perm), BATCH):
115            ix=perm[i:i+BATCH]; loss=lossf(net(ds["xtr"][ix]),ds["ytr"][ix])
116            opt.zero_grad(); loss.backward()
117            if mode == "baseline": torch.nn.utils.clip_grad_norm_(net.parameters(), cfg["clip"])
118            else: damp_fourier_grad(net, cfg["margin0"])
119            opt.step()
120    with torch.no_grad(): metric=float(((net(ds["xte"])-ds["yte"])**2).mean())
121    return metric
122
123
124def base_factory(cfg):
125    return lambda seed: train_one(seed, "baseline", cfg)
126
127def idea_factory(cfg):
128    return lambda seed: train_one(seed, "idea", cfg)
129
130
131def signature(idea_cfg):
132    rows=[]
133    for s in SEEDS:
134        metric, net, ds = train_one(s, "idea", idea_cfg, return_net=True)
135        lam, margins = proxy_sectors(net)
136        predicted = int(np.argmin(margins))
137        with torch.no_grad():
138            # Diagnostic rollout is deliberately CPU/cuDNN-free because the shared GPU
139            # may reject a second GRU allocation even after successful training.
140            net_cpu = net.cpu()
141            seq=ds["xte"][:128].view(-1, ds["xte"].shape[1]//3, 3)
142            old_cudnn = torch.backends.cudnn.enabled
143            torch.backends.cudnn.enabled = False
144            try:
145                out,h=net_cpu.rnn(seq)
146            finally:
147                torch.backends.cudnn.enabled = old_cudnn
148            a=np.abs(np.fft.fft(out.detach().numpy(), axis=2)).mean(axis=(0,1))
149            observed=int(np.argmax(a))
150        rows.append({"seed":s,"metric":metric,"predicted_sector":predicted,"observed_sector":observed,"predicted_margin":float(margins[predicted])})
151    agree=sum(r["predicted_sector"]==r["observed_sector"] for r in rows)
152    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)}
153
154
155def main():
156    torch.set_num_threads(4)
157    base = sweep_baseline(base_factory, BASE_GRID, seeds=SWEEP_SEEDS)
158    # Idea is evaluated at baseline lr plus two nearby settings; the union is in BASE_GRID.
159    idea_tried=[]
160    for cfg in IDEA_GRID:
161        r=evaluate(idea_factory(cfg), seeds=SEEDS)
162        idea_tried.append({"cfg":cfg,"full":r})
163    best=min(idea_tried,key=lambda z:z["full"]["mean"])
164    sig=signature(best["cfg"])
165    report=make_report("dynamics","rnn_small",base,best["full"],extra=sig)
166    report["idea_sweep"]=idea_tried
167    report["track_justification"]="Dynamics is structurally matched: the task is an actuated pendulum rollout and the intervention monitors recurrent stability sectors."
168    report["protocol"]={"paired_seeds":list(SEEDS),"baseline_sweep_seeds":list(SWEEP_SEEDS),"epochs":EPOCHS,"batch":BATCH,"baseline_grid":BASE_GRID,"idea_grid":IDEA_GRID}
169    with open("bench_report.json","w") as f: json.dump(report,f,indent=2)
170    print(json.dumps(report,indent=2))
171
172if __name__ == "__main__": main()