Skip to content

Generalize the MPI library into a comm library with NCCL/RCCL expansions - #2686

Open
tbennun wants to merge 17 commits into
mainfrom
comm-library
Open

tbennun wants to merge 17 commits into
mainfrom
comm-library

Conversation

@tbennun

@tbennun tbennun commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator

The communication library nodes were MPI-specific. They now live in dace.libraries.comm, and every node with an NCCL equivalent gains an NCCL implementation. The same implementation uses RCCL on the HIP backend, which exposes the identical nccl* API.

  • Send/Recv map to ncclSend/ncclRecv. NCCL has no tags, so messages to a peer match in order; strided 2-D buffers move row by row.
  • Isend/Irecv join one open NCCL group, which the next Wait or blocking operation launches. Wait/Waitall also synchronize the streams involved.
  • Bcast, Reduce, Allreduce and Allgather use the native collectives. Alltoall, Scatter and Gather go through dace_ccl.h helpers that call ncclAlltoAll/ncclGather/ncclScatter where available (NCCL >= 2.28; RCCL, which names the all-to-all ncclAllToAll before its 2.28 API) and fall back to grouped send/recv otherwise.
  • Communicators are bootstrapped over MPI: the world one at initialization, process-grid ones in the SDFG init code after the grid itself, because creating them lazily would deadlock point-to-point calls.
  • The state struct holds a pointer to the NCCL context, so the GPU translation unit needs only a forward declaration (dace_ccl_fwd.h) and no MPI headers.

library.comm.default_implementation (default MPI) selects the backend of portable nodes. Dummy, Redistribute, BlockScatter and BlockGather rely on MPI datatypes and stay MPI-only. Reduction operators accept MPI names or ReductionType names. dace.libraries.mpi remains as a deprecated alias, including its submodule paths and the environment names in saved SDFGs.

Communication nodes read host control values next to GPU buffers, which made schedule inference report a conflict, e.g. for Isend with its request array. LibraryNode.bridges_storage replaces the Copy/Fill special case in infer_types and now also covers communication nodes. The Python frontend also stops placing rank and tag scalars in the buffer's storage, where a GPU buffer made them unreadable from the host.

The communication library nodes were MPI-specific. They now live in
dace.libraries.comm, and every node with an NCCL equivalent gains an NCCL
implementation. The same implementation uses RCCL on the HIP backend, which
exposes the identical nccl* API.

- Send/Recv map to ncclSend/ncclRecv. NCCL has no tags, so messages to a
  peer match in order; strided 2-D buffers move row by row.
- Isend/Irecv join one open NCCL group, which the next Wait or blocking
  operation launches. Wait/Waitall also synchronize the streams involved.
- Bcast, Reduce, Allreduce and Allgather use the native collectives.
  Alltoall, Scatter and Gather go through dace_ccl.h helpers that call
  ncclAlltoAll/ncclGather/ncclScatter where available (NCCL >= 2.28;
  RCCL, which names the all-to-all ncclAllToAll before its 2.28 API) and
  fall back to grouped send/recv otherwise.
- Communicators are bootstrapped over MPI: the world one at
  initialization, process-grid ones in the SDFG init code after the grid
  itself, because creating them lazily would deadlock point-to-point calls.
- The state struct holds a pointer to the NCCL context, so the GPU
  translation unit needs only a forward declaration (dace_ccl_fwd.h) and
  no MPI headers.

library.comm.default_implementation (default MPI) selects the backend of
portable nodes. Dummy, Redistribute, BlockScatter and BlockGather rely on
MPI datatypes and stay MPI-only. Reduction operators accept MPI names or
ReductionType names. dace.libraries.mpi remains as a deprecated alias,
including its submodule paths and the environment names in saved SDFGs.

Communication nodes read host control values next to GPU buffers, which
made schedule inference report a conflict, e.g. for Isend with its request
array. LibraryNode.bridges_storage replaces the Copy/Fill special case in
infer_types and now also covers communication nodes. The Python frontend
also stops placing rank and tag scalars in the buffer's storage, where a
GPU buffer made them unreadable from the host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tbennun
tbennun requested a review from ThrudPrimrose October 13, 2026 05:33
tbennun and others added 16 commits October 12, 2026 22:35
…n nodes

A communication node communicates over the communicator given to its optional
_comm connector, an integer handle of an MPI_Comm or ncclComm_t, before its
process grid or the world. Applications that own their processes (and their
NCCL bootstrap) can then run DaCe programs on their own communicators.

- Every MPI expansion selects its communicator through mpi_communicator; the
  Allgather, Gather, Scatter, Send and Recv expansions no longer hardcode
  MPI_COMM_WORLD and so respect _grid like their NCCL counterparts.
- NCCL expansions derive from NcclExpansion. The NCCL environment no longer
  depends on MPI: the world communicator is created in the program's init code
  only when a node uses it, and only expansions that bootstrap over MPI add the
  MPI environment. The MPI environment defines DACE_WITH_MPI, which guards the
  MPI parts of dace_ccl.h.
