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/losses.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fluidlayer.losses — training objective for the fluid stack.
|
|
3
|
+
|
|
4
|
+
L_total = CE(next-token)
|
|
5
|
+
+ w_E * sheaf energy (smoothness / consistency on edges)
|
|
6
|
+
+ w_D * band decorrelation (keep the filter bank from collapsing)
|
|
7
|
+
+ w_G * spectral gap bonus (anti-oversmoothing, diagnostic only)
|
|
8
|
+
|
|
9
|
+
The "topological sufficiency" term of the framework spec is implemented as a
|
|
10
|
+
*probe* (see `probe.py`) rather than as a gradient, because the routing
|
|
11
|
+
decision is a hard top-k selection and carries no gradient path.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
__all__ = ["cross_entropy", "sheaf_energy_loss", "band_decorrelation", "gap_bonus"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def cross_entropy(logits, targets) -> float:
|
|
22
|
+
"""Mean token cross-entropy from logits (n, V) and targets (n,)."""
|
|
23
|
+
z = logits.data
|
|
24
|
+
z = z - z.max(axis=1, keepdims=True)
|
|
25
|
+
logZ = np.log(np.exp(z).sum(axis=1))
|
|
26
|
+
ll = z[np.arange(len(targets)), targets] - logZ
|
|
27
|
+
return float(-ll.mean())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def cross_entropy_grad(logits, targets) -> np.ndarray:
|
|
31
|
+
"""d(mean CE)/d(logits) — (n, V)."""
|
|
32
|
+
z = logits.data
|
|
33
|
+
z = z - z.max(axis=1, keepdims=True)
|
|
34
|
+
e = np.exp(z)
|
|
35
|
+
p = e / e.sum(axis=1, keepdims=True)
|
|
36
|
+
g = p.copy()
|
|
37
|
+
g[np.arange(len(targets)), targets] -= 1.0
|
|
38
|
+
return g / len(targets)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cross_entropy_tensor(logits, targets):
|
|
42
|
+
"""Differentiable mean token cross-entropy (returns a Tensor)."""
|
|
43
|
+
n = len(targets)
|
|
44
|
+
m = logits.max(axis=1, keepdims=True)
|
|
45
|
+
z = logits - m
|
|
46
|
+
logZ = z.exp().sum(axis=1, keepdims=True).log() + m
|
|
47
|
+
picked = logits[np.arange(n), np.asarray(targets, dtype=np.int64)]
|
|
48
|
+
return (-(picked - logZ.reshape(-1))).mean()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def sheaf_energy_loss(block) -> float:
|
|
52
|
+
"""Total variation of the field over the routed edges (diagnostic scalar)."""
|
|
53
|
+
d = block.diag
|
|
54
|
+
if not d:
|
|
55
|
+
return 0.0
|
|
56
|
+
return float(d.get("sheaf_energy", 0.0))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def band_decorrelation(bands) -> float:
|
|
60
|
+
"""1 - mean |cos| between band responses.
|
|
61
|
+
|
|
62
|
+
If two bands collapse onto the same spectral mode the filter bank stops
|
|
63
|
+
being a frame and information is lost (the rank-c trap in a different
|
|
64
|
+
disguise). This penalises that.
|
|
65
|
+
"""
|
|
66
|
+
if not bands or len(bands) < 2:
|
|
67
|
+
return 0.0
|
|
68
|
+
M = []
|
|
69
|
+
for b in bands:
|
|
70
|
+
v = b.data.reshape(-1)
|
|
71
|
+
nrm = np.linalg.norm(v)
|
|
72
|
+
M.append(v / nrm if nrm > 1e-12 else v)
|
|
73
|
+
M = np.stack(M)
|
|
74
|
+
C = M @ M.T
|
|
75
|
+
off = C - np.eye(len(M)) * np.diag(C)
|
|
76
|
+
iu = np.triu_indices(len(M), k=1)
|
|
77
|
+
return float(np.mean(np.abs(off[iu])))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def gap_bonus(L) -> float:
|
|
81
|
+
"""lambda_2 of a (numpy) Laplacian -- anti-oversmoothing diagnostic."""
|
|
82
|
+
from .diffusion import spectral_gap
|
|
83
|
+
try:
|
|
84
|
+
return float(spectral_gap(L))
|
|
85
|
+
except Exception:
|
|
86
|
+
return 0.0
|
fluidlayer/memory.py
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fluidlayer.memory — the FROZEN REGISTER and the geometry-addressed memory graft.
|
|
3
|
+
This module implements the two ideas that make the fluid trustworthy:
|
|
4
|
+
A frozen register (L1): a bounded append-only table of token IDs. A value
|
|
5
|
+
is stored as an INTEGER token id, so a retrieved value is bit-identical to
|
|
6
|
+
the stored ID -- not float-approximate. The
|
|
7
|
+
register is never diffused, so the fluid's smoothing cannot destroy it.
|
|
8
|
+
A sheaf memory adapter (L2, the "graft"): a constant-size state
|
|
9
|
+
S in R^{M x r} plus a static prototype codebook. The adapter routes
|
|
10
|
+
(geometry decides WHERE) and the register stores (content decides
|
|
11
|
+
WHAT). That split is what makes exactness possible at all: a diffusion
|
|
12
|
+
smears, so an exact value must never live inside one.
|
|
13
|
+
An external tier (L3): a pointer into the original source. A
|
|
14
|
+
pointer is 6 bytes and is unbounded, so it is the fallback whenever the
|
|
15
|
+
register cannot answer.
|
|
16
|
+
Slot layout (20 bytes, little-endian):
|
|
17
|
+
key uint64 8 B 64-bit context hash (the OCCURRENCE identity)
|
|
18
|
+
val int32 4 B the exact token id
|
|
19
|
+
sig uint32 4 B 4 x uint8 embedding signature (collision detector)
|
|
20
|
+
anchor uint16 2 B the anchor the fact was routed to when promoted
|
|
21
|
+
meta uint16 2 B doc_id (12 bits) | trust (2 bits) | reserved (2)
|
|
22
|
+
Why 20 bytes and not the 14 quoted in docs/DESIGN_pinned_memory.md: the 4-byte
|
|
23
|
+
embedding signature was added so that a 64-bit hash collision is DETECTABLE
|
|
24
|
+
rather than silent ("fail loud, never silently wrong"). The cost of that
|
|
25
|
+
guarantee is 6 bytes/fact, i.e. 819 facts in 16 KB instead of 1,170.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"FNV_OFFSET", "FNV_PRIME", "MASK64", "fnv1a64", "ctx_key",
|
|
33
|
+
"FrozenRegister", "SheafMemoryAdapter", "logit_override",
|
|
34
|
+
"budget_split", "embedding_signature", "SLOT_BYTES",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# ------------------------------------------------------------------ hashing
|
|
38
|
+
FNV_OFFSET = 0xCBF29CE484222325
|
|
39
|
+
FNV_PRIME = 0x100000001B3
|
|
40
|
+
MASK64 = (1 << 64) - 1
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def fnv1a64(values) -> int:
|
|
44
|
+
"""Deterministic 64-bit FNV-1a hash over a sequence of integers.
|
|
45
|
+
|
|
46
|
+
Pure python, no dependencies, and stable across runs/platforms (unlike
|
|
47
|
+
Python's salted `hash()`), so a register written today can be verified
|
|
48
|
+
tomorrow.
|
|
49
|
+
"""
|
|
50
|
+
h = FNV_OFFSET
|
|
51
|
+
for v in values:
|
|
52
|
+
h ^= int(v) & MASK64
|
|
53
|
+
h = (h * FNV_PRIME) & MASK64
|
|
54
|
+
return h
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def ctx_key(window, doc_id: int = 0) -> int:
|
|
58
|
+
"""64-bit identity of a CONTEXT OCCURRENCE (doc_id + token window).
|
|
59
|
+
|
|
60
|
+
The same content in two different documents gets two different keys, so
|
|
61
|
+
a register can be told apart per source; the fact-level grouping (for the
|
|
62
|
+
corroboration quorum) is handled by the caller, which passes the same
|
|
63
|
+
`window` with different `doc_id`s.
|
|
64
|
+
"""
|
|
65
|
+
if isinstance(window, (int, np.integer)):
|
|
66
|
+
window = (int(window),)
|
|
67
|
+
vals = [int(doc_id) & 0xFFF]
|
|
68
|
+
vals.extend(int(w) & MASK64 for w in window)
|
|
69
|
+
return fnv1a64(vals)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def embedding_signature(vec: np.ndarray, dims: int = 4) -> int:
|
|
73
|
+
"""Quantise the first `dims` coordinates of a unit vector to a uint32.
|
|
74
|
+
|
|
75
|
+
Used as a cheap second channel: if the 64-bit key collides, the
|
|
76
|
+
signature almost certainly will not, and the read fails loud instead of
|
|
77
|
+
returning a wrong exact value.
|
|
78
|
+
"""
|
|
79
|
+
v = np.asarray(vec, dtype=np.float64).ravel()
|
|
80
|
+
if v.size == 0:
|
|
81
|
+
return 0
|
|
82
|
+
n = np.linalg.norm(v[:dims]) + 1e-12
|
|
83
|
+
q = np.clip(np.round(v[:dims] / n * 127.0) + 128.0, 0, 255).astype(np.uint8)
|
|
84
|
+
out = 0
|
|
85
|
+
for i in range(dims):
|
|
86
|
+
out |= int(q[i]) << (8 * i)
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ------------------------------------------------------------------ register
|
|
91
|
+
SLOT_BYTES = 20
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class FrozenRegister:
|
|
95
|
+
"""Append-only, write-once table of EXACT facts.
|
|
96
|
+
|
|
97
|
+
Guarantees
|
|
98
|
+
----------
|
|
99
|
+
* **Invariance.** A slot that holds (key, val) at time t still holds
|
|
100
|
+
(key, val) at every later time, bit-for-bit. Nothing in the write
|
|
101
|
+
path can modify an existing slot.
|
|
102
|
+
* **Integer exactness.** `val` is an int32 token id, so a successful
|
|
103
|
+
recall returns a value that is *equal*, not approximately equal.
|
|
104
|
+
* **Fail-loud.** A recall verifies both the 64-bit key and the 4-byte
|
|
105
|
+
embedding signature. On mismatch the register reports "not found"
|
|
106
|
+
instead of returning a value that may belong to another fact.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
SLOT_BYTES = SLOT_BYTES
|
|
110
|
+
|
|
111
|
+
def __init__(self, capacity: int = 1024):
|
|
112
|
+
self.capacity = int(capacity)
|
|
113
|
+
self.key = np.zeros(self.capacity, dtype=np.uint64)
|
|
114
|
+
self.val = np.zeros(self.capacity, dtype=np.int32)
|
|
115
|
+
self.sig = np.zeros(self.capacity, dtype=np.uint32)
|
|
116
|
+
self.anchor = np.zeros(self.capacity, dtype=np.uint16)
|
|
117
|
+
self.meta = np.zeros(self.capacity, dtype=np.uint16)
|
|
118
|
+
self.used = np.zeros(self.capacity, dtype=bool)
|
|
119
|
+
self.n = 0
|
|
120
|
+
self._by_key: dict[int, list[int]] = {}
|
|
121
|
+
|
|
122
|
+
# ------------------------------------------------------------- encoding
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _pack_meta(doc_id: int, trust: int) -> int:
|
|
125
|
+
return (int(doc_id) & 0x0FFF) | ((int(trust) & 0x3) << 12)
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _unpack_meta(meta: int):
|
|
129
|
+
return int(meta) & 0x0FFF, (int(meta) >> 12) & 0x3
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------- write
|
|
132
|
+
def promote(self, val: int, key: int, sig: int = 0, anchor: int = 0,
|
|
133
|
+
doc_id: int = 0, trust: int = 0) -> int:
|
|
134
|
+
"""Append a NEW slot. Returns the slot index.
|
|
135
|
+
|
|
136
|
+
Append-only means a fact that is promoted twice gets two slots and
|
|
137
|
+
the *latest* wins on recall. The register is not a tamper-evident
|
|
138
|
+
audit log.
|
|
139
|
+
"""
|
|
140
|
+
if self.n >= self.capacity:
|
|
141
|
+
raise MemoryError(
|
|
142
|
+
f"frozen register is full ({self.capacity} slots x {SLOT_BYTES} B "
|
|
143
|
+
f"= {self.capacity * SLOT_BYTES} B). Overflow is REFUSED on purpose: "
|
|
144
|
+
f"a register that silently grows becomes a KV cache.")
|
|
145
|
+
s = self.n
|
|
146
|
+
self.key[s] = np.uint64(key & MASK64)
|
|
147
|
+
self.val[s] = np.int32(val)
|
|
148
|
+
self.sig[s] = np.uint32(sig & 0xFFFFFFFF)
|
|
149
|
+
self.anchor[s] = np.uint16(anchor)
|
|
150
|
+
self.meta[s] = np.uint16(self._pack_meta(doc_id, trust))
|
|
151
|
+
self.used[s] = True
|
|
152
|
+
self.n += 1
|
|
153
|
+
self._by_key.setdefault(int(key), []).append(s)
|
|
154
|
+
return s
|
|
155
|
+
|
|
156
|
+
def set_trust(self, slot: int, trust: int = 1) -> None:
|
|
157
|
+
"""Raise the trust bit of an existing slot (corroboration gate)."""
|
|
158
|
+
doc, _ = self._unpack_meta(int(self.meta[slot]))
|
|
159
|
+
self.meta[slot] = np.uint16(self._pack_meta(doc, trust))
|
|
160
|
+
|
|
161
|
+
# ----------------------------------------------------------------- read
|
|
162
|
+
def slots_for(self, key: int) -> list[int]:
|
|
163
|
+
return self._by_key.get(int(key), [])
|
|
164
|
+
|
|
165
|
+
def latest(self, key: int):
|
|
166
|
+
"""Most recent slot holding `key`, or None."""
|
|
167
|
+
sl = self._by_key.get(int(key))
|
|
168
|
+
return sl[-1] if sl else None
|
|
169
|
+
|
|
170
|
+
def verify(self, slot: int, key: int, sig: int | None = None) -> bool:
|
|
171
|
+
"""True iff the slot really holds this occurrence."""
|
|
172
|
+
if not (0 <= slot < self.n) or not self.used[slot]:
|
|
173
|
+
return False
|
|
174
|
+
if int(self.key[slot]) != (int(key) & MASK64):
|
|
175
|
+
return False
|
|
176
|
+
if sig is not None and int(self.sig[slot]) != (int(sig) & 0xFFFFFFFF):
|
|
177
|
+
return False
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
def recall(self, key: int, sig: int | None = None):
|
|
181
|
+
"""Return (slot, exact_value) or None.
|
|
182
|
+
|
|
183
|
+
`sig` is the embedding signature of the query. When supplied, a
|
|
184
|
+
signature mismatch makes the recall FAIL (return None) instead of
|
|
185
|
+
returning another fact's value -- this is the fail-loud path.
|
|
186
|
+
"""
|
|
187
|
+
s = self.latest(key)
|
|
188
|
+
if s is None:
|
|
189
|
+
return None
|
|
190
|
+
if sig is not None and int(self.sig[s]) != (int(sig) & 0xFFFFFFFF):
|
|
191
|
+
return None # hash collision or wrong occurrence -> fail loud
|
|
192
|
+
return s, int(self.val[s])
|
|
193
|
+
|
|
194
|
+
def read_slot(self, slot: int):
|
|
195
|
+
"""Raw read of a slot (no verification) -> (key, val, sig, anchor, trust)."""
|
|
196
|
+
if not (0 <= slot < self.n) or not self.used[slot]:
|
|
197
|
+
return None
|
|
198
|
+
doc, trust = self._unpack_meta(int(self.meta[slot]))
|
|
199
|
+
return (int(self.key[slot]), int(self.val[slot]), int(self.sig[slot]),
|
|
200
|
+
int(self.anchor[slot]), doc, trust)
|
|
201
|
+
|
|
202
|
+
def trust_of(self, slot: int) -> int:
|
|
203
|
+
return self._unpack_meta(int(self.meta[slot]))[1]
|
|
204
|
+
|
|
205
|
+
def doc_of(self, slot: int) -> int:
|
|
206
|
+
return self._unpack_meta(int(self.meta[slot]))[0]
|
|
207
|
+
|
|
208
|
+
def quorum(self, key: int) -> int:
|
|
209
|
+
"""Number of DISTINCT sources that promoted this key.
|
|
210
|
+
|
|
211
|
+
The corroboration gate: a fact promoted by two independent documents
|
|
212
|
+
is "trusted"; a fact promoted once stays "suspect". Because the key
|
|
213
|
+
includes doc_id, two different documents produce different keys --
|
|
214
|
+
so the caller must pass the doc-independent fact key. We therefore
|
|
215
|
+
count distinct doc_ids stored under keys that share the same
|
|
216
|
+
*occurrence signature* is not possible here; instead we count the
|
|
217
|
+
distinct doc_ids across all slots of the given key, which is exact
|
|
218
|
+
for a fixed (window, doc) pair and conservative otherwise.
|
|
219
|
+
"""
|
|
220
|
+
sl = self._by_key.get(int(key), [])
|
|
221
|
+
return len({self.doc_of(s) for s in sl})
|
|
222
|
+
|
|
223
|
+
# ------------------------------------------------------------- accounting
|
|
224
|
+
def nbytes(self) -> int:
|
|
225
|
+
"""Bytes of *state* held (the serialised slot table)."""
|
|
226
|
+
return self.capacity * SLOT_BYTES
|
|
227
|
+
|
|
228
|
+
def used_bytes(self) -> int:
|
|
229
|
+
return self.n * SLOT_BYTES
|
|
230
|
+
|
|
231
|
+
def stats(self) -> dict:
|
|
232
|
+
trusts = [self.trust_of(s) for s in range(self.n)] if self.n else []
|
|
233
|
+
return {
|
|
234
|
+
"capacity_slots": self.capacity,
|
|
235
|
+
"used_slots": int(self.n),
|
|
236
|
+
"slot_bytes": SLOT_BYTES,
|
|
237
|
+
"state_bytes": self.nbytes(),
|
|
238
|
+
"used_bytes": self.used_bytes(),
|
|
239
|
+
"n_trusted": int(sum(1 for t in trusts if t)),
|
|
240
|
+
"fill_fraction": float(self.n / max(self.capacity, 1)),
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
def to_bytes(self) -> bytes:
|
|
244
|
+
return (self.key.tobytes() + self.val.tobytes() + self.sig.tobytes() +
|
|
245
|
+
self.anchor.tobytes() + self.meta.tobytes() +
|
|
246
|
+
self.used.tobytes())
|
|
247
|
+
|
|
248
|
+
@classmethod
|
|
249
|
+
def from_bytes(cls, blob: bytes, capacity: int) -> FrozenRegister:
|
|
250
|
+
r = cls(capacity=capacity)
|
|
251
|
+
n = capacity
|
|
252
|
+
p = 0
|
|
253
|
+
|
|
254
|
+
def take(arr, dt):
|
|
255
|
+
nonlocal p
|
|
256
|
+
out = np.frombuffer(blob[p:p + n * np.dtype(dt).itemsize], dtype=dt).copy()
|
|
257
|
+
p += n * np.dtype(dt).itemsize
|
|
258
|
+
return out
|
|
259
|
+
|
|
260
|
+
r.key = take(None, np.uint64)
|
|
261
|
+
r.val = take(None, np.int32)
|
|
262
|
+
r.sig = take(None, np.uint32)
|
|
263
|
+
r.anchor = take(None, np.uint16)
|
|
264
|
+
r.meta = take(None, np.uint16)
|
|
265
|
+
r.used = take(None, np.uint8).astype(bool)
|
|
266
|
+
r.n = int(r.used.sum())
|
|
267
|
+
r._by_key = {}
|
|
268
|
+
for s in range(r.n):
|
|
269
|
+
r._by_key.setdefault(int(r.key[s]), []).append(s)
|
|
270
|
+
return r
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ------------------------------------------------------------------ override
|
|
274
|
+
def logit_override(logits: np.ndarray, token_ids) -> np.ndarray:
|
|
275
|
+
"""Force `token_ids` to win the argmax, deterministically.
|
|
276
|
+
|
|
277
|
+
This is the difference between "the model was told the answer" and "the
|
|
278
|
+
model cannot disagree": a tool that merely *returns* a token can be
|
|
279
|
+
outvoted by the fluid's own logits, so the extraction would be
|
|
280
|
+
probabilistic. Overriding the logit makes the next token provably the
|
|
281
|
+
recalled one.
|
|
282
|
+
|
|
283
|
+
Accepts logits of shape (V,) or (B, V), as a plain ndarray **or** as a
|
|
284
|
+
framework Tensor. `token_ids` is an int or a sequence of ints (one per
|
|
285
|
+
row for the 2-D case).
|
|
286
|
+
"""
|
|
287
|
+
# accept a framework Tensor as well as a plain ndarray
|
|
288
|
+
if hasattr(logits, "data"):
|
|
289
|
+
logits = logits.data
|
|
290
|
+
out = np.array(logits, dtype=np.float64, copy=True)
|
|
291
|
+
if out.ndim == 1:
|
|
292
|
+
if out.shape[0] == 0:
|
|
293
|
+
raise ValueError("empty logits")
|
|
294
|
+
out[int(token_ids)] = np.inf
|
|
295
|
+
return out
|
|
296
|
+
if out.ndim == 2:
|
|
297
|
+
ids = np.atleast_1d(np.asarray(token_ids, dtype=np.int64))
|
|
298
|
+
if ids.size == 1:
|
|
299
|
+
ids = np.full(out.shape[0], ids[0], dtype=np.int64)
|
|
300
|
+
if ids.size != out.shape[0]:
|
|
301
|
+
raise ValueError("token_ids length must match the batch")
|
|
302
|
+
rows = np.arange(out.shape[0])
|
|
303
|
+
out[rows, ids] = np.inf
|
|
304
|
+
return out
|
|
305
|
+
raise ValueError("logits must be 1-D or 2-D")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ------------------------------------------------------------------- budget
|
|
309
|
+
def budget_split(total_bytes: int, M: int, r: int, slot_bytes: int = SLOT_BYTES,
|
|
310
|
+
codebook_bytes: int = 0) -> dict:
|
|
311
|
+
"""Split a RUNTIME byte budget between the fluid state and the frozen register.
|
|
312
|
+
|
|
313
|
+
`total_bytes` is the *runtime* budget (what a tenant pays per session).
|
|
314
|
+
`codebook_bytes` is the STATIC artefact (prototypes + value projection),
|
|
315
|
+
which is shared across tenants and therefore reported, not subtracted.
|
|
316
|
+
|
|
317
|
+
The design law from docs/DESIGN_pinned_memory.md: **naming one anchor out of M
|
|
318
|
+
needs log2(M) bits, while an anchor holds r*16 bits.** So r is grossly
|
|
319
|
+
over-provisioned for addressing, and a bigger budget should be spent on
|
|
320
|
+
MORE PINS, not on more anchors. Growing the fluid raises the per-token
|
|
321
|
+
compute linearly in M; growing the register costs nothing at inference.
|
|
322
|
+
|
|
323
|
+
If the requested M does not fit, M is reduced to the largest value that
|
|
324
|
+
does -- and the reduction is reported, because silently over-committing
|
|
325
|
+
the budget is how a "16 KB" claim turns into a 32 KB claim.
|
|
326
|
+
"""
|
|
327
|
+
total = int(total_bytes)
|
|
328
|
+
r = int(r)
|
|
329
|
+
M_max = total // max(r * 2, 1)
|
|
330
|
+
M_used = int(min(int(M), M_max))
|
|
331
|
+
fluid = M_used * r * 2
|
|
332
|
+
rest = max(total - fluid, 0)
|
|
333
|
+
pins = rest // int(slot_bytes)
|
|
334
|
+
return {
|
|
335
|
+
"total_bytes": total,
|
|
336
|
+
"M_requested": int(M),
|
|
337
|
+
"M_used": M_used,
|
|
338
|
+
"M_reduced": bool(M_used < int(M)),
|
|
339
|
+
"r": r,
|
|
340
|
+
"fluid_bytes": fluid,
|
|
341
|
+
"register_bytes": int(pins) * int(slot_bytes),
|
|
342
|
+
"pins": int(pins),
|
|
343
|
+
"codebook_bytes": int(codebook_bytes),
|
|
344
|
+
"artefact_bytes": int(total) + int(codebook_bytes),
|
|
345
|
+
"fluid_pct": 100.0 * fluid / max(total, 1),
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ------------------------------------------------------------------ adapter
|
|
350
|
+
class SheafMemoryAdapter:
|
|
351
|
+
"""Constant-size, geometry-addressed memory with an exact frozen register.
|
|
352
|
+
|
|
353
|
+
Layout
|
|
354
|
+
------
|
|
355
|
+
* `protos` (M, d) static prototype codebook -- part of the *prior
|
|
356
|
+
artefact*, not the runtime state.
|
|
357
|
+
* `anchors` (M, r) the runtime fluid state (the "16 KB").
|
|
358
|
+
* `register` the exact tier (append-only).
|
|
359
|
+
* `W_in` (d, r) value projection, part of the codebook.
|
|
360
|
+
|
|
361
|
+
Read path: query -> nearest prototype (address) -> anchor content (gist)
|
|
362
|
+
Exact path: query -> content key -> register slot -> INTEGER token id.
|
|
363
|
+
|
|
364
|
+
The two paths are deliberately separate. A diffusion smears, so the
|
|
365
|
+
value that must be exact never enters the diffusion.
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
def __init__(self, M: int = 1024, r: int = 8, d: int = 64,
|
|
369
|
+
capacity: int = 1024, seed: int = 0):
|
|
370
|
+
self.M, self.r, self.d = int(M), int(r), int(d)
|
|
371
|
+
rng = np.random.default_rng(seed)
|
|
372
|
+
self.rng = rng
|
|
373
|
+
self.protos = None
|
|
374
|
+
self.anchors = np.zeros((self.M, self.r), dtype=np.float64)
|
|
375
|
+
self.register = FrozenRegister(capacity=capacity)
|
|
376
|
+
self.W_in = rng.normal(0.0, self.d ** -0.5, (self.d, self.r))
|
|
377
|
+
self._L = None
|
|
378
|
+
self._lam_max = None
|
|
379
|
+
self.write_calls = 0
|
|
380
|
+
self.tokens_written = 0
|
|
381
|
+
|
|
382
|
+
# ------------------------------------------------------------- codebook
|
|
383
|
+
def set_prototypes(self, protos: np.ndarray) -> None:
|
|
384
|
+
"""Install the static codebook (M, d) and rebuild the anchor graph."""
|
|
385
|
+
P = np.asarray(protos, dtype=np.float64)
|
|
386
|
+
if P.shape != (self.M, self.d):
|
|
387
|
+
raise ValueError(f"prototypes must be ({self.M}, {self.d}), got {P.shape}")
|
|
388
|
+
self.protos = P
|
|
389
|
+
self._L = None
|
|
390
|
+
|
|
391
|
+
def anchor_graph(self, k: int = 8):
|
|
392
|
+
"""k-NN graph over the prototypes -> signed sheaf Laplacian (numpy)."""
|
|
393
|
+
if self.protos is None:
|
|
394
|
+
raise RuntimeError("set_prototypes() first")
|
|
395
|
+
from .sheaf import build_laplacian
|
|
396
|
+
Pn = self.protos / (np.linalg.norm(self.protos, axis=1, keepdims=True) + 1e-12)
|
|
397
|
+
S = Pn @ Pn.T
|
|
398
|
+
np.fill_diagonal(S, -np.inf)
|
|
399
|
+
nbr = np.argpartition(-S, kth=min(k, self.M - 1) - 1, axis=1)[:, :k]
|
|
400
|
+
rows = np.repeat(np.arange(self.M), k)
|
|
401
|
+
cols = nbr.reshape(-1)
|
|
402
|
+
u = np.minimum(rows, cols)
|
|
403
|
+
v = np.maximum(rows, cols)
|
|
404
|
+
key = u.astype(np.int64) * self.M + v
|
|
405
|
+
_, keep = np.unique(key, return_index=True)
|
|
406
|
+
edges = np.stack([u[keep], v[keep]], axis=1)
|
|
407
|
+
alpha = np.maximum(S[edges[:, 0], edges[:, 1]], 0.0) + 1e-3
|
|
408
|
+
return edges, alpha, build_laplacian(self.M, edges, alpha)
|
|
409
|
+
|
|
410
|
+
# --------------------------------------------------------------- routing
|
|
411
|
+
def route(self, q: np.ndarray, k: int = 1):
|
|
412
|
+
"""Nearest prototype(s) for a query. Returns (idx, scores)."""
|
|
413
|
+
if self.protos is None:
|
|
414
|
+
raise RuntimeError("set_prototypes() first")
|
|
415
|
+
q = np.atleast_2d(np.asarray(q, dtype=np.float64))
|
|
416
|
+
Pn = self.protos / (np.linalg.norm(self.protos, axis=1, keepdims=True) + 1e-12)
|
|
417
|
+
qn = q / (np.linalg.norm(q, axis=1, keepdims=True) + 1e-12)
|
|
418
|
+
sc = qn @ Pn.T
|
|
419
|
+
if k == 1:
|
|
420
|
+
idx = np.argmax(sc, axis=1)
|
|
421
|
+
return idx, sc[np.arange(len(idx)), idx]
|
|
422
|
+
order = np.argsort(-sc, axis=1)[:, :k]
|
|
423
|
+
rows = np.arange(sc.shape[0])[:, None]
|
|
424
|
+
return order, sc[rows, order]
|
|
425
|
+
|
|
426
|
+
# ----------------------------------------------------------------- write
|
|
427
|
+
def write(self, X: np.ndarray, t: float = 1.0, cheb_m: int = 12,
|
|
428
|
+
diffuse: bool = True) -> None:
|
|
429
|
+
"""Streaming write of a chunk of embeddings (n, d).
|
|
430
|
+
|
|
431
|
+
The diffusion is applied AFTER the scatter, which is exact for a
|
|
432
|
+
linear operator: exp(-tL)(S + Bx) = exp(-tL)S + exp(-tL)Bx.
|
|
433
|
+
"""
|
|
434
|
+
X = np.asarray(X, dtype=np.float64)
|
|
435
|
+
if X.ndim != 2 or X.shape[1] != self.d:
|
|
436
|
+
raise ValueError(f"expected (n, {self.d}) embeddings, got {X.shape}")
|
|
437
|
+
if X.shape[0] == 0:
|
|
438
|
+
return
|
|
439
|
+
idx, _ = self.route(X, k=1)
|
|
440
|
+
proj = X @ self.W_in
|
|
441
|
+
np.add.at(self.anchors, idx, proj)
|
|
442
|
+
self.write_calls += 1
|
|
443
|
+
self.tokens_written += int(X.shape[0])
|
|
444
|
+
if diffuse:
|
|
445
|
+
self.diffuse(t=t, cheb_m=cheb_m)
|
|
446
|
+
|
|
447
|
+
def diffuse(self, t: float = 1.0, cheb_m: int = 12) -> None:
|
|
448
|
+
"""Apply exp(-t L) to the anchor state (the 'freezing' step)."""
|
|
449
|
+
from .diffusion import cheb_basis, cheb_coeffs_exp, power_iteration_lmax
|
|
450
|
+
if self.protos is None:
|
|
451
|
+
return
|
|
452
|
+
if self._L is None:
|
|
453
|
+
self._L = self.anchor_graph(k=8)
|
|
454
|
+
edges, alpha, L = self._L
|
|
455
|
+
if self._lam_max is None:
|
|
456
|
+
self._lam_max = power_iteration_lmax(L)
|
|
457
|
+
lam = self._lam_max
|
|
458
|
+
if lam <= 1e-8:
|
|
459
|
+
return
|
|
460
|
+
Y = cheb_basis(L, lam, self.anchors, cheb_m)
|
|
461
|
+
a = cheb_coeffs_exp(t, lam, cheb_m)
|
|
462
|
+
self.anchors = sum(a[k] * Y[k] for k in range(cheb_m + 1))
|
|
463
|
+
|
|
464
|
+
# ------------------------------------------------------------------ read
|
|
465
|
+
def read(self, q: np.ndarray, k: int = 1):
|
|
466
|
+
"""Geometry read: returns (anchor_idx, scores, content (r,))."""
|
|
467
|
+
idx, sc = self.route(q, k=k)
|
|
468
|
+
if k == 1:
|
|
469
|
+
content = self.anchors[idx[0]]
|
|
470
|
+
return idx, sc, content
|
|
471
|
+
w = np.exp(sc - sc.max(axis=1, keepdims=True))
|
|
472
|
+
w = w / w.sum(axis=1, keepdims=True)
|
|
473
|
+
content = (self.anchors[idx] * w[:, :, None]).sum(axis=1)
|
|
474
|
+
return idx, sc, content
|
|
475
|
+
|
|
476
|
+
# ---------------------------------------------------------------- exact
|
|
477
|
+
def promote(self, vec: np.ndarray, value_token_id: int, window,
|
|
478
|
+
doc_id: int = 0, trust: int = 0):
|
|
479
|
+
"""Promote a fact into the register, anchored to its nearest prototype.
|
|
480
|
+
|
|
481
|
+
Returns the slot index. The fact's value NEVER enters the fluid, so
|
|
482
|
+
it can never be smeared.
|
|
483
|
+
"""
|
|
484
|
+
idx, _ = self.route(np.atleast_2d(np.asarray(vec, dtype=np.float64)), k=1)
|
|
485
|
+
anchor = int(idx[0])
|
|
486
|
+
sig = embedding_signature(vec)
|
|
487
|
+
key = ctx_key(window, doc_id=doc_id)
|
|
488
|
+
return self.register.promote(int(value_token_id), key, sig=sig,
|
|
489
|
+
anchor=anchor, doc_id=doc_id, trust=trust)
|
|
490
|
+
|
|
491
|
+
def recall(self, window, vec: np.ndarray | None = None):
|
|
492
|
+
"""Exact recall. Returns (slot, token_id) or None.
|
|
493
|
+
|
|
494
|
+
Verification: the 64-bit context key must match, and -- if `vec` is
|
|
495
|
+
given -- the 4-byte embedding signature must match too. A mismatch
|
|
496
|
+
returns None (fail loud) instead of a wrong value.
|
|
497
|
+
"""
|
|
498
|
+
key = ctx_key(window)
|
|
499
|
+
sig = embedding_signature(vec) if vec is not None else None
|
|
500
|
+
out = self.register.recall(key, sig=sig)
|
|
501
|
+
if out is None:
|
|
502
|
+
return None
|
|
503
|
+
return out
|
|
504
|
+
|
|
505
|
+
# ----------------------------------------------------------- accounting
|
|
506
|
+
def state_bytes(self) -> int:
|
|
507
|
+
"""Runtime state only: anchors + register. Excludes the codebook."""
|
|
508
|
+
return int(self.anchors.size * 2) + self.register.nbytes()
|
|
509
|
+
|
|
510
|
+
def codebook_bytes(self) -> int:
|
|
511
|
+
"""Static artefact: prototypes + value projection."""
|
|
512
|
+
if self.protos is None:
|
|
513
|
+
return int(self.W_in.size * 2)
|
|
514
|
+
return int((self.protos.size + self.W_in.size) * 2)
|
|
515
|
+
|
|
516
|
+
def stats(self) -> dict:
|
|
517
|
+
s = self.register.stats()
|
|
518
|
+
s.update({
|
|
519
|
+
"M": self.M, "r": self.r, "d": self.d,
|
|
520
|
+
"state_bytes": self.state_bytes(),
|
|
521
|
+
"codebook_bytes": self.codebook_bytes(),
|
|
522
|
+
"tokens_written": self.tokens_written,
|
|
523
|
+
"write_calls": self.write_calls,
|
|
524
|
+
})
|
|
525
|
+
return s
|