Problem
"The dog bit the man" and "the man bit the dog" contain the same words. A model that cannot tell them apart is useless for language, yet that is exactly what plain attention is. Each token's output is a weighted average of value vectors, with weights from query–key dot products. Shuffle the input tokens and every output is shuffled the same way, with identical values: attention is permutation-equivariant. The embedding table does not help either, because it gives "dog" the same vector wherever it appears.
So order has to be injected. The question is how: add a position signal to each token's vector, learn one, or change the attention scores themselves so they depend on how far apart two tokens are. The choice decides whether a model can read sequences longer than it was trained on, how its KV cache works, and how many parameters position costs.
Intuition
Think of a wall of clocks, each hand turning at a different speed. The fastest hand goes round every few steps, the slowest takes thousands. Read all the hands at once and you know exactly what time it is, and two nearby moments have similar readings. The sinusoidal encoding is that wall of clocks: each pair of dimensions is one hand, a sine and a cosine at its own speed, and the reading is added to the token's embedding.
A learned table is the bluntest fix: give each slot 0, 1, 2, … its own trainable vector, like seat numbers in a theatre. It works, but there are no seats past the last row, so it cannot handle longer inputs.
Rotary embeddings use the clocks differently. Instead of adding a reading, they turn the query and key vectors by an angle proportional to their position. When a query at position m meets a key at position n, both have been turned, and the dot product only sees the difference between the two turns: n − m. Attention then naturally asks "how far back is this word?" rather than "which absolute slot is it in?". ALiBi asks the same question more bluntly: it subtracts a penalty proportional to distance from each attention score, so far-away tokens get less weight.
Each pair of dimensions is a clock hand at its own speed; the sine is shown, and the cosine partner is the same curve shifted a quarter turn.
Mechanics
Why attention ignores order
Let X be the n × d matrix of token vectors and P any n × n permutation matrix. Self-attention computes softmax(XWQ(XWK)ᵀ/√dk) XWV. Replacing X by PX permutes the rows and columns of the score matrix together, and softmax works row by row, so
Attention(PX) = P · Attention(X)
The outputs move with the inputs and nothing else changes. A causal mask breaks the symmetry partly, since a token can count how many tokens it may see, and Haviv et al. found causal language models without any position signal still learn some positional information that way. Every mainstream architecture adds an explicit signal anyway.
Sinusoidal encoding
Vaswani et al. add a fixed vector to each token embedding (which they first scale by √d):
PE(pos, 2i) = sin(pos · ωi) PE(pos, 2i+1) = cos(pos · ωi) ωi = 10000−2i/d
The wavelengths 2π/ωi form a geometric progression from 2π up to nearly 10000 · 2π. The paper chose sinusoids over a learned table, which it found performed about the same, hoping for two properties. The first is exact: for any fixed offset k, each (sin, cos) pair at pos + k is the pair at pos rotated by the angle k·ωi, by the angle-addition identities:
[ sin((pos+k)ω) , cos((pos+k)ω) ] = [ [cos kω, sin kω], [−sin kω, cos kω] ] · [ sin(pos·ω) , cos(pos·ω) ]
So PE(pos + k) is a linear function of PE(pos) that depends only on k, which the paper hoped would let attention learn relative offsets. A consequence is that PE(p) · PE(q) = Σi cos(ωi(p − q)) depends only on the distance between the positions. The second hope, extrapolating to longer sequences than seen in training, did not hold up well: Press et al. measured sinusoidal models degrading quickly past their training length. Because the signal is added to the token vector, query–key products mix content and position terms, and the content terms see absolute position.
Learned absolute positions
BERT and GPT-2 instead learn a second embedding table with one row per position, nn.Embedding(max_positions, d), and add row t to the token at position t. It costs max_positions × d parameters, 786,432 for GPT-2 small's 1,024 × 768 table,
and positions past max_positions simply do not exist. Its rows are free to learn whatever the data rewards, which is why it matched sinusoids in the original paper.
Rotary position embedding (RoPE)
Su et al. leave the token vectors alone and rotate the queries and keys inside every attention layer. Split a head's dk dimensions into dk/2 pairs and rotate pair i of the vector at position m by the angle m·θi, with θi = base−2i/dk (base 10000 in the paper):
RoPE(x, m)2i, 2i+1 = [ [cos mθi, −sin mθi], [sin mθi, cos mθi] ] · [ x2i , x2i+1 ]
Write Rm for that block-diagonal rotation. Rotations are orthogonal, so Rmᵀ Rn = Rn−m, and the attention score becomes
(Rm q)ᵀ (Rn k) = qᵀ Rmᵀ Rn k = qᵀ Rn−m k
The score depends on the content of q and k and on the offset n − m only, never on m or n alone. Rotation preserves length, so RoPE changes no vector's norm, and it adds no parameters. Values are not rotated, so the output mixes content only. Because each key is rotated by its own position once, a KV cache stores keys already rotated and never touches them again. Some implementations pair dimension i with dimension i + dk/2 instead of 2i with 2i + 1; that is the same method on a fixed permutation of the dimensions, but weights trained with one layout need the same layout at inference.
Shifting both positions by the same amount spins the whole picture; the angle between q and k, and so their dot product, stays put.
ALiBi: a bias, not an embedding
Press, Smith and Lewis add no position vector at all. Each head h gets a fixed slope sh, and before the softmax the score of query i against key j ≤ i is lowered in proportion to the distance:
scoreij = qi · kj / √dk − sh · (i − j)
The slopes are not learned. For 8 heads they are the geometric sequence 1/2, 1/4, …, 1/256; in general, for a power-of-two head count H, the sequence starts at 2−8/H and has that same ratio. Steep heads look locally; gentle heads see far back.
The paper's headline result is training on short sequences and evaluating on longer ones with little loss, since a longer distance just means a larger penalty. In PyTorch it is a float attn_mask for scaled_dot_product_attention, which adds it to the scores, or a score_mod in FlexAttention.
| Component | Shape/Params | Meaning | Source |
|---|---|---|---|
nn.Embedding(max_positions, d) |
weight (max_positions, d), initialized N(0, 1) | A learned absolute position table; index it with torch.arange(T) and add to the token embeddings. |
sparse.py 134-177, 179-181 |
Embedding.forward → F.embedding |
indices (*) → (*, d) | A row lookup; positions past the table raise an index error rather than extrapolating. | sparse.py 188-197, functional.py 2509-2621 |
F.scaled_dot_product_attention |
q (N, …, Hq, L, E), k (N, …, H, S, E); attn_mask broadcastable to (N, …, Hq, L, S) | A float mask is added to the scaled scores before softmax: the hook for ALiBi or any relative bias. RoPE is applied to q and k before the call. | functional.py 6423-6450, 6537-6540 |
flex_attention(…, score_mod) |
score_mod(score, batch, head, q_idx, k_idx) → score | Per-score callback with the query and key indices, so a position bias can be computed on the fly instead of materialized as an L × S mask. | flex_attention.py 2411-2428, 138 |
nn.Transformer |
d_model 512, nhead 8 by default | A reference encoder–decoder that takes already-embedded inputs: it contains no embedding table and no positional encoding. | transformer.py 58-317 |
| Sinusoidal PE, RoPE | (max_len, d) buffer; (T, dk/2) angles, no parameters | Not in PyTorch's nn; built from tensor ops as in the snippet below. |
Papers: Vaswani et al.; Su et al. |
RoPE rotates queries and keys. Why not the values too?
The relative-offset property comes from the dot product q·k, where the two rotations cancel down to Rn−m. Values never enter a dot product with each other; rotating them would just stamp absolute position onto the output vectors, which is what RoPE was designed to avoid.
Worked example
Sinusoidal encodings, d = 8. The four frequencies are ω = 1.000, 0.100, 0.010, 0.001, wavelengths 6.3, 62.8, 628.3, 6,283.2 positions.
| pos | sin ω0 | cos ω0 | sin ω1 | cos ω1 | sin ω2 | cos ω2 | sin ω3 | cos ω3 |
|---|---|---|---|---|---|---|---|---|
| 0 | 0.000 | 1.000 | 0.000 | 1.000 | 0.000 | 1.000 | 0.000 | 1.000 |
| 1 | 0.841 | 0.540 | 0.100 | 0.995 | 0.010 | 1.000 | 0.001 | 1.000 |
| 2 | 0.909 | −0.416 | 0.199 | 0.980 | 0.020 | 1.000 | 0.002 | 1.000 |
| 5 | −0.959 | 0.284 | 0.479 | 0.878 | 0.050 | 0.999 | 0.005 | 1.000 |
| 20 | 0.913 | 0.408 | 0.909 | −0.416 | 0.199 | 0.980 | 0.020 | 1.000 |
Only the first pair changes much between neighbours; the last pair barely moves until positions in the thousands. Rotating each pair of PE(2) by −3·ωi gives (−0.959, 0.284, 0.479, 0.878, 0.050, 0.999, 0.005, 1.000), exactly PE(5) in the table. The dot product PE(0) · PE(k), which equals PE(p) · PE(p + k) for every p, falls from 4.000 as the offset grows, though not monotonically: k = 0: 4.000; k = 1: 3.535; k = 2: 2.564; k = 4: 2.267; k = 8: 2.548; k = 16: 1.000.
RoPE on one head, dk = 4. To keep the angles visible, use base 100, so θ = 1.0, 0.1 radians per position. Take q = (1.0, 0.5, −0.5, 1.0) and k = (0.5, 1.0, 1.0, 0.5), whose plain dot product is 1.000. Place the pair 3 positions apart at different absolute positions:
| m | n | Rm q | Rn k | RoPE score | Score with added sinusoids |
|---|---|---|---|---|---|
| 0 | 3 | (1.000, 0.500, −0.500, 1.000) | (−0.636, −0.919, 0.808, 0.773) | −0.7264 | 2.9190 |
| 2 | 5 | (−0.871, 0.701, −0.689, 0.881) | (1.101, −0.196, 0.638, 0.918) | −0.7264 | 1.5133 |
| 10 | 13 | (−0.567, −0.964, −1.112, 0.120) | (0.034, 1.118, −0.214, 1.097) | −0.7264 | 1.6255 |
| 40 | 43 | (−1.039, 0.412, 1.084, −0.275) | (1.109, 0.139, 0.057, −1.117) | −0.7264 | −0.9096 |
The rotated vectors are different on every row, but the RoPE score is −0.7264 every time. Adding a sinusoidal encoding to q and k instead (same frequencies) gives a different score on every row, because the cross terms between content and position see absolute positions. Holding m = 0 and moving the key, the RoPE score as a function of the offset is 0: 1.000; 1: 0.034; 2: −0.850; 4: 0.401; 8: 0.009; 16: 0.508. At offset 0 it is the plain dot product.
ALiBi with 8 heads. The slopes are 0.500, 0.250, 0.125, 0.0625, 0.0312, 0.0156, 0.00781, 0.00391. A query at the last of 6 positions has raw scores (0.8, 0.1, 0.5, 0.9, 0.3, 0.6) against keys 0 to 5. After the bias and softmax:
| Head | Slope | key 0 | key 1 | key 2 | key 3 | key 4 | key 5 |
|---|---|---|---|---|---|---|---|
| no bias | — | 0.210 | 0.104 | 0.155 | 0.232 | 0.127 | 0.172 |
| 1 | 0.500 | 0.043 | 0.035 | 0.087 | 0.213 | 0.193 | 0.429 |
| 3 | 0.125 | 0.150 | 0.085 | 0.143 | 0.242 | 0.150 | 0.230 |
| 8 | 0.00391 | 0.208 | 0.104 | 0.155 | 0.232 | 0.128 | 0.173 |
Head 1 puts 0.429 of its weight on the current token and only 0.043 on the oldest one, even though key 0's raw score is second highest. Head 8's slope is so gentle that its weights stay within 0.01 of the unbiased ones.
The same matrix shape serves every head; only the slope changes, from 1/2 for head 1 down to 1/256 for head 8.
Implementation
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
def sinusoidal_table(max_len, d, base=10000.0): # (max_len, d), no parameters
pos = torch.arange(max_len).float()[:, None]
freq = base ** (-torch.arange(0, d, 2).float() / d) # (d/2,)
pe = torch.zeros(max_len, d)
pe[:, 0::2] = torch.sin(pos * freq)
pe[:, 1::2] = torch.cos(pos * freq)
return pe
learned = nn.Embedding(1024, 768) # GPT-2-style table: x = tok + learned(arange(T))
def rope_angles(seq_len, head_dim, base=10000.0):
freq = base ** (-torch.arange(0, head_dim, 2).float() / head_dim)
angles = torch.outer(torch.arange(seq_len).float(), freq) # (T, head_dim/2)
return angles.cos(), angles.sin()
def apply_rope(x, cos, sin): # x: (B, H, T, head_dim)
x1, x2 = x[..., 0::2], x[..., 1::2] # the (2i, 2i+1) pairs
out = torch.stack((x1 * cos - x2 * sin, x1 * sin + x2 * cos), dim=-1)
return out.flatten(-2) # back to (B, H, T, head_dim)
def alibi_bias(n_heads, T): # (H, T, T), for a power-of-two n_heads
slopes = torch.tensor([2 ** (-8 * (h + 1) / n_heads) for h in range(n_heads)])
i = torch.arange(T)[:, None]
j = torch.arange(T)[None, :]
bias = -slopes[:, None, None] * (i - j)
return bias.masked_fill(j > i, float("-inf")) # causal mask folded in
B, H, T, Dh = 2, 8, 16, 64
q, k, v = (torch.randn(B, H, T, Dh) for _ in range(3))
cos, sin = rope_angles(T, Dh)
out_rope = F.scaled_dot_product_attention(apply_rope(q, cos, sin), apply_rope(k, cos, sin), v, is_causal=True)
out_alibi = F.scaled_dot_product_attention(q, k, v, attn_mask=alibi_bias(H, T)) # float mask is added
apply_rope rotates the pairs exactly as the formula above; with a KV cache, slice cos and sin at the new token's position and rotate only the new query and key.
The ALiBi mask is a float tensor, so scaled_dot_product_attention adds it to the scores (its reference implementation, linked in the table, does attn_bias = attn_mask + attn_bias); a boolean mask would instead mean keep-or-drop.
For long sequences, a FlexAttention score_mod such as lambda score, b, h, q_idx, kv_idx: score - slope[h] * (q_idx - kv_idx) computes the same bias without materializing an L × S tensor.
Tradeoffs
| Method | Where it acts | Parameters | Relative by construction | Longer than training |
|---|---|---|---|---|
| Sinusoidal | added to the input once | 0 | only PE·PE; content cross terms are absolute | defined, but quality drops (Press et al.) |
| Learned absolute | added to the input once | max_len × d | no | undefined past max_len |
| RoPE | q and k in every layer | 0 | yes, q·k depends on n − m | degrades without rescaling; position interpolation and YaRN extend it |
| ALiBi | attention scores in every layer | 0 (fixed slopes) | yes | the paper's design goal |
| Learned relative bias (Shaw et al., T5) | attention scores | per bucket and head | yes | far offsets share a bucket |
- Default choice today. RoPE dominates open decoder LLMs (the Llama 3 report is one example) because it is relative, parameter-free, cache-friendly and works with fused attention kernels, since it only touches q and k before the kernel.
- Extending RoPE's context. Past the trained length, the low-frequency pairs reach angles never seen in training. Position interpolation (Chen et al.) scales positions down so they stay inside the trained range, then fine-tunes briefly; YaRN (Peng et al.) rescales frequency bands differently. Raising the base, as some long-context models do, slows every rotation.
- ALiBi's bias toward recency is built in: every head penalizes distance, so it suits tasks where nearby context matters most, and it materializes or computes a bias per score, which some fused kernels support only through a mask.
- Learned tables are simple and fine for fixed-length inputs (image patches, a fixed context window), but cost parameters and cannot generalize past their last row.
- Common bugs. Mixing the interleaved and split-half RoPE layouts between training and inference; applying RoPE to values; forgetting to offset positions by the cache length during generation; adding positions after, rather than before, the first layer.
A model was trained with a learned position table of 2,048 rows. What happens at token 2,049, and what would happen with ALiBi?
With the table, the lookup fails: there is no row 2,048 (counting from 0), so you must truncate or extend the table and fine-tune. With ALiBi there is nothing to look up; the bias for distance 2,048 is just −slope × 2,048, a larger penalty of the same form the model already uses.
Connections
Read first
- Embeddings intro
- Attention core
Read next
No page builds on this one yet.
The Transformer page shows where the encoding enters the original model, the decoder-only LLM page counts GPT-2's learned position table among its parameters, and the KV cache page explains why RoPE'd keys are stored after rotation.
References
- Vaswani et al. Attention Is All You Need. NeurIPS 2017. arXiv:1706.03762
- Su, Lu, Pan, Murtadha, Wen and Liu. RoFormer: Enhanced Transformer with Rotary Position Embedding. 2021. arXiv:2104.09864
- Press, Smith and Lewis. Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation. ICLR 2022. arXiv:2108.12409
- Shaw, Uszkoreit and Vaswani. Self-Attention with Relative Position Representations. NAACL 2018. arXiv:1803.02155
- Raffel et al. Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer. JMLR 2020. arXiv:1910.10683
- Devlin, Chang, Lee and Toutanova. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. NAACL 2019. arXiv:1810.04805
- Haviv, Ram, Press, Izsak and Levy. Transformer Language Models without Positional Encodings Still Learn Positional Information. Findings of EMNLP 2022. arXiv:2203.16634
- Chen, Wong, Chen and Tian. Extending Context Window of Large Language Models via Positional Interpolation. 2023. arXiv:2306.15595
- Peng, Quesnelle, Fan and Shippole. YaRN: Efficient Context Window Extension of Large Language Models. ICLR 2024. arXiv:2309.00071
- Llama Team. The Llama 3 Herd of Models. 2024. arXiv:2407.21783
- PyTorch v2.15.0:
torch/nn/modules/sparse.py,scaled_dot_product_attention,flex_attention,torch/nn/modules/transformer.py.