2 * This file is part of the GROMACS molecular simulation package.
4 * Copyright (c) 2019,2020, by the GROMACS development team, led by
5 * Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
6 * and including many others, as listed in the AUTHORS file in the
7 * top-level source directory and at http://www.gromacs.org.
9 * GROMACS is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU Lesser General Public License
11 * as published by the Free Software Foundation; either version 2.1
12 * of the License, or (at your option) any later version.
14 * GROMACS is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 * Lesser General Public License for more details.
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with GROMACS; if not, see
21 * http://www.gnu.org/licenses, or write to the Free Software Foundation,
22 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 * If you want to redistribute modifications to GROMACS, please
25 * consider that scientific software is very special. Version
26 * control is crucial - bugs must be traceable. We will be happy to
27 * consider code for inclusion in the official distribution, but
28 * derived work must not be called official GROMACS. Details are found
29 * in the README & COPYING files - if they are missing, get the
30 * official version at http://www.gromacs.org.
32 * To help us fund GROMACS development, we humbly ask that you cite
33 * the research papers on the package. Check out http://www.gromacs.org.
37 * \brief Implements GPU halo exchange using CUDA.
40 * \author Alan Gray <alang@nvidia.com>
42 * \ingroup module_domdec
46 #include "gpuhaloexchange_impl.cuh"
55 #include "gromacs/domdec/domdec.h"
56 #include "gromacs/domdec/domdec_struct.h"
57 #include "gromacs/domdec/gpuhaloexchange.h"
58 #include "gromacs/gpu_utils/cudautils.cuh"
59 #include "gromacs/gpu_utils/device_context.h"
60 #include "gromacs/gpu_utils/devicebuffer.h"
61 #include "gromacs/gpu_utils/gpueventsynchronizer.cuh"
62 #include "gromacs/gpu_utils/typecasts.cuh"
63 #include "gromacs/gpu_utils/vectype_ops.cuh"
64 #include "gromacs/math/vectypes.h"
65 #include "gromacs/pbcutil/ishift.h"
66 #include "gromacs/timing/wallcycle.h"
67 #include "gromacs/utility/gmxmpi.h"
69 #include "domdec_internal.h"
74 //! Number of CUDA threads in a block
75 // TODO Optimize this through experimentation
76 constexpr static int c_threadsPerBlock = 256;
79 __global__ void packSendBufKernel(float3* __restrict__ dataPacked,
80 const float3* __restrict__ data,
81 const int* __restrict__ map,
83 const float3 coordinateShift)
85 int threadIndex = blockIdx.x * blockDim.x + threadIdx.x;
86 float3* gm_dataDest = &dataPacked[threadIndex];
87 const float3* gm_dataSrc = &data[map[threadIndex]];
89 if (threadIndex < mapSize)
93 *gm_dataDest = *gm_dataSrc + coordinateShift;
97 *gm_dataDest = *gm_dataSrc;
104 /*! \brief unpack non-local force data buffer on the GPU using pre-populated "map" containing index
105 * information \param[out] data full array of force values \param[in] dataPacked packed
106 * array of force values to be transferred \param[in] map array of indices defining mapping
107 * from full to packed array \param[in] mapSize number of elements in map array
109 template<bool accumulate>
110 __global__ void unpackRecvBufKernel(float3* __restrict__ data,
111 const float3* __restrict__ dataPacked,
112 const int* __restrict__ map,
116 int threadIndex = blockIdx.x * blockDim.x + threadIdx.x;
117 const float3* gm_dataSrc = &dataPacked[threadIndex];
118 float3* gm_dataDest = &data[map[threadIndex]];
120 if (threadIndex < mapSize)
124 *gm_dataDest += *gm_dataSrc;
128 *gm_dataDest = *gm_dataSrc;
135 void GpuHaloExchange::Impl::reinitHalo(float3* d_coordinatesBuffer, float3* d_forcesBuffer)
137 wallcycle_start(wcycle_, ewcDOMDEC);
138 wallcycle_sub_start(wcycle_, ewcsDD_GPU);
140 d_x_ = d_coordinatesBuffer;
141 d_f_ = d_forcesBuffer;
143 const gmx_domdec_comm_t& comm = *dd_->comm;
144 const gmx_domdec_comm_dim_t& cd = comm.cd[dimIndex_];
145 const gmx_domdec_ind_t& ind = cd.ind[pulse_];
147 numHomeAtoms_ = comm.atomRanges.numHomeAtoms(); // offset for data recieved by this rank
149 // Determine receive offset for the dimension index and pulse of this halo exchange object
152 int numAtomsTotal = numHomeAtoms_;
153 for (int i = 0; i <= dimIndex_; i++)
155 int pulseMax = (i == dimIndex_) ? pulse_ : (comm.cd[i].numPulses() - 1);
156 for (int p = 0; p <= pulseMax; p++)
158 atomOffset_ = numAtomsTotal;
159 const gmx_domdec_ind_t& indTemp = comm.cd[i].ind[p];
160 numAtomsTotal += indTemp.nrecv[numZoneTemp + 1];
162 numZone = numZoneTemp;
163 numZoneTemp += numZoneTemp;
166 int newSize = ind.nsend[numZone + 1];
168 GMX_ASSERT(cd.receiveInPlace, "Out-of-place receive is not yet supported in GPU halo exchange");
170 // reallocates only if needed
171 h_indexMap_.resize(newSize);
172 // reallocate on device only if needed
173 if (newSize > maxPackedBufferSize_)
175 reallocateDeviceBuffer(&d_indexMap_, newSize, &indexMapSize_, &indexMapSizeAlloc_, deviceContext_);
176 reallocateDeviceBuffer(&d_sendBuf_, newSize, &sendBufSize_, &sendBufSizeAlloc_, deviceContext_);
177 reallocateDeviceBuffer(&d_recvBuf_, newSize, &recvBufSize_, &recvBufSizeAlloc_, deviceContext_);
178 maxPackedBufferSize_ = newSize;
181 xSendSize_ = newSize;
183 MPI_Sendrecv(&xSendSize_,
196 fSendSize_ = xRecvSize_;
197 fRecvSize_ = xSendSize_;
201 GMX_ASSERT(ind.index.size() == h_indexMap_.size(),
202 "Size mismatch between domain decomposition communication index array and GPU "
203 "halo exchange index mapping array");
204 std::copy(ind.index.begin(), ind.index.end(), h_indexMap_.begin());
207 &d_indexMap_, h_indexMap_.data(), 0, newSize, nonLocalStream_, GpuApiCallBehavior::Async, nullptr);
209 // This rank will push data to its neighbor, so needs to know
210 // the remote receive address and similarly send its receive
211 // address to other neighbour. We can do this here in reinit fn
212 // since the pointers will not change until the next NS step.
214 // Coordinates buffer:
215 void* recvPtr = static_cast<void*>(&d_x_[atomOffset_]);
217 MPI_Sendrecv(&recvPtr,
231 recvPtr = static_cast<void*>(d_recvBuf_);
232 MPI_Sendrecv(&recvPtr,
246 wallcycle_sub_stop(wcycle_, ewcsDD_GPU);
247 wallcycle_stop(wcycle_, ewcDOMDEC);
252 void GpuHaloExchange::Impl::communicateHaloCoordinates(const matrix box,
253 GpuEventSynchronizer* coordinatesReadyOnDeviceEvent)
256 wallcycle_start(wcycle_, ewcLAUNCH_GPU);
259 // ensure stream waits until coordinate data is available on device
260 coordinatesReadyOnDeviceEvent->enqueueWaitEvent(nonLocalStream_);
263 wallcycle_sub_start(wcycle_, ewcsLAUNCH_GPU_MOVEX);
265 // launch kernel to pack send buffer
266 KernelLaunchConfig config;
267 config.blockSize[0] = c_threadsPerBlock;
268 config.blockSize[1] = 1;
269 config.blockSize[2] = 1;
270 config.gridSize[0] = (xSendSize_ + c_threadsPerBlock - 1) / c_threadsPerBlock;
271 config.gridSize[1] = 1;
272 config.gridSize[2] = 1;
273 config.sharedMemorySize = 0;
275 const float3* sendBuf = d_sendBuf_;
276 const float3* d_x = d_x_;
277 const int* indexMap = d_indexMap_;
278 const int size = xSendSize_;
279 // The coordinateShift changes between steps when we have
280 // performed a DD partition, or have updated the box e.g. when
281 // performing pressure coupling. So, for simplicity, the box
282 // is used every step to pass the shift vector as an argument of
283 // the packing kernel.
284 const int boxDimensionIndex = dd_->dim[dimIndex_];
285 const float3 coordinateShift{ box[boxDimensionIndex][XX],
286 box[boxDimensionIndex][YY],
287 box[boxDimensionIndex][ZZ] };
289 // Avoid launching kernel when there is no work to do
292 auto kernelFn = usePBC_ ? packSendBufKernel<true> : packSendBufKernel<false>;
294 const auto kernelArgs = prepareGpuKernelArguments(
295 kernelFn, config, &sendBuf, &d_x, &indexMap, &size, &coordinateShift);
298 kernelFn, config, nonLocalStream_, nullptr, "Domdec GPU Apply X Halo Exchange", kernelArgs);
301 wallcycle_sub_stop(wcycle_, ewcsLAUNCH_GPU_MOVEX);
302 wallcycle_stop(wcycle_, ewcLAUNCH_GPU);
304 // Consider time spent in communicateHaloData as Comm.X counter
305 // ToDo: We need further refinement here as communicateHaloData includes launch time for cudamemcpyasync
306 wallcycle_start(wcycle_, ewcMOVEX);
308 communicateHaloData(d_x_, HaloQuantity::HaloCoordinates, coordinatesReadyOnDeviceEvent);
310 wallcycle_stop(wcycle_, ewcMOVEX);
315 // The following method should be called after non-local buffer operations,
316 // and before the local buffer operations. It operates in the non-local stream.
317 void GpuHaloExchange::Impl::communicateHaloForces(bool accumulateForces)
319 // Consider time spent in communicateHaloData as Comm.F counter
320 // ToDo: We need further refinement here as communicateHaloData includes launch time for cudamemcpyasync
321 wallcycle_start(wcycle_, ewcMOVEF);
323 // Communicate halo data (in non-local stream)
324 communicateHaloData(d_f_, HaloQuantity::HaloForces, nullptr);
326 wallcycle_stop(wcycle_, ewcMOVEF);
328 wallcycle_start_nocount(wcycle_, ewcLAUNCH_GPU);
329 wallcycle_sub_start(wcycle_, ewcsLAUNCH_GPU_MOVEF);
332 // If this is the last pulse and index (noting the force halo
333 // exchanges across multiple pulses and indices are called in
334 // reverse order) then perform the following preparation
336 if ((pulse_ == (dd_->comm->cd[dimIndex_].numPulses() - 1)) && (dimIndex_ == (dd_->ndim - 1)))
338 if (!accumulateForces)
340 // Clear local portion of force array (in local stream)
341 cudaMemsetAsync(d_f, 0, numHomeAtoms_ * sizeof(rvec), localStream_.stream());
344 // ensure non-local stream waits for local stream, due to dependence on
345 // the previous H2D copy of CPU forces (if accumulateForces is true)
346 // or the above clearing.
347 // TODO remove this dependency on localStream - edmine Issue #3093
348 GpuEventSynchronizer eventLocal;
349 eventLocal.markEvent(localStream_);
350 eventLocal.enqueueWaitEvent(nonLocalStream_);
353 // Unpack halo buffer into force array
355 KernelLaunchConfig config;
356 config.blockSize[0] = c_threadsPerBlock;
357 config.blockSize[1] = 1;
358 config.blockSize[2] = 1;
359 config.gridSize[0] = (fRecvSize_ + c_threadsPerBlock - 1) / c_threadsPerBlock;
360 config.gridSize[1] = 1;
361 config.gridSize[2] = 1;
362 config.sharedMemorySize = 0;
364 const float3* recvBuf = d_recvBuf_;
365 const int* indexMap = d_indexMap_;
366 const int size = fRecvSize_;
368 if (pulse_ > 0 || dd_->ndim > 1)
370 // We need to accumulate rather than set, since it is possible
371 // that, in this pulse/dim, a value could be written to a location
372 // corresponding to the halo region of a following pulse/dim.
373 accumulateForces = true;
378 auto kernelFn = accumulateForces ? unpackRecvBufKernel<true> : unpackRecvBufKernel<false>;
380 const auto kernelArgs =
381 prepareGpuKernelArguments(kernelFn, config, &d_f, &recvBuf, &indexMap, &size);
384 kernelFn, config, nonLocalStream_, nullptr, "Domdec GPU Apply F Halo Exchange", kernelArgs);
389 fReadyOnDevice_.markEvent(nonLocalStream_);
392 wallcycle_sub_stop(wcycle_, ewcsLAUNCH_GPU_MOVEF);
393 wallcycle_stop(wcycle_, ewcLAUNCH_GPU);
397 void GpuHaloExchange::Impl::communicateHaloData(float3* d_ptr,
398 HaloQuantity haloQuantity,
399 GpuEventSynchronizer* coordinatesReadyOnDeviceEvent)
408 if (haloQuantity == HaloQuantity::HaloCoordinates)
410 sendPtr = static_cast<void*>(d_sendBuf_);
411 sendSize = xSendSize_;
412 remotePtr = remoteXPtr_;
413 sendRank = sendRankX_;
414 recvRank = recvRankX_;
417 // Wait for event from receiving task that remote coordinates are ready, and enqueue that event to stream used
418 // for subsequent data push. This avoids a race condition with the remote data being written in the previous timestep.
419 // Similarly send event to task that will push data to this task.
420 GpuEventSynchronizer* remoteCoordinatesReadyOnDeviceEvent;
421 MPI_Sendrecv(&coordinatesReadyOnDeviceEvent,
422 sizeof(GpuEventSynchronizer*),
426 &remoteCoordinatesReadyOnDeviceEvent,
427 sizeof(GpuEventSynchronizer*),
433 remoteCoordinatesReadyOnDeviceEvent->enqueueWaitEvent(nonLocalStream_);
435 GMX_UNUSED_VALUE(coordinatesReadyOnDeviceEvent);
440 sendPtr = static_cast<void*>(&(d_ptr[atomOffset_]));
441 sendSize = fSendSize_;
442 remotePtr = remoteFPtr_;
443 sendRank = sendRankF_;
444 recvRank = recvRankF_;
447 communicateHaloDataWithCudaDirect(sendPtr, sendSize, sendRank, remotePtr, recvRank);
450 void GpuHaloExchange::Impl::communicateHaloDataWithCudaDirect(void* sendPtr,
459 // We asynchronously push data to remote rank. The remote
460 // destination pointer has already been set in the init fn. We
461 // don't need to worry about overwriting data the remote ranks
462 // still needs since the halo exchange is just done once per
463 // timestep, for each of X and F.
465 // send data to neighbor, if any data exists to send
468 stat = cudaMemcpyAsync(remotePtr,
470 sendSize * DIM * sizeof(float),
471 cudaMemcpyDeviceToDevice,
472 nonLocalStream_.stream());
474 CU_RET_ERR(stat, "cudaMemcpyAsync on GPU Domdec CUDA direct data transfer failed");
478 // ensure pushed data has arrived before remote rank progresses
479 // This rank records an event and sends it to the remote rank which has just been pushed data.
480 // This rank recieves event from remote rank which has pushed data here, and enqueues that event
482 GpuEventSynchronizer* haloDataTransferRemote;
484 haloDataTransferLaunched_->markEvent(nonLocalStream_);
486 MPI_Sendrecv(&haloDataTransferLaunched_,
487 sizeof(GpuEventSynchronizer*),
491 &haloDataTransferRemote,
492 sizeof(GpuEventSynchronizer*),
499 haloDataTransferRemote->enqueueWaitEvent(nonLocalStream_);
501 GMX_UNUSED_VALUE(sendRank);
502 GMX_UNUSED_VALUE(recvRank);
506 GpuEventSynchronizer* GpuHaloExchange::Impl::getForcesReadyOnDeviceEvent()
508 return &fReadyOnDevice_;
511 /*! \brief Create Domdec GPU object */
512 GpuHaloExchange::Impl::Impl(gmx_domdec_t* dd,
514 MPI_Comm mpi_comm_mysim,
515 const DeviceContext& deviceContext,
516 const DeviceStream& localStream,
517 const DeviceStream& nonLocalStream,
519 gmx_wallcycle* wcycle) :
521 sendRankX_(dd->neighbor[dimIndex][1]),
522 recvRankX_(dd->neighbor[dimIndex][0]),
523 sendRankF_(dd->neighbor[dimIndex][0]),
524 recvRankF_(dd->neighbor[dimIndex][1]),
525 usePBC_(dd->ci[dd->dim[dimIndex]] == 0),
526 haloDataTransferLaunched_(new GpuEventSynchronizer()),
527 mpi_comm_mysim_(mpi_comm_mysim),
528 deviceContext_(deviceContext),
529 localStream_(localStream),
530 nonLocalStream_(nonLocalStream),
536 GMX_RELEASE_ASSERT(GMX_THREAD_MPI,
537 "GPU Halo exchange is currently only supported with thread-MPI enabled");
539 if (usePBC_ && dd->unitCellInfo.haveScrewPBC)
541 gmx_fatal(FARGS, "Error: screw is not yet supported in GPU halo exchange\n");
544 changePinningPolicy(&h_indexMap_, gmx::PinningPolicy::PinnedIfSupported);
546 allocateDeviceBuffer(&d_fShift_, 1, deviceContext_);
549 GpuHaloExchange::Impl::~Impl()
551 freeDeviceBuffer(&d_indexMap_);
552 freeDeviceBuffer(&d_sendBuf_);
553 freeDeviceBuffer(&d_recvBuf_);
554 freeDeviceBuffer(&d_fShift_);
555 delete haloDataTransferLaunched_;
558 GpuHaloExchange::GpuHaloExchange(gmx_domdec_t* dd,
560 MPI_Comm mpi_comm_mysim,
561 const DeviceContext& deviceContext,
562 const DeviceStream& localStream,
563 const DeviceStream& nonLocalStream,
565 gmx_wallcycle* wcycle) :
566 impl_(new Impl(dd, dimIndex, mpi_comm_mysim, deviceContext, localStream, nonLocalStream, pulse, wcycle))
570 GpuHaloExchange::GpuHaloExchange(GpuHaloExchange&&) noexcept = default;
572 GpuHaloExchange& GpuHaloExchange::operator=(GpuHaloExchange&& other) noexcept
574 std::swap(impl_, other.impl_);
578 GpuHaloExchange::~GpuHaloExchange() = default;
580 void GpuHaloExchange::reinitHalo(DeviceBuffer<RVec> d_coordinatesBuffer, DeviceBuffer<RVec> d_forcesBuffer)
582 impl_->reinitHalo(asFloat3(d_coordinatesBuffer), asFloat3(d_forcesBuffer));
585 void GpuHaloExchange::communicateHaloCoordinates(const matrix box,
586 GpuEventSynchronizer* coordinatesReadyOnDeviceEvent)
588 impl_->communicateHaloCoordinates(box, coordinatesReadyOnDeviceEvent);
591 void GpuHaloExchange::communicateHaloForces(bool accumulateForces)
593 impl_->communicateHaloForces(accumulateForces);
596 GpuEventSynchronizer* GpuHaloExchange::getForcesReadyOnDeviceEvent()
598 return impl_->getForcesReadyOnDeviceEvent();