MiniSEAL / src /model /llm.py
aaronr43210's picture
Upload src
27d37fd verified
Raw History Blame Contribute Delete
5 kB
from typing import Optional
"""
Decoder-only Transformer language model.
Training objective:
Next-token prediction.
Input:
input_ids: [batch, seq]
Output:
logits: [batch, seq, vocab]
loss: optional cross-entropy loss if targets are given.
"""
import math
from dataclasses import asdict
import torch
import torch.nn as nn
import torch.nn.functional as F
from src.model.config import ModelConfig
from src.model.layers import RMSNorm, TransformerBlock
class DecoderOnlyTransformer(nn.Module):
"""
GPT/LLaMA-style decoder-only language model.
"""
def __init__(self, config: ModelConfig):
super().__init__()
self.config = config
self.tok_embeddings = nn.Embedding(config.vocab_size, config.n_embd)
self.dropout = nn.Dropout(config.dropout)
self.blocks = nn.ModuleList(
[TransformerBlock(config) for _ in range(config.n_layer)]
)
self.norm = RMSNorm(config.n_embd, config.norm_eps)
self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
self.apply(self._init_weights)
# Weight tying: common in GPT models.
self.lm_head.weight = self.tok_embeddings.weight
# GPT-style scaled residual initialization.
for name, param in self.named_parameters():
if name.endswith("out_proj.weight") or name.endswith("down_proj.weight"):
nn.init.normal_(
param,
mean=0.0,
std=0.02 / math.sqrt(2 * config.n_layer),
)
def _init_weights(self, module: nn.Module) -> None:
"""
Initializes model weights.
"""
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: torch.Tensor,
targets: Optional[torch.Tensor] = None,
):
"""
Input:
input_ids: [batch, seq]
targets: [batch, seq], optional
Output:
logits: [batch, seq, vocab]
loss: scalar tensor if targets are provided.
"""
batch, 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}"
)
x = self.tok_embeddings(input_ids)
x = self.dropout(x)
for block in self.blocks:
x = block(x)
x = self.norm(x)
logits = self.lm_head(x)
loss = None
if targets is not None:
loss = F.cross_entropy(
logits.reshape(-1, logits.size(-1)),
targets.reshape(-1),
)
return logits, loss
@torch.no_grad()
def generate(
self,
input_ids: torch.Tensor,
max_new_tokens: int,
temperature: float = 1.0,
top_k: Optional[int] = None,
eos_token_id: Optional[int] = None,
) -> torch.Tensor:
"""
Input:
input_ids: [batch, seq]
max_new_tokens: number of tokens to generate.
temperature: sampling temperature.
top_k: keep only top-k logits if provided.
eos_token_id: stop generation if this token is produced.
Output:
Generated token IDs: [batch, seq + max_new_tokens]
"""
self.eval()
# Track which sequences in the batch have already emitted EOS so we can
# stop early only once *every* sequence is finished (single-example
# callers get the same behavior with batch size 1).
batch = input_ids.size(0)
finished = torch.zeros(batch, dtype=torch.bool, device=input_ids.device)
for _ in range(max_new_tokens):
context = input_ids[:, -self.config.block_size:]
logits, _ = self(context)
logits = logits[:, -1, :] / temperature
if top_k is not None:
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 eos_token_id is not None:
finished = finished | (next_token.squeeze(-1) == eos_token_id)
if bool(finished.all()):
break
return input_ids
def get_num_params(self) -> int:
"""
Output:
Number of trainable model parameters.
"""
return sum(p.numel() for p in self.parameters() if p.requires_grad)
def config_dict(self) -> dict:
return asdict(self.config)