"""Dense decoder-only GPT model used by train.py.""" from __future__ import annotations import torch import torch.nn as nn import torch.nn.functional as F from config import ModelConfig class RMSNorm(nn.Module): def __init__(self, width: int, eps: float = 1e-5): super().__init__() self.weight = nn.Parameter(torch.ones(width)) self.eps = eps def forward(self, x: torch.Tensor) -> torch.Tensor: variance = x.float().square().mean(dim=-1, keepdim=True) return (x * torch.rsqrt(variance + self.eps)).type_as(x) * self.weight 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.output = nn.Linear(cfg.d_model, cfg.d_model, bias=False) self.dropout = cfg.dropout def forward(self, x: torch.Tensor) -> torch.Tensor: batch, length, width = x.shape q, k, v = self.qkv(x).chunk(3, dim=-1) q = q.view(batch, length, self.n_heads, self.head_dim).transpose(1, 2) k = k.view(batch, length, self.n_heads, self.head_dim).transpose(1, 2) v = v.view(batch, length, self.n_heads, self.head_dim).transpose(1, 2) output = F.scaled_dot_product_attention( q, k, v, is_causal=True, dropout_p=self.dropout if self.training else 0.0 ) return self.output(output.transpose(1, 2).contiguous().view(batch, length, width)) class SwiGLU(nn.Module): def __init__(self, cfg: ModelConfig): super().__init__() self.gate_up = nn.Linear(cfg.d_model, 2 * cfg.mlp_hidden, bias=False) self.down = nn.Linear(cfg.mlp_hidden, cfg.d_model, bias=False) def forward(self, x: torch.Tensor) -> torch.Tensor: gate, up = self.gate_up(x).chunk(2, dim=-1) return self.down(F.silu(gate) * up) class TransformerBlock(nn.Module): def __init__(self, cfg: ModelConfig): super().__init__() self.attention_norm = RMSNorm(cfg.d_model) self.attention = Attention(cfg) self.mlp_norm = RMSNorm(cfg.d_model) self.mlp = SwiGLU(cfg) def forward(self, x: torch.Tensor) -> torch.Tensor: x = x + self.attention(self.attention_norm(x)) return x + self.mlp(self.mlp_norm(x)) class GPT(nn.Module): """A dense ~50M model. Tied embeddings keep vocabulary capacity efficient.""" def __init__(self, cfg: ModelConfig): super().__init__() self.cfg = cfg self.token_embedding = nn.Embedding(cfg.vocab_size, cfg.d_model) self.position_embedding = nn.Embedding(cfg.block_size, cfg.d_model) self.blocks = nn.ModuleList([TransformerBlock(cfg) for _ in range(cfg.n_layers)]) self.final_norm = RMSNorm(cfg.d_model) self.lm_head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False) self.lm_head.weight = self.token_embedding.weight self.gradient_checkpointing = False self.apply(self._init_weights) @staticmethod def _init_weights(module: nn.Module) -> None: if isinstance(module, (nn.Linear, nn.Embedding)): nn.init.normal_(module.weight, mean=0.0, std=0.02) def forward(self, input_ids: torch.Tensor, targets: torch.Tensor | None = None): _, length = input_ids.shape if length > self.cfg.block_size: raise ValueError(f"Sequence length {length} exceeds {self.cfg.block_size}.") positions = torch.arange(length, device=input_ids.device) x = self.token_embedding(input_ids) + self.position_embedding(positions) checkpointing = self.training and self.gradient_checkpointing for block in self.blocks: x = torch.utils.checkpoint.checkpoint(block, x, use_reentrant=False) if checkpointing else block(x) x = self.final_norm(x) if targets is None: return self.lm_head(x[:, [-1]]), None # Do not allocate [batch, sequence, 32k] logits at once: this is VRAM-safe on 6GB. hidden = x[:, :-1].contiguous().view(-1, self.cfg.d_model) labels = targets[:, 1:].contiguous().view(-1) loss_sum = hidden.new_zeros((), dtype=torch.float32) for start in range(0, labels.numel(), 256): loss_sum = loss_sum + F.cross_entropy( self.lm_head(hidden[start : start + 256]).float(), labels[start : start + 256], reduction="sum" ) return None, loss_sum / labels.numel() @torch.inference_mode() def generate( self, input_ids: torch.Tensor, max_new_tokens: int, temperature: float = 0.8, top_k: int = 50, eos_id: int | None = None, ): for _ in range(max_new_tokens): logits, _ = self(input_ids[:, -self.cfg.block_size :]) logits = logits[:, -1] / max(temperature, 1e-5) if top_k > 0: threshold = torch.topk(logits, min(top_k, logits.size(-1))).values[:, [-1]] logits = logits.masked_fill(logits < threshold, float("-inf")) next_token = torch.multinomial(F.softmax(logits, dim=-1), 1) input_ids = torch.cat((input_ids, next_token), dim=1) if eos_id is not None and (next_token == eos_id).all(): break return input_ids def parameter_count(model: nn.Module) -> int: return sum(parameter.numel() for parameter in model.parameters())