mirror of
https://github.com/deepseek-ai/DeepEP.git
synced 2026-11-05 00:47:32 +00:00
[GH-ISSUE #726] get_nccl_comm_handle reuses a null _comm_ptr() when the group's NCCL comm is lazily uninitialized — ncclTeamWorld null-deref at ElasticBuffer construction (vLLM serve, 16/16 ranks) #413
Labels
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
deepseek-ai/DeepEP#413
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @dmvevents on GitHub (Aug 14, 2026).
Original GitHub issue: https://github.com/deepseek-ai/DeepEP/issues/726
Summary
get_nccl_comm_handle(deep_ep/utils/comm.py) reuses PyTorch's NCCL communicator whenEP_REUSE_NCCL_COMM=1(the default since #637):backend._comm_ptr()is a passive accessor: PyTorch creates NCCL communicators lazily, andProcessGroupNCCL::getCommPtr()returns 0 when the group+device has no communicator yet ("ncclComm is a nullptr if the communicator does not exist",ProcessGroupNCCL.cpp, torch 2.11). Comms materialize at the first collective on that group+device — or eagerly only wheninit_process_group(device_id=...)was used.So any consumer that constructs an
ElasticBufferbefore any collective has run on the EP group getsNCCLCommHandle(0), and the constructor immediately callscalculate_elastic_buffer_size(handle.get(), ...)→deep_ep::nccl::get_physical_domain_size(csrc/kernels/backend/nccl.cu) →ncclTeamWorld(comm)which dereferencescomm->nRanks(NCCLsrc/nccl_device/core.cc) → segfault.Live reproduction (not hypothetical)
vLLM expert-parallel serving (Qwen3-30B-A3B-FP8, DP16/EP16, 2× p5en over EFA) hits this deterministically on all 16 ranks at serve init:
vLLM's default path is exactly the cold-group shape:
init_process_groupwithoutdevice_id(all groups lazy), TP=1 (no TP collectives), DP-sync NCCL disabled under async scheduling, gloo-only barriers — so the EPdevice_grouphas had zero collectives whenElasticBufferis constructed fromprocess_weights_after_loading._comm_ptr()returns 0 and every rank crashes identically.Setting
EP_REUSE_NCCL_COMM=0(create-own-comm path) fixes the serve on the same substrate — confirmed E2E with sustained load (c=1→64 sweep, 0 errors).Why DeepEP's own tests don't catch it
tests/utilsinit_distpassesdevice_id=torch.device(f'cuda:{local_rank}')toinit_process_group, which makes torch eager-create communicators —_comm_ptr()is always valid in the test suite. The reuse branch's null case is only reachable from lazy-init consumers.Proposed fix
Only take the reuse branch when the pointer is non-null; otherwise fall through to the existing create-own-comm path (whose
all_gather_objectis itself a collective, so it warms torch's comm as a side effect and all ranks take the same branch on a cold boot):PR incoming with this guard.
@dmvevents commented on GitHub (Oct 27, 2026):
Status on
main@f6aa38f: this still applies after the V2.5 restructure (#763).get_nccl_comm_handlemoved fromdeep_ep/utils/comm.pytodeep_ep/comm/handle.pywith the reuse branch unchanged, so a nullbackend._comm_ptr()is still cached and returned:github.com/deepseek-ai/DeepEP@f6aa38fe06/deep_ep/comm/handle.py (L63-L66)The crash path is the same under the new layout.
EPBuffer.__init__takes the handle (deep_ep/buffers/ep.py:299), and the buffer setup callscomm::get_physical_domain_size(csrc/buffers/ep.hpp:263), which evaluatesncclTeamWorld(comm).nRankswithout a null check (csrc/kernels/comm/context.cpp:52-56). Theget_physical_domain_sizeandget_logical_domain_sizehelpers inhandle.pypass the handle through the same way.#727 targets the removed
deep_ep/utils/comm.py, which is why it now conflicts. #767 applies the same group-uniform guard todeep_ep/comm/handle.py.Either one removes the null dereference. One caveat that applies to both: the guard adds an
all_gather_objecton the group inside theEP_REUSE_NCCL_COMMblock, so every rank in the group has to reach it. On a two-node EP16 build of ours that carried #727's change, buffer construction once hung with one node's ranks waiting insideget_nccl_comm_handleand the other node's ranks past it;EP_REUSE_NCCL_COMM=0, which skips the block, avoided it, and we have not root-caused it (it may have been our build rather than the guard). A two-node run on the chosen PR's head before merging would settle that.Edited 2026-10-27: added the two-node caveat and removed the closing line about #727.