Scaled Dot-Product Attention: The Core Transformer Mechanism

Michael BrenndoerferMay 25, 202548 min read

Part of Language AI Handbook

Covers scaled dot-product attention: how queries and keys compute similarity, why dividing by sqrt(d_k) prevents softmax saturation.

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Scaled Dot-Product Attention

In the previous chapter, we built the Query, Key, Value framework and saw how three learned projections give each token distinct representations for asking questions, advertising capabilities, and contributing information. We established the formula Q=XWQQ = XW^Q, K=XWKK = XW^K, V=XWVV = XW^V and understood why separate roles matter. What we haven't yet addressed is: given these three matrices, how exactly do we compute the attention weights and the output? That's the job of scaled dot-product attention, the operation sitting at the heart of every transformer.

Scaled dot-product attention is deceptively simple. The complete formula is:

Attention(Q,K,V)=softmax ⁣(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{QK^T}{\sqrt{d_k}}\right) V

Four operations: a matrix multiply to compute similarities, a division to prevent numerical issues, a softmax to turn scores into weights, and a final matrix multiply to produce the output. Each piece solves a specific problem. Understanding those problems and their solutions is the goal of this chapter.

Think of scaled dot-product attention as a soft, differentiable database lookup. In a traditional database, you submit a query and get back exactly one record that matches. In attention, you submit a query and get back a weighted blend of all records, where the blend is proportional to how closely each record's key matches your query. The "softness" is what makes the whole system trainable: weights are smooth functions of the inputs, so gradients flow back through every attention weight to every parameter that influenced it.

the four operations in the formula are not arbitrary choices. Each one addresses a specific failure mode that would otherwise prevent the mechanism from training or working correctly. The dot product gives us a fast, geometrically meaningful similarity measure. The square-root scaling prevents the similarity values from growing so large that gradients vanish. The softmax converts raw similarities into a proper probability distribution. This ensures the output is a well-defined weighted average. And the matrix multiply with values efficiently aggregates information across the entire sequence. Strip out any one of these pieces and you either lose expressive power or make the system untrainable.

This chapter dissects each of those four pieces in detail, explains the mathematical reasons they work, and shows how they combine into the complete mechanism used by GPT, BERT, T5, and virtually every other modern language model. We also cover the two types of masking that adapt the same formula for different tasks: causal masking for autoregressive text generation, and padding masking for batched processing of variable-length sequences. By the end, you'll have a precise mental model of what happens inside every transformer attention layer, grounded in both intuition and the mathematics behind each design decision.

Historical Context

Scaled dot-product attention was introduced by Vaswani et al. in the 2017 paper "Attention Is All You Need," which proposed the transformer architecture. Earlier attention mechanisms used additive (or Bahdanau) attention, which scored query-key pairs using a small learned feed-forward network. Dot-product attention had been explored before, but Vaswani et al. identified the scaling issue caused by large dkd_k and introduced the dk\sqrt{d_k} fix. The paper noted that dot-product attention is much faster and more space-efficient than additive attention in practice because it can be implemented as a highly optimized matrix multiplication, whereas additive attention requires a separate neural network forward pass for each query-key pair. The "Attention Is All You Need" title was a bold claim at the time: the paper proposed eliminating recurrence entirely and relying solely on attention. That claim turned out to be correct, and the transformer architecture went on to become the foundation of the entire modern language model era.

Dot Product Similarity

The first step in scaled dot-product attention is measuring how relevant each key is to each query. We need a similarity function that is fast to compute, works well in high dimensions, and integrates naturally with gradient-based learning. The choice of similarity measure is not obvious, and it is worth understanding why the dot product was selected over alternatives.

One alternative is Euclidean distance: we could measure how close two vectors are in space and use closeness as a proxy for relevance. But Euclidean distance is expensive to compute for all n2n^2 query-key pairs, and minimizing distance does not naturally integrate with the maximize-gradient training objective. Another alternative is cosine similarity, which normalizes vectors to unit length before taking the dot product. Cosine similarity is interpretable, but the normalization step is expensive and, more importantly, the model cannot learn to increase similarity by scaling the magnitude of its projections, which turns out to be a useful degree of freedom.

The dot product satisfies all three requirements: it is fast to compute via matrix multiplication, its geometric interpretation aligns naturally with learned representations, and it allows the model to control similarity through both the angle and the magnitude of projection vectors. For two vectors q\mathbf{q} and k\mathbf{k} of dimension dkd_k, the dot product is:

q⋅k=∑l=1dkqlkl\mathbf{q} \cdot \mathbf{k} = \sum_{l=1}^{d_k} q_l k_l

where:

  • qlq_l: the ll-th component of the query vector q\mathbf{q}
  • klk_l: the ll-th component of the key vector k\mathbf{k}
  • dkd_k: the shared dimension of queries and keys

Geometrically, the dot product measures how much two vectors point in the same direction. If q\mathbf{q} and k\mathbf{k} are unit vectors, the dot product equals their cosine similarity. For general vectors, it equals ∥q∥∥k∥cos⁡θ\|\mathbf{q}\| \|\mathbf{k}\| \cos\theta, where θ\theta is the angle between them. Large positive values mean strong alignment; values near zero mean orthogonality; negative values mean the vectors point in opposite directions.

This geometric interpretation makes the dot product a natural fit for attention. During training, the model learns to align queries with relevant keys in the projected space. A position attending to relevant content will have its query vector pointing in a direction similar to the key vectors of those relevant positions. The projection matrices WQW^Q and WKW^K are jointly trained to produce this alignment: WQW^Q shapes queries so they point toward relevant key directions, and WKW^K shapes keys so they point toward the directions of queries that should attend to them. The learning process carves out a shared geometric space where relevance is encoded as directional alignment.

Why does this formula make sense? Notice that the dot product is bilinear: it is linear in q\mathbf{q} holding k\mathbf{k} fixed, and linear in k\mathbf{k} holding q\mathbf{q} fixed. This bilinearity is what makes the gradient flow back through both queries and keys cleanly during backpropagation. It is also what allows the score matrix to be computed as a single matrix multiplication, which modern hardware executes at peak efficiency.

The Score Matrix

In practice, we want to compute similarities between all nn queries and all nn keys simultaneously. Processing them one pair at a time would require n2n^2 separate dot product computations, which is slow and wasteful. Instead, we exploit the fact that all pairwise dot products can be expressed as a single matrix multiplication. This is a perfect job for linear algebra: the score matrix is:

S=QKTS = QK^T

where:

  • Q∈Rn×dkQ \in \mathbb{R}^{n \times d_k}: the query matrix, each row is one query vector
  • KT∈Rdk×nK^T \in \mathbb{R}^{d_k \times n}: the transposed key matrix, each column is one key vector
  • S∈Rn×nS \in \mathbb{R}^{n \times n}: the raw score matrix, where Sij=qi⋅kjS_{ij} = \mathbf{q}_i \cdot \mathbf{k}_j

Entry SijS_{ij} tells us how much position ii's query matches position jj's key. The entire matrix of n2n^2 scores is computed with a single matrix multiplication, which modern hardware executes very efficiently using BLAS routines that achieve near-peak floating-point throughput. This is one of the key reasons dot-product attention is so fast in practice: the score computation maps directly onto hardware-optimized matrix multiply.

