scheduler: CP-symmetric idle check for health-check admission
build-sglang-image / build (push) Successful in 27m52s

Health-check admit/skip used is_fully_idle(), which includes rank-local
hicache drain queues; ranks diverge right after activity, so one rank
dispatched the health-check generate while others piggyback-skipped,
deadlocking CP (hicache drain all_reduce vs CP request broadcast).
Seen on cp2/cp4 + hicache L3 after router health checks.

Recovered from b300 /data/ymk/sglang working copy (uncommitted WIP).
This commit is contained in:
2026-09-24 12:01:57 +08:00
parent 67d8368a84
commit b91137ab98
+32 -3
View File
@@ -2097,9 +2097,13 @@ class Scheduler(
vmm_errors = self._materialize_cuda_vmm_inputs(recv_req) vmm_errors = self._materialize_cuda_vmm_inputs(recv_req)
# Skip health check when server is busy — ongoing requests already carry health info. # Skip health check when server is busy — ongoing requests already carry health info.
if is_health_check_generate_req(recv_req) and not self.is_fully_idle( # NOTE: the admit/skip decision must be identical on every CP/TP rank.
for_health_check=True # is_fully_idle() includes rank-local hicache drain queues, which diverge
): # across ranks right after activity; a divergent decision lets one rank
# dispatch the health-check generate while others piggyback-skip, breaking
# collective ordering (deadlock: one rank blocks in the hicache drain
# all_reduce while another waits in the CP request broadcast).
if is_health_check_generate_req(recv_req) and not self.is_sched_idle_cp_symmetric():
self.return_health_check_ipcs.append( self.return_health_check_ipcs.append(
getattr(recv_req, "http_worker_ipc", None) getattr(recv_req, "http_worker_ipc", None)
) )
@@ -4982,6 +4986,31 @@ class Scheduler(
else: else:
self.metrics_reporter.record_scheduler_active() self.metrics_reporter.record_scheduler_active()
def is_sched_idle_cp_symmetric(self) -> bool:
"""Idle check using only state that is identical across CP/TP ranks.
Request/batch/queue state is collectively maintained (requests arrive
via broadcast, batches are collectively scheduled), so every rank
computes the same result. Rank-local hicache drain and disagg transfer
queues are deliberately excluded: those are exactly the terms that
diverge across ranks and caused the CP health-check deadlock
(hicache drain all_reduce vs CP request broadcast cross-collective
wait, seen on cp2/cp4 + hicache L3 right after router health checks).
Used only for health-check admission; all other idle logic keeps using
is_fully_idle().
"""
return (
self.running_batch.is_empty()
and self.chunked_req is None
and not self.dllm_manager.any_staging_reqs()
and (self.last_batch is None or self.last_batch.is_empty())
and (not self.enable_overlap or len(self.result_queue) == 0)
and self._pp_microbatches_drained()
and len(self.waiting_queue) == 0
and len(self.grammar_manager.grammar_queue) == 0
)
def is_fully_idle(self, for_health_check=False) -> bool: def is_fully_idle(self, for_health_check=False) -> bool:
# Health check piggybacks on running requests in process_output. # Health check piggybacks on running requests in process_output.
# Only running_batch + waiting_queue guarantee active GPU processing; # Only running_batch + waiting_queue guarantee active GPU processing;