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