Advanced

Chunked prefill for long context

A 128K-token prompt means a massive prefill. Chunked prefill breaks it into bite-sized pieces so decode requests keep flowing, latency stays bounded, and GPU utilization stays high.

Long context is the feature everyone wants and nobody wants to pay for. A 128K-token prompt on a 70B model means billions of FLOPs of prefill compute, all in one shot. If you run that as a single continuous prefill, every decode request in the same batch has to wait for it to finish. Your P99 latency spikes, your throughput tanks, and your users notice.

Chunked prefill is the fix. Instead of processing the entire prompt in one monolithic forward pass, you break it into chunks of, say, 512 or 2048 tokens, and interleave those chunks with decode steps from other requests. It is one of those ideas that sounds obvious in retrospect but requires careful engineering to get right.

The problem: prefill as a bully

Recall from the roofline post that prefill is compute-bound. It does large matrix multiplications across all prompt tokens in parallel, which is great for GPU utilization but terrible for sharing. A single 128K prefill on an H100 can take several seconds. During that time, any request in the decode phase is stuck waiting for its next token.

In a continuous batching system like vLLM, new requests can join the batch at each iteration. But if one request is doing a monster prefill, the iteration itself takes a long time. Everyone else in the batch is hostage to the longest prefill.

The math is straightforward. For a transformer layer, the prefill compute for the attention block scales as O(n^2 * d) where n is the sequence length and d is the head dimension. Double the prompt length and you quadruple the attention compute. At 128K tokens, this dominates everything.

How chunked prefill works

The idea is simple: instead of prefilling all 128K tokens in one iteration, break them into chunks of a fixed size (the "chunk budget") and process one chunk per scheduler iteration. Between chunks, the scheduler can run decode steps for other requests.

# Conceptual scheduler loop with chunked prefill
chunk_budget = 2048  # tokens per iteration for prefill

while requests_pending():
    batch = []

    # 1. Schedule decode tokens first (they're cheap)
    for req in decode_queue:
        batch.append(req.next_decode_token())

    # 2. Fill remaining budget with prefill chunks
    remaining = chunk_budget - len(batch)
    for req in prefill_queue:
        chunk = req.get_next_chunk(max_tokens=remaining)
        batch.append(chunk)
        remaining -= len(chunk)
        if remaining <= 0:
            break

    # 3. Run the batch through the model
    forward_pass(batch)

The key insight is that decode tokens are always prioritized. Each decode step for a request is exactly one token, so it is cheap. The chunk budget controls how much prefill work gets done per iteration, bounding the worst-case iteration time.

What changes inside the model

Chunked prefill introduces a complication: the KV cache for a prefilling request is built incrementally. After processing the first chunk of 2048 tokens, those KV entries are in the cache. The next chunk attends to both the cached keys/values from previous chunks and the new tokens in the current chunk.

This means the attention computation for chunk i looks different from a standard prefill:

In practice, most implementations fuse these two components. vLLM's scheduler, for example, handles this transparently. The model sees a batch containing both "partial prefill" and "decode" tokens, and FlashAttention kernels handle the mixed attention patterns efficiently.

Choosing the chunk size

The chunk budget is the main knob, and it involves a real tradeoff:

In vLLM, the default chunk size is 512 tokens for chunked prefill. Sarathi-Serve, the research system that pioneered this approach, found that chunk sizes between 256 and 2048 tokens give good tradeoffs for most workloads. The sweet spot depends on your model size, GPU, and the ratio of long-context to short-context requests in your traffic.

Rule of thumb

Set the chunk budget so that one iteration (decode batch + prefill chunk) takes roughly the same time as a decode-only iteration at your target batch size. This keeps inter-token latency stable regardless of what prefill work is happening in the background.

The Sarathi-Serve insight: stall-free batching

The Sarathi-Serve paper (Microsoft Research, 2024) formalized chunked prefill as "stall-free batching." Their key observation was that without chunking, continuous batching systems have a bimodal iteration time distribution: fast iterations when only decodes are running, and slow iterations when a prefill lands. This bimodality makes tail latency unpredictable.

With chunked prefill, every iteration does roughly the same amount of work. The iteration time distribution becomes unimodal. P99 latency drops dramatically, sometimes by 3 to 5x, even though median latency barely changes.

They also showed that chunked prefill actually improves GPU utilization in many cases. The prefill chunks fill the compute capacity that would otherwise be wasted during memory-bound decode iterations. You are essentially piggy-backing prefill compute on decode bandwidth.

Interaction with other techniques

Chunked prefill plays well with the rest of the inference stack:

What I would measure

If you are enabling chunked prefill in production, here is what to watch:

Chunked prefill is not an optimization. It is a scheduling discipline. It trades a small TTFT regression for dramatically better tail latency, and in production, tail latency is what your users feel.

Next up: small fine-tuned vs large quantized, the real deployment tradeoff nobody talks about enough.