Open the workspace
Inference 101

Chunked prefill (stall-free batching), explained

How chunked prefill splits long prompts so decodes never stall: interference math, chunking overhead, token-budget tuning and a seeded serving simulation.

Chunked prefill splits the processing of a long prompt into fixed-size pieces and runs each piece in the same forward pass as the single-token decode steps of the requests already generating. Instead of a 4,096-token prompt occupying the GPU for one 120 ms iteration while every other user waits for their next token, it is spread over eight iterations of about 16 ms, each of which also emits a token for everyone else. It is a refinement of continuous batching: the scheduler still rebuilds the batch every iteration, but now it also decides how many prompt tokens of each request to include, under a per-iteration token budget. vLLM enables it by default in its V1 engine, SGLang and TensorRT-LLM ship equivalent modes, and the single setting that controls it, the token budget, is one of the most consequential numbers in a serving configuration.

Where it came from#

The technique was introduced as chunked-prefills with decode-maximal batching in SARATHI (Agrawal, Panwar, Mohan, Kwatra, Gulavani and Ramjee, 2023). Their observation was that the two phases of a request use the GPU in opposite ways. A prefill iteration multiplies thousands of prompt tokens against the weights and saturates compute. A decode iteration multiplies one token per sequence against the same weights and is limited by memory bandwidth. If one prefill chunk is paired with as many decodes as fit, the decodes "piggyback" on weight reads the prefill was paying for anyway. SARATHI reported up to 10 times higher decode throughput and up to 1.33 times higher end-to-end throughput for LLaMA-13B on an A6000, and 6.29 times fewer pipeline bubbles with pipeline parallelism on GPT-3.

The follow-up, Sarathi-Serve (Agrawal et al., OSDI 2024), reframed the idea around latency. Iteration-level schedulers that prioritise prefills, like vLLM's original scheduler, create generation stalls: whenever a prefill is scheduled between two decode iterations, every running request's next token is delayed by the whole prefill, and the authors observed stalls lasting several seconds in vLLM. Schedulers that prioritise decodes, like FasterTransformer's request-level batching, avoid stalls but leave the batch small and throughput low. Stall-free scheduling with a token budget gets both. Sarathi-Serve reported 2.6 times higher serving capacity than vLLM for Mistral-7B on one A100 and up to 3.7 times for Yi-34B on two A100s under tail-latency constraints. In parallel, DeepSpeed-FastGen (Holmes et al., 2024) shipped the same idea under the name Dynamic SplitFuse, reporting up to 2.3 times higher effective throughput and 3.7 times lower tail latency than vLLM at the time.

The bottleneck: prefill-decode interference#

Use the roofline step-time model from the roofline article. For an iteration that processes prompt tokens and decode tokens, with parameters in 16-bit precision, achievable bandwidth , achievable compute , and KV traffic bytes:

The two phases sit on opposite sides of the ridge . With 60% of 989 TFLOP/s and 80% of 3.35 TB/s (our assumption for an H100-class GPU), tokens. A batch of 64 decodes at 1,125-token contexts takes 10.0 ms in this model, almost all of it memory traffic. A 4,096-token prefill takes 118 ms, almost all of it compute. Put them in the same iteration and the decodes cost almost nothing extra in the linear layers (they are 64 more rows in a 4,160-row matrix multiply), but they now have to wait 118 ms for their token.

The quantity users feel is the time between tokens (TBT), and its tail is what an SLO constrains. Without chunking, the worst-case gap for a running request is roughly one decode step plus the longest prompt admitted in between:

where is the token budget per iteration and the number of decoding sequences. The first grows with the longest prompt anyone sends; the second is bounded by a number you choose.

64 sequences decoding; a 4,096-token prompt arrives at t = 20 ms (model numbers)prefill-firstprefill 4,096 tokens, no decode outputchunked, 512decode stall: 118 ms with no token for 64 users8 mixed iterations of 16.2 to 17.8 ms, each emits 64 decode tokens020406080100120140160180200time (ms)decode-only iterationprefill-only iterationprefill chunk + decodes
Figure 1. The same arrival under two schedulers, drawn to scale from the step-time model. Without chunking, 64 users see a 118 ms gap. With 512-token chunks the prompt finishes in 136 ms instead of 118 ms, but nobody waits more than 18 ms for a token.

