Skip to content

[TRTLLM-14019][feat] Transfer the separate draft KV cache in disaggregated serving - #17371

Closed
zheyuf wants to merge 3 commits into
NVIDIA:feat/m3_with_msafrom
zheyuf:feat/m3-eagle3-draft-kv-transfer
Closed

[TRTLLM-14019][feat] Transfer the separate draft KV cache in disaggregated serving#17371
zheyuf wants to merge 3 commits into
NVIDIA:feat/m3_with_msafrom
zheyuf:feat/m3-eagle3-draft-kv-transfer

Conversation

@zheyuf

@zheyuf zheyuf commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

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 KVCachePageTable as
additional attention layer groups, so NIXL registration, peer matching, the
per-pool mappers, sessions, consensus and cancellation all ride the existing
machinery:

  • resource/page.pyAttentionLayerGroup gains an optional per-group
    tokens_per_block (draft 32 vs target 128 block math), carried through
    serialization.
  • resource/kv_extractor.pymerge_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.
  • native/mixers/attention/peer.py, native/peer.py — 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).
  • transceiver.py — KV slices source draft-group block ids from the draft
    manager (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 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).
  • pyexecutor/kv_cache_transceiver.py, pyexecutor/_util.py — plumb
    resources[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_transfer already frees only the
seq-slot and spec resource managers early, so draft blocks live until the
transfer completes. Pipeline parallelism is rejected for now
(layer_num_per_pp bookkeeping 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):

rate AL GSM8K accuracy
aggregated baseline 0.825 3.474 87.5%
disagg before 0.777 3.330 83.0%
disagg + this PR 0.823 3.469 85.5%

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 (aggregated
    acceptance reference, passes on this build).
  • Disaggregated acceptance validated manually as above; a disagg accuracy
    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 the
reused 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) (the
drafter 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):

  • Copy-task turn probe (restate-from-document, sharpest reuse discriminator),
    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.
  • Real AgentX traces (256K ctx, c32 disagg, same seed): baseline 2.64 →
    +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

  • PR title follows the [JIRA/NVBUG/None][type] format
  • Commits are signed off (DCO)
  • yapf (legacy files) / ruff format (new files) clean
  • CI (/bot run)

zheyuf added 2 commits August 6, 2026 10:37
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant