Purpose
The original transformer was a sequence-to-sequence model, not a decoder-only language model. This note covers that original shape and the main idea that distinguishes it from plain self-attention stacks: cross-attention from the decoder into a separately encoded source sequence.
What Problem It Solves
Given source tokens and target tokens , model
This factorization says:
- the full source sequence is available
- the target is generated autoregressively
- each target step can condition on the whole source
That is exactly the structure of translation and many other transduction tasks.
Why Bahdanau Attention Matters Here
Before transformers, Bahdanau, Cho, and Bengio argued that the plain encoder-decoder architecture suffered from a fixed-length bottleneck. Their fix was to let the decoder form a context vector by softly attending over source annotations:
with
The transformer inherits this idea and replaces recurrence with attention blocks.
The Original Transformer Architecture
Vaswani et al. use:
- encoder layers
- decoder layers
- attention heads
- dropout rate 0.1
These numbers define the base model. The “big” model scales width and head count further.
Encoder
The encoder stack applies unmasked self-attention followed by a positionwise feed-forward network. In the original post-norm presentation:
where the first sublayer is multi-head self-attention and the second is the feed-forward block.
The feed-forward block is:
with input/output dimension and inner dimension .
Decoder
Each decoder layer contains three sublayers:
- masked self-attention over the target prefix
- encoder-decoder attention
- feed-forward network
That middle step is cross-attention.
flowchart LR subgraph enc["Encoder, x N"] src["Source tokens + positions"] --> esa["Self-attention, unmasked"] esa --> effn["Feed-forward"] end subgraph dec["Decoder, x N"] tgt["Target prefix + positions"] --> dsa["Masked self-attention"] dsa --> xattn["Cross-attention"] xattn --> dffn["Feed-forward"] end effn -- "K, V" --> xattn dffn --> out["Softmax over target vocabulary"]
The single edge from encoder to decoder is the whole coupling between the two stacks. The encoder runs once per source sequence; the decoder consumes its output at every layer and every generation step.
Cross-Attention
Cross-attention differs from self-attention only in the origin of , , and :
- queries come from decoder states
- keys come from encoder states
- values come from encoder states
Formally,
This is the mechanism that lets target position align to whichever source positions matter for predicting .
Cross-attention is self-attention with one substitution
The computation is identical to self-attention; only the inputs change. Queries come from the decoder, keys and values from the encoder. No causal mask is needed because the entire source is legitimately observable. Everything else in the layer — heads, scaling, softmax, output projection — is unchanged.
Positional Encoding
Because the transformer has no recurrence and no convolution, order must be injected. The paper uses sinusoidal positional encodings:
These are added to the token embeddings.
Optimization Details That Matter
The transformer paper is worth reading for the optimizer schedule alone. They use Adam with:
and learning rate
with 4000 warmup steps.
They also use label smoothing with , which the paper says hurts perplexity but improves accuracy and BLEU.
Experimental Result
The paper reports:
- 28.4 BLEU on WMT 2014 English-to-German, improving over prior best ensembles by more than 2 BLEU
- 41.8 BLEU on WMT 2014 English-to-French for a single model
- training of the big English-to-French model in 3.5 days on 8 GPUs
Those numbers mattered historically because they made the case that attention-only sequence transduction was not just elegant. It was competitive and cheaper to train.
NumPy Cross-Attention
import numpy as np
def softmax(x, axis=-1):
x = x - x.max(axis=axis, keepdims=True)
e = np.exp(x)
return e / e.sum(axis=axis, keepdims=True)
def cross_attention(dec_states, enc_states, Wq, Wk, Wv):
Q = dec_states @ Wq
K = enc_states @ Wk
V = enc_states @ Wv
scores = Q @ np.swapaxes(K, -1, -2) / np.sqrt(Q.shape[-1])
probs = softmax(scores, axis=-1)
return probs @ VPyTorch Decoder Block with Cross-Attention
import torch
import torch.nn as nn
from einops import rearrange
class MultiHeadAttention(nn.Module):
def __init__(self, d_model: int, n_heads: int):
super().__init__()
assert d_model % n_heads == 0
self.n_heads = n_heads
self.head_dim = d_model // n_heads
self.q_proj = nn.Linear(d_model, d_model, bias=False)
self.k_proj = nn.Linear(d_model, d_model, bias=False)
self.v_proj = nn.Linear(d_model, d_model, bias=False)
self.out = nn.Linear(d_model, d_model, bias=False)
def forward(self, q_in, k_in, v_in, causal=False):
B, Tq, D = q_in.shape
Tk = k_in.shape[1]
q = rearrange(self.q_proj(q_in), "b t (h d) -> b h t d", h=self.n_heads)
k = rearrange(self.k_proj(k_in), "b t (h d) -> b h t d", h=self.n_heads)
v = rearrange(self.v_proj(v_in), "b t (h d) -> b h t d", h=self.n_heads)
scores = torch.matmul(q, k.transpose(-1, -2)) / (self.head_dim ** 0.5)
if causal:
mask = torch.triu(torch.ones(Tq, Tk, device=q_in.device, dtype=torch.bool), diagonal=1)
scores = scores.masked_fill(mask, float("-inf"))
attn = scores.softmax(dim=-1)
out = torch.matmul(attn, v)
out = rearrange(out, "b h t d -> b t (h d)")
return self.out(out)
class Seq2SeqDecoderBlock(nn.Module):
def __init__(self, d_model: int, n_heads: int):
super().__init__()
self.ln1 = nn.LayerNorm(d_model)
self.self_attn = MultiHeadAttention(d_model, n_heads)
self.ln2 = nn.LayerNorm(d_model)
self.cross_attn = MultiHeadAttention(d_model, n_heads)
self.ln3 = nn.LayerNorm(d_model)
self.mlp = nn.Sequential(
nn.Linear(d_model, 4 * d_model),
nn.ReLU(),
nn.Linear(4 * d_model, d_model),
)
def forward(self, y, enc):
y = y + self.self_attn(self.ln1(y), self.ln1(y), self.ln1(y), causal=True)
y = y + self.cross_attn(self.ln2(y), enc, enc, causal=False)
y = y + self.mlp(self.ln3(y))
return yWhere Encoder-Decoder Still Wins
- translation
- summarization from a fully observed source
- speech or vision to text
- tasks where source and target have distinct roles
Decoder-only models are simpler. Still, when one sequence is given in full and the other is generated conditioned on it, encoder-decoder structure remains natural.
Related Notes
- Decoder-Only Transformers and the WikiText-2 experiment
- Decoder-Only Transformers
- Recurrent Neural Networks