Skip to content

← Attention Variants step 8 of 14

Medium End-to-End

Causal Self-Attention Block

Causal self-attention lets a position read itself and earlier positions, while excluding later positions. For query position i, key position j is allowed exactly when j <= i.

For three positions, the allowed query-key pairs are:

           key 0  key 1  key 2
query 0      yes     no     no
query 1      yes    yes     no
query 2      yes    yes    yes

This exercise combines that rule with multiple attention heads, an output projection, a residual addition and layer normalization. Normalization comes after the residual addition, so this is a post-normalization block.

Inputs and output

Implement causal_self_attention(x, w_q, w_k, w_v, w_o, num_heads).

  • x: floating-point array of shape (N, T, d_model).
  • w_q, w_k, w_v, w_o: each has shape (d_model, d_model).
  • num_heads: a positive integer that divides d_model evenly.

All dimensions are positive. Return shape (N, T, d_model). Keep batches independent. There are no projection biases, dropout, padding masks, learned normalization parameters or feed-forward layer in this exercise.

Build the block

  1. Project Q = x @ w_q, K = x @ w_k and V = x @ w_v.
  2. Set d_head = d_model // num_heads. Reshape each projection to (N, T, num_heads, d_head), then swap the position and head axes to get (N, num_heads, T, d_head).
  3. Compute scores by multiplying Q by K with only its last two axes swapped, then dividing by sqrt(d_head). Scores have shape (N, num_heads, T, T).
  4. Replace scores at j > i with negative infinity. Apply a stable softmax over the last axis, which contains key positions. Multiply the weights by V.
  5. Swap the head and position axes back, then reshape to (N, T, d_model). Keep the original head order when joining features.
  6. Compute z = joined @ w_o + x, then normalize each token over features:
mean = mean(z, axis=-1, keepdims=True)
variance = mean((z - mean)**2, axis=-1, keepdims=True)
output = (z - mean) / sqrt(variance + 1e-5)

Use population variance, dividing by d_model, with epsilon inside the square root. No learned scale or offset follows normalization.

Masking details

Use a boolean (T, T) mask that broadcasts across batches and heads. In PyTorch, create it on the input device. Negative infinity gives excluded positions zero softmax weight when the allowed scores are finite. Each row here keeps at least its diagonal entry, so no row is completely masked.

A finite replacement such as -1e9 is not an exact exclusion: an allowed score can be even smaller, causing a future position to receive weight. Assume the allowed scaled dot products are finite.

Changing later input positions must not change an earlier output position. The residual and normalization preserve this property because they operate separately at each position.

The diagram starts on Causal. Filled cells are allowed connections, not attention weights. Other settings illustrate different patterns; implement the causal pattern shown in the table above.

Loading visualization…