How Models Know Word Order

What breaks when you exceed the trained length

Feed a model more tokens than it was trained on and it does not fail gracefully. Here is a measured comparison of five positional schemes, all degrading.

On this page 7
  1. The three ways it goes wrong
  2. Why the third one is sneaky
  3. What the measurement shows
  4. What people actually do about it
  5. Where you have felt this
  6. Remember this
  7. What to learn next

One lesson, three depths. Pick the one that fits you today — you can switch any time.

Beginner — No maths. Plain English.

A model given more tokens than it was trained on does not degrade politely. It either crashes or gets much worse, quite suddenly.

Think about driving on a road you know well. You handle it at night, in rain, in traffic. Now put yourself on a road you have never driven, in a country where they drive on the other side.

Nothing about the car changed. Your skill did not vanish. Everything you relied on to make quick decisions is now unfamiliar, and you drive badly.

That is a model past its trained length. The machinery is intact. The inputs are unfamiliar.

The three ways it goes wrong

It stops. With a learned position table, position 1024 has no row. The program raises an error and there is no output at all. This is the kindest failure, because you find out immediately.

The signals go strange. With rotary encodings, the slowest dials swing into angles never seen in training. The model is reading a gauge whose markings it does not recognise.

Attention spreads thin. More tokens compete for the same total attention. With four thousand candidates instead of five hundred, each one gets less. Sharp focus becomes vague focus.

Why the third one is sneaky

Attention has to split a fixed budget across everything it can see. Double the tokens and, all else equal, each one gets roughly half.

The model was trained where the strongest match reliably stood out. Now it is competing with eight times as many distractions, and it never learned how to be sharper.

Nothing errors. The text stays fluent. The model quietly stops finding the right thing.

What the measurement shows

Later on this page there is a small experiment. Five positional schemes, one task. Trained at length 24, tested at 32 and 48.

Every single one gets worse. Learned positions crash outright. The best of the rest loses about a fifth of its accuracy at 32 and more than half at 48.

That is the honest headline. There is no scheme in this list that reads longer inputs for free.

What people actually do about it

They do not rely on extrapolation. They retrain.

Take a model trained at 4,000 tokens. Apply a scaling method to its rotary dials. Then run a short extra training pass on long documents. That combination works. The scaling changes what the model sees; the training teaches it what the new readings mean.

Any model card advertising 128,000 tokens went through that second step.

Where you have felt this

  • A long chat where the assistant forgets what you said at the start.
  • Pasting a long document and getting a summary of only the last part.
  • A coding assistant losing track of a function defined much earlier in the file.
  • A model that handles your test paragraph and fails on the real report.

Remember this

  • Past the trained length, models crash, misread their own position signals, or spread attention too thin.
  • Every positional scheme degrades. Some crash instead, which is more honest.
  • The working fix is scaling plus a short retraining pass, not extrapolation on its own.

What to learn next

Developer — Code and libraries.

Setup

bash
pip install torch

Written against PyTorch 2.5.1. This script trains five small models on CPU. It took 283 seconds on the machine used to write this lesson, and your time will differ with core count and PyTorch build.

Five schemes, one task, measured

The task is induction: the last token repeats a token from the first half of the sequence, and the model must predict what followed that earlier occurrence. It is learnable by a two-layer transformer and it is defined at any length.

extrapolation.py
import math, time, torch, torch.nn as nn, torch.nn.functional as F

torch.set_num_threads(4)
VOCAB, DIM, HEADS, LAYERS, TRAIN_LEN = 24, 64, 4, 2, 24

