ZeroShotMind

Blog Post

Positional Encodings: From Sinusoids to RoPE

Attention is permutation-invariant. Positional encodings break that symmetry. The choice of encoding method determines whether your model can generalize to longer sequences than it trained on.

Views: 10 min read

Self-attention is a set operation. Strip away everything else and what an attention layer computes is a weighted sum over value vectors, where the weights come from dot products between queries and keys — and a dot product does not care where its operands sat in the sequence. Feed the model "The cat sat" and feed it "sat cat The," and if the token embeddings are the same, the attention output is the same.

Attn(Q,K,V)=softmax ⁣(QKdk)V\text{Attn}(Q, K, V) = \text{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right) V

Permute the rows of QQ, KK, and VV identically and every entry of that softmax matrix permutes with them, so the output is just a permutation of the unpermuted result — order carries no information on its own. A language model that cannot tell "dog bites man" from "man bites dog" is useless, so something has to inject position before the dot products are taken. That something is the positional encoding, and the forty years of choices behind it are the difference between a model that breaks at its training length and one that reads a book it has never seen.

Why not just use integers or binary?

Before arriving at sinusoids, it helps to see what simpler schemes fail and why.

Integer embeddings are the obvious first idea: assign each position its integer value, so position 0 gets embedding 0, position 1 gets embedding 1, and so on. The problem is that norms explode as sequences grow longer. Position 1000 has a norm 1000× larger than position 1, and those enormous magnitudes create gradient instability during training — the signal from late positions floods the signal from early ones. Numbers grow without bound, and the model has no way to compare "500 away" with "501 away" on the same scale it used for "2 away" and "3 away."

Binary embeddings fix the norm explosion by encoding each position as a fixed-length binary number. Position 5 becomes 0101, position 6 becomes 0110, position 13 becomes 1101 — every embedding has the same number of bits, so norms stay constant. But binary encodings are too jumpy. Adjacent positions like 7 (0111) and 8 (1000) share no bits at all: every single dimension flips at once. The model cannot learn that nearby positions are similar, because the encoding gives no smooth signal that "these two tokens are close together." Continuity is completely absent.

Sinusoids solve both problems simultaneously. Each dimension is bounded in [1,1][-1, 1], so norms are controlled regardless of sequence length. And adjacent positions differ by a continuous angular step — position pp and position p+1p+1 are related by a smooth rotation in each dimension, not a sudden binary flip. That smoothness is precisely what lets the model learn the geometry of proximity: tokens close together produce similar positional vectors, tokens far apart produce dissimilar ones, and the signal degrades gracefully as distance grows.

Integer → Binary → Sinusoidal: What Fails and Why

Positions 0–15. Each row = one position. See why integers explode, binary jumps, and sinusoids smooth.

Integer

value (→ norm)00112233445566778899101011111212131314141515

Norm grows unboundedly with position

Binary (4-bit)

32100123456789101112131415←jump

Adjacent positions share no structure (7=0111 vs 8=1000)

Sinusoidal (8 dims)

012345670123456789101112131415
-1 → +1

Bounded norms + smooth transitions ✓

The orange dashed box highlights positions 7 and 8 in the binary panel — every bit flips at once. In the sinusoidal panel the same transition is a smooth rotation in each dimension.

Absolute positions: sinusoids and lookup tables

The original transformer added a fixed sinusoidal signal to each token embedding before the first layer. Each dimension of the encoding is a sinusoid whose wavelength grows geometrically with the dimension index, so position pospos maps to a vector of alternating sines and cosines.

PE(pos,2i)=sin ⁣(pos100002i/d),PE(pos,2i+1)=cos ⁣(pos100002i/d)PE(pos, 2i) = \sin\!\left(\frac{pos}{10000^{2i/d}}\right), \qquad PE(pos, 2i+1) = \cos\!\left(\frac{pos}{10000^{2i/d}}\right)

