CoolFace
Modelpublic

epoyraz/tinystories-25m

sourceHugging Facemitupdated 4mo agoView on Hugging Face
3likes71downloads
model.py1103 linesDownload Raw Back to root
1import torch2import torch.nn as nn3import torch.nn.functional as F4import math5from torch.utils.checkpoint import checkpoint6 7 8def soft_cap(logits, cap):9    """Gemma2/modded-nanoGPT logit soft-capping: cap * tanh(logits / cap). No-op if cap falsy."""10    if cap:11        return cap * torch.tanh(logits / cap)12    return logits13 14 15def chunked_cross_entropy(hidden, weight, targets, cap=0, chunk_size=2048):16    """Memory-efficient cross-entropy. Projects hidden -> logits and reduces the loss17    in token-chunks so the full [N, vocab] logits are never materialized at once (each18    chunk's logits are recomputed in backward via checkpointing). Numerically equal to19    F.cross_entropy(soft_cap(hidden @ weight.T), targets, ignore_index=-1)."""20    hidden = hidden.reshape(-1, hidden.size(-1))21    targets = targets.reshape(-1)22    n_valid = (targets != -1).sum().clamp(min=1)23 24    def chunk_loss(h, t, w):25        logits = soft_cap(F.linear(h, w), cap)26        return F.cross_entropy(logits, t, ignore_index=-1, reduction="sum")27 28    use_ckpt = torch.is_grad_enabled() and (hidden.requires_grad or weight.requires_grad)29    total = hidden.new_zeros(())30    for i in range(0, hidden.size(0), chunk_size):31        h, t = hidden[i:i + chunk_size], targets[i:i + chunk_size]32        if use_ckpt:33            total = total + checkpoint(chunk_loss, h, t, weight, use_reentrant=False)34        else:35            total = total + chunk_loss(h, t, weight)36    return total / n_valid37 38 39# --- mHC: Manifold-Constrained Hyper-Connections ---40 41def sinkhorn(log_alpha, n_iters=5):42    for _ in range(n_iters):43        log_alpha = log_alpha - torch.logsumexp(log_alpha, dim=-1, keepdim=True)44        log_alpha = log_alpha - torch.logsumexp(log_alpha, dim=-2, keepdim=True)45    return log_alpha.exp()46 47 48class MHCResidual(nn.Module):49    def __init__(self, n_streams):50        super().__init__()51        self.n_streams = n_streams52        self.log_alpha = nn.Parameter(torch.zeros(n_streams, n_streams))53 54    def forward(self, streams, update):55        W = sinkhorn(self.log_alpha)56        mixed = torch.einsum("ij,bjte->bite", W, streams)57        mixed[:, 0] = mixed[:, 0] + update58        return mixed59 60 61class MHCExpand(nn.Module):62    def __init__(self, n_streams, n_embd):63        super().__init__()64        self.n_streams = n_streams65        self.proj = nn.Linear(n_embd, n_streams * n_embd) if n_streams > 1 else None66 67    def forward(self, x):68        if self.n_streams == 1:69            return x.unsqueeze(1)70        B, T, C = x.shape71        return self.proj(x).view(B, self.n_streams, T, C)72 73 74class MHCCollapse(nn.Module):75    def __init__(self, n_streams, n_embd):76        super().__init__()77        self.n_streams = n_streams78        self.proj = nn.Linear(n_streams * n_embd, n_embd) if n_streams > 1 else None79 80    def forward(self, streams):81        if self.n_streams == 1:82            return streams.squeeze(1)83        B, S, T, C = streams.shape84        return self.proj(streams.permute(0, 2, 1, 3).reshape(B, T, S * C))85 86 87# --- BitNet: Ternary weight linear layer ---88 89class BitLinear(nn.Module):90    def __init__(self, in_features, out_features, bias=True):91        super().__init__()92        self.in_features = in_features93        self.out_features = out_features94        self.weight = nn.Parameter(torch.empty(out_features, in_features))95        self.bias = nn.Parameter(torch.zeros(out_features)) if bias else None96        self.rms_norm = nn.RMSNorm(in_features)97        nn.init.normal_(self.weight, std=0.02)98 99    def ternary_quantize(self, w):100        alpha = w.abs().mean()101        threshold = alpha * 0.5102        w_ternary = torch.zeros_like(w)103        w_ternary[w > threshold] = alpha104        w_ternary[w < -threshold] = -alpha105        return w_ternary.detach() + (w - w.detach())106 107    def activation_quantize(self, x):108        scale = 127.0 / x.abs().max(dim=-1, keepdim=True).values.clamp(min=1e-5)109        x_scaled = x * scale110        x_q = x_scaled.round().clamp(-128, 127).detach() + (x_scaled - x_scaled.detach())111        return x_q / scale112 113    def forward(self, x):114        x = self.rms_norm(x)115        w_q = self.ternary_quantize(self.weight)116        x_q = self.activation_quantize(x)117        out = F.linear(x_q, w_q, self.bias)118        return out119 120 121class FastBitLinear(nn.Module):122    def __init__(self, in_features, out_features, bias=True):123        super().__init__()124        self.in_features = in_features125        self.out_features = out_features126        self.weight = nn.Parameter(torch.empty(out_features, in_features))127        self.bias = nn.Parameter(torch.zeros(out_features)) if bias else None128        self.rms_norm = nn.RMSNorm(in_features)129        nn.init.normal_(self.weight, std=0.02)130 131    def _int8_forward(self, x):132        w = self.weight.detach()133        alpha = w.abs().mean()134        threshold = alpha * 0.5135        # Pack the ternary weight into a single signed int8 tensor {-1,0,+1} so the136        # whole layer is ONE int8 matmul (dp4a / int8 tensor cores), not two. This is137        # exactly equivalent to (x @ w_pos.T) - (x @ w_neg.T) but ~2x cheaper, and it138        # beats fp16 at prefill/training scale.139        w_ternary = torch.zeros_like(w, dtype=torch.int8)140        w_ternary[w > threshold] = 1141        w_ternary[w < -threshold] = -1142 143        x_max = x.detach().abs().max(dim=-1, keepdim=True).values.clamp(min=1e-5)144        x_scale = 127.0 / x_max145        x_q = (x.detach() * x_scale).round().clamp(-128, 127).to(torch.int8)146 147        shape = x_q.shape148        x_2d = x_q.reshape(-1, shape[-1])149 150        rows = x_2d.shape[0]151        if rows <= 16:  # torch._int_mm requires more than 16 rows152            x_2d = torch.nn.functional.pad(x_2d, (0, 0, 0, 17 - rows))153            y = torch._int_mm(x_2d, w_ternary.T)[:rows]154        else:155            y = torch._int_mm(x_2d, w_ternary.T)156 157        y = y.float().reshape(*shape[:-1], self.out_features)158        return y * (alpha / x_scale)159 160    def _ste_forward(self, x):161        alpha = self.weight.abs().mean()162        threshold = alpha * 0.5163        w_ternary = torch.zeros_like(self.weight)164        w_ternary[self.weight > threshold] = alpha165        w_ternary[self.weight < -threshold] = -alpha166        w_q = self.weight + (w_ternary - self.weight).detach()167 168        x_scale = 127.0 / x.abs().max(dim=-1, keepdim=True).values.clamp(min=1e-5)169        x_scaled = x * x_scale170        x_q = x_scaled + (x_scaled.round().clamp(-128, 127) - x_scaled).detach()171        x_q = x_q / x_scale172 173        return F.linear(x_q, w_q, None)174 175    def forward(self, x):176        x = self.rms_norm(x)177        if self.training:178            out = self._ste_forward(x)179        else:180            out = self._int8_forward(x)181        if self.bias is not None:182            out = out + self.bias183        return out184 185 186def make_linear(in_f, out_f, bias=True, use_bitnet=False, use_fast_bitnet=False):187    if use_fast_bitnet:188        return FastBitLinear(in_f, out_f, bias=bias)189    if use_bitnet:190        return BitLinear(in_f, out_f, bias=bias)191    return nn.Linear(in_f, out_f, bias=bias)192 193 194# --- TurboQuant: KV-cache compression for inference ---195 196class PolarQuantizer:197    def __init__(self, bits=4):198        self.bits = bits199        self.levels = 2 ** bits200 201    def quantize(self, tensor):202        norms = tensor.norm(dim=-1, keepdim=True).clamp(min=1e-8)203        unit = tensor / norms204        norm_min = norms.min()205        norm_max = norms.max()206        norm_scale = (norm_max - norm_min) / (self.levels - 1)207        q_norms = ((norms - norm_min) / norm_scale.clamp(min=1e-8)).round().clamp(0, self.levels - 1)208        val_min = unit.min()209        val_max = unit.max()210        val_scale = (val_max - val_min) / (self.levels - 1)211        q_unit = ((unit - val_min) / val_scale.clamp(min=1e-8)).round().clamp(0, self.levels - 1)212        return q_norms, q_unit, (norm_min, norm_scale, val_min, val_scale)213 214    def dequantize(self, q_norms, q_unit, params):215        norm_min, norm_scale, val_min, val_scale = params216        norms = q_norms * norm_scale + norm_min217        unit = q_unit * val_scale + val_min218        unit = unit / unit.norm(dim=-1, keepdim=True).clamp(min=1e-8)219        return unit * norms220 221 222class TurboQuantKVCache:223    def __init__(self, bits=4):224        self.quantizer = PolarQuantizer(bits=bits)225        self.k_cache = []226        self.v_cache = []227 228    def update(self, k_new, v_new):229        qk_norms, qk_unit, k_params = self.quantizer.quantize(k_new)230        qv_norms, qv_unit, v_params = self.quantizer.quantize(v_new)231        self.k_cache.append((qk_norms, qk_unit, k_params))232        self.v_cache.append((qv_norms, qv_unit, v_params))233 234    def get(self):235        ks = [self.quantizer.dequantize(*entry) for entry in self.k_cache]236        vs = [self.quantizer.dequantize(*entry) for entry in self.v_cache]237        return torch.cat(ks, dim=2), torch.cat(vs, dim=2)238 239    def clear(self):240        self.k_cache.clear()241        self.v_cache.clear()242 243 244class KVCache:245    def __init__(self, max_seq_len):246        self.max_seq_len = max_seq_len247        self.k_cache = None248        self.v_cache = None249        self.pos = 0250 251    def _ensure_allocated(self, k_new, v_new):252        B, H, _, D = k_new.shape253        needs_alloc = (254            self.k_cache is None255            or self.k_cache.shape[0] != B256            or self.k_cache.shape[1] != H257            or self.k_cache.shape[3] != D258            or self.k_cache.device != k_new.device259            or self.k_cache.dtype != k_new.dtype260        )261        if needs_alloc:262            self.k_cache = torch.empty(263                B, H, self.max_seq_len, D,264                device=k_new.device,265                dtype=k_new.dtype,266            )267            self.v_cache = torch.empty(268                B, H, self.max_seq_len, D,269                device=v_new.device,270                dtype=v_new.dtype,271            )272            self.pos = 0273 274    def update(self, k_new, v_new):275        self._ensure_allocated(k_new, v_new)276        T = k_new.size(2)277        if self.pos + T > self.max_seq_len:278            raise ValueError(f"KV cache length {self.pos + T} exceeds max_seq_len {self.max_seq_len}")279        self.k_cache[:, :, self.pos:self.pos + T, :].copy_(k_new)280        self.v_cache[:, :, self.pos:self.pos + T, :].copy_(v_new)281        self.pos += T282 283    def get(self):284        if self.k_cache is None:285            return None, None286        return self.k_cache[:, :, :self.pos, :], self.v_cache[:, :, :self.pos, :]287 288    def clear(self):289        self.pos = 0290 291 292# --- MTP: Multi-Token Prediction ---293 294class MTPHead(nn.Module):295    def __init__(self, config, future_idx):296        super().__init__()297        self.future_idx = future_idx298        n_embd = config["n_embd"]299        vocab_size = config["vocab_size"]300        self.logit_cap = config.get("logit_cap", 0)301        self.use_chunked_loss = config.get("use_chunked_loss", False)302        self.loss_chunk_size = config.get("loss_chunk_size", 2048)303        self.proj = nn.Linear(n_embd, n_embd)304        self.ln = nn.LayerNorm(n_embd)305        self.lm_head = nn.Linear(n_embd, vocab_size, bias=False)306 307    def forward(self, hidden, targets=None):308        if targets is not None and self.use_chunked_loss:309            shift = self.future_idx310            if targets.size(1) <= shift:311                return None, None312            # Project only the positions with a future target, then reduce in chunks.313            h = self.ln(self.proj(hidden[:, :-shift]))314            loss = chunked_cross_entropy(315                h, self.lm_head.weight, targets[:, shift:], self.logit_cap, self.loss_chunk_size316            )317            return None, loss318 319        h = self.ln(self.proj(hidden))320        logits = soft_cap(self.lm_head(h), self.logit_cap)321        loss = None322        if targets is not None:323            shift = self.future_idx324            if targets.size(1) > shift:325                logits_shifted = logits[:, :-shift].contiguous()326                targets_shifted = targets[:, shift:].contiguous()327                loss = F.cross_entropy(328                    logits_shifted.view(-1, logits_shifted.size(-1)),329                    targets_shifted.view(-1),330                    ignore_index=-1,331                )332        return logits, loss333 334 335# --- RoPE: Rotary Position Embeddings ---336 337class RotaryEmbedding(nn.Module):338    def __init__(self, dim, max_seq_len=4096, base=10000.0):339        super().__init__()340        inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))341        self.register_buffer("inv_freq", inv_freq)342        self._build_cache(max_seq_len)343 344    def _build_cache(self, seq_len):345        t = torch.arange(seq_len, dtype=self.inv_freq.dtype)346        freqs = torch.outer(t, self.inv_freq)347        emb = torch.cat([freqs, freqs], dim=-1)348        self.register_buffer("cos_cached", emb.cos(), persistent=False)349        self.register_buffer("sin_cached", emb.sin(), persistent=False)350 351    def forward(self, seq_len):352        return self.cos_cached[:seq_len], self.sin_cached[:seq_len]353 354 355def rotate_half(x):356    x1, x2 = x.chunk(2, dim=-1)357    return torch.cat([-x2, x1], dim=-1)358 359 360def apply_rope(q, k, cos, sin):361    cos = cos.unsqueeze(0).unsqueeze(0)362    sin = sin.unsqueeze(0).unsqueeze(0)363    q = q * cos + rotate_half(q) * sin364    k = k * cos + rotate_half(k) * sin365    return q, k366 367 368# --- SwiGLU MLP ---369 370class SwiGLU(nn.Module):371    def __init__(self, config):372        super().__init__()373        n_embd = config["n_embd"]374        hidden = int(4 * n_embd * 2 / 3)375        hidden = ((hidden + 63) // 64) * 64376        use_bitnet = config.get("use_bitnet", False)377        use_fast_bitnet = config.get("use_fast_bitnet", False)378        self.gate = make_linear(n_embd, hidden, bias=False, use_bitnet=use_bitnet, use_fast_bitnet=use_fast_bitnet)379        self.up = make_linear(n_embd, hidden, bias=False, use_bitnet=use_bitnet, use_fast_bitnet=use_fast_bitnet)380        self.down = make_linear(hidden, n_embd, bias=False, use_bitnet=use_bitnet, use_fast_bitnet=use_fast_bitnet)381 382    def forward(self, x):383        return self.down(F.silu(self.gate(x)) * self.up(x))384 385 386class ReLU2MLP(nn.Module):387    """Ungated MLP with squared-ReLU activation (modded-nanoGPT). Simpler and a bit388    faster than SwiGLU; competitive quality at small scale."""389 390    def __init__(self, config):391        super().__init__()392        n_embd = config["n_embd"]393        hidden = 4 * n_embd394        use_bitnet = config.get("use_bitnet", False)395        use_fast_bitnet = config.get("use_fast_bitnet", False)396        self.fc = make_linear(n_embd, hidden, bias=False, use_bitnet=use_bitnet, use_fast_bitnet=use_fast_bitnet)397        self.proj = make_linear(hidden, n_embd, bias=False, use_bitnet=use_bitnet, use_fast_bitnet=use_fast_bitnet)398 399    def forward(self, x):400        return self.proj(F.relu(self.fc(x)).square())401 402 403# --- Core model ---404 405def make_norm(n_embd, use_rmsnorm=False):406    if use_rmsnorm:407        return nn.RMSNorm(n_embd)408    return nn.LayerNorm(n_embd)409 410 411class CausalSelfAttention(nn.Module):412    def __init__(self, config):413        super().__init__()414        self.n_head = config["n_head"]415        self.n_embd = config["n_embd"]416        self.n_kv_head = config.get("n_kv_head", self.n_head)417        if self.n_embd % self.n_head != 0:418            raise ValueError(f"n_embd ({self.n_embd}) must be divisible by n_head ({self.n_head})")419        if self.n_head % self.n_kv_head != 0:420            raise ValueError(f"n_head ({self.n_head}) must be divisible by n_kv_head ({self.n_kv_head})")421        self.head_dim = self.n_embd // self.n_head422        self.use_rope = config.get("use_rope", False)423        self.use_qk_norm = config.get("use_qk_norm", False)424        use_bitnet = config.get("use_bitnet", False)425        use_fast_bitnet = config.get("use_fast_bitnet", False)426 427        self.q_proj = make_linear(self.n_embd, self.n_head * self.head_dim, use_bitnet=use_bitnet, use_fast_bitnet=use_fast_bitnet)428        self.k_proj = make_linear(self.n_embd, self.n_kv_head * self.head_dim, use_bitnet=use_bitnet, use_fast_bitnet=use_fast_bitnet)429        self.v_proj = make_linear(self.n_embd, self.n_kv_head * self.head_dim, use_bitnet=use_bitnet, use_fast_bitnet=use_fast_bitnet)430        self.proj = make_linear(self.n_embd, self.n_embd, use_bitnet=use_bitnet, use_fast_bitnet=use_fast_bitnet)431 432        # QK-Norm (modded-nanoGPT): RMSNorm Q and K over the head dim before attention.433        if self.use_qk_norm:434            self.q_norm = nn.RMSNorm(self.head_dim)435            self.k_norm = nn.RMSNorm(self.head_dim)436 437        if self.use_rope:438            self.rope = RotaryEmbedding(self.head_dim, max_seq_len=config.get("block_size", 512))439 440    def forward(self, x, kv_cache=None, pos_offset=0):441        B, T, C = x.shape442        q = self.q_proj(x).view(B, T, self.n_head, self.head_dim).transpose(1, 2)443        k = self.k_proj(x).view(B, T, self.n_kv_head, self.head_dim).transpose(1, 2)444        v = self.v_proj(x).view(B, T, self.n_kv_head, self.head_dim).transpose(1, 2)445 446        if self.use_qk_norm:447            q = self.q_norm(q)448            k = self.k_norm(k)449 450        if self.use_rope:451            cos, sin = self.rope(pos_offset + T)452            cos, sin = cos[pos_offset:pos_offset + T], sin[pos_offset:pos_offset + T]453            q, k = apply_rope(q, k, cos, sin)454 455        if self.n_kv_head < self.n_head:456            repeats = self.n_head // self.n_kv_head457            k = k.repeat_interleave(repeats, dim=1)458            v = v.repeat_interleave(repeats, dim=1)459 460        if kv_cache is not None:461            kv_cache.update(k, v)462            k, v = kv_cache.get()463 464        use_causal = (T > 1)465        out = F.scaled_dot_product_attention(q, k, v, is_causal=use_causal)466        out = out.transpose(1, 2).reshape(B, T, C)467        return self.proj(out)468 469 470class MLP(nn.Module):471    def __init__(self, config):472        super().__init__()473        use_bitnet = config.get("use_bitnet", False)474        use_fast_bitnet = config.get("use_fast_bitnet", False)475        self.fc = make_linear(config["n_embd"], 4 * config["n_embd"], use_bitnet=use_bitnet, use_fast_bitnet=use_fast_bitnet)476        self.proj = make_linear(4 * config["n_embd"], config["n_embd"], use_bitnet=use_bitnet, use_fast_bitnet=use_fast_bitnet)477 478    def forward(self, x):479        return self.proj(F.gelu(self.fc(x)))480 481 482class Block(nn.Module):483    def __init__(self, config, layer_idx=0):484        super().__init__()485        self.use_mhc = config.get("use_mhc", False)486        use_rmsnorm = config.get("use_rmsnorm", False)487        self.ln1 = make_norm(config["n_embd"], use_rmsnorm)488        self.attn = CausalSelfAttention(config)489        self.ln2 = make_norm(config["n_embd"], use_rmsnorm)490        if config.get("use_relu2", False):491            self.mlp = ReLU2MLP(config)492        elif config.get("use_swiglu", False):493            self.mlp = SwiGLU(config)494        else:495            self.mlp = MLP(config)496        if self.use_mhc:497            n_streams = config.get("mhc_streams", 4)498            self.mhc_attn = MHCResidual(n_streams)499            self.mhc_mlp = MHCResidual(n_streams)500 501    def forward(self, x, streams=None, kv_cache=None, pos_offset=0):502        if self.use_mhc and streams is not None:503            inp = streams[:, 0]504            attn_out = self.attn(self.ln1(inp), kv_cache=kv_cache, pos_offset=pos_offset)505            streams = self.mhc_attn(streams, attn_out)506            mlp_inp = streams[:, 0]507            mlp_out = self.mlp(self.ln2(mlp_inp))508            streams = self.mhc_mlp(streams, mlp_out)509            return streams510        else:511            x = x + self.attn(self.ln1(x), kv_cache=kv_cache, pos_offset=pos_offset)512            x = x + self.mlp(self.ln2(x))513            return x514 515 516class GPT(nn.Module):517    def __init__(self, config):518        super().__init__()519        self.config = config520        self.use_mhc = config.get("use_mhc", False)521        self.use_mtp = config.get("use_mtp", False)522        self.use_rope = config.get("use_rope", False)523        self.mtp_heads_n = config.get("mtp_heads", 4)524        self.mtp_weight = config.get("mtp_weight", 0.1)525        self.use_turboquant = config.get("use_turboquant", False)526        self.turboquant_bits = config.get("turboquant_bits", 4)527        self.use_activation_checkpointing = config.get("use_activation_checkpointing", False)528        self.logit_cap = config.get("logit_cap", 0)529        self.use_chunked_loss = config.get("use_chunked_loss", False)530        self.loss_chunk_size = config.get("loss_chunk_size", 2048)531        use_rmsnorm = config.get("use_rmsnorm", False)532 533        self.tok_emb = nn.Embedding(config["vocab_size"], config["n_embd"])534        if not self.use_rope:535            self.pos_emb = nn.Embedding(config["block_size"], config["n_embd"])536        self.blocks = nn.ModuleList([Block(config, i) for i in range(config["n_layer"])])537        self.ln_f = make_norm(config["n_embd"], use_rmsnorm)538        self.lm_head = nn.Linear(config["n_embd"], config["vocab_size"], bias=False)539        self.tok_emb.weight = self.lm_head.weight540 541        if self.use_mhc:542            n_streams = config.get("mhc_streams", 4)543            self.mhc_expand = MHCExpand(n_streams, config["n_embd"])544            self.mhc_collapse = MHCCollapse(n_streams, config["n_embd"])545 546        if self.use_mtp:547            self.mtp_heads = nn.ModuleList([548                MTPHead(config, future_idx=i + 1) for i in range(self.mtp_heads_n)549            ])550            if config.get("tie_mtp_lm_head", True):551                for head in self.mtp_heads:552                    head.lm_head.weight = self.lm_head.weight553 554        self.apply(self._init_weights)555 556        # Zero-init the output projection of each block (attention out-proj + MLP557        # down-proj), muP-style (modded-nanoGPT / nanochat). Each block starts as a558        # near-identity residual and learns to contribute, which helps convergence.559        if config.get("use_zero_init", False):560            for block in self.blocks:561                torch.nn.init.zeros_(block.attn.proj.weight)562                mlp_out = getattr(block.mlp, "down", None)563                if mlp_out is None:564                    mlp_out = block.mlp.proj  # MLP / ReLU2MLP name the out-proj "proj"565                torch.nn.init.zeros_(mlp_out.weight)566 567    def _init_weights(self, module):568        if isinstance(module, (nn.Linear, BitLinear)):569            torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)570            if module.bias is not None:571                torch.nn.init.zeros_(module.bias)572        elif isinstance(module, nn.Embedding):573            torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)574 575    def _compute_hidden(self, idx):576        B, T = idx.shape577        if T > self.config["block_size"]:578            raise ValueError(f"Input length {T} exceeds block_size {self.config['block_size']}")579        x = self.tok_emb(idx)580        if not self.use_rope:581            pos = torch.arange(T, device=idx.device)582            x = x + self.pos_emb(pos)583 584        if self.use_mhc:585            streams = self.mhc_expand(x)586            for block in self.blocks:587                if self.training and self.use_activation_checkpointing:588                    streams = checkpoint(lambda s, b=block: b(x, streams=s), streams, use_reentrant=False)589                else:590                    streams = block(x, streams=streams)591            x = self.mhc_collapse(streams)592        else:593            for block in self.blocks:594                if self.training and self.use_activation_checkpointing:595                    x = checkpoint(block, x, use_reentrant=False)596                else:597                    x = block(x)598 599        return self.ln_f(x)600 601    def forward(self, idx, targets=None, return_hidden=False):602        hidden = self._compute_hidden(idx)603        loss = None604        # Chunked loss avoids materializing the full [N, vocab] logits during training.605        # It can't return logits, so fall back to the dense path when logits are needed.606        if targets is not None and self.use_chunked_loss and not return_hidden:607            logits = None608            loss = chunked_cross_entropy(609                hidden, self.lm_head.weight, targets, self.logit_cap, self.loss_chunk_size610            )611        else:612            logits = soft_cap(self.lm_head(hidden), self.logit_cap)613            if targets is not None:614                loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=-1)615        if targets is not None and self.use_mtp:616            for head in self.mtp_heads:617                _, mtp_loss = head(hidden, targets)618                if mtp_loss is not None:619                    loss = loss + self.mtp_weight * mtp_loss620        if return_hidden:621            return logits, loss, hidden622        return logits, loss623 624    def _forward_inference(self, x, kv_caches, pos_offset=0, return_hidden=False):625        if self.use_mhc:626            streams = self.mhc_expand(x)627            for block, cache in zip(self.blocks, kv_caches or [None] * len(self.blocks)):628                streams = block(x, streams=streams, kv_cache=cache, pos_offset=pos_offset)629            x = self.mhc_collapse(streams)630        else:631            for block, cache in zip(self.blocks, kv_caches or [None] * len(self.blocks)):632                x = block(x, kv_cache=cache, pos_offset=pos_offset)633        hidden = self.ln_f(x)634        logits = soft_cap(self.lm_head(hidden), self.logit_cap)635        if return_hidden:636            return logits, hidden637        return logits638 639    def _embed(self, tokens, pos_offset=0):640        x = self.tok_emb(tokens)641        if not self.use_rope:642            T = tokens.shape[1]643            pos = torch.arange(pos_offset, pos_offset + T, device=tokens.device)644            x = x + self.pos_emb(pos)645        return x646 647    def _filter_logits(self, logits, top_k=None, top_p=None, min_p=None):648        if top_k is not None and top_k > 0:649            k = min(top_k, logits.size(-1))650            values, _ = torch.topk(logits, k)651            logits = logits.masked_fill(logits < values[:, [-1]], -float("inf"))652 653        if min_p is not None and min_p > 0:654            probs = F.softmax(logits, dim=-1)655            max_probs = probs.max(dim=-1, keepdim=True).values656            remove = probs < (min_p * max_probs)657            top_token = logits.argmax(dim=-1, keepdim=True)658            remove.scatter_(dim=-1, index=top_token, value=False)659            logits = logits.masked_fill(remove, -float("inf"))660 661        if top_p is not None and 0 < top_p < 1.0:662            sorted_logits, sorted_idx = torch.sort(logits, descending=True, dim=-1)663            sorted_probs = F.softmax(sorted_logits, dim=-1)664            cumulative_probs = sorted_probs.cumsum(dim=-1)665            sorted_remove = cumulative_probs > top_p666            sorted_remove[..., 1:] = sorted_remove[..., :-1].clone()667            sorted_remove[..., 0] = False668            remove = torch.zeros_like(logits, dtype=torch.bool)669            remove.scatter_(dim=-1, index=sorted_idx, src=sorted_remove)670            logits = logits.masked_fill(remove, -float("inf"))671 672        return logits673 674    def _distribution(self, logits, temperature=0.8, top_k=40, top_p=None, min_p=None):675        if temperature <= 0:676            token = logits.argmax(dim=-1, keepdim=True)677            probs = torch.zeros_like(logits)678            probs.scatter_(1, token, 1.0)679            return token, probs680        logits = self._filter_logits(logits / temperature, top_k=top_k, top_p=top_p, min_p=min_p)681        probs = F.softmax(logits, dim=-1)682        token = torch.multinomial(probs, num_samples=1)683        return token, probs684 685    def _make_kv_caches(self, use_turboquant, use_kv_cache=True):686        if not use_kv_cache:687            return None688        if use_turboquant:689            return [TurboQuantKVCache(bits=self.turboquant_bits) for _ in self.blocks]690        return [KVCache(self.config["block_size"]) for _ in self.blocks]691 692    def _trim_or_seed_prompt(self, idx):693        block_size = self.config["block_size"]694        if idx.shape[1] == 0:695            eos_id = 1696            idx = torch.tensor([[eos_id]], dtype=idx.dtype, device=idx.device)697        return idx[:, -block_size:]698 699    def _prefill_generation(self, idx, use_turboquant=False, use_kv_cache=True):700        kv_caches = self._make_kv_caches(use_turboquant, use_kv_cache=use_kv_cache)701        seq_len = idx.shape[1]702        x = self._embed(idx)703        logits, hidden = self._forward_inference(x, kv_caches, pos_offset=0, return_hidden=True)704        return logits, hidden[:, -1:, :], kv_caches, seq_len705 706    def _advance_generation_state(self, idx, idx_next, kv_caches, seq_len, use_turboquant):707        block_size = self.config["block_size"]708        if kv_caches is not None and seq_len < block_size:709            x = self._embed(idx_next, pos_offset=seq_len)710            logits, hidden = self._forward_inference(x, kv_caches, pos_offset=seq_len, return_hidden=True)711            return logits, hidden[:, -1:, :], kv_caches, seq_len + 1712 713        use_kv_cache = kv_caches is not None714        if kv_caches:715            for cache in kv_caches:716                cache.clear()717        idx_cond = idx[:, -block_size:]718        logits, hidden, kv_caches, seq_len = self._prefill_generation(719            idx_cond,720            use_turboquant=use_turboquant,721            use_kv_cache=use_kv_cache,722        )723        return logits, hidden, kv_caches, seq_len724 725    def _generate_autoregressive(726        self,727        idx,728        max_new_tokens,729        temperature=0.8,730        top_k=40,731        top_p=None,732        min_p=None,733        use_turboquant=None,734        use_kv_cache=True,735    ):736        idx = self._trim_or_seed_prompt(idx)737        use_turboquant = self.use_turboquant if use_turboquant is None else use_turboquant738        logits, last_hidden, kv_caches, seq_len = self._prefill_generation(739            idx,740            use_turboquant=use_turboquant,741            use_kv_cache=use_kv_cache,742        )743 744        for i in range(max_new_tokens):745            idx_next, _ = self._distribution(746                logits[:, -1, :],747                temperature=temperature,748                top_k=top_k,749                top_p=top_p,750                min_p=min_p,751            )752            idx = torch.cat([idx, idx_next], dim=1)753 754            if i < max_new_tokens - 1:755                logits, last_hidden, kv_caches, seq_len = self._advance_generation_state(756                    idx, idx_next, kv_caches, seq_len, use_turboquant757                )758 759        return idx760 761    def _mtp_draft(self, last_hidden, n_tokens, temperature=0.8, top_k=40, top_p=None, min_p=None):762        draft_tokens = []763        draft_probs = []764        for head in self.mtp_heads[:n_tokens]:765            draft_logits, _ = head(last_hidden)766            token, probs = self._distribution(767                draft_logits[:, -1, :],768                temperature=temperature,769                top_k=top_k,770                top_p=top_p,771                min_p=min_p,772            )773            draft_tokens.append(token)774            draft_probs.append(probs)775        return draft_tokens, draft_probs776 777    def _resample_on_reject(self, target_token, p_probs, q_probs, temperature):778        if temperature <= 0:779            return target_token780        residual = (p_probs - q_probs).clamp(min=0)781        denom = residual.sum(dim=-1, keepdim=True)782        if denom.item() <= 1e-12:783            return target_token784        return torch.multinomial(residual / denom, num_samples=1)785 786    def _mtp_speculative_generate(787        self,788        idx,789        max_new_tokens,790        temperature=0.8,791        top_k=40,792        top_p=None,793        min_p=None,794        speculate_tokens=None,795        use_turboquant=None,796        use_kv_cache=True,797    ):798        use_turboquant = self.use_turboquant if use_turboquant is None else use_turboquant799        # Batched verification needs a single sequence, MTP draft heads, and the800        # plain (rollback-able) KV cache. TurboQuant's cache cannot be rolled back801        # token-by-token, so fall back to autoregressive there.802        if not self.use_mtp or idx.size(0) != 1 or not use_kv_cache or use_turboquant:803            return self._generate_autoregressive(804                idx,805                max_new_tokens,806                temperature=temperature,807                top_k=top_k,808                top_p=top_p,809                min_p=min_p,810                use_turboquant=use_turboquant,811                use_kv_cache=use_kv_cache,812            )813 814        idx = self._trim_or_seed_prompt(idx)815        block_size = self.config["block_size"]816        draft_width = speculate_tokens or self.mtp_heads_n817        draft_width = max(1, min(draft_width, self.mtp_heads_n))818 819        logits, last_hidden, kv_caches, seq_len = self._prefill_generation(820            idx, use_turboquant=False, use_kv_cache=True821        )822        # p0 = main-model logits for the next token (verifies the first draft).823        p0_logits = logits[:, -1, :]824        generated = 0825 826        while generated < max_new_tokens:827            remaining = max_new_tokens - generated828            n_draft = min(draft_width, remaining)829 830            # No room left in the cache window: take one plain step (this slides the831            # window via re-prefill inside _advance_generation_state) and continue.832            if seq_len + n_draft > block_size:833                idx_next, _ = self._distribution(p0_logits, temperature, top_k, top_p, min_p)834                idx = torch.cat([idx, idx_next], dim=1)835                generated += 1836                if generated < max_new_tokens:837                    logits, last_hidden, kv_caches, seq_len = self._advance_generation_state(838                        idx, idx_next, kv_caches, seq_len, False839                    )840                    p0_logits = logits[:, -1, :]841                continue842 843            # 1. Draft n tokens cheaply from the MTP heads (no main-model forward).844            draft_tokens, draft_probs = self._mtp_draft(845                last_hidden, n_draft, temperature=temperature, top_k=top_k, top_p=top_p, min_p=min_p846            )847            draft_seq = torch.cat(draft_tokens, dim=1)848 849            # 2. Verify ALL drafts in a SINGLE main-model forward pass.850            x = self._embed(draft_seq, pos_offset=seq_len)851            v_logits, v_hidden = self._forward_inference(852                x, kv_caches, pos_offset=seq_len, return_hidden=True853            )854 855            # 3. Walk the drafts left-to-right; draft j is checked against the main856            #    distribution at the previous position (p0 for j=0, else v_logits[j-1]).857            accepted = 0858            reject_token = None859            for j in range(n_draft):860                target_logits = p0_logits if j == 0 else v_logits[:, j - 1, :]861                target_token, p_probs = self._distribution(862                    target_logits, temperature, top_k, top_p, min_p863                )864                if temperature <= 0:865                    accept = torch.equal(draft_tokens[j], target_token)866                else:867                    proposed = draft_tokens[j].item()868                    p = p_probs[0, proposed]869                    q = draft_probs[j][0, proposed].clamp(min=1e-12)870                    accept = torch.rand((), device=idx.device) <= torch.minimum(torch.ones_like(p), p / q)871                if accept:872                    accepted += 1873                else:874                    reject_token = self._resample_on_reject(875                        target_token, p_probs, draft_probs[j], temperature876                    )877                    break878 879            if accepted == n_draft:880                # Every draft matched the main model: commit them all. The cache881                # already holds them and v_hidden/v_logits give the next draft state882                # for free (no extra forward, no separate bonus token needed).883                idx = torch.cat([idx, draft_seq], dim=1)884                generated += n_draft885                seq_len += n_draft886                last_hidden = v_hidden[:, -1:, :]887                p0_logits = v_logits[:, -1, :]888            else:889                # Commit the accepted prefix plus the corrected token, then roll the890                # cache back to drop the rejected drafts' (now stale) KV entries.891                commit = torch.cat(draft_tokens[:accepted] + [reject_token], dim=1)892                idx = torch.cat([idx, commit], dim=1)893                generated += accepted + 1894                for cache in kv_caches:895                    cache.pos = seq_len + accepted896                seq_len += accepted897                if generated < max_new_tokens:898                    # reject_token's KV/hidden are not cached yet; one short forward rebases.899                    logits, last_hidden, kv_caches, seq_len = self._advance_generation_state(900                        idx, reject_token, kv_caches, seq_len, False901                    )902                    p0_logits = logits[:, -1, :]903 904        return idx905 906    def generate(907        self,908        idx,909        max_new_tokens,910        temperature=0.8,911        top_k=40,912        top_p=None,913        min_p=None,914        speculative=False,915        speculate_tokens=None,916        use_turboquant=None,917        use_kv_cache=True,918    ):919        if speculative:920            return self._mtp_speculative_generate(921                idx,922                max_new_tokens,923                temperature=temperature,924                top_k=top_k,925                top_p=top_p,926                min_p=min_p,927                speculate_tokens=speculate_tokens,928                use_turboquant=use_turboquant,929                use_kv_cache=use_kv_cache,930            )931        return self._generate_autoregressive(932            idx,933            max_new_tokens,934            temperature=temperature,935            top_k=top_k,936            top_p=top_p,937            min_p=min_p,938            use_turboquant=use_turboquant,939            use_kv_cache=use_kv_cache,940        )941 942 943# --- Configs ---944 945BASE_CONFIG = {946    "vocab_size": 16384,947    "block_size": 512,948    "n_embd": 512,949    "n_head": 8,950    "n_layer": 12,951}952 953# Individual techniques954MHC_CONFIG = {**BASE_CONFIG, "use_mhc": True, "mhc_streams": 4}955BITNET_CONFIG = {**BASE_CONFIG, "use_bitnet": True}956FAST_BITNET_CONFIG = {**BASE_CONFIG, "use_fast_bitnet": True}957MTP_CONFIG = {**BASE_CONFIG, "use_mtp": True, "mtp_heads": 4, "mtp_weight": 0.1}958ROPE_CONFIG = {**BASE_CONFIG, "use_rope": True}959GQA_CONFIG = {**BASE_CONFIG, "n_kv_head": 2}960SWIGLU_CONFIG = {**BASE_CONFIG, "use_swiglu": True}961RMSNORM_CONFIG = {**BASE_CONFIG, "use_rmsnorm": True}962TURBOQUANT_CONFIG = {**BASE_CONFIG, "use_turboquant": True, "turboquant_bits": 4}963 964# Combinations965MHC_BITNET_CONFIG = {**BASE_CONFIG, "use_mhc": True, "mhc_streams": 4, "use_bitnet": True}966MHC_MTP_CONFIG = {**BASE_CONFIG, "use_mhc": True, "mhc_streams": 4, "use_mtp": True, "mtp_heads": 4, "mtp_weight": 0.1}967 968# Modern LLaMA-style (RoPE + GQA + SwiGLU + RMSNorm)969MODERN_CONFIG = {**BASE_CONFIG, "use_rope": True, "n_kv_head": 2, "use_swiglu": True, "use_rmsnorm": True}970 971# Everything972ALL_CONFIG = {973    **BASE_CONFIG,974    "use_mhc": True, "mhc_streams": 4,975    "use_bitnet": True,976    "use_mtp": True, "mtp_heads": 4, "mtp_weight": 0.1,977    "use_rope": True, "n_kv_head": 2,978    "use_swiglu": True, "use_rmsnorm": True,979    "use_turboquant": True, "turboquant_bits": 4,980}981 982RECOMMENDED_CONFIG = {983    **BASE_CONFIG,984    "use_rope": True, "n_kv_head": 2,985    "use_swiglu": True, "use_rmsnorm": True,986    "use_mtp": True, "mtp_heads": 4, "mtp_weight": 0.1,987}988 989FAST_2060_CONFIG = {990    **BASE_CONFIG,991    "block_size": 256,992    "n_embd": 384,993    "n_head": 6,994    "n_layer": 8,995    "use_rope": True,996    "n_kv_head": 2,997    "use_swiglu": True,998    "use_rmsnorm": True,999}1000 1001FAST_2060_MTP_CONFIG = {1002    **FAST_2060_CONFIG,1003    "use_mtp": True,1004    "mtp_heads": 2,1005    "mtp_weight": 0.1,1006    "tie_mtp_lm_head": True,1007}1008 1009FAST_2060_MTP_FBITNET_CONFIG = {1010    **FAST_2060_MTP_CONFIG,1011    "use_fast_bitnet": True,1012}1013 1014# modded-nanoGPT-style recipe. QK-Norm helps under any optimizer; ReLU2 and1015# logit_cap only pay off paired with Muon's higher LR. Train with --optimizer muon.1016FAST_2060_MODDED_CONFIG = {1017    **FAST_2060_MTP_CONFIG,1018    "use_swiglu": False,   # superseded by ReLU2 below1019    "use_relu2": True,1020    "use_qk_norm": True,1021    "logit_cap": 15.0,1022    "use_zero_init": True,  # measured: val 2.13 -> 2.04 at equal steps, free1023}1024 1025# Same modded recipe but WITHOUT MTP (built on FAST_2060_CONFIG, not the _mtp one).1026# This is exactly the config that won the convergence A/B (val 2.13). No MTP means a1027# cleaner pure-CE loss number and faster steps, but no speculative-decoding heads.1028FAST_2060_MODDED_NOMTP_CONFIG = {1029    **FAST_2060_CONFIG,1030    "use_swiglu": False,1031    "use_relu2": True,1032    "use_qk_norm": True,1033    "logit_cap": 15.0,1034    "use_zero_init": True,1035}1036 1037FAST_2060_MTP_TURBO_CONFIG = {1038    **FAST_2060_MTP_CONFIG,1039    "use_turboquant": True,1040    "turboquant_bits": 4,1041}1042 1043TINY_FAST_CONFIG = {1044    **BASE_CONFIG,1045    "block_size": 256,1046    "n_embd": 256,1047    "n_head": 4,1048    "n_layer": 6,1049    "use_rope": True,1050    "n_kv_head": 2,1051    "use_swiglu": True,1052    "use_rmsnorm": True,1053}1054 1055LOW_MEMORY_2060_CONFIG = {1056    **FAST_2060_CONFIG,1057    "use_activation_checkpointing": True,1058}1059 1060CONFIGS = {1061    "base": BASE_CONFIG,1062    "mhc": MHC_CONFIG,1063    "bitnet": BITNET_CONFIG,1064    "mtp": MTP_CONFIG,1065    "rope": ROPE_CONFIG,1066    "gqa": GQA_CONFIG,1067    "swiglu": SWIGLU_CONFIG,1068    "rmsnorm": RMSNORM_CONFIG,1069    "turboquant": TURBOQUANT_CONFIG,1070    "mhc_bitnet": MHC_BITNET_CONFIG,1071    "mhc_mtp": MHC_MTP_CONFIG,1072    "modern": MODERN_CONFIG,1073    "all": ALL_CONFIG,1074    "recommended": RECOMMENDED_CONFIG,1075    "fast_2060": FAST_2060_CONFIG,1076    "fast_2060_mtp": FAST_2060_MTP_CONFIG,1077    "fast_2060_mtp_fbitnet": FAST_2060_MTP_FBITNET_CONFIG,1078    "fast_2060_modded": FAST_2060_MODDED_CONFIG,1079    "fast_2060_modded_nomtp": FAST_2060_MODDED_NOMTP_CONFIG,1080    "fast_2060_mtp_turbo": FAST_2060_MTP_TURBO_CONFIG,1081    "tiny_fast": TINY_FAST_CONFIG,1082    "low_memory_2060": LOW_MEMORY_2060_CONFIG,1083}1084 1085 1086def get_model_config(name="fast_2060", **overrides):1087    if name not in CONFIGS:1088        available = ", ".join(sorted(CONFIGS))1089        raise ValueError(f"Unknown config '{name}'. Available configs: {available}")1090    return {**CONFIGS[name], **{k: v for k, v in overrides.items() if v is not None}}1091 1092 1093MODEL_CONFIG = RECOMMENDED_CONFIG1094 1095if __name__ == "__main__":1096    configs = CONFIGS1097    for name, cfg in configs.items():1098        model = GPT(cfg)1099        n_params = sum(p.numel() for p in model.parameters())1100        x = torch.randint(0, cfg["vocab_size"], (2, 64))1101        logits, loss = model(x, x)1102        print(f"{name:<12} | {n_params:>12,} params ({n_params/1e6:.1f}M) | loss: {loss.item():.2f}")1103