Skip to main content

Attention Is All You Need

Deep dive into the Transformer architecture that revolutionized NLP. Understand self-attention, multi-head attention, and positional encoding.

Paper Overview

The paper introduces the Transformer architecture, which has become the foundation of modern natural language processing. It drops recurrence and convolution entirely and draws every dependency between tokens through self-attention. Because each position can attend to every other position in a single step, the model parallelises across a sequence instead of walking it left to right, and long-range dependencies stop being something the network has to carry across many time steps.

Self-Attention and Multi-Head Attention

At the core is scaled dot-product attention: every token emits a query, a key, and a value, and the output for a position is a weighted average of all the values, with weights set by how well that position's query matches the others' keys. A single pass gives the model one view of how tokens relate. Multi-head attention runs several of these in parallel over different learned projections, giving the trained model different representation subspaces whose outputs are concatenated and projected together. The illustrative head patterns below are authored schematics, not learned syntax or coreference from a trained checkpoint.

Architecture Details

Encoder

A stack of N=6 identical layers, each pairing a multi-head self-attention block with a position-wise feed-forward network. Every sub-layer applies dropout to its output, adds a residual connection, then applies layer normalization. This original post-norm order matters: later pre-norm variants move normalization before the sublayer. The feed-forward network uses ReLU.

Decoder

Also six identical layers, but with two changes. Its self-attention is masked so a position attends up to and including its current decoder input. For teacher forcing, those inputs are shifted right by one token: BOS starts the sequence, and position i consumes the previous target rather than the target it predicts. The mask and the shift work together to prevent target leakage. Above that sits a cross-attention block, where decoder queries attend over the encoder's output, conditioning the generated sequence on the source.

Positional Encoding

Attention is order-agnostic on its own, so the Transformer adds sinusoidal positional encodings to the token embeddings — injecting absolute and relative position without reintroducing recurrence.

Implementation

The architecture is complete only when the attention heads feed residuals, normalization, the encoder memory and a shifted decoder. The following nine operations replace the constructor-only fragment with an executable one-layer encoder–decoder. Source and target lengths deliberately differ, so cross-attention cannot accidentally reuse decoder K/V.

Authored offline CPU reference for the original paper architecture: post-norm residuals, ReLU, scaled shared embeddings and interleaved sinusoidal positions. Synthetic IDs and random reduced weights expose the complete tensor path; they do not translate or demonstrate learned syntax, coreference, pretrained semantics, native GPU performance or benchmark reproduction. Equations below explain the cited operations.

The original Tensor2Tensor repository contains several later recipes. At pinned revision 15ea434070b3, transformer_base_v2 explicitly changes to pre-norm; this walkthrough follows the paper’s post-norm order instead. Its literal even/odd positional formulas also come from §3.5, rather than silently inheriting a library timing layout. This is an authored paper reference, not a port of every Tensor2Tensor default.

1. Scale embeddings and add sinusoids

Original paper

Paper excerpt · PDF p. 6§3.4–3.5 · Embeddings and positional encoding

The source and shifted target use the same embedding matrix.

Math and tensor shapes

X_p=\sqrt{D}E[t_p]+PE_p
PE_{p,2i}=\sin(p/10000^{2i/D})
PE_{p,2i+1}=\cos(p/10000^{2i/D})

D is d_model. The even/odd coordinates follow the paper exactly, including an unpaired final sine for odd D. Dropout is applied after summing the scaled embeddings and positions (§5.4). Source and target use one shared vocabulary; the same learned matrix is reused for the output readout. Sinusoids supply position, not learned linguistic meaning.

token IDs [B, N] → embeddings [B, N, D]

§3.5 · Coordinate formulas

Python implementation

