Training Foundations

Positional Encoding

Attention cannot tell "dog bites man" from "man bites dog." Here is every fix ever shipped, and the one piece of group theory that says there are only a handful of fixes possible.

Prerequisites: Trigonometry (sin, cos) + What attention does. That's it. Matrices and the exponential get built from zero.
13
Chapters
13
Simulations
2
Sources Fused

Chapter 0: Why Order Matters

Take the sentence "The dog bit the man" and rearrange it to "The man bit the dog." Very different meanings. A dog biting a human is Tuesday in the park. A human biting a dog is front page news.

Now feed both sentences to a transformer with no positional encoding. You get the same output. Exactly the same. The model cannot tell them apart.

This is not a subtle theoretical concern. It is a hole in the middle of the transformer. Self-attention, the operation that gives transformers their power, treats its input as a set, not a sequence. It has no concept of "first," "second," or "third." Every token might as well arrive at the same instant, in a bag.

Let us prove that, with actual math rather than hand waving.

Why Attention Is Permutation-Equivariant

Here is what self-attention does. Given a sequence of token embeddings X = [x1, x2, ..., xn], it computes three matrices: Queries (Q = XWQ), Keys (K = XWK), and Values (V = XWV). Then it computes:

Attention(Q, K, V) = softmax(Q · KT / √dk) · V

Now permute the input. Swap rows of X, putting "dog" where "man" was. Q, K and V are linear transformations of X, so their rows get permuted in exactly the same way. The score matrix Q·KT becomes a row and column permutation of the original. Same values, reshuffled. The final output is the original output with its rows permuted identically.

In symbols: if P is a permutation matrix and you feed PX instead of X, the output is P · Attention(X). The values are identical. They just come out in the new order. The attention mechanism has zero awareness that you rearranged anything. That property has a name: attention is permutation-equivariant.

Token embeddings do not carry position. You might think the embeddings themselves carry position because they are different vectors. They do not. embedding("the") is the same vector at position 0 and at position 5. The embedding table is a dictionary lookup, not a position-aware function. The word "the" gets vector number 4217 no matter where it appears. Position is invisible.

Hand Calculation: Three Tokens, Two Orders

Smallest possible example. Three tokens A, B, C with 2-D embeddings. We compute self-attention for two orderings and watch the outputs come out identical, just reshuffled.

Setup. Token embeddings: A = [1, 0], B = [0, 1], C = [1, 1]. For simplicity WQ = WK = WV = I (the identity), so Q = K = V = X. Scale factor √dk = √2 ≈ 1.414.

Order 1: A, B, C. The raw scores are all the pairwise dot products divided by 1.414.

A·A = 1, A·B = 0, A·C = 1
scaled: 0.707, 0.000, 0.707

Softmax of (0.707, 0.000, 0.707): exponentials are e0.707 = 2.028, e0 = 1.000, e0.707 = 2.028. Their sum is 5.056. So the weights on row A are (0.401, 0.198, 0.401).

Output for A = 0.401·[1,0] + 0.198·[0,1] + 0.401·[1,1] = [0.802, 0.599].

Order 2: C, B, A. Row C now comes first, but the dot products C·C = 2, C·B = 1, C·A = 1 are the same numbers that appeared before, in a different slot. When we get to row A (now the third row) its scores against C, B, A are 1, 0, 1 scaled to 0.707, 0.000, 0.707. The identical triple. Softmax gives the identical weights. The blend gives the identical output [0.802, 0.599].

The result. Every output vector is bit-for-bit what it was, only parked at a different index. Nothing inside attention noticed the rearrangement. That is permutation equivariance, and it is why a raw transformer is a bag-of-words machine with extra steps.

See It: Swap Tokens, Watch the Output

Click two token boxes to swap them. The bars below show the output vectors for the original order and for your order. With position encoding OFF, the bars stay the same values in a new arrangement. Turn it ON and they change for real.

Permutation Invariance Demo
When to reach for it: A code completion model reading a Python diff. The tokens a = b and b = a use the identical vocabulary items with identical embeddings, and a raw transformer scores them identically. GitHub Copilot suggesting the wrong assignment direction is not a style complaint, it is a correctness bug that ships to production. Why position matters here: the meaning lives entirely in the order, not the bag of symbols. Where a bag-of-words model fails: TF-IDF style retrieval over code was the pre-transformer answer and it could not tell an assignment from its reverse, which is why order-aware sequence models replaced it wholesale.

From Scratch: Proving It in Code

python
import numpy as np

def softmax(x, axis=-1):
    e = np.exp(x - x.max(axis=axis, keepdims=True))
    return e / e.sum(axis=axis, keepdims=True)

def attention(X):
    # W_Q = W_K = W_V = I, so Q = K = V = X
    d = X.shape[-1]
    scores = X @ X.T / np.sqrt(d)
    return softmax(scores) @ X

X = np.array([[1., 0.], [0., 1.], [1., 1.]])
P = np.array([[0,0,1], [0,1,0], [1,0,0]])  # reverse

out      = attention(X)
out_perm = attention(P @ X)

print(np.allclose(P @ out, out_perm))   # True
# The permuted input gives the permuted output. Nothing else changed.

That single True is the whole problem. Attention commutes with any reordering of its input. If we want the model to know that "dog" came first, we have to put that fact into the numbers ourselves.

So How Do We Fix This?

Two broad options, and the rest of this lesson is the story of both.

Option one: change the vectors. Before attention runs, add or transform each token's vector using its position. Sinusoidal encoding adds a wave pattern. Learned embeddings add a trained vector. RoPE rotates the query and key. The dot product then depends on position because the inputs to the dot product do.

Option two: change the score. Leave the vectors alone and add a number to the attention logit that depends on how far apart the two tokens are. ALiBi subtracts a penalty proportional to distance. T5 adds a learned bucket bias.

Where the Fix Has to Live

Before we compare methods, get the shapes straight, because the shapes decide what is cheap and what is not. A batch of B sequences of length L, with model width d and H heads of width dhead, moves through attention like this:

StageShapeCan position be injected here?
Token embeddings(B, L, d)Yes, by addition. Sinusoidal and learned absolute do this, once, before layer one.
Q, K, V after projection(B, H, L, dhead)Yes, by transforming Q and K. RoPE and GRAPE-M do this, in every layer.
Attention logits(B, H, L, L)Yes, by adding a bias. ALiBi, T5 and GRAPE-A do this.
Softmax output times V(B, H, L, dhead)No. By here the scores are already committed.

The third row is the expensive one. Anything that has to materialise an (B, H, L, L) tensor before the softmax fights every memory-efficient attention kernel, because those kernels exist precisely to avoid building it. That single constraint explains most of the design history that follows: methods that inject at row two, or that compute their row-three bias from loop indices without storing it, are the ones that survived.

The question nobody asked for eight years. Both options work. Both have dozens of variants. But which perturbations are even legal? If you write down the three properties any sane positional encoding must have, how large is the space of things that satisfy them? Chapters 5 through 9 answer that, and the answer is much smaller than anyone expected.
A transformer with no positional encoding is fed "man bites dog" and then "dog bites man". What is the relationship between the two output sequences?

Chapter 1: Sinusoidal Encoding

Vaswani et al. needed a position signal in 2017 and they had one hard constraint: it had to work for sequence lengths the model had never seen. So they built one out of sine waves.

Start with the obvious idea and watch it fail. Add the raw index to every token vector: position 0 adds 0, position 512 adds 512. Now the position term is a hundred times larger than the embedding values, which live around plus or minus one. The token identity gets buried under a number that only says "you are late in the sentence." Bad.

Second try: divide by the max length so positions land in [0, 1]. Now the signal is small enough, but the meaning of "0.5" depends on the total length. Position 5 of 10 and position 500 of 1000 both encode as 0.5. The model cannot tell a short sentence from a long one, and it has no fixed notion of "three tokens back."

What we actually want. A function of position that is bounded, that gives every position a unique fingerprint, that keeps nearby positions near each other, and that is defined for every position out to infinity so we can go past the training length. Sine and cosine give all four for free.

Building the Encoding From Scratch

Here is the trick. Think about how binary counting works. The ones bit flips every step, the twos bit every two steps, the fours bit every four. Each bit is a square wave at half the frequency of the one before, and together the bits uniquely identify any integer.

Sinusoidal encoding is that idea in continuous form. Instead of square waves at frequencies 1, 1/2, 1/4, we use sine waves at frequencies that shrink geometrically. Dimension pair 0 oscillates fast, dimension pair 1 slower, and so on down to a wave so slow it barely completes one cycle across the whole context.

PE(pos, 2i) = sin( pos / 100002i/d )
PE(pos, 2i+1) = cos( pos / 100002i/d )

Read that as: pos is the position, d is the model width, and i indexes the pair of dimensions. Each pair (2i, 2i+1) gets one angular frequency θi = 1 / 100002i/d, and we store both the sine and the cosine of pos·θi. Storing both is what makes the pair behave like a point on a circle rather than a single wobbling number. Hold that thought: a point on a circle is about to become the entire story.

Hand Calculation: d = 4, Positions 0 to 3

With d = 4 there are two pairs, so two frequencies. Compute them first.

Pair i = 0: θ0 = 1 / 100000/4 = 1 / 100000 = 1.
Pair i = 1: θ1 = 1 / 100002/4 = 1 / √10000 = 1 / 100 = 0.01.

Now evaluate at four positions. Angles are in radians.

possin(pos·1)cos(pos·1)sin(pos·0.01)cos(pos·0.01)
00.0001.0000.0001.000
10.8410.5400.0101.000
20.909−0.4160.0201.000
30.141−0.9900.0301.000

Look at what the two pairs are doing. The fast pair (columns 2 and 3) has already swung most of the way around the circle by position 3. It discriminates neighbours sharply. The slow pair (columns 4 and 5) has barely moved: 0.030 radians is under two degrees. It cannot tell position 2 from position 3, but it will still be climbing steadily at position 900, long after the fast pair has wrapped around a hundred and forty times.

Multi-scale is the whole point. One frequency alone is ambiguous because sine wraps: positions 1 and 1 + 2π look the same. Many frequencies together are unambiguous, exactly the way a clock with a second hand, a minute hand and an hour hand pins down a time that no single hand could.

Why 10000 as the Base?

The base sets the ratio between the fastest and the slowest wave. With base B, the slowest frequency is 1/B, so its wavelength is 2πB. With B = 10000 the slowest wave has a wavelength of about 62,832 tokens, comfortably longer than any 2017 context window. The fastest wave has a wavelength of 2π ≈ 6.3 tokens, fine enough to separate immediate neighbours.

Pick the base too small and the slow waves wrap around inside your context, creating aliasing: two far apart positions get the same fingerprint. Pick it too large and the slow dimensions are almost constant, wasting capacity. Ten thousand was chosen for a 512-token context. When Llama 3 went to 128k tokens it raised the RoPE base to 500,000 for exactly this reason, which we return to in Chapter 10.

The Property That Almost Gives Relativity

There is one more thing hiding in the sine and cosine pairing, and it is the germ of everything in Chapter 3. Take a single pair at frequency θ and write it as a two dimensional vector:

p(pos) = [ sin(pos·θ),   cos(pos·θ) ]

Now shift by k and expand with the angle addition formulas:

sin((pos+k)θ) = sin(posθ)cos(kθ) + cos(posθ)sin(kθ)
cos((pos+k)θ) = cos(posθ)cos(kθ) − sin(posθ)sin(kθ)

Stack those two lines and you have a matrix times p(pos), and the matrix depends only on k, not on pos. It is a rotation by kθ.

p(pos + k) = R(kθ) · p(pos)
Check it with the numbers from the table. Take θ = 1 and k = 1. p(2) = [0.909, −0.416]. Rotate p(1) = [0.841, 0.540] by one radian using cos 1 = 0.540 and sin 1 = 0.841: the first entry is 0.841·0.540 + 0.540·0.841 = 0.454 + 0.454 = 0.908, and the second is 0.540·0.540 − 0.841·0.841 = 0.292 − 0.707 = −0.415. That is p(2), to rounding.

This is why the 2017 paper argued that relative positions are recoverable: the offset shows up as a fixed linear map. But recoverable is not the same as recovered. Nothing in the architecture forces WQ and WK to exploit that structure, and the position vector was added to the token vector, so content and position are tangled before attention even sees them. RoPE's whole contribution is to apply the rotation directly to the query and the key, so relativity is not a property the model may discover but a property the arithmetic guarantees.

See It: The Frequency Ladder

Two views of the same object. Heatmap shows the encoding matrix: rows are positions, columns are dimensions, colour is the value. The stripes get wider from left to right, which is the frequency slowing down. Waves plots each dimension pair as its own curve so you can watch the ladder of periods directly. Drag the base slider down to 100 and watch the slow waves start wrapping inside the window.