Low dimensions oscillate fast and high dimensions oscillate slowly, so the full vector is a kind of binary-clock fingerprint that is unique for every position and smooth between neighbors. The signal is not learned, which means in principle it is defined for any position — including positions longer than anything seen in training — but in practice extrapolation degrades quickly, because the model learns to read these features at the scales it actually saw and has no calibration for the rest.

Sinusoidal Positional Encoding Heatmap

Each row = one position (0–49). Each column = one dimension. Low-index dims oscillate fast; high-index dims vary slowly across positions. Blue = −1, white = 0, red = +1.

−1 → 0 → +1Showing first 64 dims (out of d_model = 64)

The lazier alternative is to skip the math and learn a position embedding table indexed by slot, exactly as you would learn a token embedding. BERT does this, and it works fine inside the training window. The cost is that the table has a fixed number of rows: a model trained at 512 positions has no embedding for position 513, so it does not extrapolate at all — it simply has nothing to look up.

Relative positions: bias the logits, not the embeddings

Both absolute schemes share a conceptual flaw. What a language model usually needs is not "this token is at absolute index 274" but "this token is three words to the left of that one." Shaw et al. acted on that directly by injecting a learned relative-position term into the attention logit itself, so the score between positions ii and jj depends on a vector keyed by their distance iji-j.

aij=(xiWQ)(xjWK+rij)dka_{ij} = \frac{(x_i W_Q)\,(x_j W_K + r_{i-j})^\top}{\sqrt{d_k}}

The bias rijr_{i-j} is indexed only by the gap between the two tokens, never by where either sits in the sequence, so the same learned pattern of "look three back" applies whether the pair is at the start of a paragraph or a thousand tokens in. ALiBi strips this idea to its bones: it adds no learned vector at all, just a linear penalty on distance.

aij=qikjdkmija_{ij} = \frac{q_i k_j^\top}{\sqrt{d_k}} - m \cdot |i - j|

The slope mm is fixed per head — some heads get a steep penalty and attend locally, others get a shallow one and see far — and because the penalty is a closed-form function of distance, ALiBi extrapolates to longer sequences essentially for free, which was its original selling point.

RoPE: rotation as relative position

The encoding that modern LLMs converged on takes the relative-position idea and hides it inside a rotation. Rotary position embedding leaves the attention formula untouched and instead rotates each query and key vector by an angle proportional to its position, pairing up dimensions (2i,2i+1)(2i, 2i+1) and spinning each pair by pθip\theta_i where the per-pair frequency is θi=100002i/d\theta_i = 10000^{-2i/d}.

Additive positional encodings — the sinusoidal vectors added to token embeddings before the first layer — mix position and semantic content at the vector level. Once you add PE(p)PE(p) to the embedding e(token)e(\text{token}), the resulting vector carries both meanings entangled: the same semantic content appears at a different angle depending on which position it occupies, and gradient flow through the attention logit carries gradients back simultaneously to token content and position. The two signals are fused and cannot be disentangled downstream.

