[TRTLLM-14019][feat] Transfer the separate draft KV cache in disaggregated serving - #17371
Closed
zheyuf wants to merge 3 commits into
Closed
[TRTLLM-14019][feat] Transfer the separate draft KV cache in disaggregated serving#17371zheyuf wants to merge 3 commits into
zheyuf wants to merge 3 commits into
Conversation
…he in disaggregated serving The nvbug 5807902 WAR disables the separate draft KV cache manager whenever a cache transceiver is configured. MiniMax-M3 cannot take the shared-manager path that WAR forces: its cache manager declares supports_shared_draft_layers=False, and the drafter then inherits the target's tokens_per_block=128 pages, which miss the SM10x Eagle context cubins (the unfused-MHA fallback requests a 6.17 TiB workspace on a real 32K-token warmup) and hit the known tokens_per_block=128 trtllm-gen generation-kernel IMA. Both context and generation workers crashed during startup on every disaggregated Eagle3 attempt. Exempt MiniMax-M3 from the WAR so both worker roles keep the designed tokens_per_block=32 separate draft manager (symmetry is required for a consistent target pool layout across the KV transfer). Validated on Lyris GB300 (2xCTX TP2 + GEN TP4/ADP, NIXL): startup completes end to end, and the test_nvfp4_eagle3 chat-GSM8K acceptance workload measures AL 3.330 disagg vs 3.474 aggregated on the same build (drafter card reference 3.518). The remaining gap is the transceiver not transferring draft-layer KV, tracked separately. Signed-off-by: Zheyu Fu <zheyuf@nvidia.com>
…gated serving One-model speculative decoding with a separate draft KV cache manager (e.g. MiniMax-M3's tokens_per_block=32 Eagle3 cache) previously lost the drafter's prompt KV at the CTX->GEN handoff: the transceiver only transferred the target manager's pools, so the generation-side drafter started every request against an unwarmed prompt window. Acceptance recovered only as generated tokens filled the drafter's attention window, which short prompts amortize (-4% AL on chat-GSM8K) but long-prompt regimes do not (-13% and worse as the prompt share of the window grows). Merge the draft manager's pools into the target's KVCachePageTable as additional attention layer groups, so NIXL registration, peer matching, the per-pool mappers, sessions and cancellation all ride the existing machinery: * AttentionLayerGroup gains an optional per-group tokens_per_block (draft 32 vs target 128 block math) carried through serialization. * merge_draft_page_table() appends draft groups with re-based pool indices and global layer ids offset by 1<<30; both peers apply the same constant, so the existing global-id-overlap + pool_role matching pairs draft groups with draft groups, and a peer without a draft manager degrades gracefully to today's behavior. * Head-mismatch mappers take per-layer-group KV head counts (the draft head count differs from the target's; rank-level remains the fallback so non-merged tables behave exactly as before). * KV slices source draft-group block ids from the draft manager and pin their cached-prefix to 0 (draft KV never participates in prefix reuse; the full prompt must transfer). * Target and draft pools register with the NIXL agent as separate batches: the V2 manager derives its VMM chunk size from the pool quota, so the two managers' chunk sizes differ, and the agent requires a uniform chunk size per registration batch (its region bookkeeping is already per-region). Validated on Lyris GB300 (2xCTX TP2 + GEN TP4/ADP, NIXL/Python transceiver, chat-GSM8K acceptance workload from test_nvfp4_eagle3): * acceptance length 3.330 -> 3.469 (aggregated baseline 3.474, drafter-card reference 3.518); acceptance rate 0.777 -> 0.823 (aggregated 0.825) * per-token-position probes show the early-step deficit healed (steps 1-3: 3.08 -> 3.54; steps 3-6: 2.75 -> 3.22 vs aggregated 3.88/3.69) and parity from step 6 on * 200/200 requests, zero failures; both worker roles log the draft cache registration Contains the M3 WAR exemption commit from NVIDIA#17341; will rebase once that merges. Signed-off-by: Zheyu Fu <zheyuf@nvidia.com>
With a separate draft KV cache manager, the drafter's blocks never entered prefix reuse: the mirror created draft caches without a radix lookup and unconditionally stopped committing. On any multi-turn workload with block reuse enabled, the target reuses the previous turns' prefix while the drafter's KV for those positions does not exist; worse, the pages backing that range are recycled from the free list and typically hold a previous request's draft KV at scrambled positions, actively degrading acceptance (measured -0.2 to -0.35 AL on turns 2+ for both aggregated and disaggregated serving). Fix, in the draft manager mirror: - Defer reuse-eligible draft cache creation until after the target manager's prepare (the target runs last in the resource sweep and only then is its reuse boundary known); the executor calls the new prepare_deferred_draft_reuse() after each prepare_resources sweep. The draft radix lookup is clamped to the target boundary: a draft hit beyond it would expose radix-shared blocks to drafter writes. - Explicitly zero-fill the gap [draft_hit, target_boundary): the drafter only writes the target's context chunk and has no target hidden states for reused positions, so the gap is unrecoverable by recompute. Zero K/V behaves like masked attention and is benign, unlike recycled stale KV. - Commit draft blocks (at transfer start for disaggregated context, at the decode transition otherwise). With gaps zeroed this is always safe, and draft reuse coverage then grows turn over turn instead of deadlocking on an empty pool: in steady state the draft hit tracks the target boundary exactly. Validation (MiniMax-M3 NVFP4 + Eagle3 draft3, GB300): - Copy-task turn probe, turns 2-4: disagg 3.03-3.11 -> 3.59-3.65, agg 3.01-3.10 -> 3.63-3.82 (turn-1 parity in all arms). - Real AgentX traces (256K ctx, c32, disagg): AL 2.79 -> 3.04 on top of the draft-KV transfer; draft hits track the target boundary up to 93.7K reused tokens with zero gap fills after bootstrap. Signed-off-by: Zheyu Fu <zheyuf@nvidia.com>
4 tasks
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.
Description
Second half of the MiniMax-M3 Eagle3 disaggregated-serving work (first half:
#17341, whose commit this branch contains and will rebase away once merged).
With a separate one-model draft KV cache manager, the transceiver only
transferred the target manager's pools: the Eagle3 drafter's prompt KV
computed during context-side prefill was discarded, so the generation-side
drafter started every request against an unwarmed prompt window. Acceptance
recovered only as generated tokens filled the drafter's attention window —
short prompts amortize this (−4% AL on chat-GSM8K) but long-prompt regimes do
not (−13% on prompt-referencing prose at ~1K prompts, ~−25% at 30K-token
prompts; position-resolved probes show steps 1–12 losing 0.5–0.9 AL).
Changes
Merge the draft manager's pools into the target's
KVCachePageTableasadditional attention layer groups, so NIXL registration, peer matching, the
per-pool mappers, sessions, consensus and cancellation all ride the existing
machinery:
resource/page.py—AttentionLayerGroupgains an optional per-grouptokens_per_block(draft 32 vs target 128 block math), carried throughserialization.
resource/kv_extractor.py—merge_draft_page_table()appends draftgroups with re-based pool indices and global layer ids offset by
1<<30;both peers apply the same constant, so the existing global-id-overlap +
pool_rolematching pairs draft groups with draft groups, and a peerwithout a draft manager degrades gracefully to today's behavior.
native/mixers/attention/peer.py,native/peer.py— head-mismatch mapperstake per-layer-group KV head counts (the draft head count differs from the
target's; rank-level remains the fallback, so non-merged tables behave
exactly as before).
transceiver.py— KV slices source draft-group block ids from the draftmanager (its own group numbering, per-group tpb) and pin their
cached-prefix to 0 (draft KV never participates in prefix reuse; the full
prompt must transfer).
native/transfer.py— target and draft pools register with the NIXL agentas separate batches: the V2 manager derives its VMM chunk size from the
pool quota, so the two managers' chunk sizes differ, and the agent requires
a uniform chunk size per registration batch (its region bookkeeping is
already per-region).
pyexecutor/kv_cache_transceiver.py,pyexecutor/_util.py— plumbresources[DRAFT_KV_CACHE_MANAGER]into the Python transceiver (the C++transceiver logs a warning and keeps today's behavior).
CTX-side lifecycle needs no changes:
start_transferalready frees only theseq-slot and spec resource managers early, so draft blocks live until the
transfer completes. Pipeline parallelism is rejected for now
(
layer_num_per_ppbookkeeping covers target layers only).Validation (Lyris GB300, 2xCTX TP2 + GEN TP4/ADP, NIXL/Python transceiver)
chat-GSM8K acceptance workload from
test_nvfp4_eagle3(200 prompts, greedy,max_tokens 512; drafter-card reference rate 0.839 / AL 3.518):
Position-resolved single-request probes: the early-step deficit is healed
(steps 1–3: 3.08 → 3.54; steps 3–6: 2.75 → 3.22; parity with aggregated from
step 6 on). 200/200 requests, zero failures; both worker roles log
Registered separate draft KV cache for disaggregated transfer: 1 layer group(s), tokens_per_block=32 (target uses 128).Transfer volume: the drafter is 1 layer (vs 60 target layers); with the
current 64-head MHA draft head this adds ≤ ~25% to the per-token transfer
size, and ~1.6% with a GQA draft head.
Test Coverage
test_llm_api_pytorch.py::TestMiniMaxM3::test_nvfp4_eagle3(aggregatedacceptance reference, passes on this build).
suite variant needs an M3 disagg fixture — planned follow-up alongside
retiring the nvbug 5807902 WAR (a 3.3-hour, 91,694-request soak on this
stack showed zero failures and a flat AL trend, so the WAR's original
late-run failure no longer reproduces).
Second commit: draft prefix reuse (
a5c7584da)Transferring the drafter's prompt KV fixes single-turn requests, but with
block reuse enabled a multi-turn conversation still degrades: the target
reuses the previous turns' prefix while the drafter mirror never did a radix
lookup and unconditionally
stop_committing()-ed, so the drafter's KV for thereused range does not exist — and the physical pages backing it are recycled
free-list pages that typically hold a previous request's draft KV at
scrambled positions, actively poisoning acceptance. This affects
aggregated serving too (measured −0.2 to −0.35 AL on turns 2+ in both
topologies).
Fix (all inside the draft manager mirror + two executor hook points):
defer reuse-eligible draft cache creation until the target's reuse boundary
is known, clamp the draft radix lookup to that boundary, explicitly
zero-fill the unrecoverable gap
[draft_hit, target_boundary)(thedrafter has no target hidden states for reused positions; zero K/V behaves
like masked attention and is benign), then commit — with gaps zeroed,
committing is always safe and draft reuse coverage grows turn over turn.
Validation (M3 NVFP4 + Eagle3 draft3, GB300):
turns 2–4: disagg 3.03–3.11 → 3.59–3.65, agg 3.01–3.10 → 3.63–3.82;
turn-1 parity in all arms; restatement fidelity identical with/without.
+transfer 2.79 → +reuse 3.04; steady state shows the draft hit tracking
the target boundary exactly (hits up to 93.7K reused tokens, zero gap
fills after bootstrap).
PR Checklist
[JIRA/NVBUG/None][type]format/bot run)