Skip to content

[Bugfix] Keep disaggregation failure status terminal until clear - #22

Closed
KastanDay wants to merge 11 commits into
mainfrom
kastan/upstream-terminal-failure-absorbing
Closed

KastanDay wants to merge 11 commits into
mainfrom
kastan/upstream-terminal-failure-absorbing

Conversation

@KastanDay

@KastanDay KastanDay commented Jul 22, 2026 •

Copy link
Copy Markdown
Owner

Why

CommonKVManager.update_status() uses the numeric maximum to advance a transfer room through its lifecycle. KVPoll.Failed is 0, so a room that has already failed can be promoted back to Transferring or Success by a late completion update.

That makes transfer outcome depend on message ordering. Once any participant reports failure, later work for the same room must not make the request eligible for decode.

KV cache corruption mechanism

This race can produce incorrect model output rather than only an error or restart:

  1. A KV or auxiliary-state transfer fails after writing only part of the destination state, leaving some destination pages stale or incomplete.
  2. The room is marked KVPoll.Failed.
  3. A late transfer callback reports Transferring or Success; max(Failed, Success) changes the room back to Success.
  4. Decode treats Success as permission to commit the transfer and begin generation. The metadata readiness gate confirms that request metadata arrived, but it does not prove that every KV or model-state page was transferred successfully.
  5. Decode can therefore read stale or partially written KV/state pages, including DSA indexer state, and generate tokens from the wrong context.

The fix is fail-closed: once failure is observed for a room, late completion cannot authorize decode.

What changed

  • Make KVPoll.Failed absorbing when either the current room state or the incoming update is failed.
  • Preserve normal monotonic state advancement for non-failed rooms.
  • Add registered CPU coverage for late Transferring and Success updates, explicit room reuse, and late failure after cleanup.

Scope

This PR enforces the status-state invariant after a failure has been reported. It does not detect transfer failures or wait for native transfer work to stop before pages are released.

The changes are independent and complementary: failure detection, terminal failure state, and transfer quiescence close different corruption paths.

Validation

The registered regression test covers the state transitions directly:

python -m pytest -q \
  test/registered/unit/disaggregation/test_common_kv_status.py

Current-main validation completed with python3 -m py_compile and git diff --check. Local pytest execution is blocked on macOS by the Linux/CUDA-only triton import path; GitHub currently reports no checks for this branch.


CI States

Latest PR Test (Base): ❌ Missing run-ci label -- add it to run CI tests.
Latest PR Test (Extra): ❌ Blocked -- run-ci is required first.

@KastanDay KastanDay added the 100% truly worth submitting upstream to SGLang 100% truly worth submitting upstream to SGLang directly; current-main gap plus strong validation. label Jul 22, 2026
@KastanDay

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 3af63198b3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a6d1b36734

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 424 to 425
if status != KVPoll.Bootstrapping:
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve pre-sender Mori metadata readiness

When Mori transfer metadata arrives before the prefill scheduler creates MoriKVSender, _handle_transfer_message() intentionally accepts the absent-room case and calls update_status(..., KVPoll.WaitingForInput). This new guard discards that readiness update; the sender later creates the room as Bootstrapping, but the already-received metadata is never replayed, so MoriKVSender.poll() remains bootstrapping until it times out. Preserve this supported ordering by applying the stored readiness when the sender creates the room, while still preventing post-clear stale updates.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 98ece61104

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 256 to 257
if status != KVPoll.Bootstrapping:
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve early metadata readiness for Mooncake and NIXL

When destination metadata arrives before the prefill scheduler constructs its sender, this now discards the WaitingForInput transition. The Mooncake and NIXL bootstrap threads issue exactly that transition at mooncake/conn.py:1598 and nixl/conn.py:2450, while their sender constructors do not re-evaluate already-complete metadata after creating the room; only Mori received such a recheck. Consequently, metadata-first requests remain Bootstrapping and eventually time out despite having all destination information, so those backends need the same deferred-readiness handling before non-initial transitions are rejected.

Useful? React with 👍 / 👎.