1. Scale embeddings and add sinusoids
python
def sinusoidal_positions(length, d_model, *, dtype=torch.float32):
    """Return [N, D] with paper section 3.5 even/odd coordinates."""
    positive_integer(length, "length")
    positive_integer(d_model, "d_model")
    if not dtype.is_floating_point:
        raise ValueError("positional dtype must be floating")
    positions = torch.arange(length, dtype=dtype)[:, None]
    even = torch.arange(0, d_model, 2, dtype=dtype)
    angles = positions / (10000.0 ** (even / d_model))
    result = torch.empty(length, d_model, dtype=dtype)
    result[:, 0::2] = torch.sin(angles)
    # For odd D, the last sine has no corresponding cosine coordinate.
    result[:, 1::2] = torch.cos(angles[:, :d_model // 2])
    return result


def embed_positions(tokens, embedding, dropout):
    """Scale token embeddings, add positions, then apply residual dropout."""
    x = embedding(tokens) * math.sqrt(embedding.embedding_dim)
    positions = sinusoidal_positions(x.shape[1], x.shape[2], dtype=x.dtype)
    return dropout(x + positions[None])

2. Project queries and a separate K/V source

Original paper

Paper excerpt · PDF p. 4§3.2.2–3.2.3 · Learned projections and cross-attention

Self-attention shares the input; cross-attention uses encoder memory.

Math and tensor shapes

Q=X_qW^Q
K=X_mW^K,\quad V=X_mW^V

Three learned matrices map the model width to concatenated per-head features. In self-attention Xq and Xm are the same tensor. In encoder–decoder attention, decoder states provide Q while the encoder output supplies both K and V. The reduced reference uses bias-free attention projections as in the paper’s matrix notation. Batch and feature widths are checked explicitly.

Xq [B, Nq, D], Xm [B, Nk, D] → Q, K, V; Nq need not equal Nk

§3.2.3 · Encoder K/V source

Python implementation

2. Project queries and a separate K/V source
python
def project_qkv(queries, key_values, q_projection, k_projection, v_projection):
    """Project queries [B,Nq,D] and a separate K/V source [B,Nk,D]."""
    validate_features(queries, "queries")
    validate_features(key_values, "key_values")
    if queries.shape[0] != key_values.shape[0]:
        raise ValueError("query and key/value batch sizes must match")
    if queries.dtype != key_values.dtype:
        raise ValueError("query and key/value dtypes must match")
    if (queries.shape[-1] != q_projection.in_features
            or key_values.shape[-1] != k_projection.in_features
            or key_values.shape[-1] != v_projection.in_features):
        raise ValueError("feature width must match the projection inputs")
    return (q_projection(queries), k_projection(key_values),
            v_projection(key_values))

3. Split the width into parallel heads

Original paper

Paper excerpt · PDF p. 4§3.2.2 · Multi-head attention

Each head has its own projected representation subspace.

Math and tensor shapes

D_h=D/H
D=H D_h

The reshape groups contiguous projected coordinates into H heads; the transpose puts heads before sequence position for batched multiplication. D must be divisible by H. The paper’s base model uses D=512, H=8 and Dh=64; smoke reduces these to D=8, H=2 and Dh=4. Random reference heads have no learned syntax or coreference specialization.

[B, N, D] → reshape [B, N, H, Dh] → transpose [B, H, N, Dh]

Python implementation

3. Split the width into parallel heads
python
def split_heads(x, num_heads):
    """Map [B,N,D] to [B,H,N,Dh] without mixing token positions."""
    validate_features(x, "projected features")
    positive_integer(num_heads, "num_heads")
    batch, length, width = x.shape
    if width % num_heads:
        raise ValueError("d_model must be divisible by num_heads")
    shaped = x.reshape(batch, length, num_heads, width // num_heads)
    return shaped.transpose(1, 2)

4. Scale each head’s query–key products

Original paper

Paper excerpt · PDF p. 4§3.2.1 · Equation (1), scaled dot products

The denominator is the key width within a head.

Math and tensor shapes

S=QK^\top/\sqrt{D_h}

Scale by sqrt(Dh), not sqrt(D). Under the paper’s assumption of independent zero-mean, unit-variance query/key coordinates, the dot-product variance grows with Dh; dividing by sqrt(Dh) controls that growth. This argument is conditional, not a guarantee that every trained score distribution has unit variance. Softmax will operate over the final key axis.

[B, H, Nq, Dh] × [B, H, Dh, Nk] → scores [B, H, Nq, Nk]

Python implementation

4. Scale each head’s query–key products
python
def scaled_scores(q, k):
    """Compute [B,H,Nq,Nk] scores with per-head, not model-width, scale."""
    if q.ndim != 4 or k.ndim != 4 or min(q.shape + k.shape) < 1:
        raise ValueError("Q and K must be nonempty [B,H,N,Dh]")
    if q.shape[:2] != k.shape[:2] or q.shape[-1] != k.shape[-1]:
        raise ValueError("Q and K batch, head count and head width must match")
    scores = q @ k.transpose(-2, -1) / math.sqrt(q.shape[-1])
    if not torch.isfinite(scores).all():
        raise ValueError("attention scores must be finite before masking")
    return scores

5. Block padded keys and future inputs before softmax

Original paper

Paper excerpt · PDF p. 5§3.2.3 · Illegal connections set to negative infinity

Decoder self-attention includes its current input position.

Math and tensor shapes

A_{ij}=\mathbf{1}[j\le i]\mathbf{1}[j\text{ valid}]
S_{ij}=-\infty\quad\text{if }A_{ij}=0

True in key_padding means blocked. Encoder and cross-attention block padded source keys; decoder self-attention combines shifted-input padding with a lower triangle including the diagonal. The triangle is only accepted for equal-length self-attention. Masking keys does not force padded query outputs to zero: those outputs are excluded from the demonstration loss. Fully masked rows raise ValueError before softmax; returning NaN or inventing a uniform row would violate this API contract.

key padding [B, Nk], causal [Nq, Nk] → broadcast mask [B, 1, Nq, Nk]

Python implementation

5. Block padded keys and future inputs before softmax
python
def apply_mask(scores, key_padding=None, *, causal=False):
    """Block padded keys and future positions; reject fully masked rows."""
    if scores.ndim != 4 or min(scores.shape) < 1:
        raise ValueError("scores must be nonempty [B,H,Nq,Nk]")
    if not scores.is_floating_point() or not torch.isfinite(scores).all():
        raise ValueError("scores must be finite floating values")
    batch, _, query_length, key_length = scores.shape
    allowed = torch.ones(batch, 1, query_length, key_length, dtype=torch.bool)
    if key_padding is not None:
        if (not isinstance(key_padding, torch.Tensor)
                or key_padding.dtype != torch.bool
                or key_padding.shape != (batch, key_length)):
            raise ValueError(
                "key_padding must be boolean [B,Nk], True = blocked")
        if key_padding.device.type != "cpu":
            raise ValueError("key_padding must be a CPU tensor")
        allowed = allowed & ~key_padding[:, None, None, :]
    if causal:
        if query_length != key_length:
            raise ValueError("causal mask is for equal-length self-attention")
        # Lower triangle includes the diagonal, hence the current input token.
        triangle = torch.ones(
            query_length, key_length, dtype=torch.bool).tril()
        allowed = allowed & triangle
    if not allowed.any(dim=-1).all():
        raise ValueError("fully masked attention row has no valid softmax")
    return scores.masked_fill(~allowed, -torch.inf)

6. Mix values and project the concatenated heads

Original paper

Paper excerpt · PDF p. 4§3.2.1–3.2.2 · Attention and output projection

Each head produces a weighted value vector per query.

Math and tensor shapes

P=\operatorname{softmax}_{k}(S)
U_h=P_hV_h
Y=\operatorname{Concat}(U_1,\ldots,U_H)W^O

The stable library softmax subtracts a row maximum internally. Negative-infinity entries receive exactly zero probability. Multiplying by V sums over keys, and transposing back before reshaping preserves the head coordinate order. W_O mixes all heads into model-width output. Tests compare the whole vectorized result against an independent per-head, per-query calculation, including Nq=3 and Nk=5.

weights [B, H, Nq, Nk] → head outputs [B, H, Nq, Dh] → [B, Nq, D]

Python implementation

6. Mix values and project the concatenated heads
python
def attend_and_merge(masked_scores, values, output_projection):
    """Softmax, mix V, concatenate heads, then apply learned W_O."""
    if (masked_scores.ndim != 4 or values.ndim != 4
            or min(masked_scores.shape + values.shape) < 1
            or masked_scores.shape[:2] != values.shape[:2]
            or masked_scores.shape[-1] != values.shape[-2]
            or values.shape[1] * values.shape[-1]
            != output_projection.in_features):
        raise ValueError("score, value and output-projection shapes must match")
    if (torch.isnan(masked_scores).any() or torch.isposinf(masked_scores).any()
            or not torch.isfinite(masked_scores).any(dim=-1).all()
            or not torch.isfinite(values).all()):
        raise ValueError(
            "attention needs finite values and an unmasked key per row")
    weights = torch.softmax(masked_scores, dim=-1)
    context = weights @ values
    batch, heads, length, head_width = context.shape
    merged = context.transpose(1, 2).contiguous().reshape(
        batch, length, heads * head_width)
    return output_projection(merged), weights

7. Complete the original post-norm encoder layer

Original paper

Paper excerpt · PDF p. 3§3.1, §3.3, §5.4 · Encoder, ReLU FFN and residual dropout

Normalization follows each residual addition.

Math and tensor shapes

A=\operatorname{MHA}(X,X)
Z=\operatorname{LN}(X+\operatorname{Drop}(A))
F(Z)=\operatorname{ReLU}(ZW_1+b_1)W_2+b_2
Y=\operatorname{LN}(Z+\operatorname{Drop}(F(Z)))

Attention sees the unnormalized layer input. Each residual sum is normalized over the D coordinates at each token, then the position-wise FFN uses ReLU. Residual dropout is before addition, as §5.4 specifies. LayerNorm epsilon=1e-6 is an explicit numerical choice; the paper does not specify it. A fixed-input oracle checks this order and the population-variance normalization. There is no pre-norm, GELU, RoPE or extra final normalization.

attention [B, Ns, D]; FFN [B, Ns, D] → [B, Ns, Dff] → [B, Ns, D]

§3.3 · Equation (2), position-wise ReLU FFN

Python implementation

7. Complete the original post-norm encoder layer
python
class EncoderLayer(nn.Module):
    """Original post-norm self-attention and position-wise ReLU FFN."""

    def __init__(self, d_model, heads, d_ff, dropout=0.1):
        super().__init__()
        positive_integer(d_ff, "d_ff")
        self.attention = MultiHeadAttention(d_model, heads)
        self.ffn = nn.Sequential(nn.Linear(d_model, d_ff), nn.ReLU(),
                                 nn.Linear(d_ff, d_model))
        self.norm1 = nn.LayerNorm(d_model, eps=1e-6)
        self.norm2 = nn.LayerNorm(d_model, eps=1e-6)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, source_padding=None):
        """LayerNorm is after addition, never before the attention or FFN."""
        attention, weights = self.attention(x, x, source_padding)
        x = self.norm1(x + self.dropout(attention))
        x = self.norm2(x + self.dropout(self.ffn(x)))
        return x, weights

8. Add masked self-attention and encoder cross-attention

Original paper

Paper excerpt · PDF p. 3§3.1 · Three post-norm decoder sublayers

The middle sublayer reads the encoder output, not decoder K/V.

Math and tensor shapes

A_1=\operatorname{MHA}_{\rm causal}(X,X)
Z=\operatorname{LN}(X+\operatorname{Drop}(A_1))
A_2=\operatorname{MHA}(Z,M)
U=\operatorname{LN}(Z+\operatorname{Drop}(A_2))
Y=\operatorname{LN}(U+\operatorname{Drop}(F(U)))

First, masked self-attention sees only shifted decoder inputs up to and including its position. Next, decoder queries read encoder memory M with the source-padding mask. Finally, the same ReLU FFN pattern runs at every target position. All three residual additions precede LayerNorm. The oracle verifies these three operations with source length 3 and target length 2; behavioral tests perturb future inputs and padded memory.

decoder [B, Nt, D], memory [B, Ns, D] → cross weights [B, H, Nt, Ns]

Python implementation

8. Add masked self-attention and encoder cross-attention
python
class DecoderLayer(nn.Module):
    """Masked self-attention, encoder K/V cross-attention, then ReLU FFN."""

    def __init__(self, d_model, heads, d_ff, dropout=0.1):
        super().__init__()
        positive_integer(d_ff, "d_ff")
        self.self_attention = MultiHeadAttention(d_model, heads)
        self.cross_attention = MultiHeadAttention(d_model, heads)
        self.ffn = nn.Sequential(nn.Linear(d_model, d_ff), nn.ReLU(),
                                 nn.Linear(d_ff, d_model))
        self.norm1 = nn.LayerNorm(d_model, eps=1e-6)
        self.norm2 = nn.LayerNorm(d_model, eps=1e-6)
        self.norm3 = nn.LayerNorm(d_model, eps=1e-6)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, memory, target_padding=None, source_padding=None):
        """Memory length Nk may differ from decoder query length Nq."""
        attended, self_weights = self.self_attention(
            x, x, target_padding, causal=True)
        x = self.norm1(x + self.dropout(attended))
        attended, cross_weights = self.cross_attention(
            x, memory, source_padding)
        x = self.norm2(x + self.dropout(attended))
        x = self.norm3(x + self.dropout(self.ffn(x)))
        return x, self_weights, cross_weights

9. Run the shifted encoder–decoder tensor path

Original paper

Paper excerpt · PDF p. 5§3.1 and §3.4 · Shifted outputs and vocabulary readout

Offset target embeddings by one position before decoding.

Math and tensor shapes

T_{\rm in}=[\mathrm{BOS},t_0,\ldots,t_{N_t-2}]
L=YE^\top
p(t_i\mid t_{<i},S)=\operatorname{softmax}(L_i)

The one-layer reference reserves PAD=0 and BOS=1 in a shared 13-token synthetic vocabulary. Its forward API accepts target labels and shifts them internally: predicting target i never consumes target i. The embedding matrix E is shared by source, target and readout (§3.4). Smoke computes mean cross-entropy over nonpadding labels, backpropagates through every parameter and takes one illustrative SGD step. Dropout is configured at 0.1 but disabled in eval mode for deterministic checks; gradients still run. The paper’s six-layer depth, width512, FFN2048, training schedule, label smoothing and beam search are outside this reduced mechanics example.

source [2, 5], target [2, 3] → encoder [2, 5, 8], decoder [2, 3, 8], logits [2, 3, 13]

Python implementation

9. Run the shifted encoder–decoder tensor path
python
def shift_targets(targets, bos_id=1):
    """Input at position i is BOS or target i-1, never target i."""
    bos = torch.full_like(targets[:, :1], bos_id)
    return torch.cat((bos, targets[:, :-1]), dim=1)


class ReducedTransformer(nn.Module):
    """One encoder/decoder layer, D=8, H=2, FFN=16, vocabulary=13 by default."""

    def __init__(self, vocab_size=13, d_model=8, heads=2, d_ff=16, dropout=0.1):
        super().__init__()
        positive_integer(vocab_size, "vocab_size")
        if vocab_size < 3:
            raise ValueError(
                "vocabulary needs PAD=0, BOS=1 and a content token")
        self.embedding = nn.Embedding(vocab_size, d_model)
        self.embedding_dropout = nn.Dropout(dropout)
        self.encoder = EncoderLayer(d_model, heads, d_ff, dropout)
        self.decoder = DecoderLayer(d_model, heads, d_ff, dropout)

    def forward(self, source, targets):
        """Return states/logits and masks' probabilities for teacher forcing."""
        validate_tokens(source, self.embedding.num_embeddings, "source")
        validate_tokens(targets, self.embedding.num_embeddings, "targets")
        if source.shape[0] != targets.shape[0]:
            raise ValueError("source and target batch sizes must match")
        decoder_input = shift_targets(targets)
        source_padding = source == 0
        target_padding = decoder_input == 0
        memory, encoder_weights = self.encoder(
            embed_positions(source, self.embedding, self.embedding_dropout),
            source_padding)
        decoded, self_weights, cross_weights = self.decoder(
            embed_positions(
                decoder_input, self.embedding, self.embedding_dropout),
            memory, target_padding, source_padding)
        # One shared matrix for source embedding, target embedding and readout.
        logits = F.linear(decoded, self.embedding.weight)
        return {
            "memory": memory, "decoded": decoded, "logits": logits,
            "decoder_input": decoder_input, "encoder_weights": encoder_weights,
            "self_weights": self_weights, "cross_weights": cross_weights,
        }

Actual smoke JSON reports source SHA-256, seed, reduced dimensions, all state shapes, finite loss and parameter gradient norms, one SGD update, exactly zero future-target and padded-source interference, and zero probability on masked source keys. Fully masked rows are rejected. This is mechanics evidence from random weights.

transformer.pyrequirements-transformer.txt

python -m pip install -r requirements-transformer.txt
python transformer.py --smoke --seed 7

With that tensor path explicit, the attention machinery carries forward into BERT's bidirectional encoder and the memory and execution questions explored in FlashAttention. The reference verifies the original operations; the models below show where training and scale took them.

Practical Impact

The Transformer architecture has revolutionized NLP and beyond:

  1. Foundation for BERT, GPT, and other models

    • Enabled pre-training on massive text corpora
    • Led to state-of-the-art results across NLP tasks
  2. Cross-domain Applications

    • Computer Vision (ViT)
    • Speech Recognition
    • Protein Structure Prediction (AlphaFold)

Critical Analysis

Strengths

  • Parallel processing capability
  • Better handling of long-range dependencies
  • Scalability to large datasets

Limitations

  • Quadratic memory complexity with sequence length
  • Requires large amounts of training data
  • Computationally intensive training

Personal Notes

In my experience implementing Transformers, the key challenges include:

  • Managing attention matrix memory for long sequences
  • Proper initialization of positional encodings
  • Balancing the number of attention heads

Further Reading

  1. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
  2. GPT-3: Language Models are Few-Shot Learners
  3. Vision Transformer

If you found this paper review helpful, consider sharing it with others.

Mastodon