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 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