How it works, step by step#

Every iteration the scheduler fills a token budget in a fixed priority order:

  1. Add one token for every running decode. Each costs one unit of budget.
  2. If a prefill was already partly processed, add its next chunk: tokens starting at offset .
  3. Admit waiting requests in arrival order while budget, sequence slots and KV blocks remain, giving each as many prompt tokens as fit.
  4. Run one forward pass over the flattened tokens. Prompt chunks write their keys and values into the KV cache and attend causally to everything cached for their request so far; decodes append one token.
  5. A request whose last prompt chunk completes samples its first token in that same pass and becomes a decode in the next iteration.
def schedule(running, partial, waiting, budget, max_seqs, kv):
    batch, left = [], budget
    for s in running:                                  # 1. decodes first: never stall them
        batch.append((s, 1)); left -= 1
    for s in partial + waiting:                        # 2-3. continue partial prefills, then admit
        if left <= 0: break
        if s in waiting:
            if len(running) + len(partial) >= max_seqs or not kv.can_allocate(s): break
            waiting.remove(s); partial.append(s)
        n = min(left, s.prompt_len - s.prefilled)
        batch.append((s, n)); left -= n
    return batch                                       # 4. one ragged forward pass

The only new state is prefilled per request. The attention kernel must handle a query block of tokens attending to keys, partly read from the paged KV cache, which variable-length attention kernels such as FlashAttention's varlen and paged variants already do.

What chunking costs

Chunking is not free, and the costs set a floor on the budget.

Repeated KV reads. Chunk of a prompt (counting from 0) must read the cached tokens before it. Over chunks the extra traffic is

with the KV bytes per token. For a 4,096-token prompt in 512-token chunks on a model with bytes (Llama-3.1-8B shape), that is GB, or 0.70 ms at 2.68 TB/s. Small, because attention over a prefill chunk is compute-bound anyway, as Sarathi-Serve also notes.

Losing the ridge. The real floor is the weight read. Each chunk re-reads all bytes of weights, so a chunk smaller than is memory-bound and pays the full weight-read time for fewer than tokens of work:

In our model an 8,192-token prompt takes 251 ms in one shot. In 128-token chunks it takes 427 ms (70% overhead); at 256 the overhead is 6.2%, at 512 it is 3.0%, at 2,048 it is 0.6% (Figure 2). Running decodes fill some of the gap in a mixed batch, but the lesson stands: keep the budget comfortably above the ridge of your GPU and model.

Tile quantisation. Matrix multiplication kernels work in tiles, so a chunk of 257 tokens can cost much more than 256. Sarathi-Serve measured a 32% longer prefill for chunk size 257 than for 256. Use budgets that are multiples of 128 or 256 and remember that the decode tokens count against the budget, so the prefill share is , not .

Extra prefill time from chunking an 8,192-token prompt (model)0204060801282565121,0242,0484,096chunk size (tokens)overhead vs one-shot prefill (%)ridge, about 221 tokens8,192-token prompt
Figure 2. Extra prefill time from chunking, computed from the step-time model for an 8,192-token prompt with no concurrent decodes. Below the compute ridge each chunk pays the full weight read for too little work.

Worked example#

Sixty-four users are decoding with 1,125-token contexts. A 4,096-token prompt arrives at ms. Numbers come from the step-time model with an 8B model on one H100-class GPU.

Without chunking (prefill-first). The next iteration is the whole prefill: 118 ms in which the 64 users receive nothing. Their gap is ms instead of the usual 10 ms. The new request's first token appears at ms.

With 512-token chunks. Each iteration carries the 64 decode tokens plus one 512-token chunk of the prompt (a budget of 576 once decodes are counted). Each mixed iteration takes 16.2 to 17.8 ms, rising slightly as the chunk attends to more cached prefix, and eight of them finish the prompt in 136 ms. The new user's first token arrives 18 ms later than without chunking (15% longer TTFT for this request), and in exchange the 64 existing users never wait more than 17.8 ms, a 7.2 times smaller worst-case gap. Their median time per output token also falls, because they keep producing tokens during the prefill instead of idling through it.

