AbstractPhil commited on
Commit
9745fcf
Β·
verified Β·
1 Parent(s): 8e22bad

Create aleph_routed_trigram_lm.py

Browse files
experiments/exp_007_aleph_routed_attention/aleph_routed_trigram_lm.py ADDED
@@ -0,0 +1,532 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
75
+ # Config
76
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
77
+
78
+ @dataclass
79
+ class TrigramLMConfig:
80
+ """Everything for one basin run.
81
+
82
+ Substrate:
83
+ corpus_id: HF dataset config name (default = the aleph batteries'
84
+ corpus) OR a local .txt/.text path
85
+ max_corpus_bytes: cap on bytes loaded (None = whole corpus, ~520 MB for
86
+ wikitext-103). 50–100 MB is plenty for these runs.
87
+ seq_len: context length in TRIGRAMS (bytes seen = 3*seq_len)
88
+
89
+ Model:
90
+ dim/n_layers/n_heads: transformer shell
91
+ attn_mode: 'hub' | 'bucket' | 'standard'
92
+ K/D_addr/tau: aleph routing knobs (ignored for 'standard')
93
+ codebook_init: 'random' for the basin test (MANDATORY there) |
94
+ 'fibonacci' | (K, D_addr) array transplant
95
+
96
+ Training:
97
+ pure Adam + cosine decay to 10%; loss reported in nats and bits/byte.
98
+ """
99
+ # substrate
100
+ corpus_id: str = "wikitext-103-raw-v1"
101
+ split: str = "train"
102
+ max_corpus_bytes: Optional[int] = 100_000_000
103
+ seq_len: int = 256 # trigrams (= 768 bytes of context)
104
+ seed: int = 1234
105
+
106
+ # model
107
+ dim: int = 384
108
+ n_layers: int = 4
109
+ n_heads: int = 6
110
+ attn_mode: str = "hub" # 'hub' | 'bucket' | 'standard'
111
+ K: int = 64
112
+ D_addr: int = 4
113
+ tau: float = 0.1
114
+ codebook_init: object = "random" # basin test requires 'random'
115
+ div_weight: float = 0.0 # anti-collapse; run 0 first, observe
116
+
117
+ # paradigm + scale
118
+ shared_codebook: bool = True # ONE vocabulary, many speakers: all
119
+ # layers address the same (K,D) param,
120
+ # concentrating address pressure n_layers-x
121
+ accum_steps: int = 1 # gradient accumulation (effective batch
122
+ # = batch_size * accum_steps)
123
+ stream_segments: int = 1 # segments per sample, each seq_len long;
124
+ # codebook-memory state carried across
125
+ # (TBPTT, detached between segments).
126
+ # context = seq_len * stream_segments
127
+ # at CONSTANT attention memory. hub-only.
128
+ probe_rows: int = 200_000 # address-stats sample size (ppl estimator)
129
+
130
+ # training
131
+ steps: int = 2600 # optimizer steps (micro-batches =
132
+ # steps * accum_steps)
133
+ batch_size: int = 512
134
+ lr: float = 3e-4
135
+ lr_decay: bool = True
136
+ log_every: int = 100
137
+ eval_batches: int = 8
138
+ device: str = "cuda" if torch.cuda.is_available() else "cpu"
139
+ amp: bool = False # bf16 autocast on the shell (the
140
+ # address stays fp32 inside)
141
+
142
+ # outputs
143
+ snapshot_codebook: bool = True
144
+ snapshot_path: str = "aleph_lm_codebook_snapshots.pt"
145
+ checkpoint_path: Optional[str] = "aleph_trigram_lm.pt"
146
+
147
+ def __post_init__(self):
148
+ assert self.attn_mode in ("hub", "bucket", "standard")
149
+ assert self.dim % self.n_heads == 0
150
+ assert self.accum_steps >= 1 and self.stream_segments >= 1
151
+ if self.stream_segments > 1:
152
+ assert self.attn_mode == "hub", \
153
+ "streaming requires mode='hub' (bucket sorts globally)"
154
+
155
+
156
+
157
+
158
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
159
+ # Statute monitor β€” the program's own diagnostic geometry (Sec 3.11)
160
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
161
+
162
+ def _canon(x: Tensor) -> Tensor:
163
+ """Sign-canonicalize onto RP^(D-1): flip so the first nonzero coord is
164
+ positive (antipodes map to one representative)."""
165
+ x = F.normalize(x, dim=-1)
166
+ first_nz = x[torch.arange(len(x)), x.abs().argmax(dim=-1)]
167
+ return x * torch.sign(first_nz).unsqueeze(-1)
168
+
169
+
170
+ def _mean_projective_angle(X: Tensor) -> float:
171
+ """Mean pairwise acos|cos| over distinct pairs (radians)."""
172
+ c = (X @ X.t()).clamp(-1.0, 1.0).abs()
173
+ iu = torch.triu_indices(len(X), len(X), offset=1)
174
+ return torch.acos(c[iu[0], iu[1]]).mean().item()
175
+
176
+
177
+ _UNIFORM_BASELINE: Dict[int, float] = {}
178
+
179
+ def projective_deviation(axes: Tensor, n_ref: int = 4096,
180
+ seed: int = 0) -> float:
181
+ """Uniformity deviation per the program definition: mean pairwise
182
+ projective angle of the axes MINUS the same statistic for n_ref uniform
183
+ random projective points at the same D. Signed; sign matters."""
184
+ D = axes.shape[-1]
185
+ if D not in _UNIFORM_BASELINE:
186
+ g = torch.Generator().manual_seed(seed)
187
+ ref = F.normalize(torch.randn(n_ref, D, generator=g), dim=-1)
188
+ _UNIFORM_BASELINE[D] = _mean_projective_angle(ref)
189
+ return _mean_projective_angle(F.normalize(axes.float(), dim=-1)) \
190
+ - _UNIFORM_BASELINE[D]
191
+
192
+
193
+ def antipodal_pair_fraction(axes: Tensor, thresh: float = -0.9) -> float:
194
+ """Fraction of rows in mutual most-negative pairs with cos < thresh
195
+ (the antipodal-collapse acceptance rule)."""
196
+ A = F.normalize(axes.float(), dim=-1)
197
+ c = A @ A.t()
198
+ c.fill_diagonal_(0.0)
199
+ partner = c.argmin(dim=-1)
200
+ val = c.gather(-1, partner.unsqueeze(-1)).squeeze(-1)
201
+ mutual = partner[partner] == torch.arange(len(A))
202
+ return ((val < thresh) & mutual).float().mean().item()
203
+
204
+
205
+ def statute(axes: Tensor) -> Dict[str, object]:
206
+ """Classify per the program taxonomy: dev > +0.05 polytope-class
207
+ (repulsive packing); |dev| < 0.05 uniform-class; dev < -0.05 degenerate
208
+ (clumping, the failure statute)."""
209
+ dev = projective_deviation(axes)
210
+ pf = antipodal_pair_fraction(axes)
211
+ cls = ("polytope" if dev > 0.05 else
212
+ "degenerate" if dev < -0.05 else "uniform")
213
+ return {"deviation": dev, "pair_fraction": pf, "statute": cls}
214
+
215
+
216
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
217
+ # Substrate β€” trigram stream (mirrors ByteTrigramDataset's loading)
218
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
219
+
220
+ class TrigramStream:
221
+ """WikiText-103 (or local .txt) as a uint8 byte stream, sampled as
222
+ causal trigram sequences.
223
+
224
+ __call__(batch, seq_len) -> (ids, targets):
225
+ ids: (B, S, 3) uint8->long β€” trigram t = bytes[3t : 3t+3]
226
+ targets: (B, S, 3) β€” trigram t+1 (next-trigram prediction)
227
+ Windows are sampled at byte offsets aligned to stride 3 so the trigram
228
+ framing matches the image packing (cell i = bytes[3i : 3i+3])."""
229
+
230
+ def __init__(self, corpus_id: str, split: str = "train",
231
+ max_corpus_bytes: Optional[int] = None, seed: int = 1234):
232
+ if os.path.isfile(corpus_id) and corpus_id.endswith((".txt", ".text")):
233
+ print(f"[TrigramStream] loading local corpus {corpus_id} ...")
234
+ with open(corpus_id, "rb") as f:
235
+ raw = f.read(max_corpus_bytes) if max_corpus_bytes else f.read()
236
+ self.stream = np.frombuffer(raw, dtype=np.uint8).copy()
237
+ else:
238
+ print(f"[TrigramStream] loading HF corpus {corpus_id} ...")
239
+ from datasets import load_dataset
240
+ if corpus_id.startswith("wikitext"):
241
+ ds = load_dataset("Salesforce/wikitext", corpus_id, split=split)
242
+ else:
243
+ ds = load_dataset(corpus_id, split=split)
244
+ # accumulate utf-8 bytes directly into a byte buffer β€” never a
245
+ # Python list of ints (prototypes/CLAUDE.md memory trap #3)
246
+ buf = bytearray()
247
+ cap = max_corpus_bytes or float("inf")
248
+ for row in ds:
249
+ t = row.get("text", "")
250
+ if t:
251
+ buf.extend(t.encode("utf-8", errors="ignore"))
252
+ if len(buf) >= cap:
253
+ break
254
+ self.stream = np.frombuffer(
255
+ bytes(buf[: max_corpus_bytes] if max_corpus_bytes else buf),
256
+ dtype=np.uint8).copy()
257
+ n_tri = len(self.stream) // 3
258
+ print(f"[TrigramStream] {len(self.stream):,} bytes "
259
+ f"= {n_tri:,} trigrams")
260
+ assert n_tri > 0, "corpus too small"
261
+ self._rng = np.random.default_rng(seed)
262
+
263
+ def sample(self, batch: int, seq_len: int,
264
+ device) -> Tuple[Tensor, Tensor]:
265
+ need = 3 * (seq_len + 1) # +1 trigram for targets
266
+ hi = len(self.stream) - need
267
+ assert hi > 0, f"corpus shorter than one window ({need} bytes)"
268
+ starts = self._rng.integers(0, hi // 3, size=batch) * 3 # stride-3 aligned
269
+ idx = starts[:, None] + np.arange(need)[None, :] # (B, need)
270
+ window = self.stream[idx] # (B, need) uint8
271
+ tri = torch.from_numpy(window.astype(np.int64)) \
272
+ .view(batch, seq_len + 1, 3)
273
+ ids, targets = tri[:, :-1], tri[:, 1:]
274
+ return ids.to(device), targets.to(device)
275
+
276
+
277
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
278
+ # Model β€” byte-factored trigram LM
279
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
280
+
281
+ class TrigramLM(nn.Module):
282
+ """Causal LM over trigram positions. Embedding = sum of three per-slot
283
+ byte embeddings (+ learned positions); head = three 256-way byte heads.
284
+ Geometric-path hygiene: no BatchNorm/Dropout, pure pre-LN residual shell."""
285
+
286
+ def __init__(self, cfg: TrigramLMConfig):
287
+ super().__init__()
288
+ self.cfg = cfg
289
+ d = cfg.dim
290
+ self.byte_emb = nn.ModuleList([nn.Embedding(256, d) for _ in range(3)])
291
+ self.pos = nn.Parameter(0.02 * torch.randn(1, cfg.seq_len, d))
292
+
293
+ def make_attn() -> nn.Module:
294
+ if cfg.attn_mode == "standard":
295
+ return StandardAttention(d, cfg.n_heads, causal=True)
296
+ return AlephRoutedAttention(AlephAttentionConfig(
297
+ dim=d, num_heads=cfg.n_heads, mode=cfg.attn_mode,
298
+ K=cfg.K, D_addr=cfg.D_addr, tau=cfg.tau, causal=True,
299
+ codebook_init=cfg.codebook_init))
300
+
301
+ self.layers = nn.ModuleList([
302
+ nn.ModuleDict({
303
+ "norm1": nn.LayerNorm(d), "attn": make_attn(),
304
+ "norm2": nn.LayerNorm(d),
305
+ "mlp": nn.Sequential(nn.Linear(d, 4 * d), nn.GELU(),
306
+ nn.Linear(4 * d, d)),
307
+ }) for _ in range(cfg.n_layers)
308
+ ])
309
+ self.norm_f = nn.LayerNorm(d)
310
+ self.heads = nn.ModuleList([nn.Linear(d, 256) for _ in range(3)])
311
+
312
+ # one vocabulary, many speakers: tie every layer's codebook to layer 0's
313
+ if cfg.shared_codebook and cfg.attn_mode in ("hub", "bucket"):
314
+ shared = self.layers[0]["attn"].codebook
315
+ for L in self.layers[1:]:
316
+ L["attn"].codebook = shared
317
+
318
+ def aleph_layers(self) -> List[AlephRoutedAttention]:
319
+ return [m for m in self.modules() if isinstance(m, AlephRoutedAttention)]
320
+
321
+ def backbone(self, ids: Tensor) -> Tensor:
322
+ """ids: (B, S, 3) -> (B, S, dim)"""
323
+ x = sum(emb(ids[..., i]) for i, emb in enumerate(self.byte_emb))
324
+ x = x + self.pos[:, : ids.shape[1]]
325
+ for L in self.layers:
326
+ x = x + L["attn"](L["norm1"](x))
327
+ x = x + L["mlp"](L["norm2"](x))
328
+ return self.norm_f(x)
329
+
330
+ def forward(self, ids: Tensor) -> Tensor:
331
+ """(B, S, 3) -> byte logits (B, S, 3, 256)"""
332
+ h = self.backbone(ids)
333
+ return torch.stack([head(h) for head in self.heads], dim=2)
334
+
335
+ def loss(self, ids: Tensor, targets: Tensor) -> Tensor:
336
+ """Mean cross-entropy per byte (nats/byte). bpb = loss / ln 2."""
337
+ logits = self(ids) # (B,S,3,256)
338
+ return F.cross_entropy(logits.reshape(-1, 256), targets.reshape(-1))
339
+
340
+ # ── streaming: segment-recurrent backbone (codebook memory carried) ──
341
+ def stream_loss(self, ids: Tensor, targets: Tensor,
342
+ states: Optional[List] = None
343
+ ) -> Tuple[Tensor, List]:
344
+ """One SEGMENT with per-layer carried states. states[i] is layer i's
345
+ (Mp, Mm, zp, zm) or None. Returns (loss, new_states) β€” caller detaches
346
+ between backward passes (TBPTT-1: grads flow within segment; values
347
+ flow forever)."""
348
+ x = sum(emb(ids[..., i]) for i, emb in enumerate(self.byte_emb))
349
+ x = x + self.pos[:, : ids.shape[1]]
350
+ new_states: List = []
351
+ states = states or [None] * len(self.layers)
352
+ for L, st in zip(self.layers, states):
353
+ a, ns = L["attn"].forward_stream(L["norm1"](x), state=st)
354
+ x = x + a
355
+ x = x + L["mlp"](L["norm2"](x))
356
+ new_states.append(ns)
357
+ h = self.norm_f(x)
358
+ logits = torch.stack([head(h) for head in self.heads], dim=2)
359
+ loss = F.cross_entropy(logits.reshape(-1, 256), targets.reshape(-1))
360
+ return loss, new_states
361
+
362
+
363
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
364
+ # Training
365
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
366
+
367
+ def train_trigram_lm(cfg: TrigramLMConfig,
368
+ stream: Optional[TrigramStream] = None) -> Dict:
369
+ torch.manual_seed(cfg.seed)
370
+ dev = torch.device(cfg.device)
371
+ stream = stream or TrigramStream(cfg.corpus_id, cfg.split,
372
+ cfg.max_corpus_bytes, cfg.seed)
373
+ model = TrigramLM(cfg).to(dev)
374
+ n_params = sum(p.numel() for p in model.parameters())
375
+ opt = torch.optim.Adam(model.parameters(), lr=cfg.lr) # pure Adam, never AdamW
376
+ sched = (torch.optim.lr_scheduler.CosineAnnealingLR(
377
+ opt, T_max=cfg.steps, eta_min=cfg.lr * 0.1) if cfg.lr_decay else None)
378
+
379
+ alephs = model.aleph_layers()
380
+ for a in alephs:
381
+ a.emit_diversity = cfg.div_weight > 0
382
+
383
+ snapshots: List[Tuple[int, Tensor]] = []
384
+ if cfg.snapshot_codebook and alephs:
385
+ snapshots.append((0, alephs[0].export_codebook()))
386
+
387
+ print(f"\n=== trigram LM mode={cfg.attn_mode} params={n_params:,} "
388
+ f"ctx={cfg.seq_len}x{cfg.stream_segments} trigrams "
389
+ f"({3*cfg.seq_len*cfg.stream_segments} bytes) "
390
+ f"eff.batch={cfg.batch_size*cfg.accum_steps} "
391
+ f"shared_cb={cfg.shared_codebook} "
392
+ f"device={dev} ===")
393
+ autocast = (torch.autocast(device_type=dev.type, dtype=torch.bfloat16)
394
+ if cfg.amp and dev.type == "cuda" else None)
395
+ result: Dict = {"mode": cfg.attn_mode, "params": n_params}
396
+ t0 = time.time()
397
+
398
+ segs = cfg.stream_segments
399
+ micro_scale = 1.0 / (cfg.accum_steps * segs)
400
+ for step in range(1, cfg.steps + 1):
401
+ opt.zero_grad(set_to_none=True)
402
+ loss_sum, n_micro = 0.0, 0
403
+ for _ in range(cfg.accum_steps):
404
+ # one long sample, split into `segs` carried segments
405
+ ids, targets = stream.sample(cfg.batch_size, cfg.seq_len * segs, dev)
406
+ states = None
407
+ for s in range(segs):
408
+ sl = slice(s * cfg.seq_len, (s + 1) * cfg.seq_len)
409
+ seg_ids, seg_tgt = ids[:, sl], targets[:, sl]
410
+ if autocast:
411
+ with autocast:
412
+ if segs > 1:
413
+ loss, states = model.stream_loss(seg_ids, seg_tgt, states)
414
+ else:
415
+ loss = model.loss(seg_ids, seg_tgt)
416
+ else:
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
+ total = loss
422
+ if cfg.div_weight > 0 and alephs:
423
+ total = total + cfg.div_weight * sum(
424
+ a.diversity_loss() for a in alephs)
425
+ (total * micro_scale).backward()
426
+ loss_sum += loss.item(); n_micro += 1
427
+ if states is not None: # TBPTT boundary
428
+ states = [tuple(t.detach() for t in st) for st in states]
429
+ loss_avg = loss_sum / n_micro
430
+ gnorm = torch.nn.utils.clip_grad_norm_(
431
+ model.parameters(), max(loss_avg, 1.0)) # the clip rule
432
+ opt.step()
433
+ if sched is not None:
434
+ sched.step()
435
+
436
+ if step % cfg.log_every == 0 or step == cfg.steps:
437
+ bpb = loss_avg / math.log(2)
438
+ rate = (step * cfg.batch_size * cfg.seq_len * segs
439
+ * cfg.accum_steps) / (time.time() - t0)
440
+ line = (f" step {step:6d} loss {loss_avg:.4f} "
441
+ f"bpb {bpb:.3f} |g| {gnorm:.2f} {rate/1e3:.1f}k tri/s")
442
+ if alephs:
443
+ model.eval()
444
+ with torch.no_grad():
445
+ x_probe = model.backbone(
446
+ ids[: min(8, cfg.batch_size), -cfg.seq_len:])
447
+ st = alephs[0].address_stats(x_probe, max_rows=cfg.probe_rows)
448
+ model.train()
449
+ line += (f" ppl {st['perplexity']:.1f}/{st['max_perplexity']:.0f}"
450
+ f" margin {st['margin']:.4f}"
451
+ f" conf {st['confidence']:.3f}")
452
+ result.update(st)
453
+ if cfg.snapshot_codebook:
454
+ snapshots.append((step, alephs[0].export_codebook()))
455
+ print(line)
456
+ result.update({"loss": loss_avg, "bpb": bpb, "step": step})
457
+
458
+ if snapshots:
459
+ result["codebook_snapshots"] = snapshots
460
+ drift = (snapshots[-1][1] - snapshots[0][1]).norm().item()
461
+ result["codebook_drift"] = drift
462
+ traj = [(s, statute(cb)) for s, cb in snapshots]
463
+ result["statute_trajectory"] = traj
464
+ torch.save({"snapshots": snapshots, "statute_trajectory": traj,
465
+ "config": cfg.__dict__, "K": cfg.K, "D_addr": cfg.D_addr},
466
+ cfg.snapshot_path)
467
+ print(f"\n[basin] {len(snapshots)} snapshots -> {cfg.snapshot_path}"
468
+ f" drift |A_end - A_0| = {drift:.4f}")
469
+ print("[basin] statute trajectory (program taxonomy: polytope is the "
470
+ "substrate-matched\n statute for byte-trigram; uniform is "
471
+ "the noise/OOD statute; degenerate = failure):")
472
+ for s, st in traj:
473
+ print(f" step {s:6d} dev {st['deviation']:+.4f} "
474
+ f"pairs {st['pair_fraction']:.0%} -> {st['statute']}")
475
+ print("[basin] deeper follow-up on saved snapshots: beta_2/axis via "
476
+ "ripser on projective\n angular distances (the "
477
+ "void/symbolic fingerprint, discovery #20).")
478
+ if cfg.checkpoint_path:
479
+ torch.save({"model_state_dict": model.state_dict(),
480
+ "config": cfg.__dict__}, cfg.checkpoint_path)
481
+ print(f"[ckpt] saved -> {cfg.checkpoint_path}")
482
+ return result
483
+
484
+
485
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
486
+ # Activation
487
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
488
+
489
+ def _smoke(device: str = "cpu"):
490
+ """End-to-end on a synthetic local corpus β€” no downloads."""
491
+ print("=" * 70)
492
+ print("aleph_trigram_lm β€” smoke (synthetic corpus)")
493
+ print("=" * 70)
494
+ path = "/tmp/_smoke_corpus.txt"
495
+ rng = np.random.default_rng(0)
496
+ words = [b"the", b"aleph", b"address", b"routes", b"attention",
497
+ b"through", b"a", b"learned", b"projective", b"codebook"]
498
+ with open(path, "wb") as f:
499
+ f.write(b" ".join(words[i] for i in rng.integers(0, len(words), 60_000)))
500
+ cfg = TrigramLMConfig(corpus_id=path, steps=30, log_every=10,
501
+ dim=96, n_layers=2, n_heads=4, K=16, seq_len=64,
502
+ batch_size=8, device=device,
503
+ snapshot_path="/tmp/_smoke_snaps.pt",
504
+ checkpoint_path=None)
505
+ r = train_trigram_lm(cfg)
506
+ assert "codebook_snapshots" in r and len(r["codebook_snapshots"]) >= 2
507
+ assert math.isfinite(r["loss"]) and r["loss"] < math.log(256)
508
+ print(f"\nsmoke OK β€” loss {r['loss']:.3f} (< ln256={math.log(256):.3f} prior), "
509
+ f"drift {r['codebook_drift']:.4f}, "
510
+ f"{len(r['codebook_snapshots'])} snapshots saved")
511
+
512
+
513
+ #if __name__ == "__main__":
514
+ # import argparse
515
+ # ap = argparse.ArgumentParser(description="Aleph trigram LM β€” basin run")
516
+ # ap.add_argument("--smoke-only", action="store_true")
517
+ # ap.add_argument("--mode", default="hub", choices=["hub", "bucket", "standard"])
518
+ # ap.add_argument("--steps", type=int, default=2600)
519
+ # ap.add_argument("--corpus-mb", type=int, default=100)
520
+ # ap.add_argument("--codebook-init", default="random")
521
+ # ap.add_argument("--device",
522
+ # default="cuda" if torch.cuda.is_available() else "cpu")
523
+ # args, _unknown = ap.parse_known_args() # notebook-safe (ignores -f kernel.json)
524
+ #
525
+ # if args.smoke_only:
526
+ # _smoke(device="cpu")
527
+ # else:
528
+ # cfg = TrigramLMConfig(attn_mode=args.mode, steps=args.steps,
529
+ # max_corpus_bytes=args.corpus_mb * 1_000_000,
530
+ # codebook_init=args.codebook_init,
531
+ # device=args.device)
532
+ # train_trigram_lm(cfg)