Delay-Robust Slow Consensus Optimizer / delay_consensus_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json
  2import math
  3import numpy as np
  4from scipy.optimize import brentq
  5
  6SEED = 2029
  7np.random.seed(SEED)
  8
  9
 10def ring_adj(n):
 11    A = np.zeros((n, n))
 12    for i in range(n):
 13        A[i, (i - 1) % n] = 1.0
 14        A[i, (i + 1) % n] = 1.0
 15    return A
 16
 17
 18def complete_adj(n):
 19    A = np.ones((n, n)) - np.eye(n)
 20    return A
 21
 22
 23def delayed_linear(A, eps, k, delay_steps, h, steps, x0=None, targets=None):
 24    n = len(A)
 25    d = A.sum(1)
 26    x = np.ones(n) if x0 is None else np.asarray(x0, float).copy()
 27    targets = np.zeros(n) if targets is None else np.asarray(targets, float)
 28    # Constant prehistory is the standard well-posed history for this test.
 29    hist = [x.copy() for _ in range(delay_steps + 1)]
 30    xs = np.empty((steps + 1, n)); xs[0] = x
 31    for t in range(steps):
 32        stale = hist[0]
 33        v = -eps * (x - targets) + k * (A @ stale - d * x)
 34        x = x + h * v
 35        hist.pop(0); hist.append(x.copy())
 36        xs[t + 1] = x
 37    return xs
 38
 39
 40def rate_from_trace(trace, h, lo, hi):
 41    y = np.abs(trace[lo:hi])
 42    t = np.arange(lo, hi) * h
 43    slope = np.polyfit(t, np.log(np.maximum(y, 1e-14)), 1)[0]
 44    return float(-slope)
 45
 46
 47def exact_slow_rate(eps, kd, tau):
 48    if tau == 0: return eps
 49    # q is the positive decay rate of q=eps+kd*(1-exp(-q*tau)).
 50    # For x(t)=exp(-q t), q=eps+kd*(1-exp(q*tau)); the
 51    # principal real decay root lies between 0 and eps.
 52    f = lambda q: q - eps - kd * (1.0 - math.exp(q * tau))
 53    return brentq(f, 0.0, eps)
 54
 55
 56def main():
 57    h = 0.001
 58    n = 4
 59    A = ring_adj(n); degree = 2.0
 60    eps = 0.2
 61    # Prediction 1: slow collective rate scales as 1/(1+k*d*tau).
 62    speed_rows = []
 63    for k in [0.25, 0.5, 1.0, 2.0]:
 64        for tau in [0.0, 0.25, 0.5, 1.0]:
 65            delay_steps = round(tau / h)
 66            tr = delayed_linear(A, eps, k, delay_steps, h, 12000)
 67            # discard transient, and use mean (consensus) mode
 68            q = rate_from_trace(tr.mean(1), h, 4000, 11000)
 69            approx = eps / (1 + k * degree * tau)
 70            exact = exact_slow_rate(eps, k * degree, tau)
 71            speed_rows.append(dict(k=k, tau=tau, observed=q, approx=approx, exact=exact,
 72                                   approx_rel_error=abs(q-approx)/approx))
 73
 74    # Prediction 2: the relevant dimensionless synchronization ratio is
 75    # rho=eps*||J||/(k*lambda_2); disagreement should rise around rho ~ 1.
 76    # Heterogeneous quadratic minima create a persistent disagreement signal.
 77    targets = np.array([-1., 1., -1., 1.])
 78    lam2 = 2.0  # ring of four
 79    sync_rows = []
 80    for k in [0.05, 0.1, 0.2, 0.4, 0.8]:
 81        tr = delayed_linear(A, eps, k, 0, h, 10000, x0=np.zeros(n), targets=targets)
 82        tail = tr[-1000:]
 83        disagreement = np.mean(np.sum((tail - tail.mean(1, keepdims=True))**2, axis=1))
 84        rho = eps / (k * lam2)
 85        sync_rows.append(dict(k=k, rho=rho, disagreement=disagreement))
 86
 87    # Prediction 3: with no local field, disagreement decay is set by k*lambda_2.
 88    gap_rows = []
 89    for name, B, gap in [('ring', A, 2.0), ('complete', complete_adj(n), 4.0)]:
 90        for k in [0.1, 0.2, 0.4]:
 91            x0 = np.array([1., -1., 0.5, 0.2])
 92            tr = delayed_linear(B, 0.0, k, 0, h, 5000, x0=x0)
 93            dis = np.sqrt(np.mean((tr - tr.mean(1, keepdims=True))**2, axis=1))
 94            q = rate_from_trace(dis, h, 500, 4000)
 95            gap_rows.append(dict(graph=name, k=k, lambda2=gap, observed_rate=q,
 96                                 predicted_rate=k*gap))
 97
 98    # Small optimizer-facing comparison: four workers minimize local quadratics.
 99    # Independent SGD is the standard local baseline; delayed diffusion is the idea.
100    opt = {}
101    for name, delay in [('local', None), ('delayed_consensus', 5)]:
102        x = np.zeros(n); hist = [x.copy()] * (delay + 1 if delay is not None else 1)
103        for _ in range(3000):
104            if delay is None:
105                x += 0.01 * (-eps * (x - targets))  # gradient descent on 0.5*(x-target)^2
106            else:
107                stale = hist[0]
108                x += 0.01 * (-0.2 * (x-targets) + 0.8 * (A@stale-degree*x))
109                hist.pop(0); hist.append(x.copy())
110        obj = float(np.mean(0.5*(x-targets)**2))
111        dis = float(np.mean((x-x.mean())**2))
112        opt[name] = {'final_mean_local_objective': obj, 'final_disagreement': dis}
113
114    # Aggregate checks deliberately distinguish the first-order approximation
115    # from the exact characteristic-root prediction.
116    mean_approx_err = float(np.mean([r['approx_rel_error'] for r in speed_rows if r['tau'] > 0]))
117    mean_exact_err = float(np.mean([abs(r['observed']-r['exact'])/r['exact'] for r in speed_rows if r['tau'] > 0]))
118    # A useful mechanism check: rate decreases monotonically with k*tau.
119    monotone = all(speed_rows[i]['observed'] >= speed_rows[i+1]['observed'] - 1e-4
120                   for i in range(len(speed_rows)-1) if speed_rows[i]['k'] == speed_rows[i+1]['k'])
121    out = {'seed': SEED, 'step_h': h, 'speed_scaling': speed_rows,
122           'synchronization_ratio_sweep': sync_rows, 'spectral_gap_sweep': gap_rows,
123           'optimizer_comparison': opt,
124           'summary': {'mean_first_order_relative_error': mean_approx_err,
125                       'mean_exact_root_relative_error': mean_exact_err,
126                       'speed_monotone_in_delay': monotone,
127                       'rho_one_disagreement_at': min(sync_rows, key=lambda r: abs(r['rho']-1))}}
128    with open('results.json', 'w') as f: json.dump(out, f, indent=2)
129    print(json.dumps(out['summary'], indent=2))
130
131if __name__ == '__main__': main()