The arithmetic of the trade is general. Chunking moves latency from the many running requests to the one arriving request, and the exchange rate is favourable whenever more than a handful of requests are decoding.

Simulated numbers: choosing the budget#

We extended the seeded simulator from the continuous batching article with a chunked scheduler (decodes first, then partial prefills, then new admissions) and replayed Poisson arrivals with lognormal prompts (mean 1,000 tokens, up to 8,192) and outputs (mean 250). Because real kernels may or may not overlap decode attention with prefill compute, we ran two step-time models: an optimistic one in which memory traffic and compute in an iteration fully overlap, and a pessimistic one in which decode attention runs after the matrix multiplies and adds its full memory time. Everything below is model output, not a benchmark.

Scheduler at 16 req/sMedian TTFTp99 TTFTMedian TPOTp99 time between tokensp99 TBT, pessimistic model
Prefill-first, no chunking41 ms305 ms21.8 ms207 ms207 ms
Chunked, budget 25679 ms465 ms8.8 ms9.9 ms11.5 ms
Chunked, budget 51249 ms271 ms11.2 ms15.7 ms20.6 ms
Chunked, budget 1,02448 ms253 ms13.8 ms30.3 ms36.0 ms
Chunked, budget 2,04844 ms254 ms15.2 ms58.5 ms64.4 ms
Chunked, budget 8,19244 ms302 ms15.7 ms99.2 ms109 ms

The p99 gap between tokens is set almost entirely by the budget: roughly 16, 30, 59 and 99 ms for budgets of 512, 1,024, 2,048 and 8,192, against 207 ms without chunking. Very small budgets buy smooth decoding with slower first tokens: at 256, p99 TTFT rises to 465 ms optimistic and 836 ms pessimistic, because each prompt needs many more iterations. That matches the vLLM documentation's guidance that smaller budgets (around 2,048) favour inter-token latency and larger ones (above 8,192) favour TTFT.

p99 time between tokens vs arrival rate (simulated, 8B, one GPU)101008121620242832arrival rate (requests per second)p99 inter-token gap (ms)50 ms SLOprefill-first (no chunking)chunked, budget 512chunked, budget 2,048chunked, budget 8,192
Figure 3. p99 time between tokens against arrival rate in the optimistic model (log scale). With chunking the tail is pinned by the budget at every load level; without it, the tail grows with load until the server saturates. The prefill-first curve dips after 20 requests per second only because a permanently full batch admits new prompts less often.

Throughput is where the two step-time models disagree, which is itself the useful finding. In the optimistic model, saturated throughput rose from 4,431 output tokens per second without chunking to 5,924 with a 512 budget (34% more), then fell to 5,182 at 2,048 and 4,874 at 256, the last because chunks near the ridge pay the weight read too often. In the pessimistic model every policy saturated between 4,373 and 4,496 tokens per second. Chunked prefill is reliably a latency tool; how much throughput it adds depends on how well your kernels overlap mixed batches, which is the gap POD-Attention (Kamath et al., 2024) targets with a fused prefill-decode attention kernel that speeds attention up by up to 59%.

Goodput, the highest arrival rate at which p99 TBT stays under 50 ms and p99 TTFT under 1 s, makes the choice concrete. Prefill-first failed the TBT target at every rate we tested (8 requests per second and up). Budgets of 512 and 1,024 sustained 24 and 26 requests per second in the optimistic model and 16 and 18 in the pessimistic one. A budget of 2,048 missed the 50 ms target by a few milliseconds at every rate but sustained 24 requests per second (optimistic) under a 100 ms target.

Interaction with fine-tuned and post-trained models#

Long fixed prompts. Fine-tuned agents and RAG models often carry long system prompts, tool schemas and retrieved context, so prefill dominates GPU time. Chunking keeps those prompts from stalling everyone else, and prefix caching removes the shared part entirely: cached blocks are skipped and only the uncached suffix is chunked. A common fine-tuning win is to train the behaviour into the weights and shorten the system prompt, which cuts prefill cost far more than any scheduler setting.

