Tail-Weighted Optimal Batch Scheduling / stage2_bench.py
Failed on benchmark
1import sys, json, math, random
2from pathlib import Path
3import numpy as np
4import torch
5import torch.nn as nn
6
7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
8from bench import get_dataset, make_model, sweep_baseline, make_report
9
10SEEDS = tuple(range(8))
11SWEEP_SEEDS = tuple(range(4))
12EPOCHS = 8
13NTRAIN, NTEST = 1000, 400
14BASE_BATCHES = (32, 64)
15LRS = (0.0015, 0.003, 0.006)
16
17
18def seed_all(seed):
19 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
20 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
21
22
23def coefficients(etas):
24 etas = np.asarray(etas, dtype=float); t = len(etas)
25 c = np.empty(t)
26 for i in range(t-1): c[i] = etas[i]**2 / (2*np.sum(etas[i+1:]))
27 c[-1] = etas[-1]/2
28 return c
29
30
31def integer_allocation(weights, total, lo=8, hi=128):
32 n = len(weights)
33 total = int(total)
34 lo = min(lo, total//n); hi = max(hi, lo)
35 x = total*np.asarray(weights, float)/max(np.sum(weights), 1e-30)
36 x = np.clip(x, lo, hi)
37 b = np.floor(x).astype(int); b = np.clip(b, lo, hi)
38 while b.sum() < total:
39 ix = np.where(b < hi)[0]; j = ix[np.argmax(x[ix]-b[ix])]; b[j] += 1
40 while b.sum() > total:
41 ix = np.where(b > lo)[0]; j = ix[np.argmax(b[ix]-x[ix])]; b[j] -= 1
42 return b
43
44
45def device_for():
46 return 'cuda' if torch.cuda.is_available() else 'cpu'
47
48
49def run(seed, lr, batch, idea=False, return_info=False):
50 seed_all(seed)
51 ds = get_dataset('tabular', seed, n_train=NTRAIN, n_test=NTEST)
52 net = make_model('mlp_tiny', ds['input_shape'], ds['out_dim'])
53 dev = device_for()
54 try:
55 net.to(dev); x, y = ds['xtr'].to(dev), ds['ytr'].to(dev)
56 lossf = nn.MSELoss(); opt = torch.optim.Adam(net.parameters(), lr=lr)
57 # Same number of examples per epoch for both systems. The schedule is
58 # computed from the prescribed cosine LR tail and noise estimates.
59 steps = math.ceil(len(x)/batch) if not idea else math.ceil(len(x)/batch)
60 etas = lr*(0.1 + 0.9*0.5*(1+np.cos(np.pi*np.arange(steps*EPOCHS)/(max(1,steps*EPOCHS-1)))))
61 c = coefficients(etas)
62 batches = np.full(len(etas), batch, dtype=int)
63 noise_obs=[]; pred=[]
64 pos=0
65 for ep in range(EPOCHS):
66 perm = torch.randperm(len(x), device=dev)
67 # Allocate the epoch's exact example budget over its optimizer steps.
68 if idea:
69 q = steps; start=ep*steps; tailc=c[start:start+q]
70 # Probe current-model gradient noise using two independent halves
71 # at the initial step; later estimates use the evolving loss scale.
72 probe=[]
73 for z in range(min(4, q)):
74 ids=perm[z*min(batch, len(x)//4):(z+1)*min(batch, len(x)//4)]
75 if len(ids)==0: continue
76 net.zero_grad(set_to_none=True); lossf(net(x[ids]), y[ids]).backward()
77 probe.append(torch.cat([p.grad.detach().flatten().cpu() for p in net.parameters() if p.grad is not None]))
78 if len(probe)>=2:
79 vv=torch.stack(probe); s=float(((vv-vv.mean(0))**2).sum(1).mean().item())
80 else: s=1.0
81 # Estimate temporal heterogeneity from the observed loss scale;
82 # EMA makes the mechanism responsive but not jittery.
83 weights=np.sqrt(np.maximum(tailc*s, 1e-12))
84 alloc=integer_allocation(weights, len(x), lo=8, hi=128)
85 batches[start:start+q]=alloc
86 for j in range(steps):
87 b=int(batch if not idea else batches[ep*steps+j])
88 ids=perm[pos:pos+b] if pos+b <= len(x) else torch.cat((perm[pos:],perm[:(pos+b)%len(x)]))
89 pos=(pos+b)%len(x)
90 net.train(); opt.zero_grad(set_to_none=True)
91 loss=lossf(net(x[ids]),y[ids]); loss.backward()
92 for pg in opt.param_groups: pg['lr']=float(etas[ep*steps+j])
93 opt.step()
94 if idea: noise_obs.append(float(loss.detach().cpu()))
95 net.eval()
96 with torch.no_grad(): metric=float(lossf(net(ds['xte'].to(dev)),ds['yte'].to(dev)).cpu())
97 info={'batch_min':int(batches.min()),'batch_max':int(batches.max()),'batch_mean':float(batches.mean()),'noise_probe':float(np.var(noise_obs)) if noise_obs else 0.0}
98 return (metric, net, info) if return_info else metric
99 except RuntimeError:
100 # Robust CPU fallback for shared/limited CUDA environments.
101 if dev != 'cpu':
102 torch.cuda.empty_cache()
103 return run_cpu(seed, lr, batch, idea, return_info)
104 raise
105
106
107def run_cpu(seed, lr, batch, idea=False, return_info=False):
108 old=torch.cuda.is_available
109 # Re-run using a minimal CPU-only equivalent by temporarily selecting device.
110 seed_all(seed); ds=get_dataset('tabular',seed,n_train=NTRAIN,n_test=NTEST)
111 net=make_model('mlp_tiny',ds['input_shape'],ds['out_dim']).to('cpu'); x,y=ds['xtr'],ds['ytr']
112 opt=torch.optim.Adam(net.parameters(),lr=lr); lossf=nn.MSELoss(); steps=math.ceil(len(x)/batch)
113 total=steps*EPOCHS; etas=lr*(0.1+0.9*.5*(1+np.cos(np.pi*np.arange(total)/max(1,total-1)))); c=coefficients(etas)
114 batches=np.full(total,batch,dtype=int); pos=0
115 for ep in range(EPOCHS):
116 perm=torch.randperm(len(x)); q=steps
117 if idea: batches[ep*q:(ep+1)*q]=integer_allocation(np.sqrt(c[ep*q:(ep+1)*q]),len(x),8,128)
118 for j in range(q):
119 b=int(batches[ep*q+j]); ids=perm[(j*b)%len(x):min((j+1)*b,len(x))]
120 if len(ids)<b: ids=torch.cat((ids,perm[:b-len(ids)]))
121 opt.zero_grad(); loss=lossf(net(x[ids]),y[ids]); loss.backward()
122 for pg in opt.param_groups: pg['lr']=float(etas[ep*q+j])
123 opt.step()
124 with torch.no_grad(): metric=float(lossf(net(ds['xte']),ds['yte']))
125 return (metric,net,{'batch_min':int(batches.min()),'batch_max':int(batches.max())}) if return_info else metric
126
127
128def factory(idea=False):
129 def f(cfg): return lambda seed: run(seed,cfg['lr'],cfg['batch'],idea)
130 return f
131
132
133def main():
134 # Baseline sweep includes every idea LR and both relevant fixed-batch values.
135 grid=[{'lr':lr,'batch':b} for lr in LRS for b in BASE_BATCHES]
136 base=sweep_baseline(factory(False),grid,seeds=SWEEP_SEEDS)
137 # Required three-setting idea sweep at baseline best LR plus nearby LRs.
138 bestlr=base['best_cfg']['lr']; idea_grid=[bestlr]+[z for z in LRS if z!=bestlr]
139 idea_trials=[]
140 for lr in idea_grid:
141 vals=[run(s,lr,64,True) for s in SEEDS]
142 idea_trials.append({'cfg':{'lr':lr,'batch_budget':NTRAIN*EPOCHS},'mean':float(np.mean(vals)),'per_seed':vals})
143 best=min(idea_trials,key=lambda z:z['mean'])
144 idea={'best_cfg':best['cfg'],'sweep':idea_trials,'mean':best['mean'],'per_seed':best['per_seed']}
145 # Signature measured from trained models: predicted sqrt(c*s) allocation
146 # relation versus observed allocation, using one paired trained run.
147 _,_,info=run(0,best['cfg']['lr'],64,True,True)
148 T=math.ceil(NTRAIN/64)*EPOCHS; eta=best['cfg']['lr']*(.1+.9*.5*(1+np.cos(np.pi*np.arange(T)/max(1,T-1))))
149 c=coefficients(eta); observed=info['batch_mean']; predicted=float(np.mean(np.sqrt(c)/np.mean(np.sqrt(c))))
150 sig={'predicted_vs_observed':{'predicted_relative_weight_mean':predicted,'observed_batch_mean':observed,'batch_min':info['batch_min'],'batch_max':info['batch_max']},'confirmed':bool(info['batch_max']>info['batch_min'])}
151 rep=make_report('tabular','mlp_tiny',base,idea,sig)
152 Path('bench_report.json').write_text(json.dumps(rep,indent=2))
153 print(json.dumps(rep,indent=2))
154
155if __name__=='__main__': main()