core
Shared MPI block decomposition + asynchronous ghost-layer exchange (header-only C++20)
Loading...
Searching...
No Matches
particle_halo.hpp
Go to the documentation of this file.
1// core — portable (Kokkos) device driver for the persistent Lagrangian ghost halo.
2//
3// Kokkos counterpart of the device-resident particle gather that packing-gpu hand-rolls
4// (gatherFloat4 / device-pointer MPI): it drives ParticleHaloTopology<Dim>'s forward/reverse
5// exchanges with on-device gather/scatter kernels, so per-particle attribute arrays stay on the GPU
6// and only the compact send/recv buffers are host-staged for MPI (or handed straight to a GPU-aware
7// MPI via PECLET_CORE_GPU_AWARE_MPI). Topology comes from ParticleHaloTopology::flatten(); results
8// match the CPU exchange.
9//
10// forward<T>(owned -> ghost) : copy each owner's value into its ghost copies (verbatim).
11// reverse<T>(ghost -> owned, +=) : accumulate ghost contributions back onto owners (atomic).
12//
13// The periodic position-shift forward (forwardPositions) is payload-specific (e.g. packing's
14// float4) and lives in the consumer; this primitive is the field-agnostic core.
15#ifndef PECLET_CORE_HALO_PARTICLE_HALO_HPP
16#define PECLET_CORE_HALO_PARTICLE_HALO_HPP
17
18#include <vector>
19
23#include "peclet/core/halo/grid_halo.hpp" // detail::gpuAwareMpi()
25
26namespace peclet::core::halo {
27
28template <int Dim>
30 public:
31 ParticleHalo() = default;
32 ParticleHalo(const ParticleHalo&) = delete;
34
36 void init(const ParticleHaloTopology<Dim>& halo) {
37 auto t = halo.flatten();
38 comm_ = halo.comm();
39 sendRanks_ = t.sendRanks;
40 sendCounts_ = t.sendCounts;
41 sendOff_ = t.sendOffsets; // prefix sum into sendIdx, size sendRanks+1
42 recvRanks_ = t.recvRanks;
43 recvCounts_ = t.recvCounts;
44 recvOff_.assign(t.recvOffsets.begin(),
45 t.recvOffsets.end()); // per recv rank start in [0,numReceived)
46 nSend_ = static_cast<Index>(t.sendIdx.size());
47 numGhost_ = static_cast<Index>(halo.numGhost());
48 numReceived_ = t.numReceived; // cross-rank ghosts [0,numReceived); self-ghosts after
49 numSelf_ = numGhost_ - numReceived_;
50 d_sendIdx_ = toDevice(t.sendIdx, "peclet::core::halo::p_sendIdx");
51 d_selfIdx_ = toDevice(t.selfIdx, "peclet::core::halo::p_selfIdx");
52 }
53
55 template <class T>
56 void forward(const View<T>& owned, const View<T>& ghost, int tag = 7603) {
57 const bool aware = detail::gpuAwareMpi();
58 View<T> sendBuf = scratchAs<T>("peclet::core::halo::p_sendBuf");
59 if (nSend_) {
60 View<T> o = owned;
61 IndexView idx = d_sendIdx_;
63 Kokkos::parallel_for(
64 "peclet::core::halo::p_gather", Kokkos::RangePolicy<ExecSpace>(0, nSend_),
65 KOKKOS_LAMBDA(const Index i) { buf(i) = o(idx(i)); });
66 }
67
68 // MPI fills only the cross-rank received slots [0,numReceived); the self tail is gathered
69 // locally.
70 std::vector<T> hSend, hRecv;
71 T* sendBase;
72 T* recvBase;
73 if (aware) {
74 Kokkos::fence();
75 sendBase = sendBuf.data();
76 recvBase = ghost.data();
77 } else {
78 hSend.resize(static_cast<std::size_t>(nSend_));
79 hRecv.resize(static_cast<std::size_t>(numReceived_));
80 copyToHost(sendBuf, hSend);
81 sendBase = hSend.data();
82 recvBase = hRecv.data();
83 }
84
85 std::vector<MPI_Request> reqs;
86 postRecv(recvBase, recvRanks_, recvOff_, recvCounts_, tag, reqs);
87 postSend(sendBase, sendRanks_, sendOff_, sendCounts_, tag, reqs);
88 if (!reqs.empty())
89 MPI_Waitall(static_cast<int>(reqs.size()), reqs.data(), MPI_STATUSES_IGNORE);
90
91 if (!aware && numReceived_) {
92 auto sub = Kokkos::subview(
93 ghost, std::pair<std::size_t, std::size_t>(0, static_cast<std::size_t>(numReceived_)));
94 Kokkos::View<const T*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>> hv(
95 hRecv.data(), hRecv.size());
96 Kokkos::deep_copy(sub, hv);
97 }
98 // Local periodic self-ghosts: gather owner -> ghost tail on device (no MPI). Needed when a rank
99 // borders itself across a periodic face (undecomposed axis / np=1).
100 if (numSelf_) {
101 View<T> o = owned;
102 IndexView sidx = d_selfIdx_;
103 View<T> g = ghost;
104 const Index base = numReceived_;
105 Kokkos::parallel_for(
106 "peclet::core::halo::p_selfGather", Kokkos::RangePolicy<ExecSpace>(0, numSelf_),
107 KOKKOS_LAMBDA(const Index j) { g(base + j) = o(sidx(j)); });
108 }
109 Kokkos::fence();
110 }
111
113 template <class T>
114 void reverse(const View<T>& ghost, const View<T>& owned, int tag = 7604) {
115 const bool aware = detail::gpuAwareMpi();
116
117 std::vector<T> hGhost, hRecv;
118 T* ghostBase;
119 T* recvBase;
121 scratchAs<T>("peclet::core::halo::p_recvBuf"); // owners receive sendIdx-many contributions
122 if (aware) {
123 Kokkos::fence();
124 ghostBase = ghost.data();
125 recvBase = recvBuf.data();
126 } else {
127 // Only the cross-rank ghost slice [0,numReceived) is sent over MPI (recvOff_/recvCounts_
128 // index into it); the self-ghost tail [numReceived,numGhost) is consumed by the device
129 // self-scatter below straight from `ghost`, so it never needs to reach the host.
130 hGhost.resize(static_cast<std::size_t>(numReceived_));
131 hRecv.resize(static_cast<std::size_t>(nSend_));
132 if (numReceived_)
133 copyToHost(Kokkos::subview(ghost, std::pair<std::size_t, std::size_t>(
134 0, static_cast<std::size_t>(numReceived_))),
135 hGhost);
136 ghostBase = hGhost.data();
137 recvBase = hRecv.data();
138 }
139
140 // Mirror of forward: ghost-holders send ghost slices, owners receive into sendIdx-shaped
141 // buffer.
142 std::vector<MPI_Request> reqs;
143 postRecv(recvBase, sendRanks_, sendOff_, sendCounts_, tag, reqs);
144 postSend(ghostBase, recvRanks_, recvOff_, recvCounts_, tag, reqs);
145 if (!reqs.empty())
146 MPI_Waitall(static_cast<int>(reqs.size()), reqs.data(), MPI_STATUSES_IGNORE);
147
148 if (!aware)
149 copyToDevice(hRecv, recvBuf);
150 if (nSend_) {
151 View<T> o = owned;
152 IndexView idx = d_sendIdx_;
154 // Duplicate owned indices (a particle is a ghost on several ranks) => accumulate atomically.
155 Kokkos::parallel_for(
156 "peclet::core::halo::p_scatter", Kokkos::RangePolicy<ExecSpace>(0, nSend_),
157 KOKKOS_LAMBDA(const Index i) { Kokkos::atomic_add(&o(idx(i)), buf(i)); });
158 }
159 // Local periodic self-ghosts accumulate straight onto their (same-rank) owner.
160 if (numSelf_) {
161 View<T> o = owned;
162 IndexView sidx = d_selfIdx_;
163 View<T> g = ghost;
164 const Index base = numReceived_;
165 Kokkos::parallel_for(
166 "peclet::core::halo::p_selfScatter", Kokkos::RangePolicy<ExecSpace>(0, numSelf_),
167 KOKKOS_LAMBDA(const Index j) { Kokkos::atomic_add(&o(sidx(j)), g(base + j)); });
168 }
169 Kokkos::fence();
170 }
171
172 Index numGhost() const { return numGhost_; }
173
174 private:
175 template <class SrcView, class T>
176 static void copyToHost(const SrcView& d, std::vector<T>& h) {
177 if (h.empty())
178 return;
179 Kokkos::View<T*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>> hv(h.data(),
180 h.size());
181 Kokkos::deep_copy(hv, d);
182 }
183
188 template <class T>
189 View<T> scratchAs(const char* label) {
190 const std::size_t bytes = static_cast<std::size_t>(nSend_) * sizeof(T);
191 if (scratch_.size() < bytes)
192 scratch_ = Kokkos::View<char*, MemSpace>(
193 Kokkos::view_alloc(std::string(label), Kokkos::WithoutInitializing), bytes);
194 return View<T>(reinterpret_cast<T*>(scratch_.data()), static_cast<std::size_t>(nSend_));
195 }
196 template <class T>
197 static void copyToDevice(const std::vector<T>& h, const View<T>& d) {
198 if (h.empty())
199 return;
200 Kokkos::View<const T*, Kokkos::HostSpace, Kokkos::MemoryTraits<Kokkos::Unmanaged>> hv(h.data(),
201 h.size());
202 Kokkos::deep_copy(d, hv);
203 }
204
205 // Post one Irecv per neighbour rank into `base` at the given per-rank offsets (in elements).
206 template <class T>
207 void postRecv(T* base, const std::vector<int>& ranks, const std::vector<int>& off,
208 const std::vector<int>& cnt, int tag, std::vector<MPI_Request>& reqs) {
209 for (std::size_t k = 0; k < ranks.size(); ++k) {
210 reqs.emplace_back();
211 MPI_Irecv(base + off[k], cnt[k] * static_cast<int>(sizeof(T)), MPI_BYTE, ranks[k], tag, comm_,
212 &reqs.back());
213 }
214 }
215 template <class T>
216 void postSend(T* base, const std::vector<int>& ranks, const std::vector<int>& off,
217 const std::vector<int>& cnt, int tag, std::vector<MPI_Request>& reqs) {
218 for (std::size_t k = 0; k < ranks.size(); ++k) {
219 reqs.emplace_back();
220 MPI_Isend(base + off[k], cnt[k] * static_cast<int>(sizeof(T)), MPI_BYTE, ranks[k], tag, comm_,
221 &reqs.back());
222 }
223 }
224
225 MPI_Comm comm_ = MPI_COMM_NULL;
226 std::vector<int> sendRanks_, sendCounts_, sendOff_;
227 std::vector<int> recvRanks_, recvCounts_, recvOff_;
228 Index nSend_ = 0, numGhost_ = 0, numReceived_ = 0, numSelf_ = 0;
229 IndexView d_sendIdx_, d_selfIdx_;
230 Kokkos::View<char*, MemSpace> scratch_; // reusable per-call exchange scratch (see scratchAs)
231};
232
233} // namespace peclet::core::halo
234
235#endif // PECLET_CORE_HALO_PARTICLE_HALO_HPP
ParticleHalo & operator=(const ParticleHalo &)=delete
void forward(const View< T > &owned, const View< T > &ghost, int tag=7603)
owned[N] -> ghost[G], verbatim. Both live on the device.
void init(const ParticleHaloTopology< Dim > &halo)
Capture the (already-built) host halo's topology in device-friendly form.
ParticleHalo(const ParticleHalo &)=delete
void reverse(const View< T > &ghost, const View< T > &owned, int tag=7604)
ghost[G] -> owned[N], accumulated (owned += contributions). T must support Kokkos::atomic_add.
#define MPI_COMM_NULL
Definition mpi_stub.hpp:27
int MPI_Comm
Definition mpi_stub.hpp:16
int MPI_Waitall(int, MPI_Request *, MPI_Status *)
Definition mpi_stub.hpp:126
#define MPI_BYTE
Definition mpi_stub.hpp:37
#define MPI_STATUSES_IGNORE
Definition mpi_stub.hpp:31
int MPI_Isend(const void *, int, MPI_Datatype, int, int, MPI_Comm, MPI_Request *)
Definition mpi_stub.hpp:98
int MPI_Irecv(void *, int, MPI_Datatype, int, int, MPI_Comm, MPI_Request *)
Definition mpi_stub.hpp:101
bool gpuAwareMpi()
Whether to hand DEVICE pointers straight to MPI (GPU-aware MPI) instead of host-staging.
Definition grid_halo.hpp:29
View< T > toDevice(const std::vector< T > &h, const std::string &label)
Upload a host std::vector into a freshly-sized device View (empty vector => empty view).
Definition view.hpp:44
Kokkos::View< T *, MemSpace > View
1D device array.
Definition view.hpp:26
Kokkos::View< Index *, MemSpace > IndexView
Device array of grid/particle indices (the matched send/recv/self-copy lists).
Definition view.hpp:34
std::int64_t Index
Signed index type for grids and particles (supersedes block_decomposer's long int IndxT).
Definition types.hpp:15