| """MLM head for the hybrid MLM/CLM objective. |
| |
| Ported from temp/gpt-bert-main/pretraining/model.py MaskClassifier, but |
| configurable to match the trunk's customizability: |
| - norm_type: "layernorm" or "rmsnorm" (reuses the existing field). |
| - ffn_activation: "gelu", "swiglu", "relu", or "identity" (reuses the |
| existing field). The head's nonlinearity follows this choice. |
| - use_bias_ffn: whether the linear layers in the head use bias. |
| - weight tying: the final linear is tied to the word embedding, matching |
| gpt-bert's MaskClassifier and our lm_head. |
| |
| The head is controlled by model config (mlm_head_enabled) so checkpoint |
| loading and HF export remain strict and honest. |
| """ |
|
|
| import torch |
| import torch.nn as nn |
|
|
| from .transformer_components import RMSNorm, FeedForward |
|
|
|
|
| def _create_norm(norm_type: str, hidden_dim: int) -> nn.Module: |
| if norm_type == "layernorm": |
| return nn.LayerNorm(hidden_dim) |
| if norm_type == "rmsnorm": |
| return RMSNorm(hidden_dim) |
| raise ValueError(f"Unsupported norm_type: {norm_type}") |
|
|
|
|
| class MaskClassifier(nn.Module): |
| """MLM prediction head with configurable norm and activation. |
| |
| Architecture (matching gpt-bert's MaskClassifier): |
| norm -> FeedForward (activation, bias configurable) -> norm -> dropout -> linear (tied) |
| |
| The final linear is tied to the word embedding weight, matching |
| gpt-bert's MaskClassifier and our lm_head. |
| |
| Args: |
| hidden_dim: Model hidden dimension. |
| vocab_size: Vocabulary size for the output projection. |
| norm_type: "layernorm" or "rmsnorm". |
| ffn_activation: "gelu", "swiglu", "relu", or "identity". |
| use_bias_ffn: Whether the intermediate linear layers use bias. |
| dropout: Dropout probability. |
| word_embedding: The word embedding weight to tie the final linear to. |
| """ |
|
|
| def __init__( |
| self, |
| hidden_dim: int, |
| vocab_size: int, |
| norm_type: str, |
| ffn_activation: str, |
| use_bias_ffn: bool, |
| dropout: float, |
| word_embedding: nn.Parameter, |
| ): |
| super().__init__() |
| self.hidden_dim = hidden_dim |
| self.vocab_size = vocab_size |
| self.norm_type = norm_type |
| self.ffn_activation = ffn_activation |
| self.use_bias_ffn = use_bias_ffn |
| self.dropout_p = dropout |
|
|
| self.norm1 = _create_norm(norm_type, hidden_dim) |
| self.feed_forward = FeedForward( |
| input_dim=hidden_dim, |
| hidden_dim=hidden_dim, |
| dropout=dropout, |
| activation=ffn_activation, |
| use_bias=use_bias_ffn, |
| ) |
| self.norm2 = _create_norm(norm_type, hidden_dim) |
| self.dropout = nn.Dropout(dropout) |
| self.linear_out = nn.Linear(hidden_dim, vocab_size, bias=False) |
| self.linear_out.weight = word_embedding |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| """Apply the MLM head to contextualized embeddings. |
| |
| Args: |
| x: Contextualized embeddings [batch, seq_len, hidden_dim] or |
| flattened [num_masked, hidden_dim]. |
| |
| Returns: |
| Logits [batch, seq_len, vocab_size] or [num_masked, vocab_size]. |
| """ |
| x = self.norm1(x) |
| x = self.feed_forward(x) |
| x = self.norm2(x) |
| x = self.dropout(x) |
| x = self.linear_out(x) |
| return x |
|
|