"""Implementation of the encoder-decoder transformer used across the project.""" from __future__ import annotations import math from typing import cast import torch import torch.nn as nn from torch import Tensor from . import modules from .configs import ModelCfg from .utils import broadcast_padding_mask, create_causal_mask, sample_from_logits __all__ = ["BasicEncoderDecoderTransformer"] class BasicEncoderDecoderTransformer(nn.Module): """Full encoder-decoder model with tied input/output embeddings.""" def __init__(self, cfg: ModelCfg) -> None: super().__init__() if not isinstance(cfg, ModelCfg): raise TypeError(f"cfg must be a ModelCfg, got {type(cfg)}") if not isinstance(cfg.vocab_size, int): raise TypeError(f"vocab_size must be an int, got {type(cfg.vocab_size)}") if not cfg.vocab_size > 0: raise ValueError(f"vocab_size must be strictly greater than 0, got {cfg.vocab_size}") if not isinstance(cfg.d_model, int): raise TypeError(f"d_model must be an int, got {type(cfg.d_model)}") if not cfg.d_model > 0: raise ValueError(f"d_model must be strictly greater than 0, got {cfg.d_model}") if not isinstance(cfg.max_seq_len, int): raise TypeError(f"max_seq_len must be an int, got {type(cfg.max_seq_len)}") if not cfg.max_seq_len > 0: raise ValueError(f"max_seq_len must be greater than 0, got {cfg.max_seq_len}") if not 0 <= cfg.dropout_rate < 1: raise ValueError(f"dropout_rate must be in [0,1[, got {cfg.dropout_rate}") if not isinstance(cfg.pad_id, int): raise TypeError(f"pad_id must be an int, got {type(cfg.pad_id)}") if not (0 <= cfg.pad_id < cfg.vocab_size): raise ValueError(f"pad_id must be in [0, {cfg.vocab_size - 1}], got {cfg.pad_id}") if not isinstance(cfg.bos_id, int): raise TypeError(f"bos_id must be an int, got {type(cfg.bos_id)}") if not (0 <= cfg.bos_id < cfg.vocab_size): raise ValueError(f"bos_id must be in [0, {cfg.vocab_size - 1}], got {cfg.bos_id}") if not isinstance(cfg.eos_id, int): raise TypeError(f"eos_id must be an int, got {type(cfg.eos_id)}") if not (0 <= cfg.eos_id < cfg.vocab_size): raise ValueError(f"eos_id must be in [0, {cfg.vocab_size - 1}], got {cfg.eos_id}") self.cfg = cfg style_raw = getattr(cfg, "layer_norm_style", None) if style_raw is None: style = "post" if cfg.num_layers <= 4 else "pre" elif not isinstance(style_raw, str): raise TypeError( f"layer_norm_style must be a string when provided, got {type(style_raw)}" ) else: style = style_raw.lower() if style not in {"pre", "post"}: raise ValueError( "layer_norm_style must be either 'pre' or 'post'" f" (case-insensitive); got {style_raw!r}" ) self.layer_norm_style = style cfg.layer_norm_style = style self.embed = modules.InputEmbedding( cfg.vocab_size, cfg.d_model, cfg.max_seq_len, cfg.pad_id, cfg.dropout_rate ) self.encoder = modules.TransformerEncoder( cfg.d_model, cfg.num_heads, cfg.d_ff, cfg.num_layers, cfg.dropout_rate, layer_norm_style=style, ) self.decoder = modules.TransformerDecoder( cfg.d_model, cfg.num_heads, cfg.d_ff, cfg.num_layers, cfg.dropout_rate, layer_norm_style=style, ) self.lm_head = modules.LMHead(cfg.d_model, cfg.vocab_size) # tie weights self.lm_head.fc.weight = self.embed.token_embed.weight self.max_seq_len = cfg.max_seq_len self.pad_id = cfg.pad_id self.bos_id = cfg.bos_id self.eos_id = cfg.eos_id self.gradient_checkpointing = False def enable_gradient_checkpointing(self, enabled: bool = True) -> None: self.gradient_checkpointing = enabled self.encoder.use_ckpt = enabled self.decoder.use_ckpt = enabled def forward( self, src_ids: Tensor, tgt_ids: Tensor, src_padding_mask: Tensor, tgt_padding_mask: Tensor, ) -> Tensor: """Compute logits given input/output token ids and boolean padding masks.""" if not isinstance(src_ids, Tensor): raise TypeError(f"src_ids must be a torch.Tensor, got {type(src_ids)}") if src_ids.dim() != 2: raise ValueError( f"src_ids must be a 2D torch.Tensor of shape (B, S), got shape {tuple(src_ids.shape)}" ) if src_ids.dtype != torch.long: raise TypeError(f"src_ids must be torch.long (int64), got {src_ids.dtype}") if not isinstance(tgt_ids, Tensor): raise TypeError(f"tgt_ids must be a torch.Tensor, got {type(tgt_ids)}") if tgt_ids.dim() != 2: raise ValueError( f"tgt_ids must be a 2D torch.Tensor of shape (B, S), got shape {tuple(tgt_ids.shape)}" ) if tgt_ids.dtype != torch.long: raise TypeError(f"tgt_ids must be torch.long (int64), got {tgt_ids.dtype}") if ( not isinstance(src_padding_mask, Tensor) or src_padding_mask.dtype != torch.bool or src_padding_mask.dim() != 2 ): raise TypeError("src_padding_mask must be a boolean tensor shaped (B, S)") if ( not isinstance(tgt_padding_mask, Tensor) or tgt_padding_mask.dtype != torch.bool or tgt_padding_mask.dim() != 2 ): raise TypeError("tgt_padding_mask must be a boolean tensor shaped (B, S)") src_padding_mask = broadcast_padding_mask(src_padding_mask, self.cfg.num_heads) tgt_padding_mask = broadcast_padding_mask(tgt_padding_mask, self.cfg.num_heads) hidden_states = self.encode(src_ids, src_padding_mask) dec_hidden_states = self.decode(hidden_states, tgt_ids, src_padding_mask, tgt_padding_mask) logits = self.lm_head(dec_hidden_states) return logits # (B, T_tgt, V) def encode(self, src_ids: Tensor, src_padding_mask: Tensor) -> Tensor: """Encode source tokens into memory representations.""" if not isinstance(src_ids, torch.Tensor): raise TypeError(f"src_ids must be a torch.Tensor, got {type(src_ids)}") if src_ids.dim() != 2: raise ValueError( f"src_ids must be a 2D torch.Tensor of shape (B, S), got shape {tuple(src_ids.shape)}" ) if src_ids.dtype != torch.long: raise TypeError("src_ids must be torch.long (int64)") if ( not isinstance(src_padding_mask, Tensor) or src_padding_mask.dtype != torch.bool or src_padding_mask.dim() != 4 ): raise TypeError("src_padding_mask must be a boolean tensor shaped (B, H, 1, S)") x = self.embed(src_ids) # (B, Sx, D) hidden_states = self.encoder(x, src_padding_mask) return hidden_states # (B, Sx, D) def decode( self, hidden_states: Tensor, tgt_ids: Tensor, src_padding_mask: Tensor, tgt_padding_mask: Tensor, ) -> Tensor: if not isinstance(hidden_states, torch.Tensor): raise TypeError(f"hidden_states must be a torch.Tensor, got {type(hidden_states)}") if hidden_states.dim() != 3: raise ValueError( f"hidden_states must be a 3D torch.Tensor of shape (B, S, D); got shape {tuple(hidden_states.shape)}" ) if not isinstance(tgt_ids, torch.Tensor): raise TypeError(f"tgt_ids must be a torch.Tensor, got {type(tgt_ids)}") if tgt_ids.dim() != 2: raise ValueError( f"tgt_ids must be a 2D torch.Tensor of shape (B, S), got shape {tuple(tgt_ids.shape)}" ) if tgt_ids.dtype != torch.long: raise TypeError("tgt_ids must be torch.long (int64)") if ( not isinstance(src_padding_mask, Tensor) or src_padding_mask.dtype != torch.bool or src_padding_mask.dim() != 4 ): raise TypeError("src_padding_mask must be a boolean tensor shaped (B, H, 1, S)") if ( not isinstance(tgt_padding_mask, Tensor) or tgt_padding_mask.dtype != torch.bool or tgt_padding_mask.dim() != 4 ): raise TypeError("tgt_padding_mask must be a boolean tensor shaped (B, H, 1, S)") tgt_causal_mask = create_causal_mask(tgt_ids, self.cfg.num_heads) y = self.embed(tgt_ids) # (B, Sy, D) out = self.decoder(hidden_states, y, src_padding_mask, tgt_padding_mask, tgt_causal_mask) return out # (B, Sy, D) def generate( self, src_ids: Tensor, src_padding_mask: Tensor, max_new_tokens: int = 20, temperature: float = 1.0, top_k: int | None = None, top_p: float | None = None, do_sample: bool = False, presence_penalty: float = 0.0, frequency_penalty: float = 0.0, no_repeat_ngram: int | None = None, min_steps_before_eos: int = 0, *, seed: int | None = None, generator: torch.Generator | None = None, ) -> Tensor: if not isinstance(src_ids, torch.Tensor): raise TypeError(f"src_ids must be a torch.Tensor, got {type(src_ids)}") if src_ids.dim() != 2: raise ValueError( f"src_ids must be a 2D torch.Tensor of shape (B, S), got shape {tuple(src_ids.shape)}" ) if src_ids.dtype != torch.long: raise TypeError("src_ids must be torch.long (int64)") if not isinstance(src_padding_mask, torch.Tensor): raise TypeError( f"src_padding_mask must be a torch.Tensor, got {type(src_padding_mask)}" ) if not isinstance(max_new_tokens, int): raise TypeError(f"max_new_tokens must be an int, got {type(max_new_tokens)}") if max_new_tokens < 0: raise ValueError( f"max_new_tokens must be greater than or equal to 0, got {max_new_tokens}" ) if not isinstance(temperature, float): raise TypeError(f"temperature must be a float, got {type(temperature)}") if not (temperature > 0.0): raise ValueError(f"temperature must be strictly greater than 0, got {temperature}") if top_k is not None and not isinstance(top_k, int): raise TypeError(f"top_k must be an int or None, got {type(top_k)}") if top_k is not None and top_k < 0: raise ValueError(f"top_k must be >= 0, got {top_k}") if top_p is not None and not isinstance(top_p, int | float): raise TypeError(f"top_p must be a number or None, got {type(top_p)}") if top_p is not None: top_p = float(top_p) if not (0.0 < top_p <= 1.0): raise ValueError(f"top_p must be in (0, 1], got {top_p}") if not isinstance(do_sample, bool): raise TypeError(f"do_sample must be a bool, got {type(do_sample)}") if not isinstance(presence_penalty, int | float) or not math.isfinite( float(presence_penalty) ): raise ValueError( f"presence_penalty must be a finite number >= 0, got {presence_penalty!r}." ) presence_penalty = float(presence_penalty) if presence_penalty < 0.0: raise ValueError(f"presence_penalty must be >= 0, got {presence_penalty}.") if not isinstance(frequency_penalty, int | float) or not math.isfinite( float(frequency_penalty) ): raise ValueError( f"frequency_penalty must be a finite number >= 0, got {frequency_penalty!r}." ) frequency_penalty = float(frequency_penalty) if frequency_penalty < 0.0: raise ValueError(f"frequency_penalty must be >= 0, got {frequency_penalty}.") if no_repeat_ngram is not None: if not isinstance(no_repeat_ngram, int): raise ValueError(f"no_repeat_ngram must be int or None, got {no_repeat_ngram!r}.") if no_repeat_ngram < 2: no_repeat_ngram = None # size <2 is meaningless; treat as disabled if not isinstance(min_steps_before_eos, int) or min_steps_before_eos < 0: raise ValueError( f"min_steps_before_eos must be int >= 0, got {min_steps_before_eos!r}." ) if seed is not None and not isinstance(seed, int): raise TypeError(f"seed must be int or None, got {type(seed)}") if generator is not None and not isinstance(generator, torch.Generator): raise TypeError(f"generator must be a torch.Generator or None, got {type(generator)}") self.eval() with torch.no_grad(): batch_size, _ = src_ids.shape device = src_ids.device tgt_ids = torch.full((batch_size, 1), self.bos_id, device=device, dtype=torch.long) allowed_new = max(0, self.max_seq_len - tgt_ids.size(1)) max_new_tokens = min(max_new_tokens, allowed_new) finished = torch.zeros(batch_size, dtype=torch.bool, device=device) src_padding_mask = broadcast_padding_mask(src_padding_mask, self.cfg.num_heads) hidden_states = self.encode(src_ids, src_padding_mask) def _make_generator() -> torch.Generator: if device.type == "cpu": return torch.Generator() return torch.Generator(device=device) rng = generator if seed is not None: rng = rng if rng is not None else _make_generator() rng.manual_seed(seed) elif rng is None and do_sample: rng = _make_generator() for step in range(max_new_tokens): # decode on current tgt_ids; take last-step logits tgt_padding_mask = tgt_ids == self.pad_id tgt_padding_mask = broadcast_padding_mask(tgt_padding_mask, self.cfg.num_heads) step_hidden = self.decode( hidden_states, tgt_ids, src_padding_mask, tgt_padding_mask ) # (B, T, D) logits = self.lm_head(step_hidden)[:, -1, :] # (B, V) disallowed = [self.pad_id] + ([self.eos_id] if step < min_steps_before_eos else []) next_ids = cast( Tensor, sample_from_logits( logits, temperature=temperature, top_k=top_k, top_p=top_p, do_sample=do_sample, disallowed_tokens=disallowed, repetition_ctx=tgt_ids, presence_penalty=presence_penalty, frequency_penalty=frequency_penalty, no_repeat_ngram_size=no_repeat_ngram, rng=rng, ), ) # (B,) next_ids = torch.where(finished, torch.full_like(next_ids, self.eos_id), next_ids) tgt_ids = torch.cat([tgt_ids, next_ids.unsqueeze(1)], dim=-1) finished |= next_ids == self.eos_id if finished.all(): return tgt_ids return tgt_ids def debug_generate( self, src_ids: Tensor, src_padding_mask: Tensor | None, tokenizer, max_new_tokens: int = 20, temperature: float = 1.0, top_k: int | None = None, top_p: float | None = None, do_sample: bool = False, *, seed: int | None = None, generator: torch.Generator | None = None, ) -> Tensor: """ Like generate(), but prints debug info at each step: input tokens, ids, output ids, tokens, logits, etc. """ self.eval() with torch.no_grad(): batch_size, _ = src_ids.shape device = src_ids.device tgt_ids = torch.full((batch_size, 1), self.bos_id, device=device, dtype=torch.long) allowed_new = max(0, self.max_seq_len - tgt_ids.size(1)) max_new_tokens = min(max_new_tokens, allowed_new) finished = torch.zeros(batch_size, dtype=torch.bool, device=device) if src_padding_mask is not None: if ( src_padding_mask.dtype != torch.bool or src_padding_mask.dim() != 2 or src_padding_mask.size(0) != batch_size ): raise TypeError( "src_padding_mask must be boolean tensor shaped (B, S) when provided" ) src_mask_2d = src_padding_mask else: src_mask_2d = torch.ones_like(src_ids, dtype=torch.bool, device=device) src_mask_4d = broadcast_padding_mask(src_mask_2d, self.cfg.num_heads) hidden_states = self.encode(src_ids, src_mask_4d) def _make_generator() -> torch.Generator: if device.type == "cpu": return torch.Generator() return torch.Generator(device=device) rng = generator if seed is not None: rng = rng if rng is not None else _make_generator() rng.manual_seed(seed) elif rng is None and do_sample: rng = _make_generator() # print("[DEBUG] Input ids:", src_ids) print( "[DEBUG] Input tokens:", tokenizer.batch_decode(src_ids, skip_special_tokens=False) ) for step in range(max_new_tokens): print(f"[DEBUG] Step {step + 1}") # print(" Output ids so far:", tgt_ids) print( " Output tokens so far:", tokenizer.batch_decode(tgt_ids, skip_special_tokens=False), ) tgt_padding_mask_2d = tgt_ids != self.pad_id tgt_padding_mask_4d = broadcast_padding_mask( tgt_padding_mask_2d, self.cfg.num_heads ) step_hidden = self.decode( hidden_states, tgt_ids, src_mask_4d, tgt_padding_mask_4d ) # (B, T, D) logits = self.lm_head(step_hidden)[:, -1, :] # (B, V) next_ids = cast( Tensor, sample_from_logits( logits, temperature=temperature, top_k=top_k, top_p=top_p, do_sample=do_sample, disallowed_tokens=[self.pad_id], rng=rng, ), ) # (B,) next_ids = torch.where(finished, torch.full_like(next_ids, self.eos_id), next_ids) tgt_ids = torch.cat([tgt_ids, next_ids.unsqueeze(1)], dim=-1) finished |= next_ids == self.eos_id # print(" Next token ids:", next_ids) print( " Next tokens:", tokenizer.batch_decode(next_ids.unsqueeze(1), skip_special_tokens=False), ) # print(" Logits (first 5):", logits[0, :5].cpu().numpy() if logits.shape[0] > 0 else None) if finished.all(): print("[DEBUG] All sequences finished.") return tgt_ids print("[DEBUG] Max steps reached.") return tgt_ids