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.
@@ -0,0 +1,283 @@
1
+ """
2
+ fluidlayer.torch_graft — the same memory layer, in PyTorch, for a REAL LLM.
3
+ This is the GPU-scale twin of `fluidlayer/graft.py`. It exists because the
4
+ numpy implementation cannot be attached to a pretrained transformer: it
5
+ needs a tied embedding table to turn a stored token id back into a
6
+ vector, and it needs the model's own scales.
7
+ WHAT IS THE SAME AS THE NUMPY VERSION
8
+ the two tiers: a lossy (diffusion) state and an exact, content-keyed
9
+ store of token ids;
10
+ the attach-is-a-no-op property: `Wo_exact` and `Wo_lossy` start at ZERO,
11
+ so attaching the branch leaves the logits bit-identical;
12
+ the read gain and the contrast transform (`pc1`), because with a tied
13
+ readout a raw embedding injection boosts the token's embedding-space
14
+ neighbours too;
15
+ causality: the store is written from the pair (context ending at t-1,
16
+ token at t), so a token is never read back before it was written.
17
+ the exact store uses majority tracking and least-corroborated eviction,
18
+ matching the NumPy reference.
19
+ WHAT IS DIFFERENT
20
+ the state lives in a torch tensor (it can be a parameter, a buffer, or
21
+ just an attribute);
22
+ the writes are done under `no_grad` -- the layer is a MEMORY, and the
23
+ exactness of the tier does not depend on differentiating through it;
24
+ the contrast transform is computed once from the model's embedding table
25
+ (on the right device) and can be refreshed after the model is trained.
26
+ The optional PyTorch parity test runs in CI. `experiments/gpu_graft.py`
27
+ is the driver for GPU-scale experiments.
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import torch
32
+ import torch.nn as nn
33
+
34
+ from .graft import _ModeStore
35
+
36
+ __all__ = ["SAABranchTorch", "ctx_hash"]
37
+
38
+
39
+ def ctx_hash(tokens) -> int:
40
+ """FNV-1a 64-bit hash of a token sequence (the content key)."""
41
+ h = 0xCBF29CE484222325
42
+ for t in tokens:
43
+ h ^= int(t) & 0xFFFFFFFFFFFFFFFF
44
+ h = (h * 0x100000001B3) & 0xFFFFFFFFFFFFFFFF
45
+ return h
46
+
47
+
48
+ class SAABranchTorch(nn.Module):
49
+ """The attachable memory layer (torch).
50
+
51
+ Parameters
52
+ ----------
53
+ M, r, d : anchors, rank, model width
54
+ emb : the model's input embedding matrix (tied or not). Used to
55
+ turn a stored token id back into a vector.
56
+ ctx_len : how many trailing tokens form the content key
57
+ capacity : max entries in the exact store
58
+ read_gain : the scalar gain of the exact injection (CALIBRATE it; see
59
+ the gain sweep in results/e6_graft.json)
60
+ transform : "raw" | "center" | "pc1" -- the selectivity of the injection
61
+ """
62
+
63
+ def __init__(self, M: int = 64, r: int = 8, d: int = 64, emb=None,
64
+ ctx_len: int = 1, capacity: int = 4096, read_gain: float = 300.0,
65
+ transform: str = "pc1", use_exact: bool = True,
66
+ use_lossy: bool = False, seed: int = 0,
67
+ use_router: bool = True):
68
+ super().__init__()
69
+ for name, value in (("M", M), ("r", r), ("capacity", capacity)):
70
+ if int(value) <= 0:
71
+ raise ValueError(f"{name} must be positive, got {value}")
72
+ g = torch.Generator().manual_seed(seed)
73
+ self.M, self.r, self.d = int(M), int(r), int(d)
74
+ self.ctx_len = int(ctx_len)
75
+ self.capacity = int(capacity)
76
+ self.read_gain = float(read_gain)
77
+ self.transform = str(transform)
78
+ self.use_exact, self.use_lossy = bool(use_exact), bool(use_lossy)
79
+ self.use_router = bool(use_router)
80
+ self.protos = nn.Parameter(torch.randn(M, d, generator=g) * d ** -0.5)
81
+ self.Wq = nn.Parameter(torch.randn(d, d, generator=g) * d ** -0.5)
82
+ self.Wk = nn.Parameter(torch.randn(d, d, generator=g) * d ** -0.5)
83
+ self.Wv = nn.Parameter(torch.randn(d, r, generator=g) * d ** -0.5)
84
+ self.Wr = nn.Parameter(torch.randn(r, d, generator=g) * d ** -0.5)
85
+ # ZERO => attaching the branch is an EXACT identity
86
+ self.Wo_exact = nn.Parameter(torch.zeros(d, d))
87
+ self.Wo_lossy = nn.Parameter(torch.zeros(d, d))
88
+ self.emb = emb # NOT a parameter: it belongs to the model
89
+ self.state = torch.zeros(M, r)
90
+ self.store = _ModeStore(capacity=self.capacity)
91
+ self.anchor_store = _ModeStore(capacity=self.M)
92
+ self._buf: list[int] = []
93
+ self._contrast = None
94
+
95
+ # ------------------------------------------------------------- lifecycle
96
+ def reset(self):
97
+ self.state = torch.zeros_like(self.state)
98
+ self.store.clear()
99
+ self.anchor_store.clear()
100
+ self._buf.clear()
101
+
102
+ def refresh_contrast(self):
103
+ """Recompute the (optionally transformed) embedding table."""
104
+ self._contrast = None
105
+
106
+ def _contrast_table(self, device, dtype):
107
+ if self._contrast is None:
108
+ E = self.emb.weight if hasattr(self.emb, "weight") else self.emb
109
+ E = E.detach().to(device=device, dtype=torch.float32)
110
+ t = self.transform
111
+ if t == "center":
112
+ E = E - E.mean(0, keepdim=True)
113
+ elif t.startswith("pc"):
114
+ k = int(t[2:] or 1)
115
+ # the SVD is done ONCE, on the right device
116
+ _, _, Vt = torch.linalg.svd(E, full_matrices=False)
117
+ for j in range(min(k, Vt.shape[0])):
118
+ p = Vt[j]
119
+ E = E - torch.outer(E @ p, p)
120
+ self._contrast = E.to(dtype=dtype)
121
+ return self._contrast
122
+
123
+ def state_bytes(self) -> int:
124
+ """Logical packed-payload estimate; excludes Python/allocator overhead."""
125
+ return (self.state.numel() * 4 + len(self.store) * 20
126
+ + len(self.anchor_store) * 20)
127
+
128
+ # ----------------------------------------------------------------- maths
129
+ def _route(self, content: torch.Tensor):
130
+ """content (T, d) -> soft top-k weights (T, M) and the argmax (T,)."""
131
+ q = content @ self.Wq
132
+ kk = self.protos @ self.Wk
133
+ sc = (q @ kk.T) / (self.d ** 0.5)
134
+ k = min(4, self.M)
135
+ top = sc.topk(k, dim=-1).indices
136
+ w = torch.zeros_like(sc)
137
+ s = sc.gather(1, top)
138
+ s = s - s.max(dim=-1, keepdim=True).values
139
+ e = torch.softmax(s, dim=-1)
140
+ w.scatter_(1, top, e)
141
+ return w, top[:, 0]
142
+
143
+ def _key(self, buf):
144
+ if len(buf) < self.ctx_len:
145
+ return None
146
+ if self.ctx_len == 1:
147
+ return int(buf[-1])
148
+ return ctx_hash(buf[-self.ctx_len:])
149
+
150
+ def _put(self, key: int, value: int):
151
+ self.store.put(key, value)
152
+
153
+ def gated_logits(self, logits, info, gain: float = 8.0,
154
+ use_support: bool = False, support_scale: float = 1.0,
155
+ tau: float | None = None, c_min: float = 2.0,
156
+ big: float = 20.0, router_weight: float = 0.3,
157
+ gate_space: str = "logit"):
158
+ """Apply the NumPy reference's uncertainty-gated retrieval boost."""
159
+ retrieved = info.get("retrieved")
160
+ if retrieved is None:
161
+ return logits
162
+ z = logits
163
+ retrieved = torch.as_tensor(retrieved, device=z.device, dtype=torch.long)
164
+ T = z.shape[0]
165
+ fallback = torch.zeros(T, device=z.device, dtype=torch.bool)
166
+ rr = info.get("router_retrieved")
167
+ rconf = info.get("router_confidence")
168
+ if rr is not None:
169
+ rr = torch.as_tensor(rr, device=z.device, dtype=torch.long)
170
+ fallback = (retrieved < 0) & (rr >= 0)
171
+ retrieved = torch.where(fallback, rr, retrieved)
172
+
173
+ probs = torch.softmax(z, dim=1)
174
+ p_max = probs.max(dim=1).values
175
+ if str(gate_space) == "logit":
176
+ uncertainty = -torch.log(p_max.clamp_min(1e-12))
177
+ else:
178
+ uncertainty = 1.0 - p_max
179
+ if tau is None:
180
+ tau = 20.0 if str(gate_space) == "logit" else 2.0
181
+ boost = float(gain) * uncertainty
182
+
183
+ if rconf is not None:
184
+ rc = torch.as_tensor(rconf, device=z.device, dtype=z.dtype)
185
+ boost = torch.where(
186
+ fallback,
187
+ boost * float(router_weight) * rc.clamp(0.0, 1.0),
188
+ boost,
189
+ )
190
+ if use_support:
191
+ support = info.get("support")
192
+ if support is not None:
193
+ sup = torch.as_tensor(support, device=z.device, dtype=z.dtype)
194
+ boost = boost * (sup / float(support_scale)).clamp(max=1.0)
195
+
196
+ confidence = info.get("confidence")
197
+ if confidence is None:
198
+ conf = torch.zeros(T, device=z.device, dtype=z.dtype)
199
+ else:
200
+ conf = torch.as_tensor(confidence, device=z.device, dtype=z.dtype)
201
+ if rconf is not None:
202
+ rc = torch.as_tensor(rconf, device=z.device, dtype=z.dtype)
203
+ conf = torch.where(fallback, rc, conf)
204
+ decide = (uncertainty >= float(tau)) | (conf >= float(c_min))
205
+ boost = torch.where(decide, torch.full_like(boost, float(big)), boost)
206
+
207
+ out = z.clone()
208
+ rows = torch.nonzero(retrieved >= 0, as_tuple=False).flatten()
209
+ if rows.numel():
210
+ out[rows, retrieved[rows]] += boost[rows]
211
+ return out
212
+
213
+ # ---------------------------------------------------------------- forward
214
+ def forward(self, h: torch.Tensor, content: torch.Tensor,
215
+ token_ids: torch.Tensor):
216
+ """h, content: (T, d); token_ids: (T,) int64."""
217
+ T = token_ids.shape[0]
218
+ ids = token_ids.detach().cpu().tolist()
219
+ w, am = self._route(content.detach())
220
+ out_e = None
221
+ hits = 0
222
+ retrieved = torch.full((T,), -1, dtype=torch.long)
223
+ confidence = torch.zeros(T, dtype=torch.float32)
224
+ router_retrieved = torch.full((T,), -1, dtype=torch.long)
225
+ router_confidence = torch.zeros(T, dtype=torch.float32)
226
+ if self.use_exact and self.emb is not None:
227
+ C = self._contrast_table(h.device, h.dtype)
228
+ rows = torch.zeros(T, self.d, device=h.device, dtype=h.dtype)
229
+ buf = list(self._buf)
230
+ for t in range(T):
231
+ prev_key = self._key(buf)
232
+ buf.append(int(ids[t]))
233
+ if len(buf) > self.ctx_len:
234
+ buf = buf[-self.ctx_len:]
235
+ key = self._key(buf)
236
+ if self.use_router:
237
+ anchor = int(am[t])
238
+ value = self.anchor_store.get(anchor, -1)
239
+ if value >= 0:
240
+ router_retrieved[t] = value
241
+ router_confidence[t] = self.anchor_store.confidence(anchor)
242
+ v = self.store.get(key, -1)
243
+ if v >= 0:
244
+ rows[t] = C[v] * self.read_gain
245
+ retrieved[t] = v
246
+ confidence[t] = self.store.confidence(key)
247
+ hits += 1
248
+ if prev_key is not None:
249
+ self._put(prev_key, int(ids[t]))
250
+ if t > 0 and self.use_router:
251
+ self.anchor_store.put(int(am[t - 1]), int(ids[t]))
252
+ self._buf = buf
253
+ out_e = rows
254
+
255
+ if self.use_lossy:
256
+ with torch.no_grad():
257
+ self.state = self.state + w.T @ (h.detach() @ self.Wv.detach())
258
+ self.state = 0.9 * self.state # a simple decay
259
+ out_l = (w @ self.state) @ self.Wr
260
+ else:
261
+ out_l = None
262
+
263
+ h_out = h
264
+ if out_e is not None:
265
+ h_out = h_out + out_e @ self.Wo_exact
266
+ if out_l is not None:
267
+ h_out = h_out + out_l @ self.Wo_lossy
268
+ support = torch.tensor(
269
+ [self.store.support.get(int(v), 0) if v >= 0 else 0
270
+ for v in retrieved],
271
+ dtype=torch.long,
272
+ )
273
+ return h_out, {
274
+ "route": w,
275
+ "anchor": am,
276
+ "hits": hits,
277
+ "emb_read": out_e,
278
+ "retrieved": retrieved,
279
+ "support": support,
280
+ "confidence": confidence,
281
+ "router_retrieved": router_retrieved,
282
+ "router_confidence": router_confidence,
283
+ }
@@ -0,0 +1,376 @@
1
+ Metadata-Version: 2.4
2
+ Name: fluidlayer
3
+ Version: 2.0.0
4
+ Summary: A research memory layer for language models using cellular-sheaf diffusion
5
+ Author: Kapil Dhinwa
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/kapildhinwa2010-dev/fluidtech
8
+ Project-URL: Repository, https://github.com/kapildhinwa2010-dev/fluidtech
9
+ Project-URL: Issues, https://github.com/kapildhinwa2010-dev/fluidtech/issues
10
+ Project-URL: Changelog, https://github.com/kapildhinwa2010-dev/fluidtech/blob/main/docs/CHANGELOG.md
11
+ Keywords: llm,memory,long-context,kv-cache,retrieval,cellular-sheaves,persistent-homology
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: numpy<3,>=1.23
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7; extra == "dev"
29
+ Requires-Dist: ruff>=0.5; extra == "dev"
30
+ Requires-Dist: build; extra == "dev"
31
+ Provides-Extra: gpu
32
+ Requires-Dist: torch>=2.1; extra == "gpu"
33
+ Requires-Dist: transformers>=4.40; extra == "gpu"
34
+ Requires-Dist: accelerate>=0.30; extra == "gpu"
35
+ Requires-Dist: safetensors>=0.4; extra == "gpu"
36
+ Dynamic: license-file
37
+
38
+ # FluidLayer
39
+
40
+ **A research memory layer for LLMs — attachable with one residual-stream insertion.**
41
+
42
+ `16 KB` research-scale fluid state as a compact long-range channel ·<br>
43
+ exact recall of retained token IDs from outside the attention window ·<br>
44
+ pure NumPy core, with optional PyTorch support ·<br>
45
+ eleven measured negative results, published on purpose
46
+
47
+ [![CI](https://github.com/kapildhinwa2010-dev/fluidtech/actions/workflows/ci.yml/badge.svg)](https://github.com/kapildhinwa2010-dev/fluidtech/actions/workflows/ci.yml)
48
+ ![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue.svg)
49
+ ![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)
50
+ ![Self-verification](https://img.shields.io/badge/self--verification-50%2F50-brightgreen.svg)
51
+
52
+ ---
53
+
54
+ ## The problem, in one number
55
+
56
+ A 70B model on 2×H100 80GB cannot hold a single 128K-token session.
57
+
58
+ The weights take **131.5 GB**; the KV cache for 128K tokens needs another **40 GB**, and only **28.5 GB** is left.
59
+ This is a memory-budget illustration, not a claim that this prototype
60
+ replaces a KV cache or is a drop-in serving optimization.
61
+
62
+ The usual answer is to quantise it, compress it, or make the model smaller.
63
+ This project asks a different question:
64
+
65
+ > What if a compact, constant-size memory channel sat beside attention and
66
+ > could be addressed by content instead of position?
67
+
68
+ ---
69
+
70
+ ## What this is
71
+
72
+ A research memory layer designed to attach to a causal LM:
73
+
74
+ ```text
75
+ h' = h + W_e · E(h) + W_l · S(h)
76
+ ^^^^^^ ^^^^^
77
+ exact tier lossy tier (the constant-size fluid state)
78
+ ```
79
+
80
+ - The **exact tier** is a bounded, content-keyed store of token IDs.
81
+ **20 bytes per packed slot.** A recalled value is an `int32` — bit-exact, not approximate.
82
+ The attachable store tracks majority continuations and evicts the
83
+ least-corroborated entries. Bit-exactness describes the stored token ID,
84
+ not its truth or provenance. The Python reference's memory estimate
85
+ excludes Python object overhead.
86
+
87
+ - The **lossy tier** is a sheaf-diffusion aggregate over `M` anchors — `M × r` floats, constant size, whatever you stream through it.
88
+
89
+ The two tiers exist because a diffusion smears. If the exact value of a token lives only inside the fluid, it is mixed with its neighbours and is provably no longer exact.
90
+
91
+ So:
92
+
93
+ > **The fluid answers WHERE. The register answers WHAT.**
94
+
95
+ ---
96
+
97
+ ## The three numbers
98
+
99
+ | Claim | Measured result |
100
+ |---|---|
101
+ | Attaching the layer changes nothing | The logits are **bit-identical** (max diff `0.000e+00`). The output projections start at zero, so the branch is a **provable no-op** until someone trains it. |
102
+ | It adds a capability the model did not have | Cross-window verbatim recall: **`0.050 → 0.800`** (chance `0.125`) — **16× the base model** — for a fact planted 400 tokens outside the attention window. |
103
+ | The stored token ID is exact | A retained token ID is returned bit-exactly. This does not verify whether the token is true, safe, or authenticated. |
104
+ | The model gets better | Gated retrieval improves the base model’s perplexity by **`−0.176 nats`** (`ppl 13.61 → 11.42`) — the model’s own confidence decides when the memory speaks. |
105
+ | It beats its closest prior art in the regime it targets | Against a **kNN-LM** (the standard way to use a datastore), with a **16 KB** store the gated layer wins by **7.4×** on loss (`−0.1134 vs −0.0154`). With a 1 MB store the kNN-LM wins by 6.4×, so the claim is precise: **the gate is the contribution under a hard memory budget.** |
106
+ | The fluid tier earns its place | Used as the **router** (the geometry as an address space), it rescues recall when the exact store is too small (`0.100 → 0.625`) and fixes hard contexts where the base model has no idea (`+0.053 → −0.071 nats`) — at **0.0005%** of a layer’s compute. |
107
+ | The diffusion tier delivers none of it | With the lossy tier alone: **`0.050`**, i.e. the base model’s own number. The exact tier delivers **all** of it. |
108
+
109
+ Measured on a frozen base model, in [`results/e6_graft.json`](results/e6_graft.json).
110
+ The recipe is in [`docs/GRAFT.md`](docs/GRAFT.md).
111
+
112
+ ---
113
+
114
+ ## Try it, in 60 seconds
115
+
116
+ ```bash
117
+ git clone https://github.com/kapildhinwa2010-dev/fluidtech.git
118
+ cd fluidtech
119
+ pip install -r requirements.txt
120
+
121
+ python selftest.py # 50 machine-checked claims
122
+ python examples/quickstart.py # the tour
123
+ python examples/graft_demo.py # the capability, end to end
124
+ python examples/gate_demo.py # the gate: recall AND a better perplexity
125
+ python experiments/e11_exact.py # the exact memory (the full benchmark)
126
+ ```
127
+
128
+ To install the published package instead of cloning the repository:
129
+
130
+ ```bash
131
+ python -m pip install fluidlayer
132
+ ```
133
+
134
+ Expected output:
135
+
136
+ ```text
137
+ base model : 1/20 correct (it never saw the fact)
138
+ base + the layer : 20/20 correct (the layer retrieved it)
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Using it
144
+
145
+ ```python
146
+ import torch
147
+ from fluidlayer.torch_graft import SAABranchTorch
148
+
149
+ # Assume `model` is a loaded causal LM and `windows` contains 1-D token-ID tensors.
150
+ embedding = model.get_input_embeddings()
151
+ branch = SAABranchTorch(
152
+ M=64,
153
+ r=8,
154
+ d=embedding.weight.shape[1],
155
+ emb=embedding,
156
+ ctx_len=1,
157
+ read_gain=300.0,
158
+ transform="pc1",
159
+ use_exact=True,
160
+ use_lossy=False,
161
+ ).to(embedding.weight.device)
162
+
163
+ branch.reset() # a new session
164
+
165
+ with torch.no_grad():
166
+ for input_ids in windows: # one 1-D token-ID window at a time
167
+ content = embedding(input_ids)
168
+ residual = content
169
+ h, info = branch(residual, content, input_ids)
170
+ logits = model(inputs_embeds=h.unsqueeze(0)).logits[0]
171
+ logits = branch.gated_logits(logits, info)
172
+ ```
173
+
174
+ - **4 KB** of fluid state for `M=64, r=8`, plus a logical **20 B per populated context or anchor slot**; the Python reference has additional object overhead.
175
+ - The base model is frozen the whole time.
176
+ - The output projections start at zero, so attachment initially preserves
177
+ the model output. Enabling retrieval is a separate, calibrated operation.
178
+ - This sketch omits model-specific positional embeddings, attention masks,
179
+ session lifecycle, and gain calibration. See the [integration notes](docs/GETTING_STARTED.md#attach-the-optional-pytorch-adapter).
180
+ - The NumPy reference implementation is [`fluidlayer/graft.py`](fluidlayer/graft.py), and the Torch one mirrors it exactly.
181
+
182
+ Start with the [documentation guide](docs/README.md), then read
183
+ [Getting started](docs/GETTING_STARTED.md) and the [API reference](docs/API.md).
184
+
185
+ ---
186
+
187
+ ## What it does not do
188
+
189
+ Being explicit, because this is where grafts usually oversell:
190
+
191
+ - It does **not** make the model better at language — it makes it better at remembering what it has already seen.
192
+ - It does **not** extend the attention window — the window is still 128 tokens; the layer can quote from outside it, not reason over it.
193
+ - It does **not** compose, infer, or answer questions about what it holds.
194
+ - The diffusion tier has **no measurable upside** on real text and used to damage the model it was attached to (`+0.81 nats`) until its training signal was fixed — keep it off.
195
+
196
+ The full list, with numbers: [`docs/LIMITATION.md`](docs/LIMITATION.md)
197
+
198
+ ---
199
+
200
+ ## At ten million tokens
201
+
202
+ Give a model 10M tokens with the layer attached, and:
203
+
204
+ - The state is still **16 KB**.
205
+ The same tokens as a KV cache would require **1.31 TB** for an 8B model, **3.28 TB** for a 70B one.
206
+ - Filling it costs **80 seconds** and **no model forward at all** — the exact tier is model-free.
207
+ - With distinctive keys it recalls **6/6** facts planted from 100K to 9M tokens deep.
208
+ With realistic 12-gram keys and a 16 KB budget it recalls **0/6** — its reach is the last ~10,000 tokens.
209
+ - The perplexity delta is **`−0.0057 nats`** at the right injection gain, and **`+1.08 nats`** at the gain that maximises recall — the two knobs are three orders of magnitude apart.
210
+ The gate closes that gap: scaling the override by the model’s own uncertainty keeps recall at **`1.000`** and improves perplexity to **`11.42`**.
211
+
212
+ Details: [`docs/TEN-MILLION.md`](docs/TEN-MILLION.md)
213
+
214
+ ---
215
+
216
+ ## The negative results
217
+
218
+ This is the part that makes the rest believable.
219
+ Eleven results came out negative or against expectation, and all eleven are measured, reproducible, and published.
220
+
221
+ | What we expected | What happened |
222
+ |---|---|
223
+ | The **topological** selection criterion picks the right facts | It does **not** — `0.000` precision vs `0.970` for plain geometric affinity. |
224
+ | `‖L·1‖∞` detects the frozen state | It does **not** — the frustration index does; the old claim is now a test case. |
225
+ | `λ₂` predicts long-context quality | Barely — Pearson `0.20`. |
226
+ | A transformer loses at some scale | It **wins** at every scale we could train. |
227
+ | A domain prior beats a frequency table | It helps (`−0.006 nats`) but a **unigram table wins 6× more**. |
228
+ | The diffusion tier is neutral on a frozen model | It **degrades** the model by **`+0.81 nats`** (fixed → `+0.0000`, but no upside either). |
229
+ | Training the layer helps | It **hurts** — the training-free identity map is better (`0.800 vs 0.650`). |
230
+
231
+ Read it: [`docs/NEGATIVE-RESULTS.md`](docs/NEGATIVE-RESULTS.md)
232
+
233
+ ---
234
+
235
+ ## The repository
236
+
237
+ ```text
238
+ fluidlayer/ the package (pure NumPy; torch only in torch_graft.py)
239
+ graft.py the attachable memory layer
240
+ memory.py the frozen register (20 B per fact, bit-exact)
241
+ torch_graft.py the same layer in PyTorch, for a real LLM
242
+ gpt2_model.py a GPT-2 forward pass in PyTorch (no transformers needed)
243
+ gpt2_tokenizer.py the GPT-2 byte-level BPE, in pure Python
244
+ sheaf.py topology.py diffusion.py blocks.py autodiff.py ...
245
+
246
+ experiments/ every measurement; each writes a JSON into results/
247
+ e14_real_gpt2.py the graft on a REAL pretrained GPT-2 (124M)
248
+ e13_knn_baseline.py the layer vs. the closest prior art (kNN-LM)
249
+
250
+ tools/ the artifact checker, bundler, checkpoint downloader,
251
+ and PyPI fetcher
252
+ fluidlayer/bpe.py the byte-level BPE tokenizer
253
+
254
+ tests/ the unit tests (pytest, or run them without it)
255
+
256
+ examples/ quickstart.py, graft_demo.py, gate_demo.py
257
+
258
+ docs/ the design docs, the API, the limits, the theory report
259
+
260
+ results/ every number in the README, as JSON
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Documentation
266
+
267
+ Browse the [documentation guide](docs/README.md) for a topic-based index.
268
+ The [getting-started tutorial](docs/GETTING_STARTED.md) and
269
+ [API reference](docs/API.md) are the recommended entry points. Research
270
+ claims and their limits are explained in the
271
+ [graft report](docs/GRAFT.md), [negative results](docs/NEGATIVE-RESULTS.md),
272
+ and [limitations](docs/LIMITATION.md).
273
+
274
+ ---
275
+
276
+ ## Status — and it now includes a REAL pretrained LLM
277
+
278
+ GPT-2 (124M parameters, the real 50,257-token BPE tokenizer), run on CPU with the memory layer attached:
279
+
280
+ | Test | Result |
281
+ |---|---|
282
+ | Attaching the layer | The logits are **bit-identical** (max diff `0.000e+00`). |
283
+ | A fact planted **2,947 tokens** before the query | The base GPT-2 predicts `464`; **the layer predicts the fact**. |
284
+ | The base model’s perplexity | `4.0289` (`ppl 56.20`) → **`4.0019` (`ppl 54.70`)** with the layer. |
285
+
286
+ And one result that narrows the claim honestly:
287
+
288
+ On the real model, the classic kNN-LM interpolation beats the gated override by **8×** on perplexity — the opposite of the 131k-parameter model, where the gate won by **7.4×**.
289
+ The gate wins where the base model is weak; interpolation wins where it is strong.
290
+ The capability works on both.
291
+
292
+ The checkpoint, the forward pass and the tokenizer are all in this repository:
293
+
294
+ - [`fluidlayer/gpt2_model.py`](fluidlayer/gpt2_model.py)
295
+ - [`fluidlayer/gpt2_tokenizer.py`](fluidlayer/gpt2_tokenizer.py)
296
+
297
+ No `transformers` dependency, and the generation is verified coherent.
298
+
299
+ The Torch twin is now tested against the NumPy reference in [`tests/test_torch_parity.py`](tests/test_torch_parity.py): the two implementations must agree on the router, the store and the gated override.
300
+
301
+ That test found **four real port bugs**:
302
+
303
+ 1. A different route scale.
304
+ 2. The wrong routing content — the residual stream instead of the position-free embedding.
305
+ 3. A different confidence formula.
306
+ 4. A read/write order that broke the snapshot semantics.
307
+
308
+ Four bugs that would have shipped silently.
309
+
310
+ The checks cover the tested behaviors; they do not prove every research
311
+ claim or validate production-scale operation.
312
+
313
+ ---
314
+
315
+ ## A research prototype, honestly labelled
316
+
317
+ Everything above was measured on CPU, at small scale, on a character-level or 512-token subword corpus, with a **131,392-parameter model**.
318
+
319
+ The subword repeat is the closest thing to a realistic tokenizer in this repository.
320
+ The GPU run, [`experiments/gpu_graft.py`](experiments/gpu_graft.py), is written and has not been executed here.
321
+
322
+ ---
323
+
324
+ ## Reproducing
325
+
326
+ ```bash
327
+ python selftest.py # 50 checks
328
+ python experiments/run_all.py --full # every measurement (~40 min, CPU)
329
+ ```
330
+
331
+ Everything is pure NumPy, CPU-only, and needs no network.
332
+
333
+ The details and expected outputs: [`docs/REPRODUCING.md`](docs/REPRODUCING.md)
334
+
335
+ ---
336
+
337
+ ---
338
+
339
+ ## 🤝 Contributions & corrections
340
+
341
+ Suggestions, bug reports, and corrections are welcome, especially measurement
342
+ disagreements. Please open an issue or submit a pull request.
343
+
344
+ If a number in the docs does not match what you get, that is a bug in the code or in the documentation, and it is the most valuable report you can file.
345
+
346
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md).
347
+
348
+ ---
349
+
350
+ ## License
351
+
352
+ MIT — see [`LICENSE`](LICENSE).
353
+
354
+ If you use this, please cite it: [`CITATION.cff`](CITATION.cff).
355
+
356
+ ---
357
+
358
+ ## Confession
359
+
360
+ The project began as:
361
+
362
+ > “Replace attention with a sheaf diffusion.”
363
+
364
+ The measurements have said three times that this is not the way.
365
+
366
+ What is left is smaller and much more defensible:
367
+
368
+ A research memory layer with an operator-level stability bound, a bounded in-memory register that returns retained token IDs bit-exactly, 50 machine-checked claims, and published measurements of its limitations. The register is not tamper-evident, encrypted, or tenant-isolated by default.
369
+
370
+ ## Origin and acknowledgments
371
+
372
+ The project began with the author's idea of flowing information through a
373
+ topological structure. The implementation and repository organization were
374
+ developed with AI assistance. The measurements, limitations, and research
375
+ decisions are documented alongside the code so the work can be evaluated and
376
+ reproduced.
@@ -0,0 +1,22 @@
1
+ fluidlayer/__init__.py,sha256=HxMH5kFeBicWQHwNKsZzffn6_GoeMSsYW6SfaazBZ2Q,3892
2
+ fluidlayer/autodiff.py,sha256=Zu0pLZf-C1UWfPinzHlnulwy0hZOPF_vWUVbZTl97Gc,15442
3
+ fluidlayer/blocks.py,sha256=ftPkYT8ER55ySJlCJ6e-Q-r3-Ji_6s2UjifQ_DAKaPY,11835
4
+ fluidlayer/bpe.py,sha256=Hsg59XJEBCnQ7zh-ZJpxo4HJ-oxQ5Qj1A4TjJRZSS7o,6265
5
+ fluidlayer/data.py,sha256=E4SwzCUgoOYOm0yNsEL71Tb4Z1F4iCI0cozEqOCPaZU,3198
6
+ fluidlayer/diffusion.py,sha256=iUkBy6WNA9LuAZFgoHgGL1_GK6y7D0oSGF1944yls6o,9844
7
+ fluidlayer/gpt2_model.py,sha256=SI1igbJx-yDto8uQH6EYUmpimN7nhYG95mfKcFumw0M,5435
8
+ fluidlayer/gpt2_tokenizer.py,sha256=ZRVGknRje03Hv6KmjNjIZjxcq_ushsS_EZMYqO5CxPE,4287
9
+ fluidlayer/graft.py,sha256=vKDmFSQ3hjz6S8F4XW0SsoG0GWz95ytkXNxjESO9eBo,33068
10
+ fluidlayer/losses.py,sha256=38CbAAa86hxxO4LS-UONI6mnVr9oqZfMAXPO-1S713Y,2764
11
+ fluidlayer/memory.py,sha256=yasTGNw6MQfLJHyGDixMAw9VN24kT4iWhj8sl61It_Q,22126
12
+ fluidlayer/model.py,sha256=sZDy4mhlIjFj-2Gs490MQQe6gnZuTUYdBk3wdGa8XcU,5339
13
+ fluidlayer/optim.py,sha256=b2VxtVagwaFuGE5pFA32AbwNyLYxZJOj-2LgWgyBJhI,2033
14
+ fluidlayer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ fluidlayer/sheaf.py,sha256=qrRB-PTr0uqyVXY-arYpa5Z80PAsXh5LlbxuCEidpXo,4851
16
+ fluidlayer/topology.py,sha256=lw2OIp_5_LYY-Y4zxbjVazDsxOHWLS689B-34PsGEjE,10400
17
+ fluidlayer/torch_graft.py,sha256=ys_vYL47Y5C30FibIn2wlrpalDxxD-O614QN0y6iYW0,11997
18
+ fluidlayer-2.0.0.dist-info/licenses/LICENSE,sha256=fA16TuGQSfXe_xwf-IKXk_8CmaRRpZ9nrk-XRas0yc4,1080
19
+ fluidlayer-2.0.0.dist-info/METADATA,sha256=YCDBoRHMpTKPxQHHgcqJtBTP0-iTuL7xF_JGOc5dGVY,16163
20
+ fluidlayer-2.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
21
+ fluidlayer-2.0.0.dist-info/top_level.txt,sha256=BpmQMo4rA0HmYCInycdfRk6DF2P0IgIJ3p5XQqNEj3I,11
22
+ fluidlayer-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+