Comment on lines 263 to 267
if (
self.request_status[bootstrap_room] == KVPoll.Failed
or status == KVPoll.Failed
):
self.request_status[bootstrap_room] = KVPoll.Failed

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Serialize the terminal failure transition

When a completion callback overlaps a failure callback from another worker, heartbeat, or abort thread, this compound transition is not atomic: the completion can observe a non-failed state in this condition, the failure can then store Failed, and the completion can subsequently execute the max(...) branch and write Success. That preserves the KV-corruption race this change is intended to close under concurrent delivery; guard the full read/decision/write transition with a shared lock or otherwise make it atomic.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cf13c6c552

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if ready_rooms is None:
ready_rooms = set()
self._metadata_ready_rooms = ready_rooms
ready_rooms.add(bootstrap_room)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject metadata readiness from a cleared generation

When destination metadata arrives after clear_status() but before the same bootstrap_room is reused, this unconditionally recreates the readiness marker even though update_status() correctly ignores the absent room. The next CommonKVSender for that ID calls _mark_room_ready_if_metadata_complete() and immediately advances to WaitingForInput, allowing Mooncake/NIXL/Mori to use metadata and destination pointers belonging to the previous request; repeated room IDs are supported and some benchmark clients always use room 0. Associate readiness with a room generation or retain a tombstone so delayed metadata cannot authorize the next request.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 486eb7e65d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +294 to +298
tombstones = getattr(self, "_cleared_room_tombstones", None)
if tombstones is None:
tombstones = set()
self._cleared_room_tombstones = tombstones
tombstones.add(bootstrap_room)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bound or retire cleared-room tombstones

Every sender or receiver cleanup permanently adds its room ID to _cleared_room_tombstones, while IDs are removed only if that exact room is reused. In normal deployments where requests receive unique or monotonically increasing room IDs, this set therefore grows by one entry per completed or aborted request for the lifetime of each manager, eventually consuming unbounded host memory on long-running, high-throughput servers. Cleared-generation protection needs bounded storage or explicit retirement.

Useful? React with 👍 / 👎.

Comment on lines +303 to +306
if bootstrap_room in getattr(
self, "_cleared_room_tombstones", set()
):
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve early metadata for a reused room

When a caller reuses a cleared bootstrap_room and the new decode metadata arrives before the new prefill sender is constructed, this tombstone check discards the new generation's readiness notification. The sender subsequently removes the tombstone while entering Bootstrapping, but _metadata_ready_rooms has no entry to advance it to WaitingForInput, so the request remains bootstrapping until it times out even though its new metadata is already present in transfer_infos. This ordering is specifically supported for first-use rooms and must also work after room reuse.

Useful? React with 👍 / 👎.


def clear(self) -> None:
self.kv_mgr.request_status.pop(self.bootstrap_room, None)
self.kv_mgr.clear_status(self.bootstrap_room)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Update the NIXL cleanup test double for clear_status

This new method call breaks the pre-existing registered CPU test test_nixl_sender_failure_cleanup.py, whose lightweight kv_mgr is a SimpleNamespace without clear_status. Running PYTHONPATH=python python -m pytest -q test/registered/unit/disaggregation now fails here with AttributeError instead of verifying that NixlKVSender.failure_exception() cleans state and raises the recorded transfer error, so the registered unit suite cannot pass until the fixture or cleanup compatibility is updated.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a94801f1d6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +267 to +270
if status not in (
KVPoll.Bootstrapping,
KVPoll.WaitingForInput,
):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Block stale readiness from recreating cleared rooms

When a delayed Mori or NIXL metadata message from an old generation arrives after CommonKVSender.clear(), its handler calls update_status(..., KVPoll.WaitingForInput) while the room is absent, and this allowlist recreates the status entry while the handler also repopulates transfer_infos. If a client later reuses that bootstrap_room, the sender's Bootstrapping update remains at WaitingForInput and it can consume the previous request's destination metadata, defeating cleanup and potentially transferring KV into stale pages. Metadata-before-sender must remain supported, but readiness needs generation or tombstone correlation so readiness arriving after a clear cannot initialize the next generation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6897a882a3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

mgr=self.kv_manager,
bootstrap_addr=_bootstrap_addr(req),
bootstrap_room=req.bootstrap_room,
request_id=req.rid,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use a generation ID shared by both PD workers

When a normal OpenAI request omits the optional rid, the model gateway injects only the bootstrap host/port/room (sgl-model-gateway/src/routers/http/pd_router.rs:232-285) and dispatches the same rid-less request to both workers; each worker then independently generates a UUID in GenerateReqInput._normalize_single_inputs() (python/sglang/srt/managers/io_struct.py:427-432). Consequently, this decode-side ID differs from the prefill-side req.rid passed in prefill.py:274, so mark_metadata_ready() rejects the metadata for the active generation and the request remains bootstrapping until timeout. Generate a shared ID in the router or base generation matching on a value guaranteed to be common to both workers.

Useful? React with 👍 / 👎.

Comment on lines +275 to +279
if (
self.request_status[bootstrap_room] == KVPoll.Failed
or status == KVPoll.Failed
):
self.request_status[bootstrap_room] = KVPoll.Failed

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Validate the generation on every status update

When a room is cleared, reused, and then receives a late completion or failure callback from the old transfer—the ordering this change is intended to handle—update_status() sees that the room exists again and applies the stale status to the new request because callers provide only the room, not its request ID. A stale failure therefore makes the new generation terminally failed, while a stale success can advance it prematurely; the new _room_request_ids guard protects clear_status() and metadata only. Status callbacks need to carry and validate the generation before mutating the active room.

Useful? React with 👍 / 👎.

