From 5f913c1135b14efec9b51e3e45fca432421847bd Mon Sep 17 00:00:00 2001 From: Lijuan Tang Date: Wed, 10 Jun 2026 17:43:27 -0700 Subject: [PATCH] [Fix] Emulate PDEATHSIG on macOS to prevent orphaned worker processes (#27190) Co-authored-by: Claude Opus 4.8 (1M context) --- .../hardware_backend/mlx/parent_watchdog.py | 60 +++++++++++++++++++ python/sglang/srt/utils/common.py | 12 +++- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 python/sglang/srt/hardware_backend/mlx/parent_watchdog.py diff --git a/python/sglang/srt/hardware_backend/mlx/parent_watchdog.py b/python/sglang/srt/hardware_backend/mlx/parent_watchdog.py new file mode 100644 index 000000000..dcc81e01e --- /dev/null +++ b/python/sglang/srt/hardware_backend/mlx/parent_watchdog.py @@ -0,0 +1,60 @@ +"""Parent-death watchdog for MLX workers on Apple Silicon. + +macOS has no ``PR_SET_PDEATHSIG`` equivalent, so the kernel will not signal a +worker process when its parent dies; the worker would be reparented to PID 1 +and leak (holding GPU/host memory and ports). This module emulates PDEATHSIG +with a daemon thread that watches the parent PID via kqueue and SIGKILLs the +current process once it gets orphaned. +""" + +import os +import select +import signal +import threading + + +def start_parent_death_watcher() -> None: + """SIGKILL this process once its current parent exits (macOS only). + + kqueue with an ``EVFILT_PROC`` / ``NOTE_EXIT`` filter is the native, + event-driven mechanism on macOS (exposed via ``select.kqueue`` / + ``select.kevent``), so the watcher thread blocks until the parent actually + exits instead of waking up to poll. + + ``SIGKILL`` is sent from this watcher thread and is uncatchable / + unblockable, so it works even when the main thread is stuck inside a + blocking native call (e.g. an MLX/Metal ``mx.eval`` / ``.tolist()``). + """ + original_ppid = os.getppid() + + def _watch_parent(): + kq = select.kqueue() + kev = select.kevent( + original_ppid, + filter=select.KQ_FILTER_PROC, + flags=select.KQ_EV_ADD, + fflags=select.KQ_NOTE_EXIT, + ) + try: + # Register the EVFILT_PROC / NOTE_EXIT watch on the parent PID. + kq.control([kev], 0, None) + except (ProcessLookupError, OSError): + # The parent already exited before we could register the watch + # (ESRCH); we are already orphaned. + os.kill(os.getpid(), signal.SIGKILL) + return + # Guard against the race where the parent exits between reading + # original_ppid and registering the watch above. + if os.getppid() != original_ppid: + os.kill(os.getpid(), signal.SIGKILL) + return + # Block until the parent exits, then terminate ourselves. + kq.control(None, 1, None) + os.kill(os.getpid(), signal.SIGKILL) + + watcher = threading.Thread( + target=_watch_parent, + name="parent-death-watcher", + daemon=True, + ) + watcher.start() diff --git a/python/sglang/srt/utils/common.py b/python/sglang/srt/utils/common.py index d2650cbb8..bebf16672 100644 --- a/python/sglang/srt/utils/common.py +++ b/python/sglang/srt/utils/common.py @@ -2523,8 +2523,18 @@ def kill_itself_when_parent_died(): PR_SET_PDEATHSIG = 1 libc = ctypes.CDLL("libc.so.6") libc.prctl(PR_SET_PDEATHSIG, signal.SIGKILL) + elif sys.platform == "darwin": + # macOS has no PR_SET_PDEATHSIG equivalent; the MLX backend provides a + # kqueue-based watchdog that SIGKILLs this worker once it is orphaned. + from sglang.srt.hardware_backend.mlx.parent_watchdog import ( + start_parent_death_watcher, + ) + + start_parent_death_watcher() else: - logger.warning("kill_itself_when_parent_died is only supported in linux.") + logger.warning( + "kill_itself_when_parent_died is only supported on linux and macOS." + ) class UvicornAccessLogFilter(logging.Filter):