Download model.py from ableian/scgpt: direct link, hf CLI and curl.
- Browser
- Download file 15.7 kB
-
https://huggingface.co/spaces/ableian/scgpt/resolve/main/model.py
- Command line
-
hf download hf://spaces/ableian/scgpt/model.py
-
curl -L -o model.py https://huggingface.co/spaces/ableian/scgpt/resolve/main/model.py
15.7 kB
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| import math | |
| import json | |
| import os | |
| from dataclasses import dataclass, asdict | |
| VOCAB_SIZE = 50257 | |
| BLOCK_SIZE = 1024 | |
| LAYERS = 12 | |
| EMBEDDING_DIM = 768 | |
| NUM_HEADS = 12 | |
| DROPOUT = 0.1 | |
| class GPTConfig: | |
| vocab_size: int = VOCAB_SIZE | |
| block_size: int = BLOCK_SIZE | |
| n_layers: int = LAYERS | |
| n_embd: int = EMBEDDING_DIM | |
| n_heads: int = NUM_HEADS | |
| dropout: float = DROPOUT | |
| class CausalSelfAttention(nn.Module): | |
| def __init__(self, config: GPTConfig): | |
| super().__init__() | |
| if config.n_embd % config.n_heads != 0: | |
| raise ValueError("n_embd must be divisible by n_heads") | |
| self.n_heads = config.n_heads | |
| self.head_dim = config.n_embd // config.n_heads | |
| self.dropout = config.dropout | |
| self.qkv = nn.Linear(config.n_embd, 3 * config.n_embd) | |
| self.out_proj = nn.Linear(config.n_embd, config.n_embd) | |
| self.attn_dropout = nn.Dropout(config.dropout) | |
| self.resid_dropout = nn.Dropout(config.dropout) | |
| causal_mask = torch.tril(torch.ones(config.block_size, config.block_size, dtype=torch.bool)) | |
| self.register_buffer("causal_mask", causal_mask, persistent=False) | |
| def forward(self, x, attention_mask=None): | |
| batch_size, seq_len, channels = x.size() | |
| qkv = self.qkv(x) | |
| q, k, v = qkv.split(channels, dim=2) | |
| q = q.view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) | |
| k = k.view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) | |
| v = v.view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) | |
| att = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim) | |
| causal = self.causal_mask[:seq_len, :seq_len].view(1, 1, seq_len, seq_len) | |
| if attention_mask is not None: | |
| key_mask = attention_mask[:, None, None, :].bool() | |
| combined_mask = causal & key_mask | |
| else: | |
| combined_mask = causal | |
| # Use finite mask values to avoid NaNs on fully masked rows, then zero masked probs. | |
| mask_value = torch.finfo(att.dtype).min | |
| att = att.masked_fill(~combined_mask, mask_value) | |
| att = F.softmax(att, dim=-1) | |
| att = att.masked_fill(~combined_mask, 0.0) | |
| att = self.attn_dropout(att) | |
| y = att @ v | |
| y = y.transpose(1, 2).contiguous().view(batch_size, seq_len, channels) | |
| y = self.resid_dropout(self.out_proj(y)) | |
| return y | |
| class MLP(nn.Module): | |
| def __init__(self, config: GPTConfig): | |
| super().__init__() | |
| self.fc_in = nn.Linear(config.n_embd, 4 * config.n_embd) | |
| self.fc_out = nn.Linear(4 * config.n_embd, config.n_embd) | |
| self.dropout = nn.Dropout(config.dropout) | |
| def forward(self, x): | |
| x = self.fc_in(x) | |
| x = F.gelu(x) | |
| x = self.fc_out(x) | |
| return self.dropout(x) | |
| class TransformerBlock(nn.Module): | |
| def __init__(self, config: GPTConfig): | |
| super().__init__() | |
| self.ln_1 = nn.LayerNorm(config.n_embd) | |
| self.attn = CausalSelfAttention(config) | |
| self.ln_2 = nn.LayerNorm(config.n_embd) | |
| self.mlp = MLP(config) | |
| def forward(self, x, attention_mask=None): | |
| x = x + self.attn(self.ln_1(x), attention_mask=attention_mask) | |
| x = x + self.mlp(self.ln_2(x)) | |
| return x | |
| class GPTModel(nn.Module): | |
| def __init__(self, config: GPTConfig): | |
| super().__init__() | |
| self.config = config | |
| self.token_emb = nn.Embedding(config.vocab_size, config.n_embd) | |
| self.pos_emb = nn.Embedding(config.block_size, config.n_embd) | |
| self.drop = nn.Dropout(config.dropout) | |
| self.blocks = nn.ModuleList([TransformerBlock(config) for _ in range(config.n_layers)]) | |
| self.ln_f = nn.LayerNorm(config.n_embd) | |
| self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False) | |
| self.lm_head.weight = self.token_emb.weight | |
| self.apply(self._init_weights) | |
| def _init_weights(self, module): | |
| if isinstance(module, nn.Linear): | |
| nn.init.normal_(module.weight, mean=0.0, std=0.02) | |
| if module.bias is not None: | |
| nn.init.zeros_(module.bias) | |
| elif isinstance(module, nn.Embedding): | |
| nn.init.normal_(module.weight, mean=0.0, std=0.02) | |
| def forward(self, input_ids, attention_mask=None, labels=None): | |
| batch_size, seq_len = input_ids.shape | |
| if seq_len > self.config.block_size: | |
| raise ValueError(f"Sequence length {seq_len} exceeds block size {self.config.block_size}") | |
| pos = torch.arange(0, seq_len, device=input_ids.device).unsqueeze(0) | |
| x = self.token_emb(input_ids) + self.pos_emb(pos) | |
| x = self.drop(x) | |
| for block in self.blocks: | |
| x = block(x, attention_mask=attention_mask) | |
| x = self.ln_f(x) | |
| logits = self.lm_head(x) | |
| loss = None | |
| if labels is not None: | |
| loss = F.cross_entropy( | |
| logits.view(-1, logits.size(-1)), | |
| labels.view(-1), | |
| ignore_index=-100, | |
| ) | |
| return logits, loss | |
| def generate(self, input_ids, attention_mask=None, max_new_tokens=50, temperature=1.0, top_k=None, top_p=0.95, eos_token_id=None): | |
| self.eval() | |
| for _ in range(max_new_tokens): | |
| idx_cond = input_ids[:, -self.config.block_size :] | |
| if attention_mask is not None: | |
| mask_cond = attention_mask[:, -self.config.block_size :] | |
| else: | |
| mask_cond = None | |
| logits, _ = self(idx_cond, attention_mask=mask_cond) | |
| logits = logits[:, -1, :] / max(temperature, 1e-6) | |
| # Use one filtering strategy at a time to avoid ambiguous distributions. | |
| if top_p is not None and 0 < top_p < 1.0: | |
| sorted_logits, sorted_indices = torch.sort(logits, descending=True) | |
| probs = F.softmax(sorted_logits, dim=-1) | |
| cumulative = torch.cumsum(probs, dim=-1) | |
| sorted_mask = cumulative > top_p | |
| sorted_mask[..., 1:] = sorted_mask[..., :-1].clone() | |
| sorted_mask[..., 0] = False | |
| sorted_logits = sorted_logits.masked_fill(sorted_mask, -float("inf")) | |
| logits = torch.full_like(logits, -float("inf")) | |
| logits.scatter_(1, sorted_indices, sorted_logits) | |
| elif top_k is not None and top_k > 0: | |
| values, _ = torch.topk(logits, min(top_k, logits.size(-1))) | |
| logits[logits < values[:, [-1]]] = -float("inf") | |
| probs = F.softmax(logits, dim=-1) | |
| next_token = torch.multinomial(probs, num_samples=1) | |
| input_ids = torch.cat((input_ids, next_token), dim=1) | |
| if attention_mask is not None: | |
| attention_mask = torch.cat( | |
| (attention_mask, torch.ones_like(next_token, device=attention_mask.device)), | |
| dim=1, | |
| ) | |
| if eos_token_id is not None and torch.all(next_token == eos_token_id): | |
| break | |
| return input_ids | |
| class miniGPT: | |
| def __init__(self, model, tokenizer): | |
| self.model = model | |
| self.tokenizer = tokenizer | |
| if self.tokenizer.pad_token_id is None: | |
| self.tokenizer.pad_token = self.tokenizer.eos_token | |
| def from_config(cls, config: GPTConfig, tokenizer): | |
| model = GPTModel(config) | |
| return cls(model=model, tokenizer=tokenizer) | |
| def from_tokenizer_name(cls, tokenizer_name="gpt2", config=None): | |
| from transformers import AutoTokenizer | |
| tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) | |
| if tokenizer.pad_token_id is None: | |
| tokenizer.pad_token = tokenizer.eos_token | |
| if config is None: | |
| config = GPTConfig(vocab_size=len(tokenizer)) | |
| else: | |
| config.vocab_size = len(tokenizer) | |
| return cls.from_config(config=config, tokenizer=tokenizer) | |
| def to(self, device): | |
| self.model = self.model.to(device) | |
| return self | |
| def train( | |
| self, | |
| data, | |
| epochs=10, | |
| batch_size=32, | |
| learning_rate=3e-4, | |
| weight_decay=0.1, | |
| grad_clip=1.0, | |
| warmup_ratio=0.03, | |
| min_lr_ratio=0.1, | |
| ): | |
| device = next(self.model.parameters()).device | |
| optimizer = torch.optim.AdamW(self.model.parameters(), lr=learning_rate, weight_decay=weight_decay) | |
| pad_token_id = self.tokenizer.pad_token_id | |
| self.model.train() | |
| steps_per_epoch = max(1, (len(data) + batch_size - 1) // batch_size) | |
| total_steps = max(1, epochs * steps_per_epoch) | |
| warmup_steps = int(total_steps * warmup_ratio) | |
| global_step = 0 | |
| def _set_lr(step): | |
| if warmup_steps > 0 and step < warmup_steps: | |
| scale = step / max(1, warmup_steps) | |
| else: | |
| decay_steps = max(1, total_steps - warmup_steps) | |
| progress = (step - warmup_steps) / decay_steps | |
| progress = min(max(progress, 0.0), 1.0) | |
| cosine = 0.5 * (1.0 + math.cos(math.pi * progress)) | |
| scale = min_lr_ratio + (1.0 - min_lr_ratio) * cosine | |
| current_lr = learning_rate * scale | |
| for group in optimizer.param_groups: | |
| group["lr"] = current_lr | |
| return current_lr | |
| for epoch in range(epochs): | |
| epoch_loss = 0.0 | |
| epoch_batches = 0 | |
| for i in range(0, len(data), batch_size): | |
| batch = data[i : i + batch_size] | |
| inputs = self.tokenizer( | |
| batch, | |
| return_tensors="pt", | |
| padding=True, | |
| truncation=True, | |
| max_length=self.model.config.block_size, | |
| ) | |
| input_ids = inputs["input_ids"].to(device) | |
| attention_mask = inputs["attention_mask"].to(device) | |
| labels = input_ids.clone() | |
| labels[:, :-1] = input_ids[:, 1:] | |
| labels[:, -1] = -100 | |
| labels = labels.masked_fill(input_ids == pad_token_id, -100) | |
| _, loss = self.model(input_ids, attention_mask=attention_mask, labels=labels) | |
| current_lr = _set_lr(global_step) | |
| optimizer.zero_grad(set_to_none=True) | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(self.model.parameters(), grad_clip) | |
| optimizer.step() | |
| global_step += 1 | |
| loss_value = loss.item() | |
| epoch_loss += loss_value | |
| epoch_batches += 1 | |
| avg_loss = epoch_loss / epoch_batches | |
| print( | |
| f"Epoch {epoch + 1}/{epochs}, Batch {i // batch_size + 1}/{steps_per_epoch}, " | |
| f"Loss: {avg_loss:.4f}, LR: {current_lr:.2e}" | |
| ) | |
| if epoch_batches > 0: | |
| print(f"Epoch {epoch + 1} average loss: {epoch_loss / epoch_batches:.4f}") | |
| def generate(self, prompt, max_length=50, temperature=0.9, top_k=None, top_p=0.95): | |
| device = next(self.model.parameters()).device | |
| self.model.eval() | |
| inputs = self.tokenizer(prompt, return_tensors="pt", truncation=True, max_length=self.model.config.block_size) | |
| input_ids = inputs["input_ids"].to(device) | |
| attention_mask = inputs["attention_mask"].to(device) | |
| output_ids = self.model.generate( | |
| input_ids=input_ids, | |
| attention_mask=attention_mask, | |
| max_new_tokens=max_length, | |
| temperature=temperature, | |
| top_k=top_k, | |
| top_p=top_p, | |
| eos_token_id=self.tokenizer.eos_token_id, | |
| ) | |
| return self.tokenizer.decode(output_ids[0], skip_special_tokens=True) | |
| def save(self, output_dir): | |
| os.makedirs(output_dir, exist_ok=True) | |
| with open(os.path.join(output_dir, "config.json"), "w", encoding="utf-8") as config_file: | |
| json.dump(asdict(self.model.config), config_file, indent=2) | |
| torch.save(self.model.state_dict(), os.path.join(output_dir, "weights.pt")) | |
| self.tokenizer.save_pretrained(output_dir) | |
| def load(cls, model_dir, map_location=None): | |
| from transformers import AutoTokenizer | |
| config_path = os.path.join(model_dir, "config.json") | |
| weights_path = os.path.join(model_dir, "weights.pt") | |
| if os.path.exists(config_path) and os.path.exists(weights_path): | |
| with open(config_path, "r", encoding="utf-8") as config_file: | |
| config_dict = json.load(config_file) | |
| # Map HuggingFace field names to our GPTConfig field names | |
| filtered_dict = {} | |
| if 'vocab_size' in config_dict: | |
| filtered_dict['vocab_size'] = config_dict['vocab_size'] | |
| if 'n_ctx' in config_dict: | |
| filtered_dict['block_size'] = config_dict['n_ctx'] | |
| if 'n_layer' in config_dict: | |
| filtered_dict['n_layers'] = config_dict['n_layer'] | |
| if 'n_embd' in config_dict: | |
| filtered_dict['n_embd'] = config_dict['n_embd'] | |
| if 'n_head' in config_dict: | |
| filtered_dict['n_heads'] = config_dict['n_head'] | |
| # Use any available dropout field | |
| if 'resid_pdrop' in config_dict: | |
| filtered_dict['dropout'] = config_dict['resid_pdrop'] | |
| elif 'attn_pdrop' in config_dict: | |
| filtered_dict['dropout'] = config_dict['attn_pdrop'] | |
| elif 'embd_pdrop' in config_dict: | |
| filtered_dict['dropout'] = config_dict['embd_pdrop'] | |
| # Load weights - check if it's a full checkpoint or just state_dict | |
| loaded = torch.load(weights_path, map_location=map_location) | |
| if isinstance(loaded, dict) and "state_dict" in loaded: | |
| state_dict = loaded["state_dict"] | |
| else: | |
| state_dict = loaded | |
| # Infer actual dimensions from state_dict to handle config/weight mismatches | |
| if "token_emb.weight" in state_dict: | |
| actual_embd = state_dict["token_emb.weight"].shape[1] | |
| filtered_dict['n_embd'] = actual_embd | |
| if "pos_emb.weight" in state_dict: | |
| actual_block_size = state_dict["pos_emb.weight"].shape[0] | |
| filtered_dict['block_size'] = actual_block_size | |
| # Count actual layers from state_dict | |
| layer_count = 0 | |
| for key in state_dict.keys(): | |
| if key.startswith("blocks."): | |
| layer_num = int(key.split(".")[1]) | |
| layer_count = max(layer_count, layer_num + 1) | |
| if layer_count > 0: | |
| filtered_dict['n_layers'] = layer_count | |
| config = GPTConfig(**filtered_dict) | |
| else: | |
| checkpoint = torch.load(os.path.join(model_dir, "model.pt"), map_location=map_location) | |
| config = GPTConfig(**checkpoint["config"]) | |
| state_dict = checkpoint["state_dict"] | |
| model = GPTModel(config) | |
| model.load_state_dict(state_dict, strict=False) | |
| tokenizer = AutoTokenizer.from_pretrained(model_dir) | |
| if tokenizer.pad_token_id is None: | |
| tokenizer.pad_token = tokenizer.eos_token | |
| return cls(model=model, tokenizer=tokenizer) |