The shape of SS also matters. For a sequence of nn tokens, SS has n2n^2 entries. Entry (i,j)(i, j) captures the directional alignment between token ii's query and token jj's key. The diagonal entries SiiS_{ii} capture each token's self-similarity, while off-diagonal entries capture cross-token dependencies. Before any normalization, these are called "logits" (by analogy with classification logits): raw unnormalized scores that will be converted into probabilities by softmax.

In[3]:
Code
import numpy as np

np.random.seed(42)

n = 5  # sequence length
d = 8  # input embedding dimension
d_k = 4  # query/key dimension
d_v = 4  # value dimension

# Simulated input embeddings
X = np.random.randn(n, d)

# Simulated projection matrices (normally trained)
W_Q = np.random.randn(d, d_k) * 0.3
W_K = np.random.randn(d, d_k) * 0.3
W_V = np.random.randn(d, d_v) * 0.3

# Project to Q, K, V
Q = X @ W_Q  # (n, d_k)
K = X @ W_K  # (n, d_k)
V = X @ W_V  # (n, d_v)

# Compute raw scores
S = Q @ K.T  # (n, n)
Out[4]:
Console
Shapes:
  Q: (5, 4), K: (5, 4), V: (5, 4)
  Score matrix S = QK^T: (5, 5)

Raw score matrix (entry [i,j] = similarity of query i with key j):
[[-0.047  1.305  1.059 -1.327  2.064]
 [ 0.343 -0.085 -0.362  0.668 -0.201]
 [ 0.245 -0.748 -0.738  0.392 -0.534]
 [ 0.13   0.704  0.36  -0.434  1.3  ]
 [-0.321  0.48   0.57   0.277 -0.431]]

Each row of the score matrix corresponds to one token's query matched against all keys. Notice that the scores span a wide range and include negative values. These raw scores aren't attention weights yet; we need two more transformations before they are usable. In particular, the values here are on a scale where some entries are several times larger than others, which would cause softmax to produce extremely uneven weight distributions. Before we apply softmax, we need to address the scale problem.

The Scaling Problem and Why It Matters

Before applying softmax to the score matrix, we must address a subtle but critical issue: as dkd_k grows larger, the dot products tend to grow very large in magnitude, and softmax applied to large values has an extremely undesirable property. This is not a hypothetical edge case: it is a systematic failure mode that the original transformer paper identified and explicitly corrected with the dk\sqrt{d_k} scaling factor. Without this fix, training transformers with large key dimensions would be dramatically slower and potentially unstable.

To see why dot products grow with dimension, assume that the components of q\mathbf{q} and k\mathbf{k} are independent random variables with mean 0 and variance 1. This is a reasonable initialization assumption for learned projection vectors. Then each term qlklq_l k_l has mean 0 and variance 1, since the product of two independent mean-zero unit-variance variables has variance equal to the product of their variances. The dot product is a sum of dkd_k such terms:

q⋅k=∑l=1dkqlkl\mathbf{q} \cdot \mathbf{k} = \sum_{l=1}^{d_k} q_l k_l

By properties of variance, the variance of this sum is dkd_k, making the standard deviation dk\sqrt{d_k}. So as dkd_k grows, dot products have a spread proportional to dk\sqrt{d_k}. A dkd_k of 64 yields dot products with standard deviation around 8. A dkd_k of 512 yields dot products with standard deviation around 22.6. These are large values to feed into softmax.

Variance of a Sum

If X1,X2,…,XnX_1, X_2, \ldots, X_n are independent random variables each with variance σ2\sigma^2, then Var(X1+X2+⋯+Xn)=nσ2\text{Var}(X_1 + X_2 + \cdots + X_n) = n\sigma^2, and the standard deviation is σn\sigma\sqrt{n}. This is why dot products in high dimensions have large magnitudes: each additional dimension adds one more product term to the sum, and the sum's variance grows linearly with the number of terms.

Now consider what softmax does to large inputs. The softmax converts a vector of real numbers into a probability distribution. Given an input vector z\mathbf{z} with nn elements, the softmax output for element ii is:

softmax(zi)=ezi∑jezj\text{softmax}(z_i) = \frac{e^{z_i}}{\sum_j e^{z_j}}

where:

  • ziz_i: the ii-th score (the raw dot product value for one query-key pair)
  • ezie^{z_i}: the exponential of that score, always positive
  • ∑jezj\sum_j e^{z_j}: the sum of exponentials over all positions, serving as a normalizing constant

When one value in z\mathbf{z} is substantially larger than the others, the exponential amplifies the gap dramatically. For example, if z=[0,0,10]\mathbf{z} = [0, 0, 10], the softmax is approximately [0.0000226,0.0000226,0.9999548][0.0000226, 0.0000226, 0.9999548]: a near-one-hot vector that places almost all attention on the third position. Gradients through softmax in this regime are nearly zero everywhere except at the maximum, which means parameters receive almost no learning signal. The exponential function's property of amplifying differences between values is exactly what creates this problem: a difference of 10 units in the input becomes a factor of e10≈22000e^{10} \approx 22000 in the numerators, completely overwhelming all but the largest value.

This is the saturation problem: large inputs cause softmax to produce extremely peaked distributions, which kills gradient flow and makes training difficult. Think of it like a light switch stuck in the on position: the model collapses to always attending to one position, gradients cannot push it to distribute attention more broadly, and the mechanism loses its capacity to capture dependencies across multiple positions.

In[5]:
Code
def softmax(x, axis=-1):
    """Numerically stable softmax."""
    e_x = np.exp(x - np.max(x, axis=axis, keepdims=True))
    return e_x / e_x.sum(axis=axis, keepdims=True)


# Demonstrate saturation effect
z_small = np.array([1.0, 2.0, 0.5, 1.8])
z_large = np.array([10.0, 20.0, 5.0, 18.0])  # scaled up by 10

weights_small = softmax(z_small)
weights_large = softmax(z_large)

# Gradient proxy: softmax output * (1 - softmax output),
# larger for flatter distributions
grad_small = weights_small * (1 - weights_small)
grad_large = weights_large * (1 - weights_large)
Out[6]:
Console
Softmax of small inputs: [0.1527 0.415  0.0926 0.3398]
Softmax of large inputs: [0.     0.8808 0.     0.1192]

Gradient signal (small inputs): [0.1294 0.2428 0.084  0.2243]
Gradient signal (large inputs): [4.00000e-05 1.05021e-01 0.00000e+00 1.04990e-01]

Max gradient, small inputs: 0.2428
Max gradient, large inputs: 0.105021

The gradient signal is nearly zero when inputs are large. When dkd_k is 512 (common in transformers), unscaled dot products would have standard deviation around 22.6, pushing softmax firmly into the saturation regime. The attention mechanism would effectively be a hard argmax from the very beginning of training, with no gradient signal to guide the model toward more distributed attention patterns.

