peclet-amr 0.2.0
Block-local-Morton AMR octree + collocated cut-cell Navier–Stokes solver (header-only C++20, Kokkos + MPI)
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#ifndef PECLET_AMR_DISTRIBUTED_VIEW_HPP
25#define PECLET_AMR_DISTRIBUTED_VIEW_HPP
26
27#include <cmath>
28#include <memory>
29#include <vector>
30
31#include "peclet/amr/common.hpp"
32#include "peclet/amr/distributed_poisson.hpp" // DistributedOctree, AmrGeometry, DistributedMultigrid (ref)
33#include "peclet/amr/multigrid.hpp" // restrictField / prolongAdd (bit-exact, reused)
34#include "peclet/core/common/mpi.hpp"
35#include "peclet/core/common/view.hpp"
36#include "peclet/core/halo/grid_halo.hpp" // detail::gpuAwareMpi() (PECLET_CORE_GPU_AWARE_MPI opt-in, H1)
37
38namespace peclet::amr {
39
43template <int Dim, unsigned Bits>
45 public:
47
48 void init(const DO& d, const typename DO::GatherHaloTopology& t) {
49 comm_ = d.comm();
50 nFaces_ = t.nFaces;
51 nLocal_ = static_cast<Index>(t.localSlot.size());
52 nSend_ = static_cast<Index>(t.sendLeaf.size());
53 nRecv_ = static_cast<Index>(t.recvSlot.size());
54 d_localSlot_ = toDevice(t.localSlot, "dgh::localSlot");
55 d_localLeaf_ = toDevice(t.localLeaf, "dgh::localLeaf");
56 d_sendLeaf_ = toDevice(t.sendLeaf, "dgh::sendLeaf");
57 d_recvSlot_ = toDevice(t.recvSlot, "dgh::recvSlot");
58 sendRanks_ = t.sendRanks;
59 sendCounts_ = t.sendCounts;
60 recvRanks_ = t.recvRanks;
61 recvCounts_ = t.recvCounts;
62 sendOff_.assign(sendCounts_.size() + 1, 0);
63 for (std::size_t k = 0; k < sendCounts_.size(); ++k)
64 sendOff_[k + 1] = sendOff_[k] + sendCounts_[k];
65 recvOff_.assign(recvCounts_.size() + 1, 0);
66 for (std::size_t k = 0; k < recvCounts_.size(); ++k)
67 recvOff_[k + 1] = recvOff_[k] + recvCounts_[k];
68 d_sendBuf_ = View<double>(Kokkos::view_alloc("dgh::sendBuf", Kokkos::WithoutInitializing),
69 static_cast<std::size_t>(nSend_));
70 d_recvBuf_ = View<double>(Kokkos::view_alloc("dgh::recvBuf", Kokkos::WithoutInitializing),
71 static_cast<std::size_t>(nRecv_));
72 h_sendBuf_ = Kokkos::create_mirror_view(d_sendBuf_);
73 h_recvBuf_ = Kokkos::create_mirror_view(d_recvBuf_);
74 }
75
76 Index nFaces() const { return nFaces_; }
77
81 void gather(View<const double> x, View<double> g, int tag = 41) const {
82 const double sentinel =
83 -1e300; // == DistributedOctree::kNoNeighbor; unused in the periodic case
84 // GPU-aware MPI (H1): when enabled (PECLET_CORE_GPU_AWARE_MPI) hand the device send/recv buffer
85 // pointers straight to MPI — the field never touches the host even for the compact buffers.
86 // Default is the portable host-staged path (deep_copy to a host mirror), exactly as GridHalo
87 // does.
88 const bool aware = peclet::core::halo::detail::gpuAwareMpi();
89 Kokkos::deep_copy(g, sentinel);
90 if (nLocal_) {
91 IndexView ls = d_localSlot_, ll = d_localLeaf_;
92 View<double> gv = g;
93 Kokkos::parallel_for(
94 "dgh::local", Kokkos::RangePolicy<ExecSpace>(0, nLocal_),
95 KOKKOS_LAMBDA(const Index k) { gv(ls(k)) = x(ll(k)); });
96 }
97 if (nSend_) {
98 IndexView sl = d_sendLeaf_;
99 View<double> buf = d_sendBuf_;
100 Kokkos::parallel_for(
101 "dgh::pack", Kokkos::RangePolicy<ExecSpace>(0, nSend_),
102 KOKKOS_LAMBDA(const Index p) { buf(p) = (sl(p) >= 0) ? x(sl(p)) : sentinel; });
103 if (!aware)
104 Kokkos::deep_copy(h_sendBuf_, d_sendBuf_);
105 }
106 Kokkos::fence(); // send buffer (host-staged or device) ready before MPI reads it
107 double* sendBase = aware ? d_sendBuf_.data() : h_sendBuf_.data();
108 double* recvBase = aware ? d_recvBuf_.data() : h_recvBuf_.data();
109 std::vector<MPI_Request> reqs;
110 reqs.reserve(recvRanks_.size() + sendRanks_.size());
111 for (std::size_t k = 0; k < recvRanks_.size(); ++k) {
112 reqs.emplace_back();
113 MPI_Irecv(recvBase + recvOff_[k], recvCounts_[k] * static_cast<int>(sizeof(double)), MPI_BYTE,
114 recvRanks_[k], tag, comm_, &reqs.back());
115 }
116 for (std::size_t k = 0; k < sendRanks_.size(); ++k) {
117 reqs.emplace_back();
118 MPI_Isend(sendBase + sendOff_[k], sendCounts_[k] * static_cast<int>(sizeof(double)), MPI_BYTE,
119 sendRanks_[k], tag, comm_, &reqs.back());
120 }
121 if (!reqs.empty())
122 MPI_Waitall(static_cast<int>(reqs.size()), reqs.data(), MPI_STATUSES_IGNORE);
123 if (nRecv_) {
124 if (!aware)
125 Kokkos::deep_copy(d_recvBuf_, h_recvBuf_);
126 IndexView rs = d_recvSlot_;
127 View<double> buf = d_recvBuf_, gv = g;
128 Kokkos::parallel_for(
129 "dgh::scatter", Kokkos::RangePolicy<ExecSpace>(0, nRecv_),
130 KOKKOS_LAMBDA(const Index k) { gv(rs(k)) = buf(k); });
131 }
132 Kokkos::fence();
133 }
134
135 private:
136 MPI_Comm comm_ = MPI_COMM_NULL;
137 Index nFaces_ = 0, nLocal_ = 0, nSend_ = 0, nRecv_ = 0;
138 IndexView d_localSlot_, d_localLeaf_, d_sendLeaf_, d_recvSlot_;
139 std::vector<int> sendRanks_, sendCounts_, sendOff_, recvRanks_, recvCounts_, recvOff_;
140 View<double> d_sendBuf_, d_recvBuf_;
141 HostView<double> h_sendBuf_, h_recvBuf_;
142};
143
147template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
149 public:
150 void init(DistributedOctree<Dim, Bits>& d, double h0) { init(d, detail::filledVec<Dim>(h0)); }
152 d_ = &d;
153 h0_ = h0;
154 auto plan = d.buildFaceGatherPlan();
155 halo_.init(d, d.buildGatherHaloTopology(plan));
156 n_ = d.local().numLeaves();
157 g_ = View<double>(Kokkos::view_alloc("dpd::g", Kokkos::WithoutInitializing),
158 static_cast<std::size_t>(n_) * (2 * Dim));
159 scratch_ = View<double>(Kokkos::view_alloc("dpd::scratch", Kokkos::WithoutInitializing),
160 static_cast<std::size_t>(n_));
161 }
162
163 Index numLeaves() const { return n_; }
164 MPI_Comm comm() const { return d_->comm(); }
165
168 halo_.gather(x, g_);
169 const int F = 2 * Dim;
170 // Phase 3 (`docs/amr_anisotropic.md` §3): one weight per AXIS; the gather layout is
171 // `2*axis + (dir>0 ? 0 : 1)`. Cubic grid => every `inv[d]` is the same double.
172 Kokkos::Array<double, Dim> inv;
173 for (int d = 0; d < Dim; ++d)
174 inv[d] = 1.0 / (h0_[d] * h0_[d]);
175 View<double> gv = g_;
176 Kokkos::parallel_for(
177 "dpd::apply", Kokkos::RangePolicy<ExecSpace>(0, n_), KOKKOS_LAMBDA(const Index i) {
178 double s = 0.0;
179 for (int f = 0; f < F; ++f)
180 s += inv[f / 2] * (gv(i * F + f) - x(i));
181 y(i) = s;
182 });
183 }
184
186 void jacobi(View<double> x, View<const double> b, int sweeps, double omega = 0.8) const {
187 const int F = 2 * Dim;
188 // Phase 3: the Jacobi diagonal is the sum of the per-axis weights (2 faces each).
189 double diag = 0.0;
190 for (int d = 0; d < Dim; ++d)
191 diag += 2.0 / (h0_[d] * h0_[d]);
192 View<double> lx = scratch_;
193 for (int s = 0; s < sweeps; ++s) {
195 Kokkos::parallel_for(
196 "dpd::jacobi_update", Kokkos::RangePolicy<ExecSpace>(0, n_),
197 KOKKOS_LAMBDA(const Index i) { x(i) += omega * (lx(i) - b(i)) / diag; });
198 }
199 }
200
203 apply(x, scratch_);
204 View<double> ax = scratch_;
205 double s = 0.0;
206 Kokkos::parallel_reduce(
207 "dpd::residual", Kokkos::RangePolicy<ExecSpace>(0, n_),
208 KOKKOS_LAMBDA(const Index i, double& acc) {
209 double r = b(i) - ax(i);
210 res(i) = r;
211 acc += r * r;
212 },
213 s);
214 double g = 0.0;
215 MPI_Allreduce(&s, &g, 1, MPI_DOUBLE, MPI_SUM, d_->comm());
216 return std::sqrt(g);
217 }
218
220 apply(x, scratch_);
221 View<double> ax = scratch_;
222 double s = 0.0;
223 Kokkos::parallel_reduce(
224 "dpd::resnorm", Kokkos::RangePolicy<ExecSpace>(0, n_),
225 KOKKOS_LAMBDA(const Index i, double& acc) {
226 double r = b(i) - ax(i);
227 acc += r * r;
228 },
229 s);
230 double g = 0.0;
231 MPI_Allreduce(&s, &g, 1, MPI_DOUBLE, MPI_SUM, d_->comm());
232 return std::sqrt(g);
233 }
234
235 private:
236 DistributedOctree<Dim, Bits>* d_ = nullptr;
237 Vec<Dim> h0_ = detail::filledVec<Dim>(1.0);
238 Index n_ = 0;
240 mutable View<double> g_, scratch_;
241};
242
247template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
249 public:
251
252 void build(const IVec<Dim>& g0, const AmrGeometry<Dim>& geo,
253 const std::array<bool, Dim>& periodic, MPI_Comm comm) {
254 levels_.clear();
255 int size = 1;
256 MPI_Comm_size(comm, &size);
257 IVec<Dim> g = g0;
258 // Phase 3 (AM1): all axes coarsen by 2 together, so the aspect ratio rides down unchanged.
259 Vec<Dim> h = geo.h0;
260 for (;;) {
261 auto lvl = std::make_unique<Level>();
263 lg.h0 = h;
264 lvl->d.init(g, /*lmax=*/0, lg, periodic, comm);
265 lvl->op.init(lvl->d, h);
266 lvl->n = lvl->d.local().numLeaves();
267 lvl->x = View<double>("dmg::x", static_cast<std::size_t>(lvl->n));
268 lvl->b = View<double>("dmg::b", static_cast<std::size_t>(lvl->n));
269 lvl->res = View<double>("dmg::res", static_cast<std::size_t>(lvl->n));
270 levels_.push_back(std::move(lvl));
271 bool ok = true;
272 long prod = 1;
273 IVec<Dim> ng{};
274 for (int d = 0; d < Dim; ++d) {
275 if (g[d] % 2 != 0 || g[d] / 2 < 2)
276 ok = false;
277 ng[d] = g[d] / 2;
278 prod *= ng[d];
279 }
280 if (!ok || prod < size)
281 break;
282 g = ng;
283 for (int d = 0; d < Dim; ++d)
284 h[d] *= 2.0;
285 }
286 // Nested fine→coarse maps + the coarse→children CSR (fine-index order ⇒ the restrict gather
287 // sums in the same order as the host serial accumulation, hence bit-exact). All local (ORB
288 // nests).
289 for (std::size_t L = 0; L + 1 < levels_.size(); ++L) {
290 auto& fine = levels_[L]->d;
291 auto& coarse = levels_[L + 1]->d;
292 const Index nf = fine.local().numLeaves();
293 const Index nc = coarse.local().numLeaves();
294 std::vector<Index> c2p(static_cast<std::size_t>(nf), -1);
295 std::vector<Index> cnt(static_cast<std::size_t>(nc), 0);
296 for (Index i = 0; i < nf; ++i) {
297 IVec<Dim> gf = fine.globalRootOf(i), gc{};
298 for (int d = 0; d < Dim; ++d)
299 gc[d] = gf[d] / 2;
300 Index p = coarse.findGlobalRoot(gc);
301 c2p[static_cast<std::size_t>(i)] = p;
302 if (p >= 0)
303 ++cnt[static_cast<std::size_t>(p)];
304 }
305 std::vector<Index> start(static_cast<std::size_t>(nc) + 1, 0);
306 for (Index p = 0; p < nc; ++p)
307 start[static_cast<std::size_t>(p) + 1] =
308 start[static_cast<std::size_t>(p)] + cnt[static_cast<std::size_t>(p)];
309 std::vector<Index> idx(static_cast<std::size_t>(start[static_cast<std::size_t>(nc)]));
310 std::vector<Index> cur(start.begin(), start.end() - 1);
311 for (Index i = 0; i < nf; ++i) { // increasing fine index ⇒ children listed in fine order
312 Index p = c2p[static_cast<std::size_t>(i)];
313 if (p >= 0)
314 idx[static_cast<std::size_t>(cur[static_cast<std::size_t>(p)]++)] = i;
315 }
316 levels_[L]->c2p = toDevice(c2p, "dmg::c2p");
317 levels_[L]->childStart = toDevice(start, "dmg::cstart");
318 levels_[L]->childIdx = toDevice(idx, "dmg::cidx");
319 }
320 }
321
322 std::size_t numLevels() const { return levels_.size(); }
323 Index numLeaves(std::size_t L = 0) const { return levels_[L]->n; }
324 DistributedPoissonView<Dim, Bits>& op(std::size_t L = 0) { return levels_[L]->op; }
325 Octree& octree(std::size_t L = 0) { return levels_[L]->d; }
326 View<double> x(std::size_t L = 0) { return levels_[L]->x; }
327 View<double> b(std::size_t L = 0) { return levels_[L]->b; }
328
331 void vcycle(View<double> x, View<const double> b, int pre = 2, int post = 2, int bottom = 30,
332 std::size_t L = 0) {
333 Level& lv = *levels_[L];
334 if (L + 1 == levels_.size()) {
335 lv.op.jacobi(x, b, bottom);
336 return;
337 }
338 lv.op.jacobi(x, b, pre);
339 lv.op.residual(View<const double>(x), b, lv.res);
340 Level& cl = *levels_[L + 1];
342 View<const double>(lv.res), cl.b, cl.n);
343 Kokkos::deep_copy(cl.x, 0.0);
344 vcycle(cl.x, View<const double>(cl.b), pre, post, bottom, L + 1);
346 lv.op.jacobi(x, b, post);
347 }
348
349 private:
350 struct Level {
351 Octree d;
353 Index n = 0;
354 View<double> x, b, res;
355 View<Index> c2p, childStart, childIdx;
356 };
357 std::vector<std::unique_ptr<Level>> levels_;
358};
359
360} // namespace peclet::amr
361
362#endif // PECLET_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...
DistributedOctree< Dim, Bits > Octree
void build(const IVec< Dim > &g0, const AmrGeometry< Dim > &geo, const std::array< bool, Dim > &periodic, MPI_Comm comm)
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...
View< double > b(std::size_t L=0)
Index numLeaves(std::size_t L=0) const
View< double > x(std::size_t L=0)
DistributedPoissonView< Dim, Bits > & op(std::size_t L=0)
FaceGatherPlan buildFaceGatherPlan() const
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 init(DistributedOctree< Dim, Bits > &d, const Vec< Dim > &h0)
void init(DistributedOctree< Dim, Bits > &d, double h0)
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).
double residualNorm(View< const double > x, View< const double > b) const
void apply(View< const double > x, View< double > y) const
y = L x (L = ∇², periodic).
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.
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:46
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:82
std::vector< std::array< double, Dim > > transferFieldGradients(const BlockOctree< Dim, Bits > &oldT, const std::vector< double > &oldF)
Per-old-leaf minmod prolongation gradients (per fine-coordinate unit) — transferField's stencil,...
Definition adapt.hpp:51
World-space placement of a block-local octree: fine coordinate (0,..,0) sits at origin,...