Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
Speculative decoding helped when a decode step was stuck rereading weights for one token at a time. Long context moves the pressure earlier and keeps it around longer: prefill must ingest more tokens, the KV-cache must retain them, and the answer still has to find the right evidence.
Keep one incident in mind. A canary rollback is approved somewhere inside a year's service traces, and someone asks whether it was approved and what caused it. Sending the whole archive may fit a model's advertised limit, but that alone doesn't make the decisive note reachable or the request affordable.
Follow that request through a sequence of decisions. First count the prompt and output budget, then price prefill and cached decode. Next pack evidence so position and retrieval behavior are testable. Finally choose full context, a local window, RAG, or a hybrid and attach latency, memory, and recall gates. Advertised capacity is how many tokens a context window accepts; effective utilization is whether the model can find and reason over the right evidence under those operational limits.
The attention mechanism in a standard transformer still compares tokens to tokens during prompt ingestion, so compute grows quickly with sequence length. FlashAttention[1] and paged KV-cache serving[2] reduce memory traffic and allocation waste. They improve the bill without changing what full attention must compare.

Read the figure as three answers to the same incident. Full attention keeps every position visible, at the steepest prefill and memory cost. A sliding window keeps a local band and may block a distant note. Retrieval makes a small prompt first, so the retriever, not the attention pattern alone, decides whether the note gets in.
Why is advertised context length different from effective context use?
Answer
Advertised length says how many tokens the model can accept. Effective use asks whether the model can find and reason over the right evidence at every position, under real cost, memory, and latency constraints.
First gate: can the prompt fit, and what does it cost?
Treat a context window as a shared budget, not as an input-only bucket. System instructions, conversation history, retrieved evidence, the user request, and the output you reserve all compete for the model's limit. A prompt that fits with a ten-token answer may not fit with a useful generation allowance.
The quadratic bottleneck
Ask what happens when a 4K trace grows to 128K before reaching for an optimization. Standard full attention forms token-pair scores in Big-O notation for sequence length . Doubling the sequence therefore quadruples those pairwise checks. A 32x increase creates about 32² = 1,024x as many raw attention-score pairs during prefill.
That's the first bill. Optimized kernels reduce memory traffic and wall time, but they don't remove the full-attention scaling law. The second bill appears during decoding: each generated token reads the cached prefix, so a long prompt becomes both a compute problem and a GPU-memory scheduling problem.

