Part of Language AI Handbook
Examines the fundamental barriers limiting transformer context windows, including quadratic memory scaling, position encoding extrapolation failures.
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Context Length Challenges
Transformers have conquered natural language processing, but they harbor a fundamental limitation: fixed context windows. A model trained on 2048 tokens cannot simply process 8192 tokens at inference time. This constraint affects everything from document summarization to multi-turn conversations, where earlier context often matters most.
Think of the context window as a reading desk. A scholar can only work with the pages spread in front of them. A bigger desk lets them reference more material simultaneously, but every doubling of desk size requires exponentially more space to construct. Transformers face exactly this tradeoff: every token added to the context increases the amount the model can see, but the cost of that wider view grows faster than linearly.
Feeding more tokens into the model introduces several distinct problems: memory explosion from quadratic attention, position encodings that break beyond training lengths, the difficulty of learning dependencies across thousands of tokens, and the puzzle of evaluating whether models use long-range information. Each of these barriers operates at a different level: memory is a hardware constraint, position encoding is a representational constraint, long-range learning is a statistical constraint, and evaluation is an epistemic constraint. Solving one does not solve the others.
This chapter systematically examines each barrier. The goal isn't to present these as unsolvable problems, because subsequent chapters will show exactly how researchers overcame them. Rather, the goal is to build a precise mental model of what goes wrong and why, so that the solutions feel motivated rather than arbitrary. Understanding the challenges is the prerequisite for appreciating the solutions.
Training Sequence Length Limits
Every transformer is trained on sequences of a fixed maximum length. GPT-2 used 1024 tokens. BERT used 512. Early GPT-3 models trained on 2048 tokens. These aren't arbitrary choices but rather pragmatic compromises between model capability and computational cost.
The context window (or context length) is the maximum number of tokens a transformer can process in a single forward pass. It's determined by the position embeddings used during training and the memory constraints of the hardware.
The training length fundamentally shapes what the model learns. A model trained on 512-token sequences has never seen how pronouns resolve across 1000 tokens or how conclusions connect to distant premises. These long-range patterns simply aren't in the training data. The model can't learn what it has never been shown, regardless of how powerful its architecture is in principle. This is why extending context at inference time without additional training almost always produces degraded outputs: the model is being asked to operate in a regime it has no experience with.
# Common training context lengths over time
models = [
("GPT-2", 2019, 1024),
("BERT", 2018, 512),
("GPT-3", 2020, 2048),
("LLaMA", 2023, 2048),
("LLaMA 2", 2023, 4096),
("GPT-4 Turbo", 2023, 128000),
("Claude 3", 2024, 200000),
("Gemini 1.5", 2024, 1000000),
]
# Calculate growth
years = [m[1] for m in models]
lengths = [m[2] for m in models]Evolution of Training Context Lengths: Model Year Context Length ---------------------------------------- GPT-2 2019 1,024 BERT 2018 512 GPT-3 2020 2,048 LLaMA 2023 2,048 LLaMA 2 2023 4,096 GPT-4 Turbo 2023 128K Claude 3 2024 200K Gemini 1.5 2024 1000K
The jump from 2K to 200K+ tokens represents a 100x increase in just a few years. This explosion in context length didn't come for free. Each extension required novel techniques for position encoding, memory efficiency, and training methodology.