Out[7]:
Visualization
Bar chart showing relatively uniform softmax distribution at scale 1.
Softmax at scale 1. The distribution is relatively flat, with attention spread across all four positions. Gradients are healthy at this scale, meaning all positions receive meaningful learning signal during backpropagation.
Bar chart showing moderately peaked softmax distribution at scale 2.
Softmax at scale 2. Sharpening is visible: the dominant input (v2) takes more weight, but other positions still receive meaningful attention. Gradients are beginning to weaken for low-weight positions.
Bar chart showing highly peaked softmax distribution at scale 4.
Softmax at scale 4. Most probability mass concentrates on the maximum. Gradient signal for non-maximum positions is weakening significantly, reducing the model's ability to learn from them.
Bar chart showing near-one-hot softmax distribution at scale 8.
Softmax at scale 8. Nearly all attention weight sits on the single maximum input, approaching a one-hot distribution. Gradients for all other positions are near zero, making learning difficult. The dashed line shows the uniform (0.25) baseline.

The Scaling Fix

The original "Attention Is All You Need" paper introduced a simple fix: divide the dot products by dk\sqrt{d_k} before applying softmax. The purpose of this step is to counteract the variance growth we identified above. The scaled score matrix becomes:

S~=QKTdk\tilde{S} = \frac{QK^T}{\sqrt{d_k}}

where:

  • QKT∈Rn×nQK^T \in \mathbb{R}^{n \times n}: the raw score matrix from matrix multiplication
  • dk\sqrt{d_k}: the scaling factor, the square root of the query/key dimension
  • S~∈Rn×n\tilde{S} \in \mathbb{R}^{n \times n}: the scaled scores, ready for softmax

Why dk\sqrt{d_k} specifically? Dividing by dk\sqrt{d_k} normalizes the variance. Recall that the raw dot products have standard deviation dk\sqrt{d_k} under our initialization assumption. After dividing by dk\sqrt{d_k}, the standard deviation becomes 1, just like unit-variance inputs. This keeps the softmax operating in its sensitive, gradient-rich regime regardless of how large dkd_k is. You can think of it as a standardization step: instead of letting the scale of the inputs to softmax depend on an architectural hyperparameter, we normalize them so the effective scale is always around 1.

Why does this formula make sense? Notice that scaling by 1/dk1/\sqrt{d_k} does not change the ranking of scores: if key jj had the highest raw score for query ii, it still has the highest scaled score. What changes is the sharpness of the resulting softmax distribution. With variance-normalized inputs, softmax produces distributions in the range where gradients are non-trivial, and the model can learn to make them sharper or flatter as needed by adjusting the directions of its query and key vectors. The scaling constant is simply a normalization factor, not a learnable parameter.

In[8]:
Code
# Compare raw vs scaled score statistics
scale = np.sqrt(d_k)
S_scaled = S / scale

raw_std = S.std()
scaled_std = S_scaled.std()
Out[9]:
Console
d_k = 4, scale factor = sqrt(4) = 2.000
Raw score std:    0.7420
Scaled score std: 0.3710

After scaling, the scores have a standard deviation close to 1, placing softmax in its operating range. The scaling factor is a constant; it doesn't change what the model learns, only the numerical scale of the inputs to softmax. This is a good example of a design pattern common in deep learning: a simple normalization step that costs almost nothing computationally but enables significantly more stable and effective training.

Softmax Over Attention Logits

With scaled scores in hand, we apply softmax row-wise to convert each token's scores into a probability distribution over positions. The purpose of this step is to ensure the attention weights are non-negative and sum to one, which is necessary for the subsequent weighted sum of values to be a proper convex combination. Without softmax, the weights could be negative or unbounded, making the output an arbitrary linear combination rather than a meaningful weighted average.

The softmax is applied independently to each row of the scaled score matrix:

A=softmax ⁣(QKTdk)A = \text{softmax}\!\left(\frac{QK^T}{\sqrt{d_k}}\right)

where the softmax is applied to each row independently. Entry AijA_{ij} gives the attention weight: the fraction of position jj's value that position ii incorporates into its output. The attention weights satisfy:

∑j=1nAij=1for all i\sum_{j=1}^{n} A_{ij} = 1 \quad \text{for all } i

and all entries are non-negative (Aij≥0A_{ij} \geq 0), making each row a proper probability distribution.

Attention Weights

Attention weights are the output of softmax applied to scaled dot-product scores. They form a probability distribution over all positions for each query position. A weight AijA_{ij} close to 1 means position ii almost exclusively attends to position jj; uniform weights indicate the output will be an even blend of all positions. The row-sum constraint ensures the output is a convex combination of value vectors, which keeps output magnitudes in a predictable range and aids in training stability.

The softmax has a critical secondary effect: it sharpens the distribution. Positions with high raw scores receive exponentially more attention than positions with low scores. After training, the model learns to assign high scores to relevant positions, producing peaked attention distributions that focus on key information while still being smooth enough to allow gradient-based learning. This sharpening effect is what gives attention its ability to be selective: a model that has learned to identify the antecedent of a pronoun will have a very peaked attention distribution when that pronoun is the current query, with nearly all weight on the antecedent token.

Softmax is applied row-wise, not to the entire matrix. Each query independently produces its own probability distribution over keys. This means different tokens can attend to entirely different parts of the sequence simultaneously, which is exactly the flexibility we need. Token ii's attention distribution is completely independent of token jj's, even though they are computed from the same key matrix.

In[10]:
Code
# Apply softmax to scaled scores
A = softmax(S_scaled, axis=1)  # row-wise softmax
Out[11]:
Console
Attention weight matrix A (each row sums to 1):
[[0.1234 0.2426 0.2145 0.0651 0.3545]
 [0.2248 0.1815 0.158  0.2645 0.1712]
 [0.2517 0.1531 0.1539 0.2708 0.1704]
 [0.1666 0.222  0.1869 0.1256 0.299 ]
 [0.1575 0.2351 0.2459 0.2124 0.1491]]

Row sums: [1. 1. 1. 1. 1.]
All weights non-negative: True

Each row is now a valid probability distribution. Row ii tells us how much each position contributes to token ii's output representation. Looking at these numbers, you can read off how each position distributes its attention: does it focus on one position heavily, or spread its attention broadly? Both patterns are useful in different linguistic contexts, and the model learns which pattern to produce based on the content of the sequence.

Out[12]:
Visualization
Heatmap of 5x5 attention weights showing which positions each query token attends to.
Attention weight heatmap for a 5-token sequence after applying scaled dot-product attention. Each row shows how one query token distributes its attention across all key tokens, with darker cells indicating stronger attention. The weights in every row sum to 1, making each row a valid probability distribution over key positions. Notice how some tokens attend broadly while others show concentrated attention on specific positions.

Weighted Sum of Values

The final step uses the attention weights to aggregate the value vectors. For each query position ii, we compute a weighted combination of all value vectors. This step produces the enriched representations: it takes information from all positions in the sequence and blends it together, with the blend controlled by the attention weights we just computed.

To understand why this design makes sense, consider the alternative: using the keys directly for the output instead of having a separate value projection. If we used keys as values, the same representation would serve two incompatible purposes: advertising relevance (key role) and contributing information (value role). Keeping them separate gives the model the freedom to have token jj's key say "I am relevant to syntactic subject queries" while its value says "here is the rich semantic content I contribute when selected." This decoupling is what allows attention heads to specialize in different linguistic functions.

