Self-Attention

Self-attention is the operation that lets every token in a sequence look at every other token and pull in the information it needs. It is the core of the Transformer: a single matrix-multiply-heavy step that replaces the sequential state of an RNN with direct, content-based connections between all positions.

This page derives scaled dot-product attention, explains masks and multiple heads, implements it in PyTorch, and covers the quadratic cost and the techniques used to tame it.


1. Scaled Dot-Product Attention

Scaled Dot-Product Attention X (n x d) Q = X Wq K = X Wk V = X Wv Q K^T / sqrt(dk)n x n scores mask + softmaxrow-wise A V (n x dv) Each output row is a convex combination of value vectors, weighted by query-key similarity.

Given input rows X ∈ Rn×d, three learned projections produce queries, keys and values: Q = XWQ, K = XWK, V = XWV. Then

Attention(Q, K, V) = softmax(QKT / √dk + M) V

The dot product qi·kj measures how relevant token j is to token i; softmax turns each row of scores into weights summing to 1; the output for token i is the weighted average of value vectors. The √dk scaling keeps the variance of the logits near 1 — without it, large dot products saturate softmax into near one-hot distributions with vanishing gradients.

2. Masks: Causal and Padding

Attention Weights for One Sentence (causal mask) The cat sat on it The 1.00 -inf -inf -inf -inf cat 0.35 0.65 -inf -inf -inf sat 0.10 0.60 0.30 -inf -inf on 0.05 0.20 0.45 0.30 -inf it 0.05 0.70 0.10 0.05 0.10 rows = queries, columns = keys; each row sums to 1 Causal mask: token t only sees tokens <= t. Row 'it': most weight on 'cat' (coreference). Bidirectional encoders drop the mask and fill the upper triangle. Weights are illustrative, not from a real model.

The additive mask M holds 0 for allowed pairs and −∞ for forbidden ones, which become exactly zero weight after softmax. A causal mask (upper triangle) prevents a token from attending to the future; it is what makes decoder-only models valid next-token predictors. A padding mask hides pad tokens in batched variable-length input. Encoder-only models use only the padding mask, so attention is bidirectional.

Cross-attention is the same formula with queries from one sequence and keys/values from another — the decoder-to-encoder link in encoder-decoder models.

3. Multi-Head Attention

Multi-Head Attention X head 1: d/h dims head 2: d/h dims head 3: d/h dims head 4: d/h dims Concat x Wo Heads learn different relations (syntax, position, coreference...). Total cost equals one full-width head; heads run in parallel.

Instead of one attention over d dimensions, the model runs h heads over d/h dimensions each, concatenates them, and mixes with an output projection WO. Different heads can specialize (previous-token, syntactic dependency, coreference) at no extra FLOPs compared with a single wide head.

Decoder inference variants shrink the KV cache by sharing keys and values: multi-query attention (one K/V head for all query heads) and grouped-query attention (a K/V head per group of query heads), now common in large LLMs.

4. PyTorch Implementation

import torch, torch.nn as nn, torch.nn.functional as F, math

class MultiHeadSelfAttention(nn.Module):
    def __init__(self, d_model=512, n_heads=8, causal=False):
        super().__init__()
        assert d_model % n_heads == 0
        self.h, self.dk, self.causal = n_heads, d_model // n_heads, causal
        self.qkv = nn.Linear(d_model, 3 * d_model)
        self.out = nn.Linear(d_model, d_model)

    def forward(self, x, pad_mask=None):          # x: (B, T, d_model)
        B, T, _ = x.shape
        q, k, v = self.qkv(x).chunk(3, dim=-1)
        # (B, T, d) -> (B, h, T, dk)
        q, k, v = (t.view(B, T, self.h, self.dk).transpose(1, 2) for t in (q, k, v))
        scores = q @ k.transpose(-2, -1) / math.sqrt(self.dk)   # (B, h, T, T)
        if self.causal:
            causal = torch.ones(T, T, dtype=torch.bool, device=x.device).triu(1)
            scores = scores.masked_fill(causal, float("-inf"))
        if pad_mask is not None:                  # (B, T) True = padding
            scores = scores.masked_fill(pad_mask[:, None, None, :], float("-inf"))
        attn = F.softmax(scores, dim=-1)
        y = (attn @ v).transpose(1, 2).reshape(B, T, -1)
        return self.out(y), attn

x = torch.randn(2, 10, 512)
y, attn = MultiHeadSelfAttention(causal=True)(x)
print(y.shape, attn.shape)        # (2, 10, 512) (2, 8, 10, 10)

# In practice use the fused kernel (FlashAttention-backed when available):
# F.scaled_dot_product_attention(q, k, v, is_causal=True)

