Cadence-15M-fr / model /gpt2.py
RDTvlokip's picture
Upload folder using huggingface_hub
9aab684 verified
Raw
History Blame Contribute Delete
46.3 kB
"""GPT-2 implementation from scratch with modern improvements.
Modern features (all optional, backward compatible):
- RMSNorm (instead of LayerNorm)
- RoPE (Rotary Position Embeddings)
- SwiGLU (instead of GELU MLP)
- GQA (Grouped Query Attention)
- QK-Norm (Q/K normalization)
- KV-Cache (faster generation)
"""
import math
import warnings
from dataclasses import dataclass
from typing import Optional, Tuple, List
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.checkpoint
# Suppress harmless flash attention warning on GTX 1080 Ti and older GPUs
# (PyTorch falls back to efficient_attention which is nearly as fast)
warnings.filterwarnings("ignore", message=".*Torch was not compiled with flash attention.*")
@dataclass
class GPT2Config:
"""GPT-2 model configuration with modern improvements."""
# Core architecture
vocab_size: int = 32000
n_positions: int = 1024 # Max sequence length
n_embd: int = 768 # Embedding dimension
n_layer: int = 12 # Number of transformer blocks
n_head: int = 12 # Number of attention heads
n_inner: int = 3072 # FFN hidden dimension
# Label smoothing (0.0 = hard labels, 0.1 = 10% spread to other tokens)
label_smoothing: float = 0.0
# Pad token ID (ignored in loss computation)
pad_token_id: int = 0
# Regularization
embd_pdrop: float = 0.1
resid_pdrop: float = 0.1
attn_pdrop: float = 0.1
# Normalization
layer_norm_epsilon: float = 1e-5
activation_function: str = "gelu"
# Legacy flags (kept for compatibility)
use_rope: bool = False # Rotary Positional Embeddings
use_mqa: bool = False # Multi-Query Attention (use use_gqa + n_kv_heads=1 instead)
# Modern improvement flags
use_rmsnorm: bool = False # Replace LayerNorm with RMSNorm
use_swiglu: bool = False # Replace GELU MLP with SwiGLU
use_gqa: bool = False # Enable Grouped Query Attention
n_kv_heads: Optional[int] = None # Number of KV heads for GQA (default: n_head)
use_qk_norm: bool = False # Apply RMSNorm to Q and K
# Flash Attention (uses F.scaled_dot_product_attention)
use_flash_attention: bool = False
# LayerScale (CaiT, Touvron et al. 2021): a learned per-channel scalar
# gamma multiplies each sub-block output before the residual add, init
# very small so blocks start near-identity and "open up" gradually.
# Stabilizes small models on noisy (hapax) corpora. Near-zero param cost.
use_layerscale: bool = False
layerscale_init: float = 1e-4
# Recurrent-depth / looped transformer (Geiping 2025; Kohli et al. COLM 2026;
# Chen NCU 2026). The n_layer blocks are applied R times in a loop, giving an
# effective depth of n_layer * recurrence WITHOUT adding parameters. A learned
# per-step depth embedding is injected before each iteration so the shared
# block can tell iterations apart. Pairs with use_layerscale + zero-init
# c_proj (block starts as identity) for stable unrolling. recurrence=1 is the
# standard (non-looped) model.
recurrence: int = 1
# Zero-init the residual output projections (attn c_proj + MLP down_proj) so
# each block is an exact identity map at init — critical for stable looping
# (Kohli et al. show default Gaussian init is seed-unstable when looped).
zero_init_residual: bool = False
# Adaptive per-token halting (INFERENCE-time, idea originale Théo): during the
# recurrent loop, freeze tokens whose output entropy drops below a threshold
# (easy tokens like "le","de" stop early; rare/hapax tokens loop the full R).
# Zero added params (entropy computed from existing logits). Only active when
# recurrence > 1. See idee-profondeur-adaptative-par-token.
adaptive_halting: bool = False
halting_entropy_threshold: float = 1.0 # nats; token freezes when H(logits) < this
# Halting threshold mode:
# "absolute" — freeze when entropy < halting_entropy_threshold (fixed nats).
# Simple, but the right value depends on model size (a bigger
# model is more confident → lower entropy → freezes too early).
# "percentile" — freeze the q fraction of still-active tokens with the LOWEST
# entropy each iteration (q = halting_percentile). Recomputes
# the cutoff from the live entropy distribution, so it is
# INVARIANT to model size — q transfers across scales.
halting_mode: str = "absolute" # "absolute" | "percentile"
halting_percentile: float = 0.3 # q: fraction of active tokens to freeze/iter
# Option B: also apply halting DURING TRAINING, so the model learns to give a
# good answer at whatever depth each token halts (easy tokens learn to be good
# at R=1-2, hapax keep looping). Without this, halting-at-inference under-computes
# (the model was only ever good at fixed R). Idea originale Théo.
halting_in_training: bool = False
# Multi-token prediction (Gloeckle et al. / DeepSeek-V3): a 2nd head also
# predicts token t+2, forcing richer representations. Only affects training
# (the extra loss); generation still uses the main t+1 head.
use_multi_token: bool = False
multi_token_weight: float = 0.3 # weight λ of the t+2 loss term
# RoPE configuration
rope_theta: float = 10000.0 # Base frequency for RoPE
rope_scaling: Optional[float] = None # Scaling factor for extended context
def __post_init__(self):
"""Validate configuration."""
assert self.n_embd % self.n_head == 0, "n_embd must be divisible by n_head"
# Set default n_kv_heads
if self.n_kv_heads is None:
self.n_kv_heads = self.n_head
# Validate GQA configuration
if self.use_gqa or self.use_mqa:
assert self.n_head % self.n_kv_heads == 0, "n_head must be divisible by n_kv_heads"
# MQA is a special case of GQA with n_kv_heads=1
if self.use_mqa:
self.use_gqa = True
self.n_kv_heads = 1
# Adjust n_inner for SwiGLU to maintain approximate parameter parity
# SwiGLU has 3 projections vs 2 for standard MLP, so we reduce hidden dim
if self.use_swiglu and self.n_inner == 4 * self.n_embd:
# Standard: 2 * n_embd * n_inner params
# SwiGLU: 3 * n_embd * n_inner params
# For parity: n_inner_swiglu = 2/3 * n_inner_standard
# Round to multiple of 256 for efficiency
self.n_inner = ((2 * self.n_inner // 3 + 255) // 256) * 256
class RMSNorm(nn.Module):
"""Root Mean Square Layer Normalization.
Unlike LayerNorm, RMSNorm:
- Does not center (subtract mean)
- Does not have bias parameter
- Uses RMS for normalization: x / sqrt(mean(x^2) + eps) * weight
Reference: https://arxiv.org/abs/1910.07467
"""
def __init__(self, dim: int, eps: float = 1e-6):
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))
def _norm(self, x: torch.Tensor) -> torch.Tensor:
"""Apply RMS normalization."""
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Forward pass."""
# Cast to float32 for numerical stability, then cast back
output = self._norm(x.float()).type_as(x)
return output * self.weight
class RotaryPositionEmbedding(nn.Module):
"""Rotary Position Embedding (RoPE).
Applies rotation to query and key vectors based on position.
Key properties:
- Encodes relative position through rotation
- No learned parameters
- Naturally decays attention with distance
Reference: https://arxiv.org/abs/2104.09864
"""
def __init__(
self,
dim: int,
max_seq_len: int = 2048,
theta: float = 10000.0,
scaling_factor: Optional[float] = None,
):
super().__init__()
self.dim = dim
self.max_seq_len = max_seq_len
self.theta = theta
self.scaling_factor = scaling_factor
# Precompute frequency bands: theta_i = theta^(-2i/dim)
inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2).float() / dim))
self.register_buffer("inv_freq", inv_freq, persistent=False)
# Build initial cache
self._build_cache(max_seq_len)
def _build_cache(self, seq_len: int):
"""Build cos/sin cache for positions [0, seq_len)."""
self.max_seq_len_cached = seq_len
# Position indices
t = torch.arange(seq_len, device=self.inv_freq.device, dtype=self.inv_freq.dtype)
# Apply scaling if provided (for extended context)
if self.scaling_factor is not None:
t = t / self.scaling_factor
# Outer product: (seq_len,) x (dim/2,) -> (seq_len, dim/2)
freqs = torch.outer(t, self.inv_freq)
# Concatenate for full dimension: (seq_len, dim)
emb = torch.cat((freqs, freqs), dim=-1)
# Cache cos and sin
self.register_buffer("cos_cached", emb.cos(), persistent=False)
self.register_buffer("sin_cached", emb.sin(), persistent=False)
def forward(
self,
q: torch.Tensor,
k: torch.Tensor,
position_ids: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Apply rotary embeddings to q and k.
Args:
q: Query tensor of shape (batch, n_head, seq_len, head_dim)
k: Key tensor of shape (batch, n_kv_heads, seq_len, head_dim)
position_ids: Optional position indices (batch, seq_len)
Returns:
Tuple of rotated (q, k) with same shapes
"""
seq_len = q.size(2)
# Extend cache if needed
if seq_len > self.max_seq_len_cached:
self._build_cache(seq_len)
# Get cos/sin for this sequence
if position_ids is not None:
# Custom position indices (for KV-cache)
cos = self.cos_cached[position_ids] # (batch, seq_len, dim)
sin = self.sin_cached[position_ids]
cos = cos.unsqueeze(1) # (batch, 1, seq_len, dim)
sin = sin.unsqueeze(1)
else:
# Standard sequential positions
cos = self.cos_cached[:seq_len].unsqueeze(0).unsqueeze(0) # (1, 1, seq_len, dim)
sin = self.sin_cached[:seq_len].unsqueeze(0).unsqueeze(0)
# Apply rotation
q_rotated = self._apply_rotary(q, cos, sin)
k_rotated = self._apply_rotary(k, cos, sin)
return q_rotated, k_rotated
def _apply_rotary(
self,
x: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
) -> torch.Tensor:
"""Apply rotary embedding: x * cos + rotate_half(x) * sin
Computed in float32 for precision (fp16 RoPE drifts on long sequences),
then cast back to the input dtype.
"""
orig_dtype = x.dtype
x = x.float()
cos = cos.float()
sin = sin.float()
out = (x * cos) + (self._rotate_half(x) * sin)
return out.to(orig_dtype)
@staticmethod
def _rotate_half(x: torch.Tensor) -> torch.Tensor:
"""Rotate half the hidden dims: [x0, x1, x2, x3] -> [-x1, x0, -x3, x2]"""
x1 = x[..., : x.shape[-1] // 2]
x2 = x[..., x.shape[-1] // 2 :]
return torch.cat((-x2, x1), dim=-1)
class CausalSelfAttention(nn.Module):
"""Multi-head causal self-attention with modern improvements.
Supports:
- Standard MHA (Multi-Head Attention)
- MQA (Multi-Query Attention): n_kv_heads = 1
- GQA (Grouped Query Attention): 1 < n_kv_heads < n_head
- QK-Norm: RMSNorm applied to Q and K before attention
- RoPE: Rotary position embeddings
- KV-Cache: Efficient autoregressive generation
"""
def __init__(self, config: GPT2Config):
super().__init__()
assert config.n_embd % config.n_head == 0
self.n_head = config.n_head
self.n_kv_heads = config.n_kv_heads if config.use_gqa else config.n_head
self.n_embd = config.n_embd
self.head_dim = config.n_embd // config.n_head
self.n_rep = self.n_head // self.n_kv_heads # Repetition factor for KV
self.use_rope = config.use_rope
self.use_qk_norm = config.use_qk_norm
self.use_gqa = config.use_gqa
self.use_flash_attention = config.use_flash_attention
# Separate projections for Q, K, V (modern style, no bias)
if config.use_gqa or config.use_rope or config.use_qk_norm:
# Modern architecture: separate projections
self.q_proj = nn.Linear(config.n_embd, self.n_head * self.head_dim, bias=False)
self.k_proj = nn.Linear(config.n_embd, self.n_kv_heads * self.head_dim, bias=False)
self.v_proj = nn.Linear(config.n_embd, self.n_kv_heads * self.head_dim, bias=False)
self._use_separate_proj = True
else:
# Legacy: combined QKV projection
self.c_attn = nn.Linear(config.n_embd, 3 * config.n_embd, bias=True)
self._use_separate_proj = False
# Output projection (no bias in modern mode for consistency)
self.c_proj = nn.Linear(config.n_embd, config.n_embd, bias=not self._use_separate_proj)
# QK-Norm (optional)
if self.use_qk_norm:
self.q_norm = RMSNorm(self.head_dim, eps=config.layer_norm_epsilon)
self.k_norm = RMSNorm(self.head_dim, eps=config.layer_norm_epsilon)
# RoPE (optional)
if self.use_rope:
self.rotary_emb = RotaryPositionEmbedding(
dim=self.head_dim,
max_seq_len=config.n_positions,
theta=config.rope_theta,
scaling_factor=config.rope_scaling,
)
# Regularization
self.attn_dropout = nn.Dropout(config.attn_pdrop)
self.resid_dropout = nn.Dropout(config.resid_pdrop)
self.attn_pdrop = config.attn_pdrop
# Scaling factor
self.scale = 1.0 / math.sqrt(self.head_dim)
def forward(
self,
x: torch.Tensor,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.Tensor] = None,
past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
use_cache: bool = False,
) -> Tuple[torch.Tensor, Optional[Tuple[torch.Tensor, torch.Tensor]]]:
"""
Forward pass with optional KV-cache.
Args:
x: Input tensor (batch, seq_len, n_embd)
attention_mask: Optional attention mask
position_ids: Position indices for RoPE (batch, seq_len)
past_key_value: Cached (K, V) from previous forward passes
use_cache: Whether to return updated cache
Returns:
output: (batch, seq_len, n_embd)
present_key_value: Updated (K, V) cache if use_cache=True
"""
B, T, C = x.size()
# Project Q, K, V
if self._use_separate_proj:
q = self.q_proj(x) # (B, T, n_head * head_dim)
k = self.k_proj(x) # (B, T, n_kv_heads * head_dim)
v = self.v_proj(x) # (B, T, n_kv_heads * head_dim)
# Reshape for multi-head attention
q = q.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
k = k.view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
v = v.view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
else:
# Legacy combined projection
qkv = self.c_attn(x)
q, k, v = qkv.split(self.n_embd, 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)
# Apply QK-Norm if enabled
if self.use_qk_norm:
q = self.q_norm(q)
k = self.k_norm(k)
# Apply RoPE if enabled
if self.use_rope:
# Determine position IDs
if position_ids is None:
if past_key_value is not None:
past_len = past_key_value[0].size(2)
position_ids = torch.arange(
past_len, past_len + T, device=x.device
).unsqueeze(0).expand(B, -1)
else:
position_ids = torch.arange(T, device=x.device).unsqueeze(0)
q, k = self.rotary_emb(q, k, position_ids)
# KV-Cache: concatenate past keys/values
if past_key_value is not None:
past_k, past_v = past_key_value
k = torch.cat([past_k, k], dim=2)
v = torch.cat([past_v, v], dim=2)
# Store for cache if needed
present_key_value = (k, v) if use_cache else None
# Repeat K, V for GQA
if self.n_rep > 1:
k = self._repeat_kv(k)
v = self._repeat_kv(v)
S = k.size(2) # Total sequence length (including cache)
is_prefill = past_key_value is None # T_q == T_k when no cache
if self.use_flash_attention:
# SDPA (efficient/flash kernel). Works with KV-cache too: SDPA's
# is_causal flag assumes T_q == T_k, so we only use it on prefill.
# During decode (T_q < T_k) we build an explicit causal mask.
dropout_p = self.attn_pdrop if self.training else 0.0
if attention_mask is None and is_prefill:
# Fast path: built-in causal flag, no mask materialized.
y = F.scaled_dot_product_attention(
q, k, v, attn_mask=None, dropout_p=dropout_p, is_causal=True,
)
else:
# Build (T, S) causal mask aligned to the end of the sequence.
causal = torch.ones(T, S, device=q.device, dtype=torch.bool).tril(diagonal=S - T)
attn_mask = torch.zeros(T, S, device=q.device, dtype=q.dtype)
attn_mask.masked_fill_(~causal, float("-inf"))
if attention_mask is not None:
attn_mask = attn_mask + attention_mask.to(q.dtype)
y = F.scaled_dot_product_attention(
q, k, v, attn_mask=attn_mask, dropout_p=dropout_p, is_causal=False,
)
self._last_attention_weights = None # Not available with fused kernel
else:
# Manual attention (exposes attention weights for inspection)
att = (q @ k.transpose(-2, -1)) * self.scale # (B, n_head, T, S)
# Causal mask aligned to the end (handles both prefill and decode)
causal_mask = torch.ones(T, S, device=x.device, dtype=torch.bool).tril(diagonal=S - T)
att = att.masked_fill(~causal_mask, float("-inf"))
if attention_mask is not None:
att = att + attention_mask.to(att.dtype)
att = F.softmax(att, dim=-1)
self._last_attention_weights = att.detach()
att = self.attn_dropout(att)
y = att @ v # (B, n_head, T, head_dim)
# Reassemble heads
y = y.transpose(1, 2).contiguous().view(B, T, C)
# Output projection with dropout
y = self.resid_dropout(self.c_proj(y))
return y, present_key_value
def _repeat_kv(self, x: torch.Tensor) -> torch.Tensor:
"""Repeat KV heads to match number of query heads (for GQA)."""
B, n_kv_heads, S, head_dim = x.shape
if self.n_rep == 1:
return x
# Expand and reshape: repeat each KV head n_rep times
x = x[:, :, None, :, :].expand(B, n_kv_heads, self.n_rep, S, head_dim)
return x.reshape(B, self.n_head, S, head_dim)
class MLP(nn.Module):
"""Position-wise feed-forward network with optional SwiGLU activation.
Standard GELU MLP:
x -> Linear(n_embd, n_inner) -> GELU -> Linear(n_inner, n_embd)
SwiGLU MLP:
gate, up = Linear(n_embd, 2*n_inner).chunk(2)
output = SiLU(gate) * up
output = Linear(n_inner, n_embd)(output)
Reference: https://arxiv.org/abs/2002.05202
"""
def __init__(self, config: GPT2Config):
super().__init__()
self.use_swiglu = config.use_swiglu
if self.use_swiglu:
# SwiGLU: gate and up projection combined (no bias, modern style)
self.gate_up_proj = nn.Linear(config.n_embd, 2 * config.n_inner, bias=False)
self.down_proj = nn.Linear(config.n_inner, config.n_embd, bias=False)
self.act = nn.SiLU() # Swish activation
else:
# Standard GELU MLP
self.c_fc = nn.Linear(config.n_embd, config.n_inner, bias=True)
self.c_proj = nn.Linear(config.n_inner, config.n_embd, bias=True)
if config.activation_function == "gelu":
self.act = nn.GELU()
elif config.activation_function == "relu":
self.act = nn.ReLU()
else:
raise ValueError(f"Unsupported activation: {config.activation_function}")
self.dropout = nn.Dropout(config.resid_pdrop)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Forward pass."""
if self.use_swiglu:
# SwiGLU: split into gate and value, apply gated activation
gate_up = self.gate_up_proj(x) # (B, T, 2*n_inner)
gate, up = gate_up.chunk(2, dim=-1) # Each (B, T, n_inner)
x = self.act(gate) * up # Gated activation
x = self.down_proj(x)
else:
# Standard GELU
x = self.c_fc(x)
x = self.act(x)
x = self.c_proj(x)
x = self.dropout(x)
return x
class TransformerBlock(nn.Module):
"""Transformer block with modern improvements.
Architecture (pre-norm):
x -> Norm -> Attention -> + -> Norm -> MLP -> +
|__________________| |____________|
"""
def __init__(self, config: GPT2Config):
super().__init__()
self.gradient_checkpointing = False
# Normalization layers (RMSNorm or LayerNorm)
if config.use_rmsnorm:
self.ln_1 = RMSNorm(config.n_embd, eps=config.layer_norm_epsilon)
self.ln_2 = RMSNorm(config.n_embd, eps=config.layer_norm_epsilon)
else:
self.ln_1 = nn.LayerNorm(config.n_embd, eps=config.layer_norm_epsilon)
self.ln_2 = nn.LayerNorm(config.n_embd, eps=config.layer_norm_epsilon)
self.attn = CausalSelfAttention(config)
self.mlp = MLP(config)
# LayerScale: per-channel learned gain on each sub-block output.
self.use_layerscale = config.use_layerscale
if config.use_layerscale:
self.gamma_1 = nn.Parameter(
torch.full((config.n_embd,), config.layerscale_init)
)
self.gamma_2 = nn.Parameter(
torch.full((config.n_embd,), config.layerscale_init)
)
def forward(
self,
x: torch.Tensor,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.Tensor] = None,
past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
use_cache: bool = False,
) -> Tuple[torch.Tensor, Optional[Tuple[torch.Tensor, torch.Tensor]]]:
"""
Forward pass with optional KV-cache support.
Args:
x: Input (batch, seq_len, n_embd)
attention_mask: Optional attention mask
position_ids: Position indices for RoPE
past_key_value: Cached (K, V) for this layer
use_cache: Whether to return updated cache
Returns:
output: (batch, seq_len, n_embd)
present_key_value: Updated cache if use_cache=True
"""
# Pre-norm attention
if self.training and self.gradient_checkpointing and not use_cache:
attn_output, present_key_value = torch.utils.checkpoint.checkpoint(
self.attn,
self.ln_1(x),
attention_mask,
position_ids,
past_key_value,
use_cache,
use_reentrant=False,
preserve_rng_state=True,
determinism_check="none",
)
else:
attn_output, present_key_value = self.attn(
self.ln_1(x),
attention_mask=attention_mask,
position_ids=position_ids,
past_key_value=past_key_value,
use_cache=use_cache,
)
if self.use_layerscale:
attn_output = self.gamma_1 * attn_output
x = x + attn_output
# Pre-norm MLP
if self.training and self.gradient_checkpointing:
mlp_output = torch.utils.checkpoint.checkpoint(
self.mlp, self.ln_2(x), use_reentrant=False,
preserve_rng_state=True, determinism_check="none",
)
else:
mlp_output = self.mlp(self.ln_2(x))
if self.use_layerscale:
mlp_output = self.gamma_2 * mlp_output
x = x + mlp_output
return x, present_key_value
class GPT2(nn.Module):
"""GPT-2 Language Model with modern improvements."""
def __init__(self, config: GPT2Config):
super().__init__()
self.config = config
# Token embeddings
wte = nn.Embedding(config.vocab_size, config.n_embd)
# Position embeddings (only if not using RoPE)
wpe = None
if not config.use_rope:
wpe = nn.Embedding(config.n_positions, config.n_embd)
# Final normalization
if config.use_rmsnorm:
ln_f = RMSNorm(config.n_embd, eps=config.layer_norm_epsilon)
else:
ln_f = nn.LayerNorm(config.n_embd, eps=config.layer_norm_epsilon)
# Build transformer
self.transformer = nn.ModuleDict(
dict(
wte=wte,
drop=nn.Dropout(config.embd_pdrop),
h=nn.ModuleList([TransformerBlock(config) for _ in range(config.n_layer)]),
ln_f=ln_f,
)
)
# Add position embeddings if not using RoPE
if wpe is not None:
self.transformer["wpe"] = wpe
# Recurrent-depth: learned per-iteration depth embedding (broadcast over
# all positions), injected before each loop pass so the shared block can
# distinguish iterations. Only needed when looping (recurrence > 1).
if config.recurrence > 1:
self.transformer["depth_emb"] = nn.Embedding(config.recurrence, config.n_embd)
# Language modeling head
self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
# Optional 2nd head predicting token t+2 (multi-token prediction).
# Separate, untied head — only used to add a training loss term.
if config.use_multi_token:
self.lm_head2 = nn.Linear(config.n_embd, config.vocab_size, bias=False)
# Initialize weights first, then tie (so tying survives init)
self.apply(self._init_weights)
# Residual output projections (attn c_proj + MLP down_proj):
# - zero_init_residual: init to exactly 0 → each block is an identity map
# at start. Required for stable recurrent-depth looping (Kohli et al.).
# - otherwise GPT-2 scaled init: std = 0.02 / sqrt(2 * n_layer) to keep
# the residual-stream variance bounded with depth.
scale = (2 * config.n_layer) ** -0.5
for name, p in self.named_parameters():
if name.endswith("c_proj.weight") or name.endswith("down_proj.weight"):
if config.zero_init_residual:
torch.nn.init.zeros_(p)
else:
torch.nn.init.normal_(p, mean=0.0, std=0.02 * scale)
# Weight tying (after init to avoid double-init of the shared tensor)
self.transformer.wte.weight = self.lm_head.weight
# Report number of parameters
print(f"Number of parameters: {self.get_num_params() / 1e6:.2f}M")
def gradient_checkpointing_enable(self):
"""Enable gradient checkpointing for all transformer blocks."""
for block in self.transformer.h:
block.gradient_checkpointing = True
def gradient_checkpointing_disable(self):
"""Disable gradient checkpointing for all transformer blocks."""
for block in self.transformer.h:
block.gradient_checkpointing = False
def _init_weights(self, module):
"""Initialize weights."""
if isinstance(module, nn.Linear):
torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
if module.bias is not None:
torch.nn.init.zeros_(module.bias)
elif isinstance(module, nn.Embedding):
torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
elif isinstance(module, nn.LayerNorm):
torch.nn.init.zeros_(module.bias)
torch.nn.init.ones_(module.weight)
elif isinstance(module, RMSNorm):
torch.nn.init.ones_(module.weight)
def get_num_params(self, non_embedding: bool = False) -> int:
"""Return the number of parameters in the model."""
n_params = sum(p.numel() for p in self.parameters())
if non_embedding and "wpe" in self.transformer:
n_params -= self.transformer.wpe.weight.numel()
return n_params
def forward(
self,
input_ids: torch.Tensor,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.Tensor] = None,
labels: Optional[torch.Tensor] = None,
past_key_values: Optional[List[Tuple[torch.Tensor, torch.Tensor]]] = None,
use_cache: bool = False,
) -> Tuple[torch.Tensor, Optional[torch.Tensor], torch.Tensor, Optional[List]]:
"""
Forward pass with optional KV-cache.
Args:
input_ids: Input token IDs (batch, seq_len)
attention_mask: Optional attention mask
position_ids: Optional position indices for RoPE
labels: Optional labels for loss computation
past_key_values: List of (K, V) tuples per layer
use_cache: Whether to return updated cache
Returns:
logits: (batch, seq_len, vocab_size)
loss: Scalar if labels provided
hidden_states: (batch, seq_len, n_embd)
present_key_values: Updated cache if use_cache=True
"""
device = input_ids.device
B, T = input_ids.size()
# Calculate total sequence length (including cache)
past_length = 0
if past_key_values is not None and past_key_values[0] is not None:
past_length = past_key_values[0][0].size(2)
total_length = past_length + T
assert total_length <= self.config.n_positions, \
f"Sequence length {total_length} exceeds max {self.config.n_positions}"
# Token embeddings
x = self.transformer.wte(input_ids)
# Position embeddings (if not using RoPE)
if "wpe" in self.transformer:
if position_ids is None:
position_ids = torch.arange(past_length, total_length, device=device)
position_ids = position_ids.unsqueeze(0).expand(B, -1)
pos_emb = self.transformer.wpe(position_ids)
x = x + pos_emb
x = self.transformer.drop(x)
# Recurrent-depth: apply the stack of blocks `recurrence` times, injecting
# a per-iteration depth embedding first so the shared blocks can tell
# iterations apart. recurrence=1 is the plain (non-looped) forward.
# KV-cache is only meaningful on the LAST iteration (the one whose K/V are
# reused at the next decode step); intermediate iterations don't cache.
R = self.config.recurrence
# KV-cache across a looped forward is not yet supported (the shared blocks
# would need a separate cache slot per iteration). Looping is used in
# training and in cache-free eval (evaluate.py), which is enough to test
# the idea. Fall back cleanly if someone requests both.
if R > 1 and use_cache:
raise NotImplementedError(
"KV-cache is not supported with recurrence > 1; call with use_cache=False."
)
present_key_values = [] if use_cache else None
# depth_emb table has `config.recurrence` rows. Allow R (may be raised at
# inference for "thinking longer") to exceed it by clamping the index to
# the last learned depth embedding.
n_depth = self.transformer.depth_emb.num_embeddings if R > 1 else 0
# Adaptive per-token halting (inference only): a token freezes once its
# output entropy is low enough (easy tokens stop early, hapax loop the
# full R). `frozen` holds the final state of halted tokens; `x` carries
# the still-active computation. Frozen tokens keep serving as K/V.
# Active at inference always; at training only if halting_in_training (Option B).
halting = self.config.adaptive_halting and R > 1 and (
not self.training or self.config.halting_in_training
)
frozen = None # (B, T, C) state of halted tokens, NaN where still active
if halting:
frozen = torch.full_like(x, float("nan"))
for r in range(R):
if R > 1:
idx = min(r, n_depth - 1)
depth_vec = self.transformer.depth_emb(torch.tensor(idx, device=device))
x = x + depth_vec # broadcast over (B, T, n_embd)
for i, block in enumerate(self.transformer.h):
past_kv = past_key_values[i] if past_key_values is not None else None
x, present_kv = block(
x,
attention_mask=attention_mask,
position_ids=position_ids if self.config.use_rope else None,
past_key_value=past_kv,
use_cache=use_cache,
)
if use_cache:
present_key_values.append(present_kv)
if halting:
# Restore already-frozen tokens to their halt-time state so later
# iterations neither advance them nor let them drift.
was_frozen = ~torch.isnan(frozen[..., :1]) # (B,T,1)
x = torch.where(was_frozen, frozen, x)
if r < R - 1:
# Freeze newly-confident tokens.
probs = F.softmax(self.lm_head(self.transformer.ln_f(x)), dim=-1)
entropy = -(probs * torch.log(probs + 1e-9)).sum(-1, keepdim=True)
active = ~was_frozen # (B,T,1)
if self.config.halting_mode == "percentile":
# Size-invariant cutoff: each iteration, freeze the q
# fraction of still-active tokens with the LOWEST entropy,
# per sequence. The cutoff is the q-quantile of the ACTIVE
# tokens' entropies only (frozen ones are excluded, so they
# can't skew the quantile). q transfers across model sizes.
q = self.config.halting_percentile
ent = entropy.squeeze(-1) # (B, T)
act = active.squeeze(-1) # (B, T) bool
newly = torch.zeros_like(act) # (B, T)
for b in range(ent.size(0)):
vals = ent[b][act[b]] # entropies of active tokens in seq b
if vals.numel() == 0:
continue
cutoff = torch.quantile(vals, q)
newly[b] = act[b] & (ent[b] <= cutoff)
newly = newly.unsqueeze(-1) # (B, T, 1)
else:
# Absolute threshold (default): fixed entropy cutoff in nats.
newly = (entropy < self.config.halting_entropy_threshold) & active
frozen = torch.where(newly, x, frozen)
# Final normalization
x = self.transformer.ln_f(x)
hidden_states = x
# Language modeling head
logits = self.lm_head(x)
# Loss computation
loss = None
if labels is not None:
# Main objective: predict t+1.
shift_logits = logits[..., :-1, :].contiguous()
shift_labels = labels[..., 1:].contiguous()
loss = F.cross_entropy(
shift_logits.view(-1, shift_logits.size(-1)),
shift_labels.view(-1),
label_smoothing=self.config.label_smoothing,
ignore_index=self.config.pad_token_id,
)
# Auxiliary objective: a 2nd head predicts t+2 from the same
# hidden state, forcing the representation to "look further".
# Only adds a training-time loss term; generation is unchanged.
if self.config.use_multi_token and hasattr(self, "lm_head2"):
logits2 = self.lm_head2(x)
shift_logits2 = logits2[..., :-2, :].contiguous()
shift_labels2 = labels[..., 2:].contiguous()
loss2 = F.cross_entropy(
shift_logits2.view(-1, shift_logits2.size(-1)),
shift_labels2.view(-1),
label_smoothing=self.config.label_smoothing,
ignore_index=self.config.pad_token_id,
)
loss = loss + self.config.multi_token_weight * loss2
return logits, loss, hidden_states, present_key_values
@torch.no_grad()
def forward_layer_logits(self, input_ids: torch.Tensor, layers):
"""Return logits computed from selected intermediate layers (for DoLa).
For each requested layer index, the layer's hidden state is passed
through the final norm + lm_head, giving "early-exit" logits. The final
layer's logits are always included under key -1.
Args:
input_ids: (batch, seq_len)
layers: iterable of layer indices (0-based) to read hidden states
after. Negative or out-of-range values are ignored.
Returns:
dict {layer_index: logits (batch, seq_len, vocab)}, plus key -1 for
the final layer.
"""
device = input_ids.device
B, T = input_ids.size()
wanted = set(int(l) for l in layers)
x = self.transformer.wte(input_ids)
if "wpe" in self.transformer:
position_ids = torch.arange(T, device=device).unsqueeze(0).expand(B, -1)
x = x + self.transformer.wpe(position_ids)
else:
position_ids = torch.arange(T, device=device).unsqueeze(0)
x = self.transformer.drop(x)
out = {}
n_layer = len(self.transformer.h)
for i, block in enumerate(self.transformer.h):
x, _ = block(
x,
attention_mask=None,
position_ids=position_ids if self.config.use_rope else None,
past_key_value=None,
use_cache=False,
)
if i in wanted and i != n_layer - 1:
# Project this layer's hidden state with the shared head.
h = self.transformer.ln_f(x)
out[i] = self.lm_head(h)
# Final layer logits (always provided, key -1).
h = self.transformer.ln_f(x)
out[-1] = self.lm_head(h)
return out
@torch.no_grad()
def generate(
self,
input_ids: torch.Tensor,
max_length: int = 100,
temperature: float = 1.0,
top_k: Optional[int] = None,
top_p: Optional[float] = None,
repetition_penalty: float = 1.0,
do_sample: bool = True,
eos_token_id: Optional[int] = None,
use_cache: bool = True,
) -> torch.Tensor:
"""
Generate text autoregressively with optional KV-cache.
Args:
input_ids: Input token IDs (batch, seq_len)
max_length: Maximum tokens to generate
temperature: Sampling temperature
top_k: Top-k filtering
top_p: Nucleus sampling threshold
repetition_penalty: Penalty for repeating tokens
do_sample: Whether to sample or greedy decode
eos_token_id: Stop token ID
use_cache: Whether to use KV-cache (faster generation)
Returns:
Generated token IDs (batch, total_length)
"""
training = self.training
self.eval()
# KV-cache is incompatible with recurrent-depth looping (see forward).
if self.config.recurrence > 1:
use_cache = False
past_key_values = None
for _ in range(max_length):
# Determine input for this step
if use_cache and past_key_values is not None:
# Only process the last token when using cache
input_ids_cond = input_ids[:, -1:]
else:
# Full sequence (crop if needed)
input_ids_cond = (
input_ids if input_ids.size(1) <= self.config.n_positions
else input_ids[:, -self.config.n_positions:]
)
# Forward pass
logits, _, _, past_key_values = self(
input_ids_cond,
past_key_values=past_key_values if use_cache else None,
use_cache=use_cache,
)
# Get logits for last position
logits = logits[:, -1, :] / temperature
# Apply repetition penalty (HF semantics: divide positive logits,
# multiply negative ones, so both move toward less likely).
# Vectorized: gather logits of already-seen tokens, rescale, scatter.
if repetition_penalty != 1.0:
seen = logits.gather(1, input_ids)
seen = torch.where(seen > 0, seen / repetition_penalty, seen * repetition_penalty)
logits.scatter_(1, input_ids, seen)
# Top-k filtering (clamp k to vocab size to avoid topk error)
if top_k is not None:
k = min(top_k, logits.size(-1))
indices_to_remove = logits < torch.topk(logits, k)[0][..., -1, None]
logits[indices_to_remove] = float("-inf")
# Top-p filtering
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")
# Sample or greedy
probs = F.softmax(logits, dim=-1)
if do_sample:
next_token = torch.multinomial(probs, num_samples=1)
else:
next_token = torch.argmax(probs, dim=-1, keepdim=True)
# Append to sequence
input_ids = torch.cat([input_ids, next_token], dim=1)
# Stop on EOS
if eos_token_id is not None and (next_token == eos_token_id).all():
break
self.train(training)
return input_ids
def save_pretrained(self, save_path: str):
"""Save model checkpoint."""
torch.save(
{
"model_state_dict": self.state_dict(),
"config": self.config,
},
save_path,
)
print(f"Model saved to {save_path}")
@classmethod
def from_pretrained(cls, load_path: str, device: str = "cpu"):
"""Load model checkpoint."""
# weights_only=True blocks arbitrary code execution from a malicious
# checkpoint. The only non-tensor object we serialize is GPT2Config
# (a dataclass), so we allowlist it explicitly.
#
# add_safe_globals + weights_only=True only exist on torch >= 2.4.
# On older versions we fall back to a plain (trusted) load, since we
# only ever load checkpoints we produced ourselves.
if hasattr(torch.serialization, "add_safe_globals"):
torch.serialization.add_safe_globals([GPT2Config])
checkpoint = torch.load(load_path, map_location=device, weights_only=True)
else:
checkpoint = torch.load(load_path, map_location=device, weights_only=False)
config = checkpoint["config"]
model = cls(config)
# Backward compat: older checkpoints stored a per-layer causal mask
# buffer "attn.bias" (shape n_positions x n_positions). The mask is now
# built on the fly, so these keys are obsolete — drop them before load.
state_dict = checkpoint["model_state_dict"]
state_dict = {k: v for k, v in state_dict.items() if not k.endswith("attn.bias")}
# strict=False also tolerates the missing buffers cleanly.
missing, unexpected = model.load_state_dict(state_dict, strict=False)
if unexpected:
print(f"Warning: ignored unexpected keys: {unexpected}")
# Only the intended obsolete buffers may be "missing"; flag anything else.
real_missing = [k for k in missing if not k.endswith("attn.bias")]
if real_missing:
print(f"Warning: missing keys not initialized from checkpoint: {real_missing}")
model = model.to(device)
print(f"Model loaded from {load_path} on device: {device}")
return model