algorise's picture
download
raw
4.96 kB
"""Algorithm 1 (FAB) and its single-level special case (Push-Pull / AB), exactly
as specified in the paper's Eqs. (6)-(8) and Section 3.3.
FAB (bilevel, Eqs 6-8): each agent i keeps decision vars (x_i, y_i, z_i) and
tracking vars (t_{x,i}, t_{y,i}, t_{z,i}). Per iteration k:
1. Decision update: v_i^{k+1} = sum_j a_ij^k v_j^k - eta_v^k t_{v,i}^k (v in {x,y,z})
2. Local gradients: d_{x,i}^{k+1} = grad_x L_i, d_{y,i}^{k+1} = grad_y L_i,
d_{z,i}^{k+1} = -grad_z L_i (L_i = f_i + lambda*(g_i(x,y)-g_i(x,z)))
3. Tracking update: t_{v,i}^{k+1} = sum_j b_ij^k t_{v,j}^k + d_{v,i}^{k+1} - d_{v,i}^k
Push-Pull (single-level, Sec 3.3): same scheme with only the x-block, on
f_i(x) directly (no y/z, no lambda).
"""
from __future__ import annotations
import numpy as np
def run_fab(problem, graphs, K: int, eta_x: float, eta_y: float, eta_z: float,
lam: float, seed: int = 0, noise_std: float = 0.0):
"""Runs FAB for K iterations. Returns per-iteration x_bar, and running-min
trackers for the true hypergradient norm^2 and the x/y/z consensus errors,
as in Theorem 3.4 / Proposition 3.5 (all are `min_{0<=k<K-1}` statistics).
`noise_std` adds i.i.d. Gaussian noise to each agent's local gradient
(stochastic-gradient oracle) -- without it, this deterministic smooth
instance converges geometrically fast and never exercises the theorem's
sublinear worst-case rate; persistent noise is what keeps the problem
"hard" across the whole horizon K, the same role played by the paper's
own noise level omega (RL task) / mini-batch sampling (hyper-cleaning).
"""
n, d = problem.n, problem.d
rng = np.random.default_rng(seed)
x = rng.normal(scale=0.1, size=(n, d))
y = rng.normal(scale=0.1, size=(n, d))
z = y.copy()
def local_grads(x, y, z):
dx = np.empty((n, d))
dy = np.empty((n, d))
dz = np.empty((n, d))
for i in range(n):
dx[i] = problem.grad_x_f_i(i, x[i], y[i]) + lam * (
problem.grad_x_g_i(i, x[i], y[i]) - problem.grad_x_g_i(i, x[i], z[i]))
dy[i] = problem.grad_y_f_i(i, x[i], y[i]) + lam * problem.grad_y_g_i(i, x[i], y[i])
dz[i] = lam * problem.grad_y_g_i(i, x[i], z[i]) # d_z = -grad_z[L_i] = -grad_z[-lambda g_i(x,z)]
if noise_std > 0:
dx = dx + rng.normal(scale=noise_std, size=dx.shape)
dy = dy + rng.normal(scale=noise_std, size=dy.shape)
dz = dz + rng.normal(scale=noise_std, size=dz.shape)
return dx, dy, dz
d0x, d0y, d0z = local_grads(x, y, z)
tx, ty, tz = d0x.copy(), d0y.copy(), d0z.copy()
grad_norm_hist = np.empty(K)
cons_x_hist = np.empty(K)
cons_y_hist = np.empty(K)
cons_z_hist = np.empty(K)
for k in range(K):
A, B = graphs.step()
x_new = A @ x - eta_x * tx
y_new = A @ y - eta_y * ty
z_new = A @ z - eta_z * tz
d1x, d1y, d1z = local_grads(x_new, y_new, z_new)
tx = B @ tx + d1x - d0x
ty = B @ ty + d1y - d0y
tz = B @ tz + d1z - d0z
d0x, d0y, d0z = d1x, d1y, d1z
x, y, z = x_new, y_new, z_new
x_bar = x.mean(axis=0)
grad_norm_hist[k] = float(np.sum(problem.true_hypergradient(x_bar) ** 2))
cons_x_hist[k] = float(np.mean(np.sum((x - x_bar) ** 2, axis=1)))
cons_y_hist[k] = float(np.mean(np.sum((y - y.mean(axis=0)) ** 2, axis=1)))
cons_z_hist[k] = float(np.mean(np.sum((z - z.mean(axis=0)) ** 2, axis=1)))
return {
"grad_norm_running_min": np.minimum.accumulate(grad_norm_hist),
"cons_x_running_min": np.minimum.accumulate(cons_x_hist),
"cons_y_running_min": np.minimum.accumulate(cons_y_hist),
"cons_z_running_min": np.minimum.accumulate(cons_z_hist),
"grad_norm_hist": grad_norm_hist,
}
def run_pushpull(problem, graphs, K: int, eta_x: float, seed: int = 0, noise_std: float = 0.0):
"""Single-level Push-Pull (Theorem 3.6 / Eq. 9): min_x (1/n) sum f_i(x)."""
n, d = problem.n, problem.d
rng = np.random.default_rng(seed)
x = rng.normal(scale=0.1, size=(n, d))
def grads(x):
g = np.array([problem.grad_i(i, x[i]) for i in range(n)])
if noise_std > 0:
g = g + rng.normal(scale=noise_std, size=g.shape)
return g
d0 = grads(x)
tx = d0.copy()
grad_norm_hist = np.empty(K)
cons_hist = np.empty(K)
for k in range(K):
A, B = graphs.step()
x_new = A @ x - eta_x * tx
d1 = grads(x_new)
tx = B @ tx + d1 - d0
d0 = d1
x = x_new
x_bar = x.mean(axis=0)
grad_norm_hist[k] = float(np.sum(problem.true_gradient(x_bar) ** 2))
cons_hist[k] = float(np.mean(np.sum((x - x_bar) ** 2, axis=1)))
return {
"grad_norm_running_min": np.minimum.accumulate(grad_norm_hist),
"cons_running_min": np.minimum.accumulate(cons_hist),
}

Xet Storage Details

Size:
4.96 kB
·
Xet hash:
a316526442f2c092fa7f51fa971bc1968079ba6ed3400dcb97c2b1a15e604096

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.