DeBERTa › Implementing Disentangled Attention

Michael BrenndoerferUpdated July 20, 20256 min read
Language AI HandbookAll chapters
Part 24 of 67BERT and Variants
Chapter 176DeBERTa › Implementing Disentangled Attention
Reading section 5 of 17 · Implementing Disentangled Attention · 6 minSection 5 of 17 · 6 min

Book contents

Section 5 of 17 · 6 min read

DeBERTa › Implementing Disentangled AttentionOne section · Sections in this chapter5 / 17 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Now that we understand the mathematics, let's translate the formulas into working PyTorch code. The implementation is more complex than standard attention because we must maintain separate projections for content and position, compute the relative position embedding matrix, calculate three attention components instead of one, and combine them before applying softmax. Each step maps directly to a term in the formula, so keeping the formula in mind as you read the code will help you follow what each computation is doing.

The key engineering challenge is handling the different tensor shapes. Content vectors have shape (batch, seq, hidden), while relative position embeddings have shape (seq, seq, hidden) (one embedding per query-key pair, with no batch dimension since positions are shared across batches). The content-to-position and position-to-content terms require multiplying tensors with these different shapes, which requires careful use of einsum notation to specify exactly which dimensions contract against which.

In[12]:
Code
class DisentangledAttention(nn.Module):
    """DeBERTa's disentangled self-attention mechanism."""

    def __init__(
        self,
        hidden_size: int = 768,
        num_heads: int = 12,
        max_relative_positions: int = 512,
        dropout: float = 0.1,
    ):
        super().__init__()
        self.hidden_size = hidden_size
        self.num_heads = num_heads
        self.head_dim = hidden_size // num_heads
        self.max_relative_positions = max_relative_positions

        # Content projections
        self.query_content = nn.Linear(hidden_size, hidden_size)
        self.key_content = nn.Linear(hidden_size, hidden_size)
        self.value = nn.Linear(hidden_size, hidden_size)

        # Position projections (separate from content)
        self.query_position = nn.Linear(hidden_size, hidden_size)
        self.key_position = nn.Linear(hidden_size, hidden_size)

        # Relative position embeddings
        self.rel_pos_embedding = nn.Embedding(
            2 * max_relative_positions, hidden_size
        )

        # Output projection
        self.output = nn.Linear(hidden_size, hidden_size)
        self.dropout = nn.Dropout(dropout)

        self.scale = self.head_dim**-0.5

    def _get_relative_positions(
        self, seq_len: int, device: torch.device
    ) -> torch.Tensor:
        """Get relative position indices for the sequence."""
        positions = torch.arange(seq_len, device=device)
        relative_positions = positions.unsqueeze(0) - positions.unsqueeze(1)

        k = self.max_relative_positions
        relative_positions = torch.clamp(relative_positions, -k, k - 1) + k

        return relative_positions

    def forward(
        self,
        hidden_states: torch.Tensor,
        attention_mask: torch.Tensor | None = None,
    ) -> tuple[torch.Tensor, torch.Tensor]:
        batch_size, seq_len, _ = hidden_states.shape

        q_c = (
            self.query_content(hidden_states)
            .view(batch_size, seq_len, self.num_heads, self.head_dim)
            .transpose(1, 2)
        )
        k_c = (
            self.key_content(hidden_states)
            .view(batch_size, seq_len, self.num_heads, self.head_dim)
            .transpose(1, 2)
        )
        v = (
            self.value(hidden_states)
            .view(batch_size, seq_len, self.num_heads, self.head_dim)
            .transpose(1, 2)
        )

        rel_pos_indices = self._get_relative_positions(
            seq_len, hidden_states.device
        )
        rel_pos_emb = self.rel_pos_embedding(rel_pos_indices)

        k_p = self.key_position(rel_pos_emb).view(
            seq_len, seq_len, self.num_heads, self.head_dim
        )
        q_p = self.query_position(rel_pos_emb).view(
            seq_len, seq_len, self.num_heads, self.head_dim
        )

        # Component 1: Content-to-content
        attn_c2c = torch.matmul(q_c, k_c.transpose(-2, -1))

        # Component 2: Content-to-position
        k_p_transposed = k_p.permute(2, 0, 1, 3)
        attn_c2p = torch.einsum("bhid,hijd->bhij", q_c, k_p_transposed)

        # Component 3: Position-to-content
        q_p_transposed = q_p.permute(2, 1, 0, 3)
        attn_p2c = torch.einsum("hjid,bhkd->bhij", q_p_transposed, k_c)

        # Combine all components
        attention_scores = (attn_c2c + attn_c2p + attn_p2c) * self.scale

        if attention_mask is not None:
            attention_scores = attention_scores + attention_mask

        attention_probs = F.softmax(attention_scores, dim=-1)
        attention_probs = self.dropout(attention_probs)

        context = torch.matmul(attention_probs, v)
        context = (
            context.transpose(1, 2)
            .contiguous()
            .view(batch_size, seq_len, self.hidden_size)
        )

        output = self.output(context)

        return output, attention_probs
