Encoder-Decoder Architecture › Anatomy of a Decoder Block with Cross-Attention

Michael BrenndoerferUpdated June 20, 20256 min read
Language AI HandbookAll chapters
Part 16 of 67Transformer Architectures
Chapter 113Encoder-Decoder Architecture › Anatomy of a Decoder Block with Cross-Attention
Reading section 4 of 13 · Anatomy of a Decoder Block with Cross-Attention · 6 minSection 4 of 13 · 6 min

Book contents

Section 4 of 13 · 6 min read

Encoder-Decoder Architecture › Anatomy of a Decoder Block with Cross-AttentionOne section · Sections in this chapter4 / 13 ↓
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

Understanding the decoder block requires recognizing that it must accomplish two distinct but interrelated tasks: it must generate coherent, grammatical output (like any language model), while also staying faithful to the source sequence (which distinguishes translation from free generation). The architecture addresses these requirements through a carefully ordered sequence of three sublayers.

Sublayer 1: Masked Self-Attention. The decoder first attends to its own previous outputs. When generating the fourth target word, it considers the first three words it has already produced. This builds a representation of the generation context so far. The attention is masked (causal) to prevent the decoder from seeing future tokens, which is essential for autoregressive generation where we produce one token at a time.

Sublayer 2: Cross-Attention. Armed with context about what it has generated, the decoder now queries the encoder. This is where source information enters. The decoder's refined representation is queries, and the encoder's output provides keys and values. The cross-attention sublayer answers: "Given what I've generated so far, what source information is relevant for the next token?"

Sublayer 3: Feed-Forward Network. Finally, a position-wise FFN applies nonlinear transformation to each position independently. This adds representational capacity and allows the model to process the combined information from self-attention and cross-attention.

The ordering matters: self-attention establishes generation context, cross-attention enriches it with source information, and the FFN transforms the result. Each sublayer includes residual connections and layer normalization. This ensures stable gradient flow through deep stacks.

In[10]:
Code
def rms_norm(x, gamma, eps=1e-6):
    """RMS Layer Normalization."""
    rms = np.sqrt(np.mean(x**2, axis=-1, keepdims=True) + eps)
    return gamma * (x / rms)


def gelu(x):
    """Gaussian Error Linear Unit activation."""
    return 0.5 * x * (1 + np.tanh(np.sqrt(2 / np.pi) * (x + 0.044715 * x**3)))


def create_causal_mask(seq_len):
    """Create causal mask for decoder self-attention."""
    mask = np.triu(np.ones((seq_len, seq_len)) * float("-inf"), k=1)
    return mask


class MultiHeadAttention:
    """Multi-head attention supporting both self and cross attention."""

    def __init__(self, d_model, n_heads):
        self.d_model = d_model
        self.n_heads = n_heads
        self.d_k = d_model // n_heads

        scale = np.sqrt(2.0 / (d_model + self.d_k))
        self.W_q = np.random.randn(d_model, d_model) * scale
        self.W_k = np.random.randn(d_model, d_model) * scale
        self.W_v = np.random.randn(d_model, d_model) * scale
        self.W_o = np.random.randn(d_model, d_model) * scale

    def __call__(self, query_input, key_value_input=None, mask=None):
        """
        Apply multi-head attention.

        For self-attention: key_value_input is None, uses query_input for K, V
        For cross-attention: key_value_input provides K, V from encoder
        """
        if key_value_input is None:
            key_value_input = query_input

        q_len = query_input.shape[0]
        kv_len = key_value_input.shape[0]

        # Project to Q, K, V
        Q = query_input @ self.W_q
        K = key_value_input @ self.W_k
        V = key_value_input @ self.W_v

        # Reshape for multi-head
        Q = Q.reshape(q_len, self.n_heads, self.d_k).transpose(1, 0, 2)
        K = K.reshape(kv_len, self.n_heads, self.d_k).transpose(1, 0, 2)
        V = V.reshape(kv_len, self.n_heads, self.d_k).transpose(1, 0, 2)

        # Compute attention scores
        scores = Q @ K.transpose(0, 2, 1) / np.sqrt(self.d_k)

        if mask is not None:
            scores = scores + mask

        weights = softmax(scores, axis=-1)
        attended = weights @ V

        # Reshape and project
        attended = attended.transpose(1, 0, 2).reshape(q_len, self.d_model)
        output = attended @ self.W_o

        return output, weights


class FeedForward:
    """Position-wise feed-forward network."""

    def __init__(self, d_model, d_ff):
        scale1 = np.sqrt(2.0 / (d_model + d_ff))
        scale2 = np.sqrt(2.0 / (d_ff + d_model))

        self.W1 = np.random.randn(d_model, d_ff) * scale1
        self.b1 = np.zeros(d_ff)
        self.W2 = np.random.randn(d_ff, d_model) * scale2
        self.b2 = np.zeros(d_model)

    def __call__(self, x):
        hidden = gelu(x @ self.W1 + self.b1)
        return hidden @ self.W2 + self.b2


class EncoderDecoderBlock:
    """
    Decoder block with cross-attention for encoder-decoder architecture.

    Contains three sublayers:
    1. Masked self-attention (causal)
    2. Cross-attention to encoder
    3. Feed-forward network
    """

    def __init__(self, d_model, n_heads, d_ff):
        self.d_model = d_model

        # Three attention/FFN sublayers
        self.self_attn = MultiHeadAttention(d_model, n_heads)
        self.cross_attn = MultiHeadAttention(d_model, n_heads)
        self.ffn = FeedForward(d_model, d_ff)

        # Three layer norms (pre-norm configuration)
        self.norm1_gamma = np.ones(d_model)
        self.norm2_gamma = np.ones(d_model)
        self.norm3_gamma = np.ones(d_model)

    def __call__(self, x, encoder_output, self_attn_mask=None):
        """
        Forward pass through decoder block.

        Args:
            x: Decoder input of shape (tgt_len, d_model)
            encoder_output: Encoder output of shape (src_len, d_model)
            self_attn_mask: Causal mask for self-attention

        Returns:
            Output of shape (tgt_len, d_model)
        """
        # Sublayer 1: Masked self-attention
        normed = rms_norm(x, self.norm1_gamma)
        self_attn_out, _ = self.self_attn(normed, mask=self_attn_mask)
        x = x + self_attn_out

        # Sublayer 2: Cross-attention to encoder
        normed = rms_norm(x, self.norm2_gamma)
        cross_attn_out, cross_weights = self.cross_attn(
            normed, key_value_input=encoder_output
        )
        x = x + cross_attn_out

        # Sublayer 3: Feed-forward
        normed = rms_norm(x, self.norm3_gamma)
        ffn_out = self.ffn(normed)
        x = x + ffn_out

        return x, cross_weights
Out[11]:
Console
Encoder-Decoder Block Test
--------------------------------------------------
Encoder output (source): (6, 256)
Decoder input (target): (4, 256)
Decoder output: (4, 256)

Cross-attention weights: (8, 4, 6)
  (n_heads, tgt_len, src_len)

Each target position attends to all source positions.
Cross-attention weight sums (should be 1.0):
  [1. 1. 1. 1.]

The decoder block successfully processes target embeddings while attending to encoder output. The cross-attention weights have shape (n_heads, tgt_len, src_len), showing that each of the 8 attention heads learns independent patterns for how target positions query source positions. Weight sums of 1.0 across each target position confirm proper softmax normalization. The output maintains the same shape as the input (tgt_len, d_model), ready to be passed to subsequent decoder layers.

Out[12]:
Visualization
Heatmap showing head 1 attention weights.
Head 1 cross-attention pattern.
Heatmap showing head 2 attention weights.
Head 2 cross-attention pattern.
Heatmap showing head 3 attention weights.
Head 3 cross-attention pattern.
Heatmap showing head 4 attention weights.
Head 4 cross-attention pattern.

Each attention head learns distinct patterns for how target positions attend to source positions. Some heads may specialize in local alignment while others capture longer-range dependencies.