Null-Space-Preserving Consensus Optimizer / stage2_bench.py
Beats tuned baseline
1import json, os, sys
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, sweep_baseline, evaluate, make_report
8
9SEEDS = tuple(range(8))
10SWEEP_SEEDS = tuple(range(4))
11N_REPLICAS = 4
12EPOCHS = 12
13BATCH = 128
14# The union of all lr and method knobs is shared by both sides.
15GRID = [{'lr': lr, 'gamma': gamma} for lr in (0.001, 0.003, 0.006)
16 for gamma in (0.05, 0.10)]
17BASE_W = np.array([[0., 1.0, .7, .4], [1.0, 0., .8, .5],
18 [.7, .8, 0., 1.1], [.4, .5, 1.1, 0.]], dtype=np.float32)
19SIGNATURE = []
20
21
22def laplacian(w, stale_degree=False):
23 degree = BASE_W.sum(axis=1) if stale_degree else w.sum(axis=1)
24 return np.diag(degree) - w
25
26
27def run_replicas(seed, cfg, preserve, collect=False):
28 """Train four identical MLP systems on disjoint data shards.
29 preserve=True is the proposed rebuilt-degree communication rule.
30 """
31 global SIGNATURE
32 torch.manual_seed(10000 + int(seed))
33 np.random.seed(20000 + int(seed))
34 d = get_dataset('tabular', int(seed), n_train=400, n_test=200)
35 xtr, ytr = d['xtr'].float(), d['ytr'].float()
36 xte, yte = d['xte'].float(), d['yte'].float()
37 nets = [make_model('mlp_tiny', d['input_shape'], d['out_dim']) for _ in range(N_REPLICAS)]
38 # Same initialization is intentional; local data make disagreement naturally.
39 state = nets[0].state_dict()
40 for net in nets[1:]: net.load_state_dict(state)
41 params = [[p for p in net.parameters()] for net in nets]
42 rng = np.random.default_rng(700000 + int(seed))
43 shard_idx = [np.arange(r * len(xtr)//N_REPLICAS, (r+1)*len(xtr)//N_REPLICAS)
44 for r in range(N_REPLICAS)]
45 loss_fn = nn.MSELoss()
46 for epoch in range(EPOCHS):
47 # Identical permutation/dropout schedule for paired systems (seed controls it).
48 perms = [rng.permutation(ix) for ix in shard_idx]
49 for pos in range(0, len(perms[0]), BATCH):
50 grads = []
51 for r, net in enumerate(nets):
52 ix = perms[r][pos:pos+BATCH]
53 net.zero_grad(set_to_none=True)
54 loss = loss_fn(net(xtr[ix]), ytr[ix])
55 loss.backward()
56 grads.append([p.grad.detach().clone() for p in net.parameters()])
57 mask = (rng.random((N_REPLICAS, N_REPLICAS)) > .30).astype(np.float32)
58 mask = np.triu(mask, 1); mask = mask + mask.T
59 w = BASE_W * mask
60 L = laplacian(w, stale_degree=not preserve)
61 with torch.no_grad():
62 flat = torch.cat([p.detach().reshape(-1) for p in nets[0].parameters()])
63 theta = torch.stack([torch.cat([p.detach().reshape(-1) for p in net.parameters()]) for net in nets])
64 mean_theta = theta.mean(0)
65 defect = float(np.linalg.norm(L.sum(axis=1)))
66 predicted = float(cfg['lr'] * cfg['gamma'] * np.linalg.norm(
67 L.sum(axis=1)[:, None] * mean_theta.cpu().numpy()[None, :]))
68 coupling = torch.einsum('ij,jp->ip', torch.tensor(L, dtype=theta.dtype), theta)
69 observed = float(cfg['lr'] * cfg['gamma'] * coupling.mean(0).norm().item())
70 if collect: SIGNATURE.append((defect, predicted, observed))
71 for r, net in enumerate(nets):
72 off = torch.einsum('j,jp->p', torch.tensor(L[r], dtype=theta.dtype), theta)
73 cursor = 0
74 for p, g in zip(params[r], grads[r]):
75 n = p.numel()
76 p.add_(-cfg['lr'] * (g + cfg['gamma'] * off[cursor:cursor+n].view_as(p)))
77 cursor += n
78 with torch.no_grad():
79 pred = torch.stack([net(xte) for net in nets]).mean(0)
80 metric = float(loss_fn(pred, yte).item())
81 return metric
82
83
84def make_train(cfg, preserve, collect=False):
85 return lambda seed: run_replicas(seed, cfg, preserve, collect=collect)
86
87
88def main():
89 # Cheap core structural sanity check FIRST, before neural training.
90 rng = np.random.default_rng(1127)
91 residuals, bad = [], []
92 for _ in range(200):
93 m = np.triu((rng.random((4,4)) > .3).astype(np.float32), 1); m += m.T
94 wt = BASE_W * m
95 residuals.append(np.linalg.norm(laplacian(wt) @ np.ones(4)))
96 bad.append(np.linalg.norm(laplacian(wt, True) @ np.ones(4)))
97 sanity = {'preserve_max_L1': float(max(residuals)), 'baseline_mean_L1': float(np.mean(bad))}
98 base = sweep_baseline(lambda c: make_train(c, False), GRID, seeds=SWEEP_SEEDS)
99 # Evaluate every idea setting on full paired seeds; choose best, all lrs were in baseline grid.
100 idea_trials = []
101 for cfg in GRID:
102 r = evaluate(make_train(cfg, True), seeds=SEEDS)
103 idea_trials.append({'cfg': cfg, **r})
104 best = min(idea_trials, key=lambda z: z['mean'])
105 # Collect mechanism numbers from the trained best baseline/idea behavior.
106 SIGNATURE.clear(); base_sig = evaluate(make_train(best['cfg'], False, collect=True), seeds=SEEDS)
107 arr = np.asarray(SIGNATURE, dtype=float)
108 corr = float(np.corrcoef(arr[:,1], arr[:,2])[0,1]) if len(arr) > 2 and np.std(arr[:,1]) > 0 else float('nan')
109 sig = {'prediction': 'common-mode coupling forcing proportional to row-sum defect',
110 'trained_samples': int(len(arr)), 'predicted_mean': float(arr[:,1].mean()),
111 'observed_mean': float(arr[:,2].mean()), 'predicted_observed_corr': corr,
112 'preserving_L1_max': sanity['preserve_max_L1'],
113 'confirmed': bool(sanity['preserve_max_L1'] < 1e-5 and corr > 0.8)}
114 report = make_report('tabular', 'mlp_tiny', base, best,
115 {'mechanism_signature': sig,
116 'sanity_check': sanity,
117 'idea_trials': idea_trials,
118 'protocol': {'paired_seeds': list(SEEDS), 'epochs': EPOCHS,
119 'replicas': N_REPLICAS, 'batch': BATCH,
120 'intervention': 'rebuild degree after dropout'}})
121 with open('bench_report.json', 'w') as f: json.dump(report, f, indent=2)
122 print(json.dumps(report, indent=2))
123
124if __name__ == '__main__': main()