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/graft.py ADDED
@@ -0,0 +1,741 @@
1
+ """
2
+ fluidlayer.graft — the attachable memory layer.
3
+
4
+ WHAT THIS IS
5
+ ------------
6
+ A single module you can bolt onto ANY causal LM with one insertion:
7
+
8
+ h' = h + W_e . E(h) + W_l . S(h)
9
+ ^^^^^^^ ^^^^^^^
10
+ exact tier lossy tier (the constant-size fluid state)
11
+
12
+ E(h) = the EXACT read: a bounded, CONTENT-KEYED store of token ids,
13
+ pulled back out through the model's own embedding table.
14
+ S(h) = the LOSSY read: the diffused aggregate state (M x r floats).
15
+
16
+ WHY IT IS SAFE TO ATTACH
17
+ ------------------------
18
+ `W_e` and `W_l` are initialised to ZERO, so at the moment of attachment the
19
+ branch is an EXACT identity: h' == h, bit for bit. Every logit, every
20
+ perplexity, every sampled token is unchanged. The layer can only start to
21
+ matter after someone trains it, and training it is opt-in.
22
+
23
+ WHY THERE ARE TWO TIERS
24
+ -----------------------
25
+ A diffusion smears. If the exact value of a token lives only inside the
26
+ fluid, it is mixed with its neighbours and is no longer exact. So:
27
+
28
+ the fluid answers WHERE (an address space of M anchors)
29
+ the store answers WHAT (an int32 token id, keyed by content)
30
+
31
+ The store is CONTENT-KEYED, not anchor-keyed. That distinction matters and
32
+ it was learned the hard way: a per-anchor "last write wins" slot is evicted
33
+ by ordinary filler tokens within a few hundred tokens, because every token
34
+ routs somewhere. Keying by the content of the context means a fact is only
35
+ overwritten by the SAME context occurring again -- which is exactly the
36
+ semantics you want (and it is the same trick as the frozen register's
37
+ `ctx_key`).
38
+
39
+ CAUSALITY
40
+ ---------
41
+ The store is written CAUSALLY. At position t the branch has seen tokens
42
+ x_0..x_t only. The pair (context ending at t-1, next token x_t) is complete
43
+ at time t, so the write happens at time t -- never earlier. The read at
44
+ position t uses the context ending at t, which is a DIFFERENT key from the
45
+ one being written, so a token can never be read back before it was written
46
+ and the model can never "copy the current token" through the store.
47
+
48
+ PERSISTENCE
49
+ -----------
50
+ The state (`state`, `store`) lives on the object, NOT in the autodiff graph.
51
+ It survives across calls: that is what makes it a *memory* rather than a
52
+ layer. `reset()` clears it (a new document / a new session).
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ import numpy as np
58
+
59
+ from .autodiff import Tensor, softmax_rows
60
+ from .diffusion import cheb_coeffs_exp, power_iteration_lmax
61
+
62
+ __all__ = ["SAABranch", "GRAFT_STATE_BYTES", "ctx_hash"]
63
+
64
+
65
+ def ctx_hash(tokens) -> int:
66
+ """FNV-1a 64-bit hash of a token sequence (the content key)."""
67
+ h = 0xCBF29CE484222325
68
+ for t in tokens:
69
+ h ^= int(t) & 0xFFFFFFFFFFFFFFFF
70
+ h = (h * 0x100000001B3) & 0xFFFFFFFFFFFFFFFF
71
+ return h
72
+
73
+
74
+ def _knn_edges(X: np.ndarray, k: int) -> np.ndarray:
75
+ """Undirected kNN edge list of a point set (cosine similarity)."""
76
+ n = X.shape[0]
77
+ Xn = X / (np.linalg.norm(X, axis=1, keepdims=True) + 1e-12)
78
+ S = Xn @ Xn.T
79
+ np.fill_diagonal(S, -np.inf)
80
+ kk = min(k, n - 1)
81
+ idx = np.argsort(-S, axis=1)[:, :kk]
82
+ rows = np.repeat(np.arange(n), kk)
83
+ cols = idx.reshape(-1)
84
+ e = np.stack([np.minimum(rows, cols), np.maximum(rows, cols)], axis=1)
85
+ return np.unique(e, axis=0)
86
+
87
+
88
+ class _ModeStore:
89
+ """A content-keyed store that keeps the MAJORITY value and a confidence.
90
+
91
+ The counts fit in the frozen register's `meta uint16`; the logical
92
+ packed-slot payload is 20 bytes, excluding Python dictionary overhead.
93
+ """
94
+
95
+ __slots__ = ("value", "support", "disagree", "order", "capacity")
96
+
97
+ def __init__(self, capacity: int = 4096):
98
+ self.value: dict = {}
99
+ self.support: dict = {}
100
+ self.disagree: dict = {}
101
+ self.order: list = []
102
+ self.capacity = int(capacity)
103
+
104
+ def __len__(self):
105
+ return len(self.value)
106
+
107
+ def __contains__(self, key):
108
+ return key in self.value
109
+
110
+ def __getitem__(self, key):
111
+ return self.value[key]
112
+
113
+ def keys(self):
114
+ return list(self.order)
115
+
116
+ def get(self, key, default=-1):
117
+ return self.value.get(key, default)
118
+
119
+ def confidence(self, key) -> float:
120
+ if key not in self.value:
121
+ return 0.0
122
+ a = self.support.get(key, 1)
123
+ b = self.disagree.get(key, 0)
124
+ return float(a) / float(max(a + b, 1))
125
+
126
+ def put(self, key, value):
127
+ if key in self.value:
128
+ if self.value[key] == value:
129
+ self.support[key] = self.support.get(key, 1) + 1
130
+ else:
131
+ a = self.support.get(key, 1)
132
+ b = self.disagree.get(key, 0) + 1
133
+ if b > a:
134
+ self.value[key] = int(value)
135
+ self.support[key] = b
136
+ self.disagree[key] = a
137
+ else:
138
+ self.disagree[key] = b
139
+ return
140
+ if len(self.value) >= self.capacity:
141
+ self._evict()
142
+ self.value[key] = int(value)
143
+ self.support[key] = 1
144
+ self.disagree[key] = 0
145
+ self.order.append(int(key))
146
+
147
+ def _evict(self):
148
+ """Evict the LEAST corroborated entry, not merely the oldest."""
149
+ worst, worst_key = None, None
150
+ for k in self.order:
151
+ sc = self.support.get(k, 0) - self.disagree.get(k, 0)
152
+ if worst is None or sc < worst:
153
+ worst, worst_key = sc, k
154
+ if worst_key is None:
155
+ return
156
+ self.value.pop(worst_key, None)
157
+ self.support.pop(worst_key, None)
158
+ self.disagree.pop(worst_key, None)
159
+ if worst_key in self.order:
160
+ self.order.remove(worst_key)
161
+
162
+ def clear(self):
163
+ self.value.clear()
164
+ self.support.clear()
165
+ self.disagree.clear()
166
+ self.order.clear()
167
+
168
+
169
+ class SAABranch:
170
+ """The attachable memory layer (see the module docstring).
171
+
172
+ Parameters
173
+ ----------
174
+ M : number of anchors (the address space). M*r*8 bytes is the
175
+ constant runtime state of the lossy tier.
176
+ r : rank of each anchor's state.
177
+ d : model width.
178
+ emb : the model's embedding table (V x d). Needed to turn a stored
179
+ token id back into a vector. Shared, never copied.
180
+ ctx_len: how many trailing tokens form the content key (1 = a token-level
181
+ "what followed this token" memory; k = a k-gram memory).
182
+ capacity: max entries in the exact store.
183
+ use_exact / use_lossy : ablation switches (measured in E6).
184
+ """
185
+
186
+ def __init__(self, M: int = 64, r: int = 8, d: int = 64, emb=None,
187
+ k: int = 4, cheb_m: int = 8, t: float = 1.0, seed: int = 0,
188
+ use_exact: bool = True, use_lossy: bool = True,
189
+ capacity: int = 4096, ctx_len: int = 1,
190
+ trainable_route: bool = True, read_gain: str = "norm",
191
+ center: bool = True, read_transform: str = "pc1",
192
+ state_mode: str = "persistent", hist_len: int = 8,
193
+ inject_mode: str = "state", use_router: bool = True):
194
+ for name, val in (("M", M), ("r", r), ("capacity", capacity)):
195
+ if int(val) <= 0:
196
+ raise ValueError(f"{name} must be positive, got {val}")
197
+ if emb is not None and int(emb.shape[1]) != int(d):
198
+ raise ValueError(f"d={d} does not match the embedding width "
199
+ f"{emb.shape[1]}")
200
+ if str(state_mode) not in ("persistent", "rebuild"):
201
+ raise ValueError("state_mode must be 'persistent' or 'rebuild'")
202
+ if str(read_gain) not in ("norm", "raw"):
203
+ raise ValueError("read_gain must be 'norm' or 'raw'")
204
+ if (str(read_transform) not in ("raw", "center")
205
+ and not str(read_transform).startswith("pc")):
206
+ raise ValueError("read_transform must be 'raw', 'center' or 'pcK'")
207
+ rng = np.random.default_rng(seed)
208
+ self.M, self.r, self.d = int(M), int(r), int(d)
209
+ self.k, self.cheb_m, self.t = int(k), int(cheb_m), float(t)
210
+ self.use_exact, self.use_lossy = bool(use_exact), bool(use_lossy)
211
+ self.use_router = bool(use_router)
212
+ self.emb = emb
213
+ self.center = bool(center)
214
+ self.read_transform = str(read_transform)
215
+ self._contrast = None
216
+ self.ctx_len = int(ctx_len)
217
+ self.capacity = int(capacity)
218
+ self.read_gain = str(read_gain)
219
+ self.read_scale = 1.0
220
+ self.protos = Tensor(rng.normal(0.0, d ** -0.5, (M, d)))
221
+
222
+ # route head: content -> anchor scores. The route head is a function
223
+ # of CONTENT ONLY (no position), so the same context always lands on
224
+ # the same anchor -- the fluid addresses by geometry, not by position.
225
+ s = d ** -0.5
226
+ self.Wq = Tensor(rng.normal(0.0, s, (d, d)))
227
+ self.Wk = Tensor(rng.normal(0.0, s, (d, d)))
228
+ self.Wv = Tensor(rng.normal(0.0, s, (d, r))) # write projection
229
+ self.Wr = Tensor(rng.normal(0.0, s, (r, d))) # lossy read projection
230
+ # The LAYER-LOCAL head: the layer predicts the next token from its
231
+ # own state. This is the objective that FIXES the divergence: the
232
+ # layer is trained on a well-posed task of its own, and the base
233
+ # model's gradients never enter the layer's parameters.
234
+ V = emb.shape[0] if emb is not None else 0
235
+ self.V = int(V)
236
+ self.Wp = Tensor(rng.normal(0.0, r ** -0.5, (r, V))) if V else None
237
+ self.bias = Tensor(np.zeros(V)) if V else None
238
+ # output projections: ZERO => the branch is an exact identity at attach
239
+ self.Wo_exact = Tensor(np.zeros((d, d)))
240
+ self.Wo_lossy = Tensor(np.zeros((d, d)))
241
+ self.trainable_route = bool(trainable_route)
242
+ # "persistent": the state is a numpy accumulator (the ORIGINAL design;
243
+ # it cannot be differentiated, which is what made the training
244
+ # diverge).
245
+ # "rebuild": the state is REBUILT from a bounded history INSIDE
246
+ # the autodiff graph every call (truncated BPTT), so the
247
+ # gradient is exact and bounded. The state VALUES are
248
+ # identical (the diffusion is linear); only the gradient
249
+ # path is new.
250
+ self.state_mode = str(state_mode)
251
+ # "state": the raw diffused read is injected (the original design).
252
+ # "prior": the layer's OWN next-token distribution is projected into
253
+ # the residual through the embedding table -- a soft domain
254
+ # prior, and the only injection the layer-local objective
255
+ # can honestly justify.
256
+ self.inject_mode = str(inject_mode)
257
+ self.hist_len = int(hist_len)
258
+ self._hist: list = []
259
+
260
+ # ---- the persistent state -------------------------------------
261
+ self.state = np.zeros((M, r)) # lossy tier
262
+ # the EXACT tier: a content-keyed store (a precise answer, a bounded
263
+ # reach -- only the last ~capacity contexts)
264
+ self.store = _ModeStore(capacity=capacity)
265
+ # the ROUTER tier: ONE entry per anchor of the fluid. The fluid's
266
+ # geometry decides WHICH anchor a context belongs to, so this tier
267
+ # has the WHOLE stream's reach (M entries, never evicted) at a
268
+ # cluster-level precision. "the fluid answers WHERE, the register
269
+ # answers WHAT" -- made literal.
270
+ self.anchor_store = _ModeStore(capacity=M)
271
+ self._buf: list[int] = [] # rolling context window
272
+ self.write_calls = 0
273
+ self.tokens_written = 0
274
+ self.store_hits = 0
275
+ self.store_lookups = 0
276
+
277
+ # the address graph (constant: the prototypes are fixed)
278
+ self.edges = _knn_edges(self.protos.data, k)
279
+ n = M
280
+ A = np.zeros((n, n))
281
+ for a, b in self.edges:
282
+ A[a, b] = 1.0
283
+ A[b, a] = 1.0
284
+ self.L = np.diag(A.sum(axis=1)) - A
285
+ self.lam_max = float(power_iteration_lmax(self.L))
286
+ self._cheb = (cheb_coeffs_exp(self.t, self.lam_max, self.cheb_m)
287
+ if self.lam_max > 0 else np.array([1.0]))
288
+
289
+ # ------------------------------------------------------------- lifecycle
290
+ def reset(self):
291
+ """Clear the memory (new document / new session)."""
292
+ self.state[:] = 0.0
293
+ self.store.clear()
294
+ self.anchor_store.clear()
295
+ self._buf.clear()
296
+
297
+ def support_snapshot(self, retrieved):
298
+ """The confirmation count of each retrieved value (0 = never seen)."""
299
+ return np.array([self.store.support.get(int(r), 0) if r >= 0 else 0
300
+ for r in retrieved], dtype=np.int64)
301
+
302
+ def confidence_snapshot(self, retrieved, keys=None):
303
+ """The corroboration ratio of each retrieved value."""
304
+ out = np.zeros(len(retrieved))
305
+ for i, r in enumerate(retrieved):
306
+ if r < 0:
307
+ continue
308
+ if keys is not None:
309
+ out[i] = self.confidence(int(keys[i]))
310
+ else:
311
+ out[i] = 1.0
312
+ return out
313
+
314
+ @property
315
+ def state_bytes(self) -> int:
316
+ """Logical payload estimate; excludes Python object overhead."""
317
+ return (self.state.size * 8 + len(self.store) * 20
318
+ + len(self.anchor_store) * 20)
319
+
320
+ def params(self) -> dict:
321
+ p = {
322
+ "Wq": self.Wq, "Wk": self.Wk, "Wv": self.Wv, "Wr": self.Wr,
323
+ "Wo_exact": self.Wo_exact, "Wo_lossy": self.Wo_lossy,
324
+ }
325
+ if self.Wp is not None:
326
+ p["Wp"] = self.Wp
327
+ p["bias"] = self.bias
328
+ if not self.trainable_route:
329
+ for k in ("Wq", "Wk"):
330
+ p.pop(k, None)
331
+ return p
332
+
333
+ # ------------------------------------------------------- the contrast table
334
+ def refresh_contrast(self):
335
+ """Recompute the mean-centred embedding table.
336
+
337
+ WHY: the readout is TIED (logits = h . emb.T), so injecting a token's
338
+ embedding v into the residual boosts the logit of every token u by
339
+ gain * (emb[v] . emb[u]). Embedding rows are NOT orthogonal, so the
340
+ raw injection also boosts v's embedding-space NEIGHBOURS -- measured:
341
+ the target's logit 134.9 vs the winner's 151.1, i.e. the injection
342
+ made the WRONG token win. Removing the shared (mean) direction
343
+ makes the boost selective: the target's logit then wins outright.
344
+
345
+ The table is cached because for a real LLM (V=50k, d=4096) computing
346
+ the mean on every forward would be absurd. Call refresh_contrast()
347
+ after the base model has been (re)trained.
348
+ """
349
+ self._contrast = None
350
+
351
+ def _contrast_table(self):
352
+ """The table the exact read pulls from: the embedding matrix, possibly
353
+ transformed so that the injection is SELECTIVE.
354
+
355
+ "raw" : the plain embedding (leaks into the neighbours)
356
+ "center" : mean-centred (removes the shared direction)
357
+ "pcK" : the top-K principal directions of the embedding matrix are
358
+ projected out. This is the one that works: with a TIED
359
+ readout, logit_u += gain * (emb[v] . emb[u]), so a raw
360
+ injection boosts v's embedding-space neighbours as well.
361
+ Measured on the graft benchmark: raw 0.725, pc1 0.800.
362
+ """
363
+ if self._contrast is None:
364
+ if self.emb is None:
365
+ return None
366
+ E = self.emb.data
367
+ t = self.read_transform
368
+ if t == "raw":
369
+ self._contrast = E.copy()
370
+ elif t == "center":
371
+ self._contrast = E - E.mean(axis=0, keepdims=True)
372
+ elif t.startswith("pc"):
373
+ k = int(t[2:] or 1)
374
+ # the SVD is O(V d^2): for a real LLM do it OFFLINE once and
375
+ # ship the table, or use a randomized SVD on a subsample.
376
+ _, _, Vt = np.linalg.svd(E, full_matrices=False)
377
+ T = E.copy()
378
+ for j in range(min(k, Vt.shape[0])):
379
+ p = Vt[j]
380
+ T = T - np.outer(T @ p, p)
381
+ self._contrast = T
382
+ else:
383
+ self._contrast = E.copy()
384
+ return self._contrast
385
+
386
+ # ----------------------------------------------------------------- maths
387
+ def _diffuse(self) -> np.ndarray:
388
+ """exp(-t L) applied to the anchor state, by Chebyshev expansion.
389
+
390
+ The state is a persistent, non-differentiable accumulator (it lives
391
+ outside the autodiff graph on purpose: it is a MEMORY, not an
392
+ activation). The gradient therefore flows only through the READ.
393
+ """
394
+ if self.lam_max <= 0:
395
+ return self.state
396
+ n = self.M
397
+ Lt = (2.0 / self.lam_max) * self.L - np.eye(n)
398
+ Y = [self.state]
399
+ if self.cheb_m >= 1:
400
+ Y.append(Lt @ Y[0])
401
+ for k in range(2, self.cheb_m + 1):
402
+ Y.append(2.0 * (Lt @ Y[k - 1]) - Y[k - 2])
403
+ out = np.zeros_like(self.state)
404
+ for k, a in enumerate(self._cheb):
405
+ if k < len(Y):
406
+ out += a * Y[k]
407
+ return out
408
+
409
+ def _diffuse_tensor(self, S: Tensor) -> Tensor:
410
+ """exp(-t L) applied to a TENSOR state (the differentiable path)."""
411
+ if self.lam_max <= 0:
412
+ return S
413
+ n = self.M
414
+ Lt = (2.0 / self.lam_max) * Tensor(self.L) - Tensor(np.eye(n))
415
+ Y = [S]
416
+ if self.cheb_m >= 1:
417
+ Y.append(Lt @ Y[0])
418
+ for k in range(2, self.cheb_m + 1):
419
+ Y.append(2.0 * (Lt @ Y[k - 1]) - Y[k - 2])
420
+ out = Tensor(np.zeros_like(S.data))
421
+ for k, a in enumerate(self._cheb):
422
+ if k < len(Y):
423
+ out = out + a * Y[k]
424
+ return out
425
+
426
+ def _rebuild_state(self, h: Tensor, content: Tensor):
427
+ """Rebuild the anchor state from a bounded history, inside the graph.
428
+
429
+ The diffusion is LINEAR, so exp(-tL)(sum of writes) = sum of
430
+ exp(-tL)(writes): the rebuilt state has the SAME VALUES as the
431
+ persistent accumulator, but now every write is differentiable.
432
+ """
433
+ parts = []
434
+ for (hh, cc) in self._hist:
435
+ w_np, _ = self._route(cc)
436
+ parts.append(Tensor(w_np).T @ (Tensor(hh) @ self.Wv))
437
+ w_np, _ = self._route(content.data)
438
+ parts.append(Tensor(w_np).T @ (h @ self.Wv))
439
+ S = parts[0]
440
+ for p in parts[1:]:
441
+ S = S + p
442
+ return self._diffuse_tensor(S)
443
+
444
+ def _route(self, content: np.ndarray):
445
+ """(T, d) content -> (T, M) soft top-k routing weights + the argmax."""
446
+ q = content @ self.Wq.data # (T, d)
447
+ kk = self.protos.data @ self.Wk.data # (M, d)
448
+ sc = (q @ kk.T) / (self.d ** 0.5) # (T, M)
449
+ top = np.argsort(-sc, axis=1)[:, : min(self.k, self.M)]
450
+ w = np.zeros_like(sc)
451
+ rows = np.arange(sc.shape[0])[:, None]
452
+ s_top = sc[rows, top]
453
+ s_top = s_top - s_top.max(axis=1, keepdims=True)
454
+ e = np.exp(s_top)
455
+ e = e / e.sum(axis=1, keepdims=True)
456
+ w[rows, top] = e
457
+ return w, top[:, 0]
458
+
459
+ # ---------------------------------------------------------------- forward
460
+ def forward(self, h: Tensor, content: Tensor, token_ids: np.ndarray):
461
+ """Return (h_out, info). `h` and `content` are Tensors (T, d).
462
+
463
+ `content` is the position-free representation used for routing.
464
+ `token_ids` are the tokens CONSUMED at each position.
465
+ """
466
+ token_ids = np.asarray(token_ids, dtype=np.int64)
467
+ T = token_ids.shape[0]
468
+ w_np, am = self._route(content.data) # routing is not differentiable
469
+ w = Tensor(w_np)
470
+
471
+ # ---- the lossy tier: write, diffuse, read ----------------------
472
+ out_l = None
473
+ state_read = None
474
+ if self.use_lossy:
475
+ if self.state_mode == "rebuild":
476
+ # differentiable: the state is rebuilt inside the graph
477
+ S = self._rebuild_state(h, content)
478
+ self.state = S.data.copy() # keep a numpy copy for inspection
479
+ state_read = w @ S # (T, r) <- differentiable
480
+ out_l = state_read @ self.Wr # (T, d)
481
+ else:
482
+ self.state += w_np.T @ (h.data @ self.Wv.data) # write
483
+ self.state = self._diffuse() # diffuse
484
+ state_read = w @ Tensor(self.state) # (T, r)
485
+ out_l = state_read @ self.Wr # (T, d)
486
+ if self.inject_mode == "prior" and self.Wp is not None:
487
+ z = self.layer_logits({"state_read": state_read})
488
+ if z is not None:
489
+ pr = softmax_rows(z) # (T, V)
490
+ out_l = pr @ self.emb.data # (T, d)
491
+
492
+ # ---- the exact tier: content-keyed store -----------------------
493
+ out_e = None
494
+ hits = 0
495
+ retrieved = np.full(T, -1, dtype=np.int64)
496
+ conf_arr = np.zeros(T, dtype=float)
497
+ router_retrieved = np.full(T, -1, dtype=np.int64)
498
+ router_conf = np.zeros(T, dtype=float)
499
+ if self.use_exact and self.emb is not None:
500
+ read_ids = np.zeros((T, self.d))
501
+ buf = list(self._buf)
502
+ for t in range(T):
503
+ prev_key = self._key(buf)
504
+ buf.append(int(token_ids[t]))
505
+ if len(buf) > self.ctx_len:
506
+ buf = buf[-self.ctx_len:]
507
+ key = self._key(buf)
508
+ self.store_lookups += 1
509
+ # the ROUTER tier first (it has the whole-stream reach),
510
+ # then the exact tier (a precise answer, a bounded reach)
511
+ if self.use_router:
512
+ av = self.anchor_store.get(int(am[t]), -1)
513
+ if av >= 0:
514
+ router_retrieved[t] = av
515
+ router_conf[t] = self.anchor_store.confidence(int(am[t]))
516
+ v = self.store.get(key, -1)
517
+ if v >= 0:
518
+ retrieved[t] = v
519
+ conf_arr[t] = self.confidence(key)
520
+ vec = self.emb.data[v]
521
+ if self.center:
522
+ C = self._contrast_table()
523
+ if C is not None:
524
+ vec = C[v]
525
+ if self.read_gain == "raw":
526
+ vec = vec * float(self.read_scale)
527
+ elif self.read_gain == "norm":
528
+ # inject the recalled token AT THE RESIDUAL'S OWN SCALE.
529
+ # Without this the injected embedding is ~50x smaller
530
+ # than the residual and simply cannot move the argmax.
531
+ hn = float(np.linalg.norm(h.data[t]))
532
+ vn = float(np.linalg.norm(vec))
533
+ if vn > 1e-12:
534
+ vec = vec * (hn / vn)
535
+ read_ids[t] = vec
536
+ hits += 1
537
+ # causal write: the pair (context ending at t-1, x_t) is
538
+ # complete NOW, so it is written NOW -- never earlier.
539
+ if prev_key is not None:
540
+ self._put(prev_key, int(token_ids[t]))
541
+ # the ROUTER tier: the pair is filed under the ANCHOR of the
542
+ # previous position -- the fluid's geometry decides which
543
+ # anchor a context belongs to, so this tier never evicts and
544
+ # its reach is the WHOLE stream.
545
+ if t > 0 and self.use_router:
546
+ self.anchor_store.put(int(am[t - 1]), int(token_ids[t]))
547
+ self._buf = buf
548
+ out_e = Tensor(read_ids)
549
+
550
+ # ---- combine ----------------------------------------------------
551
+ h_out = h
552
+ if out_e is not None:
553
+ h_out = h_out + out_e @ self.Wo_exact
554
+ if out_l is not None:
555
+ h_out = h_out + out_l @ self.Wo_lossy
556
+
557
+ if self.state_mode == "rebuild":
558
+ self._hist.append((h.data.copy(), content.data.copy()))
559
+ if len(self._hist) > self.hist_len:
560
+ self._hist = self._hist[-self.hist_len:]
561
+ self.write_calls += 1
562
+ self.tokens_written += T
563
+ self.store_hits += hits
564
+ return h_out, {"route": w_np, "anchor": am, "hits": hits,
565
+ "state_read": state_read, "emb_read": out_e,
566
+ "retrieved": retrieved,
567
+ "support": self.support_snapshot(retrieved),
568
+ "confidence": conf_arr,
569
+ "router_retrieved": router_retrieved,
570
+ "router_confidence": router_conf}
571
+
572
+ # ------------------------------------------------------------- the gate
573
+ def gated_logits(self, logits, info, gain: float = 8.0,
574
+ use_support: bool = False, support_scale: float = 1.0,
575
+ tau: float | None = None, c_min: float = 2.0,
576
+ big: float = 20.0, router_weight: float = 0.3,
577
+ gate_space: str = "logit"):
578
+ """Apply the retrieval as a GATED override, in logit space.
579
+
580
+ THE PROBLEM THIS SOLVES. The injection gain that maximises RECALL
581
+ (300) and the one that maximises GENERATION quality (0.3) are three
582
+ orders of magnitude apart: a hard override must be strong enough to
583
+ win the argmax, but a strong override forces a wrong retrieval into
584
+ the model and ruins it (+1.08 nats).
585
+
586
+ THE GATE. The base model's OWN uncertainty at that position:
587
+
588
+ u_t = 1 - max_v p(v)
589
+
590
+ and the retrieved token's logit is boosted by gain * u_t.
591
+
592
+ WHY ONE KNOB SERVES BOTH. In the recall task the base model has no
593
+ idea what comes next (u ~ 1), so the retrieval decides. In the
594
+ generation task the base model is usually confident (u ~ 0), so the
595
+ retrieval is ignored. The gate is not a second hyper-parameter --
596
+ it is the model's own confidence.
597
+
598
+ TWO RULES make this exact rather than merely safe:
599
+
600
+ RULE 1 (the clueless rule) -- **OFF BY DEFAULT** (`tau=2`, i.e. it
601
+ never fires). If the base model is clueless (u_t >= tau), the
602
+ retrieval decides outright. It is off because it was measured to
603
+ be DANGEROUS: on real text a character-level model's top-1
604
+ probability is often below 0.5, so the rule fired constantly and
605
+ forced wrong retrievals into the model (+0.65 nats, E11c).
606
+
607
+ It is also unnecessary: the SOFT GATE ALREADY delivers the full
608
+ recall. At u ~ 0.99 the boost is gain * u ~ 8, which is enough to
609
+ win the argmax, and a wrong override then costs ~8 nats instead
610
+ of ~60. Measured: the soft gate alone reaches a recall of
611
+ **1.000** (E11a).
612
+
613
+ RULE 2 (the trusted-memory rule) -- **OFF BY DEFAULT** (`c_min=2`,
614
+ i.e. it never fires). If the retrieval has never been contradicted
615
+ (its confidence >= c_min), it would decide as well. It is off
616
+ because it was measured to be dangerous: on real text many
617
+ contexts have a deterministic-looking continuation that is still
618
+ wrong for a new occurrence, and forcing those overrides made the
619
+ base model's loss explode (E11c). The base model's own
620
+ uncertainty (RULE 1) is the reliable signal.
621
+
622
+ Otherwise: the soft gate, boost = gain * u_t.
623
+
624
+ `logits` is (T, V) numpy or a Tensor; the result is the same type.
625
+ """
626
+ from .autodiff import Tensor as _T
627
+ is_tensor = isinstance(logits, _T)
628
+ z = logits.data if is_tensor else np.asarray(logits)
629
+ retrieved = info.get("retrieved")
630
+ if retrieved is None:
631
+ return logits
632
+ retrieved = np.asarray(retrieved)
633
+ T, V = z.shape
634
+ # THE FALLBACK CHAIN. The exact tier answers when it has the
635
+ # context (a precise answer, a bounded reach). When it does not --
636
+ # the context is older than the store's reach -- the ROUTER tier
637
+ # answers instead: the fluid's geometry puts the context in a
638
+ # cluster, and the cluster's majority continuation is the best
639
+ # available answer. It gets a smaller boost (router_weight),
640
+ # because it is a cluster-level guess, not a fact.
641
+ rr = info.get("router_retrieved")
642
+ rconf = info.get("router_confidence")
643
+ if rr is not None:
644
+ rr = np.asarray(rr)
645
+ fallback = (retrieved < 0) & (rr >= 0)
646
+ retrieved = np.where(fallback, rr, retrieved)
647
+ else:
648
+ fallback = np.zeros(T, dtype=bool)
649
+ # THE MODEL'S OWN UNCERTAINTY.
650
+ # "logit" space (the default): u = -log p_max, i.e. how many nats
651
+ # the model is away from being sure. This is SCALE-FREE: the same
652
+ # gain works on a 131k-parameter model (whose logits span +-2) and on
653
+ # GPT-2 (whose logits span +-20). Measured on real GPT-2: the
654
+ # probability-space gate needed a gain of 50+ and hurt the
655
+ # perplexity; the logit-space gate works at a gain of ~5.
656
+ # "prob" space: u = 1 - p_max (the original formulation).
657
+ # The "never fires" threshold depends on the gate's units: in
658
+ # probability space u <= 1, in logit space u is unbounded (nats).
659
+ if tau is None:
660
+ tau = 2.0 if str(gate_space) != "logit" else 20.0
661
+ e = np.exp(z - z.max(axis=1, keepdims=True))
662
+ p = e / e.sum(axis=1, keepdims=True)
663
+ if str(gate_space) == "logit":
664
+ u = -np.log(np.clip(p.max(axis=1), 1e-12, None))
665
+ else:
666
+ u = 1.0 - p.max(axis=1)
667
+ boost = gain * u
668
+ # the router tier's answer is a CLUSTER-LEVEL guess: a smaller boost,
669
+ # and scaled by how much the cluster actually agrees with itself.
670
+ # Without the confidence scaling the fallback overrules the base
671
+ # model on contexts where the cluster's answer is a coin flip
672
+ # (measured: the fact-task recall fell from 1.000 to 0.625).
673
+ if rconf is not None:
674
+ rc = np.asarray(rconf, dtype=float)
675
+ boost = np.where(fallback,
676
+ boost * float(router_weight) * np.clip(rc, 0.0, 1.0),
677
+ boost)
678
+ if use_support:
679
+ sup = info.get("support")
680
+ if sup is not None:
681
+ conf = np.minimum(1.0, np.asarray(sup, dtype=float) / support_scale)
682
+ boost = boost * conf
683
+ # RULE 2: the retrieval's own confidence
684
+ conf = info.get("confidence")
685
+ if conf is None:
686
+ conf = np.zeros(T)
687
+ else:
688
+ conf = np.asarray(conf, dtype=float)
689
+ if rconf is not None:
690
+ conf = np.where(fallback, np.asarray(rconf, dtype=float), conf)
691
+ decide = (u >= tau) | (conf >= c_min)
692
+ boost = np.where(decide, big, boost)
693
+ out = z.copy()
694
+ rows = np.nonzero(retrieved >= 0)[0]
695
+ if len(rows):
696
+ out[rows, retrieved[rows]] += boost[rows]
697
+ return _T(out) if is_tensor else out
698
+
699
+ # ------------------------------------------------- the layer-local head
700
+ def layer_logits(self, info):
701
+ """The layer's OWN next-token prediction, from its own state.
702
+
703
+ This is the objective that replaces the base model's loss. It is
704
+ well-posed (the target is the token that actually followed), it never
705
+ touches the base model's parameters, and therefore it CANNOT diverge
706
+ the way the base-model-loss training did.
707
+ """
708
+ sr = info.get("state_read")
709
+ if sr is None or self.Wp is None:
710
+ return None
711
+ return sr @ self.Wp + self.bias
712
+
713
+ # ------------------------------------------------------------- the store
714
+ def _key(self, buf):
715
+ if len(buf) < self.ctx_len:
716
+ return None
717
+ if self.ctx_len == 1:
718
+ return int(buf[-1])
719
+ return ctx_hash(buf[-self.ctx_len:])
720
+
721
+ def _put(self, key: int, value: int):
722
+ """Record an occurrence: (context -> the token that followed it).
723
+
724
+ The store keeps the MAJORITY continuation, not the first one. The
725
+ first-write-wins policy was exact but blind: a context that occurs
726
+ everywhere kept only its first continuation, so the statistically
727
+ correct answer was lost (measured: 0/3). An online mode tracker
728
+ keeps the statistically correct answer AND counts the
729
+ corroboration, which gives a CONFIDENCE for free.
730
+ """
731
+ self.store.put(key, value)
732
+
733
+ def confidence(self, key: int) -> float:
734
+ """How often the stored value has been corroborated (1.0 = never
735
+ contradicted). 0.0 for an unknown context."""
736
+ return self.store.confidence(key)
737
+
738
+
739
+ def GRAFT_STATE_BYTES(M: int, r: int) -> int:
740
+ """Bytes of persistent state for M anchors of rank r (exact tier + fluid)."""
741
+ return M * r * 8 + 20 * 819