From 45715e7f20629d1813ff8f543a23bb437128db2e Mon Sep 17 00:00:00 2001 From: Hanming Lu <69857889+hanming-lu@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:10:48 -0700 Subject: [PATCH] [Fix] Disable NCCL graph buffer registration for the TP LM-head all-to-all (pure-DP decode hang under request bursts) (#38936) --- python/sglang/srt/arg_groups/parallel_hook.py | 28 ++++++++++ .../unit/server_args/test_server_args.py | 54 +++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/python/sglang/srt/arg_groups/parallel_hook.py b/python/sglang/srt/arg_groups/parallel_hook.py index ad80ba978..286a0790d 100644 --- a/python/sglang/srt/arg_groups/parallel_hook.py +++ b/python/sglang/srt/arg_groups/parallel_hook.py @@ -226,6 +226,34 @@ def handle_data_parallelism(server_args: Any): run_post_process_pass(server_args, _tp_lm_head_all_to_all_default) run_post_process_pass(server_args, _dp_lm_head_validation) + if resolving_view(server_args).enable_tp_lm_head_all_to_all: + _disable_nccl_graph_buffer_registration() + + +def _disable_nccl_graph_buffer_registration() -> None: + """Keep NCCL from registering the buffers of the graph-captured PyNccl + all-to-all. + + NCCL_GRAPH_REGISTER (default on) registers the send/recv buffers of every + collective captured in a CUDA graph for the lifetime of the graph, and + peers then move data through those registrations directly. The TP LM-head + all-to-all is captured in the decode graphs on graph-pool temporaries, + whose addresses the pool also hands to other tensors, and the registered + exchange does not survive that: under a burst of new requests (DP ranks + ramping at different rates) one rank finishes its step while the others + spin in ncclDevKernel_SendRecv forever, and every DP rank hangs. + Reproduced on tp4/dp4/ep4 and on a multi-node tp16/dp16/ep16 PD decode + deployment; disabling the registration removes the hang while dedicated + all-to-all buffers alone do not. Must run before the schedulers create + their NCCL communicators, which inherit this environment. An explicit + setting wins. + """ + if os.environ.setdefault("NCCL_GRAPH_REGISTER", "0") != "0": + logger.warning( + "NCCL_GRAPH_REGISTER=%s was set explicitly; the graph-captured TP " + "LM-head all-to-all can deadlock with registered buffers.", + os.environ["NCCL_GRAPH_REGISTER"], + ) def handle_dwdp(server_args: Any): diff --git a/test/registered/unit/server_args/test_server_args.py b/test/registered/unit/server_args/test_server_args.py index d4f23657c..7cef34025 100644 --- a/test/registered/unit/server_args/test_server_args.py +++ b/test/registered/unit/server_args/test_server_args.py @@ -3353,5 +3353,59 @@ class TestNoneMeansUnset(CustomTestCase): self.assertIsNotNone(server_args.mamba_full_memory_ratio) +class TestTpLmHeadAllToAllNcclGraphRegister(unittest.TestCase): + """The graph-captured TP LM-head all-to-all must not run with NCCL's + graph buffer registration: registered graph-pool temporaries deadlock the + exchange under DP-rank ramps.""" + + def _resolve(self, **kwargs): + # The handler reads the DP-adjusted prefill knobs the pipeline would + # have settled by then; the dummy-model pipeline itself returns early. + server_args = ServerArgs( + model_path="dummy", + enable_dp_attention=True, + tp_size=2, + dp_size=2, + chunked_prefill_size=8192, + cuda_graph_config=CudaGraphConfig( + prefill=PhaseConfig(backend=Backend.DISABLED) + ), + **kwargs, + ) + parallel_hook.handle_data_parallelism(server_args) + return server_args + + def test_pure_dp_decode_node_disables_nccl_graph_register(self): + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("NCCL_GRAPH_REGISTER", None) + server_args = self._resolve(disaggregation_mode="decode") + self.assertTrue( + resolution_result(server_args, "enable_tp_lm_head_all_to_all") + ) + self.assertEqual(os.environ.get("NCCL_GRAPH_REGISTER"), "0") + + def test_explicit_nccl_graph_register_is_kept(self): + with patch.dict(os.environ, {"NCCL_GRAPH_REGISTER": "1"}, clear=False): + with self.assertLogs(parallel_hook.logger, level="WARNING") as logs: + self._resolve(disaggregation_mode="decode") + self.assertEqual(os.environ["NCCL_GRAPH_REGISTER"], "1") + self.assertIn("NCCL_GRAPH_REGISTER=1", "\n".join(logs.output)) + + def test_without_all_to_all_env_is_untouched(self): + # Unified serving keeps the all-to-all off by default, and a decode + # node with the DP LM head never takes the all-to-all. + for kwargs in ( + {}, + {"disaggregation_mode": "decode", "enable_dp_lm_head": True}, + ): + with self.subTest(**kwargs), patch.dict(os.environ, {}, clear=False): + os.environ.pop("NCCL_GRAPH_REGISTER", None) + server_args = self._resolve(**kwargs) + self.assertFalse( + resolution_result(server_args, "enable_tp_lm_head_all_to_all") + ) + self.assertNotIn("NCCL_GRAPH_REGISTER", os.environ) + + if __name__ == "__main__": unittest.main()