AbstractPhil commited on
Commit
4d65276
·
verified ·
1 Parent(s): 8340738

Rename experiments/exp_007_aleph_routed_attention/aleph_routed_trigram_lm.py to experiments/exp_007_aleph_routed_attention/2_aleph_routed_trigram_lm.py

Browse files
experiments/exp_007_aleph_routed_attention/2_aleph_routed_trigram_lm.py ADDED
@@ -0,0 +1,537 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # aleph_trigram_lm.py
2
+ """
3
+ Aleph-Routed Trigram LM — the basin-test vehicle
4
+ =================================================
5
+
6
+ Causal language model over the SAME substrate the geolip-svae aleph batteries
7
+ were fed: WikiText-103 as a raw UTF-8 byte stream, trigram-packed (3 bytes per
8
+ position, stride 3 — the sequence analogue of ByteTrigramDataset's 3-bytes-per-
9
+ cell RGB encoding). One token position = one trigram, embedded byte-factored
10
+ (sum of 3 byte embeddings + position) and predicted as 3 independent 256-way
11
+ byte heads — exactly the substrate, no 256^3 softmax.
12
+
13
+ Purpose (preregistered — CORRECTED per RESEARCH_HISTORY.md):
14
+ The program established (Phase 2, discoveries #13/#15) at least TWO stable
15
+ codebook statutes, SELECTED BY SUBSTRATE: uniform-class (|dev| < 0.05; the
16
+ noise solvers) and polytope-class (dev > +0.05, pair fraction >= 45%;
17
+ repulsive packing — the BYTE-TRIGRAM solvers, in-distribution dev +0.083).
18
+ dev < -0.05 is degenerate (clumping) — the failure statute. Statute is a
19
+ property of (model x calibration), not the model alone.
20
+
21
+ Therefore, on THIS substrate, the basin question is statute-resolved:
22
+ - random init -> STATUTE-SELECTION test. Codebook settling polytope-class
23
+ (the substrate-matched statute) or uniform-class under pure attention
24
+ gradients = cross-objective attractor evidence. Degenerate = failure.
25
+ - fibonacci init -> DRIFT test. Starts in the uniform basin; migration
26
+ OUT toward polytope under the symbolic substrate = substrate-driven
27
+ statute selection in a new objective (the stronger result).
28
+ Run BOTH inits. Statute (deviation + pair fraction, computed per the
29
+ program's own Sec 3.11 definitions) is logged per snapshot inline below;
30
+ void-richness (beta_2/axis via ripser on projective angular distances,
31
+ the symbolic-substrate fingerprint, discovery #20) is the deeper follow-up
32
+ on the saved snapshots. Note the two non-interchangeable "margins"
33
+ (top-1 softmax probability vs projective |<m,a>|): the inline monitors
34
+ detect collapse only; structure evidence is deviation/statute/beta_2.
35
+
36
+ Substrate fidelity (mirrors geolip_svae.dataset_presets.ByteTrigramDataset):
37
+ - corpus: 'wikitext-103-raw-v1' via the Salesforce/wikitext namespace
38
+ (the bare 'wikitext' hub name is deprecated), or any local .txt path
39
+ - raw bytes in a single uint8 numpy array (prototypes/CLAUDE.md trap #3:
40
+ Python lists of ints are 5-7x the memory; never materialize them)
41
+ - seeded window sampling over the stream
42
+ Repo invariants honored: pure Adam (never AdamW), no BatchNorm/Dropout on the
43
+ geometric path, grad clip = max(task_loss, 1.0).
44
+
45
+ Usage (Colab / Blackwell):
46
+ from aleph_trigram_lm import TrigramLMConfig, train_trigram_lm
47
+ cfg = TrigramLMConfig(steps=10_000, device='cuda',
48
+ attn_mode='hub', codebook_init='random')
49
+ result = train_trigram_lm(cfg)
50
+ # result['codebook_snapshots'] -> [(step, (K, D_addr) tensor), ...]
51
+ # also saved to cfg.snapshot_path for the extraction pass
52
+
53
+ Baseline A/B: attn_mode='standard' runs the same LM with softmax attention.
54
+
55
+ Author: AbstractPhil + Mirel
56
+ Date: 2026-06-09
57
+ License: MIT
58
+ """
59
+
60
+ from __future__ import annotations
61
+
62
+ import math
63
+ import os
64
+ import time
65
+ from dataclasses import dataclass, field
66
+ from typing import Dict, List, Optional, Tuple
67
+
68
+ import numpy as np
69
+ import torch
70
+ import torch.nn as nn
71
+ import torch.nn.functional as F
72
+ from torch import Tensor
73
+
74
+ from aleph_routed_attention import (
75
+ AlephRoutedAttention, AlephAttentionConfig, StandardAttention,
76
+ )
77
+
78
+
79
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
80
+ # Config
81
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
82
+
83
+ @dataclass
84
+ class TrigramLMConfig:
85
+ """Everything for one basin run.
86
+
87
+ Substrate:
88
+ corpus_id: HF dataset config name (default = the aleph batteries'
89
+ corpus) OR a local .txt/.text path
90
+ max_corpus_bytes: cap on bytes loaded (None = whole corpus, ~520 MB for
91
+ wikitext-103). 50–100 MB is plenty for these runs.
92
+ seq_len: context length in TRIGRAMS (bytes seen = 3*seq_len)
93
+
94
+ Model:
95
+ dim/n_layers/n_heads: transformer shell
96
+ attn_mode: 'hub' | 'bucket' | 'standard'
97
+ K/D_addr/tau: aleph routing knobs (ignored for 'standard')
98
+ codebook_init: 'random' for the basin test (MANDATORY there) |
99
+ 'fibonacci' | (K, D_addr) array transplant
100
+
101
+ Training:
102
+ pure Adam + cosine decay to 10%; loss reported in nats and bits/byte.
103
+ """
104
+ # substrate
105
+ corpus_id: str = "wikitext-103-raw-v1"
106
+ split: str = "train"
107
+ max_corpus_bytes: Optional[int] = 100_000_000
108
+ seq_len: int = 256 # trigrams (= 768 bytes of context)
109
+ seed: int = 1234
110
+
111
+ # model
112
+ dim: int = 384
113
+ n_layers: int = 4
114
+ n_heads: int = 6
115
+ attn_mode: str = "hub" # 'hub' | 'bucket' | 'standard'
116
+ K: int = 64
117
+ D_addr: int = 4
118
+ tau: float = 0.1
119
+ codebook_init: object = "random" # basin test requires 'random'
120
+ div_weight: float = 0.0 # anti-collapse; run 0 first, observe
121
+
122
+ # paradigm + scale
123
+ shared_codebook: bool = True # ONE vocabulary, many speakers: all
124
+ # layers address the same (K,D) param,
125
+ # concentrating address pressure n_layers-x
126
+ accum_steps: int = 1 # gradient accumulation (effective batch
127
+ # = batch_size * accum_steps)
128
+ stream_segments: int = 1 # segments per sample, each seq_len long;
129
+ # codebook-memory state carried across
130
+ # (TBPTT, detached between segments).
131
+ # context = seq_len * stream_segments
132
+ # at CONSTANT attention memory. hub-only.
133
+ probe_rows: int = 200_000 # address-stats sample size (ppl estimator)
134
+
135
+ # training
136
+ steps: int = 10_000 # optimizer steps (micro-batches =
137
+ # steps * accum_steps)
138
+ batch_size: int = 32
139
+ lr: float = 3e-4
140
+ lr_decay: bool = True
141
+ log_every: int = 250
142
+ eval_batches: int = 8
143
+ device: str = "cuda" if torch.cuda.is_available() else "cpu"
144
+ amp: bool = False # bf16 autocast on the shell (the
145
+ # address stays fp32 inside)
146
+
147
+ # outputs
148
+ snapshot_codebook: bool = True
149
+ snapshot_path: str = "aleph_lm_codebook_snapshots.pt"
150
+ checkpoint_path: Optional[str] = "aleph_trigram_lm.pt"
151
+
152
+ def __post_init__(self):
153
+ assert self.attn_mode in ("hub", "bucket", "standard")
154
+ assert self.dim % self.n_heads == 0
155
+ assert self.accum_steps >= 1 and self.stream_segments >= 1
156
+ if self.stream_segments > 1:
157
+ assert self.attn_mode == "hub", \
158
+ "streaming requires mode='hub' (bucket sorts globally)"
159
+
160
+
161
+
162
+
163
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
164
+ # Statute monitor — the program's own diagnostic geometry (Sec 3.11)
165
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
166
+
167
+ def _canon(x: Tensor) -> Tensor:
168
+ """Sign-canonicalize onto RP^(D-1): flip so the first nonzero coord is
169
+ positive (antipodes map to one representative)."""
170
+ x = F.normalize(x, dim=-1)
171
+ first_nz = x[torch.arange(len(x)), x.abs().argmax(dim=-1)]
172
+ return x * torch.sign(first_nz).unsqueeze(-1)
173
+
174
+
175
+ def _mean_projective_angle(X: Tensor) -> float:
176
+ """Mean pairwise acos|cos| over distinct pairs (radians)."""
177
+ c = (X @ X.t()).clamp(-1.0, 1.0).abs()
178
+ iu = torch.triu_indices(len(X), len(X), offset=1)
179
+ return torch.acos(c[iu[0], iu[1]]).mean().item()
180
+
181
+
182
+ _UNIFORM_BASELINE: Dict[int, float] = {}
183
+
184
+ def projective_deviation(axes: Tensor, n_ref: int = 4096,
185
+ seed: int = 0) -> float:
186
+ """Uniformity deviation per the program definition: mean pairwise
187
+ projective angle of the axes MINUS the same statistic for n_ref uniform
188
+ random projective points at the same D. Signed; sign matters."""
189
+ D = axes.shape[-1]
190
+ if D not in _UNIFORM_BASELINE:
191
+ g = torch.Generator().manual_seed(seed)
192
+ ref = F.normalize(torch.randn(n_ref, D, generator=g), dim=-1)
193
+ _UNIFORM_BASELINE[D] = _mean_projective_angle(ref)
194
+ return _mean_projective_angle(F.normalize(axes.float(), dim=-1)) \
195
+ - _UNIFORM_BASELINE[D]
196
+
197
+
198
+ def antipodal_pair_fraction(axes: Tensor, thresh: float = -0.9) -> float:
199
+ """Fraction of rows in mutual most-negative pairs with cos < thresh
200
+ (the antipodal-collapse acceptance rule)."""
201
+ A = F.normalize(axes.float(), dim=-1)
202
+ c = A @ A.t()
203
+ c.fill_diagonal_(0.0)
204
+ partner = c.argmin(dim=-1)
205
+ val = c.gather(-1, partner.unsqueeze(-1)).squeeze(-1)
206
+ mutual = partner[partner] == torch.arange(len(A))
207
+ return ((val < thresh) & mutual).float().mean().item()
208
+
209
+
210
+ def statute(axes: Tensor) -> Dict[str, object]:
211
+ """Classify per the program taxonomy: dev > +0.05 polytope-class
212
+ (repulsive packing); |dev| < 0.05 uniform-class; dev < -0.05 degenerate
213
+ (clumping, the failure statute)."""
214
+ dev = projective_deviation(axes)
215
+ pf = antipodal_pair_fraction(axes)
216
+ cls = ("polytope" if dev > 0.05 else
217
+ "degenerate" if dev < -0.05 else "uniform")
218
+ return {"deviation": dev, "pair_fraction": pf, "statute": cls}
219
+
220
+
221
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
222
+ # Substrate — trigram stream (mirrors ByteTrigramDataset's loading)
223
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
224
+
225
+ class TrigramStream:
226
+ """WikiText-103 (or local .txt) as a uint8 byte stream, sampled as
227
+ causal trigram sequences.
228
+
229
+ __call__(batch, seq_len) -> (ids, targets):
230
+ ids: (B, S, 3) uint8->long — trigram t = bytes[3t : 3t+3]
231
+ targets: (B, S, 3) — trigram t+1 (next-trigram prediction)
232
+ Windows are sampled at byte offsets aligned to stride 3 so the trigram
233
+ framing matches the image packing (cell i = bytes[3i : 3i+3])."""
234
+
235
+ def __init__(self, corpus_id: str, split: str = "train",
236
+ max_corpus_bytes: Optional[int] = None, seed: int = 1234):
237
+ if os.path.isfile(corpus_id) and corpus_id.endswith((".txt", ".text")):
238
+ print(f"[TrigramStream] loading local corpus {corpus_id} ...")
239
+ with open(corpus_id, "rb") as f:
240
+ raw = f.read(max_corpus_bytes) if max_corpus_bytes else f.read()
241
+ self.stream = np.frombuffer(raw, dtype=np.uint8).copy()
242
+ else:
243
+ print(f"[TrigramStream] loading HF corpus {corpus_id} ...")
244
+ from datasets import load_dataset
245
+ if corpus_id.startswith("wikitext"):
246
+ ds = load_dataset("Salesforce/wikitext", corpus_id, split=split)
247
+ else:
248
+ ds = load_dataset(corpus_id, split=split)
249
+ # accumulate utf-8 bytes directly into a byte buffer — never a
250
+ # Python list of ints (prototypes/CLAUDE.md memory trap #3)
251
+ buf = bytearray()
252
+ cap = max_corpus_bytes or float("inf")
253
+ for row in ds:
254
+ t = row.get("text", "")
255
+ if t:
256
+ buf.extend(t.encode("utf-8", errors="ignore"))
257
+ if len(buf) >= cap:
258
+ break
259
+ self.stream = np.frombuffer(
260
+ bytes(buf[: max_corpus_bytes] if max_corpus_bytes else buf),
261
+ dtype=np.uint8).copy()
262
+ n_tri = len(self.stream) // 3
263
+ print(f"[TrigramStream] {len(self.stream):,} bytes "
264
+ f"= {n_tri:,} trigrams")
265
+ assert n_tri > 0, "corpus too small"
266
+ self._rng = np.random.default_rng(seed)
267
+
268
+ def sample(self, batch: int, seq_len: int,
269
+ device) -> Tuple[Tensor, Tensor]:
270
+ need = 3 * (seq_len + 1) # +1 trigram for targets
271
+ hi = len(self.stream) - need
272
+ assert hi > 0, f"corpus shorter than one window ({need} bytes)"
273
+ starts = self._rng.integers(0, hi // 3, size=batch) * 3 # stride-3 aligned
274
+ idx = starts[:, None] + np.arange(need)[None, :] # (B, need)
275
+ window = self.stream[idx] # (B, need) uint8
276
+ tri = torch.from_numpy(window.astype(np.int64)) \
277
+ .view(batch, seq_len + 1, 3)
278
+ ids, targets = tri[:, :-1], tri[:, 1:]
279
+ return ids.to(device), targets.to(device)
280
+
281
+
282
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
283
+ # Model — byte-factored trigram LM
284
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
285
+
286
+ class TrigramLM(nn.Module):
287
+ """Causal LM over trigram positions. Embedding = sum of three per-slot
288
+ byte embeddings (+ learned positions); head = three 256-way byte heads.
289
+ Geometric-path hygiene: no BatchNorm/Dropout, pure pre-LN residual shell."""
290
+
291
+ def __init__(self, cfg: TrigramLMConfig):
292
+ super().__init__()
293
+ self.cfg = cfg
294
+ d = cfg.dim
295
+ self.byte_emb = nn.ModuleList([nn.Embedding(256, d) for _ in range(3)])
296
+ self.pos = nn.Parameter(0.02 * torch.randn(1, cfg.seq_len, d))
297
+
298
+ def make_attn() -> nn.Module:
299
+ if cfg.attn_mode == "standard":
300
+ return StandardAttention(d, cfg.n_heads, causal=True)
301
+ return AlephRoutedAttention(AlephAttentionConfig(
302
+ dim=d, num_heads=cfg.n_heads, mode=cfg.attn_mode,
303
+ K=cfg.K, D_addr=cfg.D_addr, tau=cfg.tau, causal=True,
304
+ codebook_init=cfg.codebook_init))
305
+
306
+ self.layers = nn.ModuleList([
307
+ nn.ModuleDict({
308
+ "norm1": nn.LayerNorm(d), "attn": make_attn(),
309
+ "norm2": nn.LayerNorm(d),
310
+ "mlp": nn.Sequential(nn.Linear(d, 4 * d), nn.GELU(),
311
+ nn.Linear(4 * d, d)),
312
+ }) for _ in range(cfg.n_layers)
313
+ ])
314
+ self.norm_f = nn.LayerNorm(d)
315
+ self.heads = nn.ModuleList([nn.Linear(d, 256) for _ in range(3)])
316
+
317
+ # one vocabulary, many speakers: tie every layer's codebook to layer 0's
318
+ if cfg.shared_codebook and cfg.attn_mode in ("hub", "bucket"):
319
+ shared = self.layers[0]["attn"].codebook
320
+ for L in self.layers[1:]:
321
+ L["attn"].codebook = shared
322
+
323
+ def aleph_layers(self) -> List[AlephRoutedAttention]:
324
+ return [m for m in self.modules() if isinstance(m, AlephRoutedAttention)]
325
+
326
+ def backbone(self, ids: Tensor) -> Tensor:
327
+ """ids: (B, S, 3) -> (B, S, dim)"""
328
+ x = sum(emb(ids[..., i]) for i, emb in enumerate(self.byte_emb))
329
+ x = x + self.pos[:, : ids.shape[1]]
330
+ for L in self.layers:
331
+ x = x + L["attn"](L["norm1"](x))
332
+ x = x + L["mlp"](L["norm2"](x))
333
+ return self.norm_f(x)
334
+
335
+ def forward(self, ids: Tensor) -> Tensor:
336
+ """(B, S, 3) -> byte logits (B, S, 3, 256)"""
337
+ h = self.backbone(ids)
338
+ return torch.stack([head(h) for head in self.heads], dim=2)
339
+
340
+ def loss(self, ids: Tensor, targets: Tensor) -> Tensor:
341
+ """Mean cross-entropy per byte (nats/byte). bpb = loss / ln 2."""
342
+ logits = self(ids) # (B,S,3,256)
343
+ return F.cross_entropy(logits.reshape(-1, 256), targets.reshape(-1))
344
+
345
+ # ── streaming: segment-recurrent backbone (codebook memory carried) ──
346
+ def stream_loss(self, ids: Tensor, targets: Tensor,
347
+ states: Optional[List] = None
348
+ ) -> Tuple[Tensor, List]:
349
+ """One SEGMENT with per-layer carried states. states[i] is layer i's
350
+ (Mp, Mm, zp, zm) or None. Returns (loss, new_states) — caller detaches
351
+ between backward passes (TBPTT-1: grads flow within segment; values
352
+ flow forever)."""
353
+ x = sum(emb(ids[..., i]) for i, emb in enumerate(self.byte_emb))
354
+ x = x + self.pos[:, : ids.shape[1]]
355
+ new_states: List = []
356
+ states = states or [None] * len(self.layers)
357
+ for L, st in zip(self.layers, states):
358
+ a, ns = L["attn"].forward_stream(L["norm1"](x), state=st)
359
+ x = x + a
360
+ x = x + L["mlp"](L["norm2"](x))
361
+ new_states.append(ns)
362
+ h = self.norm_f(x)
363
+ logits = torch.stack([head(h) for head in self.heads], dim=2)
364
+ loss = F.cross_entropy(logits.reshape(-1, 256), targets.reshape(-1))
365
+ return loss, new_states
366
+
367
+
368
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
369
+ # Training
370
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
371
+
372
+ def train_trigram_lm(cfg: TrigramLMConfig,
373
+ stream: Optional[TrigramStream] = None) -> Dict:
374
+ torch.manual_seed(cfg.seed)
375
+ dev = torch.device(cfg.device)
376
+ stream = stream or TrigramStream(cfg.corpus_id, cfg.split,
377
+ cfg.max_corpus_bytes, cfg.seed)
378
+ model = TrigramLM(cfg).to(dev)
379
+ n_params = sum(p.numel() for p in model.parameters())
380
+ opt = torch.optim.Adam(model.parameters(), lr=cfg.lr) # pure Adam, never AdamW
381
+ sched = (torch.optim.lr_scheduler.CosineAnnealingLR(
382
+ opt, T_max=cfg.steps, eta_min=cfg.lr * 0.1) if cfg.lr_decay else None)
383
+
384
+ alephs = model.aleph_layers()
385
+ for a in alephs:
386
+ a.emit_diversity = cfg.div_weight > 0
387
+
388
+ snapshots: List[Tuple[int, Tensor]] = []
389
+ if cfg.snapshot_codebook and alephs:
390
+ snapshots.append((0, alephs[0].export_codebook()))
391
+
392
+ print(f"\n=== trigram LM mode={cfg.attn_mode} params={n_params:,} "
393
+ f"ctx={cfg.seq_len}x{cfg.stream_segments} trigrams "
394
+ f"({3*cfg.seq_len*cfg.stream_segments} bytes) "
395
+ f"eff.batch={cfg.batch_size*cfg.accum_steps} "
396
+ f"shared_cb={cfg.shared_codebook} "
397
+ f"device={dev} ===")
398
+ autocast = (torch.autocast(device_type=dev.type, dtype=torch.bfloat16)
399
+ if cfg.amp and dev.type == "cuda" else None)
400
+ result: Dict = {"mode": cfg.attn_mode, "params": n_params}
401
+ t0 = time.time()
402
+
403
+ segs = cfg.stream_segments
404
+ micro_scale = 1.0 / (cfg.accum_steps * segs)
405
+ for step in range(1, cfg.steps + 1):
406
+ opt.zero_grad(set_to_none=True)
407
+ loss_sum, n_micro = 0.0, 0
408
+ for _ in range(cfg.accum_steps):
409
+ # one long sample, split into `segs` carried segments
410
+ ids, targets = stream.sample(cfg.batch_size, cfg.seq_len * segs, dev)
411
+ states = None
412
+ for s in range(segs):
413
+ sl = slice(s * cfg.seq_len, (s + 1) * cfg.seq_len)
414
+ seg_ids, seg_tgt = ids[:, sl], targets[:, sl]
415
+ if autocast:
416
+ with autocast:
417
+ if segs > 1:
418
+ loss, states = model.stream_loss(seg_ids, seg_tgt, states)
419
+ else:
420
+ loss = model.loss(seg_ids, seg_tgt)
421
+ else:
422
+ if segs > 1:
423
+ loss, states = model.stream_loss(seg_ids, seg_tgt, states)
424
+ else:
425
+ loss = model.loss(seg_ids, seg_tgt)
426
+ total = loss
427
+ if cfg.div_weight > 0 and alephs:
428
+ total = total + cfg.div_weight * sum(
429
+ a.diversity_loss() for a in alephs)
430
+ (total * micro_scale).backward()
431
+ loss_sum += loss.item(); n_micro += 1
432
+ if states is not None: # TBPTT boundary
433
+ states = [tuple(t.detach() for t in st) for st in states]
434
+ loss_avg = loss_sum / n_micro
435
+ gnorm = torch.nn.utils.clip_grad_norm_(
436
+ model.parameters(), max(loss_avg, 1.0)) # the clip rule
437
+ opt.step()
438
+ if sched is not None:
439
+ sched.step()
440
+
441
+ if step % cfg.log_every == 0 or step == cfg.steps:
442
+ bpb = loss_avg / math.log(2)
443
+ rate = (step * cfg.batch_size * cfg.seq_len * segs
444
+ * cfg.accum_steps) / (time.time() - t0)
445
+ line = (f" step {step:6d} loss {loss_avg:.4f} "
446
+ f"bpb {bpb:.3f} |g| {gnorm:.2f} {rate/1e3:.1f}k tri/s")
447
+ if alephs:
448
+ model.eval()
449
+ with torch.no_grad():
450
+ x_probe = model.backbone(
451
+ ids[: min(8, cfg.batch_size), -cfg.seq_len:])
452
+ st = alephs[0].address_stats(x_probe, max_rows=cfg.probe_rows)
453
+ model.train()
454
+ line += (f" ppl {st['perplexity']:.1f}/{st['max_perplexity']:.0f}"
455
+ f" margin {st['margin']:.4f}"
456
+ f" conf {st['confidence']:.3f}")
457
+ result.update(st)
458
+ if cfg.snapshot_codebook:
459
+ snapshots.append((step, alephs[0].export_codebook()))
460
+ print(line)
461
+ result.update({"loss": loss_avg, "bpb": bpb, "step": step})
462
+
463
+ if snapshots:
464
+ result["codebook_snapshots"] = snapshots
465
+ drift = (snapshots[-1][1] - snapshots[0][1]).norm().item()
466
+ result["codebook_drift"] = drift
467
+ traj = [(s, statute(cb)) for s, cb in snapshots]
468
+ result["statute_trajectory"] = traj
469
+ torch.save({"snapshots": snapshots, "statute_trajectory": traj,
470
+ "config": cfg.__dict__, "K": cfg.K, "D_addr": cfg.D_addr},
471
+ cfg.snapshot_path)
472
+ print(f"\n[basin] {len(snapshots)} snapshots -> {cfg.snapshot_path}"
473
+ f" drift |A_end - A_0| = {drift:.4f}")
474
+ print("[basin] statute trajectory (program taxonomy: polytope is the "
475
+ "substrate-matched\n statute for byte-trigram; uniform is "
476
+ "the noise/OOD statute; degenerate = failure):")
477
+ for s, st in traj:
478
+ print(f" step {s:6d} dev {st['deviation']:+.4f} "
479
+ f"pairs {st['pair_fraction']:.0%} -> {st['statute']}")
480
+ print("[basin] deeper follow-up on saved snapshots: beta_2/axis via "
481
+ "ripser on projective\n angular distances (the "
482
+ "void/symbolic fingerprint, discovery #20).")
483
+ if cfg.checkpoint_path:
484
+ torch.save({"model_state_dict": model.state_dict(),
485
+ "config": cfg.__dict__}, cfg.checkpoint_path)
486
+ print(f"[ckpt] saved -> {cfg.checkpoint_path}")
487
+ return result
488
+
489
+
490
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
491
+ # Activation
492
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
493
+
494
+ def _smoke(device: str = "cpu"):
495
+ """End-to-end on a synthetic local corpus — no downloads."""
496
+ print("=" * 70)
497
+ print("aleph_trigram_lm — smoke (synthetic corpus)")
498
+ print("=" * 70)
499
+ path = "/tmp/_smoke_corpus.txt"
500
+ rng = np.random.default_rng(0)
501
+ words = [b"the", b"aleph", b"address", b"routes", b"attention",
502
+ b"through", b"a", b"learned", b"projective", b"codebook"]
503
+ with open(path, "wb") as f:
504
+ f.write(b" ".join(words[i] for i in rng.integers(0, len(words), 60_000)))
505
+ cfg = TrigramLMConfig(corpus_id=path, steps=30, log_every=10,
506
+ dim=96, n_layers=2, n_heads=4, K=16, seq_len=64,
507
+ batch_size=8, device=device,
508
+ snapshot_path="/tmp/_smoke_snaps.pt",
509
+ checkpoint_path=None)
510
+ r = train_trigram_lm(cfg)
511
+ assert "codebook_snapshots" in r and len(r["codebook_snapshots"]) >= 2
512
+ assert math.isfinite(r["loss"]) and r["loss"] < math.log(256)
513
+ print(f"\nsmoke OK — loss {r['loss']:.3f} (< ln256={math.log(256):.3f} prior), "
514
+ f"drift {r['codebook_drift']:.4f}, "
515
+ f"{len(r['codebook_snapshots'])} snapshots saved")
516
+
517
+
518
+ if __name__ == "__main__":
519
+ import argparse
520
+ ap = argparse.ArgumentParser(description="Aleph trigram LM — basin run")
521
+ ap.add_argument("--smoke-only", action="store_true")
522
+ ap.add_argument("--mode", default="hub", choices=["hub", "bucket", "standard"])
523
+ ap.add_argument("--steps", type=int, default=10_000)
524
+ ap.add_argument("--corpus-mb", type=int, default=100)
525
+ ap.add_argument("--codebook-init", default="random")
526
+ ap.add_argument("--device",
527
+ default="cuda" if torch.cuda.is_available() else "cpu")
528
+ args, _unknown = ap.parse_known_args() # notebook-safe (ignores -f kernel.json)
529
+
530
+ if args.smoke_only:
531
+ _smoke(device="cpu")
532
+ else:
533
+ cfg = TrigramLMConfig(attn_mode=args.mode, steps=args.steps,
534
+ max_corpus_bytes=args.corpus_mb * 1_000_000,
535
+ codebook_init=args.codebook_init,
536
+ device=args.device)
537
+ train_trigram_lm(cfg)
experiments/exp_007_aleph_routed_attention/aleph_routed_trigram_lm.py DELETED
@@ -1,792 +0,0 @@
1
- # aleph_lm.py
2
- """
3
- AlephLM — prediction through the codebook, with guarantees
4
- ===========================================================
5
-
6
- The composite reduction (2026-06-09): a causal trigram LM in which the aleph
7
- signed-projective address is load-bearing at ALL THREE stations — input
8
- addressing, mixing, and prediction. The codebook receives gradient from
9
- routing, from the predicted next-address pi, and from every candidate address
10
- kappa. One geometry, closed loop, smooth everywhere (no argmax in the train
11
- path): differential trigram-to-trigram prediction.
12
-
13
- CODEC bytes -> trigrams g_t (stride 3)
14
- EMBED e_t = sum_c E_c[g_t[c]] (byte-factored)
15
- MIX AlephRoutedAttention hub layers, shared codebook, causal
16
- PREDICT pi = softmax([w; -w]), w = W_pi h_t (free antipodal-tied)
17
- CANDIDATE kappa(tau) = address(normalize(W_k sum_c E_c[tau[c]]))
18
- SCORE logit(tau) = alpha * log( pi+ . kappa+(tau) + pi- . kappa-(tau) )
19
- OUTPUT hybrid: P(g) = g_in * P_bank(g | in) + (1-g_in) * P_byte(g)
20
-
21
- THE GUARANTEE LEDGER (all demonstrated numerically 2026-06-09; see session log):
22
- T1/T2 pi parameterization: address-constrained pi is projectively UNIMODAL
23
- (logits linear in x-hat) — a two-spike target is unreachable (best
24
- joint mass 1.4% vs 50% needed). The free antipodal-tied simplex
25
- represents any tied-logit distribution. DEFAULT: free tied simplex;
26
- address-constrained is the unimodal ablation (pi_mode='address').
27
- T3 Tied [w; -w] implies p+k * p-k is CONSTANT across k — every axis is
28
- forced to an orientation stance. Feature-or-bug: empirical.
29
- T4 The 3x256 byte-product head cannot express within-trigram byte
30
- correlation (rank-1 tensor over 256^3); it is the guaranteed-floor
31
- baseline (head='byte'), not the main head.
32
- T5 The hybrid output is a PROPER full-support distribution and its CE
33
- decomposes exactly: -log P(g) = -log gate_branch - log P_branch(g).
34
- Implemented verbatim. "Run all three banks" = ablations inside one
35
- provably-correct machine.
36
- T6 Raw-score softmax over a bank has a sharpness ceiling (scores in
37
- (0,1] => CE floor 7.32 nats at M=4096). Logits are LOG-kernel with a
38
- learnable scale alpha. Non-negotiable.
39
- T7 Output logit rank <= 2K (softmax bottleneck): K governs attention
40
- rank, output rank, and mode capacity — one knob, three proven roles.
41
- T8 The write-head target Delta-z = sum of future addresses is the
42
- ORDER-MARGINALIZED multiset of the next W trigrams (permutation-
43
- invariant by commutativity). It predicts WHAT comes, not the order.
44
- Learnability rests on the empirical rank-10 occupancy result.
45
- Lit. Sampled softmax requires the log-Q correction; with a uniform
46
- proposal the correction is constant and cancels in the softmax
47
- (target always included). head='sampled' implements exactly this.
48
-
49
- THE BRANCHING GAUGE (the [TAU] kernel invariant, inverted): a single unit row
50
- has conf = ||(p+ - p-)A|| pinned at f(tau,K,D). A PREDICTED pi is not so
51
- bound — implied confidence below the invariant is the model declaring
52
- superposition. branching_frac is monitored from step zero.
53
-
54
- Banks: 'corpus' (top-M trigrams of the training stream), 'wordnet'
55
- (AbstractPhil/wordnet-lexical-topology char_eng_3gram, frequency-ranked,
56
- filtered to exact 3-byte UTF-8), or per-step 'sampled' negatives.
57
-
58
- Usage (Blackwell / A100):
59
- from aleph_lm import AlephLMConfig, train_aleph_lm
60
- r = train_aleph_lm(AlephLMConfig(steps=10_000, device='cuda',
61
- head='hybrid', bank_source='wordnet'))
62
-
63
- Depends: aleph_routed_attention.py, aleph_trigram_lm.py in the same directory.
64
- Author: AbstractPhil + Mirel Date: 2026-06-09 License: MIT
65
- """
66
-
67
- from __future__ import annotations
68
-
69
- import math
70
- import os
71
- import time
72
- from dataclasses import dataclass
73
- from typing import Dict, List, Optional, Tuple
74
-
75
- import numpy as np
76
- import torch
77
- import torch.nn as nn
78
- import torch.nn.functional as F
79
- from torch import Tensor
80
-
81
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
82
- # Config
83
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
84
-
85
- @dataclass
86
- class AlephLMConfig:
87
- # substrate
88
- corpus_id: str = "wikitext-103-raw-v1"
89
- split: str = "train"
90
- max_corpus_bytes: Optional[int] = 100_000_000
91
- seq_len: int = 256 # trigrams (3*seq_len bytes)
92
- seed: int = 1234
93
-
94
- # tower
95
- dim: int = 384
96
- n_layers: int = 4
97
- n_heads: int = 6
98
- K: int = 64
99
- D_addr: int = 4
100
- tau: float = 0.1
101
- codebook_init: object = "random"
102
- shared_codebook: bool = True
103
-
104
- # prediction head (the ledger's resolutions)
105
- head: str = "hybrid" # 'hybrid' | 'byte' | 'bank' | 'sampled'
106
- pi_mode: str = "free" # 'free' (T2 default) | 'address' (unimodal ablation)
107
- bank_source: str = "corpus" # 'corpus' | 'wordnet'
108
- bank_size: int = 4096
109
- n_negatives: int = 1024 # head='sampled'
110
- logit_scale_init: float = 1.0 # alpha on the log-kernel logits (T6)
111
-
112
- # hybrid bank scorer: 'kernel' (log-kernel, T6) or 'pmix' — a mixture of
113
- # J pointers on S^(d_point-1): logits(c) = logsumexp_j [log w_j + T yhat_j.c]
114
- # = Mixture-of-Softmaxes in sphere coordinates. Theorem-backed twice over:
115
- # raises output rank past the T7 bottleneck (MoS, Yang et al.), and gives
116
- # the pointer J modes so the barycenter pathology (unimodal aim at a
117
- # multimodal future) is structurally removed. Full-bank softmax retained:
118
- # T5 propriety intact. PREREGISTERED statute prediction: pmix candidate
119
- # coords bypass the codebook (W_cand48), removing prediction-side
120
- # discrimination pressure -> expect dev near the zero group, vs kernel's
121
- # +0.013. The dose-response gets a within-architecture test.
122
- bank_scorer: str = "kernel" # 'kernel' | 'pmix'
123
- n_pointers: int = 4 # J mixture components (pmix)
124
-
125
- # pointer head (head='pointer'): NN-on-the-sphere decode
126
- d_point: int = 48 # pointer sphere dim (band-valid; the
127
- # capacity table gives the decode
128
- # budget theta_NN/2 at this D)
129
- pointer_k: int = 32 # hard negatives = target's k sphere-NN
130
- pointer_cos_weight: float = 0.5 # aiming regularizer (contrastive CE
131
- # is the main learner — lit. caveat)
132
- pointer_refresh: int = 200 # steps between NN-table refreshes
133
- # (candidate coords drift)
134
-
135
- # write-head (T8, auxiliary multiset prediction)
136
- write_weight: float = 0.1 # 0 disables
137
- write_horizon: int = 8 # W: the granularity dial
138
-
139
- # training
140
- steps: int = 10_000
141
- batch_size: int = 32
142
- accum_steps: int = 1
143
- lr: float = 3e-4
144
- lr_decay: bool = True
145
- div_weight: float = 0.0
146
- log_every: int = 250
147
- device: str = "cuda" if torch.cuda.is_available() else "cpu"
148
-
149
- # outputs
150
- snapshot_codebook: bool = True
151
- snapshot_path: str = "aleph_lm_snaps.pt"
152
- checkpoint_path: Optional[str] = "aleph_lm.pt"
153
-
154
- def __post_init__(self):
155
- assert self.head in ("hybrid", "byte", "bank", "sampled", "pointer")
156
- assert self.pi_mode in ("free", "address")
157
- assert self.bank_scorer in ("kernel", "pmix")
158
- assert self.bank_source in ("corpus", "wordnet") \
159
- or os.path.isfile(str(self.bank_source)), \
160
- f"bank_source must be 'corpus'|'wordnet'|path to bank .pt"
161
- assert self.dim % self.n_heads == 0
162
- assert self.write_horizon >= 1
163
- tag = self.head
164
- if self.head in ("hybrid", "bank"):
165
- b = (os.path.splitext(os.path.basename(str(self.bank_source)))[0]
166
- if os.path.isfile(str(self.bank_source)) else self.bank_source)
167
- tag += f"_{b}"
168
- if self.pi_mode != "free":
169
- tag += f"_{self.pi_mode}"
170
- if self.head == "hybrid" and self.bank_scorer == "pmix":
171
- tag += f"_pmix{self.n_pointers}"
172
- if self.checkpoint_path == "aleph_lm.pt":
173
- self.checkpoint_path = f"aleph_lm_{tag}.pt"
174
- if self.snapshot_path == "aleph_lm_snaps.pt":
175
- self.snapshot_path = f"aleph_lm_snaps_{tag}.pt"
176
-
177
-
178
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
179
- # Candidate banks
180
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
181
-
182
- def _tri_ids(tri: Tensor) -> Tensor:
183
- """(..., 3) bytes -> scalar trigram id in [0, 256^3)."""
184
- return tri[..., 0] * 65536 + tri[..., 1] * 256 + tri[..., 2]
185
-
186
-
187
- def build_corpus_bank(stream: TrigramStream, M: int,
188
- sample_bytes: int = 6_000_000) -> Tensor:
189
- """Top-M most frequent trigrams of the training stream. (M, 3) long."""
190
- n = min(sample_bytes, (len(stream.stream) // 3) * 3)
191
- tri = stream.stream[:n].reshape(-1, 3)
192
- ids = (tri[:, 0].astype(np.int64) * 65536 + tri[:, 1].astype(np.int64) * 256
193
- + tri[:, 2].astype(np.int64))
194
- uniq, counts = np.unique(ids, return_counts=True)
195
- top = uniq[np.argsort(counts)[::-1][:M]]
196
- out = np.stack([top // 65536, (top // 256) % 256, top % 256], axis=-1)
197
- return torch.from_numpy(out.astype(np.int64))
198
-
199
-
200
- def build_wordnet_bank(M: int) -> Tensor:
201
- """char_eng_3gram from AbstractPhil/wordnet-lexical-topology, frequency-
202
- ranked, filtered to exact 3-byte UTF-8. (M', 3) long, M' <= M."""
203
- from huggingface_hub import hf_hub_download
204
- import pyarrow.parquet as pq
205
- p = hf_hub_download("AbstractPhil/wordnet-lexical-topology",
206
- "data/char_eng_3gram-00000-of-00001.parquet",
207
- repo_type="dataset")
208
- t = pq.read_table(p, columns=["ngram", "rank"]).to_pandas()
209
- t = t.sort_values("rank")
210
- rows = []
211
- for s in t["ngram"]:
212
- b = str(s).encode("utf-8", errors="ignore")
213
- if len(b) == 3:
214
- rows.append([b[0], b[1], b[2]])
215
- if len(rows) >= M:
216
- break
217
- assert rows, "wordnet bank empty after 3-byte filter"
218
- return torch.tensor(rows, dtype=torch.long)
219
-
220
-
221
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
222
- # Model
223
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
224
-
225
- class AlephLM(nn.Module):
226
- """The composite reduction. forward_loss(ids, targets) -> (loss, logs)."""
227
-
228
- def __init__(self, cfg: AlephLMConfig, bank: Optional[Tensor] = None):
229
- super().__init__()
230
- self.cfg = cfg
231
- d = cfg.dim
232
-
233
- # ── EMBED (byte-factored; shared with candidate composition) ──
234
- self.byte_emb = nn.ModuleList([nn.Embedding(256, d) for _ in range(3)])
235
- self.pos = nn.Parameter(0.02 * torch.randn(1, cfg.seq_len, d))
236
-
237
- # ── MIX (hub tower, shared codebook) ──
238
- def make_attn():
239
- return AlephRoutedAttention(AlephAttentionConfig(
240
- dim=d, num_heads=cfg.n_heads, mode="hub", K=cfg.K,
241
- D_addr=cfg.D_addr, tau=cfg.tau, causal=True,
242
- codebook_init=cfg.codebook_init))
243
- self.layers = nn.ModuleList([
244
- nn.ModuleDict({"norm1": nn.LayerNorm(d), "attn": make_attn(),
245
- "norm2": nn.LayerNorm(d),
246
- "mlp": nn.Sequential(nn.Linear(d, 4 * d), nn.GELU(),
247
- nn.Linear(4 * d, d))})
248
- for _ in range(cfg.n_layers)])
249
- if cfg.shared_codebook:
250
- shared = self.layers[0]["attn"].codebook
251
- for L in self.layers[1:]:
252
- L["attn"].codebook = shared
253
- self.norm_f = nn.LayerNorm(d)
254
-
255
- # ── PREDICT: pi over 2K oriented axes ──
256
- self.W_pi = nn.Linear(d, cfg.K, bias=True) # tied logits [w; -w]
257
- if cfg.pi_mode == "address": # unimodal ablation (T2)
258
- self.W_pi_row = nn.Linear(d, cfg.D_addr, bias=False)
259
- nn.init.orthogonal_(self.W_pi_row.weight)
260
-
261
- # ── CANDIDATE: compositional addresses (banked heads) ──
262
- self.W_kappa = nn.Linear(d, cfg.D_addr, bias=False)
263
- nn.init.orthogonal_(self.W_kappa.weight)
264
- self.logit_scale = nn.Parameter(
265
- torch.tensor(float(cfg.logit_scale_init))) # alpha (T6)
266
-
267
- # ── byte-product head (T4 floor + hybrid tail) ──
268
- self.byte_heads = nn.ModuleList([nn.Linear(d, 256) for _ in range(3)])
269
-
270
- # ── hybrid gate (T5) ──
271
- self.gate = nn.Linear(d, 1)
272
-
273
- # ── pmix bank scorer: J-pointer mixture (MoS on the sphere) ──
274
- if cfg.head == "hybrid" and cfg.bank_scorer == "pmix":
275
- J = cfg.n_pointers
276
- self.W_pmix = nn.Linear(d, J * cfg.d_point, bias=False)
277
- nn.init.orthogonal_(self.W_pmix.weight)
278
- self.W_mixgate = nn.Linear(d, J)
279
- self.W_cand48 = nn.Linear(d, cfg.d_point, bias=False)
280
- nn.init.orthogonal_(self.W_cand48.weight)
281
- self.point_T = nn.Parameter(torch.tensor(10.0))
282
-
283
- # ── pointer head: predict a point on S^(d_point-1), decode by NN ──
284
- if cfg.head == "pointer":
285
- self.W_point = nn.Linear(d, cfg.d_point, bias=False)
286
- nn.init.orthogonal_(self.W_point.weight)
287
- self.W_cand48 = nn.Linear(d, cfg.d_point, bias=False)
288
- nn.init.orthogonal_(self.W_cand48.weight)
289
- self.point_T = nn.Parameter(torch.tensor(10.0)) # contrastive inv-temp
290
- self.register_buffer("_nn_table", torch.zeros(0, dtype=torch.long),
291
- persistent=False)
292
- self._nn_step = -1
293
-
294
- # ── write-head (T8): predicted Delta-z over 2K ──
295
- if cfg.write_weight > 0:
296
- self.W_write = nn.Linear(d, 2 * cfg.K)
297
-
298
- # ── bank registration ──
299
- if bank is not None:
300
- self.register_buffer("bank", bank) # (M, 3)
301
- self.register_buffer("bank_ids_sorted",
302
- _tri_ids(bank).sort().values) # membership
303
- self.register_buffer("bank_perm",
304
- _tri_ids(bank).argsort()) # sorted->orig
305
- else:
306
- self.bank = None
307
-
308
- # ---- shared codebook handle ----
309
- @property
310
- def codebook(self) -> Tensor:
311
- return self.layers[0]["attn"].codebook
312
-
313
- def aleph_layers(self) -> List[AlephRoutedAttention]:
314
- return [m for m in self.modules() if isinstance(m, AlephRoutedAttention)]
315
-
316
- # ---- address of arbitrary unit rows vs the SHARED codebook ----
317
- def _address_rows(self, rows: Tensor) -> Tuple[Tensor, Tensor]:
318
- A = F.normalize(self.codebook, dim=-1)
319
- u = (rows @ A.t()) / self.cfg.tau
320
- m = u.abs().amax(-1, keepdim=True)
321
- ep, en = torch.exp(u - m), torch.exp(-u - m)
322
- Z = (ep + en).sum(-1, keepdim=True)
323
- return ep / Z, en / Z
324
-
325
- # ---- PREDICT ----
326
- def _pi(self, h: Tensor) -> Tuple[Tensor, Tensor]:
327
- """pi over 2K oriented axes. 'free': tied simplex (T2 default).
328
- 'address': unimodal ablation."""
329
- if self.cfg.pi_mode == "address":
330
- row = F.normalize(self.W_pi_row(h), dim=-1)
331
- return self._address_rows(row)
332
- w = self.W_pi(h) # (..., K)
333
- m = w.abs().amax(-1, keepdim=True)
334
- ep, en = torch.exp(w - m), torch.exp(-w - m)
335
- Z = (ep + en).sum(-1, keepdim=True)
336
- return ep / Z, en / Z
337
-
338
- # ---- CANDIDATE addresses for a (M, 3) byte bank ----
339
- def _kappa(self, bank: Tensor) -> Tuple[Tensor, Tensor]:
340
- e = sum(emb(bank[:, i]) for i, emb in enumerate(self.byte_emb))
341
- rows = F.normalize(self.W_kappa(e), dim=-1) # (M, D_addr)
342
- return self._address_rows(rows)
343
-
344
- # ---- SCORE: log-kernel logits (T6) ----
345
- def _bank_logits(self, pi_p: Tensor, pi_m: Tensor,
346
- k_p: Tensor, k_m: Tensor) -> Tensor:
347
- s = pi_p @ k_p.t() + pi_m @ k_m.t() # (..., M), > 0
348
- return self.logit_scale * torch.log(s.clamp_min(1e-9))
349
-
350
- # ---- pmix bank logits: logsumexp over J pointers (MoS on the sphere) ----
351
- def _pmix_logits(self, h: Tensor) -> Tuple[Tensor, Dict[str, float]]:
352
- cfg = self.cfg
353
- J = cfg.n_pointers
354
- coords = self._cand_coords() # (M, d_point)
355
- y = self.W_pmix(h).view(*h.shape[:-1], J, cfg.d_point)
356
- y = F.normalize(y, dim=-1) # (B,S,J,dp)
357
- mix = F.log_softmax(self.W_mixgate(h), dim=-1) # (B,S,J)
358
- sims = torch.einsum("bsjd,md->bsjm", y, coords) * self.point_T
359
- logits = torch.logsumexp(mix.unsqueeze(-1) + sims, dim=2) # (B,S,M)
360
- with torch.no_grad(): # mode diagnostics
361
- pw = torch.einsum("bsjd,bskd->bsjk", y, y)
362
- off = pw.masked_select(~torch.eye(J, dtype=torch.bool,
363
- device=h.device)
364
- .expand_as(pw)).clamp(-1, 1)
365
- spread = torch.acos(off).mean().item() * 180 / math.pi
366
- usage = mix.exp().mean(dim=(0, 1))
367
- ent = -(usage * usage.clamp_min(1e-9).log()).sum().item() / math.log(J)
368
- return logits, {"mode_spread_deg": spread, "mix_entropy": ent}
369
-
370
- # ---- tower ----
371
- def backbone(self, ids: Tensor) -> Tensor:
372
- x = sum(emb(ids[..., i]) for i, emb in enumerate(self.byte_emb))
373
- x = x + self.pos[:, : ids.shape[1]]
374
- for L in self.layers:
375
- x = x + L["attn"](L["norm1"](x))
376
- x = x + L["mlp"](L["norm2"](x))
377
- return self.norm_f(x)
378
-
379
- # ---- byte-product log-probs of given targets (T4 tail) ----
380
- def _byte_logprob(self, h: Tensor, targets: Tensor) -> Tensor:
381
- lp = 0.0
382
- for c, head in enumerate(self.byte_heads):
383
- lp = lp + F.log_softmax(head(h), dim=-1).gather(
384
- -1, targets[..., c:c + 1]).squeeze(-1)
385
- return lp # (B, S)
386
-
387
- # ---- bank membership: target -> bank index or -1 ----
388
- def _bank_index(self, targets: Tensor) -> Tensor:
389
- tid = _tri_ids(targets)
390
- pos = torch.searchsorted(self.bank_ids_sorted, tid)
391
- pos = pos.clamp_max(len(self.bank_ids_sorted) - 1)
392
- hit = self.bank_ids_sorted[pos] == tid
393
- idx = self.bank_perm[pos]
394
- return torch.where(hit, idx, torch.full_like(idx, -1))
395
-
396
- # ---- write-head target (T8): order-marginalized future address mass ----
397
- @torch.no_grad()
398
- def _write_target(self, ids: Tensor) -> Tensor:
399
- """Delta-z over 2K for horizon W at each position (normalized)."""
400
- cfg = self.cfg
401
- a0 = self.layers[0]["attn"]
402
- x = sum(emb(ids[..., i]) for i, emb in enumerate(self.byte_emb))
403
- x = x + self.pos[:, : ids.shape[1]]
404
- kh = a0._split_addr(a0.k_addr(self.layers[0]["norm1"](x)),
405
- ids.shape[0], ids.shape[1])
406
- pk_p, pk_m = a0._address(kh) # (B,H,S,K)
407
- p = torch.cat([pk_p, pk_m], dim=-1).mean(dim=1) # (B,S,2K)
408
- cs = torch.cat([torch.zeros_like(p[:, :1]), p.cumsum(dim=1)], dim=1)
409
- W = cfg.write_horizon
410
- B, S, _ = p.shape
411
- end = torch.arange(S, device=p.device).clamp_max(S - 1)
412
- lo = cs[:, 1:] # prefix up to t (incl)
413
- hi = cs[:, torch.clamp(torch.arange(S, device=p.device) + W, max=S)]
414
- dz = (hi - lo).clamp_min(0)
415
- valid = (torch.arange(S, device=p.device) + 1 < S) # at least 1 future tok
416
- dz = dz / dz.sum(-1, keepdim=True).clamp_min(1e-9)
417
- return dz, valid
418
-
419
- # ---- pointer head: compositional D=48 candidate coordinates ----
420
- def _cand_coords(self) -> Tensor:
421
- e = sum(emb(self.bank[:, i]) for i, emb in enumerate(self.byte_emb))
422
- return F.normalize(self.W_cand48(e), dim=-1) # (M, d_point)
423
-
424
- @torch.no_grad()
425
- def _refresh_nn(self, coords: Tensor, step: int) -> None:
426
- """Hard-negative table: each candidate's k nearest sphere neighbors
427
- (excluding self). Refreshed periodically — coordinates drift."""
428
- cos = coords @ coords.t()
429
- cos.fill_diagonal_(-2.0)
430
- self._nn_table = cos.topk(self.cfg.pointer_k, dim=-1).indices # (M, k)
431
- self._nn_step = step
432
- # decode budget: theta_NN/2 of the CURRENT candidate constellation
433
- nn_deg = torch.acos(cos.max(dim=-1).values.clamp(-1, 1)) * 180 / math.pi
434
- self._decode_budget_deg = (nn_deg.median() / 2).item()
435
-
436
- def _pointer_loss(self, h: Tensor, targets: Tensor,
437
- step: int) -> Tuple[Tensor, Dict]:
438
- """NN-on-the-sphere head (T5-chained with the byte tail):
439
- in-bank: -log gate - log softmax_{target ∪ kNN(target)}(T * yhat·c)
440
- + lambda_cos (1 - yhat·c_target) [aiming term]
441
- out-bank: -log(1-gate) - log P_byte(g)
442
- Decode metric: exact-NN rate + median angular error vs the budget
443
- theta_NN/2 (the decode-correctness theorem)."""
444
- cfg = self.cfg
445
- logs: Dict[str, float] = {}
446
- coords = self._cand_coords() # (M, d_point)
447
- if step - self._nn_step >= cfg.pointer_refresh or len(self._nn_table) == 0:
448
- self._refresh_nn(coords.detach(), step)
449
-
450
- yhat = F.normalize(self.W_point(h), dim=-1) # (B,S,d_point)
451
- bidx = self._bank_index(targets)
452
- in_bank = bidx >= 0
453
- logs["coverage"] = in_bank.float().mean().item()
454
-
455
- g_logit = self.gate(h).squeeze(-1)
456
- nll_byte = -self._byte_logprob(h, targets)
457
-
458
- B, S = bidx.shape
459
- tgt = bidx.clamp_min(0) # (B,S)
460
- negs = self._nn_table[tgt] # (B,S,k) hard negatives
461
- cand_idx = torch.cat([tgt.unsqueeze(-1), negs], dim=-1) # (B,S,1+k)
462
- c = coords[cand_idx] # (B,S,1+k,d_point)
463
- logits = torch.einsum("bsd,bsnd->bsn", yhat, c) * self.point_T
464
- nll_point = F.cross_entropy(
465
- logits.reshape(-1, logits.shape[-1]),
466
- torch.zeros(B * S, dtype=torch.long, device=h.device),
467
- reduction="none").view(B, S)
468
- cos_t = torch.einsum("bsd,bsd->bs", yhat, coords[tgt])
469
- aim = cfg.pointer_cos_weight * (1.0 - cos_t)
470
-
471
- nll = torch.where(in_bank,
472
- -F.logsigmoid(g_logit) + nll_point + aim,
473
- -F.logsigmoid(-g_logit) + nll_byte)
474
- loss = nll.mean()
475
- logs["bpb"] = loss.item() / 3 / math.log(2)
476
- logs["gate_acc"] = ((torch.sigmoid(g_logit) > 0.5) == in_bank
477
- ).float().mean().item()
478
- with torch.no_grad(): # decode metrics
479
- if in_bank.any():
480
- full = (yhat @ coords.t()) # (B,S,M)
481
- pred = full.argmax(-1)
482
- logs["nn_exact"] = (pred[in_bank] == tgt[in_bank]
483
- ).float().mean().item()
484
- # PROPER eval likelihood: full-bank softmax (comparable to
485
- # hybrid bpb; the training loss above is contrastive-over-33
486
- # and is NOT a likelihood — do not compare it across heads)
487
- nll_full = F.cross_entropy(
488
- (full * self.point_T).reshape(-1, full.shape[-1]),
489
- tgt.reshape(-1), reduction="none").view_as(tgt)
490
- nll_eval = torch.where(in_bank,
491
- -F.logsigmoid(g_logit) + nll_full,
492
- -F.logsigmoid(-g_logit) + nll_byte)
493
- logs["bpb_eval"] = nll_eval.mean().item() / 3 / math.log(2)
494
- ang = torch.acos(cos_t[in_bank].clamp(-1, 1)) * 180 / math.pi
495
- logs["ang_err_deg"] = ang.median().item()
496
- logs["budget_deg"] = self._decode_budget_deg
497
- logs["in_budget"] = (ang < self._decode_budget_deg
498
- ).float().mean().item()
499
- return loss, logs
500
-
501
- # ---- the loss (T5-exact hybrid + auxiliaries) ----
502
- def forward_loss(self, ids: Tensor, targets: Tensor,
503
- step: int = 0) -> Tuple[Tensor, Dict]:
504
- cfg = self.cfg
505
- h = self.backbone(ids) # (B,S,d)
506
- logs: Dict[str, float] = {}
507
-
508
- if cfg.head == "pointer":
509
- assert self.bank is not None, "pointer head requires a bank"
510
- return self._pointer_loss(h, targets, step)
511
-
512
- if cfg.head == "byte":
513
- nll = -self._byte_logprob(h, targets) # (B,S)
514
- loss = nll.mean()
515
- logs["bpb"] = loss.item() / 3 / math.log(2)
516
- return loss, logs
517
-
518
- pi_p, pi_m = self._pi(h) # (B,S,K) each
519
-
520
- if cfg.head == "sampled":
521
- # uniform negatives + target; uniform proposal => logQ constant,
522
- # cancels in softmax (literature requirement satisfied trivially)
523
- B, S, _ = h.shape
524
- neg = torch.randint(0, 256, (cfg.n_negatives, 3), device=h.device)
525
- cand = torch.cat([targets.reshape(-1, 3), neg], dim=0)
526
- cand_ids, inv = torch.unique(_tri_ids(cand), return_inverse=True)
527
- uniq = torch.stack([cand_ids // 65536, (cand_ids // 256) % 256,
528
- cand_ids % 256], dim=-1)
529
- k_p, k_m = self._kappa(uniq)
530
- logits = self._bank_logits(pi_p.reshape(-1, cfg.K),
531
- pi_m.reshape(-1, cfg.K), k_p, k_m)
532
- tgt_idx = inv[: B * S]
533
- loss = F.cross_entropy(logits, tgt_idx)
534
- logs["bpb"] = loss.item() / 3 / math.log(2)
535
- logs["n_cand"] = float(len(uniq))
536
- return loss, logs
537
-
538
- # banked heads
539
- assert self.bank is not None, "head='hybrid'/'bank' requires a bank"
540
- if cfg.head == "hybrid" and cfg.bank_scorer == "pmix":
541
- logits, pm_logs = self._pmix_logits(h) # (B,S,M)
542
- logs.update(pm_logs)
543
- else:
544
- k_p, k_m = self._kappa(self.bank) # (M,K) each
545
- logits = self._bank_logits(pi_p, pi_m, k_p, k_m) # (B,S,M)
546
- bidx = self._bank_index(targets) # (B,S), -1 = miss
547
- in_bank = bidx >= 0
548
- logs["coverage"] = in_bank.float().mean().item()
549
-
550
- if cfg.head == "bank":
551
- # ablation head: proper only on covered targets (coverage logged)
552
- lb = F.log_softmax(logits, dim=-1)
553
- nll = -lb.gather(-1, bidx.clamp_min(0).unsqueeze(-1)).squeeze(-1)
554
- loss = nll[in_bank].mean() if in_bank.any() else logits.sum() * 0
555
- logs["bpb_inbank"] = (loss.item() / 3 / math.log(2)
556
- if in_bank.any() else float("nan"))
557
- return loss, logs
558
-
559
- # ── T5-exact hybrid: -log P(g) per position ──
560
- g_logit = self.gate(h).squeeze(-1) # (B,S)
561
- log_g = F.logsigmoid(g_logit)
562
- log_1mg = F.logsigmoid(-g_logit)
563
- lb = F.log_softmax(logits, dim=-1)
564
- nll_bank = -lb.gather(-1, bidx.clamp_min(0).unsqueeze(-1)).squeeze(-1)
565
- nll_byte = -self._byte_logprob(h, targets)
566
- nll = torch.where(in_bank, -log_g + nll_bank, -log_1mg + nll_byte)
567
- loss = nll.mean()
568
- logs["bpb"] = loss.item() / 3 / math.log(2)
569
- with torch.no_grad(): # branch-conditional currencies
570
- if in_bank.any():
571
- logs["bpb_bank_cond"] = nll_bank[in_bank].mean().item() / 3 / math.log(2)
572
- if (~in_bank).any():
573
- logs["bpb_byte_cond"] = nll_byte[~in_bank].mean().item() / 3 / math.log(2)
574
- logs["gate_acc"] = ((torch.sigmoid(g_logit) > 0.5) == in_bank
575
- ).float().mean().item()
576
-
577
- # ── auxiliaries ──
578
- if cfg.write_weight > 0:
579
- dz, valid = self._write_target(ids)
580
- pred = F.log_softmax(self.W_write(h), dim=-1)
581
- kl = F.kl_div(pred, dz, reduction="none").sum(-1)
582
- wl = kl[:, valid].mean()
583
- loss = loss + cfg.write_weight * wl
584
- logs["write_kl"] = wl.item()
585
-
586
- return loss, logs
587
-
588
- # ---- the branching gauge ([TAU] inverted) ----
589
- @torch.no_grad()
590
- def branching_gauge(self, ids: Tensor, n_baseline: int = 4096) -> Dict:
591
- h = self.backbone(ids)
592
- pi_p, pi_m = self._pi(h)
593
- A = F.normalize(self.codebook, dim=-1)
594
- conf = ((pi_p - pi_m) @ A).norm(dim=-1).reshape(-1)
595
- rows = F.normalize(torch.randn(n_baseline, self.cfg.D_addr,
596
- device=h.device), dim=-1)
597
- bp, bm = self._address_rows(rows)
598
- base = ((bp - bm) @ A).norm(dim=-1)
599
- mu, sd = base.mean(), base.std()
600
- return {"conf_mean": conf.mean().item(),
601
- "kernel_invariant": mu.item(),
602
- "branching_frac": (conf < mu - 2 * sd).float().mean().item()}
603
-
604
-
605
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
606
- # Training
607
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
608
-
609
- def train_aleph_lm(cfg: AlephLMConfig,
610
- stream: Optional[TrigramStream] = None) -> Dict:
611
- torch.manual_seed(cfg.seed)
612
- dev = torch.device(cfg.device)
613
- stream = stream or TrigramStream(cfg.corpus_id, cfg.split,
614
- cfg.max_corpus_bytes, cfg.seed)
615
- bank = None
616
- if cfg.head in ("hybrid", "bank", "pointer"):
617
- if os.path.isfile(str(cfg.bank_source)): # stratified-atlas bank
618
- d = torch.load(cfg.bank_source, map_location="cpu", weights_only=False)
619
- bank = d["bank"] if isinstance(d, dict) else d
620
- print(f"[bank] loaded {len(bank)} trigram candidates "
621
- f"from {cfg.bank_source}")
622
- elif cfg.bank_source == "wordnet":
623
- try:
624
- bank = build_wordnet_bank(cfg.bank_size)
625
- print(f"[bank] wordnet char_eng_3gram: {len(bank)} types")
626
- except Exception as e:
627
- print(f"[bank] wordnet unavailable ({e}); falling back to corpus")
628
- if bank is None:
629
- bank = build_corpus_bank(stream, cfg.bank_size)
630
- print(f"[bank] corpus top-{len(bank)} trigrams")
631
-
632
- model = AlephLM(cfg, bank=bank).to(dev)
633
- n_params = sum(p.numel() for p in model.parameters())
634
- opt = torch.optim.Adam(model.parameters(), lr=cfg.lr) # pure Adam
635
- sched = (torch.optim.lr_scheduler.CosineAnnealingLR(
636
- opt, T_max=cfg.steps, eta_min=cfg.lr * 0.1) if cfg.lr_decay else None)
637
- alephs = model.aleph_layers()
638
- for a in alephs:
639
- a.emit_diversity = cfg.div_weight > 0
640
-
641
- snapshots: List[Tuple[int, Tensor]] = []
642
- if cfg.snapshot_codebook:
643
- snapshots.append((0, alephs[0].export_codebook()))
644
-
645
- print(f"\n=== AlephLM head={cfg.head} pi={cfg.pi_mode} "
646
- f"bank={cfg.bank_source if bank is not None else '-'} "
647
- f"params={n_params:,} ctx={cfg.seq_len} tri "
648
- f"eff.batch={cfg.batch_size * cfg.accum_steps} dev={dev} ===")
649
- result: Dict = {"head": cfg.head, "params": n_params}
650
- t0 = time.time()
651
-
652
- for step in range(1, cfg.steps + 1):
653
- opt.zero_grad(set_to_none=True)
654
- loss_sum, logs_acc = 0.0, {}
655
- for _ in range(cfg.accum_steps):
656
- ids, targets = stream.sample(cfg.batch_size, cfg.seq_len, dev)
657
- loss, logs = model.forward_loss(ids, targets, step=step)
658
- total = loss
659
- if cfg.div_weight > 0:
660
- total = total + cfg.div_weight * sum(
661
- a.diversity_loss() for a in alephs)
662
- (total / cfg.accum_steps).backward()
663
- loss_sum += loss.item()
664
- logs_acc = logs
665
- loss_avg = loss_sum / cfg.accum_steps
666
- gnorm = torch.nn.utils.clip_grad_norm_(
667
- model.parameters(), max(loss_avg, 1.0))
668
- opt.step()
669
- if sched is not None:
670
- sched.step()
671
-
672
- if step % cfg.log_every == 0 or step == cfg.steps:
673
- rate = step * cfg.batch_size * cfg.seq_len * cfg.accum_steps \
674
- / (time.time() - t0)
675
- line = (f" step {step:6d} loss {loss_avg:.4f} "
676
- f"bpb {logs_acc.get('bpb', logs_acc.get('bpb_inbank', float('nan'))):.3f} "
677
- f"|g| {gnorm:.2f} {rate/1e3:.1f}k tri/s")
678
- if "coverage" in logs_acc:
679
- line += f" cov {logs_acc['coverage']:.0%}"
680
- if "gate_acc" in logs_acc:
681
- line += f" gate {logs_acc['gate_acc']:.0%}"
682
- if "nn_exact" in logs_acc:
683
- line += (f" bpbE {logs_acc.get('bpb_eval', float('nan')):.3f}"
684
- f" nn {logs_acc['nn_exact']:.0%}"
685
- f" ang {logs_acc['ang_err_deg']:.1f}/"
686
- f"{logs_acc['budget_deg']:.1f}deg"
687
- f" inBudget {logs_acc['in_budget']:.0%}")
688
- if "bpb_bank_cond" in logs_acc:
689
- line += (f" inB {logs_acc['bpb_bank_cond']:.3f}"
690
- f" outB {logs_acc.get('bpb_byte_cond', float('nan')):.3f}")
691
- if "mode_spread_deg" in logs_acc:
692
- line += (f" spread {logs_acc['mode_spread_deg']:.0f}deg"
693
- f" mixH {logs_acc['mix_entropy']:.2f}")
694
- if "write_kl" in logs_acc:
695
- line += f" wKL {logs_acc['write_kl']:.3f}"
696
- model.eval()
697
- with torch.no_grad():
698
- ids_p, _ = stream.sample(min(8, cfg.batch_size), cfg.seq_len, dev)
699
- st = alephs[0].address_stats(model.backbone(ids_p),
700
- max_rows=200_000)
701
- bg = model.branching_gauge(ids_p)
702
- model.train()
703
- line += (f" ppl {st['perplexity']:.0f}/{st['max_perplexity']:.0f}"
704
- f" conf {bg['conf_mean']:.3f}"
705
- f"/{bg['kernel_invariant']:.3f}"
706
- f" branch {bg['branching_frac']:.0%}")
707
- print(line)
708
- result.update(logs_acc)
709
- result.update({"loss": loss_avg, "step": step, **bg})
710
- if cfg.snapshot_codebook:
711
- snapshots.append((step, alephs[0].export_codebook()))
712
-
713
- if snapshots:
714
- traj = [(s, statute(cb)) for s, cb in snapshots]
715
- result["statute_trajectory"] = traj
716
- torch.save({"snapshots": snapshots, "statute_trajectory": traj,
717
- "config": cfg.__dict__}, cfg.snapshot_path)
718
- d0, d1 = traj[0][1]["deviation"], traj[-1][1]["deviation"]
719
- print(f"\n[basin] statute: dev {d0:+.4f} -> {d1:+.4f} "
720
- f"({traj[-1][1]['statute']}); snapshots -> {cfg.snapshot_path}")
721
- if cfg.checkpoint_path:
722
- torch.save({"model_state_dict": model.state_dict(),
723
- "config": cfg.__dict__,
724
- "bank": model.bank.cpu() if model.bank is not None else None},
725
- cfg.checkpoint_path)
726
- print(f"[ckpt] -> {cfg.checkpoint_path}")
727
- return result
728
-
729
-
730
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
731
- # Smoke + activation
732
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
733
-
734
- def _smoke():
735
- print("=" * 70)
736
- print("AlephLM — smoke")
737
- print("=" * 70)
738
- rng = np.random.default_rng(0)
739
- words = [b"the", b"aleph", b"predicts", b"its", b"own", b"future",
740
- b"through", b"a", b"codebook"]
741
- path = "/tmp/_alm_corpus.txt"
742
- with open(path, "wb") as f:
743
- f.write(b" ".join(words[i] for i in rng.integers(0, 9, 80000)))
744
-
745
- base = dict(corpus_id=path, max_corpus_bytes=None, steps=25, log_every=25,
746
- dim=96, n_layers=2, n_heads=4, K=16, seq_len=48, batch_size=8,
747
- bank_size=256, n_negatives=128, device="cpu",
748
- checkpoint_path=None, snapshot_path="/tmp/_alm_snaps.pt")
749
- prior_bpb = 8.0
750
- for head in ("hybrid", "byte", "bank", "sampled"):
751
- r = train_aleph_lm(AlephLMConfig(head=head, **base))
752
- bpb = r.get("bpb", r.get("bpb_inbank", float("nan")))
753
- assert math.isfinite(r["loss"]), head
754
- print(f" ✓ head={head:8s} loss {r['loss']:.3f} bpb {bpb:.2f} "
755
- f"(uniform prior {prior_bpb:.1f})")
756
- # pi ablation path + gradient to codebook through the PREDICT/CANDIDATE legs
757
- cfg = AlephLMConfig(head="hybrid", pi_mode="address", **base)
758
- stream = TrigramStream(path, max_corpus_bytes=None, seed=0)
759
- bank = build_corpus_bank(stream, cfg.bank_size)
760
- m = AlephLM(cfg, bank=bank)
761
- ids, tg = stream.sample(4, cfg.seq_len, "cpu")
762
- loss, _ = m.forward_loss(ids, tg)
763
- loss.backward()
764
- assert m.codebook.grad is not None and torch.isfinite(m.codebook.grad).all()
765
- print(f" ✓ pi_mode='address' ablation runs; codebook grad |{m.codebook.grad.norm():.3f}|")
766
- print("All smoke tests passed.")
767
-
768
-
769
- #if __name__ == "__main__":
770
- # import argparse
771
- # ap = argparse.ArgumentParser(description="AlephLM — prediction through the codebook")
772
- # ap.add_argument("--smoke-only", action="store_true")
773
- # ap.add_argument("--head", default="hybrid",
774
- # choices=["hybrid", "byte", "bank", "sampled", "pointer"])
775
- # ap.add_argument("--bank", default="corpus", choices=["corpus", "wordnet"])
776
- # ap.add_argument("--pi", default="free", choices=["free", "address"])
777
- # ap.add_argument("--scorer", default="kernel", choices=["kernel", "pmix"])
778
- # ap.add_argument("--pointers", type=int, default=4)
779
- # ap.add_argument("--steps", type=int, default=10_000)
780
- # ap.add_argument("--corpus-mb", type=int, default=100)
781
- # ap.add_argument("--device",
782
- # default="cuda" if torch.cuda.is_available() else "cpu")
783
- # args, _unknown = ap.parse_known_args()
784
- # if args.smoke_only:
785
- # _smoke()
786
- # else:
787
- # cfg = AlephLMConfig(head=args.head, bank_source=args.bank,
788
- # pi_mode=args.pi, steps=args.steps,
789
- # bank_scorer=args.scorer, n_pointers=args.pointers,
790
- # max_corpus_bytes=args.corpus_mb * 1_000_000,
791
- # device=args.device)
792
- # train_aleph_lm(cfg)