[PD] Do not admit intake-rejected requests to a PD handoff (#38935)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Shangming Cai
2026-09-16 12:44:49 +08:00
committed by GitHub
co-authored by Claude Opus 5
parent 100e1cd0d9
commit 3f8eb35ead
5 changed files with 395 additions and 0 deletions
@@ -60,6 +60,7 @@ from sglang.srt.disaggregation.utils import (
get_qsa_pending_state_indices,
is_dsv4_c128_online_enabled,
is_mla_backend,
is_unadmitted_reject,
poll_and_all_reduce,
poll_and_all_reduce_pp,
poll_and_all_reduce_with_staging,
@@ -649,6 +650,13 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
dispatch happens later, after preallocation and ``send_metadata`` (see
``pop_preallocated``).
"""
# See `PrefillBootstrapQueue.add`. A retracted or rebootstrapping
# request owns a host KV backup that `retracted_queue` releases, and by
# this point carries none of the markers `is_unadmitted_reject` reads,
# so take the caller's word for it rather than sniffing.
if not is_retracted and not is_rebootstrap and is_unadmitted_reject(req):
self.scheduler.retire_unadmitted_request(req)
return
if self._check_if_req_exceed_kv_capacity(req):
return
@@ -52,6 +52,7 @@ from sglang.srt.disaggregation.utils import (
is_aborted,
is_dsv4_c128_online_enabled,
is_mla_backend,
is_unadmitted_reject,
poll_and_all_reduce_attn_cp_tp_group,
poll_and_all_reduce_pp,
prepare_abort,
@@ -397,6 +398,13 @@ class PrefillBootstrapQueue:
return True
def add(self, req: Req, num_kv_heads: int) -> None:
# Rejected at intake: `set_finish_with_abort` left the verdict in
# `to_finish`, which `finished()` does not read, and swapped the prompt
# for a one-token stub. Bootstrapping it costs a handshake, a metadata
# buffer and a forward pass before anything unwinds it.
if is_unadmitted_reject(req):
self.scheduler.retire_unadmitted_request(req)
return
if not self.create_sender(req, num_kv_heads):
return
self.queue.append(req)
+29
View File
@@ -1704,6 +1704,35 @@ def prepare_abort(req: Req, error_message: str, status_code=None):
req.logprob.input_token_ids_logprobs_idx = []
def is_unadmitted_reject(req: Req) -> bool:
"""A request rejected at intake, before it acquired anything.
A preempted or resumed request can also carry a pending abort -- "Abort
method 3" marks a *running* request and `filter_batch` does not drop it,
since `finished()` is still False -- and its queue owns the release of
whatever it still holds.
`req.is_retracted` catches the two re-entries that declare nothing:
priority preemption and the pause/retract-all path both requeue through a
bare `_add_request_to_queue`. `release_req` always calls
`reset_for_retract`, which sets it, and its clear sites all run downstream
of these doors. The resource markers stay as a second line of defence --
on their own they miss a `seqlen <= 1` preemption, whose KV is already
freed and whose `retraction_backup` was never taken.
`DecodePreallocQueue.add` still gates on its own `is_retracted` /
`is_rebootstrap` parameters as well, since they state the caller's intent
rather than inferring it.
"""
return is_aborted(req) and not (
req.is_retracted
or req.kv.holds_kv
or req.kv.holds_mamba
or req.metadata_buffer_index >= 0
or req.kv.retraction_backup is not None
)
def is_aborted(req: Req) -> bool:
from sglang.srt.managers.schedule_batch import FINISH_ABORT
+24
View File
@@ -3198,6 +3198,30 @@ class Scheduler(
self._retry_storage_prefetch(req)
return True
def retire_unadmitted_request(self, req: Req) -> None:
"""Finish a request the disaggregation queues rejected at their door."""
# `create_req` marks a streaming session in-flight, and the pre-abort
# detach lives in `StreamingSession.find_active_slot`, which only runs
# while scheduling; a session left in-flight rejects every later request.
if req.session is not None and req.session.streaming:
req.session.abort_req()
req.session = None
# `beam_coordinator.validate_and_init` counts the group in ahead of the
# checks that reject; no-op when the request has no group.
self.beam_coordinator.retire_group(req)
# PREFILL runs `_prefetch_kvcache` before its door, so even the
# one-token stub is registered with the cache by now:
# `prefetch_from_storage` arms the paced-retry set for this attempt's
# cache handle. Only a `finish`/ABORT and a `waiting_queue` sweep clear
# that, and a retired request reaches neither.
self._release_aborted_request(req)
# `update_finish_state` returns early once `finished()`, so an already
# set `finished_reason` is what the client receives; report the same.
reason = req.finished_reason or req.to_finish
req.time_stats.trace_ctx.abort(abort_info={"reason": reason.message})
req.update_finish_state()
self.output_streamer.stream_output([req], req.return_logprob)
def _add_request_to_queue(self, req: Req, is_retracted: bool = False):
if not self._set_or_validate_priority(req):
return