Skip to content

Decoder Architecture ​

The decoder predicts output tokens one at a time, using causal masking to maintain the autoregressive constraint and cross-attention to incorporate encoder context.

Three-Layer Decoder Block Overview ​

Unlike the encoder from Module 5, which processes the entire input sequence bidirectionally, the decoder operates autoregressively and must maintain causality: each output token depends only on previously generated tokens, not future ones. The standard decoder block contains three main layers:

Key differences from the encoder:

  • Causal masking in self-attention prevents attending to future positions
  • Cross-attention layer allows the decoder to attend to encoder outputs
  • Each position processes sequentially during inference (though training uses teacher forcing)
  • Output is fed back as input for the next token generation step

Causal (Masked) Self-Attention ​

The Autoregressive Constraint ​

The decoder must satisfy a fundamental constraint: position i can only attend to positions j where j ≤ i. This ensures that when predicting token t, the model only uses information from tokens 1 through t-1 and the encoder.

Why is this necessary? Because during inference, future tokens don't exist yet. The model generates one token at a time, and each new token must depend only on what came before.

Mask Implementation ​

Causal masking is implemented by modifying the attention scores before the softmax operation. For an attention score matrix of shape (seq_len, seq_len), we create a triangular mask:

python
# Create causal mask (lower triangular matrix)
seq_len = 5
mask = torch.tril(torch.ones((seq_len, seq_len)))
# mask[i, j] = 1 if j <= i (can attend), 0 if j > i (cannot attend)

# Apply mask during attention computation:
# Before softmax, set masked positions to -∞
scores = Q @ K.T / sqrt(d_k)
scores = scores.masked_fill(mask == 0, float('-inf'))
attention_weights = softmax(scores, dim=-1)

Visualization of causal mask for sequence length 5:

Position: 0 1 2 3 4
       0 [✓ ✗ ✗ ✗ ✗]
       1 [✓ ✓ ✗ ✗ ✗]
       2 [✓ ✓ ✓ ✗ ✗]
       3 [✓ ✓ ✓ ✓ ✗]
       4 [✓ ✓ ✓ ✓ ✓]

Where ✓ means "can attend to" and ✗ means "masked out."

Causal mask heatmap showing the lower triangular attention pattern for a 6x6 sequence

Animated visualization of causal mask growing as tokens are generated

Training vs. Inference ​

Training with Teacher Forcing:

python
# During training, we have the ground truth sequence [START, w1, w2, w3, STOP]
# We can pass all positions through the decoder at once (parallel processing)
decoder_output = decoder(encoder_output, target_tokens)
# The causal mask ensures position i only attends to positions < i

# Loss computed on all positions simultaneously
loss = cross_entropy(decoder_output, target_tokens)

Inference (Autoregressive Generation):

python
output_tokens = [START_TOKEN]
while True:
    # Pass all accumulated tokens
    logits = decoder(encoder_output, output_tokens)

    # Take the logits of the last position only
    next_token_logits = logits[-1, :]
    next_token = argmax(next_token_logits)

    output_tokens.append(next_token)
    if next_token == STOP_TOKEN or len(output_tokens) > max_length:
        break

return output_tokens

This difference is critical: training is efficient (all positions in parallel), but inference is sequential (one token at a time).

KV-Cache: Optimization for Fast Inference ​

Computing attention from scratch at each generation step is wasteful. Position 5 recomputes attention over positions 1-4, which we already computed at step 4. The KV-cache solves this:

python
# Instead of recomputing K and V for all previous positions,
# we cache them and only compute K and V for the new token

cache = {}  # Dictionary of cached K, V for each layer

for step in range(max_length):
    new_token_ids = output_tokens[-1:]  # Only the newest token

    for layer_idx in range(num_layers):
        Q = compute_query(new_token_ids)

        if layer_idx not in cache:
            cache[layer_idx] = {'K': None, 'V': None}

        K_new = compute_key(new_token_ids)
        V_new = compute_value(new_token_ids)

        # Concatenate with previous K, V
        K = torch.cat([cache[layer_idx]['K'], K_new], dim=1)
        V = torch.cat([cache[layer_idx]['V'], V_new], dim=1)

        # Normal attention computation
        attention_output = scaled_dot_product_attention(Q, K, V)

        # Update cache
        cache[layer_idx]['K'] = K
        cache[layer_idx]['V'] = V

Impact on performance:

  • Without cache: O(n²) computation per token (n = sequence length so far)
  • With cache: O(n) computation per token
  • Typical speedup: 4-10x faster inference on long sequences
  • Trade-off: Uses memory to store K, V for all previous tokens

Performance comparison graph showing O(n) vs O(n squared) computation time with and without KV-cache

Cross-Attention ​

The cross-attention layer allows the decoder to incorporate information from the encoder's output. This is what makes sequence-to-sequence models work.

Mechanism ​

In cross-attention:

  • Q (Query): Comes from the decoder (what the decoder is looking for)
  • K, V (Keys, Values): Come from the encoder (what information is available)
CrossAttention(Qdecoder,Kencoder,Vencoder)=softmax(QdecoderKencoderTdk)Vencoder

