Skip to content

[PD] Don't release KV pages while Mooncake transfers are in flight - #32564

Closed
KastanDay wants to merge 13 commits into
sgl-project:mainfrom
KastanDay:kastan/mooncake-transfer-ownership-barrier
Closed

KastanDay wants to merge 13 commits into
sgl-project:mainfrom
KastanDay:kastan/mooncake-transfer-ownership-barrier

Conversation

@KastanDay

@KastanDay KastanDay commented Jul 27, 2026 •

Copy link
Copy Markdown

The bug

An abort or timeout during a prefill→decode transfer frees the request's KV pages back to the allocator, but it does not cancel the transfer. The next request can then be allocated those same pages and have them overwritten by the earlier, still-in-flight write, causing the next request to hold the first request's KV, with no error raised anywhere.

@ShangmingCai already anticipated this in #27372, which prevents the prefill starting new work after an abort and this PR implements what he mentioned as a TODO, which is handling transfers that are already in flight.

The bug's window is narrow: the transfer has to still be in flight and the pages have to be reallocated before it lands. But in our setup, we often abort requests that are too slow, so I believe we are hitting this scenario. The only symptom is a slightly wrong answer to our users.

No cost on the happy-path: no extra collective and no wait unless a request has failed.

Only ~995 lines of implementation code, the rest is tests and doc strings.

Reproduction script

This script reproduces the bug on current main (CPU only, no GPU):

[T1] transfer worker is inside a write to decode page 0xdead0000
[T2] request aborted while that write is still in flight
[T3] scheduler was told: Failed
RESULT: REPRODUCED

Reporting a terminal state is what lets the allocator hand those pages out, so Failed at [T3] is the bug. Asserting the ordering makes it deterministic. With this PR, [T3] reports Transferring and terminal arrives only after the write returns.

The core change

No new states and no new message types. KVPoll keeps its five states (Failed, Bootstrapping, WaitingForInput, Transferring, Success); what changes is which one the scheduler is told, and when:

  • Before: the first failure reported Failed immediately. A terminal state is what authorizes the allocator to hand the request's pages out — so pages could be reused while a transfer was still writing to them.
  • After: until every rank proves the pages are idle, the poll path reports Transferring — a state the scheduler already knows to wait on. Failed arrives only with proof. (Skipped entirely when nothing failed, so the happy path is unchanged.)

Correspondingly, ABORT_ACK changes meaning: from "I heard your abort" to "your pages are safe to reuse." Prefill now sends it only after that room's transfer work has drained, and it must echo a per-request nonce.

Each bootstrap room gets a lease-counted lifetime. Transfer work holds a lease; a failed request closes the room; pages are released once it is closed with zero leases and, on decode, once every peer has acknowledged.

Required for the barrier to hold

Fixed these three paths from bypassing the abort:

  • The lease would drop early. send_kvcache returns on the first failing future, but Future.cancel() is a no-op once a future is running — so siblings keep writing while the worker reports the chunk done.
  • HiCache bypassed the gate. A failed restore releases pages regardless of poll state, including while the barrier is holding.
  • Room identity is ambiguous. bootstrap_room is recycled, so a queued chunk can lease the wrong request's lifetime.

Tests

  • Integrated 3-node H200 validation passed. With two TP8 prefill and one TP8 decode (24 H200s, STRICT policy), 192/192 immediate victim reuse probes succeeded after 192 in-flight donor aborts. There were zero transport, semantic, hard, or critical failures; zero foreign canaries; a decode transfer-queue peak of 20; and zero model restarts. Raw summary, matrix, telemetry, and the admission-control disclosure.
python -m pytest -q test/registered/unit/disaggregation/test_transfer_ownership_barrier.py

71 tests named by property. Every fix has a regression test verified to fail with that fix reverted. unit/disaggregation 72 → 143 passed; unit/managers + unit/mem_cache unchanged apart from additions.

Related PRs

Glad for this to land as a completion of #27372. @AsterWang identified this race first in #24580 but I think my solution is a bit safer.


CI States

Latest PR Test (Base): ❌ Run #31566610945
Latest PR Test (Extra): ✅ Run #31620191653

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions github-actions Bot added the hicache Hierarchical Caching for SGLang label Jul 27, 2026
@KastanDay
KastanDay force-pushed the kastan/mooncake-transfer-ownership-barrier branch from d7bb2e3 to d3f10fb Compare July 27, 2026 19:10
@KastanDay KastanDay changed the title [PD] Defer KV page release until Mooncake transfers have drained [PD] Don't release KV pages while Mooncake transfers are in flight Jul 27, 2026
@KastanDay
KastanDay force-pushed the kastan/mooncake-transfer-ownership-barrier branch from a9a5465 to 7a1b41b Compare July 28, 2026 16:51
@KastanDay
KastanDay force-pushed the kastan/mooncake-transfer-ownership-barrier branch from 7a1b41b to 3d1af6c Compare August 10, 2026 16:11

@ShangmingCai ShangmingCai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @KastanDay , thank you for the digging and the PR, but I think this PR is quite complicated since it introduces many behavior changes. I will explore a more lightweight and universal fix for all transfer backends in the following week.

@ShangmingCai ShangmingCai self-assigned this Aug 13, 2026
ShangmingCai pushed a commit to ShangmingCai/sglang that referenced this pull request Aug 17, 2026
…in-ack

Blocker from review: the drain-ack fires when _staging_outstanding[room] == 0,
which assumes send_kvcache leaves no write in flight when it returns. That holds
for the default synchronous batch_transfer_sync path, and for the custom-mem-pool
executor path on success (it awaits every layer future). But on the failure
branch that path returned on the first failing future and only called
future.cancel() -- a no-op for an already-running future -- so sibling layer
writes could still be landing on the decode pages after the ack (the same
lingering-write hazard sgl-project#32564 calls out). A premature ack there = the corruption
this PR prevents.

Route the three executor loops through _await_transfer_futures(): when deferred
release is enabled it cancels the not-yet-started futures but still drains the
running ones before returning, so _staging_outstanding == 0 provably means no
write is still touching the decode pages. With the feature off it keeps the
original early-return-on-first-failure behavior byte-for-byte.

Scope: covers the KV-page write paths (send_kvcache / _dcp / _slice); send_aux is
synchronous. The mamba/state-component path (maybe_send_extra) is not routed
through this yet -- noted as a follow-up for hybrid models.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KastanDay

Copy link
Copy Markdown
Author

Mostly resolved now.

@KastanDay KastanDay closed this Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hicache Hierarchical Caching for SGLang high priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants