Repository navigation
[None][perf] V2 scheduler: equal-cost context chunking and long-prefill demotion; fix KV event flush under attention DP - #20595
Closed
reasonsolo wants to merge 3 commits into
Closed
reasonsolo wants to merge 3 commits into
reasonsolo wants to merge 3 commits into
Conversation
Under attention DP every rank runs in lockstep, so an iteration lasts as long as its most expensive rank. A context chunk of n tokens at KV depth kv costs about (kv_cost_offset + kv) * n, so a full-budget chunk of a deep request takes much longer than the same number of tokens on a shallow rank. Add opt-in equal-cost chunking to KVCacheV2Scheduler. Each rank caps its context chunks against the shared cost budget B = (kv_cost_offset + kv_depth_threshold) * max_num_tokens, which equalizes the per-rank iteration cost without cross-rank coordination. Requests shallower than the threshold keep the plain token cap. - TLLM_V2_CTX_COST_KV_OFFSET: KV-token equivalents of the KV-independent per-token work. - TLLM_V2_CTX_COST_KV_DEPTH_THRESHOLD: KV depth above which chunks shrink. - TLLM_V2_CTX_COST_PER_CHUNK_DEPTH_TOKENS: optional per-chunk prefix term; each chunk also costs R * kv_depth and the budget grows by R * kv_depth_threshold. Both of the first two knobs must be positive and context chunking enabled, otherwise the feature is off and scheduling is unchanged. The first context request of an iteration always gets at least a quarter of the token budget so a deep request is never starved into unit-sized chunks. examples/scheduler_cost_calibration/fit_context_cost.py fits the two main knobs from the per-rank iteration log of an ordinary run. Signed-off-by: Lizhi Zhou <1432185+reasonsolo@users.noreply.github.com>
… context requests Context scheduling in KVCacheV2Scheduler is FCFS, so a prefill of hundreds of thousands of uncached tokens takes the whole token budget for as many iterations as it needs, and the short, mostly cached turns queued behind it wait the whole time. Add opt-in long-prefill demotion. With TLLM_V2_CTX_DEMOTE_UNCACHED_TOKENS=T, a context request whose estimated uncached prefill exceeds T moves to the tail of the iteration's context order (FCFS within each class) from its first chunk on, and takes whatever token budget the short requests leave. Demoted requests are reordered, never capped. Once a demoted request is mid-prefill, the exact remainder releases it when T or fewer tokens are left. TLLM_V2_CTX_DEMOTE_MAX_ITERS=N lets a request through in arrival order after N consecutive demoted iterations (0 = no bound). The estimate comes from ConversationAwareADPRouter, which publishes py_uncached_prefill_tokens on each routed request while demotion is on: the whole prompt for a conversation's first turn, and the growth since the previous turn's prompt otherwise. The history is keyed by conversation_id and LRU-bounded by max_sessions. A request without an estimate is never demoted. Both knobs default to 0, which leaves scheduling and routing unchanged. Demotion also stays off when context chunking is disabled. Signed-off-by: Lizhi Zhou <1432185+reasonsolo@users.noreply.github.com>
…th under attention DP With attention DP and buffered KV cache events, KV cache manager V2's flush_iteration_events allgathers events across the attention-DP group. The overlap loop flushed them from _process_previous_batch, which runs only where `previous_batch is not None and should_process_previous_batch`. Both conditions can differ between ranks when some ranks scheduled work and others did not, so the allgather could be issued on a subset of ranks and hang or pair with an unrelated collective. The rebalance hook had the same gap because it consumes previous_batch only where one exists. Move the flush out of _process_previous_batch into a rank-symmetric helper that the overlap loop calls after the previous-batch branch, and the rebalance hook calls after consuming previous_batch. Under attention DP every rank flushes at that point; without attention DP the flush keeps its previous cadence of once per processed previous batch. The non-overlap loop already flushes under the rank-agreed can_queue gate and is unchanged. Based on a fix by Jin Li. Signed-off-by: Lizhi Zhou <1432185+reasonsolo@users.noreply.github.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.
@coderabbitai summary
Description
Two opt-in context-scheduling policies for
KVCacheV2Scheduler, aimed at attention-DP context servers with long, multi-turn prompts. Both are controlled by environment variables and are off by default. With the variables unset or0, scheduling and routing are unchanged.It also fixes a rank-divergent KV cache event flush in the overlap executor loop under attention DP (section 3), which these context servers hit with KV cache manager V2 and buffered KV events.
The GB300 Kimi K3 DWDP perf-sanity configs added in #20163 already set these variables (
tests/scripts/perf-sanity/disaggregated/gb300_kimi-k3-fp4-dspark_agentx_*.yaml). Until now main had no reader for them. This PR keeps the variable names identical, so those configs take effect without changes.Original design and implementation by @lancelly (Liao Lanyu); the KV event flush fix is originally by @liji-nv (Jin Li). Equal-cost chunking was first proposed in #18528. This PR ports it, the follow-ups and long-prefill demotion onto the current V2 scheduler.
1. Equal-cost context chunking by KV depth
Under attention DP every rank runs in lockstep, so an iteration lasts as long as its most expensive rank. A context chunk of
ntokens at KV depthkvcosts about(kv_cost_offset + kv) * n. A full-budget chunk of a deep request therefore takes much longer than the same number of tokens on a shallow rank, and the other ranks wait.With equal-cost chunking, each rank caps its context chunks against a shared cost budget:
This evens out the per-rank iteration cost without any cross-rank coordination. Requests shallower than
kv_depth_thresholdkeep the plain token cap because the token cap binds first. Leftover cost budget goes to the next context request (backfill). The first context request of an iteration always gets at leastmax(chunk_unit_size, max_num_tokens // 4)tokens, so a very deep request is never starved into unit-sized chunks that each re-read the whole prefix.TLLM_V2_CTX_COST_KV_OFFSET0(off)TLLM_V2_CTX_COST_KV_DEPTH_THRESHOLD0(off)TLLM_V2_CTX_COST_PER_CHUNK_DEPTH_TOKENS0R. Each chunk also costsR * kv_start, and the budget grows byR * kv_depth_threshold.The feature is enabled only when both
KV_OFFSETandKV_DEPTH_THRESHOLDare positive,max_num_tokensis set, and context chunking is enabled. OtherwiseBudgetTrackergets no cost budget and the cost cap is skipped.Calibration example.
examples/scheduler_cost_calibration/fit_context_cost.pyfitsKV_OFFSETandKV_DEPTH_THRESHOLDfrom the per-rank iteration log of an ordinary token-budget run (TLLM_PROFILE_LOG_RANKS=all). It finds "monopoly spans", where one rank's single deep request determines the lockstep iteration time. It fits the time-vs-depth slope with Theil-Sen, which is robust to stall outliers, and deriveskv_cost_offsetfrom the intercept. It takeskv_depth_thresholdas a percentile of the token-weighted depth distribution. It also reports a linearity check, the measured lockstep waste and the predicted gain, then prints ready-to-paste values. The README covers the workflow and the per-chunk term, which the script does not fit.2. Long-prefill demotion
V2 context scheduling is FCFS. A prefill with hundreds of thousands of uncached tokens takes the whole token budget for every iteration it needs, and the short, mostly cached turns queued behind it wait. With demotion, a request whose estimated uncached prefill exceeds the threshold moves to the tail of the iteration's context order, keeping FCFS within each class. It takes whatever token budget the short requests leave, so it is reordered but never capped: alone in the queue, it still gets the full budget. Once it is mid-prefill, the exact remainder (
context_remaining_length) releases it when no more than the threshold is left.TLLM_V2_CTX_DEMOTE_UNCACHED_TOKENS0(off)TLLM_V2_CTX_DEMOTE_MAX_ITERS0(no bound)The estimate (
LlmRequest.py_uncached_prefill_tokens) is published byConversationAwareADPRouterwhile demotion is on:conversation_id: the whole prompt.The history is keyed by
conversation_id, not by the routing key, so sub-agents that share a parent's affinity id keep their own history. It is LRU-bounded bymax_sessions. A request without an estimate is never demoted, for example when another ADP router is in use or attention DP is off. The scheduler does not probe the radix tree, which would walk every prompt on every iteration. Demotion is also off when context chunking is disabled.3. Fix: flush KV cache events outside the rank-divergent batch path under attention DP
Issue.
With attention DP and buffered KV cache events (
event_buffer_max_size > 0, not streaming), KV cache manager V2 builds itsKVCacheEventManagerwithattention_dp_gather=Distributed.get(mapping).allgather(kv_cache_manager_v2.py).EventManager::flushIterationEventscalls that gather synchronously (eventManager.cpp), soflush_iteration_events()is a collective across the attention-DP group.In
_executor_loop_overlap, the flush happened inside_process_previous_batch(). That function only runs underif self.previous_batch is not None and should_process_previous_batch:. Both conditions can differ between ranks:should_process_previous_batch = can_queue or not can_queue_this_rank. When some ranks scheduled work and others did not,can_queueis false on every rank. Ranks with local work then defer their previous batch, and idle ranks process theirs.previous_batch, while a busy rank keeps it. On the next iteration the ranks disagree onprevious_batch is not None.The loop already handles this divergence for its other collectives. It pairs
_enqueue_responses([])in theelsebranch, and calls_flush_pending_transfer_responses(),handle_timeouts_synced()and_flush_iter_stats_synced()outside the branch. The KV event flush was the remaining collective inside the branch. With attention DP, the overlap scheduler, KV cache manager V2 and buffered KV events, it could be issued on only some ranks. The result is a hang or an allgather that pairs with an unrelated collective.The KV pool rebalance hook (
_maybe_rebalance_kv_pools) has the same gap. It calls_process_previous_batch()through_consume_previous_batch_for_rebalance(), which returns early on ranks without aprevious_batch.Fix.
_process_previous_batch()no longer flushes KV cache events._add_kv_cache_events_synced(processed_previous_batch)helper:if/else, beforehandle_timeouts_synced(). Every rank reaches that point in the same order._consume_previous_batch_for_rebalance(). Every rank reaches that point once_agreed_need_adjustment()agrees._executor_loopalready flushes underif can_queue, which is rank-agreed under attention DP. It is unchanged.Behaviour without attention DP, or with KV cache events disabled, is unchanged. With attention DP, ranks also flush on iterations where they did not process a previous batch. Those flushes carry no events.
Interaction with existing V2 scheduler features
pending_ctxlist. Contributed-block collection, the prefix-skip deferral, encoder scheduling (encoder requests are never demoted) andpreempt_for_pagesrun unchanged on the new order.budget.commitcharges, including replay rows and last-chunk draft tokens. The cost cap is computed on the new-token chunk only.release_context_admissionreleases its prefix holds as for any other failed admission.Test Coverage
tests/unittest/_torch/executor/kv_cache/test_kv_cache_v2_scheduler.pyTestEqualCostChunking: off by default, off with one knob or without chunking, unlimitedBudgetTrackerwithout a cost budget, shallow requests unaffected, the deep-request formula, leftover backfill, the head-of-line floor, and the per-chunk depth term including a backfill that cannot pay its depth term.TestLongContextDemotion: off by default and without chunking, estimated-long requests wait behind short ones from the first chunk, no estimate means no demotion, the exact remainder releases the tail, and the aging bound with count reset and no leaked entries.tests/unittest/_torch/executor/test_adp_router.py::TestConversationAwareADPRouterThe existing V2 scheduler and ADP router suites pass unchanged, which shows the defaults are a no-op.
tests/unittest/_torch/executor/test_py_executor.py::TestKvCacheEventFlushRankSymmetry(CPU): drives the real_executor_loop_overlap/_process_previous_batch/_add_kv_cache_events(or_maybe_rebalance_kv_pools) with a mocked KV cache manager, and records the ordered trace of attention-DP collectives on each simulated rank.test_adp_ranks_flush_kv_cache_events_in_lockstep: one rank defers its previous batch and one processes it, and their collective traces must match. Fails onmain.test_adp_rank_without_previous_batch_joins_kv_cache_event_gather: ranks disagree onprevious_batch is not None. Fails onmain.test_rebalance_flushes_kv_cache_events_on_every_adp_rank: the rebalance hook flushes on every attention-DP rank, including one without aprevious_batch(that case fails onmain).test_without_adp_flushes_once_per_processed_previous_batch: non-attention-DP cadence is unchanged.test_kv_cache_events_disabled_never_flushes: no flush when KV cache events are off.tests/unittest/_torch/disaggregation/test_disagg_loop_transcript.py: the minimal executor stub now setsenable_kv_cache_events = False, which a realPyExecutoralways sets.PR Checklist
Please review the following before submitting your PR:
PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense.
PR Follows TRT-LLM CODING GUIDELINES to the best of your knowledge.
Test cases are provided for new code paths (see test instructions)
If PR introduces API changes, an appropriate PR label is added - either
api-compatibleorapi-breaking. Forapi-breaking, includeBREAKINGin the PR title.Any new dependencies have been scanned for license and vulnerabilities
CODEOWNERS updated if ownership changes
Documentation updated as needed
Update tava architecture diagram if there is a significant design change in PR.
The reviewers assigned automatically/manually are appropriate for the PR.
Please check this after reviewing the above items as appropriate for this PR.
GitHub Bot Help
To see a list of available CI bot commands, please comment
/bot help.