The decoder can now attend to any position in the encoder output without causal restrictions, since the encoder is fixed and non-autoregressive.

Intuition ​

Think of translation: "The cat sat on the mat" → "Le chat s'est assis sur le tapis"

As the decoder generates each French word, it attends to the encoder's understanding of the entire English sentence. The cross-attention layer asks: "Given what I just generated, what parts of the encoder's representation do I need?"

Key observation: While decoder self-attention is causal, cross-attention is not causal. The decoder can attend to any encoder position.

Autoregressive Generation ​

Generation Process ​

python
def generate(encoder_output, max_length=100, strategy='greedy'):
    batch_size = encoder_output.shape[0]

    # Initialize with start token
    current_tokens = torch.full((batch_size, 1), START_TOKEN_ID, dtype=torch.long)
    finished = torch.zeros(batch_size, dtype=torch.bool)

    for step in range(max_length):
        # Decode one token
        logits = decoder(encoder_output, current_tokens)
        next_token_logits = logits[:, -1, :]  # Shape: (batch_size, vocab_size)

        if strategy == 'greedy':
            next_tokens = next_token_logits.argmax(dim=-1, keepdim=True)
        elif strategy == 'beam_search':
            next_tokens = beam_search_step(next_token_logits, beam_width=5)
        elif strategy == 'sampling':
            probs = softmax(next_token_logits / temperature, dim=-1)
            next_tokens = torch.multinomial(probs, num_samples=1)

        # Update finished sequences
        finished |= (next_tokens == END_TOKEN_ID).squeeze(-1)
        next_tokens[finished] = PAD_TOKEN_ID

        # Append new tokens
        current_tokens = torch.cat([current_tokens, next_tokens], dim=1)

        if finished.all():
            break

    return current_tokens

Step-by-step visualization of autoregressive token generation showing how each token is predicted sequentially

Decoding Strategies ​

StrategyMethodQualitySpeedUse Case
Greedyargmax(logits)LowFastestBaselines, quick inference
Beam SearchKeep top-k sequencesMedium-HighSlowBest quality, machine translation
SamplingSample from distributionMediumFastDiverse outputs, creative tasks
Top-k/NucleusSample from filtered distributionMedium-HighFastBalanced quality and diversity

Encoder vs. Decoder vs. Encoder-Decoder ​

Different architectures suit different tasks:

ArchitectureExampleMaskingUse Case
Encoder-Only (BERT)Classification, NERBidirectional (none)Understand input without generating output
Decoder-Only (GPT, Llama)Text generationCausal maskingAutoregressive generation only
Encoder-Decoder (T5, BART)Translation, summarizationEncoder: none, Decoder: causalConditioning generation on input context

When to use each:

  • Encoder-only: Classification, question answering, sentiment analysis
  • Decoder-only: Open-ended generation, language modeling
  • Encoder-decoder: Seq2seq tasks where output depends on input (translation, summarization, paraphrasing)

Comparison of encoder-only, decoder-only, and encoder-decoder transformer architectures

KV-Cache for Efficient Inference (In-Depth) ​

Why Cache K and V? ​

During inference, the bottleneck is memory bandwidth, not computation. Computing attention scores involves many small operations that don't fully utilize GPU cores. Recomputing K and V for all previous tokens is redundant memory movement.

Trade-offs ​

Without cache:

  • Memory: Only store input + model parameters
  • Computation: Recompute K, V at each step
  • Latency per token: O(seq_len) (grows as sequence gets longer)

With cache:

  • Memory: Store K, V for all previous tokens (~2x more for 1st layer, less for deeper layers)
  • Computation: Only compute K, V for new token
  • Latency per token: Constant O(1) (independent of sequence length)

Typical requirements:

  • Caching for 2048-token context: ~4GB extra memory (for large models like LLaMA-7B)
  • Speedup: 5-20x faster depending on sequence length

Implementation Considerations ​

python
class DecoderWithCache(nn.Module):
    def forward(self, x, encoder_output, past_cache=None):
        new_cache = []

        # Self-attention with cache
        residual = x
        attn_out, self_cache = self.self_attn(x, past_cache[0] if past_cache else None)
        new_cache.append(self_cache)
        x = self.norm1(residual + attn_out)  # Residual

        # Cross-attention (no cache needed, encoder is fixed)
        residual = x
        cross_out = self.cross_attn(x, encoder_output, encoder_output)
        x = self.norm2(residual + cross_out)  # Residual

        # Feed-forward
        residual = x
        ffn_out = self.ffn(x)
        x = self.norm3(residual + ffn_out)  # Residual

        return x, new_cache

Production systems often use quantization with KV-cache to reduce memory requirements while maintaining inference speed.

Interview Questions ​

Q1: Explain why causal masking is necessary in the decoder. What happens if you remove it? ​