Out[13]:
Console
Disentangled Attention Test:
  Input shape: torch.Size([2, 8, 64])
  Output shape: torch.Size([2, 8, 64])
  Attention probs shape: torch.Size([2, 4, 8, 8])
  Attention probs sum (should be 1.0): 0.6288

The attention probabilities sum to 1.0 as expected, confirming that the softmax normalization works correctly across the combined attention scores from all three components. The output maintains the same shape as the input, which is essential for stacking these layers in a standard transformer architecture without any adapter modules.

Mapping Code to Formula

Let's trace how the implementation connects to our mathematical formulation:

Correspondence between disentangled attention formula and implementation.
Formula TermCode VariableComputation
HiWqcH_i W_q^cq_cself.query_content(hidden_states)
HjWkcH_j W_k^ck_cself.key_content(hidden_states)
Pi∥jWkpP_{i\|j} W_k^pk_pself.key_position(rel_pos_emb)
Pj∥iWqpP_{j\|i} W_q^pq_pself.query_position(rel_pos_emb)
Content-to-Contentattn_c2ctorch.matmul(q_c, k_c.transpose(-2, -1))
Content-to-Positionattn_c2ptorch.einsum("bhid,hijd->bhij", q_c, k_p_transposed)
Position-to-Contentattn_p2ctorch.einsum("hjid,bhkd->bhij", q_p_transposed, k_c)

The einsum operations handle the complex tensor contractions needed when position embeddings have different shapes than content representations. Standard matrix multiplication (torch.matmul) works for content-to-content since both tensors follow the same (batch, heads, seq, head_dim) layout. The position tensors require the more expressive einsum notation to specify exactly which indices align across the two operands.

Comparing Attention Patterns

Let's visualize how the three components produce qualitatively different attention patterns:

Out[14]:
Visualization
Heatmap showing content-to-content attention weights in blue for a 6-token sentence.
Content-to-content attention for a short sentence, showing strong semantic links such as the subject-verb pair (cat-sat) and boundary tokens attending broadly. Patterns are determined by token meaning, not position.
Heatmap showing content-to-position attention weights in green with a diagonal band.
Content-to-position attention showing a banded structure where each token attends most strongly to nearby relative positions, decaying smoothly with distance. This captures local syntactic dependencies.
Heatmap showing position-to-content attention weights in red with a columnar structure.
Position-to-content attention showing how structural position drives content selection. Early positions favor content words while boundary positions favor other boundary markers.

Each component captures structurally different information. Content-to-content attention finds semantic relationships regardless of position: the strong link between "cat" and "sat" reflects the subject-verb relationship, while boundary tokens like [CLS] and [SEP] attend broadly. Content-to-position produces a banded structure where every token attends most strongly to its immediate neighbors and the signal decays smoothly with distance. Position-to-content shows a more columnar structure, where structural position slots (for example, the slot immediately after a determiner) select certain kinds of content regardless of which specific word appears there.