The progression reveals a pattern. Until 2023, context lengths grew modestly, doubling every few years. Then came a breakthrough. Models jumped from 4K to 100K+ tokens in a single generation. This wasn't just better hardware. It required fundamental advances in how position information is encoded and how attention is computed.
Why Not Just Train Longer?
The obvious question: why didn't earlier models simply train on longer sequences? The answer involves multiple compounding constraints.
Memory scales quadratically. We explored this in detail in the previous chapter on the quadratic attention bottleneck. For a 4K token sequence, attention matrices alone consume 16x more memory than for 1K tokens. Training on 32K tokens with naive attention would require 1024x more attention memory than training on 1K tokens. When a single training step exhausts available GPU memory, the only options are to reduce the batch size, use gradient checkpointing, or limit the sequence length. In practice, researchers almost always chose the latter.
Compute scales quadratically too. Even if memory were free, the computational cost of training explodes. A batch of 32K-token sequences requires the same attention compute as 1024 batches of 1K-token sequences, holding total tokens constant. Training large language models already takes weeks or months on thousands of GPUs. Multiplying that cost by 1000 is simply not feasible for most organizations.
Long documents are rare. Most training data consists of short documents. Web pages average a few thousand tokens. Training a model to handle 100K tokens requires curating datasets with long documents, which are harder to find and more expensive to process. Books and academic papers provide some long-form content, but they're a small fraction of the web-scale data used for pretraining. A model trained on mostly short documents learns mostly short-range patterns.
Gradients become unstable. Backpropagating through very long sequences compounds numerical errors. Without careful normalization and training tricks, models can fail to learn anything useful from long sequences. The gradient must travel through many layers and many timesteps simultaneously, and small numerical errors multiply. This is less of a blocker than memory or compute, but it means that simply scaling up training length without architectural changes often produces poor results even when resources permit it.
def training_cost_ratio(target_length, base_length=2048):
"""
Estimate relative training cost for longer sequences.
Assumes:
- Attention scales O(n^2)
- Other components scale O(n)
- Attention dominates at long sequences
"""
attention_ratio = (target_length / base_length) ** 2
linear_ratio = target_length / base_length
# At long sequences, attention dominates (~70% of compute)
attention_weight = 0.7
linear_weight = 0.3
total_ratio = (
attention_weight * attention_ratio + linear_weight * linear_ratio
)
return total_ratio
target_lengths = [4096, 8192, 16384, 32768, 65536, 131072]
cost_ratios = [(n, training_cost_ratio(n)) for n in target_lengths]Training Cost Relative to 2048-Token Baseline:
Target Length Cost Ratio Effective Batch Reduction
------------------------------------------------------------
4K 3.4x 29.4%
8K 12.4x 8.1%
16K 47.2x 2.1%
32K 184.0x 0.5%
65K 726.4x 0.1%
131K 2886.4x 0.0%The numbers are stark. Training on 32K tokens costs roughly 180x more per sequence than training on 2K tokens. To maintain the same training throughput, you'd need 180x more compute budget. Alternatively, you accept 180x smaller effective batch sizes, which can destabilize training. Neither option is appealing.

Attention Memory Scaling
The attention mechanism computes pairwise interactions between all tokens. For each layer and each attention head, we must store an matrix of attention weights, where every entry represents how much token attends to token . The key insight is that this pairwise structure is what makes attention so powerful: every token can directly influence every other token in a single forward pass. But that same all-pairs structure is precisely what makes scaling to long sequences so expensive. The total memory required to store these attention matrices is:
where:
- : total memory in bytes for all attention matrices
- : number of attention heads (each head computes an independent attention pattern)
- : number of transformer layers (each layer has its own set of attention matrices)
- : sequence length in tokens
- : the size of each attention matrix (one entry per query-key pair)
- : bytes per element (2 for fp16/bf16, 4 for fp32)
The critical insight is that appears squared: doubling the sequence length quadruples the memory for attention matrices. This is the source of the quadratic memory scaling that limits context length.
But this formula only accounts for the attention weight matrices themselves. A complete picture of memory consumption includes:
- Key-value cache (for inference):
- Activations (for training): Intermediate values needed for backpropagation
- Gradients: Same size as activations
def memory_breakdown_gb(n, d=4096, h=32, L=32, dtype_bytes=2, mode="training"):
"""
Break down memory requirements for a transformer.
Args:
n: Sequence length
d: Model dimension
h: Number of attention heads
L: Number of layers
dtype_bytes: Bytes per element
mode: "training" or "inference"
Returns:
Dictionary with memory breakdown in GB
"""
bytes_to_gb = 1 / (1024**3)
# Attention matrices: h * L * n^2 (but computed one layer at a time)
# For training, we need to store all for backprop
attn_matrices = h * L * n * n * dtype_bytes
# KV cache: 2 (K and V) * L * n * d
kv_cache = 2 * L * n * d * dtype_bytes
# Activations per layer (rough estimate):
# - Input embeddings: n * d
# - Attention output: n * d
# - FFN intermediate: n * 4d (assuming 4x expansion)
# - Total per layer: ~6 * n * d
activations_per_layer = 6 * n * d * dtype_bytes
if mode == "training":
# Store activations for all layers for backprop
activations = L * activations_per_layer
gradients = activations # Same size as activations
return {
"attention_matrices": attn_matrices * bytes_to_gb,
"activations": activations * bytes_to_gb,
"gradients": gradients * bytes_to_gb,
"total": (attn_matrices + activations + gradients) * bytes_to_gb,
}
else: # inference
return {
"attention_matrices": 0, # Computed on-the-fly with FlashAttention
"kv_cache": kv_cache * bytes_to_gb,
"total": kv_cache * bytes_to_gb,
}
# Analyze for LLaMA 7B style model
sequence_lengths = [2048, 8192, 32768, 131072]Memory Breakdown for LLaMA 7B Style Model (d=4096, h=32, L=32):
Training Mode:
Seq Len Attn Matrices Activations Gradients Total
-----------------------------------------------------------------
2K 8.0 GB 3.0 GB 3.0 GB 14.0 GB
8K 128.0 GB 12.0 GB 12.0 GB 152.0 GB
32K 2048.0 GB 48.0 GB 48.0 GB 2144.0 GB
131K 32768.0 GB 192.0 GB 192.0 GB 33152.0 GB
Inference Mode:
Seq Len KV Cache
------------------------------
2K 1.0 GB
8K 4.0 GB
32K 16.0 GB
131K 64.0 GBFor training, the numbers quickly become untenable. A 32K sequence requires over 500 GB for attention matrices alone, far exceeding any single GPU. Even with gradient checkpointing (recomputing activations during backprop instead of storing them), the attention matrices remain the bottleneck.
For inference, the KV cache dominates. At 128K tokens, storing keys and values for all layers consumes over 60 GB. This explains why long-context inference requires careful memory management and often multiple GPUs.


