AI Knowledge Tree

Scaled dot-product attention

Derive the query-key-value soft lookup, show why 1/sqrt(d_k) scaling is necessary, and implement a single attention head in numpy.

⏱ 32 minPrerequisites:Seq2seq with Bahdanau attention

Bahdanau attention computes a context vector as a weighted sum of encoder states, with weights determined by a learned alignment score between the decoder state and each encoder position. The Transformer (Vaswani et al., 2017) generalizes this to a single unified mechanism: scaled dot-product attention, which any sequence -- not just encoder-decoder pairs -- can apply to itself[1].

The query-key-value abstraction

Frame attention as a soft lookup table: you have a table of key-value pairs, and a query you want to look up. Instead of returning the value for an exact key match, you return a weighted sum of all values, with weights proportional to how well each key matches the query.

For a set of nn queries Q∈Rn×dkQ \in \mathbb{R}^{n \times d_k}, mm keys K∈Rm×dkK \in \mathbb{R}^{m \times d_k}, and mm values V∈Rm×dvV \in \mathbb{R}^{m \times d_v}:

Attention(Q,K,V)=softmax ⁣(QK⊤dk)V.\text{Attention}(Q, K, V) = \text{softmax}\!\left( \frac{Q K^\top}{\sqrt{d_k}} \right) V.

Steps:

  1. Compute similarity scores: QK⊤∈Rn×mQ K^\top \in \mathbb{R}^{n \times m} (one score per query-key pair).
  2. Scale by 1/dk1/\sqrt{d_k} to prevent saturation.
  3. Apply softmax row-wise to get weight matrix A∈Rn×mA \in \mathbb{R}^{n \times m} (each row sums to 1).
  4. Return AV∈Rn×dvA V \in \mathbb{R}^{n \times d_v} (each query gets a weighted sum of values).

Why scale by 1/dk1/\sqrt{d_k}?

If the components of qq and kk are i.i.d. with mean 0 and variance 1, then q⋅k=∑i=1dkqikiq \cdot k = \sum_{i=1}^{d_k} q_i k_i has mean 0 and variance dkd_k, so the standard deviation grows as dk\sqrt{d_k}.

Without scaling, the dot products grow large in magnitude as dkd_k increases. The softmax of large values approaches a one-hot distribution: almost all weight is placed on the largest score. This pushes the attention into a nearly-argmax regime where gradients are tiny (the softmax is saturated). Dividing by dk\sqrt{d_k} keeps the inputs to softmax at unit variance, maintaining informative gradients.

Put numbers on it at dk=64d_k = 64, a typical head dimension, where the score standard deviation is 64=8\sqrt{64} = 8. Take two keys whose raw scores differ by one standard deviation, a gap of 88, with everything else held fixed so only the scaling changes:

  • Unscaled. softmax(8,0)\text{softmax}(8, 0) puts weight 0.99970.9997 on the larger key. The distribution is one-hot in all but name; the second key gets 0.00030.0003 and its gradient is starved.
  • Scaled by 1/dk=1/81/\sqrt{d_k} = 1/8. The same gap becomes 11, and softmax(1,0)\text{softmax}(1, 0) puts weight 0.7310.731 on the larger key. The attention stays soft and both keys carry gradient.

A wider gap of two standard deviations (1616 raw, 22 after scaling) saturates even harder unscaled (≈1.0000\approx 1.0000) but stays soft once scaled (0.8810.881). The scaling does not change which key wins; it changes whether the loser is heard at all.

Scaled dot-product attention: the token-by-token weight matrix softmax(QKᵀ/√dₖ) (left), and why scaling matters: with large dₖ the unscaled scores saturate the softmax onto a single key, while the 1/√dₖ factor keeps the distribution soft (right).
Scaled dot-product attention: the token-by-token weight matrix softmax(QKᵀ/√dₖ) (left), and why scaling matters: with large dₖ the unscaled scores saturate the softmax onto a single key, while the 1/√dₖ factor keeps the distribution soft (right).

Self-attention

When QQ, KK, VV are all derived from the same sequence X∈RT×dX \in \mathbb{R}^{T \times d} via linear projections:

Q=XWQ,K=XWK,V=XWV,Q = X W^Q, \quad K = X W^K, \quad V = X W^V,

the result is self-attention: every position attends to every other position in the same sequence. Each output position is a weighted sum of all value vectors, with weights reflecting how relevant each key position is to the current query position.

Causal masking

For autoregressive language modeling, position tt must not attend to positions >t> t (it cannot see the future). Causal masking sets the pre-softmax scores to −∞-\infty for all j>tj > t:

