fix(kv): reserve prefill-activation headroom and size KV pool for the tightest TP rank#1495
Draft
carlushuang wants to merge 1 commit into
Draft
fix(kv): reserve prefill-activation headroom and size KV pool for the tightest TP rank#1495carlushuang wants to merge 1 commit into
carlushuang wants to merge 1 commit into
Conversation
… tightest TP rank get_num_blocks() sized the KV pool from `budget - peak_torch - cudagraph - safety`, using a per-rank clamp of `min(..., free)`. This under-protects the pool on tensor-parallel runs for two reasons: 1. `peak_torch` is the torch-allocator high-watermark from warmup. It misses this process's non-torch resident memory (NCCL/RCCL comm buffers, HIP context, custom-all-reduce / P2P IPC), which lands unevenly across TP ranks — coordinator ranks carry tens of GB more. It can also under-measure the true serving prefill activation when warmup runs a reduced path (e.g. DeepSeek-V4 short-circuits its sparse-attention dispatch under is_dummy_run). 2. Because every rank computes the same (budget-limited) block count, the KV pool is sized for the lightest rank; the heavy ranks then overshoot gpu_memory_utilization (observed post-init: 99% vs 85% target) and OOM on the first large prefill. The failure is silent under P/D disaggregation: the prefill worker OOM-crashes and the proxy hangs waiting on the dead engine. Fix: reserve peak-activation headroom off the *actual* per-rank `free` (env ATOM_KV_PREFILL_HEADROOM_FRAC, default 0.08) instead of off the utilization budget, then all_reduce(MIN) the result across the TP group so the uniform KV pool is sized for the tightest rank. On balanced setups the utilization budget still binds on every rank, so this is a no-op (num_kvcache_blocks unchanged) with no throughput impact. Validated on 8xMI355X, DeepSeek-V4-Pro: - Aggregated TP=8 (balanced): num_kvcache_blocks 49230 -> 49228; c64/c256 output throughput unchanged (within noise). No perf drop. - TP=4 P/D (imbalanced): default max_num_seqs=512 now fails fast at init with an actionable message instead of OOM-crashing mid-serving; with max_num_seqs=128 it serves c16 at default chunk size without OOM. Refs: #1483
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
On tensor-parallel runs,
ModelRunner.get_num_blocks()can size the KV pool too large on the "heavy" ranks, so they overshoot--gpu-memory-utilizationand OOM on the first large prefill. Under P/D disaggregation this is silent: the prefill worker OOM-crashes and the proxy hangs waiting on the dead engine (see #1483 for the full investigation).Root cause is in the budget formula:
peak_torchis the torch-allocator high-watermark from warmup. It misses this process's non-torch resident memory — NCCL/RCCL comm buffers, HIP context, custom-all-reduce / P2P IPC — which lands unevenly across TP ranks (coordinator ranks carry tens of GB more; measured ~34 GB on rank 0/1 for DeepSeek-V4-Pro, and it is not weights:peak_torchis identical across ranks, and not RCCL: NCCL_DEBUG shows ~10 GB/rank uniform). It can also under-measure the true serving prefill activation when warmup runs a reduced path (DeepSeek-V4 short-circuits its sparse-attention dispatch underis_dummy_run).Because the utilization budget binds uniformly, every rank computes the same block count — sized for the lightest rank. The heavy ranks then overshoot the target (observed post-init: 99% vs an 85% target) and OOM on the first prefill whose activation doesn't fit.
Change
Reserve peak-activation headroom off the actual per-rank
free(not off the utilization budget), thenall_reduce(MIN)across the TP group so the uniform KV pool is sized for the tightest rank:On balanced setups the utilization budget still binds on every rank, so
available_for_kvis unchanged and theall_reduce(MIN)is a no-op —num_kvcache_blocksis unchanged and there is no throughput impact. On imbalanced setups the heavy rank's smallerfreebinds, so the pool is sized conservatively enough to leave real prefill headroom (or, if the request cache genuinely cannot fit, the existing "reduce--max-num-seqs" error fires at init instead of a mid-serving OOM).The headroom fraction is env-tunable via
ATOM_KV_PREFILL_HEADROOM_FRAC(default0.08).Test Result
8×MI355X (gfx950),
deepseek-ai/DeepSeek-V4-Pro, bf16 KV:Aggregated TP=8 (balanced) — no perf drop:
num_kvcache_blocks: 49230 → 49228 (0.004% — theall_reduce(MIN)picking the true-min rank; util budget still binds).TP=4 P/D (imbalanced) — OOM fixed:
--max-num-seqs 512, util 0.85, default chunk): previously started then OOM-crashed the prefill worker at c=16 (silent hang). Now fails fast at init with the actionable "reduce--max-num-seqs" message.--max-num-seqs 128(as the message suggests) at defaultattn-prefill-chunk-size: serves c=16 cleanly at 12.8 req/s, prefill worker stays alive (0 OOM events) — no chunked-prefill workaround needed.Notes / follow-ups
is_dummy_run(or adding an explicit sparse-attention activation reserve) sopeak_torchreflects the true prefill peak — is left for a separate change.0.08default headroom fraction is a heuristic; happy to tune it or derive it frommax_num_batched_tokensif preferred.Refs #1483