abe123's picture
Squash clean branch history
1b7bd7b
Raw
History Blame Contribute Delete
12.5 kB
"""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,
}