RoPE takes a multiplicative path instead. Rather than adding anything to the query or key vectors, it rotates them — and rotation is an isometry, meaning it preserves norms exactly. The magnitude of qq and kk never changes; only their orientation does. This keeps semantic content (what the token means, encoded in the vector's length and coarse direction) strictly separated from positional content (where the token sits, encoded purely in the rotation angle). There are no "positional bits" added into the magnitude, no quiet distortion of the softmax temperature, and no entanglement of content and position gradients. Rotation changes the angle; it leaves the length alone.

q~p=R(pθ)q,k~k=R(kθ)k\tilde{q}_p = R(p\theta)\, q, \qquad \tilde{k}_k = R(k\theta)\, k

RoPE: Key Rotates by Relative Position

RoPE rotates the key vector by Δp·θ. The query stays fixed; the relative rotation encodes the distance between query position and key position.

k₀R(0.00)kq

Rotation angle = Δp·θ

0.000 rad

q · R(Δp·θ)k (attention score)

0.7070

Score only depends on relative position Δp, not absolute positions — that's the key RoPE property.

The reason this is exactly relative encoding, despite looking absolute, is a property of rotation matrices: composing two rotations subtracts their angles inside a dot product, so the rotated query at position pp dotted with the rotated key at position kk depends only on the gap.

(R(pθ)q)(R(kθ)k)=qR((kp)θ)k(R(p\theta)\, q) \cdot (R(k\theta)\, k) = q^\top R\big((k - p)\theta\big)\, k

The absolute positions pp and kk cancel and only kpk - p survives, so RoPE delivers the relative-position behavior of Shaw or ALiBi without adding a single term to the logit — it is a transform applied to qq and kk before attention runs, and attention itself is none the wiser.

Think of the dimension pairs as digits on a clock, ordered from fastest to slowest. The lowest-index pair (dimensions 0, 1) has frequency θ0=100000=1\theta_0 = 10000^0 = 1 — it rotates by a full radian per position step, completing many full cycles across a short context window. The highest-index pair (dimensions d2d{-}2, d1d{-}1) has frequency θd/21=100001=0.0001\theta_{d/2-1} = 10000^{-1} = 0.0001 — it barely moves across tens of thousands of positions. Fast-rotating dimensions are like seconds on a clock: they change with every token step and distinguish nearby positions within a local window. Slow-rotating dimensions are like hours: they barely move locally but accumulate across long ranges and let the model tell "beginning of document" from "end of document." When you extend context with NTK or YaRN, you leave the fast dimensions alone and scale only the slow ones — exactly like adjusting the hour hand without touching the second hand.

That clean separation is why RoPE composes with grouped-query attention and the rest of the inference stack without special cases, and it is why LLaMA, Mistral, Qwen, Gemini, and DeepSeek all use it.

Attention Score Decay with Distance

01020304050relative distance |i−j|

Sinusoidal PE adds no explicit distance bias — all positions enter attention equally. ALiBi adds a linear penalty that grows with distance, making closer tokens always preferred. RoPE creates an implicit cosine decay through rotation — fast oscillation at frequency θ.

Implementation: shapes and broadcasting

In practice, RoPE is applied as follows: for each attention head, the head dimension dheadd_\text{head} is split into pairs — dhead/2d_\text{head}/2 rotation matrices of size 2×22 \times 2, one per dimension pair. A single rotation matrix tensor of shape [seq_len,dhead/2,2,2][\text{seq\_len},\, d_\text{head}/2,\, 2,\, 2] is precomputed from the position indices and frequencies, then broadcast across [batch,heads,seq_len,dhead/2,2,2][\text{batch},\, \text{heads},\, \text{seq\_len},\, d_\text{head}/2,\, 2,\, 2] — the same rotation applies to every attention head and every item in the batch. This broadcast is cheap: the rotation matrix is computed once per forward pass and shared across all heads and batch elements, adding essentially no memory overhead beyond the precomputed angles.

Stretching the window after training

The payoff that made RoPE indispensable is what it lets you do at inference. Because position enters only through a rotation angle, you can rescale the angle to pretend a longer sequence is shorter — position interpolation divides every position index by the ratio of target length to training length before rotating.

p=pLtrainLtargetp' = p \cdot \frac{L_\text{train}}{L_\text{target}}

A model trained at 4096 positions and asked to run at 32k has its position indices compressed back into the [0,4096)[0, 4096) range the rotations were tuned for, so the angles stay in distribution and the model degrades gracefully instead of falling off a cliff. Naive interpolation flattens the high-frequency dimensions that encode local order, so the production recipes — NTK-aware scaling, and YaRN — interpolate the low frequencies while leaving the high ones nearly untouched, extending context with a few hundred steps of fine-tuning rather than a full retrain. This is how LLaMA-2's 4k window stretched past 32k without anyone training a 32k model from scratch.

Positional encoding is the layer that decides what your model knows about order, and whether that knowledge survives past the longest sequence it ever saw. The next post moves inside the attention heads themselves — MHA, MQA, GQA — where the binding constraint turns out not to be order but memory bandwidth.