Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
Put two lines from a deployment log on a screen: “The model loaded the checkpoint” and “The checkpoint loaded the model.” They reuse the same words, but report opposite events. A model that only compares token content has no slot index to tell those commands apart.
Vision transformers expose that gap with image patches, and text has it too. The self-attention mechanism compares content, not position. Predict what happens when the token rows for “model,” “loaded,” and “ckpt” are shuffled.
The answer gives us our first diagnostic: unmasked attention shuffles its outputs along with its inputs. That’s permutation equivariance, and it explains why transformers need a position signal. We’ll follow the signal through three insertion points: vectors added to embeddings, RoPE rotations on queries and keys, and ALiBi’s linear distance bias on attention logits.
Then we’ll stress the same idea at long length. A formula can produce a value at position 32,000 even when training stopped at 4,096. That calculation is useful, but it isn’t evidence that the model can find a buried instruction there.
One boundary stays fixed throughout: a position method can rank permitted keys, while a causal mask must still hide future keys. A useful test changes order, checks the permitted scores, and confirms that forbidden positions remain forbidden.
First, watch attention lose the order
Before adding any position feature, make one prediction. If every token vector stays the same but the rows move, should content-only attention preserve the original row order, or should its outputs move with them?
Self-attention computes:
The product compares every query with every key (), scales by (the key width), turns scores into softmax weights, and mixes value rows . No term reads “first,” “second,” or “third.” The weights depend on content, not index.[1]
So a row permutation has a predictable effect: it also permutes Q, K, and V. Every query sees the same collection of content vectors, only in a new order, and each output row follows its query.
Use a three-token slice of our running example, with one 2D vector per token. Predict the shuffled output before checking the table:
| Token | Vector |
|---|---|
| model | |
| loaded | |
| ckpt |
Permutation rewrites the sequence as ckpt, model, loaded. Unmasked attention on the shuffled rows is exactly the shuffled original output: . The block has preserved content relationships, not command order.
![Two attention traces compare token order before and after permutation P=[2,0,1]. Token rows and attention-weight matrices reorder together, producing Y' = P Y and showing permutation equivariance without a position signal.](/cdn/content-image/fundamentals/positional-encoding-rope-alibi/illustrations/_generated/position_blind_attention_dark.png?v=5dab70e55295)
The small check below recomputes attention after the permutation. Read True as a diagnosis: no hidden slot number entered the calculation.
1import math
2
3tokens = {
4 "model": [1.0, 0.0],
5 "loaded": [0.0, 1.0],
6 "ckpt": [1.0, 1.0],
7}
8order = ["model", "loaded", "ckpt"]
9permutation = [2, 0, 1]
10
11def attention(rows: list[list[float]]) -> list[list[float]]:
12 scores = [
13 [sum(q_i * k_i for q_i, k_i in zip(q, k)) / math.sqrt(2) for k in rows]
14 for q in rows
15 ]
16 outputs = []
17 for row in scores:
18 normalizer = sum(math.exp(value) for value in row)
19 weights = [math.exp(value) / normalizer for value in row]
20 outputs.append([
21 sum(weight * value[column] for weight, value in zip(weights, rows))
22 for column in range(2)
23 ])
24 return outputs
25
26baseline = attention([tokens[name] for name in order])
27shuffled = attention([tokens[order[index]] for index in permutation])
28expected = [baseline[index] for index in permutation]
29
30same_output_up_to_permutation = all(
31 abs(actual - target) < 1e-12
32 for actual_row, target_row in zip(shuffled, expected)
33 for actual, target in zip(actual_row, target_row)
34)
35print(same_output_up_to_permutation)
36print([round(row[0], 3) for row in baseline])
37print([round(row[0], 3) for row in shuffled])1True
2[0.802, 0.599, 0.752]
3[0.752, 0.802, 0.599]A causal mask changes this result by removing future keys. A decoder-only model can then infer some order from which positions are visible, and Haviv et al. found NoPos runs competitive with sinusoidal and learned embeddings for causal language modeling.[2] Their bidirectional masked models failed to converge without an explicit position signal. Visibility gives a direction, but it still doesn’t provide a numeric coordinate or distance feature.
That distinction tells us where to look next. Absolute encodings, RoPE, and ALiBi answer the same ordering gap at different points in the block. They’re alternatives, not stacked stages.
The table is a map of those insertion points. For each row, ask what downstream quantity can first “feel” position.
| Approach | How it works | What it modifies | Examples |
|---|---|---|---|
| Absolute (additive) | Add a position vector to the token embedding | Embeddings | Original Transformer, BERT, GPT-2[1][3][4] |
| Relative (multiplicative) | Rotate Q and K by a position-dependent angle | Q/K projections | RoFormer, Llama family[5][6] |
| Relative (additive bias) | Add a distance penalty to attention logits | Attention scores | ALiBi[7] |


When debugging a position mismatch, inspect the first tensor that changed: the embedding, projected Q/K, or attention score matrix. That check also catches accidental stacking of methods meant to be alternatives.
The first map entry adds position before any Q, K, or V projection. Start there, then we’ll move the signal deeper into attention.
Absolute position: give every slot a multi-speed fingerprint
Nearby slots need distinguishable labels, while far slots need a slower signal that tracks broad location. The original Transformer meets both needs with several paired waves added to each token embedding.[1]
At position 0, every sine starts at 0 and every cosine at 1. Move to position 2 and the fastest pair has moved two radians, while slower pairs barely move. The combined phases form a position fingerprint before the network sees Q, K, or V.
For position and dimension , with the embedding width:
The paired waves carry one useful algebraic property. A fixed offset is a linear transform, , where the rotation depends only on . In principle, later layers can learn relative attention from that structure.
The generator below makes the dial picture concrete with an 8-by-8 table. Before running it, predict row 0 and the first pair at row 2.
1import math
2
3def sinusoidal_positional_encoding(max_len: int, d_model: int) -> list[list[float]]:
4 assert d_model % 2 == 0
5 table: list[list[float]] = []
6 for pos in range(max_len):
7 row = [0.0] * d_model
8 for i in range(0, d_model, 2):
9 frequency = math.exp(-(math.log(10000.0) * i) / d_model)
10 angle = pos * frequency
11 row[i] = math.sin(angle)
12 row[i + 1] = math.cos(angle)
13 table.append(row)
14 return table
15
16pe = sinusoidal_positional_encoding(max_len=8, d_model=8)
17print(len(pe), len(pe[0]))
18print([round(x, 3) for x in pe[0]])
19print(round(pe[2][0], 3), round(pe[2][1], 3))18 8
2[0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0]
30.909 -0.416The dials have different wavelengths. With , the table below samples one fast, one middle, and one slow pair using for each pair’s first dimension. Use it to predict which lane changes most between neighboring tokens.
| Dimension pair | Wavelength | What it tracks |
|---|---|---|
| tokens | Fine local position | |
| tokens | Medium-scale structure | |
| tokens | Coarse global position |
1import math
2
3d_model = 512
4for first_dimension in [0, 64, 510]:
5 angular_frequency = 10000 ** (-first_dimension / d_model)
6 wavelength = 2 * math.pi / angular_frequency
7 print(first_dimension, round(wavelength))10 6
264 20
3510 60611The waves are easier to read as lanes than as a matrix:

Sinusoids are fixed. BERT (Bidirectional Encoder Representations from Transformers) and GPT-2 instead learn a table : one trainable vector per slot, hard-limited at .[3][4] Predict the consequence first: what happens at a position beyond the last row?
| Property | Sinusoidal | Learned |
|---|---|---|
| Parameters | 0 (fixed formula) | |
| Flexibility | Fixed inductive bias | Adapts during training |
| Past | Formula is defined | No row exists |
| Original Transformer ablation | Strong baseline | Nearly identical result |
Vaswani et al. tested both and reported “nearly identical results” (Table 3, row E). Sinusoids were attractive because their formula can be evaluated at unseen indices. That’s a definition, not a quality guarantee: Press et al. measured sharp perplexity degradation when sinusoidal models ran far past their training length on WikiText-103.[7]
Why add instead of concatenate? Put two -wide vectors side by side and the next layer sees width . Unless you project back down, square matrices such as , , and then grow about 4x. Addition keeps width , but it also gives later projections one mixed vector rather than separate content and position channels.
Why do absolute positional encodings get added to token embeddings instead of concatenated?
Answer
Concatenation would double the hidden width from to unless the model projected back down. If the transformer stayed at width , square projections such as , , and would become roughly 4x larger. Addition keeps width and gives later layers one combined vector. Whether those layers use the mixture well is empirical.
Absolute methods leave us with three questions. Does quality hold beyond the trained length? Can the model infer an offset from two absolute vectors? Is mixing content and position in one stream a good trade? Relative methods move the distance signal into attention itself, which is the next experiment.
RoPE: make a shared offset visible inside QK
Now move the position signal past the embedding. Keep one query and one key fixed, then place both at positions . If both positions shift by 100, should their relative score change?
RoPE (Rotary Position Embeddings) answers by rotating each query and key in 2D subspaces. It comes from RoFormer and is used in Llama-family models, including Llama 3 with RoPE base .[5][6] Hold the unrotated vectors fixed, and the dot product depends on the offset between them rather than on their shared absolute shift.
The picture below makes that prediction visible. The absolute angles move, but the phase gap doesn’t.

For one 2D pair at position , RoPE applies this rotation:
Each consecutive pair spins like one clock hand. Its angle depends on the token index and the pair frequency . Production RoPE usually sets those frequencies from the per-head rotary width, not the full hidden size:
If every head dimension is rotated, . Some stacks rotate only part of each head, so use the configured rotary dimension when reproducing a checkpoint.
The cancellation comes from composing rotations. For one pair,
The same unrotated vectors at and therefore get the same positional score. The absolute coordinates differ; the offset doesn’t.
1import math
2
3def rotate_2d(vec: list[float], pos: int, theta: float) -> list[float]:
4 cosine, sine = math.cos(pos * theta), math.sin(pos * theta)
5 x, y = vec
6 return [x * cosine - y * sine, x * sine + y * cosine]
7
8def dot(left: list[float], right: list[float]) -> float:
9 return sum(x * y for x, y in zip(left, right))
10
11query = [0.8, 0.6]
12key = [0.7, 0.5]
13theta = 0.5
14score_a = dot(rotate_2d(query, 5, theta), rotate_2d(key, 2, theta))
15score_b = dot(rotate_2d(query, 105, theta), rotate_2d(key, 102, theta))
16print(round(score_a, 6), round(score_b, 6))10.040884 0.040884The output confirms the shared-shift prediction. It also marks the limit of this toy proof: a trained model’s context window depends on content-dependent Q and K vectors across many layers, not on this identity alone.
Use a unit-vector walk-through to isolate the phase term. Let and .
- Query at , key at : offset , score .
- Query at , key at : same offset, same score.
At offset , the same pair scores . Don’t turn that comparison into “farther is always weaker.” Cosine phases can rise again, and a real head sums many frequencies.
One head with contains four such pairs. With the standard base- schedule, their frequencies are , , , and . For in every pair and positions , , predict which lane turns negative before looking at the figure. The pair scores are :

Complex multiplication is the same 2D rotation: . The next helper rotates a whole head, then checks the shared-shift property across all its pairs.
1import cmath
2
3def rope_freqs(dim: int, base: float = 10000.0) -> list[float]:
4 return [1.0 / (base ** (index / dim)) for index in range(0, dim, 2)]
5
6def rotate(vec: list[float], pos: int, freqs: list[float]) -> list[float]:
7 rotated: list[float] = []
8 for pair, freq in enumerate(freqs):
9 x, y = vec[2 * pair], vec[2 * pair + 1]
10 spun = complex(x, y) * cmath.rect(1.0, pos * freq)
11 rotated.extend([spun.real, spun.imag])
12 return rotated
13
14def dot(left: list[float], right: list[float]) -> float:
15 return sum(x * y for x, y in zip(left, right))
16
17freqs = rope_freqs(dim=4)
18vec = [1.0, 0.0, 1.0, 0.0]
19score_a = dot(rotate(vec, 0, freqs), rotate(vec, 2, freqs))
20score_b = dot(rotate(vec, 1, freqs), rotate(vec, 3, freqs))
21print(round(score_a, 6), round(score_b, 6))10.583653 0.583653RoPE rotates Q and K, not V. Q and K route attention, so their relative geometry decides which value rows get weight. V supplies the rows being mixed and isn’t directly rotated in that attention step, though earlier layers can already have put position-dependent context into it.
Why does RoPE rotate Q and K without directly rotating V?
Answer
Q and K decide which values receive attention weight, so rotating them makes the routing score depend on relative offset. V supplies the information being mixed and isn't rotated in that attention step. After earlier layers, V can still carry position-dependent context.
Keep routing and aggregation separate when comparing the three methods:
| Property | Sinusoidal | Learned | RoPE |
|---|---|---|---|
| Position info | Added to embeddings | Added to embeddings | Applied to Q, K only |
| Relative position | Implicit | Implicit | Explicit in the dot product |
| Direct transform on V | Not applicable | Not applicable | None |
| Extra parameters | 0 | 0 |
RoFormer analyzes a long-term decay bound aggregated over frequencies, not a guarantee that one pair’s cosine falls forever.[5] For identical unit vectors in one pair, , which oscillates.
That distinction is worth testing directly. If distance itself must always lower the positional term, what should happen when a cosine comes back up?
1import math
2
3theta = 0.5
4distances = [1, 2, 4, 6, 8]
5scores = [math.cos(distance * theta) for distance in distances]
6print(distances)
7print([round(score, 3) for score in scores])
8print(scores[-1] > scores[-2])1[1, 2, 4, 6, 8]
2[0.878, 0.54, -0.416, -0.99, -0.654]
3True
The non-monotonic example is not just a toy warning. Men et al. show a production trap in Llama-2 experiments: perplexity can look acceptable while retrieval collapses. They saw this with a RoPE base below their derived lower bound, including a small base such as 500, and in some larger-base settings where next-token loss stayed low.[8] A changed RoPE config still needs retrieval and task evaluation at the deployed length.
RoPE exposes offset through phase. ALiBi makes a different choice: it writes “farther is weaker” directly into the score.
ALiBi: make recency a score bias
Suppose two visible keys have the same raw QK score, but one sits four positions behind the query. Should a head be allowed to prefer the newer key by a fixed amount? ALiBi (Attention with Linear Biases) answers yes without changing the embedding or rotating Q and K. After the query-key product, it adds a head-specific linear bias:[7]
In a causal decoder, the current token gets zero penalty, one step back gets , two steps back gets , and so on. Future keys stay at from the causal mask. Press et al. note that this bias is not multiplied by ; it’s added to the already-scaled (or, in their presentation, unscaled) scores.
The head slopes supply different recency strengths. For heads, the paper starts at and uses that value as the geometric ratio. For 8 heads that’s : Head 1 is short-sighted, while Head 8 barely penalizes distance. For a non-power-of-two head count, the reference implementation keeps the power-of-two schedule for the largest , then appends every other slope from the schedule until it has slopes.

Read the slope table as a prediction tool: which head should hold onto an older key more strongly?
| Head | Slope | Effect |
|---|---|---|
| Head 1 | Strong local attention | |
| Head 4 | Moderate range | |
| Head 8 | Very long-range |
Run that rule on our checkpoint example. “Loaded” at position 3 attends to “model” at position 1 with raw score . Head 4 has , so distance 2 contributes and the logit becomes . “The” at position 0 is distance 3, so its logit becomes . Head 1 would subtract and instead.
The helper below applies one slope to one visible key. Predict the two outputs for distance 4 and raw score 4.0 before comparing the steep and gentle heads.
1def alibi_score(raw_score: float, query_pos: int, key_pos: int, slope: float) -> float:
2 distance = max(query_pos - key_pos, 0)
3 return raw_score - slope * distance
4
5raw = 4.0
6query_pos = 10
7key_pos = 6
8head_1 = alibi_score(raw, query_pos, key_pos, slope=1 / 2)
9head_8 = alibi_score(raw, query_pos, key_pos, slope=1 / 256)
10print(head_1, round(head_8, 3))12.0 3.984One score hides an important boundary, so the next snippet builds the full causal bias tensor. It includes the non-power-of-two slope helper from the reference implementation and writes only for future keys.
1import math
2
3def alibi_slopes_power_of_two(n_heads: int) -> list[float]:
4 start = 2 ** (-(2 ** -(math.log2(n_heads) - 3)))
5 return [start * (start ** head) for head in range(n_heads)]
6
7def alibi_slopes(n_heads: int) -> list[float]:
8 assert n_heads > 0
9 if math.log2(n_heads).is_integer():
10 return alibi_slopes_power_of_two(n_heads)
11 closest = 2 ** math.floor(math.log2(n_heads))
12 extra = alibi_slopes(2 * closest)[0::2][: n_heads - closest]
13 return alibi_slopes_power_of_two(closest) + extra
14
15def build_alibi_bias(n_heads: int, seq_len: int) -> list[list[list[float]]]:
16 slopes = alibi_slopes(n_heads)
17 bias: list[list[list[float]]] = []
18 for slope in slopes:
19 head = []
20 for query in range(seq_len):
21 row = []
22 for key in range(seq_len):
23 if key > query:
24 row.append(float("-inf"))
25 else:
26 row.append(-slope * (query - key))
27 head.append(row)
28 bias.append(head)
29 return bias
30
31bias = build_alibi_bias(n_heads=4, seq_len=4)
32print(len(bias), len(bias[0]), len(bias[0][0]))
33print([round(value, 4) if math.isfinite(value) else value for value in bias[0][3]])
34print([round(value, 4) if math.isfinite(value) else value for value in bias[-1][3]])14 4 4
2[-0.75, -0.5, -0.25, -0.0]
3[-0.0117, -0.0078, -0.0039, -0.0]In one original-paper experiment, a 1.3 billion parameter ALiBi model trained at length 1024 and evaluated at 2048 matched the perplexity of a sinusoidal model trained at 2048, while using about 11% less training time and memory in that setup.[7] That result comes with three mechanism-level properties worth separating:
- The bias is monotonic: a farther visible key always gets a more negative penalty.
- The rule is defined for any distance, so there's no learned table to run out of.
- Fixed slopes give a spectrum from local heads to longer-range heads.
The finite entries in that matrix are a soft recency bias. Very negative logits can underflow to zero in finite precision after the softmax max-shift, but mathematically a distant visible key keeps a nonzero weight. Only the causal mask hard-zeros the future.
The final snippet tests that distinction with equal raw scores. A farther visible key should lose weight, not disappear.
1import math
2
3def softmax(logits: list[float]) -> list[float]:
4 maximum = max(logits)
5 shifted = [math.exp(value - maximum) for value in logits]
6 total = sum(shifted)
7 return [value / total for value in shifted]
8
9raw_scores = [2.0, 2.0, 2.0]
10distances = [2, 1, 0]
11biased_scores = [
12 score - 0.5 * distance
13 for score, distance in zip(raw_scores, distances)
14]
15weights = softmax(biased_scores)
16print([round(weight, 3) for weight in weights])
17print(weights[0] > 0)1[0.186, 0.307, 0.506]
2TrueTwo past tokens have equal raw attention scores, but one is four positions farther from the query under ALiBi. Does ALiBi mask the farther token?
Answer
No. ALiBi subtracts a larger linear bias from the farther token before softmax, so its weight usually falls but stays nonzero. Only the separate causal mask removes future positions outright.
A closed-form bias can be evaluated at offsets the model never trained on. Whether the checkpoint still retrieves buried evidence is a different question, and long-context evaluation makes that question concrete.
Long context: a defined formula still needs evidence
Here’s the operational handoff: a checkpoint trained at 4K now has a 32K serving target, and a passkey may sit near token 30,000. You can write down or , but that only proves the position rule is defined. It doesn’t prove the model can route attention to the passkey.
Use the table as an experiment map. Each row separates what arithmetic does past the training length from what a paper actually measured.
| Method | What happens past training length | What the papers actually measured |
|---|---|---|
| Sinusoidal | Formula can compute unseen indices | Press et al.: sharp WikiText-103 perplexity drop past train |
| Learned absolute | No row exists past | Extend or replace the table, then train and evaluate |
| RoPE (unchanged) | Rotations continue at unseen offsets | Chen et al.: poor direct extension in their Llama runs |
| ALiBi | Same linear bias at new distances | Press et al.: 1024 → 2048 perplexity match in their 1.3B setup |
| RoPE with PI or YaRN | Geometry is rescaled, usually with adaptation | Chen et al.: PI up to 32K; Peng et al.: YaRN up to 128K on evaluated Llama-family models |
Use two evidence gates for an extension: short-context behavior must survive, then retrieval and target tasks must work at the new length. A clean perplexity curve clears neither gate by itself.
These are different experiments, not a universal ranking. Treat context length as a model-and-eval claim, not a config value.[7][9][10]
PI: compress coordinates into the trained range
Position Interpolation (PI) scales position indices down so they land inside the trained range:[9]
Chen et al. started from LLaMA checkpoints trained at 2048 and, with up to 1,000 fine-tuning steps, reported windows up to 32,768. The frequencies stay put; only the index that multiplies is compressed. In a scale-4 toy (4096 → 16384), the last target position, 16383, maps to 4095.75. Predict that endpoint before running the code.
1train_length = 4096
2target_length = 16384
3scale = train_length / target_length
4
5for target_position in [0, 4096, 8192, 16383]:
6 effective_position = target_position * scale
7 print(target_position, round(effective_position, 2))10 0.0
24096 1024.0
38192 2048.0
416383 4095.75NTK-aware scaling: stretch frequencies unevenly
PI compresses every index equally. NTK-aware scaling instead changes the frequency base, stretching low-frequency bands more than high-frequency ones:[10]
Here and is the per-head rotary dimension. The first pair keeps for any base; later pairs slow down. Peng et al. document this as earlier NTK-aware work while developing YaRN. Quality still depends on model, target length, adaptation data, and evaluation. Code Llama uses the same base-scaling idea at an extreme, setting its base to .[11]
YaRN: treat frequency bands differently
YaRN (Yet another RoPE extensioN) combines NTK-by-parts interpolation with an attention temperature.[10] Its bands make a deliberate trade: high-frequency bands stay closer to their original wavelengths for local detail, low-frequency bands interpolate for global span, and middle bands ramp between the two. Peng et al. report Llama-2 variants evaluated through 128K, including passkey retrieval inside that window for their 7B and 13B YaRN runs.
The paper’s recommended temperature fit on its Llama experiments is . They implement it by scaling the complex RoPE embeddings by that factor, equivalent to scaling and without touching the attention kernel. Treat as a hyperparameter to validate on the deployed model.
The figure compares the three choices on the same 4K-to-16K target. Look for what stays local, what stretches, and where the attention factor enters:

Llama 3 takes a related production path without PI: it raises the RoPE base to 500,000 during pretraining to support longer contexts.[6] That’s trained geometry, not a config flag to copy onto an arbitrary checkpoint.
Choose by mechanism, then earn the window
Choose the insertion point that matches the behavior you want. Then test short-context quality, long-range retrieval, generation, and target tasks at the length you plan to advertise.
| Goal | Candidate | What you still have to measure |
|---|---|---|
| Reproduce the original Transformer | Sinusoidal absolute encoding | Quality at trained lengths and any longer lengths you serve |
| Finite learned slots | Learned absolute embeddings | How new rows are initialized and adapted |
| Relative offset inside QK scores | RoPE | Target-length retrieval and generation, especially after scaling |
| Explicit monotonic recency bias | ALiBi | Whether the recency prior hides evidence long tasks need |
| Extend an existing RoPE checkpoint | PI or YaRN-style adaptation | Short-context regression and long-context behavior after adaptation |
The implementation choice decides where position enters attention. The checkpoint still has to earn the window you advertise.
Check the three insertion points
- Explain why unmasked self-attention without a position signal is permutation equivariant.
- Point to where sinusoids, RoPE, and ALiBi insert position: embeddings, Q/K geometry, or logits.
- Compute a RoPE relative phase and an ALiBi distance penalty on a tiny example.
- Treat configured context length as a claim that needs retrieval and task evidence at the deployed length.
Common misconceptions
Order seems implicit: Symptom: you say unmasked attention already knows sequence order. Cause: you skipped the permutation-equivariance check. Fix: start from content-only attention, then account separately for position signals and masking.All methods sound the same: Symptom: sinusoids, learned tables, RoPE, and ALiBi blur together. Cause: you aren't tracking the insertion point. Fix: embeddings vs Q/K geometry vs logit bias.RoPE rotates the wrong tensor: Symptom: you apply RoPE to embeddings or V. Cause: routing and aggregation got merged. Fix: rotate Q and K after projection; leave V without a direct RoPE rotation.Long context looks solved on paper: Symptom: perplexity improves but retrieval still fails. Cause: you treated mathematical validity as behavioral proof. Fix: run passkey, retrieval, summarization, and target-task evals at the deployed length.Any base change is enough: Symptom: the config advertises a larger window but distant facts disappear. Cause: a new doesn't teach the model to use long-range evidence. Fix: pair geometry changes with an adaptation method, long-context data, and task-level checks.Concatenate by default: Symptom: width and projection cost balloon. Cause: you tried to keep content and position in separate channels at all costs. Fix: add position vectors unless you have a reason and a budget for a wider stack.