shakespeare-GPT / model.py
Deepjyoti's picture
Upload folder using huggingface_hub
f0b317c verified
Raw
History Blame Contribute Delete
16.4 kB
"""Standalone GPT implementation for inference only.
This is a self-contained copy of the trained model architecture. It carries
no training code and matches the exact tensor names stored in
``model.safetensors``.
Expected weights (loaded with ``strict=True``) include keys such as:
input_embedding.token_embedding.embedding.weight
input_embedding.position_embedding.embedding.weight
blocks.<i>.attention_norm.{weight,bias}
blocks.<i>.multi_head_attention.qkv_projection.weight
blocks.<i>.multi_head_attention.output_projection.weight
blocks.<i>.feed_forward_norm.{weight,bias}
blocks.<i>.feed_forward.input_projection.{weight,bias}
blocks.<i>.feed_forward.output_projection.{weight,bias}
final_norm.{weight,bias}
lm_head.weight
"""
import torch
import torch.nn.functional as F
from torch import nn
from tokenizer import CharacterTokenizer
_LOGIT_MASK_VALUE = float("-inf")
class GPTConfig:
"""Configuration for the Shakespeare GPT model."""
def __init__(
self,
*,
vocab_size: int,
max_sequence_length: int,
embedding_dim: int,
num_heads: int,
num_layers: int,
dropout_probability: float = 0.0,
bias: bool = False,
initializer_range: float = 0.02,
) -> None:
self.vocab_size = vocab_size
self.max_sequence_length = max_sequence_length
self.embedding_dim = embedding_dim
self.num_heads = num_heads
self.num_layers = num_layers
self.dropout_probability = dropout_probability
self.bias = bias
self.initializer_range = initializer_range
@classmethod
def from_json(
cls,
data: dict,
) -> "GPTConfig":
return cls(
vocab_size=data["vocab_size"],
max_sequence_length=data["max_sequence_length"],
embedding_dim=data["embedding_dim"],
num_heads=data["num_heads"],
num_layers=data["num_layers"],
dropout_probability=data.get("dropout_probability", 0.0),
bias=data.get("bias", False),
initializer_range=data.get("initializer_range", 0.02),
)
def to_json(self) -> dict:
return {
"model_type": "shakespeare-gpt",
"vocab_size": self.vocab_size,
"max_sequence_length": self.max_sequence_length,
"embedding_dim": self.embedding_dim,
"num_heads": self.num_heads,
"num_layers": self.num_layers,
"dropout_probability": self.dropout_probability,
"bias": self.bias,
"initializer_range": self.initializer_range,
}
class TokenEmbedding(nn.Module):
def __init__(
self,
vocab_size: int,
embedding_dim: int,
) -> None:
super().__init__()
self.embedding = nn.Embedding(
num_embeddings=vocab_size,
embedding_dim=embedding_dim,
)
def forward(self, token_ids: torch.Tensor) -> torch.Tensor:
return self.embedding(token_ids)
class PositionEmbedding(nn.Module):
def __init__(
self,
max_sequence_length: int,
embedding_dim: int,
) -> None:
super().__init__()
self.embedding = nn.Embedding(
num_embeddings=max_sequence_length,
embedding_dim=embedding_dim,
)
def forward(self, position_ids: torch.Tensor) -> torch.Tensor:
return self.embedding(position_ids)
class InputEmbedding(nn.Module):
def __init__(
self,
*,
config: GPTConfig,
) -> None:
super().__init__()
self.token_embedding = TokenEmbedding(
vocab_size=config.vocab_size,
embedding_dim=config.embedding_dim,
)
self.position_embedding = PositionEmbedding(
max_sequence_length=config.max_sequence_length,
embedding_dim=config.embedding_dim,
)
self.dropout = nn.Dropout(config.dropout_probability)
def forward(self, token_ids: torch.Tensor) -> torch.Tensor:
_, sequence_length = token_ids.shape
position_ids = torch.arange(
sequence_length,
device=token_ids.device,
)
token_embeddings = self.token_embedding(token_ids)
position_embeddings = self.position_embedding(position_ids)
input_embeddings = token_embeddings + position_embeddings
return self.dropout(input_embeddings)
class MultiHeadAttention(nn.Module):
def __init__(
self,
*,
config: GPTConfig,
) -> None:
super().__init__()
if config.embedding_dim % config.num_heads != 0:
raise ValueError("embedding_dim must be divisible by num_heads")
self.embedding_dim = config.embedding_dim
self.num_heads = config.num_heads
self.head_dim = config.embedding_dim // config.num_heads
self.qkv_projection = nn.Linear(
in_features=config.embedding_dim,
out_features=3 * config.embedding_dim,
bias=config.bias,
)
self.output_projection = nn.Linear(
in_features=config.embedding_dim,
out_features=config.embedding_dim,
bias=config.bias,
)
self.attention_dropout = nn.Dropout(config.dropout_probability)
self.output_dropout = nn.Dropout(config.dropout_probability)
def _split_heads(
self,
tensor: torch.Tensor,
*,
batch_size: int,
sequence_length: int,
) -> torch.Tensor:
return tensor.view(
batch_size,
sequence_length,
self.num_heads,
self.head_dim,
).transpose(1, 2)
def forward(
self,
hidden_states: torch.Tensor,
) -> torch.Tensor:
batch_size, sequence_length, _ = hidden_states.shape
query, key, value = self.qkv_projection(hidden_states).split(
self.embedding_dim,
dim=-1,
)
query = self._split_heads(
query,
batch_size=batch_size,
sequence_length=sequence_length,
)
key = self._split_heads(
key,
batch_size=batch_size,
sequence_length=sequence_length,
)
value = self._split_heads(
value,
batch_size=batch_size,
sequence_length=sequence_length,
)
attention_output = F.scaled_dot_product_attention(
query=query,
key=key,
value=value,
attn_mask=None,
dropout_p=(self.attention_dropout.p if self.training else 0.0),
is_causal=True,
)
attention_output = (
attention_output.transpose(1, 2)
.contiguous()
.view(
batch_size,
sequence_length,
self.embedding_dim,
)
)
attention_output = self.output_projection(attention_output)
return self.output_dropout(attention_output)
class FeedForward(nn.Module):
def __init__(
self,
*,
config: GPTConfig,
) -> None:
super().__init__()
hidden_dim = 4 * config.embedding_dim
self.input_projection = nn.Linear(
in_features=config.embedding_dim,
out_features=hidden_dim,
)
self.gelu = nn.GELU()
self.output_projection = nn.Linear(
in_features=hidden_dim,
out_features=config.embedding_dim,
)
self.dropout = nn.Dropout(config.dropout_probability)
def forward(
self,
hidden_states: torch.Tensor,
) -> torch.Tensor:
hidden_states = self.input_projection(hidden_states)
hidden_states = self.gelu(hidden_states)
hidden_states = self.output_projection(hidden_states)
return self.dropout(hidden_states)
class TransformerBlock(nn.Module):
def __init__(
self,
*,
config: GPTConfig,
) -> None:
super().__init__()
self.attention_norm = nn.LayerNorm(normalized_shape=config.embedding_dim)
self.multi_head_attention = MultiHeadAttention(config=config)
self.feed_forward_norm = nn.LayerNorm(normalized_shape=config.embedding_dim)
self.feed_forward = FeedForward(config=config)
def forward(
self,
hidden_states: torch.Tensor,
) -> torch.Tensor:
residual = hidden_states
hidden_states = self.attention_norm(hidden_states)
hidden_states = self.multi_head_attention(hidden_states)
hidden_states = residual + hidden_states
residual = hidden_states
hidden_states = self.feed_forward_norm(hidden_states)
hidden_states = self.feed_forward(hidden_states)
hidden_states = residual + hidden_states
return hidden_states
class GPT(nn.Module):
"""Generative pre-trained transformer for character-level text generation."""
def __init__(
self,
*,
config: GPTConfig,
) -> None:
super().__init__()
self.config = config
self.vocab_size = config.vocab_size
self.input_embedding = InputEmbedding(config=config)
self.blocks = nn.ModuleList(
[TransformerBlock(config=config) for _ in range(config.num_layers)]
)
self.final_norm = nn.LayerNorm(normalized_shape=config.embedding_dim)
self.lm_head = nn.Linear(
in_features=config.embedding_dim,
out_features=config.vocab_size,
bias=False,
)
self.apply(self._init_weights)
self._tie_weights()
def _init_weights(
self,
module: nn.Module,
) -> None:
if isinstance(module, nn.Linear):
nn.init.normal_(
module.weight,
mean=0.0,
std=self.config.initializer_range,
)
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=self.config.initializer_range,
)
def _tie_weights(self) -> None:
self.lm_head.weight = self.input_embedding.token_embedding.embedding.weight
def forward(
self,
token_ids: torch.Tensor,
targets: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
hidden_states = self.input_embedding(token_ids)
for block in self.blocks:
hidden_states = block(hidden_states)
hidden_states = self.final_norm(hidden_states)
logits = self.lm_head(hidden_states)
loss: torch.Tensor | None = None
if targets is not None:
flattened_logits = logits.reshape(-1, self.vocab_size)
flattened_targets = targets.reshape(-1)
loss = F.cross_entropy(
flattened_logits,
flattened_targets,
)
return logits, loss
def _apply_temperature(
self,
logits: torch.Tensor,
*,
temperature: float,
) -> torch.Tensor:
if temperature == 0.0:
return logits
return logits / temperature
def _apply_top_k(
self,
logits: torch.Tensor,
*,
top_k: int | None,
) -> torch.Tensor:
if top_k is None:
return logits
k = min(top_k, self.vocab_size)
values, _ = torch.topk(logits, k=k)
threshold = values[:, -1].unsqueeze(-1)
return logits.masked_fill(logits < threshold, _LOGIT_MASK_VALUE)
def _apply_top_p(
self,
logits: torch.Tensor,
*,
top_p: float | None,
) -> torch.Tensor:
if top_p is None:
return logits
sorted_logits, sorted_indices = torch.sort(
logits,
descending=True,
dim=-1,
)
sorted_probabilities = torch.softmax(sorted_logits, dim=-1)
cumulative_probabilities = torch.cumsum(sorted_probabilities, dim=-1)
sorted_mask = cumulative_probabilities > top_p
sorted_mask[..., 1:] = sorted_mask[..., :-1].clone()
sorted_mask[..., 0] = False
sorted_logits = sorted_logits.masked_fill(
sorted_mask,
_LOGIT_MASK_VALUE,
)
filtered_logits = torch.full_like(logits, _LOGIT_MASK_VALUE)
filtered_logits.scatter_(
dim=-1,
index=sorted_indices,
src=sorted_logits,
)
return filtered_logits
def _prepare_logits(
self,
logits: torch.Tensor,
*,
temperature: float,
top_k: int | None,
top_p: float | None,
) -> torch.Tensor:
logits = self._apply_temperature(
logits=logits,
temperature=temperature,
)
logits = self._apply_top_k(
logits=logits,
top_k=top_k,
)
logits = self._apply_top_p(
logits=logits,
top_p=top_p,
)
return logits
def _sample_next_token(
self,
logits: torch.Tensor,
*,
temperature: float,
) -> torch.Tensor:
if temperature == 0.0:
return torch.argmax(logits, dim=-1)
probabilities = torch.softmax(logits, dim=-1)
return torch.multinomial(probabilities, num_samples=1).squeeze(-1)
@torch.no_grad()
def generate(
self,
*,
token_ids: torch.Tensor,
max_new_tokens: int,
temperature: float = 1.0,
top_k: int | None = None,
top_p: float | None = None,
) -> torch.Tensor:
if temperature < 0:
raise ValueError("temperature must be greater than or equal to 0.")
if top_k is not None and top_k <= 0:
raise ValueError("top_k must be greater than 0.")
if top_p is not None and not (0.0 < top_p <= 1.0):
raise ValueError("top_p must be in the range (0, 1].")
if max_new_tokens <= 0:
raise ValueError("max_new_tokens must be greater than 0.")
was_training = self.training
self.eval()
try:
for _ in range(max_new_tokens):
input_ids = token_ids[:, -self.config.max_sequence_length :]
logits, _ = self(token_ids=input_ids)
logits = logits[:, -1, :]
logits = self._prepare_logits(
logits=logits,
temperature=temperature,
top_k=top_k,
top_p=top_p,
)
next_token = self._sample_next_token(
logits,
temperature=temperature,
)
token_ids = torch.cat(
(
token_ids,
next_token.unsqueeze(-1),
),
dim=-1,
)
finally:
if was_training:
self.train()
return token_ids
def load_model_weights(
model: GPT,
path: str,
*,
device: torch.device | str = "cpu",
) -> None:
from safetensors.torch import load_file
state = load_file(
path,
device=str(device),
)
model.load_state_dict(
state,
strict=True,
)
def parameter_count(
model: GPT,
) -> int:
return sum(parameter.numel() for parameter in model.parameters() if parameter.requires_grad)
def generate_text(
*,
model: GPT,
tokenizer: CharacterTokenizer,
prompt: str,
max_new_tokens: int,
temperature: float = 1.0,
top_k: int | None = None,
top_p: float | None = None,
device: torch.device | str,
) -> str:
token_ids = tokenizer.encode(prompt)
token_ids_tensor = torch.tensor(
token_ids,
dtype=torch.long,
device=device,
).unsqueeze(0)
generated = model.generate(
token_ids=token_ids_tensor,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_k=top_k,
top_p=top_p,
)
return tokenizer.decode(generated[0].tolist())