mini / model.py
StrongDev2024's picture
Add Mini checkpoint
d3166c0 verified
Raw
History Blame Contribute Delete
6.98 kB
"""A small decoder-only transformer.
It reads tokens from left to right and scores the next token.
Causal attention means a position may look at earlier tokens only.
"""
import json
from pathlib import Path
import torch
import torch.nn as nn
import torch.nn.functional as F
from pretrained import resolve_pretrained_folder
# Keys TinyGPT.__init__ accepts. config.json may also carry Hub metadata.
CONFIG_KEYS = ("vocab_size", "block_size", "n_layer", "n_head", "n_embd", "dropout")
class CausalSelfAttention(nn.Module):
def __init__(self, n_embd, n_head, block_size, dropout):
super().__init__()
self.n_head = n_head
self.head_dim = n_embd // n_head
self.qkv = nn.Linear(n_embd, 3 * n_embd)
self.proj = nn.Linear(n_embd, n_embd)
self.dropout = nn.Dropout(dropout)
# Lower triangle is 1: each token may attend to itself and the past.
mask = torch.tril(torch.ones(block_size, block_size))
self.register_buffer("mask", mask.view(1, 1, block_size, block_size))
def forward(self, x, return_attn=False):
batch, time, channels = x.shape
qkv = self.qkv(x)
query, key, value = qkv.split(channels, dim=2)
query = query.view(batch, time, self.n_head, self.head_dim).transpose(1, 2)
key = key.view(batch, time, self.n_head, self.head_dim).transpose(1, 2)
value = value.view(batch, time, self.n_head, self.head_dim).transpose(1, 2)
scores = (query @ key.transpose(-2, -1)) / (self.head_dim ** 0.5)
scores = scores.masked_fill(self.mask[:, :, :time, :time] == 0, float("-inf"))
weights = F.softmax(scores, dim=-1)
mixed = (self.dropout(weights) @ value).transpose(1, 2).contiguous().view(batch, time, channels)
out = self.dropout(self.proj(mixed))
if return_attn:
return out, weights
return out
class Block(nn.Module):
def __init__(self, n_embd, n_head, block_size, dropout):
super().__init__()
self.ln1 = nn.LayerNorm(n_embd)
self.attn = CausalSelfAttention(n_embd, n_head, block_size, dropout)
self.ln2 = nn.LayerNorm(n_embd)
self.mlp = nn.Sequential(
nn.Linear(n_embd, 4 * n_embd),
nn.GELU(),
nn.Linear(4 * n_embd, n_embd),
nn.Dropout(dropout),
)
def forward(self, x, return_attn=False):
attended = self.attn(self.ln1(x), return_attn=return_attn)
if return_attn:
attended, weights = attended
x = x + attended
x = x + self.mlp(self.ln2(x))
if return_attn:
return x, weights
return x
class TinyGPT(nn.Module):
def __init__(self, vocab_size, block_size=128, n_layer=2, n_head=4, n_embd=128, dropout=0.1):
super().__init__()
if n_embd % n_head != 0:
raise ValueError("n_embd must be divisible by n_head")
self.block_size = block_size
self.tok_emb = nn.Embedding(vocab_size, n_embd)
self.pos_emb = nn.Embedding(block_size, n_embd)
self.drop = nn.Dropout(dropout)
self.blocks = nn.ModuleList(
[Block(n_embd, n_head, block_size, dropout) for _ in range(n_layer)]
)
self.ln_f = nn.LayerNorm(n_embd)
self.head = nn.Linear(n_embd, vocab_size, bias=False)
self.config = {
"vocab_size": vocab_size,
"block_size": block_size,
"n_layer": n_layer,
"n_head": n_head,
"n_embd": n_embd,
"dropout": dropout,
}
def forward(self, idx, targets=None, return_attn=False):
_batch, time = idx.shape
positions = torch.arange(time, device=idx.device)
x = self.drop(self.tok_emb(idx) + self.pos_emb(positions))
attentions = []
for block in self.blocks:
if return_attn:
x, weights = block(x, return_attn=True)
attentions.append(weights)
else:
x = block(x)
logits = self.head(self.ln_f(x))
loss = None
if targets is not None:
loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
if return_attn:
return logits, loss, attentions
return logits, loss
@torch.no_grad()
def generate(self, idx, max_new_tokens, temperature=0.0, top_k=None, stop_ids=None):
"""Append tokens until a stop token or the length limit.
temperature 0 always picks the most likely next token.
"""
stop_ids = set(stop_ids or [])
for _ in range(max_new_tokens):
idx_cond = idx[:, -self.block_size :]
logits, _ = self(idx_cond)
logits = logits[:, -1, :]
if temperature <= 0:
next_id = torch.argmax(logits, dim=-1, keepdim=True)
else:
logits = logits / max(temperature, 1e-6)
if top_k is not None:
top_values, _ = torch.topk(logits, min(top_k, logits.size(-1)))
logits = logits.masked_fill(logits < top_values[:, [-1]], float("-inf"))
probs = F.softmax(logits, dim=-1)
next_id = torch.multinomial(probs, num_samples=1)
idx = torch.cat([idx, next_id], dim=1)
if int(next_id.item()) in stop_ids:
break
return idx
def save_pretrained(self, folder):
"""Write config.json and model.safetensors for the Hugging Face Hub."""
folder = Path(folder)
folder.mkdir(parents=True, exist_ok=True)
payload = {"model_type": "tiny-gpt", **self.config}
(folder / "config.json").write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
from safetensors.torch import save_file
state = {name: value.detach().cpu().contiguous() for name, value in self.state_dict().items()}
save_file(state, str(folder / "model.safetensors"))
@classmethod
def from_pretrained(cls, path_or_repo, **_ignored):
"""Load Mini from a local hub folder or a Hugging Face repo id.
trust_remote_code is accepted and ignored. Import this class from
model.py, then call from_pretrained with the repo id.
"""
folder = resolve_pretrained_folder(path_or_repo)
raw = json.loads((folder / "config.json").read_text(encoding="utf-8"))
missing = [key for key in CONFIG_KEYS if key not in raw]
if missing:
raise FileNotFoundError(f"{folder / 'config.json'} is missing {', '.join(missing)}")
model = cls(**{key: raw[key] for key in CONFIG_KEYS})
from safetensors.torch import load_file
model.load_state_dict(load_file(str(folder / "model.safetensors")))
model.eval()
return model