Tail-Weighted Optimal Batch Scheduling / tail_batch_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json
  2import math
  3from pathlib import Path
  4import numpy as np
  5
  6
  7def tail_coefficients(eta):
  8    eta = np.asarray(eta, dtype=float)
  9    tau = len(eta)
 10    c = np.empty(tau)
 11    for t in range(tau - 1):
 12        c[t] = eta[t] ** 2 / (2.0 * np.sum(eta[t + 1:]))
 13    c[-1] = eta[-1] / 2.0
 14    return c
 15
 16
 17def continuous_batches(c, s, budget):
 18    r = np.sqrt(np.maximum(c * s, 1e-30))
 19    return budget * r / r.sum()
 20
 21
 22def integer_batches(weights, budget, bmin=1, bmax=None):
 23    """Allocate integer examples, preserving budget and bounds as closely as possible."""
 24    n = len(weights)
 25    if bmax is None:
 26        bmax = budget
 27    if budget < n * bmin or budget > n * bmax:
 28        raise ValueError("infeasible bounds")
 29    x = budget * np.asarray(weights, dtype=float) / np.sum(weights)
 30    x = np.clip(x, bmin, bmax)
 31    # Iterative largest-remainder allocation with bounds.
 32    b = np.floor(x).astype(int)
 33    b = np.maximum(b, bmin)
 34    b = np.minimum(b, bmax)
 35    while b.sum() < budget:
 36        candidates = np.where(b < bmax)[0]
 37        score = x[candidates] - b[candidates]
 38        b[candidates[np.argmax(score)]] += 1
 39    while b.sum() > budget:
 40        candidates = np.where(b > bmin)[0]
 41        score = b[candidates] - x[candidates]
 42        b[candidates[np.argmax(score)]] -= 1
 43    return b
 44
 45
 46def objective(b, c, s):
 47    return float(np.sum(np.asarray(c) * np.asarray(s) / np.asarray(b)))
 48
 49
 50def cosine_lr(tau, eta_max=0.12, eta_min=0.012):
 51    x = np.arange(tau) / max(1, tau - 1)
 52    return eta_min + 0.5 * (eta_max - eta_min) * (1 + np.cos(np.pi * x))
 53
 54
 55def verify_math(seed=7):
 56    rng = np.random.default_rng(seed)
 57    tau = 32
 58    eta = cosine_lr(tau)
 59    c = tail_coefficients(eta)
 60    s = np.exp(rng.normal(0, 0.8, tau))
 61    C = 32 * 16
 62    b_star = continuous_batches(c, s, C)
 63    b_int = integer_batches(np.sqrt(c * s), C, 1, 64)
 64    uniform = np.full(tau, C / tau)
 65    # Predictions: (P1) ratios match sqrt(c*s); (P2) continuous optimum has the
 66    # Cauchy-Schwarz value (sum sqrt(cs))^2/C; (P3) multiplying all noise by k
 67    # scales batches by sqrt(k), while relative allocations do not change.
 68    ratio_err = np.max(np.abs((b_star / b_star[0]) / (np.sqrt(c*s) / np.sqrt(c[0]*s[0])) - 1))
 69    predicted = np.sum(np.sqrt(c * s)) ** 2 / C
 70    opt_gap = objective(b_star, c, s) / predicted - 1
 71    scales = np.array([0.25, 1.0, 4.0, 16.0])
 72    scale_rows = []
 73    for k in scales:
 74        bk = continuous_batches(c, k*s, C)
 75        # Relative allocation is predicted invariant; because C is fixed, absolute
 76        # batches are also invariant. The meaningful scaling prediction is objective.
 77        scale_rows.append({"noise_multiplier": float(k),
 78                           "objective_ratio_observed": objective(bk, c, k*s) / objective(b_star, c, s),
 79                           "objective_ratio_predicted": float(k),
 80                           "allocation_relative_max_error": float(np.max(np.abs(bk/bk.sum()-b_star/b_star.sum())))})
 81    # Tail sweep: for constant noise, compare first/last coefficient under cosine
 82    # horizons. The reported quantity is directly predicted by c_t formula.
 83    tail_rows = []
 84    for T in [8, 16, 32, 64, 128]:
 85        et = cosine_lr(T)
 86        ct = tail_coefficients(et)
 87        bt = continuous_batches(ct, np.ones(T), T * 16)
 88        tail_rows.append({"horizon": T,
 89                          "c_first_over_c_last": float(ct[0]/ct[-1]),
 90                          "batch_first_over_last": float(bt[0]/bt[-1]),
 91                          "prediction_error": float(abs(bt[0]/bt[-1] - math.sqrt(ct[0]/ct[-1])))})
 92    return {"P1_ratio_max_abs_error": float(ratio_err),
 93            "P2_optimality_relative_error": float(opt_gap),
 94            "P3_noise_scale_sweep": scale_rows,
 95            "tail_weight_sweep": tail_rows,
 96            "integer_objective_over_continuous": objective(b_int,c,s)/objective(b_star,c,s),
 97            "eta": eta.tolist(), "c": c.tolist(), "s": s.tolist(),
 98            "continuous_batches": b_star.tolist(), "integer_batches": b_int.tolist()}
 99
