Congestion-aware equimarginal MoE router / stage2_bench.py
Mechanism confirmed, baseline not beaten
1import sys, json, math, random
2from pathlib import Path
3import numpy as np
4import torch
5import torch.nn as nn
6torch.set_num_threads(2)
7
8sys.path.insert(0, "/home/maxwelhelp/all/math2nn")
9from bench import sweep_baseline, evaluate, make_report, get_dataset
10
11META = {"name": "congestion_moe_sequence", "domain": "moe-routing", "description": "Registered multi-token expert regression track"}
12
13DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
14# CUDA initialization is known to be flaky on this host; train_model-style fallback.
15def safe_device():
16 if DEVICE != "cuda": return "cpu"
17 try:
18 torch.zeros(1, device="cuda").sum().item()
19 return "cuda"
20 except Exception:
21 return "cpu"
22
23
24def project_simplex(v, z=1.0):
25 # Batched Euclidean projection, differentiable almost everywhere.
26 u, _ = torch.sort(v, dim=-1, descending=True)
27 cssv = torch.cumsum(u, dim=-1) - z
28 ind = torch.arange(1, v.shape[-1] + 1, device=v.device, dtype=v.dtype)
29 cond = u - cssv / ind > 0
30 rho = cond.sum(dim=-1).clamp_min(1).long() - 1
31 theta = cssv.gather(-1, rho.unsqueeze(-1)).squeeze(-1) / (rho + 1).to(v.dtype)
32 return (v - theta.unsqueeze(-1)).clamp_min(0)
33
34
35class TinyMoE(nn.Module):
36 def __init__(self, d=4, h=8, experts=4, temperature=0.7, idea=False, steps=4, eta=0.8):
37 super().__init__()
38 self.experts = nn.ModuleList([nn.Sequential(nn.Linear(d, h), nn.Tanh(), nn.Linear(h, h), nn.Tanh()) for _ in range(experts)])
39 self.router = nn.Linear(d, experts)
40 self.head = nn.Sequential(nn.Linear(h, h), nn.Tanh(), nn.Linear(h, 1))
41 self.temperature, self.idea, self.steps, self.eta = temperature, idea, steps, eta
42 self.last_load = None
43 self.last_soft_load = None
44
45 def forward(self, x):
46 # Sequence-level groups: each sequence is one routing player; token outputs
47 # are mixed using the same group allocation, preserving multi-token structure.
48 b, l, d = x.shape
49 logits = self.router(x).mean(dim=1) / max(self.temperature, 1e-5)
50 a = torch.exp(logits - logits.detach().amax(dim=-1, keepdim=True))
51 soft = torch.softmax(logits, dim=-1)
52 if not self.idea:
53 alloc = soft
54 else:
55 # b_i is a positive capacity prior; equal capacity is standard in this tiny bench.
56 cap = torch.full((a.shape[-1],), 0.35, device=x.device, dtype=x.dtype)
57 alloc = torch.full_like(a, 1.0 / a.shape[-1])
58 for _ in range(self.steps):
59 D = cap + alloc.sum(dim=0)
60 g = a * (D.unsqueeze(0) - alloc) / (D.unsqueeze(0).square() + 1e-8)
61 old_pay = (a * alloc / D.unsqueeze(0)).sum()
62 step = self.eta
63 proposal = project_simplex(alloc + step * g)
64 new_pay = (a * proposal / (cap + proposal.sum(dim=0)).unsqueeze(0)).sum()
65 # conservative backtracking, while retaining gradients through accepted proposal
66 for _ in range(5):
67 if new_pay.detach() >= old_pay.detach() - 1e-7: break
68 step *= 0.5
69 proposal = project_simplex(alloc + step * g)
70 new_pay = (a * proposal / (cap + proposal.sum(dim=0)).unsqueeze(0)).sum()
71 alloc = proposal
72 tok = torch.stack([e(x.reshape(-1, d)).reshape(b, l, -1) for e in self.experts], dim=2)
73 pooled = (tok * alloc[:, None, :, None]).sum(dim=2).mean(dim=1)
74 self.last_load = alloc.detach().sum(0).cpu().numpy()
75 self.last_soft_load = soft.detach().sum(0).cpu().numpy()
76 return self.head(pooled)
77
78
79def run_one(seed, lr, temperature, idea, epochs=4, return_model=False):
80 torch.manual_seed(seed); np.random.seed(seed); random.seed(seed)
81 ds = get_dataset("congestion_moe_sequence", seed, 240, 100)
82 dev = safe_device()
83 model = TinyMoE(temperature=temperature, idea=idea).to(dev)
84 xtr, ytr = torch.tensor(ds["xtr"], dtype=torch.float32, device=dev), torch.tensor(ds["ytr"], dtype=torch.float32, device=dev)
85 xte, yte = torch.tensor(ds["xte"], dtype=torch.float32, device=dev), torch.tensor(ds["yte"], dtype=torch.float32, device=dev)
86 opt = torch.optim.Adam(model.parameters(), lr=lr)
87 model.train()
88 for ep in range(epochs):
89 g = torch.Generator(device="cpu"); g.manual_seed(seed * 100 + ep)
90 ix = torch.randperm(len(xtr), generator=g, device="cpu").to(dev)
91 for start in range(0, len(ix), 128):
92 j = ix[start:start+128]
93 loss = (model(xtr[j]) - ytr[j]).square().mean()
94 opt.zero_grad(); loss.backward(); opt.step()
95 model.eval()
96 with torch.no_grad(): metric = float((model(xte) - yte).square().mean().cpu())
97 if return_model: return metric, model
98 del model
99 if dev == "cuda": torch.cuda.empty_cache()
100 return metric
101
102
103def main():
104 # Union parity: all idea learning rates are also baseline candidates; baseline's
105 # decisive temperature knob is swept as well.
106 lrs = [0.0015, 0.003, 0.006]
107 temps = [0.5, 0.7, 1.0]
108 base_grid = [{"lr": lr, "temperature": t} for lr in lrs for t in temps]
109 idea_grid = [{"lr": lr, "temperature": 0.7} for lr in lrs]
110 def base_factory(cfg): return lambda seed: run_one(seed, cfg["lr"], cfg["temperature"], False)
111 base = sweep_baseline(base_factory, base_grid)
112 # Required three idea settings, evaluated on all eight paired seeds.
113 idea_runs = []
114 for cfg in idea_grid:
115 r = evaluate(lambda s, c=cfg: run_one(s, c["lr"], c["temperature"], True))
116 idea_runs.append({"cfg": cfg, "result": r})
117 best = min(idea_runs, key=lambda q: q["result"]["mean"])
118 idea = best["result"]
119 # Signature is measured from trained systems on the same held-out task inputs.
120 sig_rows = []
121 for s in range(8):
122 bm, bmodel = run_one(s, base["best_cfg"]["lr"], base["best_cfg"]["temperature"], False, return_model=True)
123 im, imodel = run_one(s, best["cfg"]["lr"], best["cfg"]["temperature"], True, return_model=True)
124 sig_rows.append({"seed": s, "baseline_load_cv": float(np.std(bmodel.last_load)/(np.mean(bmodel.last_load)+1e-9)), "idea_load_cv": float(np.std(imodel.last_load)/(np.mean(imodel.last_load)+1e-9)), "baseline_mse": bm, "idea_mse": im})
125 base_c = base["full"]; cmp = __import__("bench").compare_results(base_c, idea)
126 observed_cv_delta = float(np.mean([r["idea_load_cv"]-r["baseline_load_cv"] for r in sig_rows]))
127 signature = {"prediction": "congestion-aware routing reduces expert-load CV while retaining a fixed row budget", "predicted_direction": "negative", "observed_load_cv_delta": observed_cv_delta, "per_seed": sig_rows, "confirmed": bool(observed_cv_delta < 0)}
128 report = make_report("congestion_moe_sequence", "tiny_moe", base, idea, {"idea_sweep": idea_runs, **signature})
129 report["custom_track"] = {"name": META["name"], "file": "custom_moe_track.py", "domain": META["domain"]}
130 report["device"] = safe_device(); report["epochs"] = 4; report["batch"] = 128
131 Path("bench_report.json").write_text(json.dumps(report, indent=2))
132 print(json.dumps(report, indent=2))
133
134if __name__ == "__main__": main()