Reasoning models. Post-trained reasoning models generate thousands of tokens per request, so at any moment many sequences are decoding. That raises the stakes of stalls (more users per stall) and also means the decode tokens eat more of the budget. With 256 running sequences, a 512 budget leaves only 256 tokens for prefill, right at the ridge. Raise the budget with the running batch or cap max_num_seqs.

LoRA adapters. Chunked iterations mix tokens from different adapters in the same pass, which multi-LoRA kernels handle per row. Adapter compute scales with tokens, so a long prompt for a high-rank adapter makes its chunks slightly more expensive; budget-based scheduling absorbs this automatically. See multi-LoRA serving.

Prefill-only models. Reward models, classifiers and embedding models fine-tuned from an LLM never decode. Chunking has no one to protect there and only adds overhead; use large budgets or disable it. The same holds for offline scoring jobs such as computing log-probabilities for DPO data or evaluations.

Speculative decoding. Draft tokens being verified count against the same budget. With speculative decoding and speculative tokens, each running sequence uses budget units, which can starve prefills unless the budget grows accordingly.

Tuning knobs#

KnobEffectStarting point
Token budget (max_num_batched_tokens in vLLM)Upper bound on iteration time, hence p99 TBT; lower bound on prefill speedAbout 2,048 for interactive chat; 512 to 1,024 for strict TBT SLOs; 8,192 or more for batch jobs
Max concurrent sequencesHow much of the budget decodes consumeKeep well above the ridge (a few hundred tokens)
Long-prompt cap (vLLM long_prefill_token_threshold)Whether one huge prompt can monopolise the prefill share of every iterationOff by default. Set it to a few thousand tokens when prompt lengths are very mixed, so short prompts are not stuck behind a long one
Chunk alignmentTile quantisationMultiples of 128 or 256

Sarathi-Serve's procedure is the right one: profile iteration time against token count for your model, GPU and parallelism once, then set the budget to the largest value whose iteration time stays within your TBT target. Then verify with a load test that replays your real length distribution and read the p99, not the mean. Our latency SLO tuning article covers the load-test side.

Failure modes and how to spot them#

Budget below the ridge. Throughput falls and TTFT rises while TBT looks excellent. Check that the prefill share is several hundred tokens at your typical running batch.

Head-of-line blocking among prefills. If a long prompt may take the whole prefill share, a 100,000-token document blocks every short prompt behind it for many iterations. Symptom: bimodal TTFT, with short prompts slow only when a long one is in flight. Cap the tokens a long prompt may take per iteration so short prompts can be admitted alongside it.

TTFT regressions after enabling it. Expected at small budgets. If TTFT matters more than smoothness, raise the budget or consider prefill/decode disaggregation, which removes the trade-off by giving prefills their own GPUs.

Mismatched expectations under pipeline parallelism. Chunking makes iterations uniform, which is what reduces pipeline bubbles. If you see bubbles anyway, check that prefill chunks are not being split across micro-batches unevenly.

When it helps, when it hurts#

WorkloadChunked prefill?Why
Interactive chat or agents with mixed prompt lengthsYes, budget 512 to 2,048Bounds TBT tail, usually raises goodput.
Long-context RAG with many concurrent usersYes, with a long-prompt capPrevents long documents from freezing streaming answers.
Offline batch generationYes, with a large budgetKeeps prefill efficient; TBT does not matter.
Prefill-only models (reward, embedding, classifier)NoNothing to protect; only overhead.
Strict TTFT and strict TBT at large scaleConsider disaggregationChunking trades one against the other on the same GPU.

Implementation sketch#

In vLLM V1 chunked prefill is on by default and the budget is the main control:

from vllm import LLM

llm = LLM(
    model="path/to/your-finetuned-model",
    enable_chunked_prefill=True,       # default in V1
    max_num_batched_tokens=1024,       # token budget per iteration: the TBT dial
    max_num_seqs=128,                  # decodes consume budget; keep C - b above the ridge
    enable_prefix_caching=True,        # skip shared system prompts entirely
)

