import torch import torch.nn as nn import torch.nn.functional as F class CausalSelfAttention(nn.Module): def __init__(self, n_embd, n_head): super().__init__() assert n_embd % n_head == 0 self.n_head = n_head self.head_dim = n_embd // n_head self.c_attn = nn.Linear(n_embd, 3 * n_embd, bias=False) self.c_proj = nn.Linear(n_embd, n_embd, bias=False) def forward(self, x): B, T, C = x.size() q, k, v = self.c_attn(x).split(self.head_dim * self.n_head, dim=2) q = q.view(B, T, self.n_head, self.head_dim).transpose(1, 2) k = k.view(B, T, self.n_head, self.head_dim).transpose(1, 2) v = v.view(B, T, self.n_head, self.head_dim).transpose(1, 2) att = F.scaled_dot_product_attention(q, k, v, is_causal=True) att = att.transpose(1, 2).contiguous().view(B, T, C) return self.c_proj(att) class MLP(nn.Module): def __init__(self, n_embd, n_inner): super().__init__() self.c_fc = nn.Linear(n_embd, n_inner, bias=False) self.c_proj = nn.Linear(n_inner, n_embd, bias=False) def forward(self, x): return self.c_proj(F.gelu(self.c_fc(x))) class Block(nn.Module): def __init__(self, n_embd, n_head): super().__init__() self.ln_1 = nn.LayerNorm(n_embd) self.attn = CausalSelfAttention(n_embd, n_head) self.ln_2 = nn.LayerNorm(n_embd) self.mlp = MLP(n_embd, 4 * n_embd) def forward(self, x): x = x + self.attn(self.ln_1(x)) x = x + self.mlp(self.ln_2(x)) return x class CharGPT(nn.Module): """Character-level causal transformer (nanoGPT-style). No bias in attention / FFN / lm_head; LayerNorm carries the affine bias.""" def __init__(self, vocab_size, block_size, n_layer, n_head, n_embd): super().__init__() self.block_size = block_size self.transformer = nn.ModuleDict({ "wte": nn.Embedding(vocab_size, n_embd), "wpe": nn.Embedding(block_size, n_embd), "drop": nn.Dropout(0.0), "h": nn.ModuleList([Block(n_embd, n_head) for _ in range(n_layer)]), "ln_f": nn.LayerNorm(n_embd), }) self.lm_head = nn.Linear(n_embd, vocab_size, bias=False) def forward(self, idx, targets=None): B, T = idx.size() assert T <= self.block_size, f"block size {self.block_size} < {T}" pos = torch.arange(0, T, device=idx.device) x = self.transformer["drop"]( self.transformer["wte"](idx) + self.transformer["wpe"](pos)) for block in self.transformer["h"]: x = block(x) x = self.transformer["ln_f"](x) logits = self.lm_head(x) loss = None if targets is not None: loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1)) return logits, loss @torch.no_grad() def generate(self, idx, max_new_tokens, temperature=0.8, top_k=40): for _ in range(max_new_tokens): idx_cond = idx[:, -self.block_size:] logits, _ = self(idx_cond) logits = logits[:, -1, :] / temperature if top_k: v, _ = torch.topk(logits, min(top_k, logits.size(-1))) logits[logits < v[:, [-1]]] = float("-inf") probs = F.softmax(logits, dim=-1) idx = torch.cat((idx, torch.multinomial(probs, num_samples=1)), dim=1) return idx def from_config(config): return CharGPT( vocab_size=config["vocab_size"], block_size=config["block_size"], n_layer=config["n_layer"], n_head=config["n_head"], n_embd=config["n_embd"], ) if __name__ == "__main__": import json cfg = json.load(open("config.json")) m = from_config(cfg) print("params:", sum(p.numel() for p in m.parameters())) # tiny smoke test x = torch.randint(0, cfg["vocab_size"], (1, 32)) logits, loss = m(x, x) print("smoke loss:", loss.item())