fluidlayer 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fluidlayer/__init__.py +96 -0
- fluidlayer/autodiff.py +472 -0
- fluidlayer/blocks.py +275 -0
- fluidlayer/bpe.py +173 -0
- fluidlayer/data.py +92 -0
- fluidlayer/diffusion.py +264 -0
- fluidlayer/gpt2_model.py +129 -0
- fluidlayer/gpt2_tokenizer.py +127 -0
- fluidlayer/graft.py +741 -0
- fluidlayer/losses.py +86 -0
- fluidlayer/memory.py +525 -0
- fluidlayer/model.py +141 -0
- fluidlayer/optim.py +57 -0
- fluidlayer/py.typed +0 -0
- fluidlayer/sheaf.py +147 -0
- fluidlayer/topology.py +294 -0
- fluidlayer/torch_graft.py +283 -0
- fluidlayer-2.0.0.dist-info/METADATA +376 -0
- fluidlayer-2.0.0.dist-info/RECORD +22 -0
- fluidlayer-2.0.0.dist-info/WHEEL +5 -0
- fluidlayer-2.0.0.dist-info/licenses/LICENSE +21 -0
- fluidlayer-2.0.0.dist-info/top_level.txt +1 -0
fluidlayer/__init__.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fluidlayer — a topological "fluid" architecture for sequence models.
|
|
3
|
+
Core idea
|
|
4
|
+
Replace the transformer's dense attention with a cellular sheaf diffusion:
|
|
5
|
+
X(t) = exp(-t L_F) X(0), L_F = delta^T delta (sheaf Laplacian)
|
|
6
|
+
The fluid "freezes" the data's geometry onto the space of global sections
|
|
7
|
+
(the Hodge limit). A multi-scale band filter bank recovers the transient,
|
|
8
|
+
a top-k route head restores sharp retrieval, and a single pointwise
|
|
9
|
+
nonlinearity keeps the model from collapsing to a linear map.
|
|
10
|
+
Exact token IDs live outside the diffused state. The attachable `SAABranch`
|
|
11
|
+
uses a bounded, majority-tracking exact store alongside its constant-size
|
|
12
|
+
fluid state. The low-level `FrozenRegister` helper separately provides an
|
|
13
|
+
append-only 20-byte slot format with explicit serialization. These logical
|
|
14
|
+
slot sizes exclude Python object overhead in the attachable reference store.
|
|
15
|
+
"""
|
|
16
|
+
from .autodiff import Tensor, concat, softmax_rows, softplus
|
|
17
|
+
from .blocks import FluidLayerBlock, RMSNorm
|
|
18
|
+
from .diffusion import (
|
|
19
|
+
apply_bands,
|
|
20
|
+
band_energies,
|
|
21
|
+
band_times,
|
|
22
|
+
cheb_basis,
|
|
23
|
+
cheb_coeffs_exp,
|
|
24
|
+
cheb_degree,
|
|
25
|
+
exact_exp_action,
|
|
26
|
+
jam_time,
|
|
27
|
+
power_iteration_lmax,
|
|
28
|
+
spectral_gap,
|
|
29
|
+
spectral_summary,
|
|
30
|
+
)
|
|
31
|
+
from .graft import GRAFT_STATE_BYTES, SAABranch, ctx_hash
|
|
32
|
+
from .losses import (
|
|
33
|
+
band_decorrelation,
|
|
34
|
+
cross_entropy,
|
|
35
|
+
cross_entropy_grad,
|
|
36
|
+
cross_entropy_tensor,
|
|
37
|
+
)
|
|
38
|
+
from .memory import (
|
|
39
|
+
SLOT_BYTES,
|
|
40
|
+
FrozenRegister,
|
|
41
|
+
SheafMemoryAdapter,
|
|
42
|
+
budget_split,
|
|
43
|
+
ctx_key,
|
|
44
|
+
embedding_signature,
|
|
45
|
+
fnv1a64,
|
|
46
|
+
logit_override,
|
|
47
|
+
)
|
|
48
|
+
from .model import FluidLayerLM, TinyTransformer
|
|
49
|
+
from .sheaf import (
|
|
50
|
+
balance_residual,
|
|
51
|
+
build_laplacian,
|
|
52
|
+
component_balance,
|
|
53
|
+
is_balanced,
|
|
54
|
+
laplacian_from_dense_A,
|
|
55
|
+
sheaf_energy,
|
|
56
|
+
)
|
|
57
|
+
from .topology import (
|
|
58
|
+
betti_numbers,
|
|
59
|
+
chain_edges,
|
|
60
|
+
connected_components,
|
|
61
|
+
edges_from_topk,
|
|
62
|
+
hodge_edge_participation,
|
|
63
|
+
hodge_embedding,
|
|
64
|
+
hodge_relevance,
|
|
65
|
+
kruskal_barcode,
|
|
66
|
+
local_density,
|
|
67
|
+
persistence_landmarks,
|
|
68
|
+
route_topk,
|
|
69
|
+
topo_descriptor,
|
|
70
|
+
unique_edges,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
__version__ = "2.0.0"
|
|
74
|
+
version = __version__
|
|
75
|
+
|
|
76
|
+
__all__ = [
|
|
77
|
+
"SAABranch", "GRAFT_STATE_BYTES", "ctx_hash",
|
|
78
|
+
"Tensor", "concat", "softmax_rows", "softplus",
|
|
79
|
+
"build_laplacian", "sheaf_energy", "balance_residual", "is_balanced",
|
|
80
|
+
"laplacian_from_dense_A", "component_balance",
|
|
81
|
+
"power_iteration_lmax", "cheb_coeffs_exp", "cheb_basis",
|
|
82
|
+
"apply_bands", "band_energies", "spectral_gap", "jam_time",
|
|
83
|
+
"cheb_degree", "exact_exp_action", "spectral_summary",
|
|
84
|
+
"band_times",
|
|
85
|
+
"route_topk", "edges_from_topk", "chain_edges", "unique_edges",
|
|
86
|
+
"kruskal_barcode", "betti_numbers", "topo_descriptor",
|
|
87
|
+
"connected_components",
|
|
88
|
+
"hodge_relevance", "hodge_embedding", "hodge_edge_participation",
|
|
89
|
+
"local_density", "persistence_landmarks",
|
|
90
|
+
"FluidLayerBlock", "RMSNorm", "FluidLayerLM", "TinyTransformer",
|
|
91
|
+
"cross_entropy", "cross_entropy_grad", "cross_entropy_tensor",
|
|
92
|
+
"band_decorrelation",
|
|
93
|
+
# --- the exact tier -------------------------------------------------
|
|
94
|
+
"FrozenRegister", "SheafMemoryAdapter", "logit_override", "budget_split",
|
|
95
|
+
"ctx_key", "fnv1a64", "embedding_signature", "SLOT_BYTES",
|
|
96
|
+
]
|
fluidlayer/autodiff.py
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fluidlayer.autodiff — a tiny reverse-mode automatic differentiation engine.
|
|
3
|
+
|
|
4
|
+
Pure NumPy, ~1 op set, enough to train the FluidLayer stack end-to-end inside
|
|
5
|
+
this sandbox (no torch available). Every op has a hand-written backward and
|
|
6
|
+
is verified against finite differences in `selftest.py`.
|
|
7
|
+
|
|
8
|
+
Design: each Tensor holds `data`, `grad`, its parent Tensors and a closure
|
|
9
|
+
`_backward` that pushes gradient contributions into the parents. The graph is
|
|
10
|
+
rebuilt every forward pass; parameter leaves accumulate `.grad` across the
|
|
11
|
+
traversal (zero them between steps).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
_SQRT_PI = np.sqrt(np.pi)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _unbroadcast(g: np.ndarray, shape: tuple) -> np.ndarray:
|
|
22
|
+
"""Reduce a broadcasted gradient back to `shape`."""
|
|
23
|
+
if g.shape == shape:
|
|
24
|
+
return g
|
|
25
|
+
# sum over broadcast axes
|
|
26
|
+
while g.ndim > len(shape):
|
|
27
|
+
g = g.sum(axis=0)
|
|
28
|
+
for i, s in enumerate(shape):
|
|
29
|
+
if s == 1 and g.shape[i] != 1:
|
|
30
|
+
g = g.sum(axis=i, keepdims=True)
|
|
31
|
+
return g.reshape(shape)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Tensor:
|
|
35
|
+
__slots__ = ("data", "grad", "parents", "op", "_backward", "requires_grad")
|
|
36
|
+
|
|
37
|
+
def __init__(self, data, parents=(), op="", requires_grad=True):
|
|
38
|
+
self.data = np.array(data, dtype=np.float64, copy=True)
|
|
39
|
+
self.grad = np.zeros_like(self.data)
|
|
40
|
+
self.parents = tuple(parents)
|
|
41
|
+
self.op = op
|
|
42
|
+
self.requires_grad = requires_grad
|
|
43
|
+
self._backward = None
|
|
44
|
+
|
|
45
|
+
# -------------------------------------------------------------- plumbing
|
|
46
|
+
@property
|
|
47
|
+
def shape(self):
|
|
48
|
+
return self.data.shape
|
|
49
|
+
|
|
50
|
+
def _apply(self, data, op, parents, backward):
|
|
51
|
+
out = Tensor(data, parents=parents, op=op)
|
|
52
|
+
out._backward = backward
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
def __repr__(self):
|
|
56
|
+
return f"Tensor(shape={self.data.shape}, op={self.op})"
|
|
57
|
+
|
|
58
|
+
# ------------------------------------------------------------ arithmetic
|
|
59
|
+
def __add__(self, other):
|
|
60
|
+
o = other if isinstance(other, Tensor) else Tensor(other, requires_grad=False)
|
|
61
|
+
out = self._apply(self.data + o.data, "+", (self, o), lambda: None)
|
|
62
|
+
|
|
63
|
+
def bw():
|
|
64
|
+
g = out.grad
|
|
65
|
+
if self.requires_grad:
|
|
66
|
+
self.grad += _unbroadcast(g, self.data.shape)
|
|
67
|
+
if o.requires_grad:
|
|
68
|
+
o.grad += _unbroadcast(g, o.data.shape)
|
|
69
|
+
out._backward = bw
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
__radd__ = __add__
|
|
73
|
+
|
|
74
|
+
def __sub__(self, other):
|
|
75
|
+
o = other if isinstance(other, Tensor) else Tensor(other, requires_grad=False)
|
|
76
|
+
neg = o.data
|
|
77
|
+
out = self._apply(self.data - neg, "-", (self, o), lambda: None)
|
|
78
|
+
|
|
79
|
+
def bw():
|
|
80
|
+
g = out.grad
|
|
81
|
+
if self.requires_grad:
|
|
82
|
+
self.grad += _unbroadcast(g, self.data.shape)
|
|
83
|
+
if o.requires_grad:
|
|
84
|
+
o.grad += _unbroadcast(-g, o.data.shape)
|
|
85
|
+
out._backward = bw
|
|
86
|
+
return out
|
|
87
|
+
|
|
88
|
+
def __rsub__(self, other):
|
|
89
|
+
o = other if isinstance(other, Tensor) else Tensor(other, requires_grad=False)
|
|
90
|
+
return o - self
|
|
91
|
+
|
|
92
|
+
def __mul__(self, other):
|
|
93
|
+
o = other if isinstance(other, Tensor) else Tensor(other, requires_grad=False)
|
|
94
|
+
out = self._apply(self.data * o.data, "*", (self, o), lambda: None)
|
|
95
|
+
|
|
96
|
+
def bw():
|
|
97
|
+
g = out.grad
|
|
98
|
+
if self.requires_grad:
|
|
99
|
+
self.grad += _unbroadcast(g * o.data, self.data.shape)
|
|
100
|
+
if o.requires_grad:
|
|
101
|
+
o.grad += _unbroadcast(g * self.data, o.data.shape)
|
|
102
|
+
out._backward = bw
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
__rmul__ = __mul__
|
|
106
|
+
|
|
107
|
+
def __truediv__(self, other):
|
|
108
|
+
o = other if isinstance(other, Tensor) else Tensor(other, requires_grad=False)
|
|
109
|
+
out = self._apply(self.data / o.data, "/", (self, o), lambda: None)
|
|
110
|
+
|
|
111
|
+
def bw():
|
|
112
|
+
g = out.grad
|
|
113
|
+
if self.requires_grad:
|
|
114
|
+
self.grad += _unbroadcast(g / o.data, self.data.shape)
|
|
115
|
+
if o.requires_grad:
|
|
116
|
+
o.grad += _unbroadcast(-g * self.data / (o.data ** 2), o.data.shape)
|
|
117
|
+
out._backward = bw
|
|
118
|
+
return out
|
|
119
|
+
|
|
120
|
+
def __rtruediv__(self, other):
|
|
121
|
+
o = other if isinstance(other, Tensor) else Tensor(other, requires_grad=False)
|
|
122
|
+
return o / self
|
|
123
|
+
|
|
124
|
+
def __neg__(self):
|
|
125
|
+
return self * -1.0
|
|
126
|
+
|
|
127
|
+
def __pow__(self, p):
|
|
128
|
+
p = float(p)
|
|
129
|
+
out = self._apply(self.data ** p, "pow", (self,), lambda: None)
|
|
130
|
+
|
|
131
|
+
def bw():
|
|
132
|
+
if self.requires_grad:
|
|
133
|
+
self.grad += out.grad * p * (self.data ** (p - 1))
|
|
134
|
+
out._backward = bw
|
|
135
|
+
return out
|
|
136
|
+
|
|
137
|
+
def __matmul__(self, other):
|
|
138
|
+
o = other if isinstance(other, Tensor) else Tensor(other, requires_grad=False)
|
|
139
|
+
out = self._apply(self.data @ o.data, "@", (self, o), lambda: None)
|
|
140
|
+
|
|
141
|
+
def bw():
|
|
142
|
+
g = out.grad
|
|
143
|
+
if self.requires_grad:
|
|
144
|
+
self.grad += g @ o.data.T
|
|
145
|
+
if o.requires_grad:
|
|
146
|
+
o.grad += self.data.T @ g
|
|
147
|
+
out._backward = bw
|
|
148
|
+
return out
|
|
149
|
+
|
|
150
|
+
# --------------------------------------------------------------- shapes
|
|
151
|
+
def reshape(self, *shape):
|
|
152
|
+
if len(shape) == 1 and isinstance(shape[0], (tuple, list)):
|
|
153
|
+
shape = tuple(shape[0])
|
|
154
|
+
old = self.data.shape
|
|
155
|
+
out = self._apply(self.data.reshape(shape), "reshape", (self,), lambda: None)
|
|
156
|
+
|
|
157
|
+
def bw():
|
|
158
|
+
self.grad += out.grad.reshape(old)
|
|
159
|
+
out._backward = bw
|
|
160
|
+
return out
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def T(self):
|
|
164
|
+
return self.transpose()
|
|
165
|
+
|
|
166
|
+
def transpose(self):
|
|
167
|
+
out = self._apply(self.data.T, "T", (self,), lambda: None)
|
|
168
|
+
|
|
169
|
+
def bw():
|
|
170
|
+
self.grad += out.grad.T
|
|
171
|
+
out._backward = bw
|
|
172
|
+
return out
|
|
173
|
+
|
|
174
|
+
def __getitem__(self, key):
|
|
175
|
+
out = self._apply(self.data[key], "getitem", (self,), lambda: None)
|
|
176
|
+
|
|
177
|
+
def bw():
|
|
178
|
+
g = out.grad
|
|
179
|
+
src = np.zeros_like(self.data)
|
|
180
|
+
k = key
|
|
181
|
+
# --- strip newaxis (None) entries: they only add output axes ---
|
|
182
|
+
if isinstance(k, tuple):
|
|
183
|
+
pos = [i for i, kk in enumerate(k) if kk is None]
|
|
184
|
+
for p in reversed(pos):
|
|
185
|
+
g = np.squeeze(g, axis=p)
|
|
186
|
+
k = tuple(kk for kk in k if kk is not None)
|
|
187
|
+
if len(k) == 1:
|
|
188
|
+
k = k[0]
|
|
189
|
+
if isinstance(k, tuple):
|
|
190
|
+
has_arr = any(isinstance(kk, np.ndarray) for kk in k)
|
|
191
|
+
if not has_arr:
|
|
192
|
+
src[k] += g # pure basic indexing
|
|
193
|
+
else:
|
|
194
|
+
arrs = [kk for kk in k if isinstance(kk, np.ndarray)]
|
|
195
|
+
bshape = np.broadcast_shapes(*[kk.shape for kk in arrs])
|
|
196
|
+
idx_arrays = []
|
|
197
|
+
for _, kk in enumerate(k):
|
|
198
|
+
if isinstance(kk, np.ndarray):
|
|
199
|
+
idx_arrays.append(np.broadcast_to(kk, bshape))
|
|
200
|
+
else:
|
|
201
|
+
idx_arrays.append(kk) # slices/ints kept as-is
|
|
202
|
+
np.add.at(src, tuple(idx_arrays), g)
|
|
203
|
+
elif isinstance(k, np.ndarray):
|
|
204
|
+
np.add.at(src, k, g)
|
|
205
|
+
elif k is None: # x[None]
|
|
206
|
+
src[...] = g.reshape(self.data.shape)
|
|
207
|
+
else:
|
|
208
|
+
src[k] += g
|
|
209
|
+
self.grad += src
|
|
210
|
+
out._backward = bw
|
|
211
|
+
return out
|
|
212
|
+
|
|
213
|
+
def gather_rows(self, idx):
|
|
214
|
+
"""out[i, j] = self[idx[i, j]] (self is (n, d), idx is (n, k) -> (n, k, d))"""
|
|
215
|
+
out = self._apply(
|
|
216
|
+
self.data[idx], "gather_rows", (self,), lambda: None
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def bw():
|
|
220
|
+
np.add.at(self.grad, idx, out.grad)
|
|
221
|
+
out._backward = bw
|
|
222
|
+
return out
|
|
223
|
+
|
|
224
|
+
# ------------------------------------------------------------ reductions
|
|
225
|
+
def sum(self, axis=None, keepdims=False):
|
|
226
|
+
out = self._apply(
|
|
227
|
+
self.data.sum(axis=axis, keepdims=keepdims),
|
|
228
|
+
"sum",
|
|
229
|
+
(self,),
|
|
230
|
+
lambda: None,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def bw():
|
|
234
|
+
g = out.grad
|
|
235
|
+
if axis is not None and not keepdims:
|
|
236
|
+
g = np.expand_dims(g, axis)
|
|
237
|
+
self.grad += np.broadcast_to(g, self.data.shape).copy()
|
|
238
|
+
out._backward = bw
|
|
239
|
+
return out
|
|
240
|
+
|
|
241
|
+
def max(self, axis=None, keepdims=False):
|
|
242
|
+
am = np.argmax(self.data, axis=axis, keepdims=True)
|
|
243
|
+
out = self._apply(
|
|
244
|
+
np.max(self.data, axis=axis, keepdims=keepdims),
|
|
245
|
+
"max",
|
|
246
|
+
(self,),
|
|
247
|
+
lambda: None,
|
|
248
|
+
)
|
|
249
|
+
mask = np.zeros_like(self.data, dtype=bool)
|
|
250
|
+
np.put_along_axis(mask, am, True, axis=axis)
|
|
251
|
+
|
|
252
|
+
def bw():
|
|
253
|
+
g = out.grad
|
|
254
|
+
if axis is not None and not keepdims:
|
|
255
|
+
g = np.expand_dims(g, axis)
|
|
256
|
+
self.grad += g * mask
|
|
257
|
+
out._backward = bw
|
|
258
|
+
return out
|
|
259
|
+
|
|
260
|
+
def mean(self, axis=None, keepdims=False):
|
|
261
|
+
n = self.data.size if axis is None else self.data.shape[axis]
|
|
262
|
+
s = self.sum(axis=axis, keepdims=keepdims)
|
|
263
|
+
return s * (1.0 / n) # do NOT override s._backward: the mul
|
|
264
|
+
# backward already routes out.grad -> s.grad
|
|
265
|
+
|
|
266
|
+
def square(self):
|
|
267
|
+
return self * self
|
|
268
|
+
|
|
269
|
+
def abs(self):
|
|
270
|
+
out = self._apply(np.abs(self.data), "abs", (self,), lambda: None)
|
|
271
|
+
|
|
272
|
+
def bw():
|
|
273
|
+
self.grad += out.grad * np.sign(self.data)
|
|
274
|
+
out._backward = bw
|
|
275
|
+
return out
|
|
276
|
+
|
|
277
|
+
def exp(self):
|
|
278
|
+
out = self._apply(np.exp(self.data), "exp", (self,), lambda: None)
|
|
279
|
+
|
|
280
|
+
def bw():
|
|
281
|
+
self.grad += out.grad * out.data
|
|
282
|
+
out._backward = bw
|
|
283
|
+
return out
|
|
284
|
+
|
|
285
|
+
def log(self):
|
|
286
|
+
out = self._apply(np.log(self.data), "log", (self,), lambda: None)
|
|
287
|
+
|
|
288
|
+
def bw():
|
|
289
|
+
self.grad += out.grad / self.data
|
|
290
|
+
out._backward = bw
|
|
291
|
+
return out
|
|
292
|
+
|
|
293
|
+
def sqrt(self):
|
|
294
|
+
out = self._apply(np.sqrt(self.data), "sqrt", (self,), lambda: None)
|
|
295
|
+
|
|
296
|
+
def bw():
|
|
297
|
+
self.grad += out.grad * 0.5 / out.data
|
|
298
|
+
out._backward = bw
|
|
299
|
+
return out
|
|
300
|
+
|
|
301
|
+
def tanh(self):
|
|
302
|
+
out = self._apply(np.tanh(self.data), "tanh", (self,), lambda: None)
|
|
303
|
+
|
|
304
|
+
def bw():
|
|
305
|
+
self.grad += out.grad * (1 - out.data ** 2)
|
|
306
|
+
out._backward = bw
|
|
307
|
+
return out
|
|
308
|
+
|
|
309
|
+
def erf(self):
|
|
310
|
+
out = self._apply(0.5 * (1 + _erf_np(self.data)), "erf", (self,), lambda: None)
|
|
311
|
+
|
|
312
|
+
def bw():
|
|
313
|
+
self.grad += out.grad * (2.0 / _SQRT_PI) * np.exp(-self.data ** 2)
|
|
314
|
+
out._backward = bw
|
|
315
|
+
return out
|
|
316
|
+
|
|
317
|
+
def relu(self):
|
|
318
|
+
out = self._apply(np.maximum(self.data, 0.0), "relu", (self,), lambda: None)
|
|
319
|
+
|
|
320
|
+
def bw():
|
|
321
|
+
self.grad += out.grad * (self.data > 0)
|
|
322
|
+
out._backward = bw
|
|
323
|
+
return out
|
|
324
|
+
|
|
325
|
+
def gelu(self):
|
|
326
|
+
out = self._apply(
|
|
327
|
+
0.5 * self.data * (1 + _erf_np(self.data / np.sqrt(2.0))),
|
|
328
|
+
"gelu",
|
|
329
|
+
(self,),
|
|
330
|
+
lambda: None,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def bw():
|
|
334
|
+
x = self.data
|
|
335
|
+
cdf = 0.5 * (1 + _erf_np(x / np.sqrt(2.0)))
|
|
336
|
+
pdf = np.exp(-0.5 * x ** 2) / np.sqrt(2 * np.pi)
|
|
337
|
+
self.grad += out.grad * (cdf + x * pdf)
|
|
338
|
+
out._backward = bw
|
|
339
|
+
return out
|
|
340
|
+
|
|
341
|
+
# --------------------------------------------------------------- scatter
|
|
342
|
+
def index_add(self, idx, src):
|
|
343
|
+
"""out = zeros_like(self.data) with src scattered into rows idx.
|
|
344
|
+
|
|
345
|
+
The scatter MUST happen in the forward pass too -- an earlier version
|
|
346
|
+
returned zeros here, which silently turned every Laplacian built with
|
|
347
|
+
this op into the zero matrix (and the whole fluid into a no-op).
|
|
348
|
+
"""
|
|
349
|
+
out_data = np.zeros_like(self.data)
|
|
350
|
+
np.add.at(out_data, idx, src.data)
|
|
351
|
+
out = self._apply(out_data, "index_add", (self, src), lambda: None)
|
|
352
|
+
|
|
353
|
+
def bw():
|
|
354
|
+
# out = base + scatter(src, idx) => d/d(src) = out.grad[idx]
|
|
355
|
+
src.grad += out.grad[idx]
|
|
356
|
+
self.grad += out.grad # d/d(base): base is the zero base
|
|
357
|
+
out._backward = bw
|
|
358
|
+
return out
|
|
359
|
+
|
|
360
|
+
def gather(self, idx):
|
|
361
|
+
"""out[i] = self[idx[i]]"""
|
|
362
|
+
out = self._apply(self.data[idx], "gather", (self,), lambda: None)
|
|
363
|
+
|
|
364
|
+
def bw():
|
|
365
|
+
src = np.zeros_like(self.data)
|
|
366
|
+
np.add.at(src, idx, out.grad)
|
|
367
|
+
self.grad += src
|
|
368
|
+
out._backward = bw
|
|
369
|
+
return out
|
|
370
|
+
|
|
371
|
+
# ----------------------------------------------------------------- misc
|
|
372
|
+
def pow(self, p):
|
|
373
|
+
return self ** float(p)
|
|
374
|
+
|
|
375
|
+
def backward(self, seed=None):
|
|
376
|
+
"""Reverse-mode backward pass.
|
|
377
|
+
|
|
378
|
+
If `seed` is given it overwrites the output gradient. Otherwise the
|
|
379
|
+
output gradient is seeded with ones *only if it is still all zeros*
|
|
380
|
+
(so an externally supplied gradient, e.g. dCE/dlogits, is respected).
|
|
381
|
+
"""
|
|
382
|
+
if seed is not None:
|
|
383
|
+
self.grad = np.array(seed, dtype=np.float64, copy=True)
|
|
384
|
+
elif not np.any(self.grad):
|
|
385
|
+
self.grad = np.ones_like(self.data)
|
|
386
|
+
topo, seen = [], set()
|
|
387
|
+
|
|
388
|
+
def build(t):
|
|
389
|
+
if id(t) in seen:
|
|
390
|
+
return
|
|
391
|
+
seen.add(id(t))
|
|
392
|
+
for p in t.parents:
|
|
393
|
+
build(p)
|
|
394
|
+
topo.append(t)
|
|
395
|
+
|
|
396
|
+
build(self)
|
|
397
|
+
for t in reversed(topo):
|
|
398
|
+
if t._backward is not None:
|
|
399
|
+
t._backward()
|
|
400
|
+
|
|
401
|
+
def zero_grad(self):
|
|
402
|
+
self.grad = np.zeros_like(self.data)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def softplus(t: Tensor) -> Tensor:
|
|
406
|
+
"""log(1 + exp(t)), numerically stable for large |t|."""
|
|
407
|
+
return (1.0 + t.exp()).log()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _erf_np(x: np.ndarray) -> np.ndarray:
|
|
411
|
+
"""Vectorised erf (Abramowitz & Stegun 7.1.26 is too weak; use tanh form)."""
|
|
412
|
+
sign = np.sign(x)
|
|
413
|
+
ax = np.abs(x)
|
|
414
|
+
p = 0.3275911
|
|
415
|
+
a1, a2, a3, a4, a5 = (
|
|
416
|
+
0.254829592,
|
|
417
|
+
-0.284496736,
|
|
418
|
+
1.421413741,
|
|
419
|
+
-1.453152027,
|
|
420
|
+
1.061405429,
|
|
421
|
+
)
|
|
422
|
+
t = 1.0 / (1.0 + p * ax)
|
|
423
|
+
y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * np.exp(-ax * ax)
|
|
424
|
+
return sign * y
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def concat(tensors, axis=0):
|
|
428
|
+
datas = [t.data for t in tensors]
|
|
429
|
+
out = Tensor(np.concatenate(datas, axis=axis), parents=tuple(tensors), op="concat")
|
|
430
|
+
|
|
431
|
+
def bw():
|
|
432
|
+
sizes = [d.shape[axis] for d in datas]
|
|
433
|
+
idx = 0
|
|
434
|
+
for t, s in zip(tensors, sizes):
|
|
435
|
+
sl = [slice(None)] * out.data.ndim
|
|
436
|
+
sl[axis] = slice(idx, idx + s)
|
|
437
|
+
t.grad += out.grad[tuple(sl)]
|
|
438
|
+
idx += s
|
|
439
|
+
out._backward = bw
|
|
440
|
+
return out
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def stack(tensors, axis=0):
|
|
444
|
+
datas = [t.data for t in tensors]
|
|
445
|
+
out = Tensor(np.stack(datas, axis=axis), parents=tuple(tensors), op="stack")
|
|
446
|
+
|
|
447
|
+
def bw():
|
|
448
|
+
for i, t in enumerate(tensors):
|
|
449
|
+
sl = [slice(None)] * out.data.ndim
|
|
450
|
+
sl[axis] = i
|
|
451
|
+
t.grad += out.grad[tuple(sl)]
|
|
452
|
+
out._backward = bw
|
|
453
|
+
return out
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def softmax_rows(x: Tensor, mask: np.ndarray | None = None) -> Tensor:
|
|
457
|
+
"""Row-wise softmax with optional additive -1e9 mask (mask=True -> masked)."""
|
|
458
|
+
z = x.data if mask is None else np.where(mask, -1e9, x.data)
|
|
459
|
+
z = z - z.max(axis=-1, keepdims=True)
|
|
460
|
+
e = np.exp(z)
|
|
461
|
+
if mask is not None:
|
|
462
|
+
e = np.where(mask, 0.0, e)
|
|
463
|
+
s = e.sum(axis=-1, keepdims=True)
|
|
464
|
+
p = e / s
|
|
465
|
+
out = x._apply(p, "softmax", (x,), lambda: None)
|
|
466
|
+
|
|
467
|
+
def bw():
|
|
468
|
+
g = out.grad
|
|
469
|
+
dot = (g * p).sum(axis=-1, keepdims=True)
|
|
470
|
+
x.grad += p * (g - dot)
|
|
471
|
+
out._backward = bw
|
|
472
|
+
return out
|