The explicit version is useful for understanding and for extracting attention maps; production code should call F.scaled_dot_product_attention, which dispatches to memory-efficient fused kernels.

5. Complexity and Efficient Attention

The score matrix is n × n per head, so time is O(n²d) and naive memory is O(n²). Doubling the context quadruples attention cost. Approaches:

6. Common Pitfalls

7. A Worked Numeric Example

Take three tokens with dk = 2. Let the query of token 1 be q = [1, 0], the keys k1 = [1, 0], k2 = [0, 1], k3 = [1, 1], and the values v1 = [1, 0], v2 = [0, 2], v3 = [1, 1].

  1. Scores: q·k = 1, 0, 1. Divide by √2: 0.707, 0, 0.707.
  2. Softmax: e0.707 ≈ 2.03 and e0 = 1, total ≈ 5.06, so the weights are about 0.40, 0.20, 0.40.
  3. Weighted sum: 0.40·[1, 0] + 0.20·[0, 2] + 0.40·[1, 1] ≈ [0.80, 0.80].

Token 1 draws mostly on tokens 1 and 3, whose keys align with its query, and less on token 2. With a causal mask, token 1 could only see itself and its output would simply be v1. Scale up the vectors by 10 and the scores become 7.07, 0, 7.07, softmax gives nearly 0.5, 0, 0.5, and the gradient through the near-zero weight almost vanishes, which is the saturation that the √dk factor is meant to prevent as dimensions grow.

8. Positional Information

Self-attention is permutation-equivariant: shuffle the input tokens and the outputs shuffle the same way. Order must therefore be injected. The original Transformer added fixed sinusoidal encodings to the embeddings; BERT and GPT-2 used learned absolute position embeddings, which do not extrapolate beyond the trained length. Most current LLMs use rotary position embeddings (RoPE, Su et al., 2021), which rotate pairs of dimensions in q and k by an angle proportional to the position, so the dot product qi·kj depends on the relative offset i − j. Context-extension methods (position interpolation, NTK-aware scaling, YaRN) rescale those rotation frequencies. ALiBi (Press et al., 2021) instead adds a distance-proportional penalty directly to the scores. Encoder models for images (see Vision Transformers) use 2D variants of the same ideas.

9. KV Cache and Head Sharing

Query and Key/Value Head Sharing MHA 8 query heads 8 K/V heads GQA (groups of 4) 8 query heads 2 K/V heads MQA 8 query heads 1 K/V head KV cache size scales with the number of K/V heads: 8 : 2 : 1 here

During generation, each new token needs the keys and values of every previous token in every layer. Storing them is the KV cache, whose size is 2 × layers × kv_heads × head_dim × tokens × bytes. For a model shaped like Llama 2 7B (32 layers, 32 heads of dimension 128, 16-bit values), that is 2 × 32 × 32 × 128 × 2 bytes = 512 KiB per token, or 2 GiB for one 4,096-token sequence; multiply by the batch size and the cache, not the weights, becomes the memory limit for serving.

Multi-query attention (Shazeer, 2019) shares a single key/value head across all query heads, cutting the cache by the number of heads at some quality cost. Grouped-query attention (Ainslie et al., 2023) is the middle ground used by many open LLMs: query heads are split into groups that each share one K/V head, recovering most of MHA's quality with a several-fold smaller cache. Serving systems add paged cache management (vLLM's PagedAttention), cache quantisation, and prefix sharing across requests; see vLLM and quantization.

10. Where Self-Attention Is Used, and Alternatives

SettingAttention patternExample models
Text understandingBidirectional self-attentionBERT, RoBERTa, sentence embedders
Text generationCausal self-attention with KV cacheGPT-style decoders
Translation, summarisationEncoder self-, decoder causal self-, plus cross-attentionT5, BART
ImagesSelf-attention over patches, sometimes windowedViT, Swin
GraphsAttention restricted to neighboursGAT

Compared with recurrence (LSTMs), attention connects any two positions in one step and parallelises across the sequence during training, at the price of quadratic cost in sequence length. Convolutions are cheaper but see only a local window per layer. State-space models such as Mamba scale linearly and carry a fixed-size state, which is attractive for very long sequences; hybrid architectures interleave a few attention layers with them, because exact retrieval of a specific earlier token remains a strength of attention.

Further reading: Bahdanau, Cho & Bengio, "Neural Machine Translation by Jointly Learning to Align and Translate" (2014); Vaswani et al., "Attention Is All You Need" (2017, arXiv:1706.03762); Shazeer, "Fast Transformer Decoding: One Write-Head is All You Need" (2019); Ainslie et al., "GQA" (2023); Su et al., "RoFormer: Enhanced Transformer with Rotary Position Embedding" (2021); Dao et al., "FlashAttention" (2022); Jain & Wallace, "Attention is not Explanation" (2019).