core
Shared MPI block decomposition + asynchronous ghost-layer exchange (header-only C++20)
Loading...
Searching...
No Matches
distributed_view.hpp
Go to the documentation of this file.
1// core — device-resident distributed AMR Poisson + multigrid (C2).
2//
3// The host DistributedPoisson / DistributedMultigrid (distributed_poisson.hpp) run every per-cell
4// apply / jacobi / residual / restrict / prolong on host std::vector, gathering cross-block ghosts
5// by re-issuing an owner request/reply each matvec. Only the MPI byte exchange truly needs the
6// host; the compute is embarrassingly parallel per cell. This header keeps the field on the device
7// and runs all of it as Kokkos kernels, mirroring only the compact ghost buffer across MPI — the
8// octree analogue of grid_halo.hpp's GridHalo.
9//
10// DistributedGatherHalo — value-only face-neighbour gather over a topology established ONCE
11// (DistributedOctree::buildGatherHaloTopology): device pack/scatter kernels + a host-staged
12// compact MPI values exchange. No per-matvec coord exchange / locateGlobal.
13// DistributedPoissonView — apply / jacobi / residual as parallel_for over the device field.
14// DistributedMultigridView — the V-cycle: device jacobi + the (local) child-CSR restrict /
15// piecewise-constant prolong reused from multigrid.hpp (bit-exact, deterministic).
16//
17// Bit-exactness contract (the suite's distributed lock): the SOLUTION is bit-for-bit identical
18// across rank counts and to the single-block MPI_COMM_SELF reference — apply/jacobi are per-cell,
19// restrict is a fixed-order child-CSR gather (no atomics), so floating-point order is preserved.
20// (The residual L2 norm uses a parallel reduction whose order differs, but the norm is only a
21// convergence scalar, never the solution.) GPU is tolerance-not-bit-exact (FMA) by the documented
22// convention.
23//
24// Requires a Kokkos build + MPI + the morton checkout (PECLET_CORE_HAVE_MORTON).
25#ifndef PECLET_CORE_AMR_DISTRIBUTED_VIEW_HPP
26#define PECLET_CORE_AMR_DISTRIBUTED_VIEW_HPP
27
28#ifdef PECLET_CORE_HAVE_MORTON
29
30#include <cmath>
31#include <memory>
32#include <vector>
33
34#include "peclet/core/amr/distributed_poisson.hpp" // DistributedOctree, AmrGeometry, DistributedMultigrid (ref)
35#include "peclet/core/amr/multigrid.hpp" // restrictField / prolongAdd (bit-exact, reused)
38#include "peclet/core/halo/grid_halo.hpp" // detail::gpuAwareMpi() (PECLET_CORE_GPU_AWARE_MPI opt-in, H1)
39
40namespace peclet::core::amr {
41
45template <int Dim, unsigned Bits>
47 public:
49
50 void init(const DO& d, const typename DO::GatherHaloTopology& t) {
51 comm_ = d.comm();
52 nFaces_ = t.nFaces;
53 nLocal_ = static_cast<Index>(t.localSlot.size());
54 nSend_ = static_cast<Index>(t.sendLeaf.size());
55 nRecv_ = static_cast<Index>(t.recvSlot.size());
56 d_localSlot_ = toDevice(t.localSlot, "dgh::localSlot");
57 d_localLeaf_ = toDevice(t.localLeaf, "dgh::localLeaf");
58 d_sendLeaf_ = toDevice(t.sendLeaf, "dgh::sendLeaf");
59 d_recvSlot_ = toDevice(t.recvSlot, "dgh::recvSlot");
60 sendRanks_ = t.sendRanks;
61 sendCounts_ = t.sendCounts;
62 recvRanks_ = t.recvRanks;
63 recvCounts_ = t.recvCounts;
64 sendOff_.assign(sendCounts_.size() + 1, 0);
65 for (std::size_t k = 0; k < sendCounts_.size(); ++k)
66 sendOff_[k + 1] = sendOff_[k] + sendCounts_[k];
67 recvOff_.assign(recvCounts_.size() + 1, 0);
68 for (std::size_t k = 0; k < recvCounts_.size(); ++k)
69 recvOff_[k + 1] = recvOff_[k] + recvCounts_[k];
70 d_sendBuf_ = View<double>(Kokkos::view_alloc("dgh::sendBuf", Kokkos::WithoutInitializing),
71 static_cast<std::size_t>(nSend_));
72 d_recvBuf_ = View<double>(Kokkos::view_alloc("dgh::recvBuf", Kokkos::WithoutInitializing),
73 static_cast<std::size_t>(nRecv_));
74 h_sendBuf_ = Kokkos::create_mirror_view(d_sendBuf_);
75 h_recvBuf_ = Kokkos::create_mirror_view(d_recvBuf_);
76 }
77
78 Index nFaces() const { return nFaces_; }
79
83 void gather(View<const double> x, View<double> g, int tag = 41) const {
84 const double sentinel =
85 -1e300; // == DistributedOctree::kNoNeighbor; unused in the periodic case
86 // GPU-aware MPI (H1): when enabled (PECLET_CORE_GPU_AWARE_MPI) hand the device send/recv buffer
87 // pointers straight to MPI — the field never touches the host even for the compact buffers.
88 // Default is the portable host-staged path (deep_copy to a host mirror), exactly as GridHalo
89 // does.
91 Kokkos::deep_copy(g, sentinel);
92 if (nLocal_) {
93 IndexView ls = d_localSlot_, ll = d_localLeaf_;
94 View<double> gv = g;
95 Kokkos::parallel_for(
96 "dgh::local", Kokkos::RangePolicy<ExecSpace>(0, nLocal_),
97 KOKKOS_LAMBDA(const Index k) { gv(ls(k)) = x(ll(k)); });
98 }
99 if (nSend_) {
100 IndexView sl = d_sendLeaf_;
101 View<double> buf = d_sendBuf_;
102 Kokkos::parallel_for(
103 "dgh::pack", Kokkos::RangePolicy<ExecSpace>(0, nSend_),
104 KOKKOS_LAMBDA(const Index p) { buf(p) = (sl(p) >= 0) ? x(sl(p)) : sentinel; });
105 if (!aware)
106 Kokkos::deep_copy(h_sendBuf_, d_sendBuf_);
107 }
108 Kokkos::fence(); // send buffer (host-staged or device) ready before MPI reads it
109 double* sendBase = aware ? d_sendBuf_.data() : h_sendBuf_.data();
110 double* recvBase = aware ? d_recvBuf_.data() : h_recvBuf_.data();
111 std::vector<MPI_Request> reqs;
112 reqs.reserve(recvRanks_.size() + sendRanks_.size());
113 for (std::size_t k = 0; k < recvRanks_.size(); ++k) {
114 reqs.emplace_back();
115 MPI_Irecv(recvBase + recvOff_[k], recvCounts_[k] * static_cast<int>(sizeof(double)), MPI_BYTE,
116 recvRanks_[k], tag, comm_, &reqs.back());
117 }
118 for (std::size_t k = 0; k < sendRanks_.size(); ++k) {
119 reqs.emplace_back();
120 MPI_Isend(sendBase + sendOff_[k], sendCounts_[k] * static_cast<int>(sizeof(double)), MPI_BYTE,
121 sendRanks_[k], tag, comm_, &reqs.back());
122 }
123 if (!reqs.empty())
124 MPI_Waitall(static_cast<int>(reqs.size()), reqs.data(), MPI_STATUSES_IGNORE);
125 if (nRecv_) {
126 if (!aware)
127 Kokkos::deep_copy(d_recvBuf_, h_recvBuf_);
128 IndexView rs = d_recvSlot_;
129 View<double> buf = d_recvBuf_, gv = g;
130 Kokkos::parallel_for(
131 "dgh::scatter", Kokkos::RangePolicy<ExecSpace>(0, nRecv_),
132 KOKKOS_LAMBDA(const Index k) { gv(rs(k)) = buf(k); });
133 }
134 Kokkos::fence();
135 }
136
137 private:
138 MPI_Comm comm_ = MPI_COMM_NULL;
139 Index nFaces_ = 0, nLocal_ = 0, nSend_ = 0, nRecv_ = 0;
140 IndexView d_localSlot_, d_localLeaf_, d_sendLeaf_, d_recvSlot_;
141 std::vector<int> sendRanks_, sendCounts_, sendOff_, recvRanks_, recvCounts_, recvOff_;
142 View<double> d_sendBuf_, d_recvBuf_;
143 HostView<double> h_sendBuf_, h_recvBuf_;
144};
145
149template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
151 public:
152 void init(DistributedOctree<Dim, Bits>& d, double h0) {
153 d_ = &d;
154 h0_ = h0;
155 auto plan = d.buildFaceGatherPlan();
156 halo_.init(d, d.buildGatherHaloTopology(plan));
157 n_ = d.local().numLeaves();
158 g_ = View<double>(Kokkos::view_alloc("dpd::g", Kokkos::WithoutInitializing),
159 static_cast<std::size_t>(n_) * (2 * Dim));
160 scratch_ = View<double>(Kokkos::view_alloc("dpd::scratch", Kokkos::WithoutInitializing),
161 static_cast<std::size_t>(n_));
162 }
163
164 Index numLeaves() const { return n_; }
165 MPI_Comm comm() const { return d_->comm(); }
166
169 halo_.gather(x, g_);
170 const int F = 2 * Dim;
171 const double inv = 1.0 / (h0_ * h0_);
172 View<double> gv = g_;
173 Kokkos::parallel_for(
174 "dpd::apply", Kokkos::RangePolicy<ExecSpace>(0, n_), KOKKOS_LAMBDA(const Index i) {
175 double s = 0.0;
176 for (int f = 0; f < F; ++f)
177 s += gv(i * F + f) - x(i);
178 y(i) = inv * s;
179 });
180 }
181
183 void jacobi(View<double> x, View<const double> b, int sweeps, double omega = 0.8) const {
184 const int F = 2 * Dim;
185 const double inv = 1.0 / (h0_ * h0_), diag = F * inv;
186 View<double> lx = scratch_;
187 for (int s = 0; s < sweeps; ++s) {
189 Kokkos::parallel_for(
190 "dpd::jacobi_update", Kokkos::RangePolicy<ExecSpace>(0, n_),
191 KOKKOS_LAMBDA(const Index i) { x(i) += omega * (lx(i) - b(i)) / diag; });
192 }
193 }
194
197 apply(x, scratch_);
198 View<double> ax = scratch_;
199 double s = 0.0;
200 Kokkos::parallel_reduce(
201 "dpd::residual", Kokkos::RangePolicy<ExecSpace>(0, n_),
202 KOKKOS_LAMBDA(const Index i, double& acc) {
203 double r = b(i) - ax(i);
204 res(i) = r;
205 acc += r * r;
206 },
207 s);
208 double g = 0.0;
209 MPI_Allreduce(&s, &g, 1, MPI_DOUBLE, MPI_SUM, d_->comm());
210 return std::sqrt(g);
211 }
212
214 apply(x, scratch_);
215 View<double> ax = scratch_;
216 double s = 0.0;
217 Kokkos::parallel_reduce(
218 "dpd::resnorm", Kokkos::RangePolicy<ExecSpace>(0, n_),
219 KOKKOS_LAMBDA(const Index i, double& acc) {
220 double r = b(i) - ax(i);
221 acc += r * r;
222 },
223 s);
224 double g = 0.0;
225 MPI_Allreduce(&s, &g, 1, MPI_DOUBLE, MPI_SUM, d_->comm());
226 return std::sqrt(g);
227 }
228
229 private:
230 DistributedOctree<Dim, Bits>* d_ = nullptr;
231 double h0_ = 1.0;
232 Index n_ = 0;
234 mutable View<double> g_, scratch_;
235};
236
241template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
243 public:
245
246 void build(const IVec<Dim>& g0, const AmrGeometry<Dim>& geo,
247 const std::array<bool, Dim>& periodic, MPI_Comm comm) {
248 levels_.clear();
249 int size = 1;
250 MPI_Comm_size(comm, &size);
251 IVec<Dim> g = g0;
252 double h = geo.h0;
253 for (;;) {
254 auto lvl = std::make_unique<Level>();
256 lg.h0 = h;
257 lvl->d.init(g, /*lmax=*/0, lg, periodic, comm);
258 lvl->op.init(lvl->d, h);
259 lvl->n = lvl->d.local().numLeaves();
260 lvl->x = View<double>("dmg::x", static_cast<std::size_t>(lvl->n));
261 lvl->b = View<double>("dmg::b", static_cast<std::size_t>(lvl->n));
262 lvl->res = View<double>("dmg::res", static_cast<std::size_t>(lvl->n));
263 levels_.push_back(std::move(lvl));
264 bool ok = true;
265 long prod = 1;
266 IVec<Dim> ng{};
267 for (int d = 0; d < Dim; ++d) {
268 if (g[d] % 2 != 0 || g[d] / 2 < 2)
269 ok = false;
270 ng[d] = g[d] / 2;
271 prod *= ng[d];
272 }
273 if (!ok || prod < size)
274 break;
275 g = ng;
276 h *= 2.0;
277 }
278 // Nested fine→coarse maps + the coarse→children CSR (fine-index order ⇒ the restrict gather
279 // sums in the same order as the host serial accumulation, hence bit-exact). All local (ORB
280 // nests).
281 for (std::size_t L = 0; L + 1 < levels_.size(); ++L) {
282 auto& fine = levels_[L]->d;
283 auto& coarse = levels_[L + 1]->d;
284 const Index nf = fine.local().numLeaves();
285 const Index nc = coarse.local().numLeaves();
286 std::vector<Index> c2p(static_cast<std::size_t>(nf), -1);
287 std::vector<Index> cnt(static_cast<std::size_t>(nc), 0);
288 for (Index i = 0; i < nf; ++i) {
289 IVec<Dim> gf = fine.globalRootOf(i), gc{};
290 for (int d = 0; d < Dim; ++d)
291 gc[d] = gf[d] / 2;
292 Index p = coarse.findGlobalRoot(gc);
293 c2p[static_cast<std::size_t>(i)] = p;
294 if (p >= 0)
295 ++cnt[static_cast<std::size_t>(p)];
296 }
297 std::vector<Index> start(static_cast<std::size_t>(nc) + 1, 0);
298 for (Index p = 0; p < nc; ++p)
299 start[static_cast<std::size_t>(p) + 1] =
300 start[static_cast<std::size_t>(p)] + cnt[static_cast<std::size_t>(p)];
301 std::vector<Index> idx(static_cast<std::size_t>(start[static_cast<std::size_t>(nc)]));
302 std::vector<Index> cur(start.begin(), start.end() - 1);
303 for (Index i = 0; i < nf; ++i) { // increasing fine index ⇒ children listed in fine order
304 Index p = c2p[static_cast<std::size_t>(i)];
305 if (p >= 0)
306 idx[static_cast<std::size_t>(cur[static_cast<std::size_t>(p)]++)] = i;
307 }
308 levels_[L]->c2p = toDevice(c2p, "dmg::c2p");
309 levels_[L]->childStart = toDevice(start, "dmg::cstart");
310 levels_[L]->childIdx = toDevice(idx, "dmg::cidx");
311 }
312 }
313
314 std::size_t numLevels() const { return levels_.size(); }
315 Index numLeaves(std::size_t L = 0) const { return levels_[L]->n; }
316 DistributedPoissonView<Dim, Bits>& op(std::size_t L = 0) { return levels_[L]->op; }
317 Octree& octree(std::size_t L = 0) { return levels_[L]->d; }
318 View<double> x(std::size_t L = 0) { return levels_[L]->x; }
319 View<double> b(std::size_t L = 0) { return levels_[L]->b; }
320
323 void vcycle(View<double> x, View<const double> b, int pre = 2, int post = 2, int bottom = 30,
324 std::size_t L = 0) {
325 Level& lv = *levels_[L];
326 if (L + 1 == levels_.size()) {
327 lv.op.jacobi(x, b, bottom);
328 return;
329 }
330 lv.op.jacobi(x, b, pre);
331 lv.op.residual(View<const double>(x), b, lv.res);
332 Level& cl = *levels_[L + 1];
334 View<const double>(lv.res), cl.b, cl.n);
335 Kokkos::deep_copy(cl.x, 0.0);
336 vcycle(cl.x, View<const double>(cl.b), pre, post, bottom, L + 1);
338 lv.op.jacobi(x, b, post);
339 }
340
341 private:
342 struct Level {
343 Octree d;
345 Index n = 0;
346 View<double> x, b, res;
347 View<Index> c2p, childStart, childIdx;
348 };
349 std::vector<std::unique_ptr<Level>> levels_;
350};
351
352} // namespace peclet::core::amr
353
354#endif // PECLET_CORE_HAVE_MORTON
355#endif // PECLET_CORE_AMR_DISTRIBUTED_VIEW_HPP
Value-only, device-resident face-neighbour gather over a fixed topology (C2).
void init(const DO &d, const typename DO::GatherHaloTopology &t)
void gather(View< const double > x, View< double > g, int tag=41) const
g (size nFaces) := the face-neighbour values of x.
Device-resident distributed geometric multigrid for the plain Laplacian (the device analogue of the h...
void vcycle(View< double > x, View< const double > b, int pre=2, int post=2, int bottom=30, std::size_t L=0)
One V-cycle of A x = b on level L (correction scheme); entirely device-resident bar the compact gathe...
DistributedPoissonView< Dim, Bits > & op(std::size_t L=0)
void build(const IVec< Dim > &g0, const AmrGeometry< Dim > &geo, const std::array< bool, Dim > &periodic, MPI_Comm comm)
GatherHaloTopology buildGatherHaloTopology(const FaceGatherPlan &plan) const
Build the value-only gather topology from a FaceGatherPlan: classify each remote coord by owner (owne...
Device-resident distributed plain-Laplacian Poisson operator: y = ∇²x with cross-block neighbours fro...
void jacobi(View< double > x, View< const double > b, int sweeps, double omega=0.8) const
sweeps damped-Jacobi relaxations of L u = b (in place). Reads only the previous iterate.
double residualNorm(View< const double > x, View< const double > b) const
double residual(View< const double > x, View< const double > b, View< double > res) const
res = b − L x (device); returns the global L2 norm (the only host scalar).
void apply(View< const double > x, View< double > y) const
y = L x (L = ∇², periodic).
void init(DistributedOctree< Dim, Bits > &d, double h0)
int MPI_Comm_size(MPI_Comm, int *s)
Definition mpi_stub.hpp:69
#define MPI_COMM_NULL
Definition mpi_stub.hpp:27
int MPI_Allreduce(const void *sbuf, void *rbuf, int count, MPI_Datatype dt, MPI_Op, MPI_Comm)
Definition mpi_stub.hpp:80
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
#define MPI_SUM
Definition mpi_stub.hpp:42
#define MPI_DOUBLE
Definition mpi_stub.hpp:40
int MPI_Irecv(void *, int, MPI_Datatype, int, int, MPI_Comm, MPI_Request *)
Definition mpi_stub.hpp:101
void prolongAdd(View< const Index > c2p, View< const double > coarse, View< double > fine, Index nFine)
Prolong (piecewise-constant) + correct: fine(i) += coarse(c2p(i)).
Definition multigrid.hpp:81
void restrictField(View< const Index > childStart, View< const Index > childIdx, View< const double > fine, View< double > coarse, Index nCoarse)
Restrict: coarse(p) = mean over p's children (CSR fixed order ⇒ deterministic).
Definition multigrid.hpp:45
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