100
101def run_sgd(seed, schedule, eta, s_profile, dim=8):
102    rng = np.random.default_rng(seed)
103    # Strongly convex quadratic; gradient noise has E||noise||^2 approximately s_t/B.
104    lam = 0.5
105    w = rng.normal(0, 1, dim)
106    target = np.zeros(dim)
107    losses = []
108    for t, b in enumerate(schedule):
109        true_g = lam * (w - target)
110        noise = rng.normal(0, math.sqrt(s_profile[t] / b / dim), dim)
111        w = w - eta[t] * (true_g + noise)
112        losses.append(0.5 * lam * float(np.dot(w, w)))
113    return np.asarray(losses)
114
115
116def mini_experiment(seed=19):
117    tau, per_step = 64, 16
118    C = tau * per_step
119    eta = cosine_lr(tau, 0.16, 0.016)
120    c = tail_coefficients(eta)
121    # Noise falls during optimization but remains deliberately heterogeneous.
122    s = 1.0 + 8.0 * np.exp(-np.arange(tau) / 18.0)
123    idea = integer_batches(np.sqrt(c*s), C, bmin=4, bmax=48)
124    static = np.full(tau, per_step, dtype=int)
125    # Common hand-designed comparator: linear growth, same total examples.
126    linear_weights = np.linspace(0.5, 1.5, tau)
127    linear = integer_batches(linear_weights, C, bmin=4, bmax=48)
128    all_results = {}
129    for name, sched in [("static", static), ("linear_growth", linear), ("tail_weighted", idea)]:
130        curves = np.array([run_sgd(seed+i, sched, eta, s) for i in range(40)])
131        all_results[name] = {"final_loss_mean": float(curves[:, -1].mean()),
132                             "final_loss_se": float(curves[:, -1].std(ddof=1)/math.sqrt(len(curves))),
133                             "loss_at_step_32": float(curves[:,31].mean()),
134                             "batch_min": int(sched.min()), "batch_max": int(sched.max()),
135                             "batch_first": int(sched[0]), "batch_last": int(sched[-1])}
136    all_results["theoretical_batch_objectives"] = {
137        "static": objective(static,c,s), "linear_growth": objective(linear,c,s), "tail_weighted": objective(idea,c,s)}
138    all_results["eta"] = eta.tolist()
139    all_results["c"] = c.tolist()
140    all_results["s_profile"] = s.tolist()
141    return all_results
142
143
144if __name__ == "__main__":
145    out = {"verification": verify_math(), "mini_experiment": mini_experiment()}
146    Path("results.json").write_text(json.dumps(out, indent=2))
147    print(json.dumps({"verification": out["verification"], "mini_experiment": out["mini_experiment"]}, indent=2))