Skip to content

← Flax step 21 of 100

Medium Primitives

Scaled Dot-Product Attention

Jump to the task

Choose a mixture of values for each query

Scaled dot-product attention compares each query with a set of keys. Softmax converts those scores into nonnegative weights that sum to one across keys. The output for that query is the weighted average of the corresponding value vectors:

scores  = Q @ K.T / sqrt(d)
weights = softmax(scores, axis=-1)
output  = weights @ V

Keys determine the scores; values supply the vectors to combine. Each key must have a corresponding value, but a value can have a different feature count from a query or key.

This exercise receives queries, keys and values directly. It includes no learned projections, masking, dropout or output projection. In a self-attention layer these inputs usually come from projections of the same sequence. In cross-attention, queries come from one sequence and keys and values from another. The calculation here supports either.

Follow the shapes

All inputs are two-dimensional, representing one sequence per input and one attention head:

Array Shape Meaning
q (T_q, d) One query vector per output position
k (T_k, d) One key vector per candidate position
v (T_k, d_v) One value vector per key
scores, weights (T_q, T_k) One row per query, one column per key
output (T_q, d_v) One weighted value vector per query

Apply softmax along the last axis so each query distributes weight across its keys. Normalizing along axis zero would distribute weight across queries instead, which is a different operation.

Why divide by the square root of the feature count?

Suppose query and key components are independent, have mean zero and variance one, and the component products are independent across features. A dot product sums d products, so its variance is d. Dividing the dot product by sqrt(d) gives variance one under these assumptions.

Large score differences can make softmax concentrate almost all its weight on one key, making its derivatives small. Scaling helps control that effect. Learned queries and keys need not satisfy the assumptions, so the scale is not a guarantee of unit variance or healthy gradients. Dividing by d would shrink the scores by an additional factor of sqrt(d) compared with the required formula.

A numerical example

Use two queries, three keys and values with two features:

import jax
import jax.numpy as jnp

q = jnp.eye(3)[:2]                          # (2, 3)
k = jnp.eye(3)                              # (3, 3)
v = jnp.array([[1., 0.], [0., 1.], [1., 1.]])
scores = q @ k.T / jnp.sqrt(3.0)           # (2, 3)
weights = jax.nn.softmax(scores, axis=-1)
out = weights @ v                         # (2, 2)

assert jnp.allclose(weights.sum(axis=-1), jnp.ones(2))
assert jnp.allclose(
    weights[0], jnp.array([0.471083, 0.264458, 0.264458]), atol=1e-6
)
assert jnp.allclose(
    out, jnp.array([[0.735541, 0.528917], [0.528917, 0.735541]]),
    atol=1e-6,
)

Query zero has score 1/sqrt(3) for key zero and zero for the other keys. Its first output feature is 0.471083 + 0.264458, because values zero and two have a 1 in that feature. Its second output feature is 0.264458 + 0.264458, from values one and two.

If all scores in a query row are equal, its output is the mean of the value vectors. Softmax is still necessary: multiplying unnormalized scores by values does not give the specified weighted average.

Problem

Implement scaled_dot_product(q, k, v):

  1. Read the query/key feature count d = q.shape[-1].
  2. Compute q @ k.T / jnp.sqrt(d).
  3. Apply jax.nn.softmax along the key axis, axis=-1.
  4. Multiply the weights by v and return the two-dimensional result.

The inputs are finite float32 arrays with the shapes above. The key count T_k and feature counts d and d_v are positive. The query count T_q may be zero, producing shape (0, d_v). Use values whose dot products stay finite in float32. Do not flatten the output or add a causal mask. No Linen module or random key is needed.

Loading visualization…