Sinusoidal Position Encoding
Base 10000
Positions 48
When to reach for it: OpenAI's Whisper audio encoder, which still uses fixed sinusoidal position encoding over 30-second mel spectrogram windows (1500 frames per window). The vectors are 1500 frames of 80-bin log-mel features projected to the model width, and every window has exactly the same length. Why sinusoidal: it costs zero parameters, it needs no training data at long positions, and with a fixed window there is nothing to extrapolate to. Where a learned table fails: it would add 1500 × d parameters that only ever see one length, buying nothing, and it would leave you with no graceful behaviour at all if you later widened the window.

From Scratch: Build the Table

python
import numpy as np

def sinusoidal_pe(max_len, d_model, base=10000.0):
    pos = np.arange(max_len)[:, None]              # (L, 1)
    i   = np.arange(0, d_model, 2)[None, :]         # (1, d/2)
    theta = 1.0 / (base ** (i / d_model))          # (1, d/2)
    ang = pos * theta                                # (L, d/2)
    pe = np.zeros((max_len, d_model))
    pe[:, 0::2] = np.sin(ang)
    pe[:, 1::2] = np.cos(ang)
    return pe                                     # (L, d)

pe = sinusoidal_pe(4, 4)
print(pe.round(3))
# [[ 0.     1.     0.     1.   ]
#  [ 0.841  0.54   0.01   1.   ]
#  [ 0.909 -0.416  0.02   1.   ]
#  [ 0.141 -0.99   0.03   1.   ]]   matches the hand table

Shapes matter. pe is (L, d) and gets broadcast-added to the token embedding tensor of shape (B, L, d) once, before the first attention layer. It is a buffer, not a parameter: no gradient flows into it, and it costs nothing beyond L·d floats you can also recompute on the fly.

The catch that motivates everything after. Sinusoidal encoding is absolute. It tells each token where it sits from the start of the sequence. Attention, though, mostly wants to know how far apart two tokens are. The paper argued that PE(pos + k) is a fixed linear function of PE(pos), so relative offsets are recoverable in principle. In practice the model has to learn that recovery inside WQ and WK, and it does so imperfectly. The next chapters make relativity structural instead of hopeful.
Why does sinusoidal encoding use many frequencies instead of one?

Chapter 2: Learned Tables and Relative Bias

BERT and GPT-2 looked at the sine wave construction and asked the obvious question: why hand-design anything? Make a lookup table with one trainable vector per position, initialise it randomly, and let gradient descent decide what "position 7" should mean.

How the Learned Table Works

You allocate a matrix Epos of shape (max_len, d). Token at index 5 gets row 5 added to its token embedding. That is the entire mechanism. GPT-2 used max_len = 1024 with d = 768, which is 786,432 extra parameters, about 0.6 percent of the model.

hi = Etok[tokeni] + Epos[i]

Hand Calculation: Token Plus Position

Setup. d = 4. Token "cat" has embedding [0.50, −0.20, 0.30, 0.10]. The learned position rows are Epos[0] = [0.10, 0.00, −0.10, 0.20] and Epos[3] = [−0.40, 0.60, 0.20, −0.30].

"cat" at position 0 becomes [0.60, −0.20, 0.20, 0.30]. The same "cat" at position 3 becomes [0.10, 0.40, 0.50, −0.20]. Two genuinely different vectors for the same word, which is exactly what we asked for.

Now compute the attention score between "cat at 0" and "cat at 3" using identity projections. Dot product: 0.60·0.10 + (−0.20)·0.40 + 0.20·0.50 + 0.30·(−0.20) = 0.06 − 0.08 + 0.10 − 0.06 = 0.02. Position changed the score. Fine. But notice what we cannot say: nothing in that number is a function of the offset 3 by itself. It is a function of the pair (0, 3).

The Length Limit

The table has max_len rows. Feed the model position 1024 when it trained on 1024 rows and there is no row to look up. You either crash or you index into uninitialised memory. There is no principled extension, because the model learned each row independently and never learned a rule that generates rows.

Worse, the late rows are undertrained. If most of your training documents are 200 tokens long, rows 800 through 1023 got a fraction of the gradient signal that row 5 got. Quality sags well before the hard limit.

The deeper problem is not the limit, it is the redundancy. "The cat sat" appearing at positions 0, 1, 2 and the same phrase at positions 500, 501, 502 are the same linguistic pattern. With absolute encoding the model sees two unrelated pairs of vectors and has to learn the pattern twice, once per neighbourhood. That is a brutal waste of capacity.

What Relative Encoding Looks Like

Shaw et al. (2018) proposed the fix that names the era. Do not tell the model where each token is. Tell it, for each pair, how far apart they are. Concretely, add a learned scalar to the attention logit that depends only on the offset i − j:

score(i, j) = qi · kj / √d + b[ clip(i − j) ]

The vector b holds one learned number per offset bucket. T5 refined this into bucketed relative bias: offsets 0 through 7 get their own bucket each, then buckets widen logarithmically out to a maximum distance, and everything beyond lands in the last bucket. Thirty-two buckets cover any distance you like, with fine resolution nearby and coarse resolution far away, which matches how language actually works.

Hand Calculation: Same Distance, Different Encodings

Take two token pairs with the same gap of 2: the pair (3, 1) and the pair (503, 501).

SchemePair (3, 1)Pair (503, 501)Same?
Learned absoluteuses rows 3 and 1uses rows 503 and 501No, four unrelated vectors
SinusoidalPE(3), PE(1)PE(503), PE(501)Related by a rotation the model must learn to exploit
Relative biasb[2]b[2]Yes, literally the same number

The relative row is the one to stare at. One learned parameter, shared by every pair at distance 2 anywhere in the sequence. Every training example at that offset improves it. That is the capacity argument, and it is why relative schemes generalise to longer sequences with far less pain.

See It: Table versus Band

Left: the learned absolute table, one independent row per position, all of them different colours because nothing ties them together. Right: the relative bias matrix, which is constant along every diagonal because the bias depends only on i − j. Slide the offset to watch a single diagonal light up. That diagonal structure is the seed of everything in Chapter 5.

Absolute Table vs Relative Bias Band
Highlight offset +2
When to reach for it: T5 and its descendants (FLAN-T5, ByT5) use bucketed relative bias for encoder-decoder translation and summarisation, where inputs range from a one-line question to a 4,000-token document. The learned object is a table of 32 buckets per head, roughly 32 × 12 numbers per layer instead of 512 × 768. Why relative bias: the same bucket parameter is trained by every pair at that distance in every document length, so a rare 900-token input still uses well-trained parameters. Where a learned absolute table fails: rows past the median document length see almost no gradient, so quality falls off a cliff exactly on the long inputs summarisation cares most about.

Hand Calculation: Which Bucket?

Buckets are where relative bias gets its length behaviour, so it is worth computing a few by hand. Take T5's defaults: 32 buckets, split 16 per direction, the first 8 offsets each getting their own bucket, and a maximum distance of 128.

Offsets 0 through 7 are exact: offset 3 is bucket 3, no arithmetic. Past 8, the bucket index is 8 + floor( ln(offset/8) / ln(128/8) × 8 ), clamped at 15.

Offset 20: ln(20/8) = ln 2.5 = 0.916, ln 16 = 2.773. 0.916 / 2.773 = 0.330. Times 8 is 2.64, floor 2. Bucket = 8 + 2 = 10.
Offset 100: ln 12.5 = 2.526. 2.526 / 2.773 = 0.911. Times 8 is 7.29, floor 7. Bucket = 15.
Offset 500: ln 62.5 = 4.135. 4.135 / 2.773 = 1.491. Times 8 is 11.9, which clamps to 7. Bucket = 15.

Offsets 100 and 500 land in the same bucket and therefore get the identical bias. That is not a bug, it is the design: past the maximum distance the scheme stops distinguishing distances at all. It is also exactly why bucketed relative bias shows up as a flat row in the extrapolation arena in Chapter 10, neither collapsing nor improving. It has stopped answering the question rather than answering it badly.

The Cost: A Bias Matrix Per Layer

Relative bias is not free. The score matrix is (L, L) per head, and the bias has to be materialised at the same shape before the softmax. At L = 8192 that is 67 million entries per head, which is exactly the tensor FlashAttention works so hard not to materialise. Shaw-style relative encodings that add vectors to the keys are worse still: they turn one matrix multiply into a gather plus a second multiply.

python
import math, torch, torch.nn as nn

class T5RelativeBias(nn.Module):
    def __init__(self, n_heads, n_buckets=32, max_dist=128):
        super().__init__()
        self.b = nn.Embedding(n_buckets, n_heads)   # (32, H)
        self.n_buckets, self.max_dist = n_buckets, max_dist

    def bucket(self, rel):                       # rel: (L, L) int
        n = self.n_buckets // 2                     # half for each sign
        sign = (rel > 0).long() * n
        r = rel.abs()
        exact = n // 2                              # small gaps get own bucket
        big = exact + (
            torch.log(r.float().clamp(min=1) / exact)
            / math.log(self.max_dist / exact) * (n - exact)
        ).long()
        return sign + torch.where(r < exact, r, big.clamp(max=n - 1))

    def forward(self, L):
        pos = torch.arange(L)
        rel = pos[None, :] - pos[:, None]           # (L, L) = j - i
        return self.b(self.bucket(rel)).permute(2, 0, 1)  # (H, L, L)

Read the shapes. The learned parameter is (32, H), tiny. The thing you add to the logits is (H, L, L), enormous. That gap between parameter cost and runtime cost is the reason the field went looking for something that could be relative without building an L by L tensor. That something is RoPE.

What is the core advantage of relative position bias over a learned absolute table?

Chapter 3: RoPE, Position as Rotation

Su et al. (2021) asked a sharper question than anyone before them. Instead of adding something to the vectors or to the score, what if we rotate the query and the key by an angle proportional to their positions?

Picture the hands of a clock. Take a 2-D slice of the query vector and turn it by m·θ radians, where m is the query's position. Take the same 2-D slice of the key and turn it by n·θ. Now take their dot product.

The magic. A dot product between two vectors depends only on their lengths and the angle between them. Rotation preserves lengths. And the angle between them, after the two rotations, is the original angle plus (m − n)·θ. The absolute positions cancel. The score depends only on the offset, and we never built an L by L bias tensor to make it happen.

Hand Calculation: One Plane, 30 Degrees

Work entirely in 2-D first. Let q = [1, 0] and k = [0, 1], so the angle between them starts at 90 degrees and their dot product is 0. Let θ = 30 degrees = 0.5236 radians.

The 2-D rotation matrix by angle φ is

R(φ) = [ cosφ  −sinφ ;   sinφ   cosφ ]

Case 1: q at position m = 2, k at position n = 1.

Query angle 2·30 = 60 degrees. cos 60 = 0.500, sin 60 = 0.866. R(60)·[1, 0] = [0.500, 0.866].

Key angle 1·30 = 30 degrees. cos 30 = 0.866, sin 30 = 0.500. R(30)·[0, 1] = [−0.500, 0.866].

Dot product: 0.500·(−0.500) + 0.866·0.866 = −0.250 + 0.750 = 0.500.

Case 2: q at position m = 7, k at position n = 6. Same offset, wildly different absolute positions.

Query angle 210 degrees: R(210)·[1, 0] = [−0.866, −0.500]. Key angle 180 degrees: R(180)·[0, 1] = [0.000, −1.000].

Dot product: (−0.866)·0.000 + (−0.500)·(−1.000) = 0.500. Identical.

Why, in one line. R(mθ)q · R(nθ)k = qTR(mθ)TR(nθ)k = qTR((n − m)θ)k, because rotations are orthogonal (RT = R−1) and composing two rotations adds their angles. Every absolute position vanishes. Only the difference survives. This one identity is the reason RoPE is in essentially every open-weight LLM.

Hand Calculation: Four Dimensions, Two Planes

Real RoPE splits a d-dimensional head into d/2 independent planes and gives each one its own frequency. Let d = 4, so two planes, with the usual base 10000: θ0 = 1 and θ1 = 0.01, the same pair we computed in Chapter 1.

Setup. q = [1, 0, 1, 0] at position m = 3, k = [0, 1, 0, 1] at position n = 1. Plane A is dims (0, 1), plane B is dims (2, 3).

Plane A, frequency 1. Query angle 3·1 = 3 rad. cos 3 = −0.990, sin 3 = 0.141. R(3)·[1, 0] = [−0.990, 0.141]. Key angle 1·1 = 1 rad. cos 1 = 0.540, sin 1 = 0.841. R(1)·[0, 1] = [−0.841, 0.540]. Partial dot: (−0.990)(−0.841) + (0.141)(0.540) = 0.833 + 0.076 = 0.909.

