Pipelined bounded-staleness gradient coding / run_bench.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import os, sys, json, copy
  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_model, sweep_baseline, evaluate, make_report
  8
  9SEEDS = tuple(range(8))
 10SWEEP_SEEDS = (0, 1, 2, 3)
 11EPOCHS = 12
 12BATCH = 128
 13NTRAIN = 800
 14NTEST = 400
 15NPART = 8
 16
 17
 18def seed_all(seed):
 19    np.random.seed(seed)
 20    torch.manual_seed(seed)
 21    if torch.cuda.is_available():
 22        torch.cuda.manual_seed_all(seed)
 23
 24
 25def device_try():
 26    return torch.device("cuda" if torch.cuda.is_available() else "cpu")
 27
 28
 29def metric(net, ds, dev):
 30    net.eval()
 31    with torch.no_grad():
 32        z = net(ds["xte"].to(dev))
 33        return float(((z - ds["yte"].to(dev)) ** 2).mean().item())
 34
 35
 36def clone_state(net):
 37    return {k: v.detach().clone() for k, v in net.state_dict().items()}
 38
 39
 40def run(seed, lr, stale_window=None, collect=False):
 41    """Train partition-wise SGD. stale_window=None is synchronous baseline.
 42    For the idea, partition i uses version max(0,t-delay_i), delay_i < c.
 43    This is the bounded-staleness coded aggregate, with the same model/data/
 44    architecture and one full partition gradient per partition per update.
 45    """
 46    seed_all(seed)
 47    ds = get_dataset("tabular", seed, n_train=NTRAIN, n_test=NTEST)
 48    dev = device_try()
 49    try:
 50        net = make_model("mlp_tiny", ds["input_shape"], ds["out_dim"]).to(dev)
 51        x, y = ds["xtr"].to(dev), ds["ytr"].to(dev)
 52        idx_parts = [a.to(dev) for a in torch.tensor_split(torch.arange(len(x), device=dev), NPART)]
 53        lossf = nn.MSELoss()
 54        opt = torch.optim.SGD(net.parameters(), lr=lr)
 55        history = [clone_state(net)]
 56        err_vals, bound_vals, correlations, displacements, ages = [], [], [], [], []
 57        # A fixed heterogeneous replica-completion pattern. Replication means
 58        # every shard has a completed result; the bounded age is the only
 59        # intervention, not a change in model or objective.
 60        rng = np.random.default_rng(seed + 9173)
 61        delays = rng.integers(0, stale_window, size=(EPOCHS * (len(x) // BATCH + 1), NPART)) if stale_window else None
 62        steps = 0
 63        for ep in range(EPOCHS):
 64            # fixed permutation is shared conceptually; full partition gradients
 65            # make each optimizer update exactly comparable across variants.
 66            for t in range(NPART):
 67                current = clone_state(net)
 68                grads = []
 69                current_grads = []
 70                used_ages = []
 71                for i, ids in enumerate(idx_parts):
 72                    if stale_window is None:
 73                        v = len(history) - 1
 74                    else:
 75                        d = int(delays[steps % len(delays), i])
 76                        v = max(0, len(history) - 1 - d)
 77                    net.load_state_dict(history[v], strict=True)
 78                    net.zero_grad(set_to_none=True)
 79                    loss = lossf(net(x[ids]), y[ids])
 80                    loss.backward()
 81                    g = [p.grad.detach().clone() for p in net.parameters()]
 82                    grads.append(g)
 83                    used_ages.append((len(history) - 1) - v)
 84                    if stale_window is not None:
 85                        net.load_state_dict(current, strict=True)
 86                        net.zero_grad(set_to_none=True)
 87                        loss_now = lossf(net(x[ids]), y[ids])
 88                        loss_now.backward()
 89                        current_grads.append([p.grad.detach().clone() for p in net.parameters()])
 90                net.load_state_dict(current, strict=True)
 91                opt.zero_grad(set_to_none=True)
 92                for j, p in enumerate(net.parameters()):
 93                    p.grad = torch.stack([g[j] for g in grads]).mean(0)
 94                if stale_window is not None:
 95                    flat_used = torch.cat([g[j].reshape(-1) for g in grads for j in range(len(g))])
 96                    flat_now = torch.cat([g[j].reshape(-1) for g in current_grads for j in range(len(g))])
 97                    diff = flat_now - flat_used
 98                    err_vals.append(float(diff.square().mean().item()))
 99                    # Local Lipschitz estimate and the paper's L^2 displacement proxy.
100                    old_flat = torch.cat([history[max(0, len(history)-1-max(used_ages))][k].reshape(-1).to(dev) for k in current])
101                    cur_flat = torch.cat([current[k].reshape(-1).to(dev) for k in current])
102                    disp = float((cur_flat - old_flat).square().mean().item())
103                    displacements.append(disp)
104                    lhat = float(torch.linalg.vector_norm(diff) / (torch.linalg.vector_norm(cur_flat-old_flat)+1e-8))
105                    bound_vals.append(lhat*lhat*disp)
106                    correlations.append(float(torch.nn.functional.cosine_similarity(flat_now, flat_used, dim=0).item()))
107                    ages.extend(used_ages)
108                opt.step()
109                history.append(clone_state(net))
110                steps += 1
111        out = metric(net, ds, dev)
112        sig = None
113        if stale_window is not None and err_vals:
114            sig = {"predicted": float(np.mean(bound_vals)),
115                   "observed": float(np.mean(err_vals)),
116                   "observed_over_predicted": float(np.mean(err_vals)/(np.mean(bound_vals)+1e-12)),
117                   "mean_gradient_cosine": float(np.mean(correlations)),
118                   "mean_displacement_sq": float(np.mean(displacements)),
119                   "mean_age": float(np.mean(ages)), "max_age": int(max(ages)),
120                   "confirmed": bool(0.05 <= np.mean(err_vals)/(np.mean(bound_vals)+1e-12) <= 20.0)}
121        return out, sig
122    except RuntimeError:
123        # Explicit CPU fallback required for shared/limited CUDA environments.
124        dev = torch.device("cpu")
125        seed_all(seed)
126        ds = get_dataset("tabular", seed, n_train=NTRAIN, n_test=NTEST)
127        # retry once on CPU, preserving exactly the same algorithm
128        old = torch.cuda.is_available
129        torch.cuda.is_available = lambda: False
130        try:
131            return run(seed, lr, stale_window, collect)
132        finally:
133            torch.cuda.is_available = old
134
135
136def baseline_factory(cfg):
137    return lambda seed: run(seed, float(cfg["lr"]), None)[0]
138
139
140def idea_factory(cfg):
141    return lambda seed: run(seed, float(cfg["lr"]), int(cfg["c"]))[0]
142
143
144def main():
145    # Union parity: every idea learning rate is in the baseline sweep.
146    grid = [{"lr": 0.003}, {"lr": 0.01}, {"lr": 0.03}]
147    base = sweep_baseline(baseline_factory, grid, seeds=SWEEP_SEEDS)
148    idea_cfgs = [{"lr": float(base["best_cfg"]["lr"]), "c": 2},
149                 {"lr": 0.003, "c": 2}, {"lr": 0.03, "c": 2}]
150    idea_trials = []
151    for cfg in idea_cfgs:
152        r = evaluate(idea_factory(cfg), seeds=SEEDS)
153        idea_trials.append({"cfg": cfg, "result": r})
154    best = min(idea_trials, key=lambda z: z["result"]["mean"])
155    idea = best["result"]
156    sigs = [run(s, best["cfg"]["lr"], best["cfg"]["c"])[1] for s in SEEDS]
157    keys = ["predicted", "observed", "observed_over_predicted", "mean_gradient_cosine", "mean_displacement_sq", "mean_age", "max_age"]
158    signature = {k: float(np.mean([q[k] for q in sigs])) for k in keys if k != "max_age"}
159    signature["max_age"] = int(max(q["max_age"] for q in sigs))
160    signature["confirmed"] = bool(all(q["confirmed"] for q in sigs))
161    signature["definition"] = "trained tabular MLP: stale-vs-current partition gradient MSE compared with local L_hat^2 parameter displacement"
162    rep = make_report("tabular", "mlp_tiny", base, idea,
163                      {"predicted": signature["predicted"], "observed": signature["observed"], "confirmed": signature["confirmed"], "details": signature})
164    rep["idea_trials"] = idea_trials
165    rep["custom_track"] = None
166    with open("bench_report.json", "w") as f:
167        json.dump(rep, f, indent=2)
168    print(json.dumps(rep, indent=2))
169
170if __name__ == "__main__":
171    main()