"Dog bites man" and "Man bites dog" β same words, completely different meanings. You understand the difference instantly. A transformer? Without help, it sees both as identical.
Transformers process all tokens in parallel. There's no left-to-right reading. No inherent concept of "first word" or "last word." Every token arrives at the attention mechanism simultaneously, like dumping a bag of Scrabble tiles on a table.
Position encoding solves this. It tells the model where each token sits. And the specific method chosen β absolute, relative, ALiBi, or RoPE β determines how well the model handles long contexts, how efficiently it trains, and whether it can extrapolate beyond its training length.
RoPE won. Let's see why.
Why Position Matters
Without position encoding, self-attention computes:
Attention(Q, K) = softmax(Q Γ K^T)This is permutation invariant β shuffling the input tokens gives the exact same attention scores. The model literally can't tell if a word is first or last.
Position encoding adds positional information so that the same token at position 3 and position 300 produces different representations.
The Three Eras of Position Encoding
Era 1: Absolute Sinusoidal (2017 β Original Transformer)
The original "Attention Is All You Need" paper used fixed sinusoidal functions:
python
PE(pos, 2i) = sin(pos / 10000^(2i/d))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d))
Each position gets a unique vector. This vector is added to the token embedding before it enters the transformer.
python
import numpy as np
def sinusoidal_pe(max_len, d_model):
pe = np.zeros((max_len, d_model))
position = np.arange(max_len).reshape(-1, 1)
div_term = 10000 ** (np.arange(0, d_model, 2) / d_model)pe[:, 0::2] = np.sin(position / div_term) # Even dimensions
pe[:, 1::2] = np.cos(position / div_term) # Odd dimensions
return pepe = sinusoidal_pe(1000, 512)pe[0] = position encoding for position 0
pe[999] = position encoding for position 999
Problem: It's absolute. Position 5 always gets the same encoding, regardless of the sequence length or what's around it. And it doesn't extrapolate β if you train on 512 tokens, positions 513+ produce unseen encodings that break the model.
Era 2: Learned Absolute (GPT-2, BERT)
Instead of fixed sinusoids, just learn the position embeddings during training:
python
GPT-2 style
self.pos_embedding = nn.Embedding(max_position, d_model)
Learns a unique vector for each position 0, 1, 2, ..., max_position-1
x = token_embedding + self.pos_embedding(position_ids)Problem: Same fundamental issue β absolute positions, fixed maximum, no extrapolation. If you train with max_position = 2048, position 2049 literally doesn't exist in the embedding table.
Era 3: Relative Position (Modern)
Instead of encoding "token at position 5", encode "token is 3 positions away from the other token." This is position-relative rather than position-absolute.
Absolute: "cat" is at position 5
Relative: "cat" is 2 positions after "the"
Relative encoding is more powerful because:
The relationship between positions 5 and 7 is the same as 1005 and 1007
It generalizes to unseen positions
It captures what matters β relative distance, not absolute location
This is where RoPE and ALiBi come in.
π§
RoPE: Rotary Position Embedding
Paper: "RoFormer: Enhanced Transformer with Rotary Position Embedding" (Su et al., 2021) Used by: Llama 1/2/3, Mistral, Qwen, Phi, CodeLlama, DeepSeek β basically everything
RoPE encodes position by rotating the query and key vectors. The rotation angle depends on the position β tokens further apart get larger angular differences.
The Intuition
Imagine each pair of dimensions in Q and K as a 2D point. RoPE rotates that point by an angle proportional to the token's position:
Position 0: rotate by 0Β°
Position 1: rotate by ΞΈ
Position 2: rotate by 2ΞΈ
Position 3: rotate by 3ΞΈ
...
Position n: rotate by nΞΈ
When you compute the dot product of a rotated Q and rotated K (which is what attention does), the result depends only on their relative rotation β i.e., the difference in their positions.
Q at position 5, rotated by 5ΞΈ
K at position 3, rotated by 3ΞΈ
Q Β· K depends on rotation difference = (5-3)ΞΈ = 2ΞΈ
β Only the relative distance (2) matters!
The Math
For each pair of dimensions (2i, 2i+1) in the query/key vectors:
python
def apply_rope(x, position, dim_pair_index, base=10000):
"""Apply RoPE to a pair of dimensions."""
theta = 1.0 / (base ** (2 * dim_pair_index / d_model))
angle = position * theta
# Rotate the 2D vector by angle
cos_a, sin_a = cos(angle), sin(angle)
x_rotated_0 = x[0] * cos_a - x[1] * sin_a
x_rotated_1 = x[0] * sin_a + x[1] * cos_a
return x_rotated_0, x_rotated_1Applied to the full vector:
python
import torch
def apply_rotary_emb(q, k, freqs_cos, freqs_sin):
"""Apply RoPE to query and key tensors."""
# Split into pairs: [..., d] β [..., d/2, 2]
q_r, q_i = q.float().reshape(*q.shape[:-1], -1, 2).unbind(-1)
k_r, k_i = k.float().reshape(*k.shape[:-1], -1, 2).unbind(-1)# Apply rotation
q_out_r = q_r * freqs_cos - q_i * freqs_sin
q_out_i = q_r * freqs_sin + q_i * freqs_cos
k_out_r = k_r * freqs_cos - k_i * freqs_sin
k_out_i = k_r * freqs_sin + k_i * freqs_cos# Merge pairs back
q_out = torch.stack([q_out_r, q_out_i], dim=-1).flatten(-2)
k_out = torch.stack([k_out_r, k_out_i], dim=-1).flatten(-2)return q_out.type_as(q), k_out.type_as(k)def precompute_freqs(dim, max_seq_len, base=10000.0):
"""Precompute the rotation frequencies."""
freqs = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
t = torch.arange(max_seq_len)
freqs = torch.outer(t, freqs)
return torch.cos(freqs), torch.sin(freqs)Why Rotation Is Genius
Relative by design: The dot product after rotation depends only on position difference
No extra parameters: RoPE doesn't add learnable parameters β it's a deterministic transformation
Applied to Q and K only: Values aren't rotated β they carry content, not position
Decays with distance: Tokens far apart have weaker attention naturally (the rotation makes dot products smaller for large distances)
π
ALiBi: Attention with Linear Biases
Paper: "Train Short, Test Long" (Press et al., 2022) Used by: BLOOM, MPT, some Falcon variants
ALiBi takes a completely different approach β no rotation, no position encoding in the embedding. Instead, it adds a linear penalty directly to the attention scores:
python
Standard attention:
scores = Q @ K.T / sqrt(d)ALiBi attention:
scores = Q @ K.T / sqrt(d) + biasWhere bias is a matrix of linear penalties:
# bias[i][j] = -m *i - j
m = slope (different per head)
The bias penalizes attention to distant tokens proportionally to distance:
For head with slope m = 0.5:
Token positions: 0 1 2 3 4
Position 0: [ 0, -0.5, -1.0, -1.5, -2.0]
Position 1: [-0.5, 0, -0.5, -1.0, -1.5]
Position 2: [-1.0, -0.5, 0, -0.5, -1.0]
Different heads get different slopes (powers of 2), so some heads attend locally and others attend globally:
python
ALiBi slopes for 8 heads
slopes = [1/1, 1/2, 1/4, 1/8, 1/16, 1/32, 1/64, 1/128]Head 1 (slope=1): strong recency bias β attends mostly to nearby tokens
Head 8 (slope=1/128): weak recency bias β attends broadly
ALiBi vs RoPE
Feature RoPE ALiBi
How it works Rotates Q, K vectors Adds bias to attention scores
Extra parameters None None
| Extrapolation | Moderate (needs NTK/YaRN scaling) | Good (linear bias naturally extends) |
|---|---|---|
| Training length sensitivity | Somewhat sensitive | More robust |
| Adopted by | Llama, Mistral, Qwen, Phi | BLOOM, MPT |
| Current dominance | Winner β used by ~90% of models | Niche |
ALiBi was promising for extrapolation, but RoPE + scaling techniques (NTK, YaRN) caught up and surpassed it. RoPE's richer positional signal ultimately produced better quality.
Extending Context: RoPE Scaling
RoPE is trained on a fixed context length (say, 8K). How do you extend to 128K?
NTK-Aware Scaling
Instead of linearly interpolating positions (which compresses them), NTK scaling changes the base frequency:
python
Original RoPE: base = 10000
NTK-scaled for 4x context: base = 10000 (4 * (d/(d-2)))
For d=128, extending from 8K to 32K (4x):
new_base = 10000 (4 * (128/126)) # β 40,518Higher frequencies (nearby tokens) barely change
Lower frequencies (distant tokens) get extended
YaRN (Yet another RoPE extensioN)YaRN combines NTK scaling with an attention temperature fix:
python
YaRN: scale + temperature correction
1. NTK-aware base frequency scaling
2. Multiply attention logits by sqrt(1/scaling_factor) for first few tokens
3. This preserves local attention patterns while extending reach
Real-World Context Extensions
Model Training Length Extended To Method
| Llama 2 | 4K | 32K | YaRN |
|---|---|---|---|
| Code Llama | 16K | 100K | RoPE frequency scaling |
| Llama 3 | 8K | 128K | Progressive training + RoPE |
| Mistral | 8K | 32K | Sliding window + RoPE |
The key insight: you don't need to train from scratch on long contexts. Train short, then fine-tune with scaled RoPE for a fraction of the compute.
Comparison Table: All Position Encodings
Method Type Parameters Extrapolation Quality Used By
Sinusoidal Absolute None Poor OK Original Transformer
| Learned | Absolute | Yes (N Γ d) | None | Good | GPT-2, BERT |
|---|---|---|---|---|---|
| RoPE | Relative (rotation) | None | Good (with scaling) | Best | Llama, Mistral, Qwen |
| ALiBi | Relative (linear bias) | None | Good | Good | BLOOM, MPT |
| Relative Bias (T5) | Relative (learned bias) | Yes (num_buckets) | Moderate | Good | T5, Flan |
π¬
Try It Yourself
python
import torch
import matplotlib.pyplot as plt
import numpy as np
Visualize RoPE rotation angles
d_model = 128
max_len = 1024
base = 10000.0
freqs = 1.0 / (base ** (torch.arange(0, d_model, 2).float() / d_model))
positions = torch.arange(max_len)
angles = torch.outer(positions, freqs)Plot rotation angles for different dimensions
plt.figure(figsize=(12, 6))
for dim_idx in [0, 10, 30, 63]:
plt.plot(angles[:100, dim_idx].numpy(), label=f'Dim pair {dim_idx}')plt.xlabel('Position')
plt.ylabel('Rotation Angle (radians)')
plt.title('RoPE Rotation Angles by Position')
plt.legend()
plt.grid(True)
plt.savefig('rope_angles.png', dpi=100)
print("Saved rope_angles.png")Key observation:
Low dimension pairs β high frequency rotation (local patterns)
High dimension pairs β low frequency rotation (global patterns)
π‘οΈ
Practical Takeaways
Transformers have no built-in word order β position encoding is mandatory
Absolute position encoding (learned/sinusoidal) can't extrapolate β model breaks beyond training length
RoPE encodes relative position via vector rotation β elegant, parameter-free, works great
ALiBi adds linear distance penalties to attention scores β simpler but less adopted
RoPE scaling (NTK, YaRN) extends context without full retraining β train at 8K, extend to 128K
RoPE won the position encoding war β used by essentially every major open model in 2024-2025
π¦
What's Next?
Episode 78: MCP (Model Context Protocol) β We've been deep in model internals for 7 episodes. Time to zoom out to the application layer. Anthropic's MCP is becoming the standard for how AI models connect to external tools and data. Resources, tools, prompts β it's the USB-C of AI tool integration.
β Previous
Ep 76: PagedAttention
Next β
Ep 78: MCP (Model Context Protocol)
Next: Episode 78 β MCP (Model Context Protocol)
Every AI assistant needs tools. Every tool has a different API. MCP is the universal standard that connects them all.
This is part of a 98-episode series covering AI engineering from tokens to production deployment.