These visualizations reveal why long-context training and inference require fundamentally different approaches. Training hits the quadratic wall with attention matrices. Inference hits a linear wall with KV cache storage. Solutions like FlashAttention address the first problem by never materializing the full attention matrix. Solutions like KV cache compression and sliding window attention address the second.
Position Encoding Extrapolation
Position encodings tell the model where each token sits in the sequence. Without them, attention treats "The cat sat on the mat" and "mat the on sat cat The" identically. The challenge: most position encoding schemes fail catastrophically when asked to encode positions beyond their training range.
Absolute Position Embeddings
The original transformer used learned absolute position embeddings: a lookup table mapping each position index to a learned vector. If you train with positions 0 through 511, the embedding table has 512 entries. What happens at position 512? The model has never seen it.
import torch.nn as nn
class AbsolutePositionEmbedding(nn.Module):
"""Learned absolute position embeddings."""
def __init__(self, max_length, d_model):
super().__init__()
self.embeddings = nn.Embedding(max_length, d_model)
self.max_length = max_length
def forward(self, positions):
# What if positions > max_length - 1?
if positions.max() >= self.max_length:
raise ValueError(
f"Position {positions.max()} exceeds max {self.max_length - 1}"
)
return self.embeddings(positions)
# Example: BERT-style embedding
pos_embed = AbsolutePositionEmbedding(max_length=512, d_model=768)Absolute Position Embedding: Maximum position: 511 Embedding dimension: 768 Total parameters: 393,216 Position 511: ✓ (within range) Position 512: ✗ (Position 512 exceeds max 511)
Absolute embeddings have zero ability to extrapolate. The model either crashes or produces garbage for unseen positions. This hard limit explains why early BERT applications required truncating documents to 512 tokens.
Sinusoidal Position Encodings
The original "Attention Is All You Need" paper proposed sinusoidal encodings that compute position vectors using trigonometric functions rather than learning them from data. The key idea is to create position-dependent patterns at multiple frequencies, allowing the model to detect both local and distant positional relationships.
For a position in the sequence and dimension index in the encoding vector:
where:
- : the encoding value at position for even dimension
- : the encoding value at position for odd dimension
- : the absolute position in the sequence (0, 1, 2, ...)
- : the dimension index within the encoding (ranges from 0 to )
- : the total dimensionality of the position encoding (typically matches the model's hidden dimension)
- : a base constant that determines the range of wavelengths
The division by creates different frequencies across dimensions. Low-indexed dimensions (small ) oscillate rapidly with position, while high-indexed dimensions oscillate slowly. This multi-scale representation allows the model to capture both fine-grained local position (from high-frequency components) and coarse global position (from low-frequency components).
Since these are deterministic mathematical functions rather than learned embeddings, they can generate vectors for any position, including positions never seen during training. But can the model use them beyond its training range?
def sinusoidal_encoding(positions, d_model):
"""Compute sinusoidal position encodings."""
positions = np.array(positions).reshape(-1, 1)
dims = np.arange(d_model).reshape(1, -1)
# Compute the divisor term
div_term = 10000 ** (2 * (dims // 2) / d_model)
# Apply sin to even indices, cos to odd
encodings = np.zeros((len(positions), d_model))
encodings[:, 0::2] = np.sin(positions / div_term[:, 0::2])
encodings[:, 1::2] = np.cos(positions / div_term[:, 1::2])
return encodings
# Generate encodings for positions within and beyond training
d_model = 64
train_positions = np.arange(512)
extrapolate_positions = np.arange(512, 2048)
train_encodings = sinusoidal_encoding(train_positions, d_model)
extrap_encodings = sinusoidal_encoding(extrapolate_positions, d_model)

Sinusoidal encodings produce mathematically valid vectors at any position. The issue is semantic: the model learned to interpret positions 0-511 during training. Position 1024 might have a perfectly valid encoding, but the model hasn't learned what attention patterns make sense when tokens are that far apart.
Rotary Position Embeddings and the Extrapolation Problem
Rotary Position Embeddings (RoPE), used in LLaMA and many modern models, encode relative positions through rotation in embedding space. They offer better extrapolation than absolute embeddings but still degrade beyond training length.
def rope_extrapolation_analysis(train_length, test_lengths, d_model=64):
"""
Analyze RoPE behavior at different positions.
RoPE encodes relative position through rotation angle:
theta = position * base^(-2i/d)
At long positions, high-frequency components rotate very fast,
creating patterns the model hasn't seen.
"""
base = 10000
dims = np.arange(0, d_model, 2)
results = []
for pos in test_lengths:
# Compute rotation frequencies at this position
thetas = pos / (base ** (dims / d_model))
# High-frequency dimensions (large dim index) rotate slowly
# Low-frequency dimensions (small dim index) rotate faster
max_rotation = thetas.max()
avg_rotation = thetas.mean()
# How many full rotations in highest frequency?
high_freq_rotations = thetas[0] / (2 * np.pi)
# Beyond training, high frequencies enter unseen territory
in_training = pos <= train_length
results.append(
{
"position": pos,
"max_theta": max_rotation,
"high_freq_rotations": high_freq_rotations,
"in_training": in_training,
}
)
return results
train_len = 2048
test_positions = [512, 1024, 2048, 4096, 8192, 16384]
rope_analysis = rope_extrapolation_analysis(train_len, test_positions)RoPE Rotation Analysis (trained on 2048 tokens):
Position Max θ High-freq Rotations Status
------------------------------------------------------------
512 512.00 81.5 In training
1,024 1024.00 163.0 In training
2,048 2048.00 325.9 In training
4,096 4096.00 651.9 EXTRAPOLATION
8,192 8192.00 1303.8 EXTRAPOLATION
16,384 16384.00 2607.6 EXTRAPOLATIONAt position 4096 (2x training length), high-frequency components have rotated 163 times. At position 16384 (8x training length), they've rotated 653 times. The model has never seen these rotation patterns. The mismatch manifests as degraded attention: tokens that should attend to each other might not, and irrelevant tokens might receive spurious attention.

The visualization shows why position interpolation (covered in the next chapter) works: instead of asking the model to interpret new rotation regimes, we rescale positions to stay within the familiar training range. Position 4096 becomes position 2048, which the model understands.
Long-Range Dependency Learning
Even if we solve the memory and position encoding problems, there's a more fundamental challenge: can models learn to use information from thousands of tokens ago?
The architecture permits long-range connections in theory. Attention can, in a single step, route information from token 1 to token 10000. But architecture and learned behavior are different things. Just because the architecture allows a connection doesn't mean the training process will discover and reinforce that connection. For that to happen, the model must receive clear gradient signal indicating that the long-range connection matters, and that signal must be strong enough to update weights reliably across many training examples.
The Gradient Flow Problem
During training, the model must learn connections between distant tokens through backpropagation. For a dependency spanning 1000 tokens, gradients must flow through 1000 attention steps. Even with attention's direct connections, this creates challenges.
Consider a simple task: matching an opening bracket to its closing bracket. If they're 10 tokens apart, the model easily learns the pattern. If they're 1000 tokens apart, the signal becomes diluted. The gradient that should say "pay attention to that opening bracket" has to compete with thousands of other gradients flowing from nearby, frequently-relevant tokens. The training process is democratic in the sense that all positions contribute gradients, and nearby positions tend to be more consistently informative than distant ones.
def gradient_signal_simulation(distances, attention_prob_per_hop=0.1):
"""
Simulate how gradient signal decays over distance.
Even with direct attention, each hop multiplies by attention probability.
For distant tokens, this probability tends to be small.
"""
# For a token at distance d, assume average attention per hop
# Gradient magnitude roughly proportional to product of attention weights
# along the "path" from source to target
results = []
for d in distances:
# Simplified model: gradient ~ attention_prob^(log(d))
# Real models are more complex, but the decay is similar
gradient_factor = attention_prob_per_hop ** np.log2(max(d, 1))
results.append(
{
"distance": d,
"gradient_factor": gradient_factor,
"log10_factor": np.log10(gradient_factor)
if gradient_factor > 0
else -np.inf,
}
)
return results
distances = [1, 10, 100, 500, 1000, 2000, 4000, 8000]
gradient_analysis = gradient_signal_simulation(distances)Gradient Signal Decay Over Distance:
Distance Relative Signal Log10 Signal
--------------------------------------------------
1 1.00e+00 0.0
10 4.77e-04 -3.3
100 2.27e-07 -6.6
500 1.08e-09 -9.0
1,000 1.08e-10 -10.0
2,000 1.08e-11 -11.0
4,000 1.08e-12 -12.0
8,000 1.08e-13 -13.0The gradient signal drops by orders of magnitude as distance increases. At 1000 tokens, the signal is roughly 10 million times weaker than at distance 1. This doesn't mean learning is impossible, but it requires many more training examples to establish long-range patterns.

Attention Dilution
Attention weights must sum to 1 across all positions (due to softmax normalization). With uniform attention distributed equally over tokens, each token receives exactly of the total attention mass. As sequence length grows, this fraction shrinks proportionally: at tokens, each token receives 1% of attention; at tokens, each receives only 0.01%.
For long sequences, this means any single token contributes a tiny fraction to the output.
def attention_dilution(sequence_length, relevant_tokens=10):
"""
Calculate how much attention mass reaches relevant tokens
under various attention patterns.
"""
# Uniform attention
uniform_per_token = 1.0 / sequence_length
uniform_total = uniform_per_token * relevant_tokens
# Local window (e.g., 256 tokens)
window = 256
if relevant_tokens <= window:
local_per_token = 1.0 / window
local_total = local_per_token * relevant_tokens
else:
local_total = 0 # Relevant tokens outside window
# Learned sparse (assume 10% sparsity)
sparse_attending = int(sequence_length * 0.1)
if sparse_attending > 0:
sparse_per_token = 1.0 / sparse_attending
# Probability relevant tokens are in sparse set
sparse_prob = min(1.0, relevant_tokens / sparse_attending)
sparse_total = sparse_per_token * relevant_tokens * sparse_prob
else:
sparse_total = 0
return {
"uniform": uniform_total,
"local_256": local_total,
"sparse_10pct": sparse_total,
}
seq_lengths = [512, 2048, 8192, 32768, 131072]
dilution_results = [attention_dilution(n) for n in seq_lengths]Attention Mass on 10 Relevant Tokens:
Seq Length Uniform Local-256 Sparse-10%
----------------------------------------------------
512 1.95% 3.91% 3.84%
2K 0.49% 3.91% 0.24%
8K 0.12% 3.91% 0.01%
32K 0.03% 3.91% 0.00%
131K 0.01% 3.91% 0.00%With uniform attention over 128K tokens, the 10 relevant tokens receive only 0.008% of attention mass combined. The signal is buried in noise. This is why models must learn to concentrate attention on relevant tokens. But learning that concentration itself requires seeing many examples where those long-range connections matter.

The visualization shows that learned concentration can maintain signal even at very long sequence lengths. But achieving this concentration requires training data where long-range dependencies matter and gradients that can reinforce the correct attention patterns.
Evaluation Challenges
How do we know if a model uses long context? Perplexity on long documents tells us the model predicts well overall, but not whether it's using information from 10K tokens ago. This evaluation gap has led to the development of specialized benchmarks.
The Needle in a Haystack Test
The most intuitive test: hide a specific fact early in a long document, then ask about it at the end. If the model can retrieve the fact, it's using long context.
def create_needle_haystack_prompt(
needle_position, total_length, context_tokens=100
):
"""
Create a needle-in-a-haystack test prompt.
The 'needle' is a specific fact. The 'haystack' is filler text.
We place the needle at a specific position and ask about it at the end.
"""
# The needle: a specific retrievable fact
needle = "The secret code is ALPHA-7392."
# Filler text (in practice, this would be coherent text)
filler_per_segment = "This is background text that provides context but is not relevant to the specific question. "
# Build the document
segments_before = needle_position // context_tokens
segments_after = (
total_length - needle_position - len(needle.split())
) // context_tokens
document = filler_per_segment * segments_before
document += needle + " "
document += filler_per_segment * segments_after
question = "\nQuestion: What is the secret code mentioned in the document?"
return {
"document": document[
: total_length * 5
], # Approximate token to char ratio
"question": question,
"answer": "ALPHA-7392",
"needle_position": needle_position,
"total_length": total_length,
}
# Test at different positions
positions = [100, 1000, 5000, 10000, 50000]
total_length = 100000
test_cases = [
create_needle_haystack_prompt(pos, total_length) for pos in positions
]Needle-in-a-Haystack Test Cases:
Needle Position % Into Document Context Distance
--------------------------------------------------------
100 0.1% 99,900
1,000 1.0% 99,000
5,000 5.0% 95,000
10,000 10.0% 90,000
50,000 50.0% 50,000Real benchmarks like RULER and LongBench extend this idea with multiple needles, multi-hop reasoning, and varying distractor difficulty. Performance typically degrades as the needle moves earlier in the document, revealing the model's effective context utilization.
Multi-Hop Reasoning Over Long Context
A harder test: require the model to combine information from multiple distant locations.
def multi_hop_test_design(num_hops, positions, total_length):
"""
Design a multi-hop reasoning test over long context.
Each hop provides a clue that points to the next location.
The model must chain together all clues to answer.
"""
clues = [
("Alice lives in", "France"),
("The capital of France is", "Paris"),
("The main airport in Paris is", "Charles de Gaulle"),
("The airline code for Charles de Gaulle is", "CDG"),
]
test = {
"num_hops": num_hops,
"positions": positions[:num_hops],
"total_length": total_length,
"clues": clues[:num_hops],
"question": "What is the airline code for the main airport in the capital of the country where Alice lives?",
"answer": clues[num_hops - 1][1] if num_hops <= len(clues) else "N/A",
}
# Compute the total "distance" the model must span
if len(positions) >= 2:
test["max_span"] = max(positions) - min(positions)
else:
test["max_span"] = 0
return test
# Different multi-hop configurations
configs = [
(2, [1000, 50000]),
(3, [1000, 25000, 50000]),
(4, [1000, 20000, 40000, 60000]),
]
multi_hop_tests = [multi_hop_test_design(n, pos, 100000) for n, pos in configs]Multi-Hop Reasoning Test Configurations:
Hops Positions Max Span
------------------------------------------------------------
2 1K, 50K 49,000
3 1K, 25K, 50K 49,000
4 1K, 20K, 40K, 60K 59,000
Example question: What is the airline code for the main airport in the capital of the country where Alice lives?
Required reasoning chain:
Hop 1: Alice lives in → France
Hop 2: The capital of France is → Paris
Hop 3: The main airport in Paris is → Charles de Gaulle
Hop 4: The airline code for Charles de Gaulle is → CDGMulti-hop tests reveal whether models can maintain and combine information across distances. Many models that pass single-needle tests fail multi-hop tests, suggesting they struggle with reasoning over long context rather than just retrieval.
The Lost in the Middle Phenomenon
A striking empirical finding: models often perform best when relevant information is at the beginning or end of the context, and worst when it's in the middle. This "lost in the middle" effect suggests attention doesn't uniformly cover the context. Liu et al. (2023) documented this phenomenon systematically across multiple models and tasks. The effect is not a quirk of a particular architecture but a consistent pattern, suggesting it reflects something fundamental about how transformers process long sequences.
The primacy effect (strong performance at the start of context) likely reflects the model's training dynamics: early tokens in a document are referenced by every subsequent token's attention computation, giving them more gradient signal and making them more salient by the end of training. The recency effect (strong performance at the end) reflects a similar mechanism: late tokens appear more recently relative to the model's generation position, so they receive stronger attention weight in causal (decoder-only) models. The tokens in the middle receive neither of these advantages.

The "lost in the middle" effect has significant practical implications. When building RAG systems or structuring long prompts, placing critical information at the beginning or end improves retrieval. Understanding this phenomenon is essential for effective use of long-context models. It also motivates architectural improvements like position-aware attention biases, which can help models attend more uniformly across the full context rather than clustering at the boundaries. In practice, if you must include many retrieved documents in a prompt and want the model to use them all effectively, ordering them with the most critical documents at the start or end rather than scattered through the middle can noticeably improve output quality.
A Worked Example: Context Length Stress Test
Let's bring together the challenges with a concrete example. We'll simulate how a model's performance degrades as we push beyond training context.
def context_stress_test(trained_length, test_lengths, model_quality=0.95):
"""
Simulate model performance at different context lengths.
Combines multiple degradation factors:
1. Position encoding extrapolation
2. Attention dilution
3. Lost-in-middle effect
4. Memory limitations (simulated by truncation)
"""
results = []
for test_len in test_lengths:
# Factor 1: Position encoding degradation
if test_len <= trained_length:
position_factor = 1.0
else:
extrapolation_ratio = test_len / trained_length
# Exponential decay beyond training length
position_factor = np.exp(-0.5 * (extrapolation_ratio - 1))
# Factor 2: Attention dilution
# Relevant info becomes harder to find in longer contexts
dilution_factor = min(1.0, trained_length / test_len)
# Factor 3: Lost-in-middle (affects middle 50% of context)
# More severe at longer lengths
middle_penalty = 0.1 * (
1 - trained_length / max(test_len, trained_length)
)
middle_factor = 1.0 - middle_penalty
# Combined performance
combined = (
model_quality * position_factor * dilution_factor * middle_factor
)
results.append(
{
"test_length": test_len,
"position_factor": position_factor,
"dilution_factor": dilution_factor,
"middle_factor": middle_factor,
"overall_performance": combined,
"within_training": test_len <= trained_length,
}
)
return results
# Model trained on 4K tokens, tested on various lengths
trained = 4096
test_lens = [512, 1024, 2048, 4096, 8192, 16384, 32768, 65536]
stress_results = context_stress_test(trained, test_lens)Context Stress Test (trained on 4096 tokens):
Test Length Position Dilution Middle Overall Status
--------------------------------------------------------------------
512 1.00 1.00 1.00 0.95 OK
1K 1.00 1.00 1.00 0.95 OK
2K 1.00 1.00 1.00 0.95 OK
4K 1.00 1.00 1.00 0.95 OK
8K 0.61 0.50 0.95 0.27 EXTRAP
16K 0.22 0.25 0.93 0.05 EXTRAP
32K 0.03 0.12 0.91 0.00 EXTRAP
65K 0.00 0.06 0.91 0.00 EXTRAP
The stress test visualizes how multiple factors compound to degrade performance beyond training length. Position encoding breaks first and most severely. Attention dilution provides a gentler decay. Together, they explain why naive extrapolation fails and motivate the techniques covered in subsequent chapters.
Limitations and Impact
The context length challenges we've examined create real limitations for language models in practice, but they've also driven innovation that has expanded what's possible. It's worth stepping back to appreciate how interconnected these challenges are. Memory constraints pushed researchers toward efficient attention algorithms. Those algorithms revealed new position encoding problems. Solving position encoding exposed gaps in evaluation methodology. Each solution created new questions, and the field advanced through this iterative cycle of problem and response.
Practical limitations remain significant. Even with 200K token context windows, models don't use all that context equally. The "lost in the middle" effect means critical information should be placed strategically. Multi-hop reasoning over very long distances remains unreliable. And the computational cost of long contexts limits their use in latency-sensitive applications.
Training costs scale prohibitively. Extending context length during training requires quadratically more compute for attention. Most long-context models are fine-tuned from shorter-context bases rather than trained from scratch, inheriting some limitations of the shorter training. True end-to-end training on 100K+ token sequences remains rare due to cost.
Evaluation doesn't capture everything. Needle-in-a-haystack tests measure retrieval but not reasoning. Models might locate information without understanding how to use it. Multi-hop benchmarks are better but still don't capture the full complexity of real-world long-document tasks like legal analysis or scientific synthesis.
Despite these challenges, the progress has been remarkable. In 2020, a 4K context window was generous. By 2024, 200K+ tokens became available. This expansion enables qualitatively new applications: analyzing entire codebases, processing full legal documents, and maintaining coherent multi-hour conversations. The techniques developed to overcome context length challenges, position interpolation, efficient attention, and memory augmentation, have become essential tools in the language modeling arsenal.
Understanding the fundamental challenges prepares us to appreciate the solutions. The next chapters explore position interpolation techniques that enable extrapolation, attention mechanisms that reduce the quadratic burden, and memory architectures that extend effective context beyond what fits in a single forward pass.
Summary
Context length limitations arise from multiple interacting factors, each creating its own barrier to long-document processing.
Training sequence limits are set by computational constraints. Both memory (quadratic in sequence length for attention matrices) and compute (also quadratic) restrict practical training lengths. Extending training length by 2x costs roughly 4x more resources.
Attention memory scaling follows for attention matrices during training and for KV cache during inference. A 32K sequence requires 256x more attention memory than a 1K sequence, quickly exceeding GPU limits.
Position encoding extrapolation fails for most schemes. Absolute embeddings cannot represent positions beyond training length. Sinusoidal and RoPE encodings can generate valid vectors but produce patterns the model hasn't learned to interpret. High-frequency components in RoPE rotate into unfamiliar regimes at long positions.
Long-range dependency learning struggles against gradient dilution and attention spread. Signals from distant tokens must compete against nearby context, and learning to focus on relevant distant tokens requires training examples where those dependencies matter.
Evaluation challenges make it hard to measure true context utilization. Simple perplexity hides whether models use distant context. Needle-in-a-haystack tests reveal retrieval ability but not reasoning. The "lost in the middle" effect shows that even large context windows don't guarantee uniform utilization.
These challenges are not insurmountable. Position interpolation, efficient attention, and memory augmentation, covered in the following chapters, each address specific aspects of the long-context problem. Understanding the challenges sets up the later discussion of the solutions.
Key Parameters
When analyzing context length limitations or designing systems that work with long sequences, these parameters directly determine feasibility and performance:
-
n (sequence length): The number of tokens in the input. This is the most critical parameter since both memory and compute scale with for attention. Typical values range from 512 (early BERT) to 1M+ (modern long-context models).
-
h (number of attention heads): Each head stores an independent attention matrix. Memory scales linearly with head count. Common values: 12 (GPT-2 Small) to 128 (large models).
-
L (number of layers): Transformer depth. Both attention memory and KV cache scale linearly with layer count. Typical range: 12-96 layers.
-
d (model dimension): The embedding size. KV cache scales as . Common values: 768 (BERT-base) to 8192 (large LLMs).
-
dtype_bytes: Precision of stored values. Using fp16/bf16 (2 bytes) instead of fp32 (4 bytes) halves memory requirements. Most modern training uses bf16.
-
training_length: The maximum sequence length seen during training. Determines the "safe" operating range for position encodings. Extrapolation beyond this length causes degradation unless mitigation techniques are applied.
-
base (RoPE): The base constant in rotary position embeddings (default 10000). Affects the frequency spectrum of position encoding. Modifying this is one approach to extending context (NTK-aware scaling).
-
window_size (sliding window attention): For models using local attention, this determines how many tokens each position can attend to. Larger windows capture more context but use more memory. Common values: 256-4096 tokens.
Quiz
Ready to test your understanding? Take this quick quiz to reinforce what you've learned about the fundamental challenges limiting transformer context lengths.
Context Length Challenges
Reference
Citation details
Cite or share this article.
Continue with the full handbook
This chapter is part of Language AI Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.
Explore Language AI HandbookStay up to date
Get articles, book updates, and news delivered to your inbox.
No spam, unsubscribe anytime.
Join the community
Sign in to remove popups, track your reading progress, and join the discussion.

Comments
No comments yet. Be the first to share your thoughts!