| """ |
| A small character-level GPT (decoder-only transformer) in tinygrad. |
| |
| Token + position embeddings -> a stack of pre-norm blocks (causal self-attention + |
| MLP, each added residually) -> a weight-tied head giving next-character logits. |
| """ |
| import math |
| from dataclasses import dataclass |
| from tinygrad import Tensor, nn |
|
|
|
|
| @dataclass |
| class GPTConfig: |
| vocab_size: int = 162 |
| block_size: int = 256 |
| n_layer: int = 6 |
| n_head: int = 6 |
| n_embd: int = 384 |
| dropout: float = 0.2 |
|
|
|
|
| class LayerNorm: |
| """Layer norm with a learnable scale and no bias: normalize each position's |
| feature vector to zero mean / unit variance, then rescale per channel.""" |
| def __init__(self, dim: int): |
| self.weight = Tensor.ones(dim) |
|
|
| def __call__(self, x: Tensor) -> Tensor: |
| return x.layernorm() * self.weight |
|
|
|
|
| class CausalSelfAttention: |
| """Multi-head self-attention where each position may only attend to earlier ones.""" |
| def __init__(self, cfg: GPTConfig): |
| self.c_attn = nn.Linear(cfg.n_embd, 3 * cfg.n_embd, bias=False) |
| self.c_proj = nn.Linear(cfg.n_embd, cfg.n_embd, bias=False) |
| self.n_head, self.dropout = cfg.n_head, cfg.dropout |
|
|
| def __call__(self, x: Tensor) -> Tensor: |
| B, T, C = x.shape |
| q, k, v = self.c_attn(x).split(C, dim=2) |
| split_heads = lambda t: t.reshape(B, T, self.n_head, C // self.n_head).transpose(1, 2) |
| q, k, v = split_heads(q), split_heads(k), split_heads(v) |
| p = self.dropout if Tensor.training else 0.0 |
| y = q.scaled_dot_product_attention(k, v, is_causal=True, dropout_p=p) |
| y = y.transpose(1, 2).reshape(B, T, C) |
| return self.c_proj(y).dropout(self.dropout) |
|
|
|
|
| class MLP: |
| """Position-wise feed-forward network: expand 4x, GELU, project back.""" |
| def __init__(self, cfg: GPTConfig): |
| self.c_fc = nn.Linear(cfg.n_embd, 4 * cfg.n_embd, bias=False) |
| self.c_proj = nn.Linear(4 * cfg.n_embd, cfg.n_embd, bias=False) |
| self.dropout = cfg.dropout |
|
|
| def __call__(self, x: Tensor) -> Tensor: |
| return self.c_proj(self.c_fc(x).gelu()).dropout(self.dropout) |
|
|
|
|
| class Block: |
| """One transformer block: pre-norm attention and MLP, each added residually.""" |
| def __init__(self, cfg: GPTConfig): |
| self.ln_1, self.attn = LayerNorm(cfg.n_embd), CausalSelfAttention(cfg) |
| self.ln_2, self.mlp = LayerNorm(cfg.n_embd), MLP(cfg) |
|
|
| def __call__(self, x: Tensor) -> Tensor: |
| x = x + self.attn(self.ln_1(x)) |
| x = x + self.mlp(self.ln_2(x)) |
| return x |
|
|
|
|
| class GPT: |
| """The full model: embeddings, a stack of blocks, and a tied output head.""" |
| def __init__(self, cfg: GPTConfig): |
| self.cfg = cfg |
| self.wte = nn.Embedding(cfg.vocab_size, cfg.n_embd) |
| self.wpe = nn.Embedding(cfg.block_size, cfg.n_embd) |
| self.dropout = cfg.dropout |
| self.blocks = [Block(cfg) for _ in range(cfg.n_layer)] |
| self.ln_f = LayerNorm(cfg.n_embd) |
| self._init_weights() |
|
|
| def _init_weights(self): |
| |
| |
| randn = lambda shape, std: Tensor.randn(*shape) * std |
| self.wte.weight = randn(self.wte.weight.shape, 0.02) |
| self.wpe.weight = randn(self.wpe.weight.shape, 0.02) |
| scaled = 0.02 / math.sqrt(2 * self.cfg.n_layer) |
| for b in self.blocks: |
| for lin in (b.attn.c_attn, b.mlp.c_fc): |
| lin.weight = randn(lin.weight.shape, 0.02) |
| for lin in (b.attn.c_proj, b.mlp.c_proj): |
| lin.weight = randn(lin.weight.shape, scaled) |
|
|
| def __call__(self, idx: Tensor) -> Tensor: |
| B, T = idx.shape |
| pos = Tensor.arange(T) |
| x = (self.wte(idx) + self.wpe(pos)).dropout(self.dropout) |
| for b in self.blocks: |
| x = b(x) |
| x = self.ln_f(x) |
| return x.linear(self.wte.weight.transpose()) |
|
|
| def generate(self, idx: Tensor, max_new_tokens: int, temperature: float = 1.0, |
| top_k: int | None = None) -> Tensor: |
| """Autoregressively extend `idx` (shape (B, T)) by sampling one token at a time. |
| |
| temperature scales the logits (lower = greedier); top_k, if set, restricts |
| sampling to the k most likely next tokens. Returns the prompt plus new tokens. |
| |
| The context is always right-padded to block_size so every forward pass has the |
| same shape (one kernel compile instead of one per length); causal masking means |
| the padding never influences the real last position. |
| """ |
| bs = self.cfg.block_size |
| for _ in range(max_new_tokens): |
| ctx = idx[:, -bs:] |
| t = ctx.shape[1] |
| if t < bs: |
| ctx = ctx.cat(Tensor.zeros(ctx.shape[0], bs - t, dtype=idx.dtype), dim=1) |
| logits = self(ctx)[:, t - 1, :] / temperature |
| if top_k: |
| kth = logits.topk(min(top_k, logits.shape[-1]))[0][:, -1:] |
| logits = (logits < kth).where(-float("inf"), logits) |
| next_id = logits.softmax(-1).multinomial(1).cast(idx.dtype) |
| idx = idx.cat(next_id, dim=1).realize() |
| return idx |
|
|