[PD] Graceful shutdown for disaggregation with RDMA cleanup - #22200
Kangyan-Zhou wants to merge 1 commit into
Conversation
… propagation) When Kubernetes sends SIGTERM to SGLang pods running PD disaggregation, the sigterm_watchdog previously sent SIGKILL to scheduler child processes, bypassing all Python cleanup (atexit, __del__, C++ destructors). This left Mooncake/NIXL RDMA memory regions registered, causing pod sandbox teardown to hang with FailedKillPod errors. Parent side (tokenizer_manager): - Add graceful_kill_process_tree() that sends SIGTERM first, waits up to a configurable timeout (SGLANG_CHILD_PROCESS_SHUTDOWN_TIMEOUT, default 10s), then SIGKILL for stragglers - Use os._exit(0) instead of sys.exit(0) to avoid SystemExit being caught by the asyncio event loop Child side (scheduler + KV managers): - Register SIGTERM handler in scheduler that calls sys.exit(143) so atexit handlers run instead of the default immediate termination - Register atexit handler that calls kv_manager.shutdown() for RDMA cleanup - Add CommonKVManager.shutdown() that closes ZMQ socket (linger=0) and terminates context, unblocking threads stuck on recv_multipart() - Add MooncakeKVManager.shutdown() that shuts down thread pool executors then deregisters all RDMA memory (kv, aux, state buffers) - Add NixlKVManager.shutdown() that deregisters RDMA memory via NIXL agent - Mark bootstrap_thread, decode_thread, heartbeat_checker as daemon=True Combines sgl-project#16484 and sgl-project#19810. Co-Authored-By: chenkaiyue <chenkaiyue2008@163.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a graceful shutdown mechanism for disaggregation components to ensure RDMA memory is deregistered and ZMQ sockets are closed, preventing Kubernetes pod teardown hangs. Key changes include adding shutdown methods to KV managers, implementing a graceful_kill_process_tree utility, and registering atexit and SIGTERM handlers in the scheduler. Feedback focuses on ensuring the idempotency of the shutdown process and preventing potential duplicate atexit registrations for the cleanup handler.
| try: | ||
| self.server_socket.close(linger=0) | ||
| except Exception as e: | ||
| logger.warning(f"Failed to close ZMQ server socket: {e}") | ||
| try: | ||
| self._zmq_context.term() | ||
| except Exception as e: | ||
| logger.warning(f"Failed to terminate ZMQ context: {e}") |
There was a problem hiding this comment.
The shutdown method should ideally be idempotent. If shutdown is called multiple times, the second call might raise an exception when attempting to close the socket or terminate the context again. Adding a check to ensure the socket and context are still valid or using a flag would make this more robust.
| def _shutdown_kv_manager(): | ||
| try: | ||
| kv_mgr = None | ||
| if hasattr(scheduler, "disagg_prefill_bootstrap_queue"): | ||
| kv_mgr = scheduler.disagg_prefill_bootstrap_queue.kv_manager | ||
| elif hasattr(scheduler, "disagg_decode_prealloc_queue"): | ||
| kv_mgr = scheduler.disagg_decode_prealloc_queue.kv_manager | ||
| if kv_mgr is not None and hasattr(kv_mgr, "shutdown"): | ||
| kv_mgr.shutdown() | ||
| except Exception as e: | ||
| logger.warning(f"Error during KV manager shutdown: {e}") | ||
|
|
||
| atexit.register(_shutdown_kv_manager) |
There was a problem hiding this comment.
The _shutdown_kv_manager function is defined inside the loop and registered with atexit. If run_scheduler_process is called multiple times or if the scheduler is re-initialized, this could lead to multiple atexit registrations. It is better to register the cleanup handler once or ensure it is cleaned up properly.
Summary
Combines #16484 and #19810 into a single PR for graceful shutdown of SGLang PD disaggregation pods.
When Kubernetes sends SIGTERM to SGLang pods, the
sigterm_watchdogpreviously sent SIGKILL to scheduler child processes, bypassing all Python cleanup (atexit,__del__, C++ destructors). This left Mooncake/NIXL RDMA memory regions registered, causing pod sandbox teardown to hang withFailedKillPoderrors.Parent side (tokenizer_manager)
graceful_kill_process_tree()that sends SIGTERM first, waits up to a configurable timeout (SGLANG_CHILD_PROCESS_SHUTDOWN_TIMEOUT, default 10s), then SIGKILL for stragglersos._exit(0)instead ofsys.exit(0)to avoidSystemExitbeing caught by the asyncio event loopChild side (scheduler + KV managers)
sys.exit(143)so atexit handlers runatexithandler that callskv_manager.shutdown()for RDMA cleanupCommonKVManager.shutdown()— closes ZMQ socket (linger=0) and terminates context, unblocking threads onrecv_multipart()MooncakeKVManager.shutdown()— shuts down thread pool executors, then deregisters all RDMA memory (kv, aux, state buffers)NixlKVManager.shutdown()— deregisters RDMA memory via NIXL agentbootstrap_thread,decode_thread,heartbeat_checkerasdaemon=TrueTest plan
FailedKillPoderrors in Kubernetes🤖 Generated with Claude Code