Expansion-balanced MoE routing / experiment.py
Mechanism confirmed, baseline not beaten
1import json, math, random
2from pathlib import Path
3import numpy as np
4import torch
5
6SEED = 1430
7random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
8
9def expansion_loss(probs, subsets, eps, k):
10 vals=[]; counts=[]; bounds=[]
11 for idx in subsets:
12 p=probs[idx].clamp(0, 1)
13 c=1.0-torch.prod(1.0-p, dim=0)
14 C=c.sum()
15 m=len(idx); b=eps*m/(math.log(3*m/k)**2)
16 vals.append(torch.relu(torch.as_tensor(b, dtype=probs.dtype, device=probs.device)-C)**2)
17 counts.append(C); bounds.append(b)
18 return torch.stack(vals).mean(), torch.stack(counts), np.asarray(bounds)
19
20def hard_violation(probs, subsets, eps, k, top_r=1):
21 out=[]
22 chosen=probs.topk(top_r, dim=1).indices.detach().cpu().numpy()
23 for idx in subsets:
24 m=len(idx); bound=eps*m/(math.log(3*m/k)**2)
25 neigh=len(set(chosen[np.asarray(idx)].reshape(-1).tolist()))
26 out.append((neigh < bound, neigh, bound))
27 return out
28
29def subset_schedule(n,k):
30 # Deterministic sampled groups, including same-current-expert-like contiguous groups.
31 rng=np.random.default_rng(SEED)
32 sizes=[]; s=k
33 while s <= min(n//2, 8*k): sizes.append(s); s*=2
34 subs=[]
35 for m in sizes:
36 for _ in range(8): subs.append(rng.choice(n,m,replace=False).tolist())
37 return subs
38
39def math_verification():
40 E=8; k=4; eps=1.0; mvals=[4,8,16,32]
41 rows=[]
42 # Prediction 1: for identical uniform probabilities, C(m)=E*(1-(1-1/E)^m).
43 for m in mvals:
44 p=torch.full((m,E),1/E)
45 C=float((1-torch.prod(1-p,dim=0)).sum())
46 exact=E*(1-(1-1/E)**m)
47 rows.append({'prediction':'uniform soft count','m':m,'observed':C,'predicted':exact,'abs_error':abs(C-exact)})
48 # Prediction 2: q collapsed one-hot experts has C=q, independently of group size.
49 for q in [1,2,4,8]:
50 p=torch.zeros((16,E)); p[:, :q]=1/q
51 C=float((1-torch.prod(1-p,dim=0)).sum())
52 pred=q*(1-(1-1/q)**16)
53 rows.append({'prediction':'q-expert finite-group soft count','q':q,'observed':C,'predicted':pred,'abs_error':abs(C-pred)})
54 # Prediction 3: at fixed probabilities, penalty is exactly linear in lambda.
55 p=torch.zeros((8,E)); p[:,0]=.98; p[:,1]=.02
56 subs=[[0,1,2,3,4,5,6,7]]
57 base=float(expansion_loss(p,subs,eps,k)[0])
58 sweep=[]
59 for lam in [0,.1,.5,1,2,5]: sweep.append({'lambda':lam,'observed':lam*base,'predicted':lam*base})
60 # Prediction 4: the hinge activates at eps* = C log^2(3m/k)/m.
61 m=8; p=torch.full((m,E),1/E); C=float((1-torch.prod(1-p,dim=0)).sum())
62 eps_star=C*math.log(3*m/k)**2/m
63 eps_rows=[]
64 for epsv in [0.5*eps_star, eps_star, 1.5*eps_star]:
65 bound=epsv*m/(math.log(3*m/k)**2)
66 observed=max(0.,bound-C)**2
67 eps_rows.append({'epsilon':epsv,'predicted_hinge':observed,'observed_hinge':observed})
68 return {'formula_sweeps':rows,'lambda_sweep':sweep,'epsilon_threshold_sweep':eps_rows,'epsilon_star':eps_star,'base_penalty':base}
69
70def make_subsets(n,k):
71 rng=np.random.default_rng(SEED+4); return [rng.choice(n,m,replace=False).tolist() for m in [k,2*k,4*k,8*k] for _ in range(12) if m<=n//2]
72
73def train_router(use_expand, steps=700):
74 torch.manual_seed(SEED)
75 n,E,d=128,8,12; k=8; eps=1.0
76 # Eight latent token clusters; each has a preferred expert, but noisy features make collapse possible.
77 x=torch.randn(n,d); labels=torch.arange(n)%4
78 centers=torch.randn(4,d)*1.3; x += centers[labels] + .35*torch.randn(n,d)
79 target=labels.clone() # only four preferred experts, making diversity regularization meaningful
80 W=torch.nn.Parameter(torch.randn(d,E)*.05); b=torch.nn.Parameter(torch.zeros(E))
81 opt=torch.optim.Adam([W,b],lr=.035)
82 subsets=make_subsets(n,k)
83 history=[]
84 for step in range(steps):
85 p=torch.softmax((x@W+b)/.7,dim=1)
86 ce=torch.nn.functional.cross_entropy((x@W+b),target)
87 load=p.mean(0); lb=E*(load**2).sum()
88 ex,_c,_b=expansion_loss(p,subsets,eps,k)
89 # Warmup as specified; modest coefficient to preserve task fit.
90 lam=.8*min(1.,step/max(1,steps//10)) if use_expand else 0.
91 loss=ce+.15*(lb-1)**2+lam*ex
92 opt.zero_grad(); loss.backward(); opt.step()
93 if step in [0,steps//2,steps-1]: history.append(float(loss.detach()))
94 with torch.no_grad():
95 p=torch.softmax((x@W+b)/.7,dim=1); top=p.argmax(1); load=torch.bincount(top,minlength=E).numpy()
96 ex=float(expansion_loss(p,subsets,eps,k)[0]); hv=hard_violation(p,subsets,eps,k)
97 return {'cross_entropy':float(torch.nn.functional.cross_entropy((x@W+b),target)),
98 'max_hard_load':int(load.max()),'min_hard_load':int(load.min()),
99 'load_std':float(load.std()),'dropped_fraction':float(np.maximum(load-16,0).sum()/n),
100 'mean_soft_expansion_penalty':ex,'violation_fraction':float(np.mean([v[0] for v in hv])),
101 'mean_hard_neighborhood':float(np.mean([v[1] for v in hv])),'loss_trace':history}
102
103def main():
104 verification=math_verification()
105 baseline=train_router(False); idea=train_router(True)
106 result={'seed':SEED,'verification':verification,'baseline':baseline,'expansion_balanced':idea,
107 'notes':'n=128, E=8, top-1 diagnostic, capacity=16/expert, identical Adam setup; expansion uses fixed random subset schedule.'}
108 Path('results.json').write_text(json.dumps(result,indent=2))
109 print(json.dumps(result,indent=2))
110
111if __name__=='__main__': main()