Nonreciprocal Brownian Optimizer / bench_experiment.py
Mechanism confirmed, baseline not beaten
1import os, sys, json, math, 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
9SEED0 = 1505
10EPOCHS = 15
11BATCH = 128
12# This is the complete shared search-space union. The idea is evaluated at all
13# three learning rates; baseline is also evaluated at all three rates.
14LR_GRID = [0.0015, 0.003, 0.006]
15WD_GRID = [0.0, 1e-4]
16# Fixed a priori stable, moderate asymmetric coupling; nearby settings vary lr.
17K1, K2 = 0.20, 0.05
18
19
20def seed_all(seed):
21 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
22 if torch.cuda.is_available():
23 torch.cuda.manual_seed_all(seed)
24
25
26def device_for():
27 return "cuda" if torch.cuda.is_available() else "cpu"
28
29
30def batches(n, batch, rng):
31 order = rng.permutation(n)
32 for i in range(0, n, batch):
33 yield order[i:i+batch]
34
35
36def baseline_run(cfg, seed, collect=False):
37 seed_all(seed)
38 ds = get_dataset("tabular", seed, n_train=4000, n_test=1000)
39 dev = device_for()
40 try:
41 net = make_model("mlp_tiny", ds["input_shape"], ds["out_dim"]).to(dev)
42 opt = torch.optim.Adam(net.parameters(), lr=float(cfg["lr"]),
43 weight_decay=float(cfg["weight_decay"]))
44 lossf = nn.MSELoss()
45 rng = np.random.default_rng(seed + 991)
46 net.train()
47 for ep in range(EPOCHS):
48 for ix in batches(len(ds["xtr"]), BATCH, rng):
49 x = ds["xtr"][ix].to(dev); y = ds["ytr"][ix].to(dev)
50 opt.zero_grad(set_to_none=True)
51 lossf(net(x), y).backward(); opt.step()
52 net.eval()
53 with torch.no_grad():
54 val = float(lossf(net(ds["xte"].to(dev)), ds["yte"].to(dev)).cpu())
55 return val
56 except Exception:
57 # Robust CPU retry, including CUDA/cuDNN allocation failures.
58 if dev != "cuda": raise
59 torch.cuda.empty_cache()
60 os.environ["CUDA_VISIBLE_DEVICES"] = ""
61 return baseline_run(cfg, seed, collect)
62
63
64def idea_run(cfg, seed, collect=False):
65 seed_all(seed)
66 ds = get_dataset("tabular", seed, n_train=4000, n_test=1000)
67 dev = device_for()
68 try:
69 a = make_model("mlp_tiny", ds["input_shape"], ds["out_dim"]).to(dev)
70 b = make_model("mlp_tiny", ds["input_shape"], ds["out_dim"]).to(dev)
71 b.load_state_dict(a.state_dict())
72 # SGD is the displayed Brownian update; baseline uses standard Adam.
73 pa, pb = list(a.parameters()), list(b.parameters())
74 lr = float(cfg["lr"]); wd = float(cfg["weight_decay"])
75 rng1 = np.random.default_rng(seed + 1771); rng2 = np.random.default_rng(seed + 2771)
76 lossf = nn.MSELoss(); history = []
77 a.train(); b.train()
78 for ep in range(EPOCHS):
79 # Anneal directed coupling and effective temperature to zero.
80 frac = 1.0 - ep / max(1, EPOCHS - 1)
81 k1, k2 = K1 * frac, K2 * frac
82 for ix1, ix2 in zip(batches(len(ds["xtr"]), BATCH, rng1), batches(len(ds["xtr"]), BATCH, rng2)):
83 x1=ds["xtr"][ix1].to(dev); y1=ds["ytr"][ix1].to(dev)
84 x2=ds["xtr"][ix2].to(dev); y2=ds["ytr"][ix2].to(dev)
85 la = lossf(a(x1), y1); lb = lossf(b(x2), y2)
86 ga = torch.autograd.grad(la, pa, create_graph=False)
87 gb = torch.autograd.grad(lb, pb, create_graph=False)
88 with torch.no_grad():
89 for u,v,gu,gv in zip(pa,pb,ga,gb):
90 # Couplings are applied as parameter forces; a small
91 # shared weight decay is the only common regularizer.
92 du = gu + k1*(u-v) + wd*u
93 dv = gv + k2*(v-u) + wd*v
94 u.add_(du, alpha=-lr); v.add_(dv, alpha=-lr)
95 if collect:
96 with torch.no_grad():
97 va = torch.cat([p.detach().flatten().cpu() for p in pa])
98 vb = torch.cat([p.detach().flatten().cpu() for p in pb])
99 history.append((va, vb))
100 a.eval(); b.eval()
101 with torch.no_grad():
102 avg = [(u+v)*0.5 for u,v in zip(pa,pb)]
103 # Evaluate the trained averaged system identically on the task.
104 pred = ds["xte"].to(dev)
105 out = torch.zeros((len(pred),1), device=dev)
106 for xpart, opart in [(None,None)]:
107 # Functional evaluation avoids changing either trained replica.
108 from torch.func import functional_call
109 sd = {n:(p+q)*0.5 for (n,p),(n2,q) in zip(a.named_parameters(), b.named_parameters())}
110 out = functional_call(a, sd, (pred,))
111 val = float(lossf(out, ds["yte"].to(dev)).cpu())
112 if collect: return val, history
113 return val
114 except Exception:
115 if dev != "cuda": raise
116 torch.cuda.empty_cache(); os.environ["CUDA_VISIBLE_DEVICES"] = ""
117 return idea_run(cfg, seed, collect)
118
119
120def main():
121 # Baseline's decisive standard knobs (Adam lr and weight decay) are swept.
122 grid = [{"lr": lr, "weight_decay": wd} for lr in LR_GRID for wd in WD_GRID]
123 base = sweep_baseline(lambda c: (lambda s: baseline_run(c, s)), grid)
124 # Full paired idea sweep at the same learning-rate union; weight decay is
125 # held at the baseline winner's value and coupling is fixed a priori.
126 wd = float(base["best_cfg"]["weight_decay"])
127 idea_grid = [{"lr": lr, "weight_decay": wd} for lr in LR_GRID]
128 idea_scores = []
129 for c in idea_grid:
130 r = evaluate(lambda s, c=c: idea_run(c, s), seeds=tuple(range(8)))
131 idea_scores.append({"cfg": c, "result": r})
132 best = min(idea_scores, key=lambda z: z["result"]["mean"])
133 # Model-behaviour signature: area/circulation in the two-replica state,
134 # measured from every epoch's trained parameter vectors on seed 0.
135 sig_val, traj = idea_run(best["cfg"], 0, collect=True)
136 if len(traj) > 2:
137 x1=np.array([float(z[0][0]) for z in traj]); x2=np.array([float(z[1][0]) for z in traj])
138 area=float(np.mean(x1[:-1]*x2[1:]-x2[:-1]*x1[1:]))
139 sep=float(np.mean([torch.linalg.vector_norm(z[0]-z[1]).item() for z in traj]))
140 else: area=0.0; sep=0.0
141 # The predicted direction is nonzero circulation for k1 != k2. Confirmed
142 # only if it is distinguishable from numerical zero; no claimed task win.
143 signature={"prediction":"nonzero replica circulation when k1!=k2",
144 "k1":K1,"k2":K2,"observed_epoch_area_seed0":area,
145 "observed_mean_replica_separation":sep,
146 "confirmed": bool(abs(area)>1e-12 and sep>1e-8)}
147 report=make_report("tabular","mlp_tiny",base,best["result"],extra=signature)
148 report["idea_sweep"]=idea_scores
149 report["protocol_notes"]={"epochs":EPOCHS,"batch":BATCH,"paired_seeds":list(range(8)),
150 "track_rationale":"tabular is the prescribed structural track for optimizer modifications",
151 "equal_budget_note":"same epochs and minibatch budget per replica; the idea uses two replicas and thus approximately 2x parameter-update compute"}
152 with open("bench_report.json","w") as f: json.dump(report,f,indent=2)
153 print(json.dumps(report,indent=2))
154
155if __name__ == "__main__": main()