def make_batch(n, seq, g):
    """The last token repeats one from the first half. Predict what followed it."""
    x = torch.randint(2, VOCAB, (n, seq), generator=g)
    idx = torch.randint(1, seq // 2, (n,), generator=g)
    x[torch.arange(n), -1] = x[torch.arange(n), idx]
    return x, x[torch.arange(n), idx + 1]

def apply_rope(x, base=10000.0):
    d, T = x.shape[-1], x.shape[-2]
    inv = 1.0 / (base ** (torch.arange(0, d, 2, dtype=torch.float32) / d))
    ang = torch.arange(T, dtype=torch.float32)[:, None] * inv[None, :]
    c, s = ang.cos(), ang.sin()
    even, odd = x[..., 0::2], x[..., 1::2]
    return torch.stack([even * c - odd * s, even * s + odd * c], -1).flatten(-2)

def alibi_slopes(n):
    start = 2 ** (-8 / n)
    return torch.tensor([start ** (i + 1) for i in range(n)])

class Block(nn.Module):
    def __init__(self, mode):
        super().__init__()
        self.mode = mode
        self.q, self.k = nn.Linear(DIM, DIM, bias=False), nn.Linear(DIM, DIM, bias=False)
        self.v, self.o = nn.Linear(DIM, DIM, bias=False), nn.Linear(DIM, DIM, bias=False)
        self.n1, self.n2 = nn.LayerNorm(DIM), nn.LayerNorm(DIM)
        self.mlp = nn.Sequential(nn.Linear(DIM, 4 * DIM), nn.GELU(), nn.Linear(4 * DIM, DIM))
        self.register_buffer("slopes", alibi_slopes(HEADS)[:, None, None])

    def forward(self, x):
        B, T, _ = x.shape
        h = self.n1(x)
        shape = (B, T, HEADS, DIM // HEADS)
        q = self.q(h).view(shape).transpose(1, 2)
        k = self.k(h).view(shape).transpose(1, 2)
        v = self.v(h).view(shape).transpose(1, 2)
        if self.mode == "rope":
            q, k = apply_rope(q), apply_rope(k)
        att = (q @ k.transpose(-1, -2)) / math.sqrt(DIM // HEADS)
        if self.mode == "alibi":
            pos = torch.arange(T)
            att = att + self.slopes * (pos[None, :] - pos[:, None]).float()
        att = att.masked_fill(torch.triu(torch.ones(T, T, dtype=torch.bool), 1), -torch.inf)
        y = (att.softmax(-1) @ v).transpose(1, 2).reshape(B, T, DIM)
        x = x + self.o(y)
        return x + self.mlp(self.n2(x))

def sinusoidal(T, d):
    pos = torch.arange(T, dtype=torch.float32)[:, None]
    i = torch.arange(d // 2, dtype=torch.float32)[None, :]
    f = pos / 10000.0 ** (2 * i / d)
    pe = torch.zeros(T, d)
    pe[:, 0::2], pe[:, 1::2] = f.sin(), f.cos()
    return pe

class Tiny(nn.Module):
    def __init__(self, mode):
        super().__init__()
        self.mode = mode
        self.emb = nn.Embedding(VOCAB, DIM)
        if mode == "learned":
            self.wpe = nn.Embedding(TRAIN_LEN, DIM)      # exactly the trained length
        self.blocks = nn.ModuleList([Block(mode) for _ in range(LAYERS)])
        self.nf = nn.LayerNorm(DIM)
        self.head = nn.Linear(DIM, VOCAB, bias=False)

    def forward(self, x):
        h = self.emb(x)
        if self.mode == "learned":
            h = h + self.wpe(torch.arange(x.shape[1]))
        elif self.mode == "sinusoidal":
            h = h + sinusoidal(x.shape[1], DIM)
        for b in self.blocks:
            h = b(h)
        return self.head(self.nf(h))[:, -1, :]           # only the last position matters

def train_and_test(mode, steps=2000):
    torch.manual_seed(0)
    g = torch.Generator().manual_seed(1)
    model = Tiny(mode)
    opt = torch.optim.AdamW(model.parameters(), lr=3e-3)
    for _ in range(steps):
        x, y = make_batch(64, TRAIN_LEN, g)
        loss = F.cross_entropy(model(x), y)
        opt.zero_grad(); loss.backward(); opt.step()
    model.eval()
    accs = []
    with torch.no_grad():
        for seq in [24, 32, 48]:
            if mode == "learned" and seq > TRAIN_LEN:
                accs.append(None); continue
            gg = torch.Generator().manual_seed(99)
            x, y = make_batch(512, seq, gg)
            accs.append((model(x).argmax(-1) == y).float().mean().item())
    return accs

print(f"trained only at length {TRAIN_LEN}. random guessing scores {1/(VOCAB-2):.3f}\n")
print(f"{'position method':<14} {'len 24':>8} {'len 32':>8} {'len 48':>8}")
t0 = time.time()
for mode in ["learned", "sinusoidal", "rope", "alibi", "nope"]:
    a = train_and_test(mode)
    cells = ["  crash " if v is None else f"{v:8.3f}" for v in a]
    print(f"{mode:<14}" + "".join(cells))
print(f"\ntotal training time on this CPU: {time.time() - t0:.0f} s")
Output
trained only at length 24. random guessing scores 0.045

position method   len 24   len 32   len 48
learned          0.789  crash   crash 
sinusoidal       0.766   0.447   0.434
rope             0.771   0.639   0.338
alibi            0.631   0.545   0.408
nope             0.475   0.404   0.314

total training time on this CPU: 283 s

These numbers move between runs and machines. Seeds are fixed, but CPU thread count, PyTorch version and BLAS backend all change reduction order, and a 64-dimensional model trained for 2000 steps is sensitive to that. Expect the same ordering and the same shape of degradation, not the same third decimal.

Reading the table

Every column falls. Not one scheme holds its accuracy at 48 tokens. The trained length is a real boundary for all of them.

learned crashes, and that is the honest failure. nn.Embedding(24, 64) has no row 24. You find out at once, with a stack trace, instead of shipping quietly wrong output.

RoPE is best at 32 and worst at 48. It holds 0.639 at a 1.3x stretch, then drops to 0.338 at 2x, below sinusoidal. Modest extrapolation, then collapse. That is exactly the pattern the whole scaling literature exists to address.

Sinusoidal is flat between 32 and 48, at 0.447 and 0.434. It gives up most of its accuracy immediately and then plateaus a little above chance. Different shape, same conclusion.

ALiBi's numbers deserve care. It scores lowest at the trained length, 0.631, and degrades most gently in relative terms. That is consistent with its design: a recency penalty helps extrapolation and hurts a task requiring retrieval from an arbitrary earlier position. This experiment does not reproduce ALiBi's published perplexity result, which was measured on natural language at far larger scale. Do not read this table as a refutation of that paper. Read it as a reminder that "extrapolates well on perplexity" and "retrieves well at long range" are different claims.

NoPE is weakest here at every length. The previous lesson's research summary reports NoPE winning on length generalisation in a careful published comparison. It does not win in this two-layer model on this task. Small-scale reproductions of published results frequently disagree with them, and saying so is more useful than tuning until the table matches.

What actually fixes it

Nothing in this table. The working recipe is:

  1. Apply a RoPE scaling method — YaRN, NTK-aware or the Llama 3 rule.
  2. Continue pretraining on long documents for a modest number of steps.
  3. Evaluate on tasks that need long-range retrieval, not on perplexity alone.

Step 2 is not optional. Scaling alone changes the geometry; the training pass teaches the model to read the new geometry.

Common mistakes

Reading a context limit as a capability. A 128k window means the input is accepted. Whether the model reasons over all of it is a separate measurement.

Testing only with needle-in-a-haystack. Retrieving a single planted sentence is far easier than summarising or reasoning across a long document. Models pass the needle test at lengths where they cannot do real work.

Assuming a longer window is always better. Accuracy on facts placed mid-context is measurably lower than at the edges. More context can reduce accuracy on the thing you care about.

Blaming the model when the position ids are wrong. Left padding, a mishandled KV cache offset, or a truncation that drops a chat template's opening tokens all produce "long context degradation" that is actually a bug. Print position_ids before concluding anything.

Try it yourself

Raise TRAIN_LEN from 24 to 48 and test at 48, 64 and 96. Every scheme improves at 48, because it is now inside the trained range, and the same cliff reappears further out. Moving the cliff is what context extension does. Removing it is not currently possible.

What to learn next

Researcher — Mathematics and papers.

Three distinct failure mechanisms

Out-of-domain positional inputs. Learned tables have no entry past $L$. RoPE has entries but the low-frequency pairs, with $\lambda_i > L$, present phases never observed during training. Formally, the model is evaluated off the support of its training distribution over positional features. This is the mechanism the interpolation literature targets.

Attention entropy growth. With logits of roughly constant scale and $n$ candidate keys, the softmax entropy grows approximately as $\ln n$, so the maximum weight decays roughly as $1/n$ absent a compensating increase in logit magnitude. Chiang and Cholak (2022) analyse this and propose scaling logits by $\log n$; YaRN's attention temperature $\sqrt{1/t} = 0.1\ln s + 1$ is an empirical version of the same correction.

Undertrained tail. Position $m$ receives gradient only from examples of length exceeding $m$. With heavy-tailed but short-dominated document lengths, positions near $L$ are trained on orders of magnitude less data. This affects learned tables directly and RoPE's low-frequency pairs indirectly.

These are separable. A model can have all three, and each has a different remedy.

Empirical picture from the literature

  • Press et al. (2022) measure perplexity beyond the training length: sinusoidal and rotary degrade sharply, ALiBi stays flat. This is a perplexity claim on natural language.
  • Kazemnejad et al. (2023) measure downstream accuracy on reasoning tasks beyond the training length, and find NoPE > T5 relative > ALiBi > RoPE > APE. Different metric, different ordering.
  • Liu et al. (2023), Lost in the Middle, document a U-shaped accuracy curve over the position of relevant evidence within a long context, present even inside the trained range.
  • Hsieh et al. (2024), RULER, construct tasks of controlled difficulty and report effective context lengths well below advertised ones for many models.

The disagreement between the first two is instructive rather than contradictory. Perplexity is dominated by local prediction, which a recency-biased model does well. Downstream length generalisation on reasoning tasks requires reliable long-range access, which recency bias actively harms.

The distinction that resolves most confusion

Two properties are routinely conflated:

  • Stability. Loss or perplexity does not blow up when the input is longer. Achievable with a decaying bias, because the model effectively ignores the extra tokens.
  • Utilisation. Accuracy improves, or at least holds, on tasks that require information from the extra tokens.

ALiBi buys stability cheaply. Utilisation at long range is the harder property, and it is what interpolation plus continued pretraining is for.

What the working recipe implies

Every deployed long-context model uses interpolation plus training, not extrapolation. Position Interpolation, NTK-aware scaling, YaRN, LongRoPE and the Llama 3 frequency rule all change the positional geometry so that inference-time inputs land inside the trained distribution, then finetune to adapt to the new geometry.

Framed that way, "length extrapolation" as a standalone capability has largely been abandoned as a design goal for large models. The goal is cheap, reliable extension.

Measurement recommendations

  • Report accuracy at several lengths, not one, and include the trained length as a control.
  • Separate retrieval from reasoning. Needle-in-a-haystack and multi-hop aggregation give very different curves.
  • Vary the position of the required evidence, not only the input length, to expose the mid-context dip.
  • Report the effective context, defined as the largest length at which the model holds some fraction of its short-context accuracy, rather than the architectural limit.

Papers

What to learn next