Answer: Causal masking enforces the autoregressive constraint: each token can only depend on previously generated tokens. This is necessary because:

  1. During inference, future tokens don't exist yet. The model generates one token at a time, and each token must be computable from previous tokens alone.

  2. Without causal masking, the model would "cheat" during training by attending to future tokens. This creates a train-test mismatch: the training objective doesn't match inference reality.

  3. Concrete example: Translating "Hello world" to French. At position 1 (generating "Bonjour"), if we allow attention to position 2 (which should be "monde"), the model learns to use information that won't be available during inference.

If you remove causal masking:

  • The model would perform better on training loss (it has access to all information)
  • It would fail catastrophically at inference (it tries to attend to non-existent tokens)

Q2: How does KV-cache reduce inference latency? What are the trade-offs? ​

Answer:The Problem: Without caching, computing the attention output for step t requires:

  • Computing K and V from all previous tokens (1 to t)
  • Computing Q from the new token
  • This is O(t) operations, growing with sequence length

The Solution: Store K and V from previous steps:

Step 1: Compute K₁, V₁, Q₁ → Output
Step 2: Reuse K₁, V₁ + compute K₂, V₂, Q₂ → Output
Step 3: Reuse K₁, K₂, V₁, V₂ + compute K₃, V₃, Q₃ → Output

Latency improvement: From O(n²) total (n steps of O(n) work) to O(n) total.

Trade-offs:

  • Memory: Must store all previous K, V. For a 7B model with 2K context, ~4GB extra.
  • Batch processing: Harder to batch different sequence lengths (need variable cache sizes).
  • Implementation complexity: Cache management adds engineering burden.

Decision: Always use in production for autoregressive inference. The latency gains far outweigh memory costs.

Q3: Why is cross-attention necessary? Can't the decoder just work with the encoder output directly? ​

Answer: Cross-attention is necessary for flexible alignment between encoder and decoder representations.

Consider a translation task: "The cat sat on the mat" → "Le chat s'est assis sur le tapis"

If the decoder only had the final encoder representation:

  • It's a fixed vector summarizing the entire input
  • As the decoder generates each word, it can't dynamically decide which parts of the input are relevant
  • Word alignment information is lost

With cross-attention:

  • The decoder can query different parts of the encoder for different words
  • When generating "chat" (cat), it attends heavily to the "cat" part of the encoder
  • This allows the model to learn explicit alignments

Why not just concatenate encoder output?

  • Cross-attention learns what to attend to—it's learned alignment, not fixed
  • This is far more flexible and generalizes better than fixed representations

Q4: What is the difference between teacher forcing and autoregressive generation? When do we use each? ​

Answer:Teacher Forcing (during training):

  • We have the correct sequence: "The cat sat on the mat"
  • We feed all tokens simultaneously to the decoder
  • The decoder learns to predict position i given ground truth positions 1 to i-1
  • All positions computed in parallel (very efficient)

Autoregressive Generation (during inference):

  • We don't have ground truth
  • We generate one token at a time
  • Each token is fed back as input for the next prediction
  • Sequential process (not parallelizable)

The Problem: This is a train-test mismatch. During training with teacher forcing:

  • Position 2 always sees correct token at position 1
  • Position 3 always sees correct tokens at positions 1-2
  • The model learns to depend on perfect previous predictions

During inference:

  • Position 2 sees the predicted token from position 1 (possibly wrong)
  • Position 3 sees predicted tokens from positions 1-2 (possibly wrong)
  • Errors compound, creating worse performance than training

Solutions:

  1. Scheduled sampling: Gradually replace ground truth with predictions during training
  2. Exposure bias training: Mix teacher-forced and predicted tokens during training
  3. Beam search: Keep multiple hypotheses to reduce the impact of early errors

Quick Reference Card ​

Decoder Block Structure ​

Masked Self-Attn → LayerNorm → Cross-Attn → LayerNorm → FFN → LayerNorm

Key Equations ​

Causal Mask Application:

Mask[i,j]={0if j≤i−∞if j>i

Cross-Attention:

Attention(Qdec,Kenc,Venc)=softmax(QdecKencTdk+M)Venc

Generation Pseudocode ​

output = [START]
while len(output) < max_len:
    logits = decoder(encoder_output, output)
    next_token = sample(logits[-1])
    output.append(next_token)
    if next_token == END:
        break

Architecture Comparison ​

FeatureEncoderDecoder
MaskingNone (bidirectional)Causal (autoregressive)
Self-Attention✓✓ (masked)
Cross-Attention✗✓
InputFull sequenceOne token at a time (inference)
ProcessingParallelParallel (train), Sequential (inference)

When to Use ​

  • Encoder-only: BERT, RoBERTa → Classification
  • Decoder-only: GPT, Llama → Generation
  • Encoder-Decoder: T5, BART → Seq2seq (translation, summarization)

Performance Optimizations ​

  1. KV-Cache: 5-20x faster inference (memory trade-off)
  2. Flash Attention: 2-4x faster attention computation
  3. Quantization: 4-8x smaller model with minimal quality loss
  4. Batch inference: Group requests for better GPU utilization

Common Pitfalls ​

  • Forgetting causal mask → train-test mismatch
  • Not using KV-cache → slow inference
  • Training only with teacher forcing → exposure bias
  • Beam search width too large → quadratic memory growth