[None][fix] PyExecutor: non-fatal KV-cache block-pool exhaustion guard#16
Open
Thachnh wants to merge 1 commit into
Open
[None][fix] PyExecutor: non-fatal KV-cache block-pool exhaustion guard#16Thachnh wants to merge 1 commit into
Thachnh wants to merge 1 commit into
Conversation
Under MAX_UTILIZATION + enable_block_reuse a saturated KV-cache block pool makes the C++ block manager raise a fatal assertion from inside prepare_resources (WindowBlockManager::allocateBlock / LRUEvictionPolicy::getFreeBlock). The throw escapes the PyExecutor event loop and kills the worker thread on every rank: the engine stops stepping while the process stays up, so health/metrics go silent until the pod is killed. Add an opt-in guard (TRTLLM_KV_EXHAUSTION_NONFATAL=1, default off) mirroring TRTLLM_NAN_GUARD_NONFATAL: when the pool is exhausted, reject the request(s) that could not be allocated and keep stepping. This preserves MAX_UTILIZATION concurrency for short-request traffic while degrading the rare over-subscription event from a pod-wide outage to a single graceful reject. - resource_manager.py: classifier (is_kv_cache_exhaustion_error) + env gate, and a guard around add_sequence_batch/add_token in V1 KVCacheManager.prepare_resources. Offending requests are removed from the batch and collected (not freed here) so an in-flight overlap forward cannot reference freed blocks; the allocation assert is a pre-allocation capacity check, so no partial block is left behind. - scheduler.py: ScheduledRequests.kv_cache_rejected_requests channel. - py_executor.py: collect after prepare_resources; drain at the top of the next iteration (fail via the request-scoped _handle_errors path) in _executor_loop and _executor_loop_overlap, so freeing happens only once no in-flight forward references the blocks and the attention-DP response gather is entered by every rank in lockstep. PP loop intentionally not guarded. - test: classifier signature coverage. Signed-off-by: Thach Nguyen <thach@deepinfra.com>
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.
Problem
Under
capacity_scheduler_policy: MAX_UTILIZATION+enable_block_reuse(e.g. Nemotron-3-Ultra-550B, FP8 KV, 256K), a saturated KV-cache block pool makes the C++ block manager raise a fatal assertion from insideprepare_resources:kvCacheManager.cpp:2480— "Can't allocate new blocks for window size 262152. No free blocks left." (WindowBlockManager::allocateBlock)evictionPolicy.cpp:160— "No free block found. This shouldn't happen!" (LRUEvictionPolicy::getFreeBlock)The throw escapes the (previously unguarded)
resource_manager.prepare_resources(scheduled_batch)call in the PyExecutor event loop and kills the worker thread on every rank: the engine stops stepping while the process stays up, so health/metrics go silent until the pod is force-killed → respawn → next burst kills it again.This recurs because MAX_UTILIZATION's accounting deliberately over-subscribes (the three existing accounting patches
4504b7882/0ea3acc95/a33649516narrow but don't close it for this config).GUARANTEED_NO_EVICTwould close it structurally but under-admits against the 262K window for a mostly-short workload, so it's not acceptable here.Fix
Opt-in, mirrors
TRTLLM_NAN_GUARD_NONFATAL: withTRTLLM_KV_EXHAUSTION_NONFATAL=1(default off → unchanged fatal behavior), when the pool is exhausted we reject the request(s) that could not be allocated and keep stepping. Keeps MAX_UTILIZATION's concurrency for short-request traffic; degrades the rare over-subscription event from a pod-wide outage to a single graceful per-request reject. Python-only (no C++ rebuild).resource_manager.py:is_kv_cache_exhaustion_error(matches the assert signatures) + env gate, and a guard aroundadd_sequence_batch/add_tokenin V1KVCacheManager.prepare_resources. Offending requests are removed from the batch and collected (not freed here).scheduler.py:ScheduledRequests.kv_cache_rejected_requestschannel.py_executor.py: collect afterprepare_resources; drain at the top of the next iteration (fail via the request-scoped_handle_errors(charge_budget=False)path) in_executor_loopand_executor_loop_overlap.Why it's safe
removeSequenceis idempotent + a clean no-op on an unknown/partially-onboarded sequence →free_resourcesis safe, no double-free.hasFreeBlocksbefore mutating) → no half-allocated sequence →prepare_resourcesstays consistent after a caught throw._handle_errorsis paired across ranks.KVCacheManagerpath (the crash path) is guarded.Validation
py_compileclean; classifier logic proven (failing→passing).pytrtllm:v1.3.0rc18-nemotron3ultra-kvguard):tensorrt_llmimports clean, classifier correct,ScheduledRequestsfield +PyExecutormethods present, gate defaults off.tests/unittest/_torch/executor/test_resource_manager.py::test_is_kv_cache_exhaustion_error_classification.TRTLLM_KV_EXHAUSTION_NONFATAL=1under long-context burst (confirm graceful reject vs crash; confirm attention-DP pairing).🤖 Generated with Claude Code