Plane B, frequency 0.01. Query angle 0.03 rad, key angle 0.01 rad. R(0.03)·[1, 0] = [1.000, 0.030]. R(0.01)·[0, 1] = [−0.010, 1.000]. Partial dot: (1.000)(−0.010) + (0.030)(1.000) = −0.010 + 0.030 = 0.020.

Total score = 0.909 + 0.020 = 0.929.

Now check the prediction. The offset is n − m = −2, so plane A should give qTR(−2)k with the unrotated q and k.

R(−2)·[0,1] = [sin 2, cos 2] = [0.909, −0.416], dotted with [1, 0] gives 0.909. Correct. Plane B: R(−0.02)·[0,1] = [0.020, 1.000], dotted with [1, 0] gives 0.020. Correct.

The four dimensional score decomposed into two independent planar scores, each a function of the offset alone. That is what "d/2 planes" buys you.

Concept and realization. Rotate Q and K only. Never V. The whole point is to make the score relative; V carries content that gets blended after the softmax and must not be spun. Shapes: q and k arrive as (B, H, L, dhead); you build cos and sin tables of shape (L, dhead/2), broadcast them over batch and head, and write back tensors of the same shape. Nothing changes downstream. FlashAttention never sees a bias tensor because there is not one.

See It: Rotate and Watch the Score

Move both position sliders. The left panel shows the rotated query and key in one plane. The right panel plots the score as a function of the offset m − n, with a marker on your current pair. Slide m and n together and the marker does not move, because the score genuinely only sees the difference.

RoPE Rotation and the Relative Score
Query pos m 6
Key pos n 4
Frequency θ 0.50
When to reach for it: Llama 3, Qwen, Mistral, DeepSeek and Gemma all ship RoPE as the default for causal language modelling with 8k to 128k context. The vectors are per-head queries and keys of width 128, split into 64 rotation planes with log-uniform frequencies. Why RoPE: relativity is structural, norms are preserved so logits do not drift, and the cost is one elementwise multiply-add per token with no L by L tensor, so FlashAttention stays usable. Where a T5-style relative bias fails: it needs an (H, L, L) bias materialised before the softmax, which at 128k context is hundreds of gigabytes per head and defeats every memory-efficient attention kernel.

The Same Rotation, Three Ways

Ladder one of three. Identical inputs, identical printed output, from plain Python up to a production PyTorch layer.

python  ·  rung 1: plain Python, no libraries
import math

