Certified dual-price MoE routing / stage2_bench.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_report
  8from bench.protocol import permutation_pvalue
  9
 10SEEDS = list(range(8))
 11LRS = [1e-3, 3e-3, 1e-2]
 12RHOS = [0.001, 0.01, 0.1]
 13E = 4
 14EPOCHS = 18
 15BATCH = 64
 16BALANCE_WEIGHTS = [0.0, 0.01, 0.1]
 17
 18class RoutedExperts(nn.Module):
 19    def __init__(self, d=4, experts=E, width=48):
 20        super().__init__()
 21        self.gate = nn.Sequential(nn.Linear(d, width), nn.ReLU(), nn.Linear(width, experts))
 22        self.experts = nn.ModuleList([
 23            nn.Sequential(nn.Linear(d, width), nn.Tanh(), nn.Linear(width, 1))
 24            for _ in range(experts)])
 25
 26    def utilities(self, x):
 27        return self.gate(x)
 28
 29    def expert_outputs(self, x):
 30        return torch.cat([m(x) for m in self.experts], dim=1)
 31
 32
 33def seed_all(seed):
 34    random.seed(seed)
 35    np.random.seed(seed)
 36    torch.manual_seed(seed)
 37    if torch.cuda.is_available():
 38        torch.cuda.manual_seed_all(seed)
 39
 40
 41def train_one(ds, seed, lr, method, rho=0.001, balance_weight=0.01):
 42    seed_all(seed)
 43    device = 'cuda' if torch.cuda.is_available() else 'cpu'
 44    try:
 45        return _train(ds, seed, lr, method, rho, balance_weight, device)
 46    except RuntimeError:
 47        return _train(ds, seed, lr, method, rho, balance_weight, 'cpu')
 48
 49
 50def _train(ds, seed, lr, method, rho, balance_weight, device):
 51    net = RoutedExperts(ds['xtr'].shape[1]).to(device)
 52    opt = torch.optim.Adam(net.parameters(), lr=lr)
 53    xtr, ytr = ds['xtr'].to(device), ds['ytr'].to(device)
 54    prices = torch.zeros(E, device=device)
 55    hist = []
 56    for _ in range(EPOCHS):
 57        net.train()
 58        perm = torch.randperm(len(xtr), device=device)
 59        total = 0.0
 60        for start in range(0, len(xtr), BATCH):
 61            ix = perm[start:start+BATCH]
 62            x, y = xtr[ix], ytr[ix]
 63            u = net.utilities(x)
 64            route = (u - prices[None, :]).argmax(1) if method == 'dual' else u.argmax(1)
 65            vals = net.expert_outputs(x)
 66            pred = vals.gather(1, route[:, None]).squeeze(1)
 67            mse = ((pred - y.squeeze(1)) ** 2).mean()
 68            counts = torch.bincount(route, minlength=E).float()
 69            cap = max(1.0, len(ix) / E)
 70            balance = ((counts / len(ix) - 1.0/E) ** 2).mean()
 71            loss = mse + balance_weight * balance
 72            opt.zero_grad(); loss.backward(); opt.step()
 73            if method == 'dual':
 74                prices = torch.clamp(prices + rho * (counts.detach() - cap), min=0.0)
 75            total += float(mse.detach()) * len(ix)
 76        hist.append(total / len(xtr))
 77    net.eval()
 78    with torch.no_grad():
 79        xt, yt = ds['xte'].to(device), ds['yte'].to(device)
 80        u = net.utilities(xt)
 81        route = (u - prices[None, :]).argmax(1) if method == 'dual' else u.argmax(1)
 82        vals = net.expert_outputs(xt)
 83        pred = vals.gather(1, route[:, None]).squeeze(1)
 84        metric = float(((pred - yt.squeeze(1)) ** 2).mean().cpu())
 85        counts = torch.bincount(route, minlength=E).float()
 86        # Certificate measured on trained router utilities and feasible acceptance.
 87        test_cap = max(1, int(np.ceil(len(xt) / E)))
 88        keep = torch.zeros(len(xt), dtype=torch.bool, device=device)
 89        for e in range(E):
 90            ids = torch.where(route == e)[0]
 91            if len(ids):
 92                take = ids[torch.argsort(u[ids, e], descending=True)[:test_cap]]
 93                keep[take] = True
 94        L = float((prices * test_cap).sum() + (u - prices[None, :]).amax(1).sum())
 95        P = float(u[torch.arange(len(xt), device=device)[keep], route[keep]].sum())
 96        gap = L - P
 97        excess = counts - test_cap
 98        price_excess_corr = float(torch.corrcoef(torch.stack([prices.cpu(), excess.cpu()]))[0, 1]) if torch.std(prices) > 0 and torch.std(excess) > 0 else 0.0
 99    return {'metric': metric, 'history': hist, 'counts': counts.cpu().tolist(), 'prices': prices.cpu().tolist(), 'dual_gap': gap, 'price_excess_corr': price_excess_corr, 'model': net}
