File size: 16,470 Bytes
7ccad85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c63ff5
 
7ccad85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c63ff5
 
 
 
 
c4f1fc0
 
 
 
 
 
 
 
 
 
 
2c63ff5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7ccad85
 
 
 
 
 
 
 
 
 
 
 
 
2c63ff5
7ccad85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c63ff5
 
7ccad85
 
 
 
 
 
 
 
 
 
 
 
 
2c63ff5
7ccad85
 
 
 
2c63ff5
7ccad85
 
 
 
2c63ff5
 
 
 
 
 
 
 
 
 
 
7ccad85
 
 
 
 
2c63ff5
 
 
 
 
 
 
 
 
 
c4f1fc0
 
 
 
2c63ff5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c4f1fc0
 
 
 
2c63ff5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7ccad85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0d63228
7ccad85
0d63228
7ccad85
 
 
 
 
 
 
 
 
 
 
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
"""AlephLM — the full craft, config-driven.

Trigram byte (or BPE) embedding -> pre-norm stack (CausalSDPA majority,
CausalSplatHUB at the configured depths) -> LayerNorm -> DualHead.

Toggle surface (the causal contribution ledger, run at every eval):
    forward(idx, disable_bank=True)   dispatched experts off (exact C6 null)
    forward(idx, disable_hub=True)    hub attention residuals skipped
    forward(idx, disable_head_aleph=True)  gamma path off
"""
from __future__ import annotations

from typing import NamedTuple, Optional

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.checkpoint  # noqa: F401 — torch<=2.4 does NOT auto-import
# this submodule; without it the hub_ckpt path AttributeErrors at step 1
# (the recorded A40 landmine, 2026-08-06)


class LMOutput(NamedTuple):
    """Still a tuple — `logits, loss = model(x)` keeps working — but also
    HF-duck-typed (`out.logits`, `out.loss`) so frozen-trunk tooling like
    amoe-lora drives the model natively."""
    logits: torch.Tensor
    loss: Optional[torch.Tensor]

from .presets import AlephLMConfig
from .attention import CausalSDPA, CausalSplatHUB
from .bank import AnchoredBank
from .embedding import TrigramByteEmbedding, TokenEmbedding
from .head import DualHead
# model/fusion.py is imported lazily (only when a config asks for fusion),
# so a vendored copy of this file without it keeps loading unfused crafts.


class Block(nn.Module):
    def __init__(self, cfg: AlephLMConfig, layer_idx: int):
        super().__init__()
        d = cfg.d_model
        self.is_hub = layer_idx in cfg.hub_layers
        self.ckpt_mode = getattr(cfg, "hub_ckpt", 0)
        self.n1 = nn.LayerNorm(d)
        self.n2 = nn.LayerNorm(d)
        if self.is_hub:
            self.attn = CausalSplatHUB(d, cfg.hub_K, cfg.hub_D, cfg.tau,
                                       chunk=cfg.hub_chunk,
                                       n_const=getattr(cfg, "hub_const", 1))
        else:
            self.attn = CausalSDPA(d, cfg.n_heads)
        self.bank = AnchoredBank(d, cfg.bank_experts, cfg.bank_ff, cfg.tau,
                                 cfg.gate_init)

    def forward(self, x, disable_bank=False, disable_hub=False):
        # hub_ckpt (2026-08-26): recompute-in-backward for the heavy
        # branches — at v2 scale the retained scan tensors alone exceed a
        # 95GB card (measured OOM). Training-path only; eval/decode and
        # the census replay (which calls attn/bank directly) are untouched.
        ckpt = self.ckpt_mode and self.training and torch.is_grad_enabled()
        if not (disable_hub and self.is_hub):
            if ckpt:
                x = x + torch.utils.checkpoint.checkpoint(
                    lambda t: self.attn(t), self.n1(x), use_reentrant=False)
            else:
                x = x + self.attn(self.n1(x))
        if ckpt and self.ckpt_mode >= 2 and not disable_bank:
            return x + torch.utils.checkpoint.checkpoint(
                lambda t: self.bank(t), self.n2(x), use_reentrant=False)
        return x + self.bank(self.n2(x), disable_dispatch=disable_bank)

    def prefill(self, x):
        a, cache = self.attn.prefill(self.n1(x))
        x = x + a
        return x + self.bank(self.n2(x)), cache

    def step(self, x_t, cache):
        x_t = x_t + self.attn.step(self.n1(x_t), cache)
        return x_t + self.bank(self.n2(x_t))


