Download model.py from nandutt/mini-llm-tinystories: direct link, hf CLI and curl.
- Browser
- Download file 7.63 kB
-
https://huggingface.co/nandutt/mini-llm-tinystories/resolve/main/model.py
- Command line
-
hf download hf://nandutt/mini-llm-tinystories/model.py
-
curl -L -o model.py https://huggingface.co/nandutt/mini-llm-tinystories/resolve/main/model.py
7.63 kB
| """ | |
| Stage 2 — ARCHITECTURE (lecture: Architecture, covered briefly because it is "not the hardest part"). | |
| Llama-style decoder-only Transformer: | |
| - RMSNorm (pre-norm) : cheaper than LayerNorm, stable | |
| - RoPE : position via query/key rotation instead of absolute position embeddings | |
| - SwiGLU FFN : gated MLP, standard in modern models | |
| - Causal self-attention via F.scaled_dot_product_attention (uses FlashAttention when available) | |
| - Weight tying: embedding <-> output head | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from config import ModelConfig | |
| class RMSNorm(nn.Module): | |
| def __init__(self, dim: int, eps: float = 1e-6): | |
| super().__init__() | |
| self.eps = eps | |
| self.weight = nn.Parameter(torch.ones(dim)) | |
| def forward(self, x): | |
| x_f = x.float() | |
| x_f = x_f * torch.rsqrt(x_f.pow(2).mean(-1, keepdim=True) + self.eps) | |
| return x_f.type_as(x) * self.weight | |
| def rope_cache(seq_len: int, head_dim: int, base: float = 10000.0): | |
| inv_freq = 1.0 / (base ** (torch.arange(0, head_dim, 2).float() / head_dim)) | |
| t = torch.arange(seq_len).float() | |
| freqs = torch.outer(t, inv_freq) # (T, hd/2) | |
| return freqs.cos(), freqs.sin() | |
| def apply_rope(x, cos, sin): | |
| # x: (B, H, T, hd) — rotate (even, odd) dimension pairs | |
| x1, x2 = x[..., 0::2], x[..., 1::2] | |
| cos, sin = cos[None, None], sin[None, None] | |
| out = torch.stack((x1 * cos - x2 * sin, x1 * sin + x2 * cos), dim=-1) | |
| return out.flatten(-2).type_as(x) | |
| class Attention(nn.Module): | |
| def __init__(self, cfg: ModelConfig): | |
| super().__init__() | |
| assert cfg.d_model % cfg.n_heads == 0 | |
| self.n_heads = cfg.n_heads | |
| self.head_dim = cfg.d_model // cfg.n_heads | |
| self.qkv = nn.Linear(cfg.d_model, 3 * cfg.d_model, bias=False) | |
| self.proj = nn.Linear(cfg.d_model, cfg.d_model, bias=False) | |
| self.dropout = cfg.dropout | |
| def forward(self, x, cos, sin): | |
| B, T, C = x.shape | |
| q, k, v = self.qkv(x).split(C, dim=-1) | |
| q, k, v = (t.view(B, T, self.n_heads, self.head_dim).transpose(1, 2) for t in (q, k, v)) | |
| q, k = apply_rope(q, cos, sin), apply_rope(k, cos, sin) | |
| y = F.scaled_dot_product_attention( | |
| q, k, v, is_causal=True, dropout_p=self.dropout if self.training else 0.0 | |
| ) | |
| return self.proj(y.transpose(1, 2).contiguous().view(B, T, C)) | |
| class SwiGLU(nn.Module): | |
| def __init__(self, cfg: ModelConfig): | |
| super().__init__() | |
| hidden = int(cfg.ffn_mult * cfg.d_model) | |
| hidden = 64 * ((hidden + 63) // 64) | |
| self.w_gate = nn.Linear(cfg.d_model, hidden, bias=False) | |
| self.w_up = nn.Linear(cfg.d_model, hidden, bias=False) | |
| self.w_down = nn.Linear(hidden, cfg.d_model, bias=False) | |
| def forward(self, x): | |
| return self.w_down(F.silu(self.w_gate(x)) * self.w_up(x)) | |
| class Block(nn.Module): | |
| def __init__(self, cfg: ModelConfig): | |
| super().__init__() | |
| self.norm1 = RMSNorm(cfg.d_model) | |
| self.attn = Attention(cfg) | |
| self.norm2 = RMSNorm(cfg.d_model) | |
| self.ffn = SwiGLU(cfg) | |
| self.drop = nn.Dropout(cfg.dropout) | |
| def forward(self, x, cos, sin): | |
| x = x + self.drop(self.attn(self.norm1(x), cos, sin)) | |
| x = x + self.drop(self.ffn(self.norm2(x))) | |
| return x | |
| class GPT(nn.Module): | |
| def __init__(self, cfg: ModelConfig): | |
| super().__init__() | |
| self.cfg = cfg | |
| self.embed = nn.Embedding(cfg.vocab_size, cfg.d_model) | |
| self.blocks = nn.ModuleList(Block(cfg) for _ in range(cfg.n_layers)) | |
| self.norm = RMSNorm(cfg.d_model) | |
| self.head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False) | |
| if cfg.tie_embeddings: | |
| self.head.weight = self.embed.weight | |
| cos, sin = rope_cache(cfg.context_len, cfg.d_model // cfg.n_heads) | |
| self.register_buffer("rope_cos", cos, persistent=False) | |
| self.register_buffer("rope_sin", sin, persistent=False) | |
| self.apply(self._init) | |
| # scaled init for residual projections (GPT-2): keeps residual-stream variance in check | |
| for n, p in self.named_parameters(): | |
| if n.endswith("proj.weight") or n.endswith("w_down.weight"): | |
| nn.init.normal_(p, 0.0, 0.02 / math.sqrt(2 * cfg.n_layers)) | |
| def _init(m): | |
| if isinstance(m, nn.Linear): | |
| nn.init.normal_(m.weight, 0.0, 0.02) | |
| elif isinstance(m, nn.Embedding): | |
| nn.init.normal_(m.weight, 0.0, 0.02) | |
| def num_params(self, non_embedding: bool = True) -> int: | |
| n = sum(p.numel() for p in self.parameters()) | |
| if non_embedding: | |
| n -= self.embed.weight.numel() | |
| return n | |
| def forward(self, idx, targets=None, loss_mask=None): | |
| B, T = idx.shape | |
| assert T <= self.cfg.context_len, f"length {T} > context_len {self.cfg.context_len}" | |
| x = self.embed(idx) | |
| cos, sin = self.rope_cos[:T], self.rope_sin[:T] | |
| for blk in self.blocks: | |
| x = blk(x, cos, sin) | |
| logits = self.head(self.norm(x)) | |
| if targets is None: | |
| return logits, None | |
| loss = F.cross_entropy(logits.float().view(-1, logits.size(-1)), targets.view(-1), reduction="none") | |
| if loss_mask is not None: | |
| m = loss_mask.view(-1).float() | |
| loss = (loss * m).sum() / m.sum().clamp(min=1) | |
| else: | |
| loss = loss.mean() | |
| return logits, loss | |
| def configure_optimizer(self, lr: float, weight_decay: float, betas=(0.9, 0.95)): | |
| # weight decay only on matrices (>=2D), not norms/biases | |
| decay = [p for p in self.parameters() if p.requires_grad and p.dim() >= 2] | |
| no_decay = [p for p in self.parameters() if p.requires_grad and p.dim() < 2] | |
| groups = [{"params": decay, "weight_decay": weight_decay}, {"params": no_decay, "weight_decay": 0.0}] | |
| fused = torch.cuda.is_available() | |
| return torch.optim.AdamW(groups, lr=lr, betas=betas, fused=fused) | |
| def generate(self, idx, max_new_tokens: int, temperature: float = 1.0, top_k: int | None = 50, | |
| eos_id: int | None = None, valid_vocab: int | None = None): | |
| """ | |
| Autoregressive sampling for a batch of equal-length prompts. | |
| valid_vocab: tokenizer vocab size — ids above it (spare embedding rows) are never sampled. | |
| Returns (new_tokens [B, <=max_new], finished [B]) — tokens after EOS are filled with eos_id. | |
| """ | |
| B = idx.size(0) | |
| finished = torch.zeros(B, dtype=torch.bool, device=idx.device) | |
| new = [] | |
| for _ in range(max_new_tokens): | |
| ctx = idx[:, -self.cfg.context_len:] | |
| logits, _ = self(ctx) | |
| logits = logits[:, -1, :].float() / max(temperature, 1e-5) | |
| if valid_vocab is not None and valid_vocab < logits.size(-1): | |
| logits[:, valid_vocab:] = -float("inf") | |
| if top_k: | |
| v, _ = torch.topk(logits, min(top_k, logits.size(-1))) | |
| logits[logits < v[:, [-1]]] = -float("inf") | |
| nxt = torch.multinomial(F.softmax(logits, dim=-1), 1) | |
| if eos_id is not None: | |
| nxt = torch.where(finished[:, None], torch.full_like(nxt, eos_id), nxt) | |
| finished |= nxt.squeeze(1) == eos_id | |
| idx = torch.cat([idx, nxt], dim=1) | |
| new.append(nxt) | |
| if eos_id is not None and finished.all(): | |
| break | |
| return torch.cat(new, dim=1), finished | |