def rope_plain(v, pos, base=10000.0):
    d = len(v)
    out = [0.0] * d
    for i in range(d // 2):
        theta = 1.0 / (base ** (2.0 * i / d))
        ang = pos * theta
        c, s = math.cos(ang), math.sin(ang)
        x, y = v[2*i], v[2*i + 1]        # the i-th plane
        out[2*i]     = x * c - y * s
        out[2*i + 1] = x * s + y * c
    return out

q = rope_plain([1., 0., 1., 0.], 3)
k = rope_plain([0., 1., 0., 1.], 1)
print(round(sum(a*b for a, b in zip(q, k)), 4))   # 0.9288
python  ·  rung 2: NumPy, whole sequence at once
import numpy as np

def rope_numpy(x, pos, base=10000.0):
    # x: (L, d) with d even.  pos: (L,)
    d = x.shape[-1]
    inv = 1.0 / (base ** (np.arange(0, d, 2) / d))   # (d/2,)
    ang = pos[:, None] * inv[None, :]              # (L, d/2)
    c, s = np.cos(ang), np.sin(ang)
    xe, xo = x[:, 0::2], x[:, 1::2]                # even / odd halves of each plane
    out = np.empty_like(x)
    out[:, 0::2] = xe * c - xo * s
    out[:, 1::2] = xe * s + xo * c
    return out

q = rope_numpy(np.array([[1., 0., 1., 0.]]), np.array([3]))
k = rope_numpy(np.array([[0., 1., 0., 1.]]), np.array([1]))
print(round(float(q @ k.T), 4))                  # 0.9288
python  ·  rung 3: PyTorch, batched and cached
import torch

class RoPE(torch.nn.Module):
    def __init__(self, dim, max_len=8192, base=10000.0):
        super().__init__()
        inv = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
        ang = torch.outer(torch.arange(max_len).float(), inv)  # (L, d/2)
        self.register_buffer("cos", ang.cos(), persistent=False)
        self.register_buffer("sin", ang.sin(), persistent=False)

    def forward(self, x, offset=0):
        # x: (B, H, L, d) -> same shape, rotated in place per plane
        L = x.shape[-2]
        c = self.cos[offset:offset+L][None, None]    # (1,1,L,d/2)
        s = self.sin[offset:offset+L][None, None]
        xe, xo = x[..., 0::2], x[..., 1::2]
        out = torch.empty_like(x)
        out[..., 0::2] = xe * c - xo * s
        out[..., 1::2] = xe * s + xo * c
        return out

rope = RoPE(4)
q = rope(torch.tensor([[[[1., 0., 1., 0.]]]]), offset=3)
k = rope(torch.tensor([[[[0., 1., 0., 1.]]]]), offset=1)
print(round((q * k).sum().item(), 4))               # 0.9288

All three print 0.9288, which is the 0.929 we computed by hand. The offset argument in rung three is not decoration: during generation you rotate one new token at a time, and the offset is its absolute index in the running sequence.

A RoPE attention score is computed for query position 900 and key position 898. Which other pair produces exactly the same score, given the same underlying q and k vectors?

Chapter 4: ALiBi, the Additive Ramp

Press, Smith and Lewis (2021) went the other way. Do not touch the vectors at all. Leave q and k exactly as the projections produced them, and simply subtract a penalty from the attention logit that grows with distance.

score(i, j) = qi · kj / √d − mh · |i − j|

That is the entire method. One scalar mh per head, fixed, not learned. The name is Attention with Linear Biases, and the whole paper is that formula plus the observation that it extrapolates better than anything that came before it.

The intuition is recency. Language has a strong locality prior: the token you are predicting depends far more on the last twenty tokens than on tokens two thousand back. ALiBi bakes that prior into the architecture as a soft, differentiable exponential decay. Because softmax(x − c·dist) equals softmax(x)·e−c·dist up to normalisation, a linear penalty in the logit is a geometric decay in the attention weight.

Why Every Head Gets a Different Slope

If every head had the same slope, every head would have the same field of view, and the model could not simultaneously look at the last three tokens and the whole document. So ALiBi assigns slopes as a geometric sequence. For 8 heads:

mh = 2−h,   h = 1 ... 8:   1/2, 1/4, 1/8, 1/16, 1/32, 1/64, 1/128, 1/256

Head 1 has slope 0.5, so a token 20 back gets a penalty of 10 logits, which after softmax is a factor of e−10 ≈ 0.000045. That head is effectively blind past a handful of tokens: it is a local syntax head. Head 8 has slope 0.0039, so the same token 20 back is penalised by 0.078 logits, almost nothing. That head sees the whole document.

For n heads the general rule is a geometric ratio of 2−8/n, chosen so the slope range stays roughly the same whether you have 8 heads or 64.

Hand Calculation: One Bias Row

Take head 2, slope m = 0.25, and a causal query at position i = 4. The bias added to each key position j:

key j01234
|i − j|43210
bias = −0.25·|i−j|−1.00−0.75−0.50−0.250.00

Now suppose the raw scaled dot products for that row are all equal, say 2.00 across the board, so content carries no preference at all. Add the bias: (1.00, 1.25, 1.50, 1.75, 2.00). Exponentiate: (2.718, 3.490, 4.482, 5.755, 7.389), which sum to 23.834. The attention weights are:

(0.114, 0.146, 0.188, 0.241, 0.310)

Purely from geometry, the nearest token gets 2.7 times the weight of the furthest. Ratios between adjacent weights are all e0.25 = 1.284. The bias turned a flat content signal into a clean exponential recency profile, and it did so with one number per head and zero parameters trained.

Concept and realization. The bias matrix is (H, L, L) in principle, but it is never stored: it is slope[h] * (j − i), computable inside the attention kernel from the two loop indices. FlashAttention implementations add it in registers while the tile is live. The KV cache holds raw, unmodified keys. Adding a token costs nothing extra.

See It: Slopes Across Heads

The heatmap is the causal bias matrix for one head: rows are query positions, columns are key positions, and the upper triangle is masked. Change the head and watch the visible band tighten or widen. The curve underneath is the resulting attention weight as a function of distance, which is where the geometric decay becomes obvious.

ALiBi Bias and the Field of View
Head (of 8) 2
When to reach for it: BLOOM (176B) trained with ALiBi at 2,048 tokens and served usefully well past that, and MPT-7B-StoryWriter fine-tuned at 65k and generated at 84k. The objects involved are eight fixed head slopes per layer and nothing else. Why ALiBi: the KV cache stores raw keys, so a streaming server can extend context at inference time with no retraining and no cache rewrite. Where RoPE fails in that same setting: past the training length, the low frequency planes reach rotation angles the model never saw, and quality degrades sharply unless you rescale the base and fine-tune.

ALiBi From Scratch

python
import math, torch

def alibi_slopes(n_heads):
    # geometric sequence with ratio 2 ** (-8 / n_heads)
    start = 2.0 ** (-8.0 / n_heads)
    return torch.tensor([start ** (h + 1) for h in range(n_heads)])

def alibi_bias(n_heads, L):
    pos = torch.arange(L)
    rel = (pos[None, :] - pos[:, None]).float()      # (L, L), j - i
    rel = rel.masked_fill(rel > 0, float("-inf"))       # causal mask
    return alibi_slopes(n_heads)[:, None, None] * rel   # (H, L, L)

b = alibi_bias(8, 5)
print(alibi_slopes(8))          # [0.5, 0.25, 0.125, ..., 0.0039]
print(b[1, 4])                  # [-1.00, -0.75, -0.50, -0.25, 0.00]

Row b[1, 4] is the hand-computed row above, to the digit. In a real kernel you never build b; you recompute slope * (j - i) from the tile indices, which is why ALiBi's memory overhead is exactly zero.

What ALiBi gives up. Because the penalty depends only on distance and not on direction within a plane, ALiBi cannot express "the token exactly three back matters, the one four back does not." It is monotone by construction. RoPE's rotations can encode phase, and therefore periodic and non-monotone patterns. That difference in expressiveness is exactly what Chapter 7 will call the difference between the rotation family and the ramp family.
Why does ALiBi give each attention head a different slope rather than one shared slope?

Chapter 5: What Encodings Are Even Possible?

We now have four working methods and no idea whether there are four hundred more waiting to be discovered. In April 2026 Alok Puranik, an ML researcher at Jane Street, spent a Friday afternoon on exactly that question and published the answer: Using group theory to explore the space of positional encodings for attention.

His framing, in his words: "what positional encodings are even possible? As I started investigating, I discovered to my surprise that the space is quite constrained." This chapter and the two after it walk his derivation. It needs nothing you do not already have.

Step 1: Write Down What a PE Even Is

We have a time series of queries q(t) and a time series of keys k(s). Time is written as a continuous variable so the argument covers irregularly sampled series and learned notions of time as in the Mamba family, but you can read t and s as plain integer indices if you prefer.

Without any encoding, attention computes the inner product q(t)Tk(s). We want to perturb that so it knows about position. To stay cheap, we do not modify each pair independently. Instead we transform the queries and the keys themselves with two explicitly time-dependent functions F and G:

q̃(t) = F(t) q(t),    k̃(s) = G(s) k(s)
score(t, s) = q̃(t)T k̃(s) = q(t)T F(t)TG(s) k(s)

Everything that follows is about that bold middle object. A positional encoding, under this framing, is the square matrix F(t)TG(s). Change nothing else and you have changed nothing.

Property 1: Linearity

F(t) is linear in q and G(s) is linear in k. Puranik is candid that this one is not rigorously justifiable: "we're working with a vector space, so it's only right that our encoding should be linear. We'll leave the study of nonlinear encodings to others."

Linearity is what lets us write F and G as square matrices at all. Drop it and the object F(t)TG(s) is not a matrix and the enumeration is hopeless. Note what it costs: ALiBi, which maps the dot product to qTk + m(t − s), is not literally of this form. Hold that thought until Chapter 7, where it comes back through a side door.

Property 2: Translation Invariance

For any times t, s and any shift r:

F(t + r)TG(s + r) = F(t)TG(s)

Shift both tokens later by the same amount and the score does not change. Only relative positions are observable. The practical motivation is the one we hit in Chapter 2: if the absolute index is observable, what do you do when you trained on sequences up to length L and now need to handle a longer one?

Here is the picture that makes it click. Lay out F(t)TG(s) for every pair (t, s) in a table. Translation invariance says every entry on a given diagonal is the same matrix, because moving one step down and one step right leaves the value alone.

The F(t)TG(s) Table

Left: 36 independent matrices, one per (t, s) pair. Right: what translation invariance leaves you with. Move the sliders and watch every cell on your diagonal light up with the same label.

Query time t 3
Key time s 1
Count what just happened. A 6 by 6 table of d by d matrices is 36 independent objects. Translation invariance collapses it to 11, one per diagonal. In the next chapter one more assumption collapses those 11 to a single d by d matrix. The design space shrinks by two orders of magnitude in two moves.

Step 2: Normalise, and Meet A(t)

One more harmless assumption: F(t)TG(t) = I. At equal times the positional modification simply drops out. This costs nothing, because if we had started with F(t)TG(t) = M for some fixed M, we could fold M into the keys by redefining k, and we already agreed to ignore time-independent transformations.

Two consequences follow immediately. F(t) and G(t) are inverses of each other, and F(t)TG(s) depends only on t − s. So define

A(t − s)  =  F(t)T G(s)

and the whole table becomes one function of one variable. A(0) sits on the main diagonal, A(1) one below, A(−1) one above.

The intuition, in Puranik's words. A(t − s) "says how much you should perturb the inner product when shifted by t − s. In other words, A(t − s) is a translation across time that brings the key from the past, at time s, to the present, at time t, where it can be compared to q." Once you see A as a time machine for keys, two facts become obvious rather than derived. Shifting by zero should be a no-op. Shifting by s and then by t should be the same as shifting by s + t.
A(0) = I   and   A(s + t) = A(s) A(t)

Worked Example: Filling In the Table

Abstractions are easier to trust once you have watched them hold with real numbers. Pick the simplest non-trivial F and G: two dimensional rotations by 30 degrees per step, so F(t) = R(30t) and G(s) = R(30s).

Then F(t)TG(s) = R(30t)TR(30s) = R(30(s − t)), because a rotation's transpose is its inverse and composing rotations adds their angles. Tabulate that for the first three times.

s = 0s = 1s = 2
t = 0R(0)R(30)R(60)
t = 1R(−30)R(0)R(30)
t = 2R(−60)R(−30)R(0)

Read the diagonals. The main diagonal is R(0), which is the identity, as our normalisation demanded. One step below is R(−30) everywhere. Two steps below is R(−60) everywhere. Nine cells, three distinct values, and each value is determined by t − s alone.

Now check the composition law numerically. R(−30) has entries 0.866, 0.500 on the top row and −0.500, 0.866 on the bottom. Multiply it by itself:

top-left: 0.866·0.866 + 0.500·(−0.500) = 0.750 − 0.250 = 0.500
top-right: 0.866·0.500 + 0.500·0.866 = 0.433 + 0.433 = 0.866

That is R(−60), which is exactly the entry two steps below the diagonal. A(1)·A(1) = A(2), checked by hand. Nothing about rotations was special here; the same collapse happens for any F and G obeying the two properties. The next chapter explains why.

When to reach for it: A clinical transformer over ICU vitals, where the inputs are irregularly sampled measurements and "position" is elapsed hours since the previous reading, not a row index. The vectors are per-reading feature embeddings (heart rate, mean arterial pressure, lactate) tagged with a real-valued time. Why translation invariance is the property to demand: a patient admitted at 3am and one admitted at noon should produce identical attention patterns for identical vital trajectories, so only the gaps between readings may be observable. Where an absolute index encoding fails: it makes "the fourteenth reading" meaningful, which conflates a patient sampled every five minutes with one sampled hourly, and it breaks entirely on a record with more readings than the training maximum.

Property 3: Continuity

A(t) is a continuous function of t. Puranik is blunt about why this is safe to assume: "There are discontinuous functions that satisfy our constraints, but they require the axiom of choice to describe and are far too deranged to implement on a physical computer."

Three properties, in order: linearity, translation invariance, continuity. That is the complete set of assumptions. Everything from here is consequence.

Sanity check against RoPE. RoPE sets F(t) = G(t) = R(tθ), a block of rotations. Then F(t)TG(s) = R(tθ)TR(sθ) = R((s − t)θ), which depends only on the offset. A(0) = I holds. A(s)A(t) = R(sθ)R(tθ) = R((s+t)θ) = A(s+t) holds. RoPE is a point in this space, and the derivation above is about to tell us how large the space around it really is.
You lay out the matrix F(t)TG(s) for every pair (t, s) in a grid. What does translation invariance say about that grid?

Chapter 6: A One-Parameter Group

We ended the last chapter with two facts about the key-translating matrix A(t):

A(0) = I   and   A(s + t) = A(s) A(t),   A continuous

A family of matrices satisfying exactly that is called a one-parameter group. And one-parameter groups are not merely well studied, they are completely classified. Every one of them has the same shape:

A(t) = exp(t L)

for a single fixed matrix L called the generator. That is the whole answer to "what positional encodings are possible." Pick a generator, and you have picked a positional encoding. There is nothing else to pick.

Why this is startling. Before this argument, a positional encoding felt like an open-ended design problem: any function of position at all. The design space just collapsed to one d by d matrix of numbers. RoPE, ALiBi, NoPE, exponential decay and everything anyone has shipped are single points in that space, and every unexplored corner is also a choice of L.

What exp of a Matrix Even Means

The scalar exponential has a power series that converges for every real x: ex = 1 + x + x²/2! + x³/3! + ... Matrices can be multiplied and added, so plug one in:

exp(M) = I + M + M²/2! + M³/3! + M4/4! + ...

That series converges for every square matrix. And the reason it is the right object here is a single identity: if two matrices commute (and tL and sL always do, being multiples of the same L), then exp(sL)·exp(tL) = exp((s+t)L). The group law is the exponential's defining property, transplanted from numbers to matrices.

Hand Calculation: exp of a Skew Matrix Is a Rotation

Take the smallest interesting generator, the 2 by 2 skew matrix

L = [ 0  −1 ;   1    0 ]

The trick is to compute its powers first. L² = −I. Therefore L³ = L²·L = −L, and L4 = I, and the cycle repeats with period 4. Substitute into the series and sort the terms into the ones carrying I and the ones carrying L:

exp(tL) = I(1 − t²/2! + t4/4! − ...) + L(t − t³/3! + ...)
= cos(t)·I + sin(t)·L

Written out, that is exactly the rotation matrix:

exp(tL) = [ cos t  −sin t ;   sin t    cos t ]

Check it numerically at t = 0.5236 (30 degrees). Term by term, starting from I:

terms kepttop-lefttop-right
I1.00000.0000
+ tL1.0000−0.5236
+ t²L²/20.8629−0.5236
+ t³L³/60.8629−0.4997
+ t4L4/240.8660−0.4997
exact0.8660−0.5000

Five terms and we are at cos 30 = 0.8660 and −sin 30 = −0.5000. RoPE is what you get when the generator is skew symmetric. Not by analogy. By computing an exponential.

Hand Calculation: exp of a Nilpotent Matrix Is a Shear

Now the other interesting small generator, the nilpotent matrix

N = [ 0   1 ;   0   0 ]

N² = 0 exactly, so every term past the second in the series vanishes. The exponential is a two-term polynomial:

exp(tN) = I + tN = [ 1   t ;   0   1 ]

Apply it to the vector [x, v]: you get [x + tv, v]. Puranik points out that this is not a pathology, it is physics. "Consider a frictionless hockey puck launched from position x with velocity v at time 0. At time t, its new position is x + tv, and its velocity remains v. The time evolution of this simple physical system is governed precisely by exp(tN)."

Verify the group law by hand: [1, s; 0, 1]·[1, t; 0, 1] = [1, s + t; 0, 1]. The offsets add. That is A(s)A(t) = A(s+t), and it is why the puck's position after s then t seconds equals its position after s + t seconds. Chapter 7 turns this exact matrix into ALiBi.

The Third Behaviour: Decay

If the generator has a real eigenvalue c, then on that one-dimensional subspace A(t) acts as multiplication by ect. Three cases, and Puranik disposes of them quickly.

eigenvalueA(t) on that axisVerdict
c > 0ect, growsThrown out. The influence of a key blows up exponentially as the gap grows, which is the opposite of what any sane model wants.
c = 01, constantThe identity on that axis. Do this everywhere and you have recovered NoPE, no positional encoding at all.
c < 0ect, decaysKept. This is the exponential forgetting that appears throughout linear attention, and combined with rotation it is the encoding used by RetNet and Mamba-3.

Two footnotes worth keeping. First, when a model learns a data-dependent decay factor, the right way to read it through this lens is not "the model changes the rate of decay" but "the model learns how far to advance time." Second, the decaying case only makes sense for causal attention. Without a mask, tokens far in the future would have exponentially increased influence on early queries.

Rotation, shear, decay. Three primitive behaviours, and every real generator is built out of them: rotations on 2-D blocks, decay on the diagonal, shears wherever the matrix is defective. Chapter 7 sorts the shipped encodings into those bins.

From a Real Matrix to Rotation and Decay

One gap worth closing. Real generators do not usually arrive as neat skew or nilpotent blocks. The blog's route is to diagonalise L, which is convenient because it chops the encoding into non-interacting pieces: the d-dimensional space becomes a direct sum of one dimensional spaces, and on each of them the encoding is just multiplication by a scalar exponential.

There is a wrinkle. Diagonalising a real matrix generally needs a complex basis, and the diagonal entries come out complex. But if L was real, the non-real eigenvalues arrive in conjugate pairs, so we take a slightly coarser decomposition: real eigenvalues get their own one dimensional subspace, and each conjugate pair gets a two dimensional one.

On a conjugate pair, write the eigenvalue in polar form as c + iω. Then on that two dimensional block,

A(t) = ect · R(ωt)

where R is the ordinary rotation matrix. Converting to that form is a real change of basis, which the blog calls the real canonical form, and we already agreed to ignore time-independent basis changes. So a real generator always decomposes into rotations at fixed frequencies, each optionally damped, plus pure decay on the real axes.

Hand check that the group law survives the damping. Take c = −0.1 and ω = 1. Then A(1) = e−0.1R(1) and A(2) = e−0.2R(2). Multiply A(1) by itself: the scalars give e−0.1·e−0.1 = e−0.2, and the rotations give R(1)R(1) = R(2). The product is A(2). Scalars multiply, angles add, and the one-parameter group property holds. This is the encoding the blog calls exponentially damped RoPE, and it names RetNet and Mamba-3 as the systems that use it.

The Payoff: One Slider, Every Encoding

This is the showcase. Pick a generator L, drag t, and watch A(t) = exp(tL) do its thing to a vector. The orbit is the path the vector traces as t sweeps. The matrix on the right is A(t) computed from the closed forms above, and the check line multiplies A(t/2) by itself to confirm the group law numerically.

The Generator Explorer
Offset t 1.20
Strength 1.00
When to reach for it: RetNet and Mamba-3 both use the mixed generator, a rotation combined with negative real decay, for causal decoder-only language modelling in the linear attention regime. The per-plane object is a complex number of modulus below one, applied to a running state rather than to an L by L score matrix. Why the mixed generator: linear attention needs a recurrence whose state stays bounded, and pure rotation leaves the state norm constant so old content never fades. Where pure RoPE fails there: with no decay the recurrent state accumulates every past token at equal weight and saturates, which is why RetNet writes its decay explicitly into the generator instead of hoping the network learns it.

The Awkward Case: Defective Generators

One loose end. The clean classification above assumed L is diagonalisable. Some matrices are not. A defective matrix is one whose eigenvectors do not span the space, and the nilpotent N above is the smallest example: its only eigenvalue is 0, twice, but it has a single eigenvector.

Defective generators are handled by putting L in Jordan normal form, and they behave differently from everything else. Puranik: "unlike diagonalizable matrices which produce only exponential and trigonometric factors, defective matrices give rise to positional encodings involving polynomial terms." We just saw the simplest polynomial, the linear term t in the shear. Bigger Jordan blocks give t², t³ and up.

Keep that in your pocket. It is the seed of both ALiBi and the one genuinely unexplored family, and Chapter 7 opens both.

Why does the property A(s + t) = A(s)A(t) force A to be a matrix exponential?

Chapter 7: The Families

Every positional encoding is exp(tL) for some generator L. Now we sort the generators. A real matrix decomposes into blocks, and there are only three kinds of block, so there are only three families.

Family 1: Rotations

Complex conjugate eigenvalue pairs give 2-D blocks that act as rotation. In real canonical form the block is the skew matrix from Chapter 6, and the exponential is a rotation by a constant angular frequency ω:

A(t)|block = ect · [ cos(ωt)  −sin(ωt) ;   sin(ωt)    cos(ωt) ]

With c = 0 that is exactly RoPE, one rotation per plane, one frequency per plane. With c < 0 it is exponentially damped RoPE, the encoding used by RetNet and Mamba-3. With c > 0 it blows up and gets thrown out. Everything in this family preserves or shrinks norms and gives a score that oscillates with offset.

Family 2: Unipotent Ramps

Defective blocks give polynomials. The smallest defective block is the nilpotent N, whose exponential is the shear I + tN. This is where ALiBi lives, and getting it there is a genuinely cute argument.

Puranik notes the objection first. Stefan Gliga pointed out on X that restricting to linear functions of q and k excludes ALiBi, "which applies a penalty to the attention dot product." True, and it looks fatal: ALiBi maps qTk to qTk + m(t − s), and adding a constant is not a linear map of q. But ALiBi is reachable if we allow ourselves to augment the queries and keys with two extra constant coordinates.

The construction. The first term qTk is free, so we only need to manufacture a dot product that evaluates to m(t − s). We need a generator that grows a component linearly in time. We have one: the nilpotent N, with exp(uN) = [1, u; 0, 1]. Now pick two fixed 2-D vectors to append. Append (m, 0) to the query and (0, 1) to the key.

Multiply it out by hand. With u = t − s:

exp(uN) · (0, 1) = (u, 1)
(m, 0) · (u, 1) = m·u = m(t − s)

Exactly the ALiBi bias, produced by a legal linear positional encoding generated by a defective matrix. Puranik's dry summary: "So, intentionally or otherwise, Stefan found a useful example of a positional encoding generated by a defective matrix."

Remember these two vectors. Query gets (m, 0), key gets (0, 1). In Chapter 9 the GRAPE paper writes the same construction as q̂ = [q; 1; 0] and k̂ = [k; 0; 1] with the slope moved into the generator instead of the query. Same lift, same nilpotent, same result, arrived at independently.

Family 3: The Strange Legal Class

The shear was the simplest defective block. Nothing stops L from having a 3 by 3 Jordan block, or a 4 by 4 block built from a doubled conjugate pair of complex eigenvalues. Those produce higher polynomials in t: a score term proportional to t², or to t³, or to t²cos(ωt).

This family is legal under all three properties. It is also, as far as anyone can tell, entirely unused. Puranik: "it's entirely unclear how to interpret these polynomials in the context of attention. I failed to find any existing literature which directly addresses the possibility of defective generators for positional encodings in deep learning, and most likely they have no practical application. Nevertheless, the curious possibility remains."

Notice why it is unattractive rather than illegal. A t² term means the influence of a key changes quadratically with the gap, so either it explodes faster than any softmax can absorb, or, if the sign points the other way, it suppresses everything past a short horizon far harder than a linear ramp does. Neither is obviously a good prior for language. But nobody has actually tried it.

See It: Three Families, One Axis

Each curve is the perturbation a family adds to the attention score as the offset grows. Rotation oscillates and never dies. Damped rotation oscillates inside a shrinking envelope. The unipotent ramp is a straight line, which after softmax is geometric decay. The polynomial curls away from all of them.

Score Perturbation vs Offset
Frequency ω 0.60
Decay c 0.10

The Whole Space, On One Page

FamilyGenerator blockA(t) on that block Score vs offsetShipped as
Rotation skew, eigenvalues ±iω rotation by ωt oscillates, norm preserved RoPE, sinusoidal in spirit, GRAPE-M
Damped rotation skew plus negative real part ect times rotation oscillates inside a decaying envelope RetNet, Mamba-3, xPos
Pure decay negative real eigenvalue ect monotone decay linear attention gates, FoX
Zero L = 0 identity flat NoPE
Unipotent ramp nilpotent, N² = 0 I + tN linear in the offset ALiBi, GRAPE-A
Higher defective Jordan block of size 3 or more polynomial in t t² and beyond nothing, as far as anyone knows
When to reach for this table: An architecture review at a lab about to propose a new positional encoding. The concrete artefact is a candidate formula on a whiteboard. Why this table earns its place: it turns "is this new?" into a mechanical check, namely write down the generator and read off which row it lands in. Where intuition fails instead: xPos, RetNet decay and NTK base rescaling all feel like separate inventions, and without the generator view a team can spend a quarter rediscovering row two.
The reassuring conclusion. Puranik's own summary is worth quoting in full: the analysis shows "that there are only a few families of valid positional encoding" and "actually all of the sensible ones are already being used in real systems. It was a reassuring finding, because it means that we don't need to rack our brains to come up with some perfect positional encoding, as we are probably already using it."
Which family does ALiBi belong to, and what makes its generator unusual?

Chapter 8: GRAPE, Rotations You Can Learn

The blog proves the space of positional encodings is small. A paper published at ICLR 2026 builds the same object on purpose and puts learnable parameters in it: Group Representational Position Encoding by Yifan Zhang, Zixiang Chen, Yifeng Liu, Zhen Qin, Huizhuo Yuan, Kangping Xu, Yang Yuan, Quanquan Gu and Andrew Chi-Chih Yao. GRAPE for short.

The two sources are one idea seen from two sides. The blog asks which encodings are legal and answers "only exp(tL)." GRAPE asks how to walk that space with gradient descent and answers "parameterise L and learn it." The blog is a classification. GRAPE is a constructor. Puranik added an addendum acknowledging the paper after Yifan Zhang pointed out the connection, and notes that GRAPE "attacks a closely related problem" using "essentially the same framework of one-parameter groups."

GRAPE's central object is exactly the one we derived:

G(n) = exp(n ω L)

with n the position, ω a frequency, and L the generator. Two choices of L give the two halves of the framework. A skew-symmetric L in so(d) gives Multiplicative GRAPE, rotations in the special orthogonal group SO(d), which is this chapter. A nilpotent L gives Additive GRAPE, unipotent actions in the general linear group GL, which is the next.

The Rank-2 Generator

The smallest non-trivial skew generator rotates inside a single plane, and you build it from the two vectors that span that plane. For a, b in d dimensions:

L = a bT − b aT

Check it is skew: transpose the whole thing and you get b aT − a bT = −L. So L lies in so(d), the Lie algebra of skew-symmetric matrices, which is exactly the set of generators whose exponentials are rotations. Define the plane scalars

α = ‖a‖²,   β = ‖b‖²,   γ = aTb,   Δ = αβ − γ²,   s = √Δ

Δ is the squared area of the parallelogram a and b span, so s is that area. It will turn out to be the rotation angle per unit of nω, which is a satisfying thing for an area to be.

The One Fact That Makes It Cheap

Apply L twice and something clean happens. GRAPE's key algebraic observation:

L² = −s² PU,   U = span{a, b}

where PU is the orthogonal projector onto the plane U. In words: squaring L flattens everything onto the action plane and scales by −s². Its eigenvalues are ±is and then zero, d − 2 times, and its minimal polynomial is λ(λ² + s²).

Because the minimal polynomial has degree three, every power of L past the second reduces to a combination of I, L and L². So the infinite exponential series collapses to three terms. This is the Rodrigues formula, the same one used for 3-D rotations in robotics since 1840:

exp(L) = I + (sin s / s) L + ((1 − cos s) / s²) L²

Substituting L² = −s²PU makes the geometry explicit:

exp(L) = I − (1 − cos s) PU + (sin s / s) L
Read that as a sentence. Start with the identity. Inside the plane U, rotate by angle s. Outside U, on the whole orthogonal complement, do absolutely nothing. A rank-2 generator is a rotation in one plane and the identity everywhere else. That is the entire content of the formula.

Hand Calculation: A Plane RoPE Cannot Reach

Take d = 4 and pick the plane spanned by coordinates 1 and 3, which is exactly what canonical RoPE cannot do, since RoPE always pairs (1,2) and (3,4).

Setup. a = (1, 0, 0, 0), b = (0, 0, 1, 0). Then α = 1, β = 1, γ = 0, Δ = 1, s = 1. Take nω = 0.5236, which is 30 degrees.

Rotate the vector x = (1, 0, 0, 0), which is a itself. Two inner products first: u = a·x = 1 and v = b·x = 0.

Now L·x = a(b·x) − b(a·x) = a·0 − b·1 = −b = (0, 0, −1, 0). And PUx = x, because x lies in the plane.

Plug into the geometric form with angle nωs = 0.5236:

exp(nωL)x = x − (1 − 0.8660)·x + (0.5000 / 1)·(−b)
= 0.8660·(1,0,0,0) − 0.5000·(0,0,1,0)
= (0.866, 0, −0.500, 0)

Norm check: 0.866² + 0.500² = 0.750 + 0.250 = 1.000. Preserved exactly, as an SO(d) element must. Coordinates 2 and 4 never moved. A learned generator just coupled dimension 1 with dimension 3 by rotation, and canonical RoPE has no way to express that.

Cost: O(d), Never Materialised

GRAPE never builds the d by d matrix G(n). Applying it to a vector x is:

u = ⟨a, x⟩,   v = ⟨b, x⟩
y = x + f1(n)(a v − b u) + f2(n)[γ(a v + b u) − β a u − α b v]

with f1 = sin(nωs)/s and f2 = (1 − cos(nωs))/s², guarded by their Taylor series as s approaches 0. Two inner products and a handful of scaled vector adds: O(d) flops per head, with a small constant.

Run our hand example through it as a check. u = 1, v = 0, γ = 0, α = β = 1, f1 = 0.5000, f2 = 0.1340. y = x + 0.5(a·0 − b·1) + 0.134[0 − a·1 − 0] = x − 0.5b − 0.134a = (0.866, 0, −0.5, 0). The same answer.

Why this matters against the alternative. LieRE also learns rotation generators, but it parameterises a dense skew-symmetric matrix and calls a numerical matrix exponential, which the paper puts at O(d³) time and O(d²) parameters per head. GRAPE decomposes into rank-2 planes with the closed form above, so it needs only vector-vector operations at O(d) per head. Same expressiveness class, three orders of magnitude apart in cost at d = 128.

The b = J a Shortcut

Let J be the canonical 90-degree rotation, block diagonal with JT = −J and J² = −I. Setting b = J a means "b is a rotated a quarter turn." Then γ = aTJ a = 0 and β = α, so s = α and the whole thing simplifies to a pure planar rotation by angle nωα. If you normalise ‖a‖ = 1, the angle is just nω. The paper calls this frequency-norm coupling: without normalisation the effective frequency is ω‖a‖², so the scale of a can always be absorbed into ω.

Multi-Subspace: RoPE Falls Out

One rank-2 generator touches one plane and leaves d − 2 dimensions alone. To cover the whole head, sum d/2 of them on mutually orthogonal planes. Orthogonal planes commute, so their exponentials multiply cleanly:

L = ∑i θi Li,   [Li, Lj] = 0
G(n) = exp(nL) = blockdiag( R(nθ1), ..., R(nθd/2) )

Stare at the right-hand side. A block diagonal product of planar rotations with per-plane angles nθi. That is the definition of RoPE. The paper states it as Proposition 3.1: choose d/2 mutually orthogonal vectors ai, set bi = J ai, and the commuting product equals the standard RoPE map in a possibly learned orthogonal basis. Make the planes the canonical coordinate pairs and the frequencies log-uniform, and you recover canonical RoPE exactly.

RoPE is not an analogy to GRAPE. It is a value of its parameters. Fixed canonical planes plus a fixed log-uniform spectrum. GRAPE frees both: learn the frequencies θi, and replace the coordinate selectors with a learned orthogonal basis B in SO(d) so L = ∑i θi B Ui J UiT BT. Commutativity survives, so the exact relative law survives, and the cost stays O(d).

Letting the Planes Interact

Commuting planes never talk to each other. To let them, GRAPE uses a thin compression: write L = E Lr ET with E an orthonormal d by r frame and Lr a small r by r skew matrix. The spectrum of L is the spectrum of Lr padded with zeros, so the expressive power of a non-commuting mixture is fully captured by r/2 mode angles. Cost rises to O(rd) per head, still linear in d.

The reason to bother: cross-subspace feature coupling. In canonical RoPE, dimension 5 can never be rotated into dimension 40 no matter what the data says. With a learned or non-commuting generator it can.

Inside Multi-Head Attention

Concretely, with H heads and per-head width d, the operator Gh(t) in SO(d) is applied to the query and the key of head h at position t, and the value is left alone:

q̃t,h = Gh(t) qt,h,   k̃t,h = Gh(t) kt,h,   ṽt,h = vt,h

The logit is the usual scaled dot product of the transformed pair. Because Gh comes from a one-parameter subgroup, Gh(t)TGh(j) equals Gh(j − t) and the logit depends only on the offset. Every head can carry its own generator, so one head can rotate slowly in a learned plane while another rotates fast in a different one.

Two shape facts worth holding onto. The learned parameters are two d-vectors per head, so 2·H·d numbers, which for a 32-head model with d = 128 is 8,192 numbers per layer. Nothing about the attention output projection, the value path, or the residual stream changes. A GRAPE-M layer is a drop-in replacement for a RoPE layer.

Spectral parameterisation is a free knob. Classical RoPE fixes the per-plane frequencies on a log-uniform grid. In GRAPE they can be learned, shared across heads, or tied across layers, and the exact relative law does not care which you choose, because it comes from the group structure alone and not from the spectrum. That is a rare thing in architecture design: a degree of freedom you can open without paying for it in correctness.

See It: Tilt the Plane

The grid shows a 4-D head as its two coordinate pairs. RoPE mode rotates inside the fixed pairs and the cross terms stay dark. Learned mode tilts the action plane so a rotation in one plane leaks into another dimension, which is the cross-subspace coupling the paper is after. The bars show the output vector.

Rank-2 Generator and Its Action Plane
Position nω 0.52
Plane tilt 0.90

The Same Group Action, Three Ways

Ladder two of three: the general exp(nωL) action for a rank-2 generator, closed form, no scipy.linalg.expm anywhere. All three rungs print the hand-computed vector from earlier in the chapter.

python  ·  rung 1: plain Python, no libraries
import math

def grape_m_plain(x, a, b, n, omega):
    dot = lambda p, q: sum(pi*qi for pi, qi in zip(p, q))
    al, be, ga = dot(a, a), dot(b, b), dot(a, b)
    s = math.sqrt(max(al*be - ga*ga, 0.0))
    th = n * omega * s
    if s < 1e-8:                          # series guard as s -> 0
        f1, f2 = n * omega, 0.5 * (n*omega)**2
    else:
        f1, f2 = math.sin(th) / s, (1 - math.cos(th)) / (s*s)
    u, v = dot(a, x), dot(b, x)
    return [
        x[i]
        + f1 * (a[i]*v - b[i]*u)
        + f2 * (ga*(a[i]*v + b[i]*u) - be*a[i]*u - al*b[i]*v)
        for i in range(len(x))
    ]

a = [1., 0., 0., 0.]; b = [0., 0., 1., 0.]
print([round(z, 3) for z in grape_m_plain([1.,0.,0.,0.], a, b, 1, 0.5236)])
# [0.866, 0.0, -0.5, 0.0]   the hand calculation, exactly
python  ·  rung 2: NumPy, a whole sequence of positions
import numpy as np

def grape_m_numpy(x, a, b, n, omega):
    # x: (L, d)   n: (L,)   a, b: (d,)
    al, be, ga = a @ a, b @ b, a @ b
    s = np.sqrt(max(al*be - ga*ga, 0.0))
    th = n * omega * s                          # (L,)
    f1 = np.where(s < 1e-8, n*omega, np.sin(th) / max(s, 1e-8))
    f2 = np.where(s < 1e-8, 0.5*(n*omega)**2, (1 - np.cos(th)) / max(s*s, 1e-16))
    u = x @ a; v = x @ b                        # (L,) each
    return (x
        + f1[:, None] * (np.outer(v, a) - np.outer(u, b))
        + f2[:, None] * (ga*(np.outer(v, a) + np.outer(u, b))
                        - be*np.outer(u, a) - al*np.outer(v, b)))

x = np.array([[1., 0., 0., 0.]])
print(grape_m_numpy(x, np.array([1.,0.,0.,0.]),
                   np.array([0.,0.,1.,0.]), np.array([1.]), 0.5236).round(3))
# [[ 0.866  0.    -0.5    0.   ]]
python  ·  rung 3: PyTorch, learnable a and b, batched heads
import torch, torch.nn as nn

class GrapeM(nn.Module):
    """One rank-2 learned plane per head. No matrix is ever built."""
    def __init__(self, n_heads, dim, omega=1.0):
        super().__init__()
        self.a = nn.Parameter(torch.randn(n_heads, dim) / dim**0.5)
        self.b = nn.Parameter(torch.randn(n_heads, dim) / dim**0.5)
        self.omega = omega

    def forward(self, x, pos):
        # x: (B, H, L, d)   pos: (L,)
        a, b = self.a[None, :, None, :], self.b[None, :, None, :]
        al = (a*a).sum(-1, keepdim=True)
        be = (b*b).sum(-1, keepdim=True)
        ga = (a*b).sum(-1, keepdim=True)
        s  = (al*be - ga*ga).clamp(min=1e-12).sqrt()      # (1,H,1,1)
        th = pos[None, None, :, None] * self.omega * s     # (1,H,L,1)
        f1, f2 = th.sin() / s, (1 - th.cos()) / (s*s)
        u = (x*a).sum(-1, keepdim=True)                  # (B,H,L,1)
        v = (x*b).sum(-1, keepdim=True)
        return (x + f1*(a*v - b*u)
                  + f2*(ga*(a*v + b*u) - be*a*u - al*b*v))

Shapes to notice in rung three. The learned parameters are two vectors per head, 2·H·d numbers total, against LieRE's H·d². The forward pass allocates nothing bigger than x itself. And because G(n)TG(n) = I by construction, the rotated keys can be written straight into a KV cache and never touched again, which is the streaming property Chapter 9 makes explicit.

When to reach for it: A vision or robotics model where the input is not a line. GRAPE's related-work section points at 2-D RoPE for high resolution ViTs, LieRE for n-dimensional inputs, and STRING for 2-D and 3-D coordinates in vision and robotics. The vectors are per-head patch queries and keys, and the positions are (row, column) or (x, y, z) rather than an index. Why a learned rank-2 generator: the right rotation planes for spatial data are not the arbitrary coordinate pairs the projection happened to produce, and a rank-2 parameterisation lets gradient descent find them at O(d). Where canonical RoPE fails: it hard-codes which dimensions pair up, so any spatial structure that does not align with that pairing has to be relearned inside the weights.
In what precise sense is RoPE a special case of Multiplicative GRAPE?

Chapter 9: GRAPE, Ramps Are Group Actions Too

ALiBi adds a number to the logit. That does not look like a group action at all: group actions multiply, biases add. GRAPE's second half shows the appearance is wrong, using the same trick computer graphics has used since the 1970s.

The Homogeneous Lift

A translation is not a linear map. Shifting every point by a fixed vector moves the origin, and linear maps must fix the origin. The standard fix is to add one coordinate, pin it to 1, and do the translation as a shear in the larger space.

x̂ = [x ; 1],   A = [ 0d×d   u ;   0T   0 ],   A² = 0
exp(nωA) x̂ = (I + nωA) x̂ = [x + nωu ; 1]

Read the middle line. A is nilpotent, so its exponential truncates after two terms exactly as in Chapter 6. The result is unipotent, meaning all of its eigenvalues equal 1. And it is still a one-parameter group: Gadd(n + m) = Gadd(n)Gadd(m), because the nωu terms simply add.

So an additive bias is a multiplicative group action in disguise. Nothing was fudged. We are still inside exp(tL); we just chose a nilpotent L in a space one or two dimensions bigger than the head. That is precisely the family the blog reached from the other direction in Chapter 7.

Applying It, and Why the Inverse Transpose

GRAPE applies the group element to the query and the inverse transpose to the key:

q̃i = Gadd(i) q̂i,   k̃j = Gadd(j)−T k̂j

Then q̃iTk̃j = q̂iT Gadd(j − i)−T k̂j, which depends only on j − i. In SO(d) the transpose is the inverse, so this pairing is invisible in Chapter 8; in GL it is not, and you have to write the inverse transpose explicitly to keep the relative law exact.

Exact ALiBi as a Rank-1 Unipotent in GL(d+2)

Now recover ALiBi on the nose. Augment with two constant coordinates, one carried by the query and one by the key:

q̂i = [qi ; 1 ; 0],   k̂j = [kj ; 0 ; 1]
AhT = −βh ed+1 ed+2T,   (AhT)² = 0

The lift is asymmetric on purpose: the extra coordinates are 1 and 0 for the query, 0 and 1 for the key, so the base inner product is unchanged, q̂Tk̂ = qTk with no extra constant. Then Gadd,h(m)−T = I − m AhT and the score is

q̂iT Gadd,h(j − i)−T k̂j = qiTkj + (j − i)βh

Hand Calculation: The Chapter 4 Row, Rebuilt from the Lift

Take head 2 again, β = 0.25, query at i = 4, and suppose every raw score qTk equals 2.00 as before. Only one entry of the rank-1 update ever fires: the query supplies a 1 in coordinate d+1, the key supplies a 1 in coordinate d+2, and every other product is zero. So the extra term is exactly (j − i)β.

key j01234
m = j − i−4−3−2−10
m·β−1.00−0.75−0.50−0.250.00
final logit1.001.251.501.752.00

Identical to the ALiBi row we computed in Chapter 4, digit for digit. In causal attention j is never greater than i, so m is never positive and the term is always a penalty, never a reward.

Why two extra coordinates and not one. The paper is explicit about this. General additive biases such as key-gated slopes fit in GL(d+1). Exact ALiBi, whose bias is entirely content-independent, needs the (d+2) lift: generating a non-zero scalar bias from a nilpotent generator requires the interaction of two distinct constant coordinates, one for the query and one for the key, to avoid self-loops that would violate the trace-zero property of the generator.

Making the Slope Content-Dependent

ALiBi's slope is a constant per head. GRAPE lets the tokens choose it, without breaking anything, by scaling the same nilpotent basis with non-negative gates:

λq(qi) = softplus(vTqi / √d),   λk(kj) = softplus(uTkj / √d)
score = qiTkj + (j − i) ω [ λq(qi) + λk(kj) ]

The paper calls this GRAPE-A-QK. Because both gates scale the same nilpotent basis, the generators still commute, so the group structure and the exact relative law survive. The softplus guarantees the rate is non-negative, so with j at most i the term is always a penalty. Two learned vectors u and v per head buy a slope the model can adapt per token pair.

When to reach for it: A long-document reader where "how far back matters" is not constant. A token opening a quotation should keep looking back until the quote closes; a token in the middle of boilerplate should not. The learned objects are two d-vectors per head plus the frequency ω. Why a gated slope: ALiBi's fixed per-head slope forces one recency prior for the entire corpus, and the corpus does not have one. Where fixed ALiBi fails: raise the slope enough to sharpen local syntax and you cut the long-range heads; lower it and the local heads smear.

FoX Falls Out Too

The Forgetting Transformer learns a per-head, per-token forget gate ft in (0, 1] and adds the cumulative log-gate to the logits: Dij = ∑ℓ=j+1i log fℓ. Set ωt = log ft and the additive bias b(t, j) = ∑ℓ=j+1t ωℓ coincides with FoX's bias exactly. GRAPE proves FoX is an exact instance of the path-integral additive case with endpoint-independent edges, and when the gates are constant per head, FoX reduces to exact ALiBi.

Three published methods, one generator. ALiBi is the constant-slope nilpotent. FoX is the same nilpotent with a learned per-token rate. GRAPE-A-QK is the same nilpotent with a rate gated by both endpoints' content. They differ in how ω is produced, not in what group they live in.

Streaming: What the KV Cache Sees

The reason all of this is practical is a cache policy that never rewrites. Because the group element attached to a token depends only on that token's own position, you transform the key once, when the token arrives, and store the transformed vector:

cache k*j = G(j) kj  when token j arrives
at step t: q̃t = G(t) qt,   logit = q̃tT k*j

When t increments, nothing in the cache needs touching. The group law does the bookkeeping: G(t)TG(j) is automatically G(j − t). The complexity matches RoPE, and the same policy holds for the additive family.

Concept and realization. This is the single most important implementation fact on the page. A KV cache is (B, H, L, d) and can be tens of gigabytes. Any scheme that requires rewriting cached keys as the sequence grows is dead on arrival for serving, because it turns a constant-cost append into a linear-cost rewrite of the whole cache on every token. Group structure is what buys write-once.

See It: Write Once, Read Many

Press play. Tokens arrive on the left and each key is transformed exactly once at write time, then frozen (the lock appears). The query at the current step is transformed and dotted against the frozen cache. Switch to the "needs rewrite" mode to see what a non-group scheme would cost: every cached key lights up again on every step.

KV Cache Under a Group Action

Composing the Two Halves

Nothing forces you to choose between rotating and biasing. The two families compose, and GRAPE gives two equivalent ways to say so.

The easy way is at the logit level. Rotate the query and key with a multiplicative generator, then add the additive bias to the resulting score:

logit = qtT Gh(j − t) kj / √d  +  (j − t) ω [ λq + λk ]

The rigorous way is a single group action in GL(d+2): take the direct sum of the skew generator acting on the original d dimensions and the gated nilpotent generator acting on the two lifted coordinates. Because they live on disjoint coordinate blocks they commute, so the exponential of the sum is the product of the exponentials, and the composed map is still a one-parameter group with the same exact relative law and the same write-once cache policy.

Why anyone would want both. Rotation supplies phase, which lets a head express "the token exactly four back," and it preserves norms so logits do not drift. The additive ramp supplies a monotone recency prior, which is what actually carries long-range behaviour and length extrapolation. They answer different questions, and the group view says you may have both for the price of adding two generators.

The Same Bias, Three Ways

Ladder three of three: the additive family, from the definition to a production layer. All three print the hand table above.

python  ·  rung 1: plain Python, straight from the lift
def additive_score_plain(q, k, i, j, beta):
    # homogeneous lift: q_hat = [q, 1, 0], k_hat = [k, 0, 1]
    q_hat = list(q) + [1.0, 0.0]
    k_hat = list(k) + [0.0, 1.0]
    m = j - i
    # G(m)^-T = I - m * A^T, with A^T = -beta * e_{d+1} e_{d+2}^T
    # so the ONLY extra term is q_hat[d] * (m * beta) * k_hat[d+1]
    base  = sum(x*y for x, y in zip(q_hat, k_hat))
    extra = q_hat[-2] * (m * beta) * k_hat[-1]
    return base + extra

q = [1.0, 1.0]; k = [1.0, 1.0]        # raw dot product = 2.00
print([round(additive_score_plain(q, k, 4, j, 0.25), 2) for j in range(5)])
# [1.0, 1.25, 1.5, 1.75, 2.0]
python  ·  rung 2: NumPy, the whole causal matrix
import numpy as np

def additive_bias_numpy(L, slopes):
    pos = np.arange(L)
    m = pos[None, :] - pos[:, None]              # (L, L) = j - i
    bias = slopes[:, None, None] * m[None]         # (H, L, L)
    return np.where(m[None] > 0, -np.inf, bias)   # causal

scores = np.full((1, 5, 5), 2.0)
out = scores + additive_bias_numpy(5, np.array([0.25]))
print(out[0, 4])                                # [1.   1.25 1.5  1.75 2.  ]
python  ·  rung 3: PyTorch, content-gated slope (GRAPE-A-QK)
import torch, torch.nn as nn, torch.nn.functional as F

class GrapeA(nn.Module):
    def __init__(self, n_heads, dim, omega=1.0):
        super().__init__()
        self.v = nn.Parameter(torch.zeros(n_heads, dim))   # query gate
        self.u = nn.Parameter(torch.zeros(n_heads, dim))   # key gate
        self.omega = omega

    def forward(self, q, k, logits):
        # q, k: (B, H, L, d)   logits: (B, H, L, L)
        d = q.shape[-1]
        lam_q = F.softplus((q * self.v[None, :, None, :]).sum(-1) / d**0.5)
        lam_k = F.softplus((k * self.u[None, :, None, :]).sum(-1) / d**0.5)
        L = q.shape[-2]
        pos = torch.arange(L, device=q.device)
        m = (pos[None, :] - pos[:, None]).float()          # (L, L)
        rate = lam_q[..., :, None] + lam_k[..., None, :]      # (B, H, L, L)
        return logits + self.omega * m * rate               # penalty, m <= 0

Set v = u = 0 in rung three and softplus(0) = 0.693 for both gates, giving a constant slope: you are back at ALiBi with β = 1.386ω. The learned version is a strict superset, which is the entire pitch of the additive half of GRAPE.

Why does GRAPE call an additive attention bias a group action, when adding a number is not multiplying by a matrix?

Chapter 10: Length Extrapolation

Everything so far has been about correctness at training length. This chapter is about the thing that actually breaks in production: you trained at 4,096 tokens and a user pastes 60,000.

Why RoPE Breaks, Precisely

RoPE is relative by construction, so it should extrapolate. It does not, and the reason is not the mechanism, it is the training distribution.

Take a Llama-shaped head: d = 128, so 64 planes, base 10,000. The slowest plane is i = 63, with θ = 1 / 10000126/128 = 1 / 8660 = 0.0001155 radians per token.

Hand calculation. Over a 4,096-token training context, that slowest plane sweeps 4096 × 0.0001155 = 0.473 radians, which is 27 degrees. Barely a slice of the circle. At position 32,768 it would need to sweep 3.78 radians, or 217 degrees, over half a turn into a region the model has never seen a single training example for.

That is the failure. Not that the formula stops working, but that the low-frequency planes present rotation angles that are out of distribution. The dot product formula is still exact; the weights that consume it were never fit on those inputs.

Compare the fastest plane, i = 0, with θ = 1. It completes a full turn every 6.28 tokens, so across 4,096 tokens it wraps 652 times. Every possible angle appeared thousands of times in training. High frequencies are saturated with data; low frequencies are starved. Any fix has to treat them differently, and every good fix does.

See It: The Arena

Each row is a method, each column an evaluation length. Colour is modelled perplexity quality relative to the training length: teal is holding up, warm is degrading, red is broken. The vertical line marks where training stopped. Drag it and watch the picture reorganise.

Length Extrapolation Arena
Train length 4k

What the Arena Shows

MethodInside training length2× beyond8× beyondThe reason
Learned absolute StrongFails immediatelyFails No row exists past max_len; nothing to look up
Sinusoidal StrongDegradesPoor Defined everywhere, but the attention weights that decode it were never fit on those patterns
RoPE, base 10k StrongDegradesPoor Low-frequency planes reach unseen angles
RoPE plus NTK base StrongGoodUsable Slow planes rescaled so their angle range stays inside what training covered
ALiBi GoodGoodGood, bounded The penalty is defined for any distance and is monotone, so nothing is out of distribution
Relative bias buckets StrongGoodFlat Everything past the last bucket collapses into one shared parameter

Two lessons sit in that table. First, being relative is necessary but not sufficient: sinusoidal and RoPE both encode relativity and both still degrade, because the values they present go out of range. Second, methods that clamp or saturate (ALiBi, bucketed bias) extrapolate gracefully precisely because they throw information away past a horizon.

Fix 1: Position Interpolation

Chen et al. (2023) had the blunt idea. If position 32,768 is out of range, lie about it. Divide every position by the extension factor s so that a 32,768-token sequence presents positions in the range the model already knows.

θ'(pos) = θ · (pos / s),   s = Lnew / Ltrain

It works, with a cost. Every plane gets squeezed, including the fast ones that were perfectly happy. Adjacent tokens that used to be 1 radian apart in the fast plane are now 1/8 of a radian apart, so the model's ability to tell neighbours apart degrades. Position interpolation needs fine-tuning to recover, typically a thousand steps or so.

Fix 2: NTK-Aware Base Scaling

The insight that makes interpolation nearly free: do not scale positions, scale the base. Because the base sets a geometric ladder of frequencies, raising it stretches the slow planes hard and the fast planes almost not at all, which is exactly the treatment the analysis above asked for.

base' = base · sd/(d−2)
Hand calculation. d = 128, extending 4,096 to 32,768, so s = 8. The exponent is 128/126 = 1.0159. 81.0159 = e1.0159 · ln 8 = e2.1124 = 8.27. So base' = 10000 × 8.27 = 82,700.

Check both ends of the ladder.

Planeθ at base 10,000θ at base 82,700Effect
i = 0 (fastest)1.01.0Untouched, because 100000 = 827000 = 1
i = 32 (middle)0.01070.0038Slowed 2.8 times
i = 63 (slowest)0.00011550.0000145Slowed 7.96 times, essentially the full factor of 8

That last row is the whole trick. The slowest plane got divided by almost exactly the extension factor, so over 32,768 tokens it now sweeps 32768 × 0.0000145 = 0.475 radians, the same 27 degrees it saw during training. The fastest plane kept its full resolution. Nothing is out of distribution and nothing lost precision.

Frequency Ladder Under Base Rescaling

Each bar is one plane. Its length is the total angle that plane sweeps across the evaluation length. Anything past the dashed line is angle the model never saw in training.

Base 10000
Eval length 32k

Fix 3: YaRN, Per Frequency

YaRN (Peng et al., 2023) makes the treatment explicit instead of implicit. Sort the planes by how many full cycles they complete inside the training context, then apply three different policies:

Plane groupCriterionPolicyWhy
High frequency wavelength much shorter than Ltrain Extrapolate, leave alone Every angle already appeared thousands of times; squeezing only costs resolution
Low frequency wavelength longer than Ltrain Interpolate fully, divide by s These never completed a period, so their angles at long range are pure extrapolation
Middle in between Ramp smoothly between the two A hard switch creates a discontinuity in the spectrum that shows up as a quality notch

YaRN adds one more piece: a temperature on the attention logits, because spreading positions changes the expected magnitude of the scores and the softmax needs rescaling to keep its entropy where training left it.

When to reach for it: Extending an already-trained 8k model to 128k without a full retrain, which is the situation every team inherits at some point. The artefacts are a config change (rope_theta) and a short fine-tuning run of a few hundred steps on long documents. Why NTK or YaRN: they preserve the high-frequency resolution that local syntax depends on while fixing only the starved low-frequency planes. Where naive position interpolation fails: it squeezes every plane equally, so the model loses the ability to distinguish adjacent tokens and needs much more fine-tuning to claw it back.
What Llama 3 actually did. Trained with RoPE base 500,000 rather than 10,000 from the start. Read that through the ladder above: a fifty times larger base makes every slow plane fifty times slower, so all of them stay inside a small, well-covered arc of the circle even at very long contexts. Paying for long context in the base at training time is cheaper than patching it afterwards.
python  ·  the YaRN per-frequency ramp
import math, torch

def yarn_inv_freq(dim, base, scale, L_train,
                   alpha=1.0, beta=32.0):
    """alpha, beta: how many full turns a plane must complete inside
    L_train before we stop interpolating it. Below alpha turns the plane
    is starved and gets the full 1/scale; above beta it is saturated and
    is left alone; in between it ramps."""
    i = torch.arange(0, dim, 2).float()
    inv = 1.0 / (base ** (i / dim))            # (d/2,) untouched freqs
    turns = L_train * inv / (2 * math.pi)         # cycles inside training ctx

    # ramp: 0 where starved (interpolate fully), 1 where saturated
    ramp = ((turns - alpha) / (beta - alpha)).clamp(0.0, 1.0)

    interp = inv / scale                            # squeeze by the factor
    return interp * (1 - ramp) + inv * ramp        # blend, per plane

f = yarn_inv_freq(128, 10000.0, scale=8.0, L_train=4096)
print(float(f[0]), float(f[-1]))
# 1.0                    fastest plane: untouched, 652 turns in training
# 1.4435e-05             slowest plane: divided by 8, it never finished a turn

Read the two printed numbers against the hand calculation above. The fastest plane keeps its frequency of 1.0 exactly, because it completed 652 turns during training and needs no help. The slowest plane drops from 0.0001155 to 0.00001444, a factor of 8, because it swept only 27 degrees and every angle past that is unseen. The ramp decides where the boundary sits, and the two constants alpha and beta are the only tuning YaRN really asks for.

python  ·  the entire NTK fix
def ntk_base(base, dim, scale):
    # scale = new_context / train_context, e.g. 32768 / 4096 = 8
    return base * scale ** (dim / (dim - 2))

print(round(ntk_base(10000, 128, 8)))       # 82685

# In practice this is one line in the model config:
#   "rope_theta": 82685
# followed by a few hundred fine-tuning steps on long documents.
Why does raising the RoPE base help length extrapolation more than dividing every position by the extension factor?

Chapter 11: Choosing and Shipping a PE

Theory says the space is small. Engineering says pick a point in it and live with the consequences. This chapter is the decision, the checklist, and the list of things that actually go wrong.

The Picker

Answer three questions and read the recommendation. This is not a flowchart of boxes; it is the actual decision, with the reason and the runner-up, because the runner-up is what you switch to when the first choice breaks.

Position Encoding Picker
1. What shape is the task?
2. Will you serve past the training length?
3. Can you retrain or fine-tune?

The Checklist

One row per method. Read the second column first: it is the discriminating property that decides everything else.

Method Generator (what group element) Relative by construction? Core question it answers Reach for it when Cost per head
Sinusoidal None; an absolute additive code No Where am I in the sequence? Fixed-length windows, zero parameter budget Free, a cached buffer
Learned absolute None; a lookup table No What should this exact index mean? Short fixed context, encoder tasks, BERT-shaped models max_len × d parameters
Relative bias (T5) None; a learned bias on the logit Yes, by construction How far apart, in learned buckets? Encoder-decoder, wide range of input lengths, moderate context An (H, L, L) tensor before softmax
RoPE Skew, canonical planes, log-uniform spectrum Yes, exactly What is the phase difference between us? Default for causal LMs; anything using FlashAttention O(d), one multiply-add
ALiBi Nilpotent, rank 1, constant slope Yes, exactly How much should distance alone cost? Unbounded streaming, serving past train length with no retrain O(1), computed in registers
GRAPE-M Skew, learned planes, learned spectrum Yes, exactly Which subspaces should couple as position moves? Non-1-D inputs, or when fixed coordinate pairing is a bad prior O(d), or O(rd) non-commuting
GRAPE-A / FoX Nilpotent, rank 1, learned or gated rate Yes, exactly How far back should this token look? Long documents where the right recency prior varies by token O(d) for the gates, O(1) for the bias

What GRAPE Actually Measured

Numbers matter more than frameworks, so here is exactly what the paper reports and nothing else. All of it is from Section 6.

Setup (Section 6.1). The nanoGPT codebase with a Llama model, changing only the positional encoding. FineWeb-Edu 100B, of which 50B tokens were used for training. Context length 4,096, batch size 480, QK RMSNorm for stability, AdamW with a cosine schedule and 2,000 warm-up steps, and RoPE base 10,000.

Medium models are 353M parameters with 24 layers, 8 heads and hidden size 1024. Large models are 770M with 36 layers, 10 heads, hidden size 1280 and head dimension 128. Evaluation is 0-shot with lm-evaluation-harness, averaged over seven tasks.

Method353M average (Table 1)770M average (Table 2)
RoPE51.7355.76
GRAPE-M-nonctx51.7954.81
GRAPE-M-ctx51.7854.73
ALiBi52.8756.44
FoX52.9656.30
GRAPE-A-QK53.00not reported
GRAPE-AP53.2556.91
Read those honestly. The additive family wins on this benchmark, not the multiplicative one. GRAPE-AP is the best average at both sizes, 53.25 at 353M and 56.91 at 770M. Multiplicative GRAPE is a statistical tie with RoPE at 353M (51.79 against 51.73) and is behind RoPE at 770M (54.81 against 55.76). The paper's own claim for GRAPE-M is structural, that it generalises RoPE at the same O(d) cost, not that it beats RoPE on these runs. With the KV-shift module enabled at 770M the ordering shifts again: FoX leads at 57.09, ALiBi 56.92, GRAPE-AP 56.86, RoPE 55.32.

One more observation the paper makes in Section 6.2, visible in its Figure 3: at 770M the RoPE run suffers training instability, while the GRAPE run improves steadily throughout. Stability is a real reason to care about a positional encoding, separate from any benchmark average.

The Gotchas That Actually Bite

Rotate Q and K. Never V. The point of the rotation is to make the score relative. V carries the content that gets blended after the softmax. Rotating V spins the content itself, and the model has to spend capacity undoing it. This is the single most common bug in a from-scratch RoPE implementation.
Cache the rotated key, not the raw key. Apply G(j) to key j once when the token arrives, store that, and never touch it again. If you store the raw key and rotate at read time you pay a rotation for the whole cache on every generated token, turning an O(1) append into O(L) work per step.
Watch the order against RMSNorm. QK RMSNorm, which GRAPE used for training stability, normalises the query and key before the score. RoPE is norm-preserving so the two commute, but a rotation applied after normalisation and a rotation applied before it are the same transformation only when the norm is exactly preserved. If you introduce a damped variant with a decay term, the order matters and you must pick one and be consistent between training and serving.
Interpolation and extrapolation are different failures. A model that is bad at 32k because positions are out of range needs base rescaling. A model that is bad at 32k because it never saw a document that long needs long-document training data. NTK scaling fixes the first and does nothing at all for the second, and teams routinely misdiagnose one as the other.
For images, use 2-D RoPE, not flattened 1-D. Flattening a 32 by 32 patch grid to 1,024 tokens makes the patch directly below a query 32 steps away and the patch to its right 1 step away, encoding a spatial neighbour as a distant one. Split the head dimension and rotate one half by the row index and the other by the column index. GRAPE's Appendix G covers the 2-D and 3-D cases explicitly.
Streaming servers should look hard at the additive family. Because the ALiBi and GRAPE-A bias is computed from two loop indices inside the attention kernel, the KV cache holds unmodified keys and the context can be extended at serve time with no config change. That is a genuinely different operational posture from RoPE, where extending context means changing rope_theta and fine-tuning.

A Five-Minute Audit

When you inherit an attention implementation and want to know whether its positional encoding is sound, these five checks find almost every real bug.

1. Is V rotated?
Grep the attention block. If the position transform touches the value tensor, that is the bug. Q and K only.
↓
2. Does the score depend only on the offset?
Feed the same q and k at positions (3, 1) and (503, 501) and compare the logits. They must match to floating point noise. If they do not, the scheme is absolute, whatever the docs say.
↓
3. Is the cache written once?
Instrument the KV write path and count transforms over a hundred generated tokens. It should be a hundred, not five thousand.
↓
4. Does the slowest plane finish a turn?
Compute the training length times the smallest frequency. If that is well under 6.28 radians, the model is going to extrapolate badly and the base is the lever.
↓
5. Do training and serving agree?
Same base, same ordering against normalisation, same offset convention for the first token. A silent off-by-one in the position index costs a surprising amount of quality.
When to reach for the whole framework: Debugging a long-context regression that appeared after a config change. The concrete artefact is a diff that moved rope_theta or added a decay term. Why the generator view helps: write the change as a change to L and you can immediately say which family it moved into, whether the exact relative law still holds, and whether the KV cache policy is still write-once. Where a purely empirical sweep fails: you can spend a week of GPU time bisecting configs and still not know whether the new encoding is even relative.
You are serving a 7B model trained at 8k with RoPE. Customers paste 40k-token documents, you cannot retrain, and quality collapses past about 10k. What is the correct first move?

Chapter 12: Cheat Sheet and Connections

Everything on one page. Nothing new here, so read it as a reference rather than a lesson.

Symbol Glossary

SymbolMeans
q(t), k(s)Query at time t, key at time s, before any position is applied
F(t), G(s)The matrices a positional encoding applies to the query and the key
A(t − s)F(t)TG(s); the translation that brings a key from the past to the present
LThe generator; A(t) = exp(tL). One matrix that determines the entire encoding
exp(M)Matrix exponential, I + M + M²/2! + ...; maps a generator to a group element
so(d)Skew-symmetric d by d matrices; the generators of rotations
SO(d)Rotation matrices; orthogonal with determinant 1, so the transpose is the inverse
GL(d)All invertible d by d matrices; where the additive family lives after the lift
UnipotentA matrix all of whose eigenvalues equal 1; what exp of a nilpotent gives
θiAngular frequency of plane i; 1 / base2i/d in canonical RoPE
βh, mhALiBi slope for head h
a, bThe two vectors spanning a rank-2 generator's action plane in GRAPE
s = √(αβ − γ²)The area of the parallelogram a and b span; the rotation angle per unit position
PUOrthogonal projector onto the action plane U = span{a, b}

Every Formula in Plain English

FormulaIn words
PE(pos, 2i) = sin(pos·θi)Position becomes a chord of sine and cosine waves at geometrically spaced frequencies
hi = Etok[t] + Epos[i]Look up a trained vector for this index and add it to the token
score += b[bucket(i − j)]Add one learned number that depends only on how far apart the pair is
R(mθ)q · R(nθ)k = qTR((n−m)θ)kRotate query and key by their positions; the absolute angles cancel and only the gap survives
score −= mh|i − j|Charge each token a fixed toll per step of distance, with a different toll per head
F(t + r)TG(s + r) = F(t)TG(s)Shift both tokens by the same amount and the score does not change
A(s + t) = A(s)A(t)Translating by s then t is translating by s plus t; this is the group law
A(t) = exp(tL)Every continuous encoding satisfying the above is generated by one fixed matrix
L = abT − baTThe smallest rotation generator, built from the two vectors spanning its plane
exp(L) = I − (1−cos s)PU + (sin s / s)LRotate by angle s inside the plane, do nothing outside it
exp(nωA) = I + nωAWhen A squares to zero, the exponential is just two terms, and the result is a shear
base' = base · sd/(d−2)To stretch context s times, slow the low frequencies by s and leave the high ones alone

The Full Comparison

MethodYearFamilyParams Runtime costExtrapolatesUsed by
Sinusoidal2017Absolute additive0Free bufferPoorlyOriginal Transformer, Whisper
Learned absolute2018Absolute additiveL·dLookupNot at allBERT, GPT-2, ViT
Shaw relative2018Relative additiveSmall tableExtra gather and matmulFlat past the clipTransformer-XL lineage
T5 bias2019Relative additive32·H per layer(H, L, L) tensorFlat past the last bucketT5, FLAN-T5
RoPE2021Rotation, skew L0O(d) multiply-addNeeds base rescalingLlama, Qwen, Mistral, Gemma, DeepSeek
ALiBi2021Unipotent, nilpotent L0O(1) in kernelYes, gracefullyBLOOM, MPT
xPos / RetNet2022Damped rotation0O(d)Better than RoPERetNet, Mamba-3
NTK / YaRN2023RoPE respecified0O(d)Yes, with a short fine-tuneLong-context Llama variants, Code Llama
FoX2025Unipotent, learned rateOne gate per headO(d) plus a cumulative sumYesForgetting Transformer
GRAPE-M2026Rotation, learned planes2·H·dO(d), or O(rd) coupledSame class as RoPEProposed in the GRAPE paper
GRAPE-A / AP2026Unipotent, gated rate2·H·dO(d)YesProposed in the GRAPE paper

The Timeline

2017 · Sinusoidal
Fixed waves, added to embeddings. Position is a chord.
↓
2018 · Learned tables
BERT and GPT-2 let gradient descent choose. Hard length limit arrives.
↓
2018 to 2019 · Relative bias
Shaw, then T5 buckets. Distance is shared across the sequence, at the cost of an L by L tensor.
↓
2021 · RoPE and ALiBi
Two answers in the same year. Rotate the vectors, or tax the score. Both relative, neither needs a bias tensor.
↓
2023 · Context stretching
Position interpolation, NTK base scaling, YaRN. Long context becomes a config change.
↓
2026 · The map
GRAPE builds encodings from generators; the Jane Street analysis proves there was never much to build.

Six Things People Get Wrong

The claimWhy it is wrong
"RoPE extrapolates, that is the point of being relative." Being relative makes the score a function of the offset. It does not make the values that function receives familiar. The slow planes present rotation angles no training example produced, and the model has no idea what to do with them.
"ALiBi is just a windowed attention mask." A mask is a hard cutoff and a step function. ALiBi is a smooth, differentiable penalty, so a far token can still win if its content score is high enough. Every head keeps a full but soft field of view.
"Sinusoidal encoding is relative because PE(pos + k) is a linear function of PE(pos)." That property is real, and it makes relativity recoverable, not present. The encoding is added to the token vector before the projections, so content and position are tangled, and the model has to learn the recovery inside WQ and WK.
"GRAPE replaces RoPE." GRAPE contains RoPE. On the paper's own benchmark the multiplicative variant ties RoPE at 353M and trails it at 770M. The contribution is the design space and the additive results, not a drop-in win.
"You can just increase rope_theta and serve any context." The base change fixes out-of-range angles. It does nothing about a model that never saw a document longer than 2,000 tokens and therefore never learned any long-range behaviour to preserve.
"There must be a better positional encoding nobody has found." Under linearity, translation invariance and continuity there is one generator and three kinds of block. The only unexplored corner produces polynomial score terms, and it is unexplored because it looks unattractive, not because it is hidden.

Where to Go Next

Transformers for the attention mechanism these encodings modify. Attention Variants for MHA, GQA and FlashAttention, which constrain what a positional encoding is allowed to cost. Vision Transformers for the 2-D case where the coordinate pairing question is unavoidable. State Space Models for the recurrent view, where the damped rotation generator is the state transition itself. Embedding Layers for how token and position embeddings share a vector space in the first place.

Key Sources

The closing thought. Two teams, working independently, arrived at the same object in the same season. One asked what is possible and found a one-parameter group. The other asked what is buildable and built a one-parameter group. When the map and the territory agree that completely, you are probably looking at the real structure of the problem rather than at a convenient description of it. As Puranik puts it, the finding is reassuring: we do not need to rack our brains for a perfect positional encoding, because we are probably already using it.