Comment on lines +2443 to +2448
request_ids = {
info.request_id for info in self.transfer_infos[room].values()
}
if (
len(self.transfer_infos[room]) == required_dst_info_num
and len(request_ids) == 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Purge stale transfer metadata instead of only checking its ID

When metadata from a cleared generation arrives before a reused room's sender is created, this handler stores it in transfer_infos even though mark_metadata_ready() only records its request ID separately. If the new generation then reports metadata under new agent names, the stale entry makes the map larger than required_dst_info_num and this equality can never pass; with a reused agent name, a late stale packet can instead overwrite a new destination after the room is already ready because the handler does not check the active status or ID before assignment. The transfer-info table must be partitioned or purged by request ID, not merely checked for ID consensus after mutation.

Useful? React with 👍 / 👎.

Comment on lines +1491 to +1492
if not clear_status(self.bootstrap_room, self.request_id):
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Propagate a rejected clear to backend cleanup

When an old receiver calls clear() after its room has been reused, this generation check correctly leaves the new status intact but returns only from CommonKVReceiver.clear(). MoriKVReceiver.clear() (mori/conn.py:1780-1784) then continues into _cleanup_room_tracking() and removes the new receiver's room_to_bootstrap_addr and addr_to_rooms_tracker entries; a subsequent prefill heartbeat failure therefore no longer marks the active request failed and it remains stuck until its waiting timeout. Return the clear result to subclasses or make the Mori cleanup conditional on the same generation check.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b6feb523a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

is_last_chunk=is_last_chunk,
prefill_aux_index=aux_index,
state_indices=state_indices,
request_id=request_id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Correlate Mooncake staging notifications with the generation

When staging is enabled and a bootstrap room is reused after an abort or clear, this generation ID is carried by the queued chunk but _send_chunk_ready() still sends only the room and chunk metadata. A delayed CHUNK_READY from the retired generation is therefore accepted by the decode thread and routed to the new room's DecodeRequest; once enough old writers arrive, handle_chunk_arrived() scatters and releases the new request's staging allocation before its own transfer completes, which can commit stale or incomplete KV data. Include and validate the request generation in the CHUNK_READY protocol, as is already done for terminal status messages.

Useful? React with 👍 / 👎.

@@ -271,6 +271,7 @@ def create_sender(self, req: Req, num_kv_heads: int) -> bool:
dest_tp_ranks=dest_tp_ranks,
pp_rank=self.pp_rank,
req_has_disagg_prefill_dp_rank=req.disagg_prefill_dp_rank is not None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Supply a shared request ID for MiniLB PD dispatches

When a request without an explicit rid uses the supported Python --mini-lb path, mini_lb.py forwards copies of the same payload to prefill and decode but only injects bootstrap fields; each server therefore generates an independent UUID for req.rid. Making that UUID the generation key here means decode metadata is stored under the decode UUID while the prefill room is active under a different UUID, so store_transfer_info() rejects or partitions the metadata and the request eventually times out in bootstrapping. The Rust PD router's new rid injection does not cover MiniLB, so every PD dispatcher must inject one shared ID before dual dispatch (or the protocol must use a field already shared by both requests).

Useful? React with 👍 / 👎.

Comment on lines +460 to +462
retired = getattr(self, "_retired_room_generations", {})
if request_id and (bootstrap_room, request_id) in retired:
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Allow intentional rebootstrap of a retired request ID

During PD true-retraction rebootstrap, build_rebootstrap_payload() intentionally reuses the original rid and bootstrap_room, and the decode path sends transfer metadata before submitting the HTTP prefill recompute. The original prefill sender has already cleared and retired exactly this tuple, so if the ZMQ metadata arrives before the recompute request constructs the replacement sender, this branch discards the only metadata message as stale; the replacement sender then remains in Bootstrapping until timeout. Rebootstrap needs a fresh generation token, or the tuple must be explicitly reopened before metadata is sent rather than treating every reuse of the same request ID as retired.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bddfc1f901

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +585 to +590
request_id = _generation_request_id(req, is_rebootstrap)
kv_receiver = kv_receiver_class(
mgr=self.kv_manager,
bootstrap_addr=_bootstrap_addr(req),
bootstrap_room=req.bootstrap_room,
request_id=request_id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep generation IDs stable across parallel sampling

When n > 1, the tokenizer manager independently replaces each sample's RID with random UUIDs on the prefill and decode servers (tokenizer_manager.py:1655 and :1674). Using that RID here as the transfer generation therefore gives the two sides different request_id values, so the prefill rejects the decode metadata as belonging to another generation and the request eventually times out. This affects parallel-sampling requests even through the updated built-in routers; derive the generation from shared deterministic data such as the bootstrap room, or make the per-sample RID expansion deterministic across both workers.

Useful? React with 👍 / 👎.

Comment on lines +428 to +431
return bootstrap_room in self.request_status and (
active_request_id == request_id
or (active_request_id is None and request_id == "")
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Accept legacy messages during rolling upgrades

In a mixed-version prefill/decode deployment, the older peer omits the newly added request-ID frame, which all of the updated parsers explicitly accept as None; however, this strict comparison normalizes that to "" and rejects it whenever the new peer's active generation has a router-injected RID. A new prefill therefore discards metadata from an old decode, while a new decode discards completion notifications from an old prefill, causing every in-flight request across the version boundary to time out. Either negotiate generation support or retain a safe legacy fallback for messages without the optional frame.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce1a4761eb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

mgr.agent = NotificationFakeAgent(messages)
rooms = {int(message.split("_", 1)[0]) for message in messages}
mgr.request_status = {room: KVPoll.WaitingForInput for room in rooms}
mgr._room_request_ids = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Initialize legacy room IDs in the notification fixture

Running PYTHONPATH=python python -m pytest -q test/registered/unit/disaggregation/test_nixl_backend_basic.py now fails the existing untagged notification cases because this empty mapping makes is_current_generation(room, None) compare None with the normalized empty request ID and reject every message at NixlKVManager.update_transfer_status(). Populate each fixture room with the legacy empty ID (for example, {room: "" for room in rooms}) before overriding room 9 in the generation-specific test; otherwise the registered suite fails for KV, auxiliary, state, and staging notifications.

Useful? React with 👍 / 👎.

Comment on lines +283 to +285
obj.insert(
Self::RID_KEY.to_string(),
Value::Array(rids.into_iter().map(Value::from).collect()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep a scalar RID for chat parallel sampling

For /v1/chat/completions with n > 1 and no caller RID, get_chat_batch_size() returns n, so this inserts an n-element RID array even though SGLang models the request as one base prompt with parallel_sample_num=n. GenerateReqInput._normalize_rid() requires an incoming RID list to have the base batch_size (1 here), and consequently raises ValueError: The specified rids length mismatch with the batch_size for batch processing. before generation. Distinguish parallel sampling from a true input batch and inject one shared scalar RID for this case.

Useful? React with 👍 / 👎.

Comment on lines +163 to +168
digest = hashlib.blake2b(
f"{base_room}:{base_rid}:{sample_index}".encode("utf-8"),
digest_size=8,
person=b"sglang-pd",
).digest()
return int.from_bytes(digest, "big") & ((1 << 63) - 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve DP affinity for derived parallel-sample rooms

When PD workers use follow_bootstrap_room with DP size greater than one and no external DP-rank override, DataParallelController.follow_bootstrap_room_scheduler() selects the worker using bootstrap_room % len(workers). The common-prefix warmup remains on base_room while this unconstrained hash sends most samples to different DP workers, whose independent radix caches do not contain that warmed prefix; parallel sampling therefore recomputes the prompt on roughly all but 1 / dp_size samples and defeats the optimization this path performs. Derive distinct rooms that retain the base room's DP affinity.

Useful? React with 👍 / 👎.

Comment on lines 2431 to 2433
if (
room_to_be_aborted in self.request_status
self.is_current_generation(room_to_be_aborted, request_id)
and self.check_status(room_to_be_aborted) != KVPoll.Success

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Accept legacy aborts before metadata negotiation

During a rolling upgrade, an old decode worker sends the four-frame ABORT format, so request_id is None. If it aborts during handshake before sending transfer metadata, the new prefill manager has not yet called allow_legacy_generation(), causing this generation check to reject the abort even though the room is active; the prefill request and its tree-cache ownership then remain until the bootstrap timeout. The Mooncake handler has the same ordering problem, so legacy capability must be established before metadata arrives or untagged early aborts need a safe compatibility path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a64d1d6967

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1710 to +1714
sample_room = _parallel_sample_bootstrap_room(
objs[i].bootstrap_room,
objs[i].rid,
sample_index,
getattr(self.server_args, "dp_size", 1),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Derive shared sample rooms independently of local DP size

When the prefill and decode deployments have different dp_size values, each TokenizerManager calls this helper with its own local size, so the same base request and sample index almost always produce different bootstrap rooms on the two sides. Heterogeneous prefill/decode DP configurations are supported by the routing and bootstrap code, but parallel-sampling requests in such configurations will now leave every sender and receiver waiting in different rooms until timeout; the shared room must be derived from data that is identical on both workers (or from the prefill topology explicitly communicated to both).

Useful? React with 👍 / 👎.

@@ -271,6 +271,7 @@ def create_sender(self, req: Req, num_kv_heads: int) -> bool:
dest_tp_ranks=dest_tp_ranks,
pp_rank=self.pp_rank,
req_has_disagg_prefill_dp_rank=req.disagg_prefill_dp_rank is not None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Generate one shared RID in every PD router

For requests routed through experimental/sgl-router, this new generation tag differs between prefill and decode: that router's build_outgoing_body injects only bootstrap_host, bootstrap_port, and bootstrap_room (chat.rs:723-738), while each engine independently assigns a random UUID when rid is absent (io_struct.py:431-432). Consequently the receiver sends one RID and this sender registers another, so the prefill side rejects the transfer metadata as a stale generation and ordinary OpenAI requests time out. The experimental router must inject a shared RID into the body just like the two routers updated in this commit.

Useful? React with 👍 / 👎.

@KastanDay
KastanDay force-pushed the kastan/upstream-terminal-failure-absorbing branch from a64d1d6 to 2c57835 Compare August 10, 2026 16:11
@KastanDay

Copy link
Copy Markdown
Owner Author

Superseded by PR 30, which carries the corruption-relevant core of this change (absorbing Failed status plus a locked status read-modify-write) without the room-generation tagging protocol — the tagging here silently drops untagged abort signals from older peers, which is a fail-open regression, and the branch now conflicts with main.

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

Labels

100% truly worth submitting upstream to SGLang 100% truly worth submitting upstream to SGLang directly; current-main gap plus strong validation. model-gateway

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant