Skip to content
Markdown

Runbook: inference KV-cache OOM / preemption thrash

Scope: stabilize an LLM server thrashing on KV-cache pressure (preemptions/recompute, requests evicted, latency spikes) by sizing KV memory, capping concurrency/context, and tuning the scheduler.

Run this when an LLM endpoint is thrashing on KV-cache: num_preemptions climbing, requests evicted and recomputed, KV-cache usage pinned near 100%, and TTFT/TPOT spiking under load. Severity: user-facing degradation, page-worthy when it pushes the SLO. Stabilise concurrency first, then size KV memory correctly so the engine stops evicting.

Reference templates on real APIs; pin versions and validate before production use.

KV-cache is the per-request decode state held in GPU memory; when the running set needs more KV blocks than the cache holds, the scheduler preempts requests, either swapping their KV to CPU or recomputing it on resume, and that recompute/swap churn is the thrash. This is the KV-pressure branch of the inference-SLO-breach runbook, taken to its hardware root cause. KV-cache mechanics (paged blocks, swap vs recompute, prefix reuse) are in KV-cache management; the prefill/decode two-phase model is in inference serving. Metric names below are vLLM V1 (vllm:kv_cache_usage_perc, vllm:num_preemptions); legacy V0 used vllm:gpu_cache_usage_perc and vllm:num_preemptions_total, so confirm against the running engine version.

Trigger

  • Preemptions climbing. vllm:num_preemptions rate non-zero and rising; the engine logs Sequence group ... is preempted by ... mode repeatedly.
  • KV-cache pinned. vllm:kv_cache_usage_perc sits at ~1.0 (100%) while vllm:num_requests_waiting stays non-zero, so the running batch cannot grow.
  • Recompute/swap churn. TTFT spikes on resumed requests (recompute redoes prefill); goodput drops while raw GPU utilisation stays high.
  • Often triggered by a traffic shift to longer prompts / longer max_tokens, a concurrency bump, or a deploy that raised --max-num-seqs or --max-model-len.

