Download model.py from StrongDev2024/mini: direct link, hf CLI and curl.
- Browser
- Download file 6.98 kB
-
https://huggingface.co/StrongDev2024/mini/resolve/main/model.py
- Command line
-
hf download hf://StrongDev2024/mini/model.py
-
curl -L -o model.py https://huggingface.co/StrongDev2024/mini/resolve/main/model.py
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 | |
| 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")) | |
| 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 | |