core 0.5.0
Shared MPI block decomposition + asynchronous ghost-layer exchange (header-only C++20)
Loading...
Searching...
No Matches
distributed_flow_mg.hpp
Go to the documentation of this file.
1// core — distributed device openness multigrid for the AMR flow pressure (rung 3 of
2// docs/amr_distributed_flow.md): the distributed counterpart of Multigrid (multigrid.hpp),
3// i.e. the aperture-path pressure hierarchy AmrFlow::presMG_ runs — graded octree, cut-cell
4// openness area-averaged down the ladder, Jacobi smoothing, per-level nullspace projection —
5// with every level a ghost-slot face CSR + its own LeafHalo.
6//
7// Structure (all proven pieces composed):
8// * Level ladder: per-rank coarsenIf of the LOCAL octree (same as AmrMultigrid::build),
9// each level a COPY of the flow's DistributedOctree with the coarsened local() — the copy
10// carries the decomposition, so a rebalanced (weighted-ORB) flow octree keeps correct
11// owner lookups. Ranks reaching their root brick early PAD to the global max level count
12// (Allreduce MAX) by repeating the root brick: the padded levels' transfers are identity
13// (the covering c2p maps a leaf to itself) and their smoothing exactly mirrors what the
14// whole-domain COMM_SELF ladder does in its already-coarse regions (the
15// GradedDistributedMultigrid argument) — and the level counts must match anyway for the
16// per-level halo point-to-points to pair up.
17// * Per level: an AmrPoisson with the LeafHalo resolver seam (setResolver/setGhosts/
18// setFrameShift — probes that exit the block resolve to ghost slots; world-coordinate
19// evaluations in the GLOBAL frame), built to the miss-collect fixpoint, then host
20// assembleFv → device FvOp whose faceNbr may reference the ghost tail.
21// * Openness: level 0 α from the world-coord openFn (ghost rows sampled locally — the same
22// world points as the owner, bit-identical); level L>0 LOCAL α by the exact
23// AmrMultigrid::coarsenOpenness child-face averaging (children of a local coarse cell are
24// always local, visited in the same relative Z-order as COMM_SELF ⇒ bit-identical), and
25// GHOST α rows exchanged from the owner once per build (exact by construction — never
26// re-derived).
27// * V-cycle: jacobiFv with a ghost refresh before every sweep, local restrict/prolong
28// (parents never cross blocks), Allreduce'd volume-weighted mean removal (removeMean).
29//
30// Bit-exactness: at np=1 every probe resolves locally (zero ghosts) and the whole cycle is
31// the single-rank Multigrid arithmetic verbatim. Across ranks the smoother/transfers are
32// order-independent ⇒ WORLD==SELF bit-exact with removeMean OFF; the mean removal (a global
33// reduction) and PCG dots are np-invariant only to reduction order ⇒ tolerance, the suite's
34// Krylov contract.
35//
36// Kokkos + MPI header (include in device TUs; the AmrFlow oracle stays single-rank).
37#ifndef PECLET_CORE_AMR_DISTRIBUTED_FLOW_MG_HPP
38#define PECLET_CORE_AMR_DISTRIBUTED_FLOW_MG_HPP
39
40#ifdef PECLET_CORE_HAVE_MORTON
41
42#include <array>
43#include <memory>
44#include <vector>
45
47
51#include "peclet/core/amr/multigrid.hpp" // restrictField / prolongAdd (shared transfer kernels)
54
55namespace peclet::core::amr {
56
59inline void removeMeanFvDist(const FvOp& op, View<double> u, MPI_Comm comm) {
60 auto invVol = op.invVol;
61 auto fs = op.faceStart;
62 auto fw = op.faceW;
63 auto bc = op.bcDiag;
64 double sx = 0.0, sv = 0.0;
65 Kokkos::parallel_reduce(
66 "amr::fv_rmean", op.n,
67 KOKKOS_LAMBDA(const Index i, double& an, double& ad) {
68 double d = bc(i);
69 for (Index k = fs(i); k < fs(i + 1); ++k)
70 d += fw(k);
71 if (d > 1e-30) {
72 an += u(i) / invVol(i);
73 ad += 1.0 / invVol(i);
74 }
75 },
76 sx, sv);
77 double loc[2] = {sx, sv}, glob[2] = {0.0, 0.0};
79 if (glob[1] <= 0.0)
80 return;
81 const double m = glob[0] / glob[1];
82 Kokkos::parallel_for(
83 "amr::fv_rmean_sub", op.n, KOKKOS_LAMBDA(const Index i) {
84 double d = bc(i);
85 for (Index k = fs(i); k < fs(i + 1); ++k)
86 d += fw(k);
87 if (d > 1e-30)
88 u(i) -= m;
89 });
90}
91
92template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
94 public:
96 using Octree = typename DO::Octree;
98 using M = typename Octree::M;
99 using Code = typename Octree::Code;
100
110 template <class OpenFn>
111 void build(const DO& finest, double h0, OpenFn&& openFn,
112 const LeafHalo<Dim, Bits>* shared0 = nullptr) {
113 buildImpl(finest, h0, shared0);
114 // Openness ladder: finest level directly from the world-coord openFn (local + ghost rows,
115 // both exact); coarser levels by the exact single-rank child-face averaging for local rows
116 // + a one-time owner exchange for ghost rows.
117 levels_[0]->ap.buildOpenness(openFn);
118 for (std::size_t L = 0; L + 1 < levels_.size(); ++L)
119 coarsenOpennessTo(L);
120 finishOps();
121 }
122
123 void setRemoveMean(bool on) { removeMean_ = on; }
124
125 std::size_t numLevels() const { return levels_.size(); }
126 Index numLeaves(std::size_t L = 0) const { return levels_[L]->n; }
127 Index extendedSize(std::size_t L = 0) const { return levels_[L]->nExt; }
128 View<double> x(std::size_t L = 0) { return levels_[L]->x; }
129 View<double> b(std::size_t L = 0) { return levels_[L]->b; }
130 const FvOp& op(std::size_t L = 0) const { return levels_[L]->op; }
131 MPI_Comm comm() const { return comm_; }
132 const Poisson& poisson(std::size_t L = 0) const { return levels_[L]->ap; }
133 const LeafHalo<Dim, Bits>& halo(std::size_t L = 0) const { return *levels_[L]->hp; }
134
136 void sync(std::size_t L, View<double> v) const { levels_[L]->ex.exchange(v); }
137
141 void vcycle(int pre = 2, int post = 2, int bottom = 40, double omega = 0.8, std::size_t L = 0) {
142 Level& lv = *levels_[L];
144 if (L + 1 == levels_.size()) {
145 for (int s = 0; s < bottom; ++s) {
146 lv.ex.exchange(lv.x);
147 jacobiFv(lv.op, lv.x, bc, lv.tmp, omega);
148 }
149 if (removeMean_)
150 removeMeanFvDist(lv.op, lv.x, comm_);
151 return;
152 }
153 for (int s = 0; s < pre; ++s) {
154 lv.ex.exchange(lv.x);
155 jacobiFv(lv.op, lv.x, bc, lv.tmp, omega);
156 }
157 lv.ex.exchange(lv.x);
158 residualFv(lv.op, View<const double>(lv.x), bc, lv.res);
159 Level& cl = *levels_[L + 1];
160 restrictField(lv.childStart, lv.childIdx, View<const double>(lv.res), cl.b, cl.n);
161 Kokkos::deep_copy(cl.x, 0.0);
162 vcycle(pre, post, bottom, omega, L + 1);
163 prolongAdd(lv.c2p, View<const double>(cl.x), lv.x, lv.n);
164 for (int s = 0; s < post; ++s) {
165 lv.ex.exchange(lv.x);
166 jacobiFv(lv.op, lv.x, bc, lv.tmp, omega);
167 }
168 if (removeMean_)
169 removeMeanFvDist(lv.op, lv.x, comm_);
170 }
171
172 private:
173 struct Level {
174 DO d; // coarsened copy of the flow octree (carries the decomposition)
175 Poisson ap;
176 LeafHalo<Dim, Bits> halo; // own registry (unused when hp aliases shared0)
177 const LeafHalo<Dim, Bits>* hp = nullptr; // the registry in force (own or the shared one)
179 FvOp op;
180 Index n = 0, nExt = 0;
181 View<double> x, b, res, tmp; // x/b sized nExt (PCG deep_copies match); res/tmp local
182 View<Index> c2p, childStart, childIdx;
183 std::vector<Index> c2pHost; // kept for the openness coarsening
184 };
185
186 void buildImpl(const DO& finest, double h0, const LeafHalo<Dim, Bits>* shared0) {
187 comm_ = finest.comm();
188 h0_ = h0;
189 levels_.clear();
190 // Ladder of coarsened copies of the SAME distributed octree (decomposition preserved).
191 {
192 auto l0 = std::make_unique<Level>();
193 l0->d = finest;
194 levels_.push_back(std::move(l0));
195 for (;;) {
196 Octree c = levels_.back()->d.local();
197 const Index before = c.numLeaves();
198 const Index merged = c.coarsenIf([](Code, unsigned) { return true; });
199 if (merged == 0 || c.numLeaves() == before)
200 break;
201 auto lv = std::make_unique<Level>();
202 lv->d = levels_.back()->d;
203 lv->d.local() = std::move(c);
204 levels_.push_back(std::move(lv));
205 if (levels_.back()->d.local().numLeaves() == 1)
206 break;
207 }
208 // Pad to the global max level count (identity root-brick levels): the per-level halo
209 // point-to-points and build collectives must pair up across ranks, and the extra
210 // smoothing exactly mirrors COMM_SELF's already-coarse regions.
211 int nl = static_cast<int>(levels_.size()), gnl = nl;
212 MPI_Allreduce(&nl, &gnl, 1, MPI_INT, MPI_MAX, comm_);
213 while (static_cast<int>(levels_.size()) < gnl) {
214 auto lv = std::make_unique<Level>();
215 lv->d = levels_.back()->d;
216 levels_.push_back(std::move(lv));
217 }
218 }
219 // Per level: seam install + discovery fixpoint + topology freeze. Collective per level —
220 // every rank walks its levels in the same order (counts padded), so the resolveMisses /
221 // coverLevels rounds stay matched.
222 const Vec<Dim> gorigin = finest.globalGeometry().origin;
223 std::array<long, Dim> shift{};
224 for (int a = 0; a < Dim; ++a)
225 shift[a] = finest.blockFineOrigin()[a];
226 shift_ = shift;
227 bool first = true;
228 for (auto& lvp : levels_) {
229 Level& lv = *lvp;
230 lv.n = lv.d.local().numLeaves();
231 lv.ap.init(lv.d.local(), h0_);
232 lv.ap.setOrigin(gorigin);
233 lv.ap.setFrameShift(shift);
234 // Install the ghost metadata of a registry into ap (level/lo lookups during the walks).
235 auto installGhosts = [&](const LeafHalo<Dim, Bits>& h) {
236 std::vector<std::array<long, Dim>> glo(static_cast<std::size_t>(h.numGhosts()));
237 std::vector<unsigned> glv(static_cast<std::size_t>(h.numGhosts()));
238 for (Index g = 0; g < h.numGhosts(); ++g) {
239 for (int a = 0; a < Dim; ++a)
240 glo[static_cast<std::size_t>(g)][a] =
241 static_cast<long>(h.ghostCoord(g)[a]) - shift[a];
242 glv[static_cast<std::size_t>(g)] = static_cast<unsigned>(h.level(h.numLocal() + g));
243 }
244 lv.ap.setGhosts(std::move(glo), std::move(glv));
245 };
246 if (first && shared0) {
247 // Level 0 on the flow's frozen ±2 registry: same extended layout as the flow's cell
248 // vectors; every ±1 face probe is already cached ⇒ const lookups, no discovery.
249 lv.hp = shared0;
250 lv.ap.setResolver([shared0, shift](const std::array<long, Dim>& p) -> Index {
251 std::array<long, Dim> g = p;
252 for (int a = 0; a < Dim; ++a)
253 g[a] += shift[a];
254 return shared0->lookupGlobal(g);
255 });
257 } else {
258 lv.hp = &lv.halo;
259 lv.halo.init(lv.d);
260 LeafHalo<Dim, Bits>* hp = &lv.halo;
261 lv.ap.setResolver([hp, shift](const std::array<long, Dim>& p) -> Index {
262 std::array<long, Dim> g = p;
263 for (int a = 0; a < Dim; ++a)
264 g[a] += shift[a];
265 return hp->resolveGlobal(g);
266 });
267 // Ghosts must be (re-)installed into ap at the TOP of every fixpoint round: a probe
268 // that resolved in an earlier round returns its ghost slot immediately, and probeSlot
269 // then reads ap.levelOf(slot) — which must already cover it (newly-PENDING coords are
270 // fine: they return kPending and are skipped until the next round).
271 for (;;) {
272 installGhosts(lv.halo);
273 for (Index i = 0; i < lv.n; ++i)
274 lv.ap.forEachFaceNeighbor(i, [](Index, Real, int, double) {});
275 if (lv.halo.resolveMisses() == 0)
276 break;
277 }
278 lv.halo.finalize();
279 }
280 lv.nExt = lv.hp->extendedSize();
281 lv.ex.init(*lv.hp);
282 first = false;
283 }
284 // Local covering-leaf transfers (parents never cross blocks); identity on padded levels.
285 for (std::size_t L = 0; L + 1 < levels_.size(); ++L) {
286 const Octree& f = levels_[L]->d.local();
287 const Octree& c = levels_[L + 1]->d.local();
288 const Index nf = f.numLeaves(), nc = c.numLeaves();
289 std::vector<Index>& c2p = levels_[L]->c2pHost;
290 c2p.assign(static_cast<std::size_t>(nf), -1);
291 std::vector<Index> cnt(static_cast<std::size_t>(nc), 0);
292 for (Index i = 0; i < nf; ++i) {
293 const Index p = c.find(f.code(i)); // covering construction (== single-rank Multigrid)
294 c2p[static_cast<std::size_t>(i)] = p;
295 if (p >= 0)
296 ++cnt[static_cast<std::size_t>(p)];
297 }
298 std::vector<Index> start(static_cast<std::size_t>(nc) + 1, 0);
299 for (Index p = 0; p < nc; ++p)
300 start[static_cast<std::size_t>(p) + 1] =
301 start[static_cast<std::size_t>(p)] + cnt[static_cast<std::size_t>(p)];
302 std::vector<Index> idx(static_cast<std::size_t>(start[static_cast<std::size_t>(nc)]));
303 std::vector<Index> cur(start.begin(), start.end() - 1);
304 for (Index i = 0; i < nf; ++i) { // fine order ⇒ deterministic restrict accumulation
305 const Index p = c2p[static_cast<std::size_t>(i)];
306 if (p >= 0)
307 idx[static_cast<std::size_t>(cur[static_cast<std::size_t>(p)]++)] = i;
308 }
309 levels_[L]->c2p = toDevice(c2p, "dfmg_c2p");
310 levels_[L]->childStart = toDevice(start, "dfmg_cstart");
311 levels_[L]->childIdx = toDevice(idx, "dfmg_cidx");
312 }
313 }
314
315 // Area-average level-L face openness onto level L+1 — the EXACT AmrMultigrid::coarsenOpenness
316 // arithmetic for the local rows (children of a local coarse cell are local, summed in local
317 // Z-order == COMM_SELF's relative order ⇒ bit-identical), then ghost α rows exchanged from
318 // the owners through the level-(L+1) halo (kFaces one-time host exchanges).
319 void coarsenOpennessTo(std::size_t L) {
320 const Octree& f = levels_[L]->d.local();
321 const Octree& c = levels_[L + 1]->d.local();
322 Poisson& capL = levels_[L]->ap;
323 const int F = 2 * Dim;
324 const Index nc = c.numLeaves();
325 const Index ngc = levels_[L + 1]->hp->numGhosts();
326 std::vector<double> ca(static_cast<std::size_t>(nc + ngc) * F, 0.0);
327 std::vector<int> cnt(static_cast<std::size_t>(nc) * F, 0);
328 const std::vector<Index>& c2p = levels_[L]->c2pHost;
329 for (Index i = 0; i < f.numLeaves(); ++i) {
330 const Index p = c2p[static_cast<std::size_t>(i)];
331 if (p < 0)
332 continue;
333 const std::size_t base = static_cast<std::size_t>(p) * F;
334 if (c.level(p) == f.level(i)) {
335 for (int axis = 0; axis < Dim; ++axis)
336 for (int dir = -1; dir <= 1; dir += 2) {
337 const int fi = Poisson::faceIndex(axis, dir);
338 ca[base + static_cast<std::size_t>(fi)] += capL.faceOpenness(i, axis, dir);
339 cnt[base + static_cast<std::size_t>(fi)] += 1;
340 }
341 } else {
342 const unsigned oct = M::from_code(f.code(i)).child_index(f.level(i));
343 for (int axis = 0; axis < Dim; ++axis) {
344 const int dir = ((oct >> axis) & 1) ? +1 : -1;
345 const int fi = Poisson::faceIndex(axis, dir);
346 ca[base + static_cast<std::size_t>(fi)] += capL.faceOpenness(i, axis, dir);
347 cnt[base + static_cast<std::size_t>(fi)] += 1;
348 }
349 }
350 }
351 for (std::size_t k = 0; k < static_cast<std::size_t>(nc) * F; ++k)
352 ca[k] = cnt[k] ? ca[k] / cnt[k] : 1.0;
353 // Ghost rows from the owners (their local rows were computed by the identical arithmetic).
354 const LeafHalo<Dim, Bits>& h = *levels_[L + 1]->hp;
355 std::vector<double> col(static_cast<std::size_t>(levels_[L + 1]->nExt), 0.0);
356 for (int fi = 0; fi < F; ++fi) {
357 for (Index i = 0; i < nc; ++i)
358 col[static_cast<std::size_t>(i)] = ca[static_cast<std::size_t>(i) * F + fi];
359 h.exchangeHost(col);
360 for (Index g = 0; g < ngc; ++g)
361 ca[static_cast<std::size_t>(nc + g) * F + fi] =
362 col[static_cast<std::size_t>(nc + g)];
363 }
364 levels_[L + 1]->ap.setOpennessRaw(std::move(ca));
365 }
366
367 // Host-assemble each level's operator through the resolver seam and upload (the distributed
368 // mirror of the D5 device assembly — same CSR, ghost columns included), then the scratch.
369 void finishOps() {
370 for (auto& lvp : levels_) {
371 Level& lv = *lvp;
372 auto A = lv.ap.assembleFv();
373 lv.op.n = lv.n;
374 lv.op.invVol = toDevice(A.invVol, "dfmg_invvol");
375 lv.op.faceStart = toDevice(A.start, "dfmg_start");
376 lv.op.faceNbr = toDevice(A.nbr, "dfmg_nbr");
377 lv.op.faceW = toDevice(A.coef, "dfmg_w");
378 lv.op.bcDiag = toDevice(A.bcDiag, "dfmg_bc");
379 lv.x = View<double>("dfmg_x", static_cast<std::size_t>(lv.nExt));
380 lv.b = View<double>("dfmg_b", static_cast<std::size_t>(lv.nExt));
381 lv.res = View<double>("dfmg_res", static_cast<std::size_t>(lv.n));
382 lv.tmp = View<double>("dfmg_tmp", static_cast<std::size_t>(lv.n));
383 Kokkos::deep_copy(lv.x, 0.0);
384 Kokkos::deep_copy(lv.b, 0.0);
385 }
386 }
387
388 MPI_Comm comm_ = MPI_COMM_NULL;
389 double h0_ = 1.0;
390 std::array<long, Dim> shift_{};
391 std::vector<std::unique_ptr<Level>> levels_;
392 bool removeMean_ = false;
393};
394
395} // namespace peclet::core::amr
396
397#endif // PECLET_CORE_HAVE_MORTON
398#endif // PECLET_CORE_AMR_DISTRIBUTED_FLOW_MG_HPP
static int faceIndex(int axis, int dir)
Definition poisson.hpp:144
const LeafHalo< Dim, Bits > & halo(std::size_t L=0) const
void sync(std::size_t L, View< double > v) const
Refresh the ghost tail of a level-L vector (the PCG matvec hook uses level 0).
void vcycle(int pre=2, int post=2, int bottom=40, double omega=0.8, std::size_t L=0)
One V-cycle on level L (correction scheme), the distributed mirror of Multigrid::vcycle: ghost refres...
void build(const DO &finest, double h0, OpenFn &&openFn, const LeafHalo< Dim, Bits > *shared0=nullptr)
Build with cut-cell openness openFn(faceCentreWorld, axis) → [0,1] on the flow's distributed octree (...
const Poisson & poisson(std::size_t L=0) const
Device-resident value refresh over a finalized LeafHalo: pack the owner's local values as a Kokkos ke...
#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
#define MPI_INT
Definition mpi_stub.hpp:38
#define MPI_MAX
Definition mpi_stub.hpp:43
#define MPI_SUM
Definition mpi_stub.hpp:42
#define MPI_DOUBLE
Definition mpi_stub.hpp:40
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 removeMeanFvDist(const FvOp &op, View< double > u, MPI_Comm comm)
removeMeanFv with the two mean sums folded across ranks (identical kernels; the Allreduce sits betwee...
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:53
void jacobiFv(const FvOp &op, View< double > u, View< const double > rhs, View< double > tmp, double omega)
One weighted-Jacobi sweep of H u = rhs (in place).
Definition fv_op.hpp:135
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
void residualFv(const FvOp &op, View< const double > u, View< const double > rhs, View< double > res)
res = rhs − H u.
Definition fv_op.hpp:121
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
double Real
Default host floating type. Device kernels may use float; conversions happen at the boundary.
Definition types.hpp:18
std::int64_t Index
Signed index type for grids and particles (supersedes block_decomposer's long int IndxT).
Definition types.hpp:15
View< double > invVol
1/V_i, size n
Definition fv_op.hpp:81
View< double > faceW
w_f = openness·A_f/d_f per face, size nFaces
Definition fv_op.hpp:84
View< Index > faceStart
CSR row offsets, size n+1.
Definition fv_op.hpp:82
View< double > bcDiag
Dirichlet boundary diagonal per cell (0 if periodic), size n.
Definition fv_op.hpp:85