MODULE 11  Β·  Tokenizers & Data Formats

RoPE & Position Encoding: How AI Knows Word Order

Transformers process all tokens in parallel. No left-to-right reading. So how does AI know word order? Position encoding.

πŸ“… Mar 2026
⏱ 10 min read
🎯 Episode 77 of 98
RoPEPosition EncodingTransformersArchitecture
In this episode

"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:

snippet
code
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):

example
code
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)
example
code
pe[:, 0::2] = np.sin(position / div_term)  # Even dimensions
    pe[:, 1::2] = np.cos(position / div_term)  # Odd dimensions
    return pe
snippet
code
pe = 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

snippet
code
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

snippet
code
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_1

Applied to the full vector:

python

import torch

def apply_rotary_emb(q, k, freqs_cos, freqs_sin):

example
code
"""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)
example
code
# 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
example
code
# 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)
example
code
return q_out.type_as(q), k_out.type_as(k)
snippet
code
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:

snippet
code
scores = Q @ K.T / sqrt(d)

ALiBi attention:

snippet
code
scores = Q @ K.T / sqrt(d) + bias

Where 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

snippet
code
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

ExtrapolationModerate (needs NTK/YaRN scaling)Good (linear bias naturally extends)
Training length sensitivitySomewhat sensitiveMore robust
Adopted byLlama, Mistral, Qwen, PhiBLOOM, MPT
Current dominanceWinner β€” used by ~90% of modelsNiche

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):

snippet
code
new_base = 10000  (4 * (128/126))  # β‰ˆ 40,518

Higher frequencies (nearby tokens) barely change

Lower frequencies (distant tokens) get extended

snippet
code
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 24K32KYaRN
Code Llama16K100KRoPE frequency scaling
Llama 38K128KProgressive training + RoPE
Mistral8K32KSliding 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

LearnedAbsoluteYes (N Γ— d)NoneGoodGPT-2, BERT
RoPERelative (rotation)NoneGood (with scaling)BestLlama, Mistral, Qwen
ALiBiRelative (linear bias)NoneGoodGoodBLOOM, MPT
Relative Bias (T5)Relative (learned bias)Yes (num_buckets)ModerateGoodT5, Flan

πŸ”¬

Try It Yourself

python

import torch

import matplotlib.pyplot as plt

import numpy as np

Visualize RoPE rotation angles

snippet
code
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]:

example
code
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)

snippet
code
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.

← Previous Ep 76: PagedAttention: How vLLM Serves AI Like an Operating Sy…