Skip to content

← Attention Variants step 10 of 14

Hard End-to-End

Causal RoPE and SwiGLU Block

Build a causal attention block with rotary position embeddings, followed by a gated feed-forward network. Each branch receives a normalized input, but its output is added to the unnormalized residual stream.

The components are often used in language models. This exercise specifies a simplified block with ordinary multi-head attention and no learned RMSNorm scale. Follow the operations below rather than copying a complete model's architecture or parameter layout.

Inputs and output

Implement llama_block(x, w_q, w_k, w_v, w_o, w_gate, w_up, w_down, num_heads, freqs_cos, freqs_sin). Keep the supplied function name.

  • x has shape (N, T, d_model).
  • w_q, w_k, w_v, w_o each have shape (d_model, d_model).
  • w_gate and w_up have shape (d_model, d_ff).
  • w_down has shape (d_ff, d_model).
  • num_heads divides d_model; d_head = d_model // num_heads is even.
  • freqs_cos and freqs_sin each have shape (T, d_head // 2).

All arrays are floating-point and all dimensions are positive. Return shape (N, T, d_model). Use the supplied matrix orientations and cosine/sine tables directly. There are no biases, dropout, padding masks or learned normalization parameters. Keep batch items independent.

Normalize without subtracting the mean

Use this RMSNorm separately for each token, reducing over the final axis:

rmsnorm(t) = t / sqrt(mean(t*t, axis=-1, keepdims=True) + 1e-6)

Unlike layer normalization, this does not subtract the mean. For example, a nonzero constant vector stays nonzero after RMSNorm. Add epsilon inside the square root and do not apply a learned scale.

Attention branch

  1. Compute norm1 = rmsnorm(x). Project Q, K and V from norm1 using their respective weight matrices. Keep the original x for the residual.
  2. Reshape each projection to (N, T, num_heads, d_head), then swap the head and sequence axes to get (N, num_heads, T, d_head).
  3. Rotate adjacent feature pairs in Q and K only. For pair (a, b) at position t and pair index k, take c = freqs_cos[t, k] and s = freqs_sin[t, k]. Replace the pair with (a*c - b*s, a*s + b*c). Use both original coordinates, broadcast tables over batches and heads, and interleave the results. Leave V unchanged.
  4. Compute scores = Q_rot @ transpose_last_two_axes(K_rot) / sqrt(d_head). The score shape is (N, num_heads, T, T). Allow key j for query i only when j <= i. Replace future scores with negative infinity, then apply a stable softmax over keys and multiply by V.
  5. Swap the head and sequence axes back and join features in their original order. Compute y = x + joined @ w_o.

Create the causal mask on the input device in PyTorch. Every row includes its diagonal, so none is fully masked. Assume allowed scores are finite. A finite sentinel such as -1e9 is not exact exclusion when valid scores can be even smaller.

Gated feed-forward branch

Normalize the updated residual y, then compute:

norm2 = rmsnorm(y)
gate = silu(norm2 @ w_gate)
up = norm2 @ w_up
ffn_out = (gate * up) @ w_down
output = y + ffn_out

Here silu(t) = t * sigmoid(t). The gate and up arrays both have shape (N, T, d_ff). Their multiplication is elementwise; multiplication by w_down is a matrix product that restores width d_model. Use a numerically stable sigmoid or the framework's SiLU operation.

Check the residual order

There is no final normalization after either residual addition. The first residual adds to x; the second adds to y, which already includes attention. If both w_o and w_down are zero, the complete block returns x unchanged. Neither branch can use future positions: attention is causally masked, and RMSNorm and the feed-forward network work separately at each position.