Spaces:
Running
Running
| """ | |
| GOAT-GPT Nano v5 β Hugging Face Spaces Demo | |
| βββββββββββββββββββββββββββββββββββββββββββββ | |
| Loads the trained checkpoint (goat_gpt_nano_gen.pt) and serves a simple | |
| text-generation UI with Gradio. | |
| Expected files in the Space repo root: | |
| - app.py (this file) | |
| - requirements.txt | |
| - goat_gpt_nano_gen.pt (your trained checkpoint β upload this too) | |
| The checkpoint is expected to be a dict with: | |
| {'model_state_dict': ..., 'model_config': {...}} | |
| as produced by `save_model_for_hf` / the generation-checkpoint save in the | |
| training script. | |
| """ | |
| import math | |
| import os | |
| from dataclasses import dataclass | |
| from typing import Optional, Tuple | |
| import gradio as gr | |
| import torch | |
| import torch.nn as nn | |
| from torch.nn import functional as F | |
| from transformers import AutoTokenizer | |
| CHECKPOINT_PATH = os.environ.get("CHECKPOINT_PATH", "goat_gpt_nano_gen.pt") | |
| TOKENIZER_NAME = os.environ.get("TOKENIZER_NAME", "NousResearch/Llama-2-7b-hf") | |
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # MODEL DEFINITION (must match the architecture used for training β v5) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class ModelConfig: | |
| block_size: int = 512 | |
| vocab_size: int = 32000 | |
| n_layer: int = 10 | |
| n_head: int = 10 | |
| n_embd: int = 640 | |
| intermediate_size: int = 1707 | |
| dropout: float = 0.0 | |
| bias: bool = False | |
| rope_theta: float = 10000.0 | |
| use_qk_norm: bool = True | |
| tie_weights: bool = True | |
| class RotaryEmbedding(nn.Module): | |
| def __init__(self, dim: int, max_seq_len: int = 2048, theta: float = 10000.0): | |
| super().__init__() | |
| inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2).float() / dim)) | |
| self.register_buffer("inv_freq", inv_freq) | |
| t = torch.arange(max_seq_len, device=inv_freq.device, dtype=inv_freq.dtype) | |
| freqs = torch.outer(t, inv_freq) | |
| emb = torch.cat((freqs, freqs), dim=-1) | |
| self.register_buffer("cos_cached", emb.cos()[None, None, :, :]) | |
| self.register_buffer("sin_cached", emb.sin()[None, None, :, :]) | |
| def forward(self, x, seq_len: int) -> Tuple[torch.Tensor, torch.Tensor]: | |
| return self.cos_cached[:, :, :seq_len, :], self.sin_cached[:, :, :seq_len, :] | |
| def rotate_half(x): | |
| x1, x2 = x.chunk(2, dim=-1) | |
| return torch.cat((-x2, x1), dim=-1) | |
| def apply_rotary_pos_emb(q, k, cos, sin): | |
| return (q * cos) + (rotate_half(q) * sin), (k * cos) + (rotate_half(k) * sin) | |
| class RMSNorm(nn.Module): | |
| def __init__(self, dim: int, eps: float = 1e-6): | |
| super().__init__() | |
| self.eps = eps | |
| self.weight = nn.Parameter(torch.ones(dim)) | |
| def forward(self, x): | |
| return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) * self.weight | |
| class MultiHeadAttention(nn.Module): | |
| def __init__(self, config: ModelConfig): | |
| super().__init__() | |
| assert config.n_embd % config.n_head == 0 | |
| self.n_head = config.n_head | |
| self.head_dim = config.n_embd // config.n_head | |
| self.use_qk_norm = config.use_qk_norm | |
| self.qkv_proj = nn.Linear(config.n_embd, 3 * config.n_embd, bias=config.bias) | |
| self.o_proj = nn.Linear(config.n_embd, config.n_embd, bias=config.bias) | |
| self.rotary = RotaryEmbedding(self.head_dim, max_seq_len=config.block_size, theta=config.rope_theta) | |
| if self.use_qk_norm: | |
| self.q_norm = RMSNorm(self.head_dim, eps=1e-6) | |
| self.k_norm = RMSNorm(self.head_dim, eps=1e-6) | |
| def forward(self, x): | |
| B, T, C = x.size() | |
| qkv = self.qkv_proj(x) | |
| q, k, v = qkv.split(C, 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) | |
| cos, sin = self.rotary(q, seq_len=T) | |
| q, k = apply_rotary_pos_emb(q, k, cos, sin) | |
| if self.use_qk_norm: | |
| q, k = self.q_norm(q), self.k_norm(k) | |
| y = F.scaled_dot_product_attention(q, k, v, is_causal=True) | |
| y = y.transpose(1, 2).contiguous().view(B, T, C) | |
| return self.o_proj(y) | |
| class SwiGLU(nn.Module): | |
| def __init__(self, config: ModelConfig): | |
| super().__init__() | |
| self.gate_proj = nn.Linear(config.n_embd, config.intermediate_size, bias=config.bias) | |
| self.up_proj = nn.Linear(config.n_embd, config.intermediate_size, bias=config.bias) | |
| self.down_proj = nn.Linear(config.intermediate_size, config.n_embd, bias=config.bias) | |
| def forward(self, x): | |
| return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x)) | |
| class TransformerBlock(nn.Module): | |
| def __init__(self, config: ModelConfig, layer_idx: int): | |
| super().__init__() | |
| self.layer_idx = layer_idx | |
| self.attn = MultiHeadAttention(config) | |
| self.input_norm = RMSNorm(config.n_embd) | |
| self.post_attn_norm = RMSNorm(config.n_embd) | |
| self.mlp = SwiGLU(config) | |
| def forward(self, x): | |
| h = x + self.attn(self.input_norm(x)) | |
| h = h + self.mlp(self.post_attn_norm(h)) | |
| return h | |
| class GOATGPT(nn.Module): | |
| def __init__(self, config: ModelConfig): | |
| super().__init__() | |
| self.config = config | |
| self.transformer = nn.ModuleDict({ | |
| 'wte': nn.Embedding(config.vocab_size, config.n_embd), | |
| 'h': nn.ModuleList([TransformerBlock(config, i) for i in range(config.n_layer)]), | |
| 'ln_f': RMSNorm(config.n_embd), | |
| }) | |
| self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=config.bias) | |
| if config.tie_weights: | |
| self.transformer['wte'].weight = self.lm_head.weight | |
| def forward(self, idx, targets=None): | |
| b, t = idx.size() | |
| x = self.transformer['wte'](idx) | |
| 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), ignore_index=-1) | |
| return logits, loss | |
| def generate(self, idx, max_new_tokens, temperature=1.0, top_k=None, top_p=None): | |
| for _ in range(max_new_tokens): | |
| idx_cond = idx if idx.size(1) <= self.config.block_size else idx[:, -self.config.block_size:] | |
| logits, _ = self(idx_cond) | |
| logits = logits[:, -1, :] / max(temperature, 1e-5) | |
| if top_k is not None: | |
| v, _ = torch.topk(logits, min(top_k, logits.size(-1))) | |
| logits[logits < v[:, [-1]]] = float('-inf') | |
| if top_p is not None: | |
| sorted_logits, sorted_indices = torch.sort(logits, descending=True) | |
| cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1) | |
| sorted_indices_to_remove = cumulative_probs > top_p | |
| sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone() | |
| sorted_indices_to_remove[..., 0] = 0 | |
| indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove) | |
| logits[indices_to_remove] = float('-inf') | |
| probs = F.softmax(logits, dim=-1) | |
| idx_next = torch.multinomial(probs, num_samples=1) | |
| idx = torch.cat((idx, idx_next), dim=1) | |
| return idx | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # LOAD MODEL + TOKENIZER (once, at startup) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print(f"[Startup] Device: {DEVICE}") | |
| print(f"[Startup] Loading checkpoint from {CHECKPOINT_PATH} ...") | |
| if not os.path.exists(CHECKPOINT_PATH): | |
| raise FileNotFoundError( | |
| f"Checkpoint not found at '{CHECKPOINT_PATH}'. " | |
| f"Upload your trained .pt file to the Space and set CHECKPOINT_PATH " | |
| f"if it's not named goat_gpt_nano_gen.pt." | |
| ) | |
| checkpoint = torch.load(CHECKPOINT_PATH, map_location=DEVICE) | |
| cfg_dict = checkpoint.get("model_config", {}) | |
| model_config = ModelConfig(**{k: v for k, v in cfg_dict.items() if k in ModelConfig.__dataclass_fields__}) | |
| model = GOATGPT(model_config) | |
| state_dict = checkpoint.get("model_state_dict", checkpoint.get("model")) | |
| model.load_state_dict(state_dict) | |
| model.to(DEVICE) | |
| model.eval() | |
| print(f"[Startup] Model loaded: {model_config.n_layer}L/{model_config.n_head}H/{model_config.n_embd}D") | |
| print(f"[Startup] Loading tokenizer: {TOKENIZER_NAME}") | |
| tokenizer = AutoTokenizer.from_pretrained(TOKENIZER_NAME) | |
| tokenizer.pad_token = tokenizer.eos_token | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # GENERATION FN | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_text(prompt, max_new_tokens, temperature, top_k, top_p): | |
| if not prompt or not prompt.strip(): | |
| return "Please enter a prompt." | |
| input_ids = tokenizer.encode(prompt, return_tensors="pt").to(DEVICE) | |
| top_k = int(top_k) if top_k and top_k > 0 else None | |
| top_p = float(top_p) if top_p and top_p < 1.0 else None | |
| output_ids = model.generate( | |
| input_ids, | |
| max_new_tokens=int(max_new_tokens), | |
| temperature=float(temperature), | |
| top_k=top_k, | |
| top_p=top_p, | |
| ) | |
| return tokenizer.decode(output_ids[0], skip_special_tokens=True) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # GRADIO UI | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Blocks(title="AM-64M") as demo: | |
| gr.Markdown( | |
| "# AM-64M\n" | |
| "A small from-scratch language model (RoPE, QK-Norm, RMSNorm, SwiGLU) " | |
| "trained on TinyTextbooks. Enter a prompt and generate a continuation." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| prompt = gr.Textbox( | |
| label="Prompt", | |
| value="The theory of relativity states that", | |
| lines=4, | |
| ) | |
| generate_btn = gr.Button("Generate", variant="primary") | |
| output = gr.Textbox(label="Output", lines=10) | |
| with gr.Column(scale=1): | |
| max_new_tokens = gr.Slider(8, 512, value=128, step=8, label="Max new tokens") | |
| temperature = gr.Slider(0.1, 2.0, value=0.8, step=0.05, label="Temperature") | |
| top_k = gr.Slider(0, 100, value=40, step=1, label="Top-k (0 = disabled)") | |
| top_p = gr.Slider(0.0, 1.0, value=1.0, step=0.01, label="Top-p (1.0 = disabled)") | |
| generate_btn.click( | |
| fn=generate_text, | |
| inputs=[prompt, max_new_tokens, temperature, top_k, top_p], | |
| outputs=output, | |
| ) | |
| gr.Examples( | |
| examples=[ | |
| "The theory of relativity states that", | |
| "In the early history of computing,", | |
| "Photosynthesis is the process by which", | |
| ], | |
| inputs=prompt, | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue().launch() | |