Repository navigation
fix(sglang): remove TP worker sockets left behind by SIGINT - #553
rishabhsinha17 wants to merge 9 commits into
Conversation
汇总并转发各 scheduler 的 IPC 段名,补齐 controller readiness 回归与真实 spawn/fork 进程测试。 验证:Linux CPU 1270 passed、12 skipped;真实进程测试 4 passed;RTX 4090 物理 2、3 号卡上的 DP=2 SIGTERM、同名重启和重复 Engine.shutdown 均通过。18 次 HTTP 与 2 次 Engine 输出均与原 PR 对照一致,退出后无控制段残留。
shipiyouniao
left a comment
There was a problem hiding this comment.
Tested 74bda5f on Linux with Python 3.11 and psutil 7.1.0. The related CPU suites pass, but both spawn cases in the real-process socket suite fail on two runs (2 failed, 2 passed each time). The remaining issue is completing cleanup during the final shutdown call, not the identity/liveness protection itself. Details inline. These are real subprocess tests, not a SGLang GPU serving run.
Co-authored-by: Ursasi <zshprint@163.com>
On SIGINT, python -m sglang.launch_server and sglang serve end with their own kill_process_tree(os.getpid(), include_parent=False). It SIGKILLs every scheduler before the atexit hook that unlinks the scheduler's IPC socket can run, and /tmp/kvcached-tp-<name>-<hash>/w<rank>.sock is left behind. The shutdown owner that releases the control segments once every recorded child is gone now releases the deployment's worker sockets too, through the same WorkerSocketCleanup as the vLLM side, which removes a node only while it keeps the identity remembered at readiness and refuses a connect. That keeps the socket of a scheduler which still serves it until the next kill call, and never touches a same-name replacement. Each scheduler reports the socket paths its IPC listeners bound in its ready message, next to the pool names, and the DP controller forwards both the same way. The launching process remembers the reported nodes while the schedulers are alive and claims nothing outside the layout tp_ipc_util binds. A scheduler without a listener, TP=1, reports nothing. Fixes ovg-project#548 Signed-off-by: Rishabh Sinha <rsinha17@terpmail.umd.edu>
A scheduler killed with SIGKILL is reported as a zombie while the rest of its threads are still being torn down, and its IPC listener accepts connections until the last of them has released the socket. The owner's exit check passed on that report, the socket probe still saw a served socket, and the owner stayed queued for a kill call that the final teardown of launch_server.py never makes, so w<rank>.sock stayed behind on Linux with the spawn start method (review of PR ovg-project#553). The owner now retries the entries that are not done yet, at the exit check's interval and until its deadline, and the kill wrapper grants that deadline whenever no child is skipped: the upstream wait returns at the zombie report too. The identity and connect checks run on every attempt, a socket a live worker serves is still kept, and a skipped live child still keeps every file without a wait. Signed-off-by: Rishabh Sinha <rsinha17@terpmail.umd.edu>
74bda5f to
574dd7b
Compare
|
round 2 pushed, 574dd7b on #546 at 61b97e5. the owner now waits, bounded, for a killed scheduler to release its socket, and the schedulers report the sockets their listeners bound instead of the parent deriving them from the tp and pp sizes, which also retires the utility wrap since #546 binds the entry points itself. the linux spawn case is in the real-process file, 10 of 10 green at the head, details in the thread and the refreshed description. |
Summary
On SIGINT,
python -m sglang.launch_serverandsglang serveleave/tmp/kvcached-tp-<name>-<hash>/behind with the schedulers'w<rank>.sockentries, which RixinLiu filed as #548 after the 2x L4 runs at tp size 2. This is the SGLang half of the socket finding from the #482 validation that became #510 and #543 for vLLM. #546's shutdown owner, which releases the control segments once every recorded child process is gone, now releases the deployment's worker sockets as well, through theWorkerSocketCleanup#543 added for vLLM, and waits, bounded, for a killed scheduler to release a socket that is still open in it.Stacked on #546 at 61b97e5. Below it are ursasi's commits, the last two are this change, and the diff shrinks to those two once #546 merges. The branch is force-pushed on every re-stack.
Root cause
launch_server.py:86andcli/serve.py:207end with their ownkill_process_tree(os.getpid(), include_parent=False), bound fromsglang.srt.utilsinto their own namespace. It SIGKILLs every scheduler and waits until none holds resources. Exiting on its own, a scheduler runs the atexit hook that unlinks its socket, and that hook printed "IPC listener stopped" in the 0.5B runs of the issue. Killed before that hook, it exits with -9 and its socket node and directory stay. On SIGTERM the path sendsShutdownReqand waits before killing, and the hook usually runs there. #546 at 61b97e5 binds thelaunch_server,cli.serve,http_serverand__main__names itself. The SIGINT call reaches its owner there, and the utility wrap from the first round of this PR is gone.shipiyouniao's review of the first round found the second half. SGLang's own wait at 0.5.20 returns at the zombie report without reaping, and so does the owner's exit check. psutil reports a killed scheduler as a zombie while the rest of its threads are still being torn down, and its listener accepts connections until the last of them has released the socket. So the final kill call of
launch_server.pysaw a served socket, kept it, and left the owner queued for a call that never comes. Reproduced on Linux with the spawn start method, 2 failed / 2 passed twice on his side and 5 of 5 runs here.Changes
kvcached/integration/sglang/shutdown.py. Each scheduler's ready message now carries_kvcached_worker_sockets, the socket paths its IPC listeners bound (tp_ipc_util.worker_socket_paths()), next to the pool names, and the DP controller forwards both keys the same way._capture_ownerremembers the reported nodes while the schedulers are alive, as aWorkerSocketCleanup.for_pathsentry among the owner's segments, and claims nothing outside the layouttp_ipc_utilbinds,w<rank>.sockin akvcached-tp-*directory or one of itspp<k>stage directories. At TP=1 there is no listener and the scheduler does not report any. In the first round the parent derived the paths from the tp and pp sizes and the reported pool names, and that derivation is gone._ShutdownOwner.cleanupretries the entries that are not done yet, at the exit check's interval and until the same deadline, and the kill wrapper grants that deadline unless the call skips a child. Before, it granted it only whenwait_timeoutwas None, and the upstream wait with a timeout returns at the zombie report as well. Identity and connect checks run on every attempt. Sockets a live worker serves are still kept and the next kill call retries the owner, a skipped live child keeps every file without a wait, and the "Keeping worker socket" line can appear once or twice before the "Removed" line while a killed scheduler lets go.kvcached/utils.pyaddsWorkerSocketCleanup.for_paths(paths)andget_tp_socket_dir_of(path), the inverse ofget_tp_worker_socket_path, so the deployment root and a stage directory still go once empty. Nothing changes for the vLLM constructor.kvcached/tp_ipc_util.pyaddsworker_socket_paths()over the listener registry.Validation
tests/test_sglang_shutdown_sockets.py, 20 cases in the fake-module style oftest_sglang_shutdown.py, covering the readiness report from the listener registry and without the extension, the SIGINT sequence through thesglang.launch_serverbinding,Engine.shutdown, pp stage sockets, the self-exit path removing the sockets before the parent's own kill, a skipped live scheduler, a served socket kept in bounded time with the owner retried, a same-name replacement left alone served or dead, reported nodes absent at readiness never claimed, repeated kill calls leaving a replacement directory, an empty report, four reported paths outside the layout claiming nothing, the DP forwarding of both keys, the socket released after the exit report removed by the one final call, the identity check during the wait, and kvcached disabled. Of these, the released-after-exit case fails at 88be58f (the first commit) and passes at the head.tests/test_sglang_shutdown_sockets_process.py, 4 cases over spawn and fork, with real SIGKILLed children that serve the socket from a thread like the listener does, a kill that polls to the zombie report the way 0.5.20's wait does and never reaps, plus a survivor that keeps every socket until the next call. In a Linux container from thepython:3.11image (3.11.17, psutil 7.1.0, aarch64, kernel 6.8), 5 of 5 runs at 88be58f fail withKeeping worker socket ...: a worker still accepts connections on it, a spawn case in every run and a fork case once, and 10 of 10 runs at the head pass. macOS passes the 4 cases at the head as well.tests/test_tp_socket_cleanup.pygets two cases forfor_pathsand the inverse. Both new files are in the CPU manifest, following #546's move oftest_sglang_shutdown_process.pythere.#546's own files pass unchanged,
test_sglang_shutdown.pyandtest_sglang_shutdown_process.pyamong them. Full CPU manifest on macOS with Python 3.11, 1562 passed and 14 skipped, failure set byte-identical to #546's head at 61b97e5 with 1536 passed there, the known shm tracker, ipc timeout, listener device and pip target root cases. ruff 0.11.7, isort 6.0.1, codespell 2.4.1 clean. CI-exact mypy 1.11.1, 3.10 to 3.13, error set identical to #546's head onkvcachedandtests. Classification check valid.CPU only. No SGLang GPU run here. Still open is the issue's own protocol,
python -m sglang.launch_serverat tp size 2 on 0.5.20 with kvcached, serve a few requests, SIGINT the process group, wait for every process to exit, inspect/tmp/kvcached-tp-<name>-*, and the same for SIGTERM and forEngine.shutdown(). On main the directory stays for the models in the issue's table.Fixes #548