File size: 9,498 Bytes
3afc977
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Shared tiny Transformer backbone.

One class, two modes. `causal=False` β†’ bidirectional attention, the diffusion
denoiser (all positions see all positions, no KV cache; DESIGN.md Β§1).
`causal=True` β†’ causal mask, the AR baseline, WITH a KV cache for generation β€”
AR's natural advantage, which a fair iso-latency comparison must grant it.
Diffusion inherently has no cache: each denoising step reprocesses the whole
sequence (EVALUATION.md is explicit that this makes a diffusion step heavier).

Same depth/width/heads in both modes, so the comparison isolates the attention
pattern + objective, not capacity. No recurrent depth, no MoE β€” Stage 0 is the
bare substrate (EXPERIMENTS.md).
"""

from __future__ import annotations

from contextlib import nullcontext

import torch
import torch.nn as nn
import torch.nn.functional as F

from .config import ModelConfig


def amp_ctx(device: str):
    """fp16 autocast on GPU backends β€” ~5x faster attention on MPS, big memory
    savings. CPU stays fp32."""
    if device in ("mps", "cuda"):
        return torch.autocast(device_type=device, dtype=torch.float16)
    return nullcontext()


class MHA(nn.Module):
    def __init__(self, cfg: ModelConfig):
        super().__init__()
        self.h = cfg.n_heads
        self.hd = cfg.d_model // cfg.n_heads
        self.qkv = nn.Linear(cfg.d_model, 3 * cfg.d_model)
        self.proj = nn.Linear(cfg.d_model, cfg.d_model)
        self.dropout = cfg.dropout

    def forward(self, x, attn_bias=None, cache=None, return_kv=False):
        """x: (B,T,C). attn_bias: precomputed additive mask (B,1,T,Tk) or None
        (built once per forward by the Transformer). cache: (k,v) past or None.
        Returns (out, kv_or_None)."""
        B, T, C = x.shape
        qkv = self.qkv(x).view(B, T, 3, self.h, self.hd).permute(2, 0, 3, 1, 4)
        q, k, v = qkv[0], qkv[1], qkv[2]  # each (B,h,T,hd)

        incremental = cache is not None
        if incremental:
            pk, pv = cache
            if pk is not None:
                k = torch.cat([pk, k], dim=2)
                v = torch.cat([pv, v], dim=2)
        new_kv = (k, v) if (return_kv or incremental) else None

        # Incremental single-step decode attends all cached keys β†’ no mask needed
        # (causal-correct because tokens are fed in order).
        mask = None if incremental else attn_bias
        if mask is not None and mask.dtype != q.dtype:
            mask = mask.to(q.dtype)

        out = F.scaled_dot_product_attention(
            q, k, v,
            attn_mask=mask,
            dropout_p=self.dropout if self.training else 0.0,
        )
        out = out.transpose(1, 2).reshape(B, T, C)
        return self.proj(out), new_kv


class Block(nn.Module):
    def __init__(self, cfg: ModelConfig):
        super().__init__()
        self.ln1 = nn.LayerNorm(cfg.d_model)
        self.attn = MHA(cfg)
        self.ln2 = nn.LayerNorm(cfg.d_model)
        self.mlp = nn.Sequential(
            nn.Linear(cfg.d_model, cfg.d_ff),
            nn.GELU(),
            nn.Linear(cfg.d_ff, cfg.d_model),
            nn.Dropout(cfg.dropout),
        )

    def forward(self, x, attn_bias=None, cache=None, return_kv=False):
        a, kv = self.attn(self.ln1(x), attn_bias, cache, return_kv)
        x = x + a
        x = x + self.mlp(self.ln2(x))
        return x, kv


class Transformer(nn.Module):
    def __init__(self, cfg: ModelConfig, causal: bool):
        super().__init__()
        self.cfg = cfg
        self.causal = causal
        self.tok_emb = nn.Embedding(cfg.vocab_size, cfg.d_model)
        self.pos_emb = nn.Embedding(cfg.max_len, cfg.d_model)
        self.drop = nn.Dropout(cfg.dropout)
        self.blocks = nn.ModuleList([Block(cfg) for _ in range(cfg.n_layers)])
        self.ln_f = nn.LayerNorm(cfg.d_model)
        self.head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False)
        self.head.weight = self.tok_emb.weight  # weight tying
        self.apply(self._init)

    def _init(self, m):
        if isinstance(m, nn.Linear):
            nn.init.normal_(m.weight, std=0.02)
            if m.bias is not None:
                nn.init.zeros_(m.bias)
        elif isinstance(m, nn.Embedding):
            nn.init.normal_(m.weight, std=0.02)

    def _build_bias(self, attn_keep, causal):
        """Additive attention mask (B,1,T,T), built ONCE per forward. -inf where a
        query may not attend (padding, and future positions when causal)."""
        B, T = attn_keep.shape
        device = attn_keep.device
        bias = torch.zeros(B, 1, T, T, device=device)
        bias = bias.masked_fill(~attn_keep[:, None, None, :], float("-inf"))
        if causal:
            cmask = torch.triu(torch.ones(T, T, device=device, dtype=torch.bool), 1)
            bias = bias.masked_fill(cmask, float("-inf"))
        return bias

    def forward(self, ids, attn_keep):
        """Full pass. ids:(B,T); attn_keep:(B,T) True=real token. Returns (B,T,V)."""
        B, T = ids.shape
        pos = torch.arange(T, device=ids.device).unsqueeze(0)
        x = self.drop(self.tok_emb(ids) + self.pos_emb(pos))
        bias = self._build_bias(attn_keep, self.causal)
        for blk in self.blocks:
            x, _ = blk(x, attn_bias=bias, cache=None)
        return self.head(self.ln_f(x))

    # ---- block diffusion (Nemotron-style, KV-cacheable) ----

    def _block_bias(self, attn_keep, region, block_id):
        """Block-causal additive mask (B,1,T,T) for training/teacher-forcing.
        Clean context attends only to context (so its representation is
        independent of the generated region -> cacheable). A region query in
        block b attends to all context plus region blocks <= b, bidirectional
        within its block."""
        keepj = attn_keep[:, None, None, :]            # (B,1,1,T)
        reg_i = region[:, None, :, None]               # query is region
        reg_j = region[:, None, None, :]               # key is region
        bid_i = block_id[:, None, :, None]
        bid_j = block_id[:, None, None, :]
        allow_region_q = (~reg_j) | (reg_j & (bid_j <= bid_i))
        allow = torch.where(reg_i, allow_region_q, ~reg_j) & keepj
        return torch.zeros_like(allow, dtype=torch.float32).masked_fill(~allow, float("-inf"))

    def forward_blocks(self, ids, attn_keep, region, block_id):
        """Full teacher-forcing pass under the block-causal mask. Returns (B,T,V)."""
        B, T = ids.shape
        pos = torch.arange(T, device=ids.device).unsqueeze(0)
        x = self.drop(self.tok_emb(ids) + self.pos_emb(pos))
        bias = self._block_bias(attn_keep, region, block_id)
        for blk in self.blocks:
            x, _ = blk(x, attn_bias=bias, cache=None)
        return self.head(self.ln_f(x))

    @torch.no_grad()
    def encode_context(self, ctx_ids, ctx_pos):
        """Per-layer K/V for the clean context (prefix+suffix), attending only
        among itself. ctx_ids,ctx_pos: (1,Lc). Returns list of (k,v)."""
        x = self.tok_emb(ctx_ids) + self.pos_emb(ctx_pos)
        caches = []
        for blk in self.blocks:
            x, kv = blk(x, attn_bias=None, cache=None, return_kv=True)
            caches.append(kv)
        return caches

    @torch.no_grad()
    def decode_block(self, blk_ids, blk_pos, caches):
        """Forward one block's positions against cached context+committed K/V.
        blk_ids,blk_pos: (1,Lb). Returns (logits (1,Lb,V), new_caches) where
        new_caches has the block's K/V appended (use after committing)."""
        x = self.tok_emb(blk_ids) + self.pos_emb(blk_pos)
        new_caches = []
        for blk, c in zip(self.blocks, caches):
            x, kv = blk(x, attn_bias=None, cache=c, return_kv=True)
            new_caches.append(kv)
        return self.head(self.ln_f(x)), new_caches

    @torch.no_grad()
    def generate(self, head_ids, max_new, eos_id):
        """KV-cached greedy decode (causal only). head_ids: 1-D prompt tensor.
        Returns the list of generated token ids (excluding eos)."""
        assert self.causal
        device = head_ids.device
        L = head_ids.size(0)
        pos = torch.arange(L, device=device).unsqueeze(0)
        x = self.tok_emb(head_ids.unsqueeze(0)) + self.pos_emb(pos)

        # Prompt pass: causal among the prompt, seed the caches.
        cbias = torch.triu(
            torch.full((1, 1, L, L), float("-inf"), device=device), diagonal=1
        )
        caches = []
        for blk in self.blocks:
            x, kv = blk(x, attn_bias=cbias, cache=None, return_kv=True)
            caches.append(kv)
        logits = self.head(self.ln_f(x))
        nxt = int(logits[0, -1].argmax().item())

        out = []
        cur_len = L
        for _ in range(max_new):
            if nxt == eos_id:
                break
            out.append(nxt)
            cur_len += 1
            if cur_len >= self.cfg.max_len:
                break
            tok = torch.tensor([[nxt]], device=device)
            p = torch.tensor([[cur_len - 1]], device=device)
            x = self.tok_emb(tok) + self.pos_emb(p)
            new_caches = []
            for blk, c in zip(self.blocks, caches):
                x, kv = blk(x, attn_bias=None, cache=c, return_kv=True)
                new_caches.append(kv)
            caches = new_caches
            logits = self.head(self.ln_f(x))
            nxt = int(logits[0, -1].argmax().item())
        return out

    def num_params(self) -> int:
        return sum(p.numel() for p in self.parameters())