Prefill pays for the pairwise comparison: score computations per layer. Decode pays for keeping prior key/value vectors in the KV cache and reading them again for each new token. Extending the usable range adds a third question about training and careful RoPE scaling. These are separate costs, so one successful benchmark won't answer all three.
Why can moving from 4K to 128K context become more than 32 times harder for full attention?
Answer
Full attention compares token pairs, so prefill attention work scales with . A 32x increase in sequence length creates about 32² = 1,024x more pairwise attention scores before kernel and hardware optimizations.
Memory in concrete numbers
Put a number on the second bill. Llama 3 70B is an 80-layer decoder with 64 query heads, 8 KV heads, hidden size 8192, and head dimension 128.[3] Llama 3.1 keeps that shape and advertises a 128K window. Use this as a dated fixture: BF16 KV tensors, 2 bytes per cached element, and a 128K prompt (131,072 tokens).
With GQA, the cache holds 8 KV heads, so the calculation gives 40 GiB per active sequence. Replace those 8 heads with the 64 heads of full multi-head attention (MHA) and the cache becomes 320 GiB. The BF16 weight file is about 130 GiB ( bytes). One GQA request fits below the weight file; four concurrent requests need 160 GiB of KV before activations or runtime buffers.
The general formula that produced those numbers is:
Read the result as per active sequence. For a GPU capacity estimate, multiply by the number of requests decoding at once, then leave headroom for weights, activations, and runtime buffers. The code below makes that multiplication visible.
1def kv_cache_gib(
2 layers: int,
3 kv_heads: int,
4 head_dim: int,
5 sequence_tokens: int,
6 bytes_per_element: int,
7) -> float:
8 bytes_used = (
9 2 * layers * kv_heads * head_dim * sequence_tokens * bytes_per_element
10 )
11 return bytes_used / (1024**3)
12
13WEIGHTS_GIB = (70 * 10**9 * 2) / (1024**3)
14
15for label, heads, dtype_bytes, n_seq in [
16 ("1x GQA BF16", 8, 2, 1),
17 ("1x GQA FP8", 8, 1, 1),
18 ("4x GQA BF16", 8, 2, 4),
19 ("1x MHA BF16", 64, 2, 1),
20]:
21 cache = n_seq * kv_cache_gib(80, heads, 128, 131_072, dtype_bytes)
22 print(
23 f"{label}: {cache:.0f} GiB KV vs {WEIGHTS_GIB:.0f} GiB weights "
24 f"({'KV wins' if cache > WEIGHTS_GIB else 'weights still larger'})"
25 )11x GQA BF16: 40 GiB KV vs 130 GiB weights (weights still larger)
21x GQA FP8: 20 GiB KV vs 130 GiB weights (weights still larger)
34x GQA BF16: 160 GiB KV vs 130 GiB weights (KV wins)
41x MHA BF16: 320 GiB KV vs 130 GiB weights (KV wins)For long-context serving, why can a few 128K requests crowd out other users even when one cache is smaller than the weights?
Answer
The KV cache is per active sequence. On a Llama 3 70B-class decoder, one 128K GQA cache is about 40 GiB, still under the ~130 GiB weight file, but four concurrent 128K sequences already store about 160 GiB of KV before weights, activations, and runtime buffers.
Prefill vs. decode: two different bottlenecks
The same 128K request can hurt twice, for different reasons.[2] During prefill, the model ingests the prompt and full attention pays for its long pairwise score matrix. During decode, the model emits tokens one at a time; the KV cache avoids recomputing old projections, but each new token still reads the cached prefix.
For a single decode step, a useful lower bound on KV read traffic (reading K and V for the active prefix; ignoring writes of the new step and weight traffic) is:
where is layers, is KV heads, is head dim, is cached tokens, and is bytes per element. It resembles the residency formula because one decode step streams one K and one V value for every layer, head, and cached token. Here the result means bytes streamed per step, not bytes resident in the cache.
Compare that stream with weight-stream bytes for the same step, using a rough weight-only model that loads active parameters once per token. At a short context, low-batch decode often spends more bandwidth streaming weights than KV. As grows, KV reads can overtake weights and make the step KV-bandwidth bound. On the fixture above, one 128K sequence reads about 40 GiB of KV per decode step versus about 130 GiB of BF16 weights under this simple comparison; four concurrent sequences push the KV side to 160 GiB. GQA, paged KV, KV quantization, and latent KV such as MLA reduce different terms in that bill.
Keep the phase attached to each intervention. FlashAttention reduces attention-kernel IO, which matters most during large prefills and time to first token (TTFT). Chunked prefill lets the scheduler interleave a long ingestion with in-flight decodes.[4] PagedAttention, GQA, and KV-cache quantization reduce allocation waste or bytes per cached token. Prefix reuse skips repeated prefill for matching prefixes. The right fix is whichever moves your measured bottleneck.
Which optimizations mostly help prefill, and which mostly help decode?
Answer
FlashAttention and chunked prefill mainly help prompt ingestion and prefill scheduling. Prefix reuse cuts repeated prefill for shared prompts. PagedAttention, GQA, KV-cache quantization, and cache compression mainly reduce decode memory pressure and concurrency limits.
Attention variants that cut long-context cost
Instead of keeping every position visible, change the visibility pattern. Mistral 7B pairs GQA with sliding window attention (SWA): each token attends to a fixed local window rather than the entire prefix.[5] If that window has width , attention cost moves from toward . Gemma 2 alternates local layers with global-attention layers, using a 4096-token local window and an 8192-token global span in its report.[6] Local layers pay less; global layers provide occasional long-range paths.
Test the dependency before choosing that trade. If the answer lives near the cursor, a local window may be enough. If it requires connecting a policy clause to a log line tens of thousands of tokens away, a pure window can block the path even though the tokens are present. Sliding-window attention is a cheaper visibility pattern, not full-prefix access at a discount.
Here's a second failure to predict when the window rolls forward. Once early tokens leave the cache, quality can collapse in some models. Xiao et al. linked this behavior in their evaluated models to attention sinks, initial tokens that attract disproportionate attention; their StreamingLLM experiments retained a few sink KV entries alongside the recent window for stable long generation without fine-tuning.[7] Sink count and quality need model-specific tests. Retaining sinks can stabilize generation, but it doesn't restore lookup of every evicted fact.
When is sliding-window attention a good fit, and when is it dangerous?
Answer
It fits tasks where most dependencies are local, such as continuation or code near the cursor. It's dangerous for forensic QA, policy lookup, or multi-hop reasoning where an answer may depend on evidence far outside the local window. A naive window that evicts the very first tokens can also destabilize generation unless it retains attention-sink tokens.
When the input doesn't fit: truncation and compaction
Visibility is only useful after the prompt fits. In a multi-turn chat or agent loop, history grows on every turn, so the manager must decide which state survives the next request.
Truncation is the hard cut: protect the system prompt and latest user turn, then drop old turns until the token budget closes. It's cheap and predictable, but an omitted fact is gone. Count tokens rather than characters or message count because code and prose consume the budget at different rates.
Summarization replaces old turns with a model-written state note. Compaction applies the same idea to an agent transcript: fold completed steps into a smaller state before continuing. It preserves more meaning per token, at the cost of another model call and the risk that the summarizer drops a detail that becomes important later.
Either route starts with the same token-budgeting rule: reserve system instructions and expected output, then fill remaining space from newest to oldest. Classify each older turn as disposable or as state that must be compacted.