100
101
102def aggregate(records):
103    return {'per_seed': [float(r['metric']) for r in records], 'mean': float(np.mean([r['metric'] for r in records])), 'std': float(np.std([r['metric'] for r in records], ddof=1)), 'details': [{k:v for k,v in r.items() if k != 'model'} for r in records]}
104
105
106def baseline_sweep(datasets):
107    tried=[]
108    for lr in LRS:
109        for bw in BALANCE_WEIGHTS:
110            rs=[train_one(datasets[s],s,lr,'baseline',balance_weight=bw) for s in SEEDS[:4]]
111            tried.append({'cfg':{'lr':lr,'balance_weight':bw},'mean':float(np.mean([r['metric'] for r in rs]))})
112    best=min(tried,key=lambda z:z['mean'])
113    cfg=best['cfg']
114    full=aggregate([train_one(datasets[s],s,cfg['lr'],'baseline',balance_weight=cfg['balance_weight']) for s in SEEDS])
115    return {'best_cfg':cfg,'sweep':tried,'full':full}
116
117
118def main():
119    track='router_regime_regression'
120    datasets={s:get_dataset(track,s,n_train=400,n_test=400) for s in SEEDS}
121    base=baseline_sweep(datasets)
122    # Idea grid includes baseline-best lr and two nearby rho settings; all lrs are in baseline grid.
123    configs=[{'lr':base['best_cfg']['lr'],'rho':r} for r in RHOS]
124    idea_runs=[]
125    for cfg in configs:
126        rs=[train_one(datasets[s],s,cfg['lr'],'dual',rho=cfg['rho'],balance_weight=0.0) for s in SEEDS]
127        idea_runs.append({'cfg':cfg,'result':aggregate(rs)})
128    best=min(idea_runs,key=lambda z:z['result']['mean'])
129    br=base['full']['per_seed']; ir=best['result']['per_seed']
130    diffs=[a-b for a,b in zip(ir,br)]
131    # Signature is from trained-model behaviour, not an analytical toy identity.
132    sig=best['result']['details']
133    signature={'prediction':'positive expert prices should correspond to positive realized load excess and reduce load imbalance','trained_price_excess_correlations':[x['price_excess_corr'] for x in sig],'trained_dual_gaps':[x['dual_gap'] for x in sig],'mean_abs_dual_gap':float(np.mean(np.abs([x['dual_gap'] for x in sig]))),'confirmed':bool(np.mean([x['price_excess_corr'] for x in sig])>0.1 and np.mean(np.abs([x['dual_gap'] for x in sig]))>=-1e-6)}
134    report=make_report(track,'custom_routed_experts',base,best['result'],{'mechanism_signature':signature,'idea_sweep':idea_runs,'custom_track':{'name':'router_regime_regression','file':'bench/custom_tracks/router_regime_regression.py','domain':'moe-routing'}})
135    report['idea']['best_cfg']=best['cfg']
136    report['paired_raw_diffs']=diffs
137    with open('bench_report.json','w') as f: json.dump(report,f,indent=2)
138    print(json.dumps(report,indent=2))
139
140if __name__=='__main__': main()