Update experiments/exp_007_aleph_routed_attention/aleph_routed_trigram_lm.py
Browse files
experiments/exp_007_aleph_routed_attention/aleph_routed_trigram_lm.py
CHANGED
|
@@ -1,60 +1,67 @@
|
|
| 1 |
-
#
|
| 2 |
"""
|
| 3 |
-
|
| 4 |
-
=================================================
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
"""
|
| 59 |
|
| 60 |
from __future__ import annotations
|
|
@@ -62,7 +69,7 @@ from __future__ import annotations
|
|
| 62 |
import math
|
| 63 |
import os
|
| 64 |
import time
|
| 65 |
-
from dataclasses import dataclass
|
| 66 |
from typing import Dict, List, Optional, Tuple
|
| 67 |
|
| 68 |
import numpy as np
|
|
@@ -76,250 +83,292 @@ from torch import Tensor
|
|
| 76 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 77 |
|
| 78 |
@dataclass
|
| 79 |
-
class
|
| 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
|
| 104 |
seed: int = 1234
|
| 105 |
|
| 106 |
-
#
|
| 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"
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
#
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
# training
|
| 131 |
-
steps: int =
|
| 132 |
-
|
| 133 |
-
|
| 134 |
lr: float = 3e-4
|
| 135 |
lr_decay: bool = True
|
| 136 |
-
|
| 137 |
-
|
| 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 = "
|
| 145 |
-
checkpoint_path: Optional[str] = "
|
| 146 |
|
| 147 |
def __post_init__(self):
|
| 148 |
-
assert self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
assert self.dim % self.n_heads == 0
|
| 150 |
-
assert self.
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
|
| 158 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 159 |
-
#
|
| 160 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 161 |
|
| 162 |
-
def
|
| 163 |
-
"""
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
"""
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 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 |
-
#
|
| 218 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 219 |
|
| 220 |
-
class
|
| 221 |
-
"""
|
| 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 |
-
|
| 294 |
-
|
| 295 |
-
return StandardAttention(d, cfg.n_heads, causal=True)
|
| 296 |
return AlephRoutedAttention(AlephAttentionConfig(
|
| 297 |
-
dim=d, num_heads=cfg.n_heads, mode=cfg.
|
| 298 |
-
|
| 299 |
codebook_init=cfg.codebook_init))
|
| 300 |
-
|
| 301 |
self.layers = nn.ModuleList([
|
| 302 |
-
nn.ModuleDict({
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 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:
|
|
@@ -327,206 +376,417 @@ class TrigramLM(nn.Module):
|
|
| 327 |
x = x + L["mlp"](L["norm2"](x))
|
| 328 |
return self.norm_f(x)
|
| 329 |
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
(
|
| 346 |
-
|
| 347 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
x = sum(emb(ids[..., i]) for i, emb in enumerate(self.byte_emb))
|
| 349 |
x = x + self.pos[:, : ids.shape[1]]
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
|
| 362 |
|
| 363 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 364 |
# Training
|
| 365 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 366 |
|
| 367 |
-
def
|
| 368 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
n_params = sum(p.numel() for p in model.parameters())
|
| 375 |
-
opt = torch.optim.Adam(model.parameters(), lr=cfg.lr)
|
| 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
|
| 385 |
snapshots.append((0, alephs[0].export_codebook()))
|
| 386 |
|
| 387 |
-
print(f"\n===
|
| 388 |
-
f"
|
| 389 |
-
f"
|
| 390 |
-
f"eff.batch={cfg.batch_size*cfg.accum_steps} "
|
| 391 |
-
|
| 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,
|
| 403 |
for _ in range(cfg.accum_steps):
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 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))
|
| 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 |
-
|
| 438 |
-
|
| 439 |
-
* cfg.accum_steps) / (time.time() - t0)
|
| 440 |
line = (f" step {step:6d} loss {loss_avg:.4f} "
|
| 441 |
-
f"bpb {bpb
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
f"
|
| 451 |
-
f"
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
print(line)
|
| 456 |
-
result.update(
|
|
|
|
|
|
|
|
|
|
| 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__,
|
| 466 |
-
|
| 467 |
-
print(f"\n[basin] {
|
| 468 |
-
f"
|
| 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__
|
| 481 |
-
|
|
|
|
|
|
|
| 482 |
return result
|
| 483 |
|
| 484 |
|
| 485 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 486 |
-
#
|
| 487 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 488 |
|
| 489 |
-
def _smoke(
|
| 490 |
-
"""End-to-end on a synthetic local corpus β no downloads."""
|
| 491 |
print("=" * 70)
|
| 492 |
-
print("
|
| 493 |
print("=" * 70)
|
| 494 |
-
path = "/tmp/_smoke_corpus.txt"
|
| 495 |
rng = np.random.default_rng(0)
|
| 496 |
-
words = [b"the", b"aleph", b"
|
| 497 |
-
b"through", b"a", b"
|
|
|
|
| 498 |
with open(path, "wb") as f:
|
| 499 |
-
f.write(b" ".join(words[i] for i in rng.integers(0,
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
|
| 512 |
|
| 513 |
#if __name__ == "__main__":
|
| 514 |
# import argparse
|
| 515 |
-
# ap = argparse.ArgumentParser(description="
|
| 516 |
# ap.add_argument("--smoke-only", action="store_true")
|
| 517 |
-
# ap.add_argument("--
|
| 518 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|
| 524 |
-
#
|
| 525 |
# if args.smoke_only:
|
| 526 |
-
# _smoke(
|
| 527 |
# else:
|
| 528 |
-
# cfg =
|
| 529 |
-
#
|
| 530 |
-
#
|
| 531 |
-
#
|
| 532 |
-
#
|
|
|
|
|
|
| 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
|
|
|
|
| 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
|
|
|
|
| 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:
|
|
|
|
| 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)
|