Rotations and orthogonal matrices

An orthogonal matrix turns space without stretching it, so every length, angle and dot product survives. In 2D it is a rotation by an angle (or a mirror). Language models use rotations to tell the attention scores where tokens are (RoPE) and to tame outliers before quantization (Hadamard rotations).

Key idea
Orthogonal matrices (QᵀQ = I) keep lengths, angles and dot products; 2D rotation [[cos θ, −sin θ], [sin θ, cos θ]]; rotate pairs of dimensions; RoPE; Hadamard rotations spread outliers
Real world
Turning a paper map on the table to face north. Every distance and angle on the map is the same afterwards; only the direction it faces has changed, and turning it back undoes it exactly.
In an LLM
Llama, Mistral and Qwen rotate each query and key by an angle proportional to the token’s position (RoPE). Because rotations keep dot products, the attention score ends up depending only on how far apart two tokens are.

Turning without stretching

Most matrices stretch, squash or skew space, as in the transformations lesson. An does none of that. Its columns all have length 1 and are at right angles to each other, and as a result it keeps:

  • every length: ∥Qx∥=∥x∥\lVert Q\mathbf{x}\rVert = \lVert\mathbf{x}\rVert,
  • every angle between two vectors,
  • and therefore every dot product: (Qa)⋅(Qb)=a⋅b(Q\mathbf{a})\cdot(Q\mathbf{b}) = \mathbf{a}\cdot\mathbf{b}.

It is also trivial to undo: the inverse is the transpose, Q⊤Q=IQ^\top Q = I. In 2D the orthogonal matrices are exactly the rotations (determinant 1) and the reflections (determinant −1). The rotation by an angle θ\theta is

R(θ)=[cos⁡θ−sin⁡θsin⁡θcos⁡θ].R(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix}.

Its first column is where (1,0)(1, 0) lands, a point at angle θ\theta on the unit circle; the second column is the same turned another 90°.

Q
0.77−0.640.640.77
|a| → |Qa|2.28 → 2.28
|b| → |Qb|1.90 → 1.90
angle a, b56° → 56°
det1.00
A rotation by 40°: cos and sin of the angle in the first column, the same rotated by 90° in the second. Lengths stay 2.28 and 1.90, the angle stays 56°, so every dot product is unchanged. Determinant 1: no stretching, no flipping.
The faint arrows are the originals. Move the slider (or spin): the blue and amber arrows keep their lengths and the angle between them. Switch to shear to see a matrix that does not.
example · turning a map

You put a paper map on the table and turn it so north points the right way. Distances between towns on the map haven’t changed, nor has the angle at any road junction. Turn it back and it is exactly as before. Flip it over and it is a reflection: still the same distances, but the writing is mirrored. A photocopier set to 120% is not orthogonal: it changes every distance.

Rotating many dimensions in pairs

In higher dimensions a simple way to build a rotation is to split the vector into pairs, (x1,x2),(x3,x4),…(x_1, x_2), (x_3, x_4), \dots, and rotate each pair by its own angle with a 2 × 2 rotation. The full matrix is block-diagonal: 2 × 2 rotation blocks along the diagonal and zeros elsewhere. Because each block keeps lengths, the whole thing does.

RoPE: position as an angle

Attention scores are dot products of queries and keys, and on their own they know nothing about where the tokens are. Rotary position embedding (RoPE) fixes that with rotations:

  1. Split each query and key into pairs of numbers.
  2. Rotate pair kk of the token at position mm by the angle m θkm\,\theta_k, where θk=10000−2k/d\theta_k = 10000^{-2k/d}: fast for the first pairs, very slow for the last.
  3. Compute attention scores as usual.

Because a rotation keeps dot products, rotating the query by mθm\theta and the key by nθn\theta gives the same score as rotating only the key by (n−m)θ(n - m)\theta:

(Rmθ q)⋅(Rnθ k)=q⋅(R(n−m)θ k).(R_{m\theta}\,\mathbf{q})\cdot(R_{n\theta}\,\mathbf{k}) = \mathbf{q}\cdot(R_{(n-m)\theta}\,\mathbf{k}).

So the score depends on the distance between the tokens, not on where they are in the text. Slide the positions, then press “move both by 5”:

distance m − n5
score q·k after rotating0.754
score for every distance from 0 to 40 (same query and key)
The query at position 12 is rotated by 12 × θ in each pair, the key at position 7 by 7 × θ. A dot product only sees the angle between the two arrows in each dial, which is (12 − 7) × θ. So the score, 0.754, depends only on the distance 5: press “move both by 5” and it doesn’t change. Fast pairs (left) tell nearby positions apart; slow pairs (right) change little and carry meaning across long distances.
An 8-number query and key, split into 4 pairs. Each pair is drawn as a pair of arrows on its own dial: query solid, key dashed. Each pair turns at its own speed as the position grows.

The widget uses 4 pairs and a base of 100 so that every dial moves visibly. A real Llama head has 64 pairs (head size 128), with base 10,000 in Llama 2 and 500,000 in Llama 3, so its slowest pairs turn a full circle only over tens of thousands of positions or more. See RoPE for how it fits into the model and how context is extended by changing the base.

example · the hands of a clock

A clock shows the time with hands that rotate at different speeds: the second hand fast, the minute hand slower, the hour hand slowest. The angle between two clocks’ second hands tells you the difference in seconds, but repeats every minute; the hour hands tell you the difference in hours. RoPE’s pairs are hands turning at different speeds: the fast ones tell neighbouring tokens apart, the slow ones keep track of long distances.

Rotations against outliers

Hidden states in large models have a few huge numbers (outliers) in fixed dimensions. That is bad for quantization: a shared scale must cover the outlier, so all the small numbers round to zero. Rotating the vector first keeps its length but spreads the outlier over every coordinate. A Hadamard matrix, all ±1\pm 1 divided by n\sqrt{n}, does this cheaply:

H (×1/√8)
111111111−11−11−11−111−1−111−1−11−1−111−1−111111−1−1−1−11−11−1−11−1111−1−1−1−1111−1−11−111−1
length6.06
largest |value|6.00
largest ÷ RMS2.80
One number is 20 times bigger than the rest. If you quantize this vector to a few bits with one shared scale, the step size has to cover 6.0 and the small numbers all round to 0. Switch to H x.
A hidden state with one outlier (dimension 4). Multiply by the 8 × 8 Hadamard matrix (±1 entries, divided by √8): an orthogonal matrix, so the length is kept.

Since Q⊤Q=IQ^\top Q = I, the model can be rewritten so the rotation costs almost nothing: xW=(xQ)(Q⊤W)\mathbf{x}W = (\mathbf{x}Q)(Q^\top W), and Q⊤WQ^\top W is computed once, ahead of time. QuaRot uses Hadamard rotations this way to run Llama models with 4-bit weights, activations and KV cache; SpinQuant learns the rotations instead. See rotations in quantization.

going deeper

Facts

  • QQ is orthogonal when Q⊤Q=QQ⊤=IQ^\top Q = QQ^\top = I. Its rows, and its columns, are unit vectors at right angles to each other (orthonormal).
  • det⁡Q=±1\det Q = \pm 1. Products and transposes of orthogonal matrices are orthogonal.
  • Preserving dot products: (Qa)⊤(Qb)=a⊤Q⊤Q b=a⊤b(Q\mathbf{a})^\top(Q\mathbf{b}) = \mathbf{a}^\top Q^\top Q\,\mathbf{b} = \mathbf{a}^\top\mathbf{b}.
  • R(α)R(β)=R(α+β)R(\alpha)R(\beta) = R(\alpha + \beta) and R(θ)⊤=R(−θ)R(\theta)^\top = R(-\theta). This is why Rmθ⊤Rnθ=R(n−m)θR_{m\theta}^\top R_{n\theta} = R_{(n-m)\theta} in RoPE.
  • The SVD from the low-rank lesson, M=UΣV⊤M = U\Sigma V^\top, says every matrix is a rotation (or reflection), then a stretch along the axes, then another rotation.

In code

import numpy as np
def R(theta):
    c, s = np.cos(theta), np.sin(theta)
    return np.array([[c, -s], [s, c]])
 
Q = R(0.7)
np.allclose(Q.T @ Q, np.eye(2))          # True
a, b = np.array([2.2, 0.6]), np.array([0.6, 1.8])
np.isclose((Q @ a) @ (Q @ b), a @ b)     # True
 
# RoPE on a (T, d) matrix of queries, pairs (0,1), (2,3), ...
def rope(x, base=10000.0):
    T, d = x.shape
    theta = base ** (-np.arange(0, d, 2) / d)          # (d/2,)
    ang = np.arange(T)[:, None] * theta[None, :]       # (T, d/2)
    c, s = np.cos(ang), np.sin(ang)
    x1, x2 = x[:, 0::2], x[:, 1::2]
    out = np.empty_like(x)
    out[:, 0::2] = x1 * c - x2 * s
    out[:, 1::2] = x1 * s + x2 * c
    return out
 
from scipy.linalg import hadamard
H = hadamard(8) / np.sqrt(8)
np.allclose(H @ H.T, np.eye(8))          # True

Many implementations (Llama’s Hugging Face code among them) pair dimension ii with i+d/2i + d/2 instead of with its neighbour. That is the same rotation applied to a reordered vector.

In papers

Related

  • transformations - A matrix is also a machine that moves every point in space. Its columns say where the axis arrows land, and everything else follows, with straight lines staying straight. Scaling, rotating, shearing, reflecting and flattening are all matrices, and doing one after another is multiplying them.
  • dot-product - Multiply two vectors position by position and add the results. The single number you get is large and positive when the vectors point the same way, zero when they are at right angles, and negative when they point apart. It is how a language model measures similarity, and every attention score is one.
  • norms - A norm measures how long a vector is. The usual one is the straight-line length; another adds up the sizes of the numbers. Dividing a vector by its length keeps its direction and makes its length 1. Language models do this to every hidden state, many times per token, with LayerNorm or RMSNorm.
  • low-rank - A column times a row gives a whole matrix with a single pattern in it, a rank-1 matrix. Any matrix is a sum of such pieces, and the singular value decomposition orders them by importance. Keep only a few and you get a small, close approximation. LoRA fine-tunes language models this way.