[Fix] Disable NCCL graph buffer registration for the TP LM-head all-to-all (pure-DP decode hang under request bursts) (#38936)
This commit is contained in:
@@ -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, _tp_lm_head_all_to_all_default)
|
||||||
run_post_process_pass(server_args, _dp_lm_head_validation)
|
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):
|
def handle_dwdp(server_args: Any):
|
||||||
|
|||||||
@@ -3353,5 +3353,59 @@ class TestNoneMeansUnset(CustomTestCase):
|
|||||||
self.assertIsNotNone(server_args.mamba_full_memory_ratio)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user