SabaPivot's picture
download
raw
19.6 kB
"""
Independent re-derivation of the constructions behind arXiv:2605.01702 (OpenReview g89qqA6qmD).
We do not transcribe the paper's proof. We build our own floating-point sigma-networks
that satisfy the *statements*:
Lemma 3.4 : f = f* and D_{f,x}(y) = 0 (values kept, AD gradient killed)
Lemma 3.5 : f = 0 and D_{f,x}(h*(x)) = g*(x) (values killed, AD gradient arbitrary)
Theorem 3.1: f = f* and D_{f,x}(h*(x)) = g*(x) (f = f2 # f1)
for every sigma in {ReLU, ELU, GELU, Swish, Sigmoid, tanh}, in genuine IEEE-754 arithmetic.
Both halves rest on the non-associativity of floating-point summation:
wipe identity : (t (+) C) (-) C = 0 whenever |t| < ulp(C)/2 ,
while the same pair leaves an accumulator that is *reset* to 0 alone.
* put the +C/-C pair inside the AD accumulation grad(x_k) = (+)_r s_r (x) A_1[r,k]
-> the value path's gradient is annihilated, the forward pass never sees it.
* put it inside a forward accumulation -> the value is annihilated, AD never sees it.
* for the saturating activations sigma'=round(sigma_hat') underflows to exactly 0
where sigma is exactly +-1: values survive, gradients do not.
Layer-1 unit order (this order IS the summation order of the AD accumulation):
[f1 detector features][f2 detector features][P Q][f2 carriers][K1 K2]
^^^ wipes everything to its left
"""
import numpy as np
from fpnet import Act, Builder, FPNet
SAT = {"sigmoid", "tanh"}
ACT_NAMES = ["relu", "elu", "gelu", "swish", "sigmoid", "tanh"]
def acfg(name, dtype, act=None):
"""Operating points of the construction; every entry is verified numerically.
Saturating sigma: BP is the smallest power of two at which sigma is exactly
+-1 AND the rounded derivative sigma' has underflowed to exactly 0 -- this is
the paper's Condition-1 regime |sigma'(gamma)| << |sigma(gamma)|.
Non-saturating sigma: we work in the exact-identity region (sigma(t)=t, sigma'(t)=1).
"""
a = act if act is not None else Act(name, dtype)
c = dict(name=name, sat=(name in SAT), act=a, dtype=dtype)
def S(t):
return a.s(np.array([t], dtype=dtype))[0]
def Sp(t):
return a.sp(np.array([t], dtype=dtype))[0]
if name in SAT:
BP = None
for e in range(2, 200):
P = dtype(2.0) ** e
if (
S(P) == 1
and abs(S(-P)) == (0 if name == "sigmoid" else 1)
and Sp(P) == 0
and Sp(-P) == 0
):
BP = P
break
GP = None
for e in range(1, 200):
P = dtype(2.0) ** e
if S(P) == 1 and Sp(P) != 0:
GP = P
break
assert BP is not None and GP is not None, name
c["BP"] = BP
c["ONA"] = dtype(1.0)
c["gate_on_pre"] = GP
c["const_pre"] = GP
else:
c["BP"] = dtype(2.0) ** 59
c["ONA"] = dtype(2.0) ** 100
c["gate_on_pre"] = dtype(2.0) ** 80
c["const_pre"] = dtype(2.0) ** 60
for p in (c["ONA"], c["gate_on_pre"], c["const_pre"], c["BP"]):
assert S(p) == p and Sp(p) == 1, (name, p)
c["gate_off_pre"] = -c["BP"]
c["D_on"] = S(c["BP"])
c["D_off"] = S(-c["BP"])
assert Sp(-c["BP"]) == 0, name
c["const_val"] = S(c["const_pre"])
return c
def _relay(v_on, v_off, p_on, p_off, dtype):
v_on, v_off = np.float64(v_on), np.float64(v_off)
w = (np.float64(p_on) - np.float64(p_off)) / (v_on - v_off)
b = np.float64(p_on) - w * v_on
return dtype(w), dtype(b)
def _pow2_ceil(v, dtype):
if v == 0:
return dtype(1.0)
e = int(np.ceil(np.log2(float(abs(v)))))
return dtype(2.0) ** e
class Construction:
"""f = f2 # f1 for one activation / dtype / finite grid domain."""
def __init__(
self,
name,
dtype,
hp,
Z,
d=1,
mode="thm31",
n_carrier=4,
alpha_exp=-30,
margin=None,
):
self.name, self.dtype, self.hp, self.d = name, dtype, hp, d
self.cfg = acfg(name, dtype)
self.Z = np.asarray(Z, dtype=np.int64).reshape(-1, d)
self.m = self.Z.shape[0]
self.mode = mode
self.nc = n_carrier
self.alpha = dtype(2.0) ** alpha_exp
self.margin = int(np.finfo(dtype).nmant + 6) if margin is None else int(margin)
self.h = dtype(2.0) ** hp
self.X = np.asarray(self.Z, dtype=np.float64) * float(self.h)
self.X = self.X.astype(dtype)
self.use_f1 = mode in ("thm31", "lem34")
self.use_f2 = mode in ("thm31", "lem35")
# ---------------- targets ------------------------------------------
def draw_targets(
self, seed=0, fexp=(-8, 8), gexp=(-8, 8), hexp=(-4, 4), zero_frac=0.15
):
rng = np.random.default_rng(seed)
dt, m, d = self.dtype, self.m, self.d
def rf(lo, hi, size):
e = rng.integers(lo, hi + 1, size=size)
man = 1.0 + rng.integers(0, 2**20, size=size) / 2.0**20
s = rng.choice([-1.0, 1.0], size=size)
return (s * man * 2.0**e).astype(dt)
self.fstar = rf(*fexp, m)
self.hstar = rf(*hexp, m)
zero = rng.random(m) < zero_frac
self.hstar[zero] = dt(0.0)
self.gstar = rf(*gexp, (m, d)).astype(dt)
self.gstar[zero, :] = dt(0.0)
self.zero_mask = zero
return self
def set_targets(self, fstar=None, hstar=None, gstar=None):
dt = self.dtype
if fstar is not None:
self.fstar = np.asarray(fstar, dtype=dt)
if hstar is not None:
self.hstar = np.asarray(hstar, dtype=dt)
if gstar is not None:
self.gstar = np.asarray(gstar, dtype=dt).reshape(self.m, self.d)
self.zero_mask = self.hstar == 0
return self
# ---------------- skeleton -----------------------------------------
def _skeleton(self):
dt, cfg, d, m = self.dtype, self.cfg, self.d, self.m
sat = cfg["sat"]
S60 = dt(2.0) ** 60
W1 = dt(2.0) ** (60 - self.hp)
bld = Builder(d, 5)
i1 = list(range(m)) if self.use_f1 else []
i2 = list(range(m)) if self.use_f2 else []
self.i1, self.i2 = i1, i2
# ---- layer 1 ----
self.hat1 = {i: self._features(bld, i, W1, S60) for i in i1}
self.hat2 = {i: self._features(bld, i, W1, S60) for i in i2}
self.pair = None
if (not sat) and (self.use_f1 or self.use_f2):
P = bld.add(0, {k: 1.0 for k in range(d)}, float(dt(2.0) ** 100))
Q = bld.add(0, {k: 1.0 for k in range(d)}, float(dt(2.0) ** 100))
self.pair = (P, Q)
self.carr = {}
for i in i2:
self.carr[i] = [
[bld.add(0, {k: 0.0}, float(cfg["const_pre"])) for _ in range(self.nc)]
for k in range(d)
]
self.k12 = None
if self.use_f2:
self.k12 = (
bld.add(0, {}, float(cfg["const_pre"])),
bld.add(0, {}, float(cfg["const_pre"])),
)
# ---- layer 2 ----
self.sink = None
if self.pair is not None:
P, Q = self.pair
self.sink = (
bld.add(1, {P: 1.0, Q: -1.0}, float(cfg["const_pre"])),
bld.add(1, {}, float(cfg["const_pre"])),
)
self.det1 = {i: self._detector(bld, self.hat1[i], S60) for i in i1}
self.det2 = {i: self._detector(bld, self.hat2[i], S60) for i in i2}
self.wipe = {}
for i in i2:
ins = {}
for k in range(d):
for c in self.carr[i][k]:
ins[c] = float(self.alpha)
ins[self.k12[0]] = 0.0
ins[self.k12[1]] = 0.0
self.wipe[i] = bld.add(1, ins, float(cfg["const_pre"]))
# ---- layer 3 ----
self.T = None
if self.sink is not None:
s1, s2 = self.sink
self.T = (
bld.add(2, {s1: 1.0, s2: -1.0}, float(cfg["const_pre"])),
bld.add(2, {}, float(cfg["const_pre"])),
)
self.V = {}
for i in i1:
w, b = self._value_relay()
self.V[i] = bld.add(2, {self.det1[i]: float(w)}, float(b))
self.gate = {}
for i in i2:
self.gate[i] = bld.add(2, {self.det2[i]: 0.0, self.wipe[i]: 0.0}, 0.0)
# ---- layer 4 ----
self.U = None
if self.T is not None:
t1, t2 = self.T
self.U = (
bld.add(3, {t1: 1.0, t2: -1.0}, float(cfg["const_pre"])),
bld.add(3, {}, float(cfg["const_pre"])),
)
self.R = {i: bld.add(3, {self.gate[i]: 0.0}, 0.0) for i in i2}
self.kout = None
if self.use_f2:
self.kout = (
bld.add(3, {}, float(cfg["const_pre"])),
bld.add(3, {}, float(cfg["const_pre"])),
)
self.A = {i: bld.add(3, {self.V[i]: 0.0}, 0.0) for i in i1}
# ---- layer 5 (output) ----
ins = {}
if self.U is not None:
ins[self.U[0]] = 1.0
ins[self.U[1]] = -1.0
for i in i2:
ins[self.R[i]] = 0.0
if self.kout is not None:
ins[self.kout[0]] = 0.0
ins[self.kout[1]] = 0.0
for i in i1:
ins[self.A[i]] = 0.0
bld.add(4, ins, 0.0)
self.bld = bld
return bld
def _features(self, bld, i, W1, S60):
out = []
for k in range(self.d):
nz = int(self.Z[i, k])
offs = [0.5, -0.5] if self.cfg["sat"] else [1.0, 0.0, -1.0]
out.append(
[
bld.add(0, {k: float(W1)}, float((np.float64(o) - nz) * float(S60)))
for o in offs
]
)
return out
def _detector(self, bld, hats, S60):
"""detector pre-activation is +BP at x=z_i and <= -BP elsewhere."""
ins = {}
BP = float(self.cfg["BP"])
if self.cfg["sat"]:
# sigmoid: u1-u2 in {0,1}; tanh: u1-u2 in {0,2}
sc = 2.0 * BP if self.name == "sigmoid" else BP
for k in range(self.d):
ins[hats[k][0]] = sc
ins[hats[k][1]] = -sc
theta = -(2.0 * self.d - 1.0) * BP
else:
for k in range(self.d):
ins[hats[k][0]] = 1.0
ins[hats[k][1]] = -2.0
ins[hats[k][2]] = 1.0
theta = -(float(self.d) - 0.5) * float(S60)
return bld.add(1, ins, theta)
def _value_relay(self):
dt = self.dtype
return {
"sigmoid": (dt(2.0) * dt(self.cfg["BP"]), -dt(self.cfg["BP"])),
"tanh": (dt(self.cfg["BP"]) / dt(2.0), dt(self.cfg["BP"]) / dt(2.0)),
"elu": (dt(2.0) ** 41, dt(2.0) ** 41),
}.get(self.name, (dt(2.0) ** 41, dt(0.0)))
# ---------------- calibration ---------------------------------------
def build(self, verbose=False):
dt, cfg, d, m = self.dtype, self.cfg, self.d, self.m
bld = self._skeleton()
act = cfg["act"]
ONA = cfg["ONA"]
# --- f1 output weights: w5 = f*/ONA (exact: ONA is a power of two) ---
for i in self.i1:
aw, ab = {
"sigmoid": (dt(2.0) * dt(cfg["BP"]), -dt(cfg["BP"])),
"tanh": (dt(cfg["BP"]), dt(0.0)),
}.get(self.name, (dt(1.0), dt(0.0)))
bld.set_weight(3, self.A[i], self.V[i], float(aw))
bld.set_bias(3, self.A[i], float(ab))
bld.set_weight(4, 0, self.A[i], float(dt(self.fstar[i]) / ONA))
# --- f2 gate + R relays ---
for i in self.i2:
self._set_gate(bld, i)
net = bld.build(act)
if not self.use_f2:
self._calibrate_pair(bld, net)
return bld.build(act)
# --- output-weight scale so that the carrier multiplier ~ max|g*| ---
for it in range(3):
net = bld.build(act)
if self._calibrate_scale(bld, net):
break
# --- carriers (iterative, exact) ---
for it in range(8):
net = bld.build(act)
ok = self._calibrate_carriers(bld, net)
if ok:
break
# --- forward wipe constants ---
net = bld.build(act)
self._calibrate_wipes(bld, net)
net = bld.build(act)
self._calibrate_pair(bld, net)
# carriers again (pair changes nothing upstream, but be safe)
for it in range(4):
net = bld.build(act)
if self._calibrate_carriers(bld, net):
break
net = bld.build(act)
self._calibrate_wipes(bld, net)
return bld.build(act)
def _set_gate(self, bld, i):
dt, cfg = self.dtype, self.cfg
aW = dt(2.0) ** -20
wg, bg = _relay(
cfg["D_on"], cfg["D_off"], cfg["gate_on_pre"], cfg["gate_off_pre"], dt
)
bld.set_weight(2, self.gate[i], self.det2[i], float(wg))
bld.set_weight(2, self.gate[i], self.wipe[i], float(aW))
# the wipe unit is a constant sigma(const_pre); subtract its contribution
bld.set_bias(2, self.gate[i], float(dt(bg) - dt(aW) * dt(cfg["const_val"])))
# R : gate value -> {ident, 0}
g_on = cfg["act"].s(np.array([cfg["gate_on_pre"]], dtype=dt))[0]
g_off = cfg["act"].s(np.array([cfg["gate_off_pre"]], dtype=dt))[0]
p_off = dt(0.0) if self.name != "sigmoid" else -(dt(2.0) ** 59)
wr, br = _relay(g_on, g_off, cfg["const_pre"], p_off, dt)
bld.set_weight(3, self.R[i], self.gate[i], float(wr))
bld.set_bias(3, self.R[i], float(br))
bld.set_weight(4, 0, self.R[i], 1.0)
# gradient at the pre-activation of a layer-1 unit
def _grad_l1(self, net, x, gin):
net.forward(x)
_, tr = net.backward(np.array([gin], dtype=self.dtype), debug=True)
return tr[("post_act", 0)]
def _calibrate_scale(self, bld, net):
"""choose the layer-5 weight of each R_i so the AD multiplier arriving at
the carriers is ~ max_k |g*(z_i)_k| (keeps carrier weights O(1))."""
dt = self.dtype
stable = True
for i in self.i2:
hs = self.hstar[i]
if hs == 0:
continue
gl1 = self._grad_l1(net, self.X[i], hs)
mu = abs(dt(gl1[self.carr[i][0][0]]))
desired = _pow2_ceil(np.max(np.abs(self.gstar[i])), dt)
if mu == 0:
stable = False
continue
e = int(round(np.log2(float(desired) / float(mu))))
if e != 0:
stable = False
cur = dt(net.As[4][0, self.R[i]])
bld.set_weight(4, 0, self.R[i], float(cur * dt(2.0) ** e))
return stable
def _calibrate_carriers(self, bld, net):
dt, d = self.dtype, self.d
done = True
for i in self.i2:
x, hs = self.X[i], self.hstar[i]
if hs == 0:
for k in range(d):
for c in self.carr[i][k]:
bld.set_weight(0, c, k, 0.0)
continue
gl1 = self._grad_l1(net, x, hs)
for k in range(d):
mult = [dt(gl1[c]) for c in self.carr[i][k]]
target = dt(self.gstar[i, k])
if all(mu == 0 for mu in mult):
done = False
continue
ws, acc = [], dt(0.0)
for j, mu in enumerate(mult):
if mu == 0:
ws.append(dt(0.0))
continue
r = dt(target - acc) if j else target
w = dt(np.float64(r) / np.float64(mu))
ws.append(w)
acc = dt(acc + dt(mu * w))
if acc != target:
done = False
for j, c in enumerate(self.carr[i][k]):
bld.set_weight(0, c, k, float(ws[j]))
return done
def _calibrate_wipes(self, bld, net):
"""set the +C/-C constants that annihilate the forward accumulations."""
dt, cfg = self.dtype, self.cfg
vconst = dt(cfg["const_val"])
# (a) layer-2 wipe rows: kill the carrier contributions
As = net.As
worst = {i: dt(0.0) for i in self.i2}
for xi in range(self.m):
v1 = self._layer_values(net, self.X[xi], 0)
for i in self.i2:
s = dt(0.0)
for k in range(self.d):
for c in self.carr[i][k]:
s = dt(s + dt(self.alpha * dt(v1[c])))
worst[i] = max(worst[i], abs(s))
for i in self.i2:
C0 = _pow2_ceil(worst[i], dt) * (dt(2.0) ** self.margin)
w = float(dt(C0) / vconst)
bld.set_weight(1, self.wipe[i], self.k12[0], w)
bld.set_weight(1, self.wipe[i], self.k12[1], -w)
# (b) output row: kill the accumulated f2 contributions
net2 = bld.build(cfg["act"])
worst_o = dt(0.0)
for xi in range(self.m):
v4 = self._layer_values(net2, self.X[xi], 3)
s = dt(0.0)
for i in self.i2:
s = dt(s + dt(dt(net2.As[4][0, self.R[i]]) * dt(v4[self.R[i]])))
worst_o = max(worst_o, abs(s))
C1 = _pow2_ceil(worst_o, dt) * (dt(2.0) ** self.margin)
w = float(dt(C1) / vconst)
bld.set_weight(4, 0, self.kout[0], w)
bld.set_weight(4, 0, self.kout[1], -w)
def _layer_values(self, net, x, l):
net.forward(x)
return net.act.s(net._pres[l])
def _calibrate_pair(self, bld, net):
"""scale the P/Q pair so that its +C/-C annihilates the detector gradients."""
if self.pair is None:
return
dt = self.dtype
P, Q = self.pair
# with the pair's output weight at 0 the pair contributes nothing
bld.set_weight(4, 0, self.U[0], 0.0)
bld.set_weight(4, 0, self.U[1], 0.0)
base = bld.build(net.act)
worst_t, worst_s = dt(0.0), None
ratios = []
for xi in range(self.m):
for hs in set([dt(self.hstar[xi]), dt(1.0), dt(-1.0)]):
if hs == 0:
continue
base.forward(self.X[xi])
g, tr = base.backward(np.array([hs], dtype=dt), debug=True)
t = np.max(np.abs(g)) if g.size else dt(0.0)
# gradient reaching P with unit output weight
bld.set_weight(4, 0, self.U[0], 1.0)
bld.set_weight(4, 0, self.U[1], -1.0)
probe = bld.build(net.act)
probe.forward(self.X[xi])
_, tr2 = probe.backward(np.array([hs], dtype=dt), debug=True)
sP = abs(dt(tr2[("post_act", 0)][P]))
bld.set_weight(4, 0, self.U[0], 0.0)
bld.set_weight(4, 0, self.U[1], 0.0)
if sP > 0:
ratios.append(float(t) / float(sP) if sP else 0.0)
need = max(ratios) if ratios else 0.0
w5p = (
_pow2_ceil(dt(need), dt) * (dt(2.0) ** self.margin)
if need > 0
else dt(2.0) ** self.margin
)
bld.set_weight(4, 0, self.U[0], float(w5p))
bld.set_weight(4, 0, self.U[1], float(-w5p))

Xet Storage Details

Size:
19.6 kB
·
Xet hash:
b248b23036dc59167de6cd2575f5d632634cb2424cf725bf634c6370ba7c2143

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