Mij={0j≤i−∞j>i.M_{ij} = \begin{cases} 0 & j \le i \\ -\infty & j > i \end{cases}.

After softmax, −∞-\infty becomes 0, so future positions receive zero weight.

Implementation

python · numpy
import numpy as np

def scaled_dot_product_attention(Q, K, V, mask=None):
    """
    Q: (n, d_k)  K: (m, d_k)  V: (m, d_v)  mask: (n, m) boolean (True = masked out)
    Returns: (n, d_v) output, (n, m) attention weights
    """
    d_k = Q.shape[-1]
    scores = Q @ K.T / np.sqrt(d_k)          # (n, m)
    if mask is not None:
        scores[mask] = -1e9                   # set masked positions to -inf
    attn = np.exp(scores - scores.max(axis=-1, keepdims=True))
    attn /= attn.sum(axis=-1, keepdims=True)  # softmax row-wise
    out = attn @ V                            # (n, d_v)
    return out, attn

# Demo: 5-token sequence, d_k = d_v = 4
T, d_k, d_v = 5, 4, 4
rng = np.random.default_rng(0)
X  = rng.normal(0, 1, (T, 8))   # input embeddings (d_model=8)
Wq = rng.normal(0, 0.1, (8, d_k))
Wk = rng.normal(0, 0.1, (8, d_k))
Wv = rng.normal(0, 0.1, (8, d_v))

Q, K, V = X @ Wq, X @ Wk, X @ Wv

# Causal mask: True where j > i
mask = np.triu(np.ones((T, T), dtype=bool), k=1)

out, attn = scaled_dot_product_attention(Q, K, V, mask=mask)
print("Output shape:", out.shape)
print("Attention weights (row = query, col = key):")
print(np.round(attn, 3))
print("Each row sums to:", attn.sum(axis=1).round(4))

Verify that the upper triangle of the attention matrix is zero (masked), and that each row in the lower-triangular region sums to 1.

Complexity

For a sequence of length TT:

  • Computing QK⊤Q K^\top: O(T2dk)O(T^2 d_k) -- quadratic in sequence length.
  • Computing AVA V: O(T2dv)O(T^2 d_v).
  • Total: O(T2d)O(T^2 d) time and O(T2)O(T^2) memory (the attention matrix).

This quadratic scaling in TT is the core limitation of standard attention, motivating FlashAttention (D7) and linear attention variants.

Exercises

Exercise 1. A single query q=(1,0)q = (1, 0) attends to two keys k1=(1,0)k_1 = (1, 0) and k2=(0,1)k_2 = (0, 1) with values v1=(1,0)v_1 = (1, 0) and v2=(0,1)v_2 = (0, 1), head dimension dk=2d_k = 2. Compute the scaled scores, the attention weights, and the output vector by hand.

Solution

Step 1. Raw scores are the dot products q⋅k1=1q \cdot k_1 = 1 and q⋅k2=0q \cdot k_2 = 0. Scale by 1/dk=1/21/\sqrt{d_k} = 1/\sqrt{2}:

(12, 0)=(0.7071, 0).\left( \tfrac{1}{\sqrt 2},\ 0 \right) = (0.7071,\ 0).

Step 2. Softmax the scaled scores. The weight on k1k_1 is

e0.7071e0.7071+e0=2.02812.0281+1=0.6698,\frac{e^{0.7071}}{e^{0.7071} + e^{0}} = \frac{2.0281}{2.0281 + 1} = 0.6698,

so the weights are (0.6698, 0.3302)(0.6698,\ 0.3302).

Step 3. The output is the weighted sum of values:

0.6698⋅(1,0)+0.3302⋅(0,1)=(0.6698, 0.3302).0.6698 \cdot (1, 0) + 0.3302 \cdot (0, 1) = (0.6698,\ 0.3302).

The query leans toward the matching key k1k_1 but stays soft; the non-matching value still contributes a third of the output.

Exercise 2. Two keys produce raw scores that differ by 88 (everything else fixed). Compute the softmax weight on the larger key with no scaling, and again after dividing by dk=8\sqrt{d_k} = 8. Explain why the scaled case keeps gradient flowing to the smaller key.

Solution

Step 1. Unscaled, the scores are (8,0)(8, 0). The weight on the larger key is

e8e8+e0=2980.962981.96=0.9997,\frac{e^{8}}{e^{8} + e^{0}} = \frac{2980.96}{2981.96} = 0.9997,

so the smaller key receives 0.00030.0003.

Step 2. After dividing by dk=8\sqrt{d_k} = 8, the gap becomes 11 and the scores are (1,0)(1, 0):

e1e1+e0=2.71833.7183=0.7311,\frac{e^{1}}{e^{1} + e^{0}} = \frac{2.7183}{3.7183} = 0.7311,

so the smaller key now holds 0.26890.2689.

Step 3. The softmax gradient with respect to a logit is proportional to the weight times one minus the weight, w(1−w)w(1 - w). At w=0.9997w = 0.9997 this is about 0.00030.0003, vanishingly small; at w=0.7311w = 0.7311 it is about 0.1970.197, three orders of magnitude larger. Scaling pulls the distribution out of the saturated regime so both keys carry usable gradient.

Exercise 3 (code). Implement scaled dot-product attention and verify two structural facts on a random causal example: every attention row sums to 11, and the strictly upper triangle (future positions) is exactly zero.

Solution
python · numpy
import numpy as np

def attention(Q, K, V, mask=None):
    d_k = Q.shape[-1]
    scores = Q @ K.T / np.sqrt(d_k)
    if mask is not None:
        scores = np.where(mask, -1e9, scores)
    attn = np.exp(scores - scores.max(axis=-1, keepdims=True))
    attn /= attn.sum(axis=-1, keepdims=True)
    return attn @ V, attn

rng = np.random.default_rng(0)
T, d_k, d_v = 6, 8, 8
Q, K, V = rng.normal(size=(T, d_k)), rng.normal(size=(T, d_k)), rng.normal(size=(T, d_v))
mask = np.triu(np.ones((T, T), dtype=bool), k=1)   # True above diagonal = future

out, attn = attention(Q, K, V, mask=mask)
assert np.allclose(attn.sum(axis=1), 1.0)          # rows are distributions
assert np.allclose(attn[mask], 0.0)                # no weight on the future
print("rows sum to 1, upper triangle zeroed; output shape", out.shape)

The mask drives future logits to −109-10^9, which softmax sends to zero, while each row still normalizes to a valid distribution over the visible past.

The next lesson stacks hh independent attention heads in parallel -- multi-head attention -- which allows different heads to attend to different representation subspaces.


References

  1. [1]Vaswani, A., et al. (2017). "Attention is all you need". NeurIPS. arXiv:1706.03762↩