ml-models / Language models / Positional encoding core

Positional encoding

Attention compares every token with every other token, but on its own it has no idea which came first. This page shows why, then builds the three main fixes from their formulas: the original sinusoidal encoding, learned position tables, and rotary embeddings (RoPE), which most open LLMs use today, plus ALiBi, which skips embeddings entirely and biases the attention scores instead. Every number below is computed, and every claim about PyTorch links the lines that back it.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. Why attention ignores order
  5. Sinusoidal encoding
  6. Learned absolute positions
  7. Rotary position embedding (RoPE)
  8. ALiBi: a bias, not an embedding
  9. Worked example
  10. Implementation
  11. Tradeoffs
  12. Connections
  13. Read first
  14. Read next
  15. References

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.

Sinusoidal encoding: one sine wave per frequency pair Four sine curves over positions 0 to 40, for a 64-dimensional encoding. Pair 0 has frequency 1 and oscillates fastest, with a period of about 6.3 positions. Pairs 2, 4 and 8 have frequencies 0.562, 0.316, 0.100, so they oscillate progressively slower. Reading the value of every curve at a position gives that position's encoding, and nearby positions give similar readings. pair 0 ω = 1.000 pair 2 ω = 0.562 pair 4 ω = 0.316 pair 8 ω = 0.100 0 10 20 30 40 pos

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.

RoPE rotates a query and a key; the angle between them depends only on their offset Two panels show the first dimension pair of the worked example's query and key, with rotation speed θ = 1 radian per position. Left: the query at position 2 and the key at position 5. Right: the same query at position 10 and key at position 13. The vectors point in different absolute directions in the two panels, but the angle between query and key is the same, because both pairs are 3 positions apart. q@2 k@5 m = 2, n = 5: score −0.726 q@10 k@13 m = 10, n = 13: score −0.726

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.

ComponentShape/ParamsMeaningSource
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.

possin ω0cos ω0sin ω1cos ω1sin ω2cos ω2sin ω3cos ω3
00.0001.0000.0001.0000.0001.0000.0001.000
10.8410.5400.1000.9950.0101.0000.0011.000
20.909−0.4160.1990.9800.0201.0000.0021.000
5−0.9590.2840.4790.8780.0500.9990.0051.000
200.9130.4080.909−0.4160.1990.9800.0201.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:

mnRm qRn kRoPE scoreScore with added sinusoids
03(1.000, 0.500, −0.500, 1.000)(−0.636, −0.919, 0.808, 0.773)−0.72642.9190
25(−0.871, 0.701, −0.689, 0.881)(1.101, −0.196, 0.638, 0.918)−0.72641.5133
1013(−0.567, −0.964, −1.112, 0.120)(0.034, 1.118, −0.214, 1.097)−0.72641.6255
4043(−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:

HeadSlopekey 0key 1key 2key 3key 4key 5
no bias—0.2100.1040.1550.2320.1270.172
10.5000.0430.0350.0870.2130.1930.429
30.1250.1500.0850.1430.2420.1500.230
80.003910.2080.1040.1550.2320.1280.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.

ALiBi bias matrix for head 1, slope 0.5 A 6 by 6 grid of the bias added to each query–key score. Rows are query positions and columns key positions. The diagonal is 0; each step to the left subtracts another 0.5, down to −2.5 in the bottom-left corner. The upper triangle is masked out by causality. Darker cells mean a larger penalty. q0 0.0 masked masked masked masked masked q1 −0.5 0.0 masked masked masked masked q2 −1.0 −0.5 0.0 masked masked masked q3 −1.5 −1.0 −0.5 0.0 masked masked q4 −2.0 −1.5 −1.0 −0.5 0.0 masked q5 −2.5 −2.0 −1.5 −1.0 −0.5 0.0 k0 k1 k2 k3 k4 k5 bias = −slope × (i − j), added to the score before softmax; no position vectors anywhere

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

MethodWhere it actsParametersRelative by constructionLonger than training
Sinusoidaladded to the input once0only PE·PE; content cross terms are absolutedefined, but quality drops (Press et al.)
Learned absoluteadded to the input oncemax_len × dnoundefined past max_len
RoPEq and k in every layer0yes, q·k depends on n − mdegrades without rescaling; position interpolation and YaRN extend it
ALiBiattention scores in every layer0 (fixed slopes)yesthe paper's design goal
Learned relative bias (Shaw et al., T5)attention scoresper bucket and headyesfar 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

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.