[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:
co-authored by
Claude Opus 5
parent
100e1cd0d9
commit
3f8eb35ead
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user