| """Minimal self-attention core for decoder language models.""" |
|
|
| import math |
| from typing import Optional, Tuple |
|
|
| import torch |
| import torch.nn as nn |
|
|
| from .transformer_components import PositionalInfo, apply_rotary_emb |
|
|
|
|
| def _activate_scores(scores: torch.Tensor, activation: str) -> torch.Tensor: |
| if activation == "softmax": |
| return torch.softmax(scores, dim=-1) |
| if activation == "identity": |
| return scores |
| if activation == "relu": |
| return torch.relu(scores) |
| if activation == "tanh": |
| return torch.tanh(scores) |
| if activation == "sigmoid": |
| return torch.sigmoid(scores) |
| if activation == "gelu": |
| return torch.nn.functional.gelu(scores) |
| raise ValueError(f"Unsupported attention activation: {activation}") |
|
|
|
|
| class MultiHeadAttentionBase(nn.Module): |
| def __init__( |
| self, |
| query_dim: int, |
| output_dim: int, |
| key_dim: Optional[int] = None, |
| value_dim: Optional[int] = None, |
| n_heads: int = 4, |
| hidden_dim: int = 64, |
| dropout: float = 0.0, |
| total_n_heads: Optional[int] = None, |
| activation: str = "softmax", |
| use_bias_qkv: bool = False, |
| use_bias_out: bool = True, |
| ): |
| super().__init__() |
| self.query_dim = query_dim |
| self.key_dim = query_dim if key_dim is None else key_dim |
| self.value_dim = self.key_dim if value_dim is None else value_dim |
| self.output_dim = output_dim |
| self.n_heads = n_heads |
| self.hidden_dim = hidden_dim |
| self.total_n_heads = n_heads if total_n_heads is None else total_n_heads |
| self.activation = activation |
| self.use_bias_qkv = use_bias_qkv |
| self.use_bias_out = use_bias_out |
|
|
| if self.total_n_heads <= 0: |
| raise ValueError(f"total_n_heads must be positive, got {self.total_n_heads}") |
| if hidden_dim % self.total_n_heads != 0: |
| raise ValueError( |
| f"hidden_dim ({hidden_dim}) must be divisible by total_n_heads ({self.total_n_heads})" |
| ) |
|
|
| self.head_dim = hidden_dim // self.total_n_heads |
| self.scale = 1.0 / math.sqrt(self.head_dim) |
| projected_dim = n_heads * self.head_dim |
| self.q_proj = nn.Linear(self.query_dim, projected_dim, bias=use_bias_qkv) |
| self.k_proj = nn.Linear(self.key_dim, projected_dim, bias=use_bias_qkv) |
| self.v_proj = nn.Linear(self.value_dim, projected_dim, bias=use_bias_qkv) |
| self.o_proj = nn.Linear(projected_dim, output_dim, bias=use_bias_out) |
| self.dropout = nn.Dropout(dropout) |
| self.attn_dropout = nn.Dropout(dropout) |
| self.last_attn_weights = None |
|
|
| self._init_weights() |
|
|
| def _init_weights(self) -> None: |
| gain = 1.0 / math.sqrt(2.0) |
| for projection in (self.q_proj, self.k_proj, self.v_proj, self.o_proj): |
| nn.init.xavier_uniform_(projection.weight, gain=gain) |
| if projection.bias is not None: |
| nn.init.zeros_(projection.bias) |
|
|
| def _reshape_for_multihead(self, x: torch.Tensor, batch_size: int, seq_len: int) -> torch.Tensor: |
| x = x.view(batch_size, seq_len, self.n_heads, self.head_dim) |
| return x.transpose(1, 2) |
|
|
| def _process_mask(self, mask: Optional[torch.Tensor]) -> Optional[torch.Tensor]: |
| if mask is None: |
| return None |
| if mask.dim() == 2: |
| if mask.shape[0] != mask.shape[1]: |
| raise ValueError( |
| "2D decoder attention masks must be square [seq, seq]; " |
| f"got {tuple(mask.shape)}. Expand padding masks at the LM boundary." |
| ) |
| return mask.bool().unsqueeze(0).unsqueeze(0) |
| if mask.dim() == 3: |
| return mask.bool().unsqueeze(1) |
| raise ValueError(f"Mask must be 2D or 3D, got {mask.dim()}D") |
|
|
| def _compute_attn_scores( |
| self, |
| q: torch.Tensor, |
| k: torch.Tensor, |
| pos_info: Optional[PositionalInfo] = None, |
| ) -> torch.Tensor: |
| if pos_info is not None and pos_info.rope_freqs is not None: |
| freqs_cos, freqs_sin = pos_info.rope_freqs |
| q, k = apply_rotary_emb(q, k, freqs_cos, freqs_sin) |
| return torch.matmul(q, k.transpose(-2, -1)) * self.scale |
|
|
| def _apply_activation_and_mask(self, scores: torch.Tensor, mask: Optional[torch.Tensor]) -> torch.Tensor: |
| processed_mask = self._process_mask(mask) |
| if processed_mask is not None and self.activation in {"softmax", "sigmoid", "tanh"}: |
| scores.masked_fill_(~processed_mask, torch.finfo(scores.dtype).min) |
| weights = _activate_scores(scores, self.activation) |
| if processed_mask is not None: |
| weights = weights.masked_fill(~processed_mask, 0.0) |
| return weights |
|
|
| def _apply_mask(self, scores: torch.Tensor, mask: Optional[torch.Tensor]) -> torch.Tensor: |
| return self._apply_activation_and_mask(scores, mask) |
|
|
| def forward( |
| self, |
| query: torch.Tensor, |
| key: Optional[torch.Tensor] = None, |
| value: Optional[torch.Tensor] = None, |
| mask: Optional[torch.Tensor] = None, |
| pos_info: Optional[PositionalInfo] = None, |
| ) -> Tuple[torch.Tensor, torch.Tensor]: |
| batch_size, seq_len, _ = query.shape |
| key = query if key is None else key |
| value = key if value is None else value |
| key_len = key.shape[1] |
|
|
| if key.shape[0] != batch_size or value.shape[0] != batch_size: |
| raise ValueError( |
| f"Batch size mismatch: query={batch_size}, key={key.shape[0]}, value={value.shape[0]}" |
| ) |
| if key.shape[1] != value.shape[1]: |
| raise ValueError( |
| f"Key and value sequence length mismatch: {key.shape[1]} vs {value.shape[1]}" |
| ) |
|
|
| q = self.q_proj(query) |
| k = self.k_proj(key) |
| v = self.v_proj(value) |
|
|
| q = self._reshape_for_multihead(q, batch_size, seq_len) |
| k = self._reshape_for_multihead(k, batch_size, key_len) |
| v = self._reshape_for_multihead(v, batch_size, key_len) |
|
|
| attn_scores = self._compute_attn_scores(q, k, pos_info) |
| attn_weights = self._apply_activation_and_mask(attn_scores, mask) |
| attn_weights = self.attn_dropout(attn_weights) |
|
|
| attn_output = torch.matmul(attn_weights, v) |
| attn_output = attn_output.transpose(1, 2).contiguous() |
| attn_output = attn_output.view(batch_size, seq_len, self.n_heads * self.head_dim) |
| attn_output = self.o_proj(attn_output) |
| attn_output = self.dropout(attn_output) |
|
|
| self.last_attn_weights = attn_weights.detach() |
| return attn_output, attn_weights |
|
|
|
|
| class SelfAttention(MultiHeadAttentionBase): |
| def __init__( |
| self, |
| input_dim: int, |
| n_heads: int = 4, |
| hidden_dim: int = 64, |
| dropout: float = 0.0, |
| supports_relative: bool = False, |
| total_n_heads: Optional[int] = None, |
| use_bias_qkv: bool = False, |
| use_bias_out: bool = True, |
| ): |
| head_dim = hidden_dim // (n_heads if total_n_heads is None else total_n_heads) |
| super().__init__( |
| query_dim=input_dim, |
| output_dim=n_heads * head_dim, |
| key_dim=input_dim, |
| value_dim=input_dim, |
| n_heads=n_heads, |
| hidden_dim=hidden_dim, |
| dropout=dropout, |
| total_n_heads=total_n_heads, |
| activation="softmax", |
| use_bias_qkv=use_bias_qkv, |
| use_bias_out=use_bias_out, |
| ) |
| self.supports_relative = supports_relative |
| if self.supports_relative: |
| self.rel_k_proj = nn.Linear(self.head_dim, self.head_dim, bias=False) |
| self.rel_v_proj = nn.Linear(self.head_dim, self.head_dim, bias=False) |
| nn.init.xavier_uniform_(self.rel_k_proj.weight) |
| nn.init.xavier_uniform_(self.rel_v_proj.weight) |
|
|
| def forward( |
| self, |
| x: torch.Tensor, |
| mask: Optional[torch.Tensor] = None, |
| pos_info: Optional[PositionalInfo] = None, |
| ) -> Tuple[torch.Tensor, torch.Tensor]: |
| if self.supports_relative and pos_info is not None and pos_info.rel_embeddings is not None: |
| batch_size, seq_len, _ = x.shape |
| rel_embeddings = pos_info.rel_embeddings |
| if rel_embeddings.shape[:2] != (seq_len, seq_len): |
| raise ValueError( |
| f"Relative embeddings shape {rel_embeddings.shape} does not match sequence length {seq_len}" |
| ) |
| if rel_embeddings.shape[2] != self.head_dim: |
| raise ValueError( |
| f"Relative embeddings dim {rel_embeddings.shape[2]} does not match head_dim {self.head_dim}" |
| ) |
|
|
| rel_k = self.rel_k_proj(rel_embeddings) |
| rel_v = self.rel_v_proj(rel_embeddings) |
|
|
| q = self.q_proj(x) |
| k = self.k_proj(x) |
| v = self.v_proj(x) |
|
|
| q = self._reshape_for_multihead(q, batch_size, seq_len) |
| k = self._reshape_for_multihead(k, batch_size, seq_len) |
| v = self._reshape_for_multihead(v, batch_size, seq_len) |
|
|
| attn_scores = self._compute_attn_scores(q, k, pos_info) |
| rel_scores = torch.einsum("bhid,ijd->bhij", q, rel_k) * self.scale |
| attn_scores = attn_scores + rel_scores |
|
|
| attn_weights = self._apply_mask(attn_scores, mask) |
| attn_weights = self.attn_dropout(attn_weights) |
|
|
| attn_output = torch.matmul(attn_weights, v) |
| rel_output = torch.einsum("bhij,ijd->bhid", attn_weights, rel_v) |
| attn_output = attn_output + rel_output |
|
|
| attn_output = attn_output.transpose(1, 2).contiguous() |
| attn_output = attn_output.view(batch_size, seq_len, self.n_heads * self.head_dim) |
| attn_output = self.o_proj(attn_output) |
| attn_output = self.dropout(attn_output) |
| self.last_attn_weights = attn_weights.detach() |
| return attn_output, attn_weights |
|
|
| return super().forward(query=x, mask=mask, pos_info=pos_info) |
|
|
|
|
| class CrossAttention(MultiHeadAttentionBase): |
| def __init__( |
| self, |
| input_dim: int, |
| output_dim: int, |
| context_dim: Optional[int] = None, |
| n_heads: int = 8, |
| hidden_dim: int = 64, |
| dropout: float = 0.0, |
| supports_relative: bool = False, |
| use_bias_qkv: bool = False, |
| use_bias_out: bool = True, |
| ): |
| resolved_context_dim = input_dim if context_dim is None else context_dim |
| super().__init__( |
| query_dim=input_dim, |
| key_dim=resolved_context_dim, |
| value_dim=resolved_context_dim, |
| output_dim=output_dim, |
| n_heads=n_heads, |
| hidden_dim=hidden_dim, |
| dropout=dropout, |
| activation="softmax", |
| use_bias_qkv=use_bias_qkv, |
| use_bias_out=use_bias_out, |
| ) |
| self.supports_relative = supports_relative |
| if supports_relative: |
| self.rel_k_proj = nn.Linear(self.head_dim, self.head_dim, bias=False) |
| self.rel_v_proj = nn.Linear(self.head_dim, self.head_dim, bias=False) |
| nn.init.xavier_uniform_(self.rel_k_proj.weight) |
| nn.init.xavier_uniform_(self.rel_v_proj.weight) |
|
|
| def forward( |
| self, |
| inputs: torch.Tensor, |
| context: torch.Tensor, |
| mask: Optional[torch.Tensor] = None, |
| pos_info: Optional[PositionalInfo] = None, |
| ) -> Tuple[torch.Tensor, torch.Tensor]: |
| if self.supports_relative and pos_info is not None and pos_info.rel_embeddings is not None: |
| batch_size, seq_len_q, _ = inputs.shape |
| _, seq_len_k, _ = context.shape |
| rel_embeddings = pos_info.rel_embeddings |
| if rel_embeddings.shape[:2] != (seq_len_q, seq_len_k): |
| raise ValueError( |
| f"Relative embeddings shape {rel_embeddings.shape} does not match " |
| f"sequence lengths {(seq_len_q, seq_len_k)}" |
| ) |
| if rel_embeddings.shape[2] != self.head_dim: |
| raise ValueError( |
| f"Relative embeddings dim {rel_embeddings.shape[2]} does not match head_dim {self.head_dim}" |
| ) |
|
|
| rel_k = self.rel_k_proj(rel_embeddings) |
| rel_v = self.rel_v_proj(rel_embeddings) |
| q = self._reshape_for_multihead(self.q_proj(inputs), batch_size, seq_len_q) |
| k = self._reshape_for_multihead(self.k_proj(context), batch_size, seq_len_k) |
| v = self._reshape_for_multihead(self.v_proj(context), batch_size, seq_len_k) |
| attn_scores = self._compute_attn_scores(q, k, pos_info) |
| attn_scores = attn_scores + torch.einsum("bhid,ijd->bhij", q, rel_k) * self.scale |
| attn_weights = self._apply_activation_and_mask(attn_scores, mask) |
| attn_weights = self.attn_dropout(attn_weights) |
| self.last_attn_weights = attn_weights.detach() |
| attn_output = torch.matmul(attn_weights, v) |
| attn_output = attn_output + torch.einsum("bhij,ijd->bhid", attn_weights, rel_v) |
| attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len_q, -1) |
| attn_output = self.o_proj(attn_output) |
| attn_output = self.dropout(attn_output) |
| return attn_output, attn_weights |
|
|
| return super().forward( |
| query=inputs, |
| key=context, |
| value=context, |
| mask=mask, |
| pos_info=pos_info, |
| ) |
|
|