[Feature] Add graceful scheduler shutdown; free hisparse host buffer on exit (#28779)

This commit is contained in:
Liangsheng Yin
2026-06-21 15:08:10 -07:00
committed by GitHub
parent 8e890391f5
commit e6722c751b
7 changed files with 107 additions and 2 deletions
@@ -189,6 +189,13 @@ class HiSparseCoordinator:
def set_decode_producer_stream(self, stream) -> None:
self.decode_producer_stream = stream
def destroy(self) -> None:
# Drain in-flight transfers so the buffer is idle, then unregister it.
# See HostKVCache.destroy for why the explicit unregister matters.
self.write_staging_stream.synchronize()
self.decode_backup_stream.synchronize()
self.mem_pool_host.destroy()
def get_token_stats(self) -> HiSparseTokenStats:
device_allocator = self.token_to_kv_pool_allocator.hisparse_attn_allocator
device_capacity = device_allocator.size
+7
View File
@@ -1814,6 +1814,13 @@ class FreezeGCReq(BaseReq):
pass
@dataclass
class ShutdownReq(BaseReq):
# Broadcast across TP ranks via the normal recv path, so all ranks break
# the scheduler loop on the same iteration.
pass
@dataclass
class ConfigureLoggingReq(BaseReq):
log_requests: Optional[bool] = None
+27 -1
View File
@@ -135,6 +135,7 @@ from sglang.srt.managers.io_struct import (
SendWeightsToRemoteInstanceReqOutput,
SetInternalStateReq,
SetInternalStateReqOutput,
ShutdownReq,
SlowDownReqInput,
SlowDownReqOutput,
TokenizedEmbeddingReqInput,
@@ -352,6 +353,9 @@ class Scheduler(
self.enable_hisparse = server_args.enable_hisparse
self.hisparse_coordinator: Optional[HiSparseCoordinator] = None
# Set by the ShutdownReq handler to break the event loop for graceful shutdown.
self.gracefully_exit = False
# Distributed rank info
attn_tp_rank, attn_tp_size, attn_dp_rank, attn_dp_size = (
compute_dp_attention_world_info(
@@ -1389,6 +1393,7 @@ class Scheduler(
lambda req: self.profiler_manager._profile(req),
),
(FreezeGCReq, self.handle_freeze_gc),
(ShutdownReq, self.handle_shutdown),
(GetInternalStateReq, self.get_internal_state),
(SetInternalStateReq, self.set_internal_state),
(RpcReqInput, self.handle_rpc_request),
@@ -1445,6 +1450,12 @@ class Scheduler(
return result_dict
def release_host_resources(self) -> None:
# Release pinned host buffers in userspace on graceful shutdown; see
# HostKVCache.destroy. Called from run_scheduler_process's finally.
if self.hisparse_coordinator is not None:
self.hisparse_coordinator.destroy()
def run_event_loop(self) -> None:
"""Run the scheduler's event loop.
@@ -1473,6 +1484,9 @@ class Scheduler(
def event_loop_normal(self):
"""A normal scheduler loop."""
while True:
if self.gracefully_exit:
break
# Receive requests
recv_reqs = self.request_receiver.recv_requests()
self.process_input_requests(recv_reqs)
@@ -1509,6 +1523,9 @@ class Scheduler(
self.process_batch_result(tmp_batch, tmp_result)
while True:
if self.gracefully_exit:
break
# Receive requests
recv_reqs = self.request_receiver.recv_requests()
self.process_input_requests(recv_reqs)
@@ -4002,6 +4019,11 @@ class Scheduler(
self.ipc_channels.send_to_detokenizer.send_output(recv_req, recv_req)
return None
def handle_shutdown(self, recv_req: ShutdownReq):
# Break the event loop; the finally in run_scheduler_process releases resources.
self.gracefully_exit = True
return None
def configure_logging(self, recv_req: ConfigureLoggingReq):
if recv_req.log_level is not None:
logging.getLogger().setLevel(recv_req.log_level.upper())
@@ -4182,7 +4204,7 @@ def run_scheduler_process(
# Send initialization info back to the parent process
pipe_writer.send(scheduler.get_init_info())
# Run the event loop (blocks until shutdown)
# Run the event loop (blocks until a ShutdownReq sets gracefully_exit)
scheduler.run_event_loop()
except Exception:
@@ -4201,3 +4223,7 @@ def run_scheduler_process(
# FPM has a background ZMQ publisher thread that needs explicit
# teardown to flush queued metrics and close the socket cleanly.
scheduler.metrics_reporter._shutdown_fpm()
# Graceful path only: on the exception path the GPU may be wedged
# and the synchronize() in destroy() could itself hang.
if scheduler.gracefully_exit:
scheduler.release_host_resources()
@@ -70,6 +70,7 @@ from sglang.srt.managers.io_struct import (
OpenSessionReqOutput,
PauseGenerationReqInput,
SessionParams,
ShutdownReq,
TokenizedEmbeddingReqInput,
TokenizedGenerateReqInput,
UpdateWeightFromDiskReqInput,
@@ -2642,6 +2643,15 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin):
else:
break
# Stop the watchdog: child exits are expected during shutdown, not crashes.
if self._subprocess_watchdog is not None:
self._subprocess_watchdog.stop()
# Ask schedulers to release resources in userspace and exit (see
# ShutdownReq), then wait for them before hard-killing the rest.
self.send_to_scheduler.send_pyobj(ShutdownReq())
deadline = time.monotonic() + 15
while time.monotonic() < deadline and collect_scheduler_processes():
time.sleep(0.1)
kill_process_tree(os.getpid(), include_parent=True)
sys.exit(0)
+28 -1
View File
@@ -10,10 +10,17 @@ import psutil
import torch
from sglang.srt.mem_cache.memory_pool import KVCache
from sglang.srt.mem_cache.pool_host.common import get_allocator_from_storage
from sglang.srt.mem_cache.pool_host.common import (
_cuda_host_unregister,
get_allocator_from_storage,
)
from sglang.srt.utils import is_cuda, is_hip
logger = logging.getLogger(__name__)
_is_cuda = is_cuda()
_is_hip = is_hip()
# Host RAM to leave free when sizing HiCache pools (OS, other processes).
HICACHE_HOST_MEMORY_RESERVE_BYTES: int = 10 * (1024**3)
@@ -86,6 +93,26 @@ class HostKVCache(abc.ABC):
self.lock = threading.RLock()
self.clear()
def destroy(self):
"""Unregister pinned host buffers in userspace before process exit.
Large cudaHostRegister'd buffers are otherwise unpinned by the kernel
during SIGKILL reclaim, which can stall teardown in uninterruptible
sleep for tens of seconds. Idempotent. (Only the host_register path
needs this; npu/musa pin_memory buffers are freed by torch.)
"""
if getattr(self, "_destroyed", False):
return
self._destroyed = True
buffers = getattr(self, "kv_buffer", None)
if buffers is not None and self.pin_memory and (_is_cuda or _is_hip):
if not isinstance(buffers, (list, tuple)):
buffers = [buffers]
for buf in buffers:
if buf is not None:
_cuda_host_unregister(buf)
self.kv_buffer = None
@abc.abstractmethod
def get_size_per_token(self):
raise NotImplementedError()
@@ -57,6 +57,19 @@ def _cuda_host_register(buffer: torch.Tensor) -> None:
)
def _cuda_host_unregister(buffer: torch.Tensor) -> None:
cudart = torch.cuda.cudart()
rc = cudart.cudaHostUnregister(buffer.data_ptr())
if int(rc) != 0:
# Best-effort on shutdown: warn, don't raise -- a leak is reclaimed at exit.
logger.warning(
"cudaHostUnregister failed (rc=%d, %s) for ptr=%#x",
int(rc),
cudart.cudaGetErrorString(rc),
buffer.data_ptr(),
)
def alloc_with_host_register(
dims: tuple,
dtype: torch.dtype,