class AlephLM(nn.Module):
    def __init__(self, cfg: AlephLMConfig):
        super().__init__()
        self.cfg = cfg
        if cfg.tokenizer == "byte-trigram":
            assert cfg.vocab_size == 256, "byte crafts use vocab 256"
            self.embed = TrigramByteEmbedding(cfg.d_model, cfg.context)
            tied = None
        else:
            self.embed = TokenEmbedding(cfg.vocab_size, cfg.d_model, cfg.context)
            tied = self.embed.emb.weight if cfg.tie_embeddings else None
        self.blocks = nn.ModuleList(
            Block(cfg, i) for i in range(cfg.n_layers))
        self.nf = nn.LayerNorm(cfg.d_model)
        self.head = DualHead(cfg.d_model, cfg.vocab_size, cfg.head_K,
                             cfg.head_D, cfg.tau, tied_weight=tied)
        # v3: weak-token fusion (model/fusion.py). None keeps the byte-
        # resolution trunk verbatim; otherwise the middle blocks run over
        # units and the front/back blocks stay at byte resolution.
        spec = getattr(cfg, "fusion", None)
        if spec:
            # VENDORED: the live package reads
            #     from .fusion import Fusion
            # transformers' remote-code loader resolves every relative
            # import to a flat file beside this one, so written that way
            # it demands a fusion.py here and refuses to load the model.
            # This craft is fusion-free (its config carries no `fusion`
            # key, so this branch is never taken); fusion belongs to the
            # training side and itself imports the data subpackage, which
            # a flat package cannot carry. Deferred and resolved by name.
            from importlib import import_module
            Fusion = import_module("geolip.alephllm.model.fusion").Fusion
            self.fusion = Fusion(spec, cfg.d_model)
        else:
            self.fusion = None
        if self.fusion is not None:
            assert self.fusion.k_lo + self.fusion.k_hi <= cfg.n_layers, \
                "fusion: k_lo + k_hi must not exceed n_layers"

    def _ranges(self):
        L = len(self.blocks)
        lo, hi = self.fusion.k_lo, L - self.fusion.k_hi
        return self.blocks[:lo], self.blocks[lo:hi], self.blocks[hi:]

    def _trunk(self, x, idx, disable_bank=False, disable_hub=False):
        if self.fusion is None:
            for b in self.blocks:
                x = b(x, disable_bank=disable_bank, disable_hub=disable_hub)
            return x
        front, middle, back = self._ranges()
        plan = self.fusion.plan(idx)
        for b in front:
            x = b(x, disable_bank=disable_bank, disable_hub=disable_hub)
        if len(middle):
            u = plan.pool(x)
            for b in middle:
                u = b(u, disable_bank=disable_bank, disable_hub=disable_hub)
            x = x + plan.unpool(u, self.fusion.null)
        else:
            x = x + self.fusion.null.to(x.dtype)
        for b in back:
            x = b(x, disable_bank=disable_bank, disable_hub=disable_hub)
        return x

    def forward(self, idx=None, targets=None, disable_bank=False,
                disable_hub=False, disable_head_aleph=False,
                input_ids=None, labels=None, attention_mask=None):
        """HF-style aliases are accepted so frozen-trunk tooling drives the
        model unchanged, WITH HF semantics: `labels` are same-position and
        shifted internally (logits[:-1] vs labels[1:]); `targets` are the
        package's own pre-shifted convention and used as-is. attention_mask
        is deliberately ignored: under causal attention with right-padding
        and -100 label masking, pads can never influence a scored position."""
        if idx is None:
            idx = input_ids
        x = self.embed(idx)
        x = self._trunk(x, idx, disable_bank=disable_bank, disable_hub=disable_hub)
        h = self.nf(x)
        logits = self.head(h, disable_aleph=disable_head_aleph)
        if targets is not None:                     # pre-shifted (ours)
            loss = F.cross_entropy(
                logits.reshape(-1, logits.shape[-1]).float(),
                targets.reshape(-1), ignore_index=-100)
        elif labels is not None:                    # HF: shift internally
            loss = F.cross_entropy(
                logits[:, :-1].reshape(-1, logits.shape[-1]).float(),
                labels[:, 1:].reshape(-1), ignore_index=-100)
        else:
            return LMOutput(logits, None)
        return LMOutput(logits, loss)

    # ---------------------------------------------------- incremental decode
    @torch.no_grad()
    def prefill(self, idx):
        """Run the prompt once, return (last-position logits, decode cache).
        The cache carries per-layer attention state, the trigram history
        bytes, and the absolute position cursor."""
        self.eval()
        if self.fusion is not None:
            return self._prefill_fused(idx)
        from .embedding import PAD_ROW
        caches = []
        x = self.embed(idx)
        for b in self.blocks:
            x, c = b.prefill(x)
            caches.append(c)
        h = self.nf(x)
        logits = self.head(h[:, -1:])
        n = idx.shape[1]
        prev2 = idx[:, -2] if n >= 2 else torch.full_like(idx[:, -1], PAD_ROW)
        return logits, {"layers": caches, "t": n,
                        "prev1": idx[:, -1], "prev2": prev2}

    def _embed_step(self, next_id, cache, t):
        if isinstance(self.embed, TrigramByteEmbedding):
            e = (self.embed.emb0(next_id) + self.embed.emb1(cache["prev1"])
                 + self.embed.emb2(cache["prev2"])).unsqueeze(1) \
                + self.embed.pos[:, t:t + 1]
            cache["prev3"] = cache.get("prev2")
            cache["prev2"] = cache["prev1"]
            cache["prev1"] = next_id
        else:
            e = self.embed.emb(next_id).unsqueeze(1) + self.embed.pos[:, t:t + 1]
        return e

    @torch.no_grad()
    def decode_step(self, next_id, cache):
        """One token through the cached path. next_id: (B,) or (B,1)."""
        next_id = next_id.reshape(-1)
        t = cache["t"]
        assert t < self.cfg.context, "decode exceeded the position table"
        if self.fusion is not None:
            return self._decode_step_fused(next_id, cache, t)
        x = self._embed_step(next_id, cache, t)
        for b, c in zip(self.blocks, cache["layers"]):
            x = b.step(x, c)
        cache["t"] = t + 1
        return self.head(self.nf(x))

    # ------------------------------------------------- fused decode path
    @torch.no_grad()
    def _prefill_fused(self, idx):
        """The prompt through the hourglass: front blocks at byte
        resolution (batched), the middle over each row's COMPLETED units
        (the last unit stays open until a later byte starts the next), the
        back blocks at byte resolution. The middle's caches are merged
        across rows (the hub's prefix state has one shape per row)."""
        from .embedding import PAD_ROW
        from .governor import raw_block
        # VENDORED: from .fusion import cat_caches, zero_cache_rows (see __init__)
        from importlib import import_module
        _f = import_module("geolip.alephllm.model.fusion")
        cat_caches, zero_cache_rows = _f.cat_caches, _f.zero_cache_rows
        front, middle, back = self._ranges()
        for b in middle:
            assert raw_block(b).is_hub, "fused decode needs hub blocks in the middle"
        B, T = idx.shape
        plan = self.fusion.plan(idx)
        x = self.embed(idx)
        front_c = []
        for b in front:
            x, c = b.prefill(x)
            front_c.append(c)
        x_front = x
        u_all = plan.pool(x_front)                          # (B, J, d) incl. the open unit
        n_closed = plan.n_units - 1
        d = x.shape[-1]
        v_pad = torch.zeros(B, plan.J, d, device=x.device, dtype=x.dtype)
        mid_rows = []
        for r in range(B):
            nc = int(n_closed[r])
            u = u_all[r:r + 1, :max(nc, 1)]
            row_c = []
            for b in middle:
                u, c = b.prefill(u)
                row_c.append(c)
            if nc > 0:
                v_pad[r, :nc] = u[0]
            mid_rows.append(row_c)
        mid_c = [cat_caches([row[i] for row in mid_rows]) for i in range(len(middle))]
        empty = n_closed == 0
        if bool(empty.any()):
            mid_c = [zero_cache_rows(c, empty) for c in mid_c]
        g = plan.unpool(v_pad, self.fusion.null)              # (B, T, d)
        x = x_front + g
        back_c = []
        for b in back:
            x, c = b.prefill(x)
            back_c.append(c)
        logits = self.head(self.nf(x)[:, -1:])
        pad = lambda k: idx[:, -k] if T >= k else torch.full_like(idx[:, -1], PAD_ROW)  # noqa: E731
        return logits, {"front": front_c, "mid": mid_c, "back": back_c, "t": T,
                        "prev1": idx[:, -1], "prev2": pad(2), "prev3": pad(3),
                        "front_last": x_front[:, -1], "g": g[:, -1]}

    @torch.no_grad()
    def _decode_step_fused(self, next_id, cache, t):
        from .embedding import PAD_ROW
        # VENDORED: from .fusion import snapshot, restore_rows (see __init__)
        from importlib import import_module
        _f = import_module("geolip.alephllm.model.fusion")
        snapshot, restore_rows = _f.snapshot, _f.restore_rows
        front, middle, back = self._ranges()
        prev1, prev2, prev3 = cache["prev1"], cache["prev2"], cache["prev3"]
        st = self.fusion.starts_step(next_id, prev1, prev2, prev3, PAD_ROW)   # (B,) closes the open unit
        if bool(st.any()) and len(middle):
            u = cache["front_last"].unsqueeze(1)
            for b, c in zip(middle, cache["mid"]):
                old = snapshot(c)
                u = b.step(u, c)
                restore_rows(c, old, st)
            cache["g"] = torch.where(st.unsqueeze(-1), u[:, 0], cache["g"])
        x = self._embed_step(next_id, cache, t)
        for b, c in zip(front, cache["front"]):
            x = b.step(x, c)
        cache["front_last"] = x[:, 0]
        x = x + cache["g"].unsqueeze(1)
        for b, c in zip(back, cache["back"]):
            x = b.step(x, c)
        cache["t"] = t + 1
        return self.head(self.nf(x))

    @staticmethod
    def _sample(logits, temperature, top_p):
        logits = logits[:, -1].float()
        if temperature <= 0.02:
            return logits.argmax(-1, keepdim=True)
        probs = F.softmax(logits / temperature, dim=-1)
        sp, si = probs.sort(dim=-1, descending=True)
        keep = (sp.cumsum(-1) - sp) < top_p
        keep[..., :1] = True   # top-1 always survives: top_p<=0 must never
        sp = sp * keep         # yield an all-zero row (CUDA multinomial on
        return si.gather(-1, torch.multinomial(  # zeros poisons the context)
            sp / sp.sum(-1, keepdim=True), 1))

    @torch.no_grad()
    def generate(self, idx, max_new: int = 128, temperature: float = 1.0,
                 top_p: float = 0.95, use_cache: bool = True):
        """Cached decode while the sequence fits the position table; any
        remainder (long prompts, fills past the context) continues through
        the sliding-window parallel path — the hub's constant-size state
        cannot evict, so sliding continuation must recompute."""
        self.eval()
        ctx = self.cfg.context
        if use_cache and idx.shape[1] < ctx and max_new > 0:
            n_cached = min(max_new, ctx - idx.shape[1])
            logits, cache = self.prefill(idx)
            for i in range(n_cached):
                nxt = self._sample(logits, temperature, top_p)
                idx = torch.cat([idx, nxt], dim=1)
                if i + 1 < n_cached:
                    logits = self.decode_step(nxt, cache)
            max_new -= n_cached
        for _ in range(max_new):
            logits, _ = self(idx[:, -ctx:])
            nxt = self._sample(logits, temperature, top_p)
            idx = torch.cat([idx, nxt], dim=1)
        return idx

    def compile_hubs(self, **compile_kw):
        """Opt-in: torch.compile each hub's attention (measured 4.0x over
        eager at ctx 2048 with the fused forward, parity 1.4e-06 vs the
        naive oracle; MHA-parity wall-clock by ctx 8192). Call AFTER
        loading weights, and do NOT save a checkpoint while compiled (the
        OptimizedModule wrapper prefixes state_dict keys). TRAINING use is
        gated on a grad-parity + throughput check on the training
        hardware — the speed verdict was no-grad forward only."""
        import torch as _torch
        from .governor import raw_block
        for wrap in self.blocks:
            blk = raw_block(wrap)
            if blk.is_hub:
                blk.attn = _torch.compile(blk.attn, **compile_kw)
        return self

    def param_count(self) -> int:
        seen, total = set(), 0
        for p in self.parameters():
            if id(p) not in seen:
                seen.add(id(p))
                total += p.numel()
        return total