SabaPivot's picture
download
raw
9.5 kB
"""
Floating-point neural network + automatic-differentiation simulator.
Faithful to the formalism of arXiv:2605.01702 (Park, Park, Hwang):
f(x) = rho_L o sigma o ... o sigma o rho_1 (x), rho_l(x) = A_l (x) (+) b_l
* every scalar operation is an IEEE-754 operation in the working format
* matrix-vector products are accumulated STRICTLY LEFT TO RIGHT
( (+)_{j} a_ij (x) x_j ) then (+) b_i
* the AD ("backward") gradient is
D_{f,x}(g) = g (x) A_L (x) sigma'(y_{L-1}) (x) ... (x) sigma'(y_1) (x) A_1
again evaluated strictly left to right; every row-vector-times-matrix
product is itself accumulated left to right.
Reproduction of ICML 2026 submission g89qqA6qmD. Author: SabaPivot.
"""
import numpy as np
from fractions import Fraction
# --------------------------------------------------------------------------
# activation table: sigma = round(sigma_hat), sigma' = round(sigma_hat')
# the real-valued sigma_hat / sigma_hat' are evaluated in float64 and then
# rounded to the working format (this is the paper's ceil(sigma_hat) ).
# --------------------------------------------------------------------------
def _sigmoid64(t):
t = np.asarray(t, dtype=np.float64)
out = np.empty_like(t)
pos = t >= 0
out[pos] = 1.0 / (1.0 + np.exp(-t[pos]))
e = np.exp(t[~pos])
out[~pos] = e / (1.0 + e)
return out
def _phi64(t): # standard normal cdf
from scipy.special import ndtr
return ndtr(np.asarray(t, dtype=np.float64))
def _npdf64(t):
t = np.asarray(t, dtype=np.float64)
return (
np.exp(-0.5 * np.minimum(t * t, 1500.0)) / np.sqrt(2 * np.pi) * (np.abs(t) < 40)
)
ACTS = {}
def _reg(name, s, sp):
ACTS[name] = (s, sp)
_reg(
"relu",
lambda t: np.maximum(np.asarray(t, np.float64), 0.0),
lambda t: (np.asarray(t, np.float64) > 0).astype(np.float64),
)
_reg(
"elu",
lambda t: np.where(
np.asarray(t, np.float64) > 0,
np.asarray(t, np.float64),
np.expm1(np.minimum(np.asarray(t, np.float64), 0.0)),
),
lambda t: np.where(
np.asarray(t, np.float64) > 0,
1.0,
np.exp(np.minimum(np.asarray(t, np.float64), 0.0)),
),
)
_reg(
"gelu",
lambda t: np.asarray(t, np.float64) * _phi64(t),
lambda t: _phi64(t) + np.asarray(t, np.float64) * _npdf64(t),
)
_reg(
"swish",
lambda t: np.asarray(t, np.float64) * _sigmoid64(t),
lambda t: _sigmoid64(t) * (1.0 + np.asarray(t, np.float64) * (1.0 - _sigmoid64(t))),
)
def _dsigmoid64(t):
"""accurate sigmoid' = e^{-|t|} / (1+e^{-|t|})^2 (no cancellation)."""
e = np.exp(-np.abs(np.asarray(t, np.float64)))
return e / (1.0 + e) ** 2
def _dtanh64(t):
"""accurate tanh' = sech^2 = 4 e^{-2|t|} / (1+e^{-2|t|})^2."""
e = np.exp(-2.0 * np.abs(np.asarray(t, np.float64)))
return 4.0 * e / (1.0 + e) ** 2
_reg("sigmoid", lambda t: _sigmoid64(t), _dsigmoid64)
_reg("tanh", lambda t: np.tanh(np.asarray(t, np.float64)), _dtanh64)
# framework-style derivatives: PyTorch computes sigmoid/tanh backward from the
# *output*, so sigma' underflows to 0 even earlier than the rounded true derivative.
ACTS_FRAMEWORK = dict(ACTS)
ACTS_FRAMEWORK["sigmoid"] = (
_sigmoid64,
lambda t: _sigmoid64(t) * (1.0 - _sigmoid64(t)),
)
ACTS_FRAMEWORK["tanh"] = (
lambda t: np.tanh(np.asarray(t, np.float64)),
lambda t: 1.0 - np.tanh(np.asarray(t, np.float64)) ** 2,
)
ACT_NAMES = ["relu", "elu", "gelu", "swish", "sigmoid", "tanh"]
class Act:
def __init__(self, name, dtype):
self.name = name
self.dtype = dtype
self._s, self._sp = ACTS[name]
def s(self, t):
return np.asarray(self._s(np.asarray(t, np.float64)), dtype=self.dtype)
def sp(self, t):
return np.asarray(self._sp(np.asarray(t, np.float64)), dtype=self.dtype)
# --------------------------------------------------------------------------
# network container
# --------------------------------------------------------------------------
class FPNet:
"""A floating-point sigma-network. As[l] is (d_l x d_{l-1}); bs[l] is (d_l,)."""
def __init__(self, As, bs, act):
self.As = [np.asarray(A, dtype=act.dtype) for A in As]
self.bs = [np.asarray(b, dtype=act.dtype) for b in bs]
self.act = act
self.dtype = act.dtype
self.L = len(As)
# ---- forward, strictly left-to-right accumulation -------------------
def forward(self, x, keep=True):
dt = self.dtype
v = np.asarray(x, dtype=dt)
pres = []
for l in range(self.L):
A, b = self.As[l], self.bs[l]
acc = np.zeros(A.shape[0], dtype=dt)
for k in range(A.shape[1]):
col = A[:, k]
if not col.any():
continue # 0 (x) v = 0 and acc (+) 0 = acc, exactly
acc = acc + col * v[k]
acc = acc + b
pres.append(acc if keep else None)
v = self.act.s(acc) if l < self.L - 1 else acc
self._pres = pres
return v
# ---- AD gradient, strictly left-to-right ----------------------------
def backward(self, g_out, pres=None, debug=False):
dt = self.dtype
pres = self._pres if pres is None else pres
g = np.asarray(np.atleast_1d(g_out), dtype=dt)
trace = {}
for l in range(self.L - 1, -1, -1):
A = self.As[l]
newg = np.zeros(A.shape[1], dtype=dt)
for j in range(A.shape[0]):
if g[j] == 0:
continue # exact no-op
row = A[j, :]
if not row.any():
continue
newg = newg + g[j] * row
if debug:
trace[("pre_act", l)] = newg.copy() # grad wrt output of layer l-1
if l > 0:
newg = newg * self.act.sp(pres[l - 1])
if debug:
trace[("post_act", l - 1)] = (
newg.copy()
) # grad wrt pre-act of layer l-1
g = newg
return (g, trace) if debug else g
def value_and_grad(self, x, gin):
y = self.forward(x)
g = self.backward(np.atleast_1d(np.asarray(gin, dtype=self.dtype)))
return y, g
# ---- exact-real-arithmetic reference (no rounding at all) -----------
def forward_exact(self, x, sigma_exact, sigmap_exact):
v = [Fraction(float(t)) for t in np.asarray(x, dtype=self.dtype)]
pres = []
for l in range(self.L):
A, b = self.As[l], self.bs[l]
acc = []
for j in range(A.shape[0]):
s = Fraction(0)
for k in range(A.shape[1]):
if A[j, k] != 0:
s += Fraction(float(A[j, k])) * v[k]
s += Fraction(float(b[j]))
acc.append(s)
pres.append(acc)
v = [sigma_exact(t) for t in acc] if l < self.L - 1 else acc
return v, pres
def backward_exact(self, gin, pres, sigmap_exact):
g = [Fraction(float(t)) for t in np.atleast_1d(gin)]
for l in range(self.L - 1, -1, -1):
A = self.As[l]
newg = [Fraction(0)] * A.shape[1]
for j in range(A.shape[0]):
if g[j] == 0:
continue
for k in range(A.shape[1]):
if A[j, k] != 0:
newg[k] += g[j] * Fraction(float(A[j, k]))
if l > 0:
newg = [
newg[k] * sigmap_exact(pres[l - 1][k]) for k in range(len(newg))
]
g = newg
return g
# --------------------------------------------------------------------------
# incremental builder: units are appended to layers, order is preserved and
# IS SEMANTICALLY MEANINGFUL (it fixes the left-to-right summation order).
# --------------------------------------------------------------------------
class Builder:
def __init__(self, d_in, L):
self.d_in = d_in
self.L = L
self.units = [
[] for _ in range(L)
] # units[l] = list of (dict{in_idx: w}, bias)
def add(self, l, ins, bias=0.0):
self.units[l].append((dict(ins), float(bias)))
return len(self.units[l]) - 1
def set_weight(self, l, j, k, w):
self.units[l][j][0][k] = float(w)
def set_bias(self, l, j, b):
ins, _ = self.units[l][j]
self.units[l][j] = (ins, float(b))
def width(self, l):
return len(self.units[l])
def build(self, act):
As, bs = [], []
prev = self.d_in
for l in range(self.L):
n = len(self.units[l])
A = np.zeros((n, prev), dtype=act.dtype)
b = np.zeros(n, dtype=act.dtype)
for j, (ins, bias) in enumerate(self.units[l]):
for k, w in ins.items():
A[j, k] = act.dtype(w)
b[j] = act.dtype(bias)
As.append(A)
bs.append(b)
prev = n
return FPNet(As, bs, act)
# --------------------------------------------------------------------------
# format constants
# --------------------------------------------------------------------------
def fmt(dtype):
fi = np.finfo(dtype)
return dict(
M=fi.nmant,
emax=fi.maxexp - 1,
emin=fi.minexp,
Omega=dtype(fi.max),
omega=dtype(fi.smallest_subnormal),
)

Xet Storage Details

Size:
9.5 kB
·
Xet hash:
4255b13106376040edd7cafe22e87f3fa939a979f4348c01a9a48128546567a6

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