The diagram makes that decision explicit. Reserve non-negotiable space before touching history. If an older turn is disposable, delete it; if it still carries state, compact it and repack. This keeps a full prompt from stealing room from the answer you need it to produce.
1def fit_history(
2 messages: list[dict],
3 token_budget: int,
4 count_tokens,
5) -> list[dict]:
6 """Keep the system prompt plus the newest turns that fit the budget."""
7 system = [m for m in messages if m["role"] == "system"]
8 turns = [m for m in messages if m["role"] != "system"]
9
10 used = sum(count_tokens(m["content"]) for m in system)
11 kept: list[dict] = []
12 # Walk newest to oldest so recent context survives truncation.
13 for msg in reversed(turns):
14 cost = count_tokens(msg["content"])
15 if used + cost > token_budget:
16 break
17 kept.insert(0, msg)
18 used += cost
19
20 return system + kept
21
22# Concrete example: a tiny word-count stand-in for a real tokenizer.
23def count_tokens(text: str) -> int:
24 return len(text.split())
25
26history = [
27 {"role": "system", "content": "You are an incident assistant."},
28 {"role": "user", "content": "incident one with a fairly long trace summary"},
29 {"role": "assistant", "content": "triaged incident one"},
30 {"role": "user", "content": "what failed in canary"},
31]
32
33kept = fit_history(history, token_budget=10, count_tokens=count_tokens)
34print([m["role"] for m in kept])1['system', 'user']In the example, the oldest turns fall away while the system prompt and newest user turn survive. That's the right result only if those old turns are disposable. If one contains the canary's root cause, compact it into a state note before it crosses the budget boundary.
When should you compact history instead of truncating it?
Answer
Truncate when older turns are safely disposable and you want zero extra cost. Compact or summarize when early facts still matter later in the conversation, accepting one extra model call to preserve meaning at a fraction of the token cost.
Second gate: do stretched positions still mean what you think?
The positional encoding article explains why attention needs a position signal. Here the operational question is narrower: what happens when a model trained on one range receives positions far beyond it? Rotary Position Embeddings (RoPE)[8] rotates those position encodings into attention, and interpolation or rescaling tries to extend that learned coordinate system. Llama 3 also raised its RoPE base to 500,000, stretching the useful range before interpolation.[3]
RoPE basics
Picture each pair of embedding dimensions as a dial. Dials rotate at different speeds, so a token's position is a combination of angles rather than one scalar. Comparing two rotated vectors exposes their relative offset, which gives attention distance-aware information without adding a separate lookup row for every position.
RoPE encodes position as rotations in 2D subspaces of the embedding dimension:
Reading the formula
Read the formula from left to right: position rotates component by an angle proportional to . Fast frequencies preserve fine local order; slow frequencies change more gradually and can represent larger separations. The relative angle between two positions is what attention can compare.
Position interpolation and NTK-aware scaling
Suppose training stopped at 8,192 tokens and deployment targets 32,768. Naive extrapolation sends the dials into angles the model never saw. Position interpolation instead maps target positions back into the trained range:
Uniform interpolation protects the range by compressing every frequency band equally. NTK-aware (Neural Tangent Kernel) scaling changes that compromise: it stretches low-frequency dimensions more while keeping high-frequency dimensions closer to their original behavior. Try it to protect local precision, not because every checkpoint tolerates the same factor.
1def interpolate_position(position: int, trained_window: int, target_window: int) -> float:
2 """Map an extended position into the original coordinate range."""
3 return position * trained_window / target_window
4
5trained_window = 8_192
6target_window = 32_768
7for position in [0, 8_192, 16_384, 32_767]:
8 mapped = interpolate_position(position, trained_window, target_window)
9 print(f"extended position {position:>5} -> trained coordinate {mapped:7.2f}")1extended position 0 -> trained coordinate 0.00
2extended position 8192 -> trained coordinate 2048.00
3extended position 16384 -> trained coordinate 4096.00
4extended position 32767 -> trained coordinate 8191.75The arithmetic explains the idea; serving code usually selects a tested variant in configuration. In Hugging Face Transformers, rope_parameters names the scaling family: linear is uniform interpolation, dynamic is the NTK-style option, yarn is YaRN, longrope follows LongRoPE, and llama3 is the Llama 3.1 frequency split.[9][10]
1from transformers import LlamaConfig
2
3config = LlamaConfig()
4config.rope_parameters = {
5 "rope_type": "dynamic",
6 "rope_theta": 10000.0,
7 "factor": 4.0,
8}The configuration still belongs to the checkpoint. A "yarn" setup carries fields such as original_max_position_embeddings and, optionally, attention_factor.[9] Llama 3 checkpoints use a larger rope_theta than the 10,000 default in this example, so copy the published model value instead of guessing.