For each query position ii, the output is:

outputi=∑j=1nAijvj\text{output}_i = \sum_{j=1}^{n} A_{ij} \mathbf{v}_j

where:

  • AijA_{ij}: the attention weight from position ii to position jj, giving the fraction of position jj's value to include
  • vj\mathbf{v}_j: the value vector for position jj, containing the information that position jj contributes when selected
  • outputi\text{output}_i: the attention output for position ii, a dvd_v-dimensional context-enriched vector

In matrix form across all positions simultaneously:

Attention(Q,K,V)=AV=softmax ⁣(QKTdk)V\text{Attention}(Q, K, V) = AV = \text{softmax}\!\left(\frac{QK^T}{\sqrt{d_k}}\right) V

where:

  • A∈Rn×nA \in \mathbb{R}^{n \times n}: the attention weight matrix, rows are probability distributions over positions
  • V∈Rn×dvV \in \mathbb{R}^{n \times d_v}: the value matrix, rows are value vectors for each position
  • AV∈Rn×dvAV \in \mathbb{R}^{n \times d_v}: the output matrix, rows are attention outputs for each position

The output for each position is a soft lookup: a blended summary of information from all positions, weighted by relevance. Positions that received high attention weights contribute most; positions with low weights contribute little. The output has shape (n,dv)(n, d_v), matching the input shape (with dvd_v instead of the original embedding dimension dd). This means each token gets a new representation of the same dimension, enriched by context from the whole sequence.

Why is this better than a hard lookup (selecting the single most relevant position)? Because language rarely has crisp, discrete dependencies. Understanding "it" in "The cat sat on the mat and it purred" requires integrating information from multiple positions: "cat" is most relevant, but the verb tense and surrounding context also contribute. The soft weighted sum captures this gracefully. It is also differentiable everywhere, which means gradients flow smoothly through the output back to both the value vectors and the attention weights, allowing end-to-end training to optimize all components jointly.