Pre-checks

  • Confirm it is KV pressure, not a node fault. A throttling or ECC-degraded GPU (gpu health gating) presents as the same latency regression. Rule it out before tuning the server:
    nvidia-smi --query-gpu=name,memory.used,memory.total,clocks_event_reasons.active,\
    temperature.gpu,ecc.errors.uncorrected.volatile.total --format=csv,noheader
    
    A non-0x0000000000000000 throttle reason diverts to the thermal-emergency runbook or the GPU-fault runbook.
  • Confirm true OOM vs pressure. A CUDA OOM at startup or mid-serve (torch.OutOfMemoryError, killed worker) is a sizing failure (too high --gpu-memory-utilization for the resident model + activations), not steady-state preemption. OOM crashes the replica; preemption degrades it.
  • Correlate with change. Did a deploy in the burn window raise --max-num-seqs, --max-model-len, or change quantisation / --kv-cache-dtype? A deploy-correlated breach short-circuits to Rollback (SRE and MLOps practices).
  • Scope it. One replica or the whole fleet? Check per-replica vllm:kv_cache_usage_perc and preemption rate.
  • Note the available KV blocks logged at engine start (# GPU blocks: N); this is the KV budget you are tuning against (KV-cache management).

Flow

flowchart TB
    A["Preemptions rising, KV usage ~100%"] --> B{"Node healthy?"}
    B -->|"throttle / ECC"| C["Divert: thermal or GPU-fault runbook"]
    B -->|"healthy"| D{"Replica OOM-crashing?"}
    D -->|"yes, CUDA OOM"| E["Lower --gpu-memory-utilization; restart"]
    D -->|"no, steady preemption"| F{"Deploy in burn window?"}
    F -->|"yes"| G["Rollback config / revision"]
    F -->|"no"| H["Cordon + drain one replica"]
    H --> I["Cap concurrency: lower --max-num-seqs / --max-model-len"]
    I --> J{"Recovered?"}
    J -->|"no"| K["Add KV headroom: raise gpu-mem-util, prefix cache, chunked prefill"]
    J -->|"yes"| L["Verify: preemptions ~0, KV usage off ceiling"]
    K --> M{"Still pressured?"}
    M -->|"yes"| N["Scale replicas out / disaggregate"]
    M -->|"no"| L
    N --> L

Procedure

KV pressure is fixed by shrinking demand (fewer/shorter concurrent sequences) or growing supply (more KV blocks). Do the safe demand-side cap first to stop the bleed, then size supply. Restart a serving replica only after cordon + drain so in-flight requests finish elsewhere.

EP=inference-llm                 # deployment / service
NS=serving
  1. Read the engine metrics to confirm the diagnosis and capture a baseline (KV-cache management). On vLLM, scrape /metrics:

    curl -s http://$EP.$NS:8000/metrics | grep -E \
      'vllm:kv_cache_usage_perc|vllm:num_requests_waiting|vllm:num_requests_running|vllm:num_preemptions|vllm:prefix_cache_hits|vllm:prefix_cache_queries'
    
    kv_cache_usage_perc near 1.0 + non-zero num_requests_waiting + rising num_preemptions confirms KV-cache preemption thrash.

  2. Cordon and drain one replica before mutating it. Never restart a serving pod that is taking traffic. Remove it from the router first so in-flight requests finish on healthy replicas (inference serving):

    kubectl -n $NS scale deployment/$EP --replicas=$(($(kubectl -n $NS get deploy/$EP -o jsonpath='{.spec.replicas}')+1))
    kubectl -n $NS rollout status deployment/$EP          # bring up a spare first
    # then take the target pod out of rotation and let it drain
    kubectl -n $NS label pod/<target-pod> serving=draining --overwrite
    kubectl -n $NS delete pod/<target-pod> --grace-period=60
    
    This keeps capacity flat while you reconfigure; do not edit a live, in-rotation replica.

  3. Cap concurrency to stop preemption (demand side). Lower the running-set size so the KV budget covers it without eviction (KV-cache management):

  4. Lower --max-num-seqs. Fewer concurrent sequences directly cut KV-block demand; this is the fastest lever to stop preemption.
  5. Lower --max-model-len if the traffic does not need the full context. KV footprint scales with sequence length, so capping context bounds worst-case per-request KV.
  6. Lower --max-num-batched-tokens to bound prefill chunk size so a long prompt cannot grab the whole KV pool at once.

  7. Grow KV supply (supply side) once demand is bounded (KV-cache management):

  8. Raise --gpu-memory-utilization toward (not to) the limit. vLLM sizes the KV-block pool from the GPU memory left after model weights + activations, so a higher fraction yields more KV blocks. Leave headroom for activation spikes or you trade preemption for a CUDA OOM.
  9. Enable --enable-prefix-caching for prefix-heavy traffic: shared prompt prefixes reuse cached KV blocks instead of recomputing, cutting both prefill cost and KV demand. Confirm with vllm:prefix_cache_hits / vllm:prefix_cache_queries.
  10. Enable --enable-chunked-prefill so long prefills interleave with decodes instead of monopolising a step and forcing decodes to preempt.

  11. Provision CPU swap if recompute is the cost (KV-cache management). vLLM's default preemption recomputes evicted KV on resume; with --swap-space <GiB> it can swap KV to CPU and copy back, which is cheaper than full recompute for long sequences. Set per-GPU swap space deliberately; it is a tradeoff (PCIe copy vs recompute), not a free win. Validate the effect on TTFT before keeping it.

  12. Out of single-replica headroom: scale or disaggregate (inference serving). If demand is capped and supply maxed but the fleet is still pressured, add decode capacity:

    kubectl -n $NS scale deployment/$EP --replicas=<N+delta>
    kubectl -n $NS rollout status deployment/$EP
    
    At high volume with long/variable prompts, disaggregating prefill and decode into separately-scaled pools is the structural fix; see step 4 of the inference-SLO-breach runbook. It is a planned change, not an incident lever.

Verification

  • Preemptions return to ~0 and stay there. The rate of vllm:num_preemptions flattens to zero across a sustained load window:
    rate(vllm:num_preemptions[5m])     # target ~0; V0 engine: vllm:num_preemptions_total
    
  • KV-cache usage off the ceiling. vllm:kv_cache_usage_perc sits below ~0.9 at steady state with vllm:num_requests_waiting draining to near zero, proving the running set fits the KV budget.
  • Latency back under SLO. TTFT and TPOT recover on the histograms; resumed-request TTFT spikes disappear because nothing is being evicted (the inference-SLO-breach runbook).
  • No new OOM. The reconfigured replica runs a sustained load test without torch.OutOfMemoryError or worker kills; confirm nvidia-smi memory is not at the edge.

Rollback

KV pressure is fixed by config changes; rollback is reverting them single-variable to the recorded last-good values (SRE and MLOps practices).

  • If the breach is deploy-correlated, revert the revision the way it shipped; keep a warm previous canary so the shift is instant rather than a cold redeploy:
    kubectl -n $NS rollout undo deployment/$EP        # or: git revert <sha> && argocd app sync $EP
    kubectl -n $NS rollout status deployment/$EP
    
  • If a tuning lever overshot (e.g. raising --gpu-memory-utilization traded preemption for a CUDA OOM), revert that single flag to its last-good value and restart the drained replica (step 2), not the live fleet.
  • Restore any temporarily-added replicas / swap space once the root cause (config or traffic) is addressed, so the steady-state footprint matches the recorded baseline.

References

  • vLLM production metrics (vllm:kv_cache_usage_perc, vllm:num_preemptions, vllm:num_requests_waiting, prefix-cache hits/queries): https://docs.vllm.ai/en/latest/usage/metrics.html
  • vLLM engine arguments (--gpu-memory-utilization, --max-num-seqs, --max-model-len, --max-num-batched-tokens, --swap-space, --kv-cache-dtype, --block-size): https://docs.vllm.ai/en/latest/configuration/engine_args/
  • vLLM optimization and tuning (preemption, recompute vs swap, prefix caching, chunked prefill): https://docs.vllm.ai/en/stable/configuration/optimization/
  • vLLM automatic prefix caching: https://docs.vllm.ai/en/latest/features/automatic_prefix_caching.html
  • Kubernetes Deployments (scale, rollout undo, drain): https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
  • nvidia-smi (clocks_event_reasons, ECC, memory query): https://docs.nvidia.com/deploy/nvidia-smi/index.html

Related: KV-Cache Management · Inference Serving · Inference SLO Breach · GPU Health Gating · Thermal Emergency · GPU Fault / RMA · Operational Runbooks · Glossary