Coverage for pySDC/helpers/NCCL_communicator.py: 90%
97 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-25 20:28 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-25 20:28 +0000
1from mpi4py import MPI
2from cupy.cuda import nccl
3import cupy as cp
4import numpy as np
7class NCCLComm(object):
8 """
9 Wraps an MPI communicator and performs some calls to NCCL functions instead.
10 """
12 #: One NCCL communicator per MPI communicator, keyed by the MPI handle.
13 #:
14 #: Creating one costs about 17 MB of device memory that is never released: NCCL has no
15 #: reference counting, and destroying a communicator is itself collective, so a `__del__`
16 #: would have ranks tearing them down whenever their garbage collectors happened to run --
17 #: in different orders, which deadlocks. Making them once and sharing them avoids both.
18 #:
19 #: The MPI communicator is kept alongside so it cannot be freed and have its handle reused
20 #: for a different one, which would hand out the wrong NCCL communicator.
21 _communicators = {}
23 def __init__(self, comm):
24 """
25 Args:
26 comm (mpi4py.Intracomm): MPI communicator
27 """
28 self.commMPI = comm
30 # `py2f` rather than the communicator itself: mpi4py defines `__eq__` without `__hash__`,
31 # so a communicator cannot be a dictionary key, and the handle is the same for any two
32 # Python wrappers around one communicator.
33 key = comm.py2f()
34 if key not in NCCLComm._communicators:
35 uid = comm.bcast(nccl.get_unique_id(), root=0)
36 NCCLComm._communicators[key] = (comm, nccl.NcclCommunicator(comm.size, uid, comm.rank))
38 self.commNCCL = NCCLComm._communicators[key][1]
40 def __getattr__(self, name):
41 """
42 Pass calls that are not explicitly overridden by NCCL functionality on to the MPI communicator.
43 When performing any operations that depend on data, we have to synchronize host and device beforehand.
45 Args:
46 Name (str): Name of the requested attribute
47 """
48 if name not in ['size', 'rank', 'Get_rank', 'Get_size', 'Split', 'Create_cart', 'Is_inter', 'Get_topology']:
49 cp.cuda.get_current_stream().synchronize()
51 return getattr(self.commMPI, name)
53 @staticmethod
54 def get_dtype(data):
55 """
56 As NCCL doesn't support complex numbers, we have to act as if we're sending two real numbers if using complex.
57 """
58 dtype = data.dtype
59 if dtype in [np.dtype('float32'), np.dtype('complex64')]:
60 return nccl.NCCL_FLOAT32
61 elif dtype in [np.dtype('float64'), np.dtype('complex128')]:
62 return nccl.NCCL_FLOAT64
63 elif dtype in [np.dtype('int32')]:
64 return nccl.NCCL_INT32
65 elif dtype in [np.dtype('int64')]:
66 return nccl.NCCL_INT64
67 else:
68 raise NotImplementedError(f'Don\'t know what NCCL dtype to use to send data of dtype {data.dtype}!')
70 @staticmethod
71 def get_count(data):
72 """
73 As NCCL doesn't support complex numbers, we have to act as if we're sending two real numbers if using complex.
74 """
75 if cp.iscomplexobj(data):
76 return data.size * 2
77 else:
78 return data.size
80 def get_op(self, MPI_op):
81 if MPI_op == MPI.SUM:
82 return nccl.NCCL_SUM
83 elif MPI_op == MPI.PROD:
84 return nccl.NCCL_PROD
85 elif MPI_op == MPI.MAX:
86 return nccl.NCCL_MAX
87 elif MPI_op == MPI.MIN:
88 return nccl.NCCL_MIN
89 else:
90 raise NotImplementedError('Don\'t know what NCCL operation to use to replace this MPI operation!')
92 def reduce(self, sendobj, op=MPI.SUM, root=0):
93 sync = False
94 if hasattr(sendobj, 'data'):
95 if hasattr(sendobj.data, 'ptr'):
96 sync = True
97 if sync:
98 cp.cuda.Device().synchronize()
100 return self.commMPI.reduce(sendobj, op=op, root=root)
102 def allreduce(self, sendobj, op=MPI.SUM):
103 sync = False
104 if hasattr(sendobj, 'data'):
105 if hasattr(sendobj.data, 'ptr'):
106 sync = True
107 if sync:
108 cp.cuda.Device().synchronize()
110 return self.commMPI.allreduce(sendobj, op=op)
112 def Reduce(self, sendbuf, recvbuf, op=MPI.SUM, root=0):
113 if not hasattr(sendbuf.data, 'ptr'):
114 return self.commMPI.Reduce(sendbuf=sendbuf, recvbuf=recvbuf, op=op, root=root)
116 dtype = self.get_dtype(sendbuf)
117 count = self.get_count(sendbuf)
118 op = self.get_op(op)
119 recvbuf = cp.empty(1) if recvbuf is None else recvbuf
120 stream = cp.cuda.get_current_stream()
122 self.commNCCL.reduce(
123 sendbuf=sendbuf.data.ptr,
124 recvbuf=recvbuf.data.ptr,
125 count=count,
126 datatype=dtype,
127 op=op,
128 root=root,
129 stream=stream.ptr,
130 )
132 def Allreduce(self, sendbuf, recvbuf, op=MPI.SUM):
133 if not hasattr(sendbuf.data, 'ptr'):
134 return self.commMPI.Allreduce(sendbuf=sendbuf, recvbuf=recvbuf, op=op)
136 dtype = self.get_dtype(sendbuf)
137 count = self.get_count(sendbuf)
138 op = self.get_op(op)
139 stream = cp.cuda.get_current_stream()
141 self.commNCCL.allReduce(
142 sendbuf=sendbuf.data.ptr, recvbuf=recvbuf.data.ptr, count=count, datatype=dtype, op=op, stream=stream.ptr
143 )
145 def Bcast(self, buf, root=0):
146 if not hasattr(buf.data, 'ptr'):
147 return self.commMPI.Bcast(buf=buf, root=root)
149 dtype = self.get_dtype(buf)
150 count = self.get_count(buf)
151 stream = cp.cuda.get_current_stream()
153 self.commNCCL.bcast(buff=buf.data.ptr, count=count, datatype=dtype, root=root, stream=stream.ptr)
155 def Send(self, buf, dest, tag=0):
156 """
157 Send a buffer to another rank through NCCL.
159 NCCL has no tags: a send is matched to whichever receive the destination posts next for
160 this pair of ranks. That is fine when one message is in flight at a time, and wrong when
161 several are, which is why the non-blocking `Issend`/`Irecv` that pySDC's time-parallel
162 controller uses are left to fall through to MPI, where tags mean what they say.
163 """
164 if not hasattr(buf.data, 'ptr'):
165 return self.commMPI.Send(buf, dest=dest, tag=tag)
167 stream = cp.cuda.get_current_stream()
168 self.commNCCL.send(buf.data.ptr, self.get_count(buf), self.get_dtype(buf), dest, stream.ptr)
169 stream.synchronize()
171 def Recv(self, buf, source, tag=0):
172 """
173 Receive a buffer from another rank through NCCL. See `Send` on the absence of tags.
174 """
175 if not hasattr(buf.data, 'ptr'):
176 return self.commMPI.Recv(buf, source=source, tag=tag)
178 stream = cp.cuda.get_current_stream()
179 self.commNCCL.recv(buf.data.ptr, self.get_count(buf), self.get_dtype(buf), source, stream.ptr)
180 stream.synchronize()
182 def Barrier(self):
183 cp.cuda.get_current_stream().synchronize()
184 self.commMPI.Barrier()