- dace.libraries.comm.communicators: nccl_unique_id, nccl_init_rank and
  nccl_destroy (ctypes on the NCCL/RCCL library the environment links) and
  mpi_handle for mpi4py communicators.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g connectors

- ReduceScatter: MPI_Reduce_scatter_block / ncclReduceScatter.
- Alltoallv: per-process symbolic send and receive counts; MPI_Alltoallv, or
  grouped ncclSend/ncclRecv (dace::comm::AlltoAllv).
- The average ("avg") as a reduction operator: ncclAvg, or a sum divided by the
  number of processes in the MPI expansions of Allreduce, Reduce and
  ReduceScatter.
- Wait and Waitall accept ordering-only connectors (the buffer of a pending
  receive or send), which the expansions ignore, and write only the status
  outputs that are connected.
- NCCL has no requests: the _request connectors of the NCCL Isend, Irecv, Wait
  and Waitall expansions are optional, so point-to-point programs over a given
  communicator need no MPI types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NCCL operations have no requests. An integer container connected to _request
is set to zero, so it orders a nonblocking NCCL operation before its Wait
without MPI types; MPI_Request containers are still set to MPI_REQUEST_NULL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The expansions communicate a count of elements from a pointer. Simplification
may hand a collective a strided view (e.g., a transpose) in place of the
contiguous copy a frontend wrote, and a packed permutation is contiguous memory
but not in row-major order. pack_noncontiguous_buffers routes such buffers
through contiguous transients, with one transient for in-place buffers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI runs the MPI tests on two ranks. The subgroup of every rank but the last
has one member there, and send/recv runs over the world in reverse order, so
communicator ranks still differ from world ranks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The c_char array field stopped at the first zero byte of the binary ID. Tested
on a GPU: collectives and a self-exchange over an NCCL communicator created in
Python, with no MPI in the program.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rank 0 broadcasts its compilation error instead of a build folder, and the
final barrier becomes an exchange of load outcomes, so a failure no longer
leaves the other ranks waiting. A rank that holds an SDFG checks that it is
the one rank 0 compiled. Only Get_rank, bcast and allgather are used, so
non-mpi4py communicators work too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Allreduce, Allgather, ReduceScatter, Bcast, Alltoall and Alltoallv derive
from NonblockingCommNode, whose nonblocking property starts the operation
and writes a request that a Wait completes. MPI calls the MPI_I* variant
(Alltoallv keeps its count arrays alive until completion); NCCL launches the
operation right away and tracks its stream for the Wait, with a token
request. Nonblocking nodes need contiguous buffers (a packed copy would not
outlive the operation), and MPI cannot average without blocking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dace.libraries.comm.schedule.overlap_communication moves nonblocking starts
as early, and their waits as late, as data, symbol and view dependencies
allow within each scope; a wait inherits the buffers of the starts whose
requests it completes. Starts keep their order relative to other starts and
blocking operations (collectives start in the same order on every process),
and waits keep theirs. Every communication node then gets a state of its
own, which state fusion keeps (communication has side effects), so code
generation runs start, independent work and wait in this order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A start moves ahead of the work before it together with the nodes it
depends on, so that independent work that precedes its producers also runs
while the communication is in flight; a wait likewise moves behind the work
that does not depend on it. Relative orders within each group are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A collective runs asynchronously on the stream of its node already, so a
nonblocking one is launched right away like any other and needs no request,
stream tracking or Wait: code generation orders its readers on the device
and runs independent work on other streams. A Wait (and Waitall) connected
to GPU memory makes its stream wait for the pending point-to-point streams
through events, instead of synchronizing them on the host, which also keeps
code generation from forcing it onto the default stream. Isend accepts
ordering output connectors, so that a send can be ordered before its wait
through the sent buffer: a request or token in host memory makes the host
wait for the stream. The overlap pass leaves NCCL nodes in the states of
their neighbors, whose end would make the host wait for them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Packing copies into and out of contiguous buffers are ordered on the GPU
stream of the node, so stream-ordered (NCCL or RCCL) expansions pack the
strided buffers of a nonblocking node like those of any other; only MPI,
whose Wait completes the operation later, still requires contiguous buffers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The requirement that a nonblocking node have contiguous buffers holds for
MPI only, so it moves out of pack_noncontiguous_buffers (and its
stream_ordered escape hatch) into NonblockingCommNode.validate_nonblocking,
which the MPI expansions call before packing, together with the request
check. NCCL/RCCL expansions pack as for any node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main replaced all_states() with a recursive states() (#2652).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rotocol

distributed_compile now ends with an allgather of every rank's outcome and
broadcasts (folder, digest, error) rather than the folder alone. The fake
communicator gains allgather in place of the unused Barrier, and the test
compares against the broadcast folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant