[bugfix] fail fast when graphlearn sampler server dies#554
Conversation
e4b553f to
fb6edba
Compare
The GL negative-sampler server runs as a daemon process of LOCAL_RANK 0. graphlearn's launch_server start()s it but never join()s it and returns no handle, so if it is killed (most often an OOM kill when too many dataloader workers each buffer large sampled batches) every sampler client blocks forever on its RPC; the failure then surfaces only ~10min later as an opaque ALLTOALL collective (NCCL) timeout with no hint of the real cause. Inline the server launch so we keep the exact server process handle, and run a background watchdog on rank 0 that exits the rank immediately when the server dies abnormally, so torchrun tears the job down fast instead of hanging until the collective timeout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012uPyFcxcLAdNpgFFCYSZ2f
fb6edba to
ff958c2
Compare
| self.assertEqual(len(res["float_b"]), 8) | ||
| self.assertEqual(len(res["str_c"]), 8) | ||
|
|
||
| def test_server_liveness_watchdog_aborts_on_server_death(self): |
There was a problem hiding this comment.
The new test covers the firing path (abnormal death → os._exit(1)) well, but the disarm / false-positive-avoidance path is untested — i.e. the stop.is_set() or proc.exitcode == 0 branch in _watch, plus the atexit/__del__ disarm. That logic is what prevents a healthy job from being killed on a normal teardown, which is arguably the more dangerous regression: an inverted check (e.g. exitcode != 0) would still pass the whole suite.
Consider a fast, GL-free unit test that calls _watch's logic against a stub proc (is_alive()→False, exitcode==0) and asserts it returns without exiting, covering all three branches (alive / clean-exit / abnormal-exit) deterministically.
| # inline the launch so we keep the server handle for the watchdog. | ||
| proc = mp.Process( | ||
| target=_run_gl_server, | ||
| args=(self._g, self._cluster, int(os.environ.get("GROUP_RANK", 0))), | ||
| daemon=True, | ||
| ) | ||
| proc.start() | ||
| self._server_proc = proc | ||
| self._start_server_liveness_watchdog() |
There was a problem hiding this comment.
Inlining drops graphlearn's SERVER_LAUNCHED duplicate-launch guard (which raised on a second launch). It's latent today (one launch per process: train launches, eval reuses the cluster), but the guard removal turns a previously-idempotent call into a footgun — a second launch_server() would now start a second server Process + watchdog + atexit registration. Cheap insurance:
if self._server_proc is not None:
returnat the top of the LOCAL_RANK == 0 block.
| cluster: Dict[str, Union[int, str]], | ||
| task_index: int, | ||
| ) -> None: | ||
| """Run a graphlearn server until clients disconnect (inlined _server_manager).""" |
There was a problem hiding this comment.
Minor: "(inlined _server_manager)" implies a faithful copy, but this intentionally drops the counts_dict.update(graph.server_get_stats()) bookkeeping (tzrec never reads the stats). Worth noting the simplification so a future reader doesn't "restore" gl.get_counts() — e.g. "simplified from graphlearn's _server_manager; the server-stats return value is intentionally discarded."
Review summarySolid, well-targeted fix. The watchdog design is sound and I verified the trickiest parts hold up:
Noteworthy feedback left inline (none blocking):
I dismissed a few flagged items as non-issues after checking them: the |
Brings in upstream alibaba#551 (v1.3.0: torch 2.12.1 / torchrec 1.7.0 / fbgemm 1.7.0 / numpy 2 / cu130 TRT), alibaba#556 (pandas>=3), alibaba#554 (graphlearn sampler fail-fast), alibaba#552 (SID RQ-VAE/RQ-KMeans docs), alibaba#553 (CI runtime cuts). No conflicts — local work is confined to the SID quantizer subsystem; upstream touched requirements, docs, CI, and framework files that don't overlap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problem
The graphlearn negative-sampler server runs as a daemon process of
LOCAL_RANK 0. graphlearn'slaunch_serverstart()s it but neverjoin()s it and returns no handle, so when the server dies — most often an OOM kill when many dataloader workers each buffer large sampled batches, but also any crash — every sampler RPC client blocks forever (graphlearn retries the dead connection rather than erroring). The failure then surfaces only ~10 min later as an opaqueALLTOALLcollective (NCCL) timeout that gives no hint of the real cause, and the dead server lingers as a<defunct>process.Fix
Inline graphlearn's server launch (it uses only public
Graph.init/server_get_stats/close, and tzrec never reads graphlearn'sSTATS_DICT/SERVER_LAUNCHED/stats) so we hold the exact serverProcesshandle. A background daemon thread on rank 0 then watches that process and, on an abnormal exit, logs the cause andos._exit(1)s so torchrun tears the job down in seconds instead of hanging until the collective timeout.A thread is needed because rank 0's main thread is blocked inside the dataloader fetch when the server dies and never returns to self-check (SIGCHLD was avoided since PyTorch's DataLoader already installs its own SIGCHLD handler). A
threading.Eventset from anatexithook disarms the watchdog before normal interpreter shutdown SIGTERMs the daemon server, so a clean teardown is never mistaken for a crash (no false positives during healthy training).Verification
test_server_liveness_watchdog_aborts_on_server_deathlaunches a real GL server, kills it, and asserts the watchdog aborts the rank; it passes alongside the existingtest_negative_sampler(which confirms the inlined launch still serves correctly).kill -9of the server triggered the watchdog in ~10 s and the job was torn down in ~15 s (vs a ~600 s NCCL-timeout hang previously), with rank 0 reported as the root cause.Note
This makes the failure loud and fast; it does not prevent the OOM itself. The operational mitigation when keeping a large negative-sample count is to lower
data_config.num_workers, which bounds the number of worker processes buffering large sampled batches.Also bumps the package version to 1.2.23.
🤖 Generated with Claude Code
https://claude.ai/code/session_012uPyFcxcLAdNpgFFCYSZ2f