In[13]:
Code
# Compute the attention output
output = A @ V  # (n, d_v)
Out[14]:
Console
Output shape: (5, 4)
Attention output matrix (each row is one token's enriched representation):
[[-0.0954 -0.66    0.6243 -0.1932]
 [-0.246  -0.6701  0.1977  0.2133]
 [-0.2605 -0.6365  0.1641  0.26  ]
 [-0.1539 -0.6517  0.4929 -0.0397]
 [-0.1038 -0.7851  0.2428 -0.0734]]

Each row of the output is a dvd_v-dimensional vector: a context-aware representation of that token, blended from value vectors according to attention weights. Tokens that attended broadly have outputs that are smooth blends of many values; tokens that attended sharply to a few positions have outputs dominated by those positions' values. After this step, each token's representation contains information from the entire sequence, weighted by learned relevance, which is precisely what makes attention powerful for understanding language.

The Full Attention Formula

Putting it all together, the complete scaled dot-product attention formula is:

Attention(Q,K,V)=softmax ⁣(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{QK^T}{\sqrt{d_k}}\right) V

This single formula packages four sequential operations:

  1. QKTQK^T: compute all pairwise query-key similarities as a matrix multiplication, producing an n×nn \times n score matrix
  2. /dk/ \sqrt{d_k}: scale to prevent softmax saturation, normalizing variance to approximately 1
  3. softmax(⋅)\text{softmax}(\cdot): normalize each row into an attention weight distribution, producing non-negative weights summing to 1
  4. ×V\times V: produce context-enriched outputs as weighted sums of value vectors, blending information across positions

Each operation is differentiable everywhere (the scaling is a constant, softmax is smooth, and matrix multiply is linear), so gradients flow cleanly backward through the entire chain. This is what makes end-to-end training possible: a loss computed on the final outputs can propagate gradients back through the weighted sum, back through the softmax, back through the scaling and dot products, and ultimately back to the projection matrices WQW^Q, WKW^K, and WVW^V that produced QQ, KK, and VV.

In[15]:
Code
def scaled_dot_product_attention(Q, K, V, mask=None):
    """
    Scaled dot-product attention.

    Args:
        Q: Query matrix, shape (n, d_k)
        K: Key matrix, shape (n, d_k)
        V: Value matrix, shape (n, d_v)
        mask: Optional boolean mask, shape (n, n). True = mask out.

    Returns:
        output: Attention output, shape (n, d_v)
        weights: Attention weights, shape (n, n)
    """
    d_k = Q.shape[-1]
    scores = Q @ K.T / np.sqrt(d_k)

    if mask is not None:
        scores = np.where(mask, -1e9, scores)

    weights = softmax(scores, axis=-1)
    output = weights @ V
    return output, weights
Out[16]:
Console
Full attention output shape: (5, 4)
Attention weights (first two rows):
[[0.1234 0.2426 0.2145 0.0651 0.3545]
 [0.2248 0.1815 0.158  0.2645 0.1712]]

This implementation handles the common case without masking. The optional mask argument will become important when we discuss causal attention in the next section. Notice that the function is entirely self-contained: given QQ, KK, and VV, it computes the output in four lines of code. The elegance of the implementation reflects the mathematical elegance of the formula itself.

Masking for Causal Attention

So far, attention has been bidirectional: each position can attend to every other position, including those that come later in the sequence. This is fine for encoding tasks like classification or feature extraction, where the full sequence is available at inference time. But for language generation, where we predict token t+1t+1 given tokens 11 through tt, we need to prevent each position from seeing future tokens. This is causal (or autoregressive) attention.

The reason this constraint is critical goes beyond preventing "cheating." If a model is allowed to attend to future tokens during training, it learns to predict tokens by copying from the future rather than modeling language. The training loss would be low, but the model would be useless for generation, where the future tokens are exactly what we're trying to predict. Causal masking enforces the correct inductive bias: the model must learn to predict each token from the past context alone.

Causal Attention

Causal attention (also called masked self-attention) restricts each position to attend only to itself and earlier positions. This preserves the autoregressive property needed for text generation: each token's representation depends only on the tokens that preceded it. The GPT family of models uses causal attention exclusively. Encoder-only models like BERT use bidirectional attention, relying on the full context in both directions. Encoder-decoder models like T5 use causal attention in the decoder and bidirectional attention in the encoder.

The mechanism is straightforward: before applying softmax, we add a very large negative value (effectively −∞-\infty) to the scores for all positions that should be masked. After softmax, e−∞=0e^{-\infty} = 0, so those positions receive zero attention weight. The elegance of this approach is that it requires no changes to the softmax function itself: the masking is applied to the inputs of softmax, and softmax naturally produces zero weights for −∞-\infty inputs.

The causal mask is an upper-triangular matrix of True values (above the diagonal represents future positions):

Mij={True (mask)if j>iFalse (keep)if j≤iM_{ij} = \begin{cases} \text{True (mask)} & \text{if } j > i \\ \text{False (keep)} & \text{if } j \leq i \end{cases}

where ii is the query position (row) and jj is the key position (column). Position ii can attend to positions 0,1,…,i0, 1, \ldots, i but not to positions i+1,i+2,…,n−1i+1, i+2, \ldots, n-1. The diagonal entries (j=ij = i, self-attention) are not masked: each token can always attend to itself.

In[17]:
Code
def make_causal_mask(n):
    """Create an upper-triangular causal mask.
    True means 'mask out this position'.
    """
    mask = np.triu(np.ones((n, n), dtype=bool), k=1)
    return mask


# Create causal mask for sequence of length n
causal_mask = make_causal_mask(n)
Out[18]:
Console
Causal mask (True = masked out):
[[0 1 1 1 1]
 [0 0 1 1 1]
 [0 0 0 1 1]
 [0 0 0 0 1]
 [0 0 0 0 0]]

Meaning: row i can only attend to columns 0..i (its own and previous positions)
In[19]:
Code
# Apply causal masking
output_causal, weights_causal = scaled_dot_product_attention(
    Q, K, V, mask=causal_mask
)
Out[20]:
Console
Causal attention weights (upper triangle is zero):
[[1.     0.     0.     0.     0.    ]
 [0.5533 0.4467 0.     0.     0.    ]
 [0.4505 0.2741 0.2755 0.     0.    ]
 [0.2376 0.3166 0.2666 0.1792 0.    ]
 [0.1575 0.2351 0.2459 0.2124 0.1491]]

Row sums still equal 1 (weights redistribute among unmasked positions):
[1. 1. 1. 1. 1.]

With causal masking, token 0 attends only to itself, token 1 attends to tokens 0 and 1, and so on. The weight in each row still sums to 1, since softmax normalizes over only the unmasked positions. This is exactly what language models like GPT use during training: each token in the sequence produces predictions for the next token, with the mask ensuring the model never peeks ahead. An important practical benefit is that this allows processing the entire sequence in parallel during training: rather than processing token 1, then token 2, then token 3, we can process all tokens simultaneously with the causal mask ensuring each position only sees its allowed context. This is a major advantage over recurrent neural networks, which are inherently sequential.

Out[21]:
Visualization
Binary heatmap of causal attention mask showing lower-triangular structure.
Causal attention mask for a 5-token sequence. White cells are allowed (query can attend to key), dark cells are blocked. The lower-triangular structure ensures each position only attends to earlier positions and itself, enforcing the autoregressive property needed for text generation. This allows the full sequence to be processed in parallel during training.
Attention weight heatmap with upper triangle zeroed out by causal masking.
Attention weights after applying the causal mask. The upper triangle is exactly zero, confirming no future token contributes to any current token's representation. Weights within the allowed lower triangle sum to 1 for each row, with the redistribution becoming more concentrated for early tokens that have fewer allowed positions.

Padding Masks

Beyond causal masking, transformers also use padding masks to handle variable-length sequences. When batching sequences of different lengths, shorter sequences are padded with a special token to reach a common length. The model should not attend to padding positions, since they contain no real information: a padded position is an artifact of the batching process, not a meaningful part of the input.

The problem with attending to padding positions is subtle but real. If a padding token happens to receive a high dot-product score, the softmax will assign it a non-trivial weight, and the padding token's value vector will contribute to the output. Since padding tokens carry no semantic content, this is wasted attention capacity at best and harmful noise at worst. The padding mask removes this possibility by zeroing out attention weights for all padding positions.

A padding mask marks padded positions as invalid by setting the corresponding columns of the score matrix to −∞-\infty before softmax, just as the causal mask does for future positions. Combined with the causal mask (when needed), the model correctly ignores both future tokens and padding tokens.

In[22]:
Code
def make_padding_mask(n, valid_length):
    """Create a padding mask.
    Positions >= valid_length are masked as padding.

    Returns a (1, n) mask that broadcasts to (n, n) when combined with scores.
    """
    # Column j is masked if j >= valid_length
    mask = np.arange(n) >= valid_length
    # Broadcast to (n, n) by repeating for all queries
    return np.broadcast_to(mask[np.newaxis, :], (n, n))


# Simulate sequence where only first 3 of 5 tokens are real
valid_length = 3
padding_mask = make_padding_mask(n, valid_length)
Out[23]:
Console
Padding mask (True = padded, should be masked):
[[0 0 0 1 1]
 [0 0 0 1 1]
 [0 0 0 1 1]
 [0 0 0 1 1]
 [0 0 0 1 1]]
In[24]:
Code
# Combine causal and padding masks
combined_mask = causal_mask | padding_mask

# Apply combined mask
output_masked, weights_masked = scaled_dot_product_attention(
    Q, K, V, mask=combined_mask
)
Out[25]:
Console
Combined (causal + padding) mask:
[[0 1 1 1 1]
 [0 0 1 1 1]
 [0 0 0 1 1]
 [0 0 0 1 1]
 [0 0 0 1 1]]

Attention weights with combined mask:
[[1.     0.     0.     0.     0.    ]
 [0.5533 0.4467 0.     0.     0.    ]
 [0.4505 0.2741 0.2755 0.     0.    ]
 [0.2895 0.3858 0.3247 0.     0.    ]
 [0.2467 0.3682 0.3851 0.     0.    ]]

The first three tokens attend only to earlier (non-padded) tokens within their causal window. Tokens 3 and 4 are padding positions; in practice we discard their outputs, but their attention patterns correctly reflect only valid positions. Notice that combining masks is simple: we take the logical OR of the causal mask and the padding mask, and any position that is masked by either constraint is masked in the combined mask.

Matrix Form for Batched Computation

In practice, transformers process batches of sequences simultaneously. Training on a single sequence at a time would leave GPUs severely underutilized: modern GPUs achieve peak throughput when they can process hundreds or thousands of examples in parallel. The attention formula naturally extends to batched computation by adding a batch dimension. For a batch of BB sequences each of length nn:

  • Q∈RB×n×dkQ \in \mathbb{R}^{B \times n \times d_k}: query matrices for all sequences in the batch
  • K∈RB×n×dkK \in \mathbb{R}^{B \times n \times d_k}: key matrices for all sequences in the batch
  • V∈RB×n×dvV \in \mathbb{R}^{B \times n \times d_v}: value matrices for all sequences in the batch
  • Output ∈RB×n×dv\in \mathbb{R}^{B \times n \times d_v}: enriched representations for all sequences

The matrix multiplication QKTQK^T uses batched matrix multiply: for each example in the batch, we compute the n×nn \times n score matrix independently. NumPy and PyTorch handle this efficiently through the @ operator or torch.bmm. The key point is that sequences in a batch are completely independent: the attention weights for sequence 1 do not influence the attention weights for sequence 2. The batch dimension is purely an efficiency mechanism, not a semantic one.

In[26]:
Code
def batched_attention(Q_batch, K_batch, V_batch, mask=None):
    """
    Batched scaled dot-product attention.

    Args:
        Q_batch: (B, n, d_k)
        K_batch: (B, n, d_k)
        V_batch: (B, n, d_v)
        mask: optional (n, n) mask, applied to all examples

    Returns:
        output: (B, n, d_v)
        weights: (B, n, n)
    """
    d_k = Q_batch.shape[-1]
    # Batched matrix multiply: (B, n, d_k) @ (B, d_k, n) -> (B, n, n)
    scores = Q_batch @ K_batch.transpose(0, 2, 1) / np.sqrt(d_k)

    if mask is not None:
        scores = np.where(mask[np.newaxis, :, :], -1e9, scores)

    # Softmax over last dimension (keys)
    weights = softmax(scores, axis=-1)
    # (B, n, n) @ (B, n, d_v) -> (B, n, d_v)
    output = weights @ V_batch
    return output, weights


# Create a batch of 3 sequences
B = 3
Q_batch = np.random.randn(B, n, d_k)
K_batch = np.random.randn(B, n, d_k)
V_batch = np.random.randn(B, n, d_v)
Out[27]:
Console
Batch size: 3, sequence length: 5, d_k: 4, d_v: 4
Batched output shape: (3, 5, 4)
Batched weights shape: (3, 5, 5)

The batched formulation is identical mathematically to processing each example independently, but the vectorized computation is substantially faster on GPUs due to their parallel execution model. On a modern GPU, processing a batch of 32 sequences takes roughly the same wall-clock time as processing a single sequence, because all 32 matrix multiplications can execute in parallel across thousands of GPU cores.

Numerical Stability of Scaling

The dk\sqrt{d_k} scaling prevents saturation at the start of training. It is also essential for maintaining numerical stability throughout training as model weights adapt. This distinction is worth dwelling on, because it explains why the scaling is a permanent architectural feature rather than just an initialization trick.

Consider what can go wrong without scaling. As the model trains, projection matrices WQW^Q and WKW^K can grow in magnitude to produce peaked attention distributions. Gradient descent does not naturally constrain the scale of learned parameters: if slightly larger weights consistently reduce the loss, the optimizer will keep increasing them. Once queries and keys have large dot products, softmax saturates, gradients vanish, and parameters stop updating. The model gets stuck with sharp, nearly-fixed attention patterns that cannot adapt as training proceeds. The training curve flattens, and the model is trapped in a suboptimal configuration.

With dk\sqrt{d_k} scaling, the effective inputs to softmax remain in a numerically stable range. The model can still learn to produce peaked attention (by making queries and keys point in similar directions), but the gradients remain non-zero throughout, enabling continued learning. Think of the scaling as maintaining an implicit contract: the softmax always receives inputs with unit-scale variance, regardless of how the learned weights have grown. This contract is what keeps training stable over thousands of gradient updates.

There is a secondary numerical consideration: the softmax computation itself. A naive implementation computes ezie^{z_i} for each score ziz_i. Even after scaling, some scores may be positive enough that ezie^{z_i} overflows floating-point representation. The standard fix is the log-sum-exp trick: subtract the maximum score before exponentiating.

softmax(zi)=ezi−zmax⁡∑jezj−zmax⁡\text{softmax}(z_i) = \frac{e^{z_i - z_{\max}}}{\sum_j e^{z_j - z_{\max}}}

where:

  • ziz_i: the ii-th score value
  • zmax⁡=max⁡jzjz_{\max} = \max_j z_j: the maximum score value in the row
  • ezi−zmax⁡e^{z_i - z_{\max}}: the shifted exponential, at most e0=1e^0 = 1, preventing overflow
  • ∑jezj−zmax⁡\sum_j e^{z_j - z_{\max}}: the shifted normalization constant

Subtracting the max shifts the largest value to zero, preventing overflow while producing identical results mathematically (the max cancels in both numerator and denominator). Why does this formula make sense? Notice that ezi−zmax⁡∑jezj−zmax⁡=ezi/ezmax⁡∑jezj/ezmax⁡=ezi∑jezj\frac{e^{z_i - z_{\max}}}{\sum_j e^{z_j - z_{\max}}} = \frac{e^{z_i} / e^{z_{\max}}}{\sum_j e^{z_j} / e^{z_{\max}}} = \frac{e^{z_i}}{\sum_j e^{z_j}}, which is exactly the original softmax. The shift is mathematically neutral but numerically essential.

In[28]:
Code
def stable_softmax(z, axis=-1):
    """Numerically stable softmax using log-sum-exp trick."""
    z_max = np.max(z, axis=axis, keepdims=True)
    e_z = np.exp(z - z_max)
    return e_z / e_z.sum(axis=axis, keepdims=True)


# Demonstrate: naive softmax can overflow; stable version doesn't
z_extreme = np.array([1000.0, 1001.0, 999.0])

naive_result = (
    np.exp(z_extreme) / np.exp(z_extreme).sum()
)  # Will overflow -> NaN
stable_result = stable_softmax(z_extreme)
Out[29]:
Console
Naive softmax (overflow): [nan nan nan]
Stable softmax (correct): [0.244728 0.665241 0.090031]

Our softmax function already uses the log-sum-exp trick. Production implementations in PyTorch and JAX also use this by default, so overflow is typically not a concern in practice, but understanding why it matters explains why the stable version is the standard. When building your own attention implementations, always use the numerically stable version.

Computational Complexity

Scaled dot-product attention has a computational cost that shapes how transformers scale with sequence length. Understanding this cost motivates both current architectural choices and research into more efficient attention mechanisms. The quadratic scaling is the primary bottleneck limiting the sequence length modern transformers can process.

The dominant cost is the QKTQK^T matrix multiplication. Multiplying an n×dkn \times d_k matrix by a dk×nd_k \times n matrix requires O(n2dk)O(n^2 d_k) multiply-add operations. The subsequent AVAV multiply is O(n2dv)O(n^2 d_v). Together, the total computation is:

O(n2d)O(n^2 d)

where d=max⁡(dk,dv)d = \max(d_k, d_v), assuming dkd_k and dvd_v are comparable. The quadratic factor n2n^2 means that doubling the sequence length quadruples the computation, and multiplying the sequence length by 10 increases computation by 100x.

The memory cost is also O(n2)O(n^2): we must store the n×nn \times n score matrix and the n×nn \times n attention weight matrix. For typical sequence lengths of 512 or 1024 tokens with 32-bit floats, this is manageable. But for very long sequences (tens of thousands of tokens), storing O(n2)O(n^2) matrices becomes infeasible. A sequence of length 32,768 tokens would require a single attention matrix with over a billion entries, far exceeding available GPU memory.

In[30]:
Code
# Compute FLOPs for scaled dot-product attention
def attention_flops(n, d_k, d_v=None):
    """Approximate FLOPs for one forward pass of attention."""
    if d_v is None:
        d_v = d_k
    # QK^T: n queries x d_k x n keys -> O(n^2 * d_k) multiply-adds
    qkt_flops = 2 * n * n * d_k  # factor 2 for multiply + add
    # AV: n x n attention weights x d_v -> O(n^2 * d_v)
    av_flops = 2 * n * n * d_v
    return qkt_flops + av_flops


seq_lengths = [32, 64, 128, 256, 512, 1024, 2048]
dk = 64
flops = {n_len: attention_flops(n_len, dk) for n_len in seq_lengths}
memory_n2 = {n_len: n_len**2 for n_len in seq_lengths}
Out[31]:
Console
  Seq length           FLOPs   Attention matrix
------------------------------------------------
          32         262,144              1,024
          64       1,048,576              4,096
         128       4,194,304             16,384
         256      16,777,216             65,536
         512      67,108,864            262,144
        1024     268,435,456          1,048,576
        2048   1,073,741,824          4,194,304

The quadratic growth is visible in the numbers. Going from sequence length 512 to 2048 (4x increase) multiplies the FLOPs by 16x. Going from 64 to 2048 (32x increase) multiplies FLOPs by 1024x. This quadratic bottleneck is why large-context language models require specialized attention mechanisms. Research into efficient alternatives has produced sparse attention (attending to a fixed local window plus selected global tokens), Longformer's sliding window attention, Linformer's low-rank key-value compression, and FlashAttention's memory-efficient tiling approach. We'll explore efficient alternatives in the Attention Complexity chapter.

Out[32]:
Visualization
Line plot showing quadratic FLOPs growth with sequence length from 32 to 2048.
FLOPs for scaled dot-product attention as a function of sequence length, showing clear quadratic growth. Doubling the sequence length quadruples the computation: at length 2048, the cost is 64x that of length 256. This scaling behavior is the central motivation for efficient attention research targeting long-document and long-context applications.
Line plot showing quadratic memory growth with sequence length from 32 to 2048.
Attention matrix memory cost (number of elements) as a function of sequence length, also growing quadratically. At 2048 tokens, a single attention matrix has over 4 million entries. With 12 attention heads, 32-bit floats, and a batch size of 32, attention matrices alone require over 6 GB per layer, making long-context inference memory-bound before any other model parameters are considered.

A Complete Worked Example

Theory becomes concrete when you trace through a small example by hand. Let's work through a minimal but complete attention computation with a 4-token sequence: "the", "cat", "sat", "there". We'll use small dimensions (dk=dv=3d_k = d_v = 3) so every intermediate value fits on screen and we can interpret each step.

The goal of this example is to make the abstract operations tangible. When we say "query ii attends to key jj," what does that look like numerically? When we say "the output is a weighted blend of values," what blend weights are we computing? Working through these questions with concrete numbers builds the intuition needed to understand attention in larger models.

Think of this worked example as running the attention formula in slow motion: we pause at each step to print the intermediate matrices, check their shapes, and interpret what the numbers mean before moving to the next step.

In[33]:
Code
# Fixed random seed for reproducibility
np.random.seed(7)

# 4-token sequence, d=6 embedding dim, d_k=d_v=3
tokens = ["the", "cat", "sat", "there"]
n_ex = 4
d_ex = 6
dk_ex = 3

# Simulated learned embeddings for each token
X_ex = np.random.randn(n_ex, d_ex)

# Simulated learned projection weights
W_Q_ex = np.random.randn(d_ex, dk_ex) * 0.5
W_K_ex = np.random.randn(d_ex, dk_ex) * 0.5
W_V_ex = np.random.randn(d_ex, dk_ex) * 0.5

# Project to Q, K, V
Q_ex = X_ex @ W_Q_ex
K_ex = X_ex @ W_K_ex
V_ex = X_ex @ W_V_ex

# Step 1: Raw scores
S_ex = Q_ex @ K_ex.T

# Step 2: Scale
S_scaled_ex = S_ex / np.sqrt(dk_ex)

# Step 3: Softmax
A_ex = softmax(S_scaled_ex, axis=1)

# Step 4: Weighted sum of values
out_ex = A_ex @ V_ex
Out[34]:
Console
=== Step 1: Raw scores QK^T ===
         the      cat      sat    there
  the    3.977    3.688    2.635    3.222
  cat    4.704    5.336    2.664    4.826
  sat   -2.616   -1.913   -2.144   -0.687
there    3.107    2.737    2.534    0.223

=== Step 2: Scaled scores (divide by sqrt(3)=1.732) ===
         the      cat      sat    there
  the    2.296    2.129    1.521    1.861
  cat    2.716    3.081    1.538    2.786
  sat   -1.510   -1.105   -1.238   -0.396
there    1.794    1.580    1.463    0.129

=== Step 3: Attention weights (softmax) ===
         the      cat      sat    there
  the   0.3385   0.2865   0.1560   0.2190
  cat   0.2617   0.3769   0.0806   0.2808
  sat   0.1458   0.2187   0.1915   0.4441
there   0.3683   0.2974   0.2646   0.0697

=== Step 4: Output (A @ V) shape: (4, 3) ===
  the:  [-1.5223 -0.9709  0.1333]
  cat:  [-1.8853 -0.8206  0.1676]
  sat:  [-1.7339 -0.792   0.4547]
there:  [-1.1056 -1.034   0.0768]

Walk through what happened at each step. The raw score matrix in Step 1 contains unscaled query-key dot products: each entry is the raw similarity between one token's query and one token's key, before any normalization. After scaling in Step 2, the values shrink by a factor of 3≈1.73\sqrt{3} \approx 1.73, bringing them into the range where softmax operates without saturation. The attention weight matrix in Step 3 shows which positions each token attends to most: each row is a proper probability distribution, and you can see which keys received the most attention weight from each query. Finally, Step 4 shows the output for each token, which is a 3-dimensional vector representing that token's context-enriched representation after blending in information from across the sequence.

One observation worth making: even though the token sequence has semantic structure, these attention patterns are driven by random projection matrices rather than learned ones. In a trained model, the patterns would be far more meaningful: "the" might strongly attend to the noun it modifies ("cat"), "sat" might attend to the subject ("cat") and object ("there"), and so on. The random weights here demonstrate the mechanics; training is what makes the weights encode linguistic structure.

Key Parameters

Scaled dot-product attention introduces two primary hyperparameters, both of which interact with the quadratic complexity we analyzed above.

The first is dkd_k (query/key dimension): the dimensionality of the query and key projections. Larger dkd_k allows queries and keys to encode more complex patterns. This provides more expressive capacity for the similarity computation. However, it increases computational cost as O(n2dk)O(n^2 d_k) and also increases the number of parameters in WQW^Q and WKW^K. Too small a dkd_k may not provide enough expressive capacity to represent the diversity of relationships the model needs to capture. Too large a dkd_k wastes computation on dimensions that carry little signal. Typical values range from 64 to 256 per head in multi-head attention, with 64 being the value used in the original transformer paper's base model.

The second is dvd_v (value dimension): the dimensionality of the value projections and the output. The value dimension controls the richness of the information each token contributes when attended to. Setting dvd_v smaller than dkd_k creates a bottleneck that forces the model to compress information, which can be a useful regularizer. Setting dvd_v larger allows richer information transfer. In practice, dv=dkd_v = d_k is by far the most common choice, and it simplifies the architecture by making all projection dimensions equal. Changing dvd_v changes the size of the output representation without affecting the attention weights.

The scaling factor dk\sqrt{d_k} is not a hyperparameter: it is derived from dkd_k and applied without modification. There is no reason to tune it separately; it is a fixed normalization constant whose derivation we worked through in the scaling section above.

Limitations and Considerations

Scaled dot-product attention is elegant and effective, but several limitations shape how it's used in practice. Understanding these limitations is not just academic: they directly motivate architectural choices in every major language model, and they explain why significant research effort has gone into attention variants over the past several years.

The most significant limitation is the O(n2)O(n^2) computational and memory cost. For a sequence of 1024 tokens processed with 12 attention heads, the attention matrices alone require storing millions of floating-point values per layer. Extending to 100,000 tokens (the range needed for document-level tasks such as analyzing entire research papers or legal contracts) makes standard attention impractical without modification. A sequence of 100,000 tokens would require 101010^{10} entries per attention layer, far beyond the memory capacity of any current hardware. Research into efficient attention has produced many alternatives: sparse attention (attending to a fixed local window plus selected global tokens), low-rank approximations (approximating the n×nn \times n matrix as a product of smaller matrices), and kernel-based methods (reformulating attention to avoid materializing the full matrix). None has fully replaced standard attention for general-purpose models, but each has found niches where it offers meaningful advantages.

Softmax attention also has a tendency toward attention dilution on long sequences. Because softmax normalizes over all positions, each attention weight becomes smaller as sequence length grows: if the sequence has 1000 tokens, the average attention weight is 1/1000, even if the most relevant position truly deserves a weight of 0.5. This means the model must learn to produce very large raw scores to overcome the normalization and assign meaningful weight to relevant positions. In practice, this can cause trained models to show unexpectedly diffuse attention on long sequences, where they fail to focus on the most relevant context even when that context is clearly present. Researchers have explored alternatives like entmax (a sparse softmax that produces exactly zero weights for irrelevant positions) and attention temperature scaling at inference time, but softmax remains the dominant choice due to its well-understood behavior and hardware efficiency.

From a representational standpoint, standard scaled dot-product attention is permutation-equivariant: swapping two tokens in the input swaps the corresponding rows and columns of the attention matrix, and the output rows swap accordingly. The mechanism has no inherent notion of position or order. Feed the same sentence in different word orders, and the attention mechanism treats it as the same set of tokens in a different arrangement. This is why transformers require explicit positional encoding: without it, the model cannot distinguish "the cat sat on the mat" from "the mat sat on the cat." Positional encodings inject order information into the token representations before they enter the attention computation, effectively breaking the permutation equivariance. We'll cover positional encodings in Part XIV of the handbook.

There is also a more subtle limitation related to the form of the similarity function. Dot-product attention computes similarity as a bilinear form, which means the similarity score between query ii and key jj can only depend on the specific pair (i,j)(i, j) through their dot product. This is a restricted form of interaction: there is no way for the attention score to depend on the relationship between key jj and other keys, or on global properties of the sequence. More expressive attention variants have been proposed, but the dot product's simplicity and efficiency have kept it dominant.

Summary

Scaled dot-product attention maps QKV inputs into context-enriched representations through four sequential operations:

  • Dot product (QKTQK^T): measures pairwise similarity between all queries and all keys, producing an n×nn \times n score matrix with O(n2dk)O(n^2 d_k) cost
  • Scaling (/dk/ \sqrt{d_k}): normalizes scores to have unit variance, preventing softmax saturation and enabling stable gradient flow throughout training
  • Softmax: converts scores row-wise into probability distributions over positions, producing attention weights Aij≥0A_{ij} \geq 0 with ∑jAij=1\sum_j A_{ij} = 1
  • Weighted sum (AVAV): aggregates value vectors according to attention weights, producing a dvd_v-dimensional context-aware representation for each position

The causal mask extends this mechanism to autoregressive generation by zeroing out attention to future positions before softmax. Combined with the padding mask for batched computation, the masking mechanism handles all common production use cases. Both types of masking work by adding −∞-\infty to score matrix entries before softmax, which causes those positions to receive exactly zero weight in the output.

The quadratic complexity O(n2d)O(n^2 d) in both computation and memory is the central scalability challenge. Efficient attention variants address this bottleneck, which we'll examine in the Attention Complexity chapter. Before that, the next chapter covers multi-head attention, which runs multiple instances of scaled dot-product attention in parallel to allow the model to attend to different aspects of the input simultaneously. Multi-head attention is the direct generalization of everything we've covered here: it applies scaled dot-product attention not once but hh times, with different learned projections for each head, and concatenates the results. Understanding scaled dot-product attention fully, as we've done in this chapter, is the necessary foundation for understanding why multi-head attention works so well in practice.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about scaled dot-product attention.

Scaled Dot-Product Attention Quiz

Question 1 of 70 of 7 completed
What is the full scaled dot-product attention formula?

Comments

1 comment

  1. AvadorMember

    Really appreciate the way you have explained the concepts.

    Simple to understand, yet builds up knowledge exponentially, all while reinforcing it with examples repeatedly

    I've read the last two chapters `74.Self-attention & 75.Q,K,V ` and I've to admit, my concepts have never been clearer.

    `However, this chapter [75. Scaled Dot-Product Attention] feels redundant.` All repetition. I skimmed through the contents and attempted the quiz. Completed it.

    Don't understand the thought process behind organizing it this way, just thought I'd let you know.

    (You really are doing an amazing job breaking down maths and complex concepts like a story)

    On a side note, this question in Quiz wasn't being displayed properly.

    In the attention formula Attention(QAttention(Q,K,V)=\text{softmax}(QK^T/\sqrt{d_k})V, what is the shape of the output if QhasQhas shape (n(n,d_k) and VhasVhas shape (n(n,d_v)?

    1. Michael BrenndoerferMember

      Thanks for your feedback, Avador!

      I will revisit and see if it can be structured more effectively. Also, thanks for pointing out the Quiz render errors.

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2025scaleddot, author = {Michael Brenndoerfer}, title = {Scaled Dot-Product Attention: The Core Transformer Mechanism}, year = {2025}, url = {https://mbrenndoerfer.com/writing/scaled-dot-product-attention-transformer-mechanism}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-09-27} }
APAAcademic
Michael Brenndoerfer (2025). Scaled Dot-Product Attention: The Core Transformer Mechanism. Retrieved from https://mbrenndoerfer.com/writing/scaled-dot-product-attention-transformer-mechanism
MLAAcademic
Michael Brenndoerfer. "Scaled Dot-Product Attention: The Core Transformer Mechanism." 2026. Web. September 27, 2026. <https://mbrenndoerfer.com/writing/scaled-dot-product-attention-transformer-mechanism>.
CHICAGOAcademic
Michael Brenndoerfer. "Scaled Dot-Product Attention: The Core Transformer Mechanism." Accessed September 27, 2026. https://mbrenndoerfer.com/writing/scaled-dot-product-attention-transformer-mechanism.
HARVARDAcademic
Michael Brenndoerfer (2025) 'Scaled Dot-Product Attention: The Core Transformer Mechanism'. Available at: https://mbrenndoerfer.com/writing/scaled-dot-product-attention-transformer-mechanism (Accessed: September 27, 2026).
SimpleBasic
Michael Brenndoerfer (2025). Scaled Dot-Product Attention: The Core Transformer Mechanism. https://mbrenndoerfer.com/writing/scaled-dot-product-attention-transformer-mechanism

About the author

Continue with the full handbook

This chapter is part of Language AI Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.

Explore Language AI Handbook
Newsletter

Stay up to date

Get articles, book updates, and news delivered to your inbox.

No spam, unsubscribe anytime.

or

Join the community

Sign in to remove popups, track your reading progress, and join the discussion.