SGLang exposes the same control as --chunked-prefill-size. To find your own ridge, time a forward pass for token counts from 64 to 8,192 with no decodes and plot time against tokens: the knee where time starts growing linearly is . In POFE, served fine-tunes default to a budget picked from that profile and the target TBT, and the load test that checks it uses the prompt-length distribution from your own traffic.

Further reading#

Agrawal et al. (2023) introduced chunked prefill as a throughput technique, and Agrawal et al. (2024) is the essential paper on its latency role, with the token-budget methodology and the generation-stall analysis. Holmes et al. (2024) describe the equivalent Dynamic SplitFuse. Kamath et al. (2024) show why the attention kernel is the remaining inefficiency in mixed batches. Zhong et al. (2024) argue that past a certain SLO strictness, separating the phases beats mixing them, which is the subject of our disaggregation article.

Frequently asked questions#

What does chunked prefill do?

It splits a long prompt into pieces that fit a per-iteration token budget and runs each piece in the same forward pass as the decode tokens of requests already generating. Running users keep receiving tokens while a long prompt is processed, instead of waiting for the whole prefill.

What chunk size or token budget should I use?

Profile iteration time against token count and pick the largest budget that keeps an iteration within your time-between-tokens target. Around 2,048 suits interactive chat, 512 to 1,024 strict latency targets, 8,192 or more batch jobs. Keep the prefill share well above the GPU's compute ridge, a few hundred tokens.

Does chunked prefill increase time to first token?

For the arriving request, slightly: in our model a 4,096-token prompt took 136 ms in 512-token chunks versus 118 ms in one shot. At small budgets such as 256 the effect is larger because each prompt needs many iterations.

Does chunked prefill improve throughput?

Sometimes. SARATHI reported up to 1.33 times end-to-end throughput because decodes piggyback on prefill weight reads. In our simulation the gain ranged from 34% to almost nothing depending on whether kernels overlap decode attention with prefill compute. Its reliable benefit is a bounded tail latency.

What is the difference between chunked prefill and disaggregation?

Chunked prefill mixes prefill and decode on the same GPU and bounds their interference with a token budget. Disaggregation runs them on separate GPUs and transfers the KV cache, removing interference at the cost of transfer bandwidth and a fixed split of capacity.

References#

  1. Amey Agrawal, Ashish Panwar, Jayashree Mohan, Nipun Kwatra, Bhargav S. Gulavani, Ramachandran Ramjee (2023). SARATHI: Efficient LLM Inference by Piggybacking Decodes with Chunked Prefills. arxiv.org/abs/2308.16369
  2. Amey Agrawal, Nitin Kedia, Ashish Panwar, Jayashree Mohan, Nipun Kwatra, Bhargav S. Gulavani, Alexey Tumanov, Ramachandran Ramjee (2024). Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve. arxiv.org/abs/2403.02310
  3. Connor Holmes, Masahiro Tanaka, Michael Wyatt, Ammar Ahmad Awan, Jeff Rasley, Samyam Rajbhandari, Reza Yazdani Aminabadi, Heyang Qin, et al. (2024). DeepSpeed-FastGen: High-throughput Text Generation for LLMs via MII and DeepSpeed-Inference. arxiv.org/abs/2401.08671
  4. Aditya K Kamath, Ramya Prabhu, Jayashree Mohan, Simon Peter, Ramachandran Ramjee, Ashish Panwar (2024). POD-Attention: Unlocking Full Prefill-Decode Overlap for Faster LLM Inference. arxiv.org/abs/2410.18038
  5. Yinmin Zhong, Shengyu Liu, Junda Chen, Jianbo Hu, Yibo Zhu, Xuanzhe Liu, Xin Jin, Hao Zhang (2024). DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving. arxiv.org/abs/2401.09670
  6. vLLM project (2025). vLLM documentation: Optimization and Tuning. docs.vllm.ai/en/latest/configuration/optimization.html

Bring a model and a metric.

Tell us what the model should get better at. A post-training engineer replies within one business day with a plan, a compute estimate and an eval design.