Critical-Batch Momentum Scaling / critical_batch_momentum.py
Mechanism failed
1import json, math, time
2from dataclasses import dataclass
3import numpy as np
4
5@dataclass
6class Controller:
7 eta0: float
8 B0: int
9 rho0: float
10 kind: str = "polyak"
11 safety: float = 0.5
12 beta: float = 1.5
13
14 def eta(self, B, rho):
15 if self.kind == "nesterov":
16 scale = self.safety * (B / self.B0) ** self.beta * (1-rho)/(1-self.rho0)
17 else:
18 scale = self.safety * (B / self.B0) * (1-rho)/(1-self.rho0)
19 return self.eta0 * min(1.0, scale)
20
21def spectral_radius(kind, eta, rho, lam=1.0):
22 if kind == "polyak":
23 A = np.array([[1-eta*lam, -eta*rho], [lam, rho]], float)
24 elif kind == "nesterov":
25 A = np.array([[1-eta*lam, -eta*rho*(1-eta*lam)],
26 [lam, rho*(1-eta*lam)]], float)
27 else:
28 return abs(1-eta*lam)
29 return float(np.max(np.abs(np.linalg.eigvals(A))))
30
31def critical_eta(kind, rho, lam=1.0):
32 lo, hi = 0.0, 1.0 / lam
33 while spectral_radius(kind, hi, rho, lam) < 1.0:
34 hi *= 2
35 for _ in range(70):
36 mid = (lo + hi) / 2
37 if spectral_radius(kind, mid, rho, lam) < 1.0:
38 lo = mid
39 else:
40 hi = mid
41 return lo
42
43def stochastic_run(kind, B, eta, rho, seed, steps=400, d=8, noise=1.0):
44 rng = np.random.default_rng(seed)
45 x = rng.normal(size=d)
46 v = np.zeros(d)
47 losses = []
48 updates = []
49 for _ in range(steps):
50 if kind == "nesterov":
51 grad = x - eta * rho * v + rng.normal(size=d) * noise / math.sqrt(B)
52 else:
53 grad = x + rng.normal(size=d) * noise / math.sqrt(B)
54 old = x.copy()
55 v = rho * v + grad
56 x = x - eta * v
57 losses.append(0.5 * float(np.dot(x, x)))
58 updates.append(float(np.linalg.norm(x-old)))
59 if not np.isfinite(losses[-1]) or losses[-1] > 1e12:
60 return {"stable": False, "final_loss": float("inf"), "median_loss": float("inf"),
61 "update": float("inf"), "steps": len(losses)}
62 return {"stable": bool(np.isfinite(losses).all()),
63 "final_loss": losses[-1], "median_loss": float(np.median(losses[-50:])),
64 "update": float(np.median(updates[-50:])), "steps": steps}
65
66def scan_stability(kind, B, rho, eta_grid, repeats=3):
67 out=[]
68 for eta in eta_grid:
69 runs=[stochastic_run(kind,B,eta,rho,100+r,steps=300) for r in range(repeats)]
70 out.append((eta, all(r["stable"] for r in runs)))
71 stable=[e for e, ok in out if ok]
72 return max(stable) if stable else 0.0
73
74def main():
75 rho0=0.9; B0=1; eta0=0.05; beta=1.5
76 # Core math: compare critical-rate ratios to the predicted capped scaling.
77 rho_values=[0.0,0.5,0.9,0.95]
78 math_rows=[]
79 for kind in ["polyak","nesterov"]:
80 base=critical_eta(kind,rho0)
81 for rho in rho_values:
82 c=critical_eta(kind,rho)
83 predicted=min(1.0, (1-rho)/(1-rho0))
84 math_rows.append({"kind":kind,"rho":rho,"critical":c,
85 "ratio":c/base,"predicted_uncapped_ratio":predicted})
86 # Batch controller experiment with fixed number of samples (steps shrink as B grows).
87 rows=[]
88 for kind in ["polyak","nesterov"]:
89 ctrl=Controller(eta0,B0,rho0,kind,0.5,beta)
90 for B in [1,2,4,8,16]:
91 eta=ctrl.eta(B,rho0)
92 fixed=stochastic_run(kind,B,eta0,rho0,123,steps=max(20,800//B))
93 controlled=stochastic_run(kind,B,eta,rho0,123,steps=max(20,800//B))
94 rows.append({"kind":kind,"B":B,"eta_controller":eta,
95 "fixed_stable":fixed["stable"],"controller_stable":controlled["stable"],
96 "fixed_final_loss":fixed["final_loss"],"controller_final_loss":controlled["final_loss"],
97 "controller_update":controlled["update"]})
98 # Empirical maximum stable rates at each batch, plus theoretical deterministic boundary.
99 scan=[]
100 for kind in ["polyak","nesterov"]:
101 for B in [1,2,4,8,16]:
102 grid=np.geomspace(0.002,2.0,45)
103 empirical=scan_stability(kind,B,rho0,grid)
104 scan.append({"kind":kind,"B":B,"empirical_max_eta":empirical,
105 "deterministic_quadratic_eta":critical_eta(kind,rho0)})
106 result={"math_check":math_rows,"controller_runs":rows,"stability_scan":scan,
107 "config":{"eta0":eta0,"rho0":rho0,"beta":beta,"safety":0.5}}
108 with open("results.json","w") as f: json.dump(result,f,indent=2)
109 print(json.dumps(result,indent=2))
110
111if __name__ == "__main__":
112 main()