| """Dual-attention primitives for decoder language models.""" |
|
|
| import math |
| from typing import Optional |
|
|
| import torch |
| import torch.nn as nn |
|
|
| from .transformer_components import PositionalInfo |
| from .transformer_core import MultiHeadAttentionBase |
|
|
|
|
| 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 RelationalAttentionBase(MultiHeadAttentionBase): |
| def __init__( |
| self, |
| hidden_dim: int, |
| symbol_dim: int, |
| n_heads: int, |
| total_n_heads: int, |
| dropout: float = 0.0, |
| n_relations: Optional[int] = None, |
| rel_activation: str = "identity", |
| symmetric_rels: bool = False, |
| use_relative_positional_symbols: bool = False, |
| use_bias_qkv: bool = False, |
| use_bias_out: bool = True, |
| ): |
| head_dim = hidden_dim // total_n_heads |
| output_dim = n_heads * head_dim |
| super().__init__( |
| query_dim=hidden_dim, |
| output_dim=output_dim, |
| key_dim=hidden_dim, |
| value_dim=symbol_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.symbol_dim = symbol_dim |
| self.rel_activation = rel_activation |
| self.symmetric_rels = symmetric_rels |
| self.use_relative_positional_symbols = use_relative_positional_symbols |
| self.n_relations = n_heads if n_relations is None else n_relations |
| total_rel_dim = self.head_dim * n_heads |
| if total_rel_dim % self.n_relations != 0: |
| raise ValueError( |
| f"head_dim * n_heads ({total_rel_dim}) must be divisible by n_relations " |
| f"({self.n_relations})" |
| ) |
| self.rel_proj_dim = total_rel_dim // self.n_relations |
| self.rel_scale = 1.0 / math.sqrt(self.rel_proj_dim) |
| rel_total_dim = self.n_relations * self.rel_proj_dim |
| self.wq_rel = nn.Linear(hidden_dim, rel_total_dim, bias=False) |
| self.wk_rel = self.wq_rel if symmetric_rels else nn.Linear( |
| hidden_dim, |
| rel_total_dim, |
| bias=False, |
| ) |
| nn.init.xavier_uniform_(self.wq_rel.weight) |
| if self.wk_rel is not self.wq_rel: |
| nn.init.xavier_uniform_(self.wk_rel.weight) |
|
|
| def _compute_base_attention( |
| self, |
| x: torch.Tensor, |
| mask: Optional[torch.Tensor], |
| pos_info: Optional[PositionalInfo], |
| ) -> torch.Tensor: |
| batch_size, seq_len, _ = x.shape |
| q = self._reshape_for_multihead(self.q_proj(x), batch_size, seq_len) |
| k = self._reshape_for_multihead(self.k_proj(x), batch_size, seq_len) |
| scores = self._compute_attn_scores(q, k, pos_info) |
| return self._apply_activation_and_mask(scores, mask) |
|
|
| def compute_relational_scores( |
| self, |
| x: torch.Tensor, |
| mask: Optional[torch.Tensor], |
| return_as: str, |
| ) -> torch.Tensor: |
| batch_size, seq_len, _ = x.shape |
| q_rel = self.wq_rel(x).view(batch_size, seq_len, self.n_relations, self.rel_proj_dim) |
| k_rel = self.wk_rel(x).view(batch_size, seq_len, self.n_relations, self.rel_proj_dim) |
| q_rel = q_rel.transpose(1, 2) |
| k_rel = k_rel.transpose(1, 2) |
| relations = torch.matmul(q_rel, k_rel.transpose(-2, -1)) * self.rel_scale |
| processed_mask = self._process_mask(mask) |
| if self.rel_activation == "softmax" and processed_mask is not None: |
| relations.masked_fill_(~processed_mask, torch.finfo(relations.dtype).min) |
| relations = _activate_scores(relations, self.rel_activation) |
| if processed_mask is not None: |
| relations = relations.masked_fill(~processed_mask, 0.0) |
| if return_as == "vectors": |
| return relations.permute(0, 2, 3, 1) |
| if return_as == "scores": |
| return relations |
| raise ValueError(f"return_as must be 'vectors' or 'scores', got {return_as}") |
|
|
| def combine_attention_and_relations( |
| self, |
| attn_weights: torch.Tensor, |
| relation_scores: torch.Tensor, |
| ) -> torch.Tensor: |
| return attn_weights * relation_scores |
|
|
| def _process_symbols_with_attention( |
| self, |
| symbols: torch.Tensor, |
| attn_weights: torch.Tensor, |
| ) -> torch.Tensor: |
| batch_size, _, seq_len_q, seq_len_k = attn_weights.shape |
| values = self.v_proj(symbols) |
| if self.use_relative_positional_symbols: |
| values = values.view(seq_len_q, seq_len_k, self.n_heads, self.head_dim) |
| return torch.einsum("bhij,ijhd->bihd", attn_weights, values) |
|
|
| values = values.view(batch_size, seq_len_k, self.n_heads, self.head_dim) |
| values = values.transpose(1, 2) |
| output = torch.matmul(attn_weights, values) |
| return output.transpose(1, 2) |
|
|
| def _apply_output_projection(self, output: torch.Tensor) -> torch.Tensor: |
| batch_size, seq_len = output.shape[:2] |
| output = output.contiguous().view(batch_size, seq_len, self.output_dim) |
| output = self.o_proj(output) |
| output = self.dropout(output) |
| return output |
|
|
| def _validate_symbols(self, x: torch.Tensor, symbols: torch.Tensor) -> None: |
| if self.use_relative_positional_symbols: |
| seq_len = x.shape[1] |
| expected_shape = (seq_len, seq_len, self.symbol_dim) |
| if tuple(symbols.shape) != expected_shape: |
| raise ValueError( |
| f"Relative symbols must have shape {expected_shape}, got {tuple(symbols.shape)}" |
| ) |
|
|
|
|
| class RelationalAttention(RelationalAttentionBase): |
| def __init__( |
| self, |
| hidden_dim: int, |
| symbol_dim: int, |
| n_heads: int, |
| total_n_heads: int, |
| n_relations: int, |
| dropout: float = 0.0, |
| rel_activation: str = "identity", |
| symmetric_rels: bool = False, |
| use_relative_positional_symbols: bool = False, |
| use_bias_qkv: bool = False, |
| use_bias_out: bool = True, |
| ): |
| super().__init__( |
| hidden_dim=hidden_dim, |
| symbol_dim=symbol_dim, |
| n_heads=n_heads, |
| total_n_heads=total_n_heads, |
| dropout=dropout, |
| n_relations=n_relations, |
| rel_activation=rel_activation, |
| symmetric_rels=symmetric_rels, |
| use_relative_positional_symbols=use_relative_positional_symbols, |
| use_bias_qkv=use_bias_qkv, |
| use_bias_out=use_bias_out, |
| ) |
| self.wr_proj = nn.Parameter(torch.empty(n_heads, self.head_dim, n_relations)) |
| nn.init.xavier_uniform_(self.wr_proj) |
|
|
| def forward( |
| self, |
| x: torch.Tensor, |
| symbols: torch.Tensor, |
| mask: Optional[torch.Tensor], |
| pos_info: Optional[PositionalInfo], |
| ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]: |
| self._validate_symbols(x, symbols) |
| attn_weights = self._compute_base_attention(x, mask, pos_info) |
| attn_weights = self.attn_dropout(attn_weights) |
| relation_vectors = self.compute_relational_scores(x, mask, return_as="vectors") |
| attended_symbols = self._process_symbols_with_attention(symbols, attn_weights) |
| projected_relations = torch.einsum( |
| "bhij,bijr,hdr->bihd", |
| attn_weights, |
| relation_vectors, |
| self.wr_proj, |
| ) |
| output = self._apply_output_projection(attended_symbols + projected_relations) |
| self.last_attn_weights = attn_weights.detach() |
| return output, {"attention": attn_weights, "relations": relation_vectors} |
|
|
|
|
| class RelationalCrossAttention(MultiHeadAttentionBase): |
| def __init__( |
| self, |
| hidden_dim: int, |
| symbol_dim: int, |
| n_heads: int, |
| total_n_heads: int, |
| dropout: float = 0.0, |
| activation: str = "identity", |
| use_relative_positional_symbols: bool = False, |
| use_bias_qkv: bool = False, |
| use_bias_out: bool = True, |
| ): |
| head_dim = hidden_dim // total_n_heads |
| super().__init__( |
| query_dim=hidden_dim, |
| output_dim=n_heads * head_dim, |
| key_dim=hidden_dim, |
| value_dim=symbol_dim, |
| n_heads=n_heads, |
| hidden_dim=hidden_dim, |
| dropout=dropout, |
| total_n_heads=total_n_heads, |
| activation=activation, |
| use_bias_qkv=use_bias_qkv, |
| use_bias_out=use_bias_out, |
| ) |
| self.symbol_dim = symbol_dim |
| self.use_relative_positional_symbols = use_relative_positional_symbols |
|
|
| def forward( |
| self, |
| x: torch.Tensor, |
| symbols: torch.Tensor, |
| mask: Optional[torch.Tensor], |
| pos_info: Optional[PositionalInfo], |
| ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]: |
| if self.use_relative_positional_symbols: |
| batch_size, seq_len, _ = x.shape |
| expected_shape = (seq_len, seq_len, self.symbol_dim) |
| if tuple(symbols.shape) != expected_shape: |
| raise ValueError( |
| f"Relative symbols must have shape {expected_shape}, got {tuple(symbols.shape)}" |
| ) |
|
|
| q = self._reshape_for_multihead(self.q_proj(x), batch_size, seq_len) |
| k = self._reshape_for_multihead(self.k_proj(x), batch_size, seq_len) |
| values = self.v_proj(symbols).view(seq_len, seq_len, self.n_heads, self.head_dim) |
| scores = self._compute_attn_scores(q, k, pos_info) |
| weights = self._apply_activation_and_mask(scores, mask) |
| weights = self.attn_dropout(weights) |
| output = torch.einsum("bhij,ijhd->bihd", weights, values) |
| output = output.contiguous().view(batch_size, seq_len, self.output_dim) |
| output = self.o_proj(output) |
| output = self.dropout(output) |
| self.last_attn_weights = weights.detach() |
| return output, {"attention": weights} |
|
|
| output, weights = super().forward( |
| query=x, |
| key=x, |
| value=symbols, |
| mask=mask, |
| pos_info=pos_info, |
| ) |
| return output, {"attention": weights} |
|
|
|
|
| class DisentangledRelationalCrossAttention(RelationalAttentionBase): |
| def __init__( |
| self, |
| hidden_dim: int, |
| symbol_dim: int, |
| n_heads: int, |
| total_n_heads: int, |
| dropout: float = 0.0, |
| rel_activation: str = "identity", |
| use_relative_positional_symbols: bool = False, |
| use_bias_qkv: bool = False, |
| use_bias_out: bool = True, |
| ): |
| super().__init__( |
| hidden_dim=hidden_dim, |
| symbol_dim=symbol_dim, |
| n_heads=n_heads, |
| total_n_heads=total_n_heads, |
| dropout=dropout, |
| n_relations=None, |
| rel_activation=rel_activation, |
| symmetric_rels=False, |
| use_relative_positional_symbols=use_relative_positional_symbols, |
| use_bias_qkv=use_bias_qkv, |
| use_bias_out=use_bias_out, |
| ) |
|
|
| def forward( |
| self, |
| x: torch.Tensor, |
| symbols: torch.Tensor, |
| mask: Optional[torch.Tensor], |
| pos_info: Optional[PositionalInfo], |
| ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]: |
| self._validate_symbols(x, symbols) |
| attn_weights = self._compute_base_attention(x, mask, pos_info) |
| relation_scores = self.compute_relational_scores(x, mask, return_as="scores") |
| combined_weights = self.attn_dropout( |
| self.combine_attention_and_relations(attn_weights, relation_scores) |
| ) |
| output = self._process_symbols_with_attention(symbols, combined_weights) |
| output = self._apply_output_projection(output) |
| self.last_attn_weights = attn_weights.detach() |
| return output, { |
| "attention": attn_weights, |
| "relations": relation_scores, |
| "combined": combined_weights, |
| } |
|
|