Why does naive RoPE extrapolation degrade when context exceeds the training range?
Answer
The model sees position rotations and frequency combinations it didn't learn during training. Scaling methods map or reshape those positions to preserve useful relative distances, but aggressive extension still needs validation and often continued training.
YaRN (Yet another RoPE extensioN)
YaRN makes the selective compromise explicit. It combines NTK scaling with a temperature factor on attention logits and a smooth frequency ramp.[11] High-frequency dimensions receive no interpolation to protect local order, low-frequency dimensions receive full interpolation to gain range, and middle frequencies ramp between them.
That treatment improved long-context perplexity over plain interpolation at aggressive extension ratios in the YaRN evaluation.[11] It's evidence for testing the strategy, not a portability guarantee. Each new model family needs its own recall and loss checks.
Why does YaRN treat frequency bands differently instead of scaling every RoPE dimension the same way?
Answer
High-frequency bands carry local order and syntax, so over-compressing them hurts short-range precision. Lower-frequency bands can stretch more to represent longer distances. YaRN blends those regimes instead of applying one uniform scale.
Third gate: can the model reach the evidence?
Now return to the canary note. Even with a valid budget and an extended position range, retrieval can depend on where that note lands. Liu et al. found that accuracy varied with evidence position in their evaluated tasks and models, with stronger performance near the beginning or end than in the middle.[12]
That makes prompt layout part of system design. A model can ingest every trace and still miss the rollback approval if it sits in the weak middle. Treat the context as a map of reachable evidence, not as a bag whose tokens all receive equal attention.
What the curve looks like
The exact numbers depend on model and task, but Liu et al. report the same shape across many evaluations:[12]
| Placement | Typical Pattern |
|---|---|
| Beginning of context | Often among the strongest positions |
| Middle of context | Most failure-prone |
| End of context | Usually recovers relative to the middle |
What does the lost-in-the-middle curve imply for prompt layout?
Answer
Must-keep evidence and instructions should sit near the head or tail of the prompt, not in the middle alone. Middle evidence should be duplicated, summarized, extracted, or promoted when recall matters.
Mitigation strategies
Strategic information placement
When a depth sweep exposes a middle miss, change layout before changing the model. Suppose five retrieved chunks describe the failed deployment: a rollback approval, a canary error, a deploy note, a noisy log, and a generic runbook clause. Put the two facts that answer the incident at opposite edges, then leave weaker support in the middle.
The function below turns that hypothesis into a candidate prompt. It doesn't prove the layout works; it gives you a controlled prompt to compare with the unchanged baseline:
1from dataclasses import dataclass
2
3@dataclass
4class Document:
5 text: str
6 relevance: float
7
8def arrange_context(
9 system_prompt: str,
10 retrieved_docs: list[Document],
11 user_query: str,
12) -> str:
13 """Put the two strongest chunks at the edges; leave the rest in the middle."""
14 ranked_docs = sorted(retrieved_docs, key=lambda d: d.relevance, reverse=True)
15 head_docs = ranked_docs[:1]
16 tail_docs = ranked_docs[1:2]
17 middle_docs = ranked_docs[2:]
18
19 context = [system_prompt]
20 context.extend(d.text for d in head_docs)
21 context.extend(d.text for d in middle_docs)
22 context.extend(d.text for d in tail_docs)
23 context.append(user_query)
24
25 return "\n\n".join(context)
26
27docs = [
28 Document("Rollback approved on 2024-03-15 by incident lead #42.", 0.95),
29 Document("Original canary error: auth callback returned 500.", 0.92),
30 Document("Deploy owner requested a staged rollback.", 0.88),
31 Document("Noisy log excerpt: cache warmed successfully.", 0.45),
32 Document("Generic rollback runbook clause 7B.", 0.30),
33]
34
35prompt = arrange_context(
36 system_prompt="You are an incident assistant. Answer using only the evidence below.",
37 retrieved_docs=docs,
38 user_query="Was the rollback approved?",
39)
40print(prompt)1You are an incident assistant. Answer using only the evidence below.
2
3Rollback approved on 2024-03-15 by incident lead #42.
4
5Deploy owner requested a staged rollback.
6
7Noisy log excerpt: cache warmed successfully.
8
9Generic rollback runbook clause 7B.
10
11Original canary error: auth callback returned 500.
12
13Was the rollback approved?The output puts rollback approval at the head and the canary error at the tail, with weaker support between them. Compare this candidate with the unchanged baseline on the same depth sweep before adopting it.
Why does the sample packing function split high-relevance chunks between head and tail?
Answer
Both edges tend to be easier for long-context models to use than the middle. Splitting top evidence across head and tail gives must-keep facts two strong positions instead of burying everything in one region.
Repeated key information
If a fact is essential and cheap to restate, put a compact version in the system frame and near the query. The two placements give the same fact two opportunities to be used, while the depth sweep tells you whether duplication helps enough to justify its token cost.
Chunked processing
If one prompt still has too much distractor text, process the document in chunks and aggregate the partial results. Chunking changes the question from “did one pass find every fact?” to “did the aggregation preserve the facts needed for the final answer?”

