algorise's picture
download
raw
4.76 kB
"""Synthetic nonconvex stochastic objective used to verify the Lion convergence
theorems (Jiang & Zhang, arXiv:2508.12327).
f(x) = (1/N) sum_i phi(a_i^T x - b_i), phi(z) = z^2 / (1 + z^2)
This is the standard smooth-but-nonconvex "correntropy" / robust-regression
loss: L-smooth (bounded Hessian), bounded below (0 <= f), and its gradient is
uniformly bounded (needed for the paper's Assumption 9, used by the
communication-efficient variants). Both properties are checked in
src/sanity_checks.py.
phi'(z) = 2z / (1+z^2)^2
grad f(x) = (1/N) sum_i phi'(a_i^T x - b_i) * a_i
"""
import numpy as np
def phi(z):
return z * z / (1.0 + z * z)
def phi_prime(z):
denom = (1.0 + z * z) ** 2
return 2.0 * z / denom
def make_dataset(N, d, seed, x_star=None, feature_scale=1.0, label_noise=0.0):
"""i.i.d. Gaussian design matrix + labels generated from a "true" x_star,
with optional label noise. Returns (A, b, x_star)."""
rng = np.random.default_rng(seed)
A = rng.normal(scale=feature_scale, size=(N, d)) / np.sqrt(d)
if x_star is None:
x_star = rng.normal(size=d)
b = A @ x_star
if label_noise > 0:
b = b + rng.normal(scale=label_noise, size=N)
return A, b, x_star
def full_grad(x, A, b):
r = A @ x - b
coeff = phi_prime(r)
return (A * coeff[:, None]).mean(axis=0)
def full_value(x, A, b):
r = A @ x - b
return phi(r).mean()
def stochastic_grad(x, A, b, batch_size, rng):
"""Minibatch stochastic gradient: uniform sampling without replacement."""
N = A.shape[0]
idx = rng.integers(0, N, size=batch_size) # with replacement -> i.i.d. noise, matches Assumption 3
Ab = A[idx]
bb = b[idx]
r = Ab @ x - bb
coeff = phi_prime(r)
return (Ab * coeff[:, None]).mean(axis=0)
def make_heterogeneous_nodes(n, N_per_node, d, seed, heterogeneity=0.5, feature_scale=1.0):
"""n nodes with DIFFERENT local minimizers x_star_j = x_star_global +
heterogeneity * N(0, I) -- satisfies the paper's heterogeneous setting
(f_j allowed to differ significantly across nodes) while keeping the
global average f = (1/n) sum f_j well-defined and smooth.
Returns stacked arrays A (n, N_per_node, d) and b (n, N_per_node) so the
per-node stochastic-gradient computation in src/lion.py can be fully
vectorized across nodes (no Python-level loop over n)."""
rng = np.random.default_rng(seed)
x_star_global = rng.normal(size=d)
A = np.empty((n, N_per_node, d))
b = np.empty((n, N_per_node))
for j in range(n):
x_star_j = x_star_global + heterogeneity * rng.normal(size=d)
A_j, b_j, _ = make_dataset(N_per_node, d, seed=seed * 100003 + j,
x_star=x_star_j, feature_scale=feature_scale)
A[j], b[j] = A_j, b_j
return A, b
def full_grad_nodes(x, A, b):
"""Vectorized per-node full gradient. A: (n,N,d), b: (n,N). Returns (n,d)."""
r = np.einsum('njd,d->nj', A, x) - b
coeff = phi_prime(r)
return np.einsum('nj,njd->nd', coeff, A) / A.shape[1]
def stochastic_grad_nodes(x, A, b, idx):
"""Vectorized per-node minibatch stochastic gradient.
A: (n,N,d), b: (n,N), idx: (n,batch) per-node sample indices (int).
Returns (n,d)."""
n = A.shape[0]
rows = np.arange(n)[:, None]
Ab = A[rows, idx] # (n, batch, d)
bb = b[rows, idx] # (n, batch)
r = np.einsum('nbd,d->nb', Ab, x) - bb
coeff = phi_prime(r)
return np.einsum('nb,nbd->nd', coeff, Ab) / idx.shape[1]
def max_grad_coeff_bound():
"""max_z |phi'(z)| -- loose analytical bound on the per-SAMPLE gradient
contribution phi'(r_i), not on a gradient (mean-of-N, feature-scaled)
component -- see empirical_grad_bound for the bound that actually
matters for Assumption 9 / the S_G(.) compression radius."""
# phi'(z) = 2z/(1+z^2)^2, maximized at z = 1/sqrt(3): value = 3*sqrt(3)/8
return 3.0 * np.sqrt(3.0) / 8.0
def empirical_grad_bound(A, b, x_radius, n_samples=150, seed=0, margin=1.5):
"""sup_x ||grad f_j(x)||_inf, estimated by sampling x over a ball of the
radius actually traversed during optimization (x starts at 0 and moves
toward x_star, so x_radius ~ typical ||x_star||), with a safety margin.
A, b are the stacked per-node arrays (n,N,d) / (n,N); a single (N,d)/(N,)
pair also works (treated as one "node")."""
rng = np.random.default_rng(seed)
stacked = A.ndim == 3
d = A.shape[-1]
worst = 0.0
for _ in range(n_samples):
x = rng.normal(size=d) * rng.uniform(0, x_radius)
if stacked:
g = full_grad_nodes(x, A, b)
else:
g = full_grad(x, A, b)
worst = max(worst, np.abs(g).max())
return margin * worst

Xet Storage Details

Size:
4.76 kB
·
Xet hash:
713b8a0feda9425072f7116c9e6148cbfa220d7f47b7a5ca1ba24b84bed93354

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