vLLM throughput collapses as the scheduler preempts and recomputes the same requests
When KV cache blocks run out, the vLLM scheduler evicts in-flight sequences and recomputes them later. Under sustained load the recomputation consumes the cache and compute that would have retired other requests, so evictions beget evictions and a large share of GPU time goes to work that has already been done once.
Lower --max-num-seqs. The instinct is to add memory, but capping admission ends the loop directly, and no error will ever fire to tell you it is happening — only the preemption counter.
What this failure is
A stable degraded regime in which vLLM's KV cache is oversubscribed, so the scheduler repeatedly evicts and re-prefills the same sequences and spends an increasing share of GPU time on repeated work rather than on completing requests.
Why it happens (the mechanism)
The scheduler will admit more sequences than the cache can serve, because admission and capacity are governed by different limits. When the cache runs dry it must evict something, and eviction throws away decode progress rather than pausing it. The evicted request comes back needing a full prefill, so the cache pressure that caused the eviction is reproduced by the recovery from it.
What you'll observe
- Throughput falls sharply while GPU utilisation stays high, so the card looks busy and productive
- Latency becomes erratic, with individual requests taking many times their usual time
- Nothing errors, so no alert fires and no exception appears in the logs
- Adding load makes total completed requests go down rather than up
Common symptoms and what they mean
| Symptom | Why it happens |
|---|---|
| Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space. | Preemption is a safety valve that keeps the server up when the cache cannot hold every batched request. It is not free: an evicted sequence loses the decode work it had already done and is prefilled again from the beginning when it is re-admitted. |
| The same warning repeating continuously rather than appearing in occasional bursts | That makes the failure self-sustaining. Re-admitted sequences need cache and compute to redo their prefill, which is exactly the pressure that caused the eviction, so the scheduler evicts again. Once the loop closes, a growing fraction of each step is spent recomputing rather than retiring requests, and the fleet does less work the more traffic it is given. |
| A steadily climbing vllm:num_preemptions_total in the Prometheus metrics | It is easy to miss because nothing fails. There is no exception and no error rate to alert on; the only signals are a warning line and a counter, and utilisation stays high throughout because the GPU is genuinely busy doing the wasted work. |
| total_cumulative_preemption_cnt rising in the periodic stats line | Preemption is a safety valve that keeps the server up when the cache cannot hold every batched request. It is not free: an evicted sequence loses the decode work it had already done and is prefilled again from the beginning when it is re-admitted. |
Which systems are affected
- vLLM V1, where RECOMPUTE is the default preemption mode because it is cheaper than swapping in that architecture
- vLLM V0, where the same warning names PreemptionMode.SWAP instead
- Deployments admitting more concurrent sequences than the KV cache can hold at the served context length
- Long-context workloads, where each sequence reserves capacity for its full length
How to confirm this is the problem
Apply the following checklist to a small reproduction: each box below is a positive signal that you are looking at this exact failure rather than a sibling in the same taxonomy.
- ✓Scrape vllm:num_preemptions_total twice a minute apart. A counter that keeps climbing under steady traffic indicates a thrashing loop rather than an occasional spike.
- ✓Halve --max-num-seqs and re-measure completed requests per second. If total throughput rises while concurrency falls, the deployment was thrashing.
- ✓Compare average generation throughput against prompt throughput in the periodic stats line. Prefill work far exceeding what the arriving traffic can account for is recomputation.
Example training logs (fingerprint)
WARNING: Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space. This can affect the end-to-end performance. Increase gpu_memory_utilization or tensor_parallel_size to provide more KV cache memory.
INFO: Avg prompt throughput: 2140.0 tokens/s, Avg generation throughput: 41.2 tokens/s, Running: 48 reqs, Preempted: 31 reqsTimestamps and exact values vary across runs, but the pattern. An info-level start, an early WARN, an ERROR carrying the symptom. Is the actual fingerprint you should alert on. The Denpex platform flags this combination automatically.
Root cause, fix & prevention, signed in
Sign up free to see why this failure really happens, the exact remediation steps, and the production-grade prevention pattern. You also get 3 free full diagnoses for your own training logs.
Sign up free. Unlock the full analysisNo credit card · 3 free diagnoses · Instant access
Why the recommended fix works
Reducing the admission limit means the working set fits, so nothing has to be evicted and no work is repeated. Enlarging the cache can also work, but it moves the threshold rather than removing the feedback, so a traffic increase puts the deployment straight back into the loop. Capping concurrency is the change that makes the regime unreachable.
Code examples
// Typical pattern:
import torch.optim as optim
optimizer = optim.AdamW(model.parameters(), lr=3e-4)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=num_training_steps)
for step in range(num_training_steps):
optimizer.zero_grad()
loss = model(batch)
loss.backward()
optimizer.step()
scheduler.step()Adapt the snippet to your framework. The same pattern holds for PyTorch Lightning, Hugging Face Trainer, DeepSpeed, Megatron-LM, and vLLM training wrappers. Where the wrapper exposes a config flag (for examplelr_scheduler_type in Trainer), prefer the flag over the imperative API to keep the schedule declarative and reproducible.
Best practices by model family
| Model / Stack | Recommendation | Notes |
|---|---|---|
| Thrashing under steady traffic | Lower --max-num-seqs first | Ends the feedback loop rather than moving its threshold. |
| Context window far above workload need | Lower --max-model-len | Each sequence reserves capacity for its full declared length. |
| Genuinely needs the concurrency | Raise tensor parallel size or quantize the KV cache | Adds real capacity instead of trading against activation headroom. |
| Considering chunked prefill | Expect no memory relief from it | It addresses head-of-line blocking; capacity is still reserved for the full sequence length. |
With the fix vs without the fix
| Dimension | With the fix | Without the fix |
|---|---|---|
| How the failure announces itself | A warning line and a rising counter | Assumed an error would be raised |
| What GPU utilisation shows | High, because recomputation is real work | Read as evidence the hardware is saturated |
| Effect of adding traffic | Completed requests per second falls | Assumed to rise until a plateau |
Real engineering notes
“This is the failure most likely to be mistaken for needing bigger hardware. Utilisation is high, the GPU is hot, and the obvious reading is that the card is saturated, so teams add replicas. Adding replicas does help, by splitting the traffic each one sees, which reinforces the wrong conclusion. Halving the admission limit on one replica and watching completed requests per second rise is the cheap experiment that settles it.”
Visual fingerprint
admit more sequences than the cache holds
|
v
cache exhausted -> evict a sequence, discarding its decode progress
^ |
| v
re-prefill consumes cache <- re-admit the evicted sequenceRelated failures to investigate next
Root cause, fix & prevention
Frequently asked questions
Twelve targeted questions that engineers and on-call staff most commonly ask about this failure.
Nothing is erroring. Is this really a failure?
GPU utilisation is high. Do I need more GPUs?
Should I just raise gpu_memory_utilization?
Will chunked prefill fix it?
Don't just read the fix, diagnose your run
The encyclopedia tells you what went wrong. Denpex tells you what went wrong in YOUR training run. With your logs, your config, and your stack.