The packing policy places the two strongest facts at the sequence edges and lower-priority support in the middle. That's a hypothesis about reachability, not a rule to memorize. If position-sliced evaluation still misses evidence, change the packing order or retrieve a smaller set and rerun the same cases.
Fourth gate: should you read everything or retrieve first?
The serving path starts with the question, not the model's maximum window. For the rollback incident, ask whether the evidence fits with output headroom, whether it must be fresh or cited, whether the same corpus will be queried repeatedly, and whether the answer needs a joint scan or one targeted fact.

The product choice is often a large context window versus retrieval-augmented generation (RAG). Long context scans a selected packet together, which helps when relationships across that packet matter but makes prefill larger. RAG finds candidate sections first, which shrinks the generation prompt but adds retrieval latency and a new failure: the needed section may never be selected.
For one question over a short, stable policy, full context is a sensible baseline. For repeated questions, fresh data, or a targeted lookup across a large archive, retrieval is usually the first comparison to measure. The table keeps those costs visible before you commit to a route.
| Factor | Long Context | RAG |
|---|---|---|
| Latency | One generation call, but large prefills can dominate | Retrieval adds a stage, while smaller prompts can reduce generation cost |
| Cost | Pays for packed input on each uncached request | Pays for indexing/retrieval plus selected chunks |
| Failure mode | Evidence is present but may be missed by position or distractors | Needed evidence may never be retrieved |
| Corpus scale | Bounded by usable prompt budget | Searches corpora larger than one prompt, subject to retrieval quality |
| Operational work | Packing, caching, and context evaluation | Chunking, indexing, ranking, and retrieval evaluation |
Repeated queries over the same large prefix deserve a separate look. Even if the corpus fits, resending it on every turn repeats prefill work and keeps the cache occupied. Cache or retrieve reusable evidence first, then spend the remaining long-context budget on the part that needs joint reasoning.
When should you prefer RAG or hybrid retrieval even if a document technically fits in the model context?
Answer
Prefer RAG or hybrid when data must be fresh, citations matter, queries repeat over the same corpus, or only a small evidence subset is relevant. Fitting everything can still be slower, costlier, and less reliable because of position effects.
A concrete decision example
Suppose the archive holds 200,000 tokens of service traces and the model limit is 128K. The question is, “Which service emitted the most timeout errors in March?” A top-k retriever might omit records needed for a count, while the full archive can't fit. The route is hybrid: filter or retrieve March entries into a bounded pack, aggregate over that pack, and validate against known totals.
Predict the function's result before reading it: the corpus is too large and the answer needs a joint scan, so it should return hybrid. The code encodes that routing rule:
1def choose_strategy(
2 corpus_size_tokens: int,
3 model_context_limit: int,
4 requires_global_reasoning: bool,
5 needs_freshness: bool,
6 repeated_queries: bool,
7) -> str:
8 """Choose between long context, RAG, and a hybrid pipeline."""
9
10 fits_in_context = corpus_size_tokens <= model_context_limit
11
12 if needs_freshness:
13 return "hybrid" if requires_global_reasoning else "rag"
14
15 if not fits_in_context:
16 return "hybrid" if requires_global_reasoning else "rag"
17
18 if repeated_queries:
19 return "hybrid" if requires_global_reasoning else "rag"
20
21 return "long_context"
22
23# Concrete example
24strategy = choose_strategy(
25 corpus_size_tokens=200_000,
26 model_context_limit=131_072,
27 requires_global_reasoning=True,
28 needs_freshness=False,
29 repeated_queries=False,
30)
31print(strategy)1hybridIt returns hybrid. Retrieval narrows the archive; long-context reasoning then scans the bounded March packet instead of pretending top-k retrieval can answer a global count.
Why is the 200,000-token trace-log example a hybrid case?
Answer
The corpus exceeds the 128K prompt limit, but the question needs broad March aggregation rather than one isolated chunk. Retrieval narrows the corpus to March evidence, then long-context reasoning scans the packed subset.
Fifth gate: can the chosen path run on real GPUs?
Once the evidence path is chosen, capacity becomes the release constraint. Long-context serving often runs out of room in the KV cache before it runs out of model weights, especially when several long requests decode together.
Grouped query attention (GQA)
GQA (Grouped-Query Attention)[13] lowers KV-cache bytes by sharing key/value heads across query groups. MHA gives every query head its own K/V heads, GQA shares K/V heads across groups, and MQA shares one K/V set across all query heads. In an otherwise comparable architecture, moving from 64 query/KV heads to 8 KV heads cuts the cache term by 8x; moving to one cuts it by 64x. Those are memory ratios, not quality or throughput guarantees.
See our MQA/GQA deep-dive for the full architecture details.
Head sharing isn't the only architectural KV cut. Multi-head latent attention (MLA) stores a compressed latent per token instead of full key/value heads. DeepSeek-V2 reports a cache about as small as GQA with 2.25 groups and a 93.3% KV-memory reduction versus DeepSeek 67B.[14] MLA is a trained architecture choice, not a serving flag for a GQA checkpoint. You still need quality and concurrency measurements after any cache change.
1def relative_kv_bytes(query_heads: int, kv_heads: int) -> float:
2 return kv_heads / query_heads
3
4query_heads = 64
5for label, kv_heads in [("MHA", 64), ("GQA", 8), ("MQA", 1)]:
6 fraction = relative_kv_bytes(query_heads, kv_heads)
7 print(f"{label}: {fraction:.3f}x MHA KV bytes ({1 / fraction:.0f}x smaller)")1MHA: 1.000x MHA KV bytes (1x smaller)
2GQA: 0.125x MHA KV bytes (8x smaller)
3MQA: 0.016x MHA KV bytes (64x smaller)Why does GQA matter so much for long context?
Answer
The KV cache stores key and value heads, not query heads. Sharing K/V heads across query groups reduces bytes per token, so the savings multiply across long context length and concurrent requests.
Quantized KV-cache
If the cache still crowds out concurrency, reduce bytes per element. BF16 or FP16 stores each cached element in 2 bytes; an FP8 KV-cache candidate uses 1 byte, so the 40 GiB fixture falls to about 20 GiB. That buys capacity, not free quality. Calibrated scales and long-depth tests decide whether the trade survives your workload.
| Cache dtype | Bytes per cached element | Relative KV size |
|---|---|---|
| BF16 / FP16 | 2 bytes | 1.0x |
| FP8 | 1 byte | ~0.5x |
The snippet below follows current vLLM documentation for an uncalibrated FP8 cache, where scales start at 1.0. For higher quality, calibrate scales on a dataset through llm-compressor and load them from the checkpoint. Sliding-window layers may be more sensitive, so the same documentation exposes kv_cache_dtype_skip_layers.[15] Check support for your runtime version, model, and accelerator before treating this as a capacity plan.
1from vllm import LLM
2
3llm = LLM(
4 model="your-org/your-model",
5 kv_cache_dtype="fp8",
6 kv_cache_dtype_skip_layers=["sliding_window"],
7)1def admitted_sequences(memory_budget_gib: float, kv_per_request_gib: float) -> int:
2 return int(memory_budget_gib // kv_per_request_gib)
3
4cache_budget = 64.0 # example budget after reserving weights and runtime memory
5for dtype, kv_gib in [("BF16", 40.0), ("FP8 candidate", 20.0)]:
6 slots = admitted_sequences(cache_budget, kv_gib)
7 print(f"{dtype}: at most {slots} full-length request(s) in cache budget")1BF16: at most 1 full-length request(s) in cache budget
2FP8 candidate: at most 3 full-length request(s) in cache budgetWhat should you verify after enabling FP8 KV cache?
Answer
Verify long-context retrieval, multi-hop reasoning, ordering, and latency on your workload. FP8 can roughly halve KV memory, but poor calibration or unsupported kernels can damage quality or fail to improve throughput.
PagedAttention (vLLM)
Bytes per token are only half of capacity. A contiguous reservation can strand free memory as requests grow and finish. PagedAttention manages KV in non-contiguous blocks or “pages,” like virtual memory, so fixed-size blocks are allocated on demand (see our KV cache and PagedAttention deep-dive). The vLLM paper describes near-zero waste and sharing for common prefixes in its evaluated workloads.[2]
That layout reduces reservation and fragmentation waste, but it does not change the intrinsic KV bytes for a model and sequence length. It can make more of the GPU usable; it can't make one 128K sequence weigh less. The paper reported higher throughput in its evaluated serving workloads.[2]
Prefix reuse and prompt caching
The incident archive may also have a 48K prefix shared by every question: instructions, schemas, and a stable runbook. Prefix sharing reuses materialized prompt blocks for common prefixes instead of recomputing them from scratch.[2][16] In current vLLM, the switch is enable_prefix_caching=True.[16]
Prefix reuse cuts repeated prefill; it doesn't raise the context limit or improve model quality. The example below counts work across three questions so you can see why repeated queries change the economics.
1def prefill_tokens_without_reuse(shared_prefix: int, unique_suffixes: list[int]) -> int:
2 return sum(shared_prefix + suffix for suffix in unique_suffixes)
3
4def prefill_tokens_with_reuse(shared_prefix: int, unique_suffixes: list[int]) -> int:
5 return shared_prefix + sum(unique_suffixes)
6
7shared_prefix = 48_000
8questions = [800, 1_200, 600]
9uncached = prefill_tokens_without_reuse(shared_prefix, questions)
10reused = prefill_tokens_with_reuse(shared_prefix, questions)
11print(f"uncached input tokens processed: {uncached:,}")
12print(f"with reusable prefix candidate: {reused:,}")
13print(f"avoided repeated prefix tokens: {uncached - reused:,}")1uncached input tokens processed: 146,600
2with reusable prefix candidate: 50,600
3avoided repeated prefix tokens: 96,000Ring attention across multiple GPUs
PagedAttention improves allocation on each device. It doesn't solve a request whose attention work can't fit on one device. Ring Attention partitions blockwise attention across devices and overlaps KV-block communication with blockwise computation; its paper reports context scaling with added devices in evaluated setups.[17] Communication and implementation overhead remain deployment constraints, so this is a scale-out choice, not a replacement for budgeting.
How do PagedAttention and Ring Attention solve different long-context problems?
Answer
PagedAttention packs KV cache efficiently on each device and reduces fragmentation. Ring Attention distributes one very long sequence across devices when the request itself can't fit on a single GPU.
Final gate: does it work at the lengths and depths you will serve?
Capacity numbers and a successful short prompt aren't a release test. NIAH (Needle-in-a-Haystack) hides a known fact, the “needle,” at controlled positions or “depths” inside filler, the “haystack,” then asks the model to retrieve it.[18] For the incident, the needle can be the rollback approval while irrelevant traces fill the rest.
Sweep context lengths from 4K to 128K and depths from 0% to 100% to build a heatmap. A model that retrieves every tested needle stays uniform; a position-sensitive model develops weak middle cells as length grows. The visual below is an illustrative failure surface, not a score for any named model.

NIAH tests one fact. RULER[19] broadens the probe to multiple needles, multi-hop tracing across distant records, and aggregation before answering. Those tasks expose whether the model can combine evidence rather than merely spot a highlighted sentence.
Pair retrieval scores with perplexity or next-token loss by sequence length. A healthy extension should not show a sudden loss spike immediately beyond the original training window. If RoPE or cache changes cause a sharp jump, investigate configuration or distribution shift before blaming a harder task.
A 2025 Chroma report tested 18 models and described reliability degradation with longer inputs, distractors, and less explicit query-answer links as context rot.[20] Treat that report as a prompt to test your model and workload, not as a universal accuracy curve. A larger window permits more input; it doesn't prove every added token helps.
1results = {
2 4_096: {0: True, 50: True, 100: True},
3 131_072: {0: True, 50: False, 100: True},
4}
5
6def weakest_depths(depth_results: dict[int, bool]) -> list[int]:
7 return [depth for depth, found in depth_results.items() if not found]
8
9for length, depth_results in results.items():
10 misses = weakest_depths(depth_results)
11 print(f"{length:>6} tokens: missed depths={misses or 'none'}")14096 tokens: missed depths=none
2131072 tokens: missed depths=[50]A real runner must control depth without spending the output budget. The next function uses integer token ids so you can inspect the insertion index, actual depth, and generation headroom before wiring in a model.
1def place_needle(
2 haystack: list[int],
3 needle: list[int],
4 instruction: list[int],
5 context_limit: int,
6 requested_depth: float,
7 max_new_tokens: int,
8) -> tuple[list[int], float]:
9 """Insert needle ids at a requested depth and keep input-plus-output inside the limit."""
10 document_budget = context_limit - max_new_tokens - len(instruction)
11 if document_budget < len(needle):
12 raise ValueError("context limit is too small for instruction, needle, and output")
13
14 filler_budget = document_budget - len(needle)
15 clipped = haystack[:filler_budget]
16 insert_idx = int(len(clipped) * requested_depth)
17 document = clipped[:insert_idx] + needle + clipped[insert_idx:]
18 packed = instruction + document
19 if len(packed) + max_new_tokens > context_limit:
20 raise AssertionError("packed prompt plus generation exceeds the context limit")
21 actual_depth = insert_idx / max(len(clipped), 1)
22 return packed, actual_depth
23
24packed, depth = place_needle(
25 haystack=list(range(20)),
26 needle=[99, 99],
27 instruction=[1, 2],
28 context_limit=24,
29 requested_depth=0.5,
30 max_new_tokens=4,
31)
32print(packed)
33print(f"tokens={len(packed)} actual_depth={depth:.2f}")1[1, 2, 0, 1, 2, 3, 4, 5, 6, 7, 99, 99, 8, 9, 10, 11, 12, 13, 14, 15]
2tokens=20 actual_depth=0.50The packed sequence keeps 2 instruction tokens, inserts the needle at 50% of the clipped haystack, and leaves 4 generation tokens under the 24-token limit. A real harness must still vary filler templates, needles, and seeds because distractors can change retrieval.
1def approve_long_context_change(
2 baseline_middle_recall: float,
3 candidate_middle_recall: float,
4 p95_latency_ratio: float,
5 memory_ratio: float,
6) -> bool:
7 recall_ok = candidate_middle_recall >= baseline_middle_recall
8 latency_ok = p95_latency_ratio <= 1.10
9 memory_ok = memory_ratio <= 1.05
10 return recall_ok and latency_ok and memory_ok
11
12approved = approve_long_context_change(
13 baseline_middle_recall=0.86,
14 candidate_middle_recall=0.89,
15 p95_latency_ratio=1.06,
16 memory_ratio=1.02,
17)
18print(f"long-context candidate approved: {approved}")1long-context candidate approved: TrueWhy is a single needle-in-a-haystack result not enough to trust a long-context model?
Answer
One needle tests one fact, one distractor style, one position, and often one seed. Real systems need sweeps over positions, lengths, multiple needles, distractor templates, and synthesis tasks such as RULER-style multi-hop reasoning.
What to carry forward
Start with fit, but don't stop at acceptance. A context window can admit long input while the model misses evidence at particular depths or under distractors, so depth sweeps and synthesis tasks measure effective use.
Treat RoPE scaling as controlled interpolation. Position interpolation, NTK-aware scaling, YaRN, LongRoPE, and Llama 3.1's frequency split try to extend range while protecting local resolution, but each checkpoint still needs validation.
Treat lost-in-the-middle as a layout failure you can test. Put must-keep evidence at the head or tail, duplicate or summarize it when its token cost is justified, and compare against a baseline.
Choose long context or RAG from the evidence shape. Fit, freshness, query repetition, citation needs, and joint reasoning determine whether you should read a packet, retrieve sections, or combine both.
Then price the serving path twice. GQA, MLA, sliding-window layers, FP8 KV caches, PagedAttention, prefix reuse, and distributed attention change different prefill, decode, or allocation terms. Benchmark them against recall, latency, quality, and capacity gates.
That's the systems connection: position extension decides what coordinates mean, evidence layout decides what can be reached, and KV-cache math decides whether the request can run at useful concurrency.