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
leaf_halo.hpp
Go to the documentation of this file.
1// core — LeafHalo: the ±2 leaf ghost registry + value halo for the distributed AmrFlow.
2//
3// The distributed flow solver (docs/amr_distributed_flow.md) keeps every per-leaf field in an
4// extended array: local leaves [0, nLocal), ghost leaves [nLocal, nLocal + nGhost). Every CSR the
5// step reads (momentum, FaceGeom, closure overlays, cf deltas) may reference ghost slots; the
6// kernels launch over local rows only and are otherwise unchanged. This header supplies the two
7// pieces that make that work:
8//
9// LeafHalo (host) — the ghost REGISTRY + resolve seam the operator builders thread
10// their neighbour probes through. resolve(globalFineCoord) → covering local leaf | ghost
11// slot | kPending. Unknown coords queue as misses; resolveMisses() is ONE collective
12// coverLevels round (owner request/reply) that learns each miss's covering-leaf level, then
13// canonicalizes the coord to the leaf's global ANCHOR (lo corner) so any number of probes
14// into the same remote leaf dedup to ONE ghost slot. Builders run their enumeration pass to
15// a miss-collect fixpoint: for(;;){ attempt build; if(halo.resolveMisses()==0) break; }
16// — the reach is bounded (±2 cells) so this terminates in ≤3 rounds, and the Allreduce
17// inside resolveMisses keeps the collectives matched across ranks. finalize() then
18// establishes the owner↔ghost value topology ONCE (DistributedOctree::buildGatherHaloTopology
19// — one NBX round does the owner-side locateGlobal; never again per exchange).
20//
21// LeafHaloExchange (device, Kokkos-guarded) — the per-use value refresh: device pack of the
22// owner's local values → compact host-staged MPI buffers (GPU-aware opt-in, exactly as
23// GridHalo / DistributedGatherHalo) → device scatter into the ghost tail of the extended
24// field. exchange3 batches the 3 velocity components into one message round (the projection
25// syncs 3-vectors at every point; per-field latency would triple the halo count).
26//
27// np = 1: every wrapped probe lands back in the block ⇒ zero ghosts, resolve() returns local
28// leaves only, and the distributed build path is bit-identical to the single-rank one by
29// construction (no code touches a ghost slot that does not exist).
30//
31// Bit-exactness: ghost values are unmodified copies of the owner's doubles, so any consumer
32// whose per-row arithmetic order is decomposition-independent stays bit-exact WORLD==SELF (the
33// DistributedFvOperator argument). GPU is tolerance-not-bit-exact vs host (FMA) as documented.
34//
35// device exchanger is guarded on KOKKOS_INLINE_FUNCTION (include after a Kokkos-carrying header
36// in device TUs, like ghost_projection.hpp).
37#ifndef PECLET_AMR_LEAF_HALO_HPP
38#define PECLET_AMR_LEAF_HALO_HPP
39
40#include <array>
41#include <map>
42#include <memory>
43#include <mutex>
44#include <stdexcept>
45#include <vector>
46
47#include "peclet/amr/common.hpp"
49#include "peclet/core/common/mpi.hpp"
50#include "peclet/core/common/types.hpp"
51
52// Device-exchanger dependencies, only when the TU already carries Kokkos (the host part of this
53// header must stay compilable without it).
54#ifdef KOKKOS_INLINE_FUNCTION
55#include "peclet/core/common/view.hpp"
56#include "peclet/core/halo/grid_halo.hpp" // halo::detail::gpuAwareMpi() (GPU-aware opt-in)
57#endif
58
59namespace peclet::amr {
60
61template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
62class LeafHalo {
63 public:
65 using M = typename DO::M;
66 using Coord = typename DO::Coord;
67 using CoordArr = std::array<Coord, Dim>;
68
69 static constexpr Index kPending = -2;
70 static constexpr Index kNone = -1;
71
72 void init(DO& d) {
73 d_ = &d;
74 nLocal_ = d.local().numLeaves();
75 for (int a = 0; a < Dim; ++a) {
76 fineOrigin_[a] = static_cast<long>(d.blockFineOrigin()[a]);
77 fineSize_[a] = static_cast<long>(d.blockBrick()[a]) * static_cast<long>(d.rootSpan());
78 globalFine_[a] = static_cast<long>(d.globalFineSize()[a]);
79 }
80 periodic_ = d.periodic();
81 probeSlot_.clear();
82 anchorSlot_.clear();
83 ghostCoords_.clear();
84 ghostLevels_.clear();
85 misses_.clear();
86 frozen_ = false;
87 }
88
89 Index numLocal() const { return nLocal_; }
90 Index numGhosts() const { return static_cast<Index>(ghostCoords_.size()); }
91 Index extendedSize() const { return nLocal_ + numGhosts(); }
92 MPI_Comm comm() const { return d_->comm(); }
93
96 bool wrap(std::array<long, Dim>& g) const {
97 for (int a = 0; a < Dim; ++a) {
98 const long gf = globalFine_[a];
99 if (g[a] < 0 || g[a] >= gf) {
100 if (!periodic_[a])
101 return false;
102 g[a] = ((g[a] % gf) + gf) % gf;
103 }
104 }
105 return true;
106 }
107
111 Index resolve(const CoordArr& gc) {
112 bool inBlock = true;
113 for (int a = 0; a < Dim; ++a) {
114 const long v = static_cast<long>(gc[a]) - fineOrigin_[a];
115 if (v < 0 || v >= fineSize_[a]) {
116 inBlock = false;
117 break;
118 }
119 }
120 if (inBlock) {
121 std::array<Coord, Dim> lc{};
122 for (int a = 0; a < Dim; ++a)
123 lc[a] = static_cast<Coord>(static_cast<long>(gc[a]) - fineOrigin_[a]);
124 return d_->local().find(M::encode(lc).code()); // covering local leaf
125 }
126 auto it = probeSlot_.find(gc);
127 if (it != probeSlot_.end())
128 return it->second;
129 if (frozen_)
130 throw std::runtime_error("amr::LeafHalo::resolve: unknown coord after finalize()");
131 // THREAD SAFETY (setup-parallel F1, resolved): resolve() may be called concurrently from
132 // the host-parallel builders. During a build pass the only mutation is THIS emplace —
133 // probeSlot_/anchorSlot_/ghost arrays change only in resolveMisses(), BETWEEN passes, and
134 // post-finalize the frozen_ check above makes the whole method mutation-free. Guarding the
135 // miss registration alone therefore makes every caller thread-safe. Determinism is free:
136 // misses_ is a coord-KEYED std::map, so resolveMisses() iterates in sorted-coord order and
137 // ghost-slot numbering depends only on the per-round miss SET, never on emplace order —
138 // any thread interleaving canonicalizes to the serial result bitwise.
139 {
140 std::lock_guard<std::mutex> lk(*missMx_);
141 misses_.emplace(gc, kPending);
142 }
143 return kPending;
144 }
145
147 Index resolveGlobal(std::array<long, Dim> g) {
148 if (!wrap(g))
149 return kNone;
150 CoordArr gc{};
151 for (int a = 0; a < Dim; ++a)
152 gc[a] = static_cast<Coord>(g[a]);
153 return resolve(gc);
154 }
155
160 Index lookup(const CoordArr& gc) const {
161 bool inBlock = true;
162 for (int a = 0; a < Dim; ++a) {
163 const long v = static_cast<long>(gc[a]) - fineOrigin_[a];
164 if (v < 0 || v >= fineSize_[a]) {
165 inBlock = false;
166 break;
167 }
168 }
169 if (inBlock) {
170 std::array<Coord, Dim> lc{};
171 for (int a = 0; a < Dim; ++a)
172 lc[a] = static_cast<Coord>(static_cast<long>(gc[a]) - fineOrigin_[a]);
173 return d_->local().find(M::encode(lc).code());
174 }
175 auto it = probeSlot_.find(gc);
176 if (it == probeSlot_.end())
177 throw std::runtime_error("amr::LeafHalo::lookup: coord not in the frozen registry");
178 return it->second;
179 }
180 Index lookupGlobal(std::array<long, Dim> g) const {
181 if (!wrap(g))
182 return kNone;
183 CoordArr gc{};
184 for (int a = 0; a < Dim; ++a)
185 gc[a] = static_cast<Coord>(g[a]);
186 return lookup(gc);
187 }
188
194 if (frozen_)
195 throw std::runtime_error("amr::LeafHalo::resolveMisses after finalize()");
196 long localPending = static_cast<long>(misses_.size()), globalPending = 0;
198 if (globalPending == 0)
199 return 0;
200 std::vector<CoordArr> coords;
201 coords.reserve(misses_.size());
202 for (const auto& kv : misses_)
203 coords.push_back(kv.first);
204 // Collective owner request/reply — every rank participates (possibly with zero requests).
205 std::vector<int> lv = d_->coverLevels(coords);
206 for (std::size_t k = 0; k < coords.size(); ++k) {
207 const int L = lv[k];
208 if (L < 0)
209 throw std::runtime_error(
210 "amr::LeafHalo: a wrapped probe has no covering leaf on its owner (octree hole?)");
212 for (int a = 0; a < Dim; ++a)
213 anchor[a] = static_cast<Coord>((coords[k][a] >> L) << L); // covering leaf lo corner
214 Index g;
215 auto it = anchorSlot_.find(anchor);
216 if (it != anchorSlot_.end()) {
217 g = it->second;
218 } else {
219 g = static_cast<Index>(ghostCoords_.size());
220 anchorSlot_.emplace(anchor, g);
221 ghostCoords_.push_back(anchor);
222 ghostLevels_.push_back(L);
223 }
224 probeSlot_[coords[k]] = nLocal_ + g;
225 probeSlot_[anchor] = nLocal_ + g; // a later probe may hit the anchor directly
226 }
227 misses_.clear();
228 return globalPending;
229 }
230
232 int level(Index slot) const {
233 if (slot < nLocal_)
234 return static_cast<int>(d_->local().level(slot));
235 return ghostLevels_[static_cast<std::size_t>(slot - nLocal_)];
236 }
239 const CoordArr& ghostCoord(Index g) const { return ghostCoords_[static_cast<std::size_t>(g)]; }
240
243 void finalize() {
244 typename DO::FaceGatherPlan plan;
245 plan.nFaces = extendedSize();
246 plan.remoteCoords = ghostCoords_;
247 plan.remoteSlot.resize(ghostCoords_.size());
248 for (std::size_t g = 0; g < ghostCoords_.size(); ++g)
249 plan.remoteSlot[g] = nLocal_ + static_cast<Index>(g);
250 topo_ = d_->buildGatherHaloTopology(plan);
251 // An out-of-block coord is never self-owned (a rank's ORB region IS its block), so nothing
252 // may fold into the local-fill list — that would silently alias a ghost onto a local leaf.
253 if (!topo_.localSlot.empty())
254 throw std::runtime_error("amr::LeafHalo::finalize: ghost anchor resolved self-owned");
255 for (Index l : topo_.sendLeaf)
256 if (l < 0)
257 throw std::runtime_error(
258 "amr::LeafHalo::finalize: an owner cannot locate a requested ghost leaf");
259 frozen_ = true;
260 }
261
262 const typename DO::GatherHaloTopology& topology() const { return topo_; }
263
266 void exchangeHost(std::vector<double>& x, int tag = 45) const {
267 const auto& t = topo_;
268 std::vector<double> sendBuf(t.sendLeaf.size()), recvBuf(t.recvSlot.size());
269 for (std::size_t k = 0; k < t.sendLeaf.size(); ++k)
270 sendBuf[k] = x[static_cast<std::size_t>(t.sendLeaf[k])];
271 std::vector<MPI_Request> reqs;
272 reqs.reserve(t.recvRanks.size() + t.sendRanks.size());
273 std::size_t off = 0;
274 for (std::size_t k = 0; k < t.recvRanks.size(); ++k) {
275 reqs.emplace_back();
276 MPI_Irecv(recvBuf.data() + off, t.recvCounts[k] * static_cast<int>(sizeof(double)), MPI_BYTE,
277 t.recvRanks[k], tag, d_->comm(), &reqs.back());
278 off += static_cast<std::size_t>(t.recvCounts[k]);
279 }
280 off = 0;
281 for (std::size_t k = 0; k < t.sendRanks.size(); ++k) {
282 reqs.emplace_back();
283 MPI_Isend(sendBuf.data() + off, t.sendCounts[k] * static_cast<int>(sizeof(double)), MPI_BYTE,
284 t.sendRanks[k], tag, d_->comm(), &reqs.back());
285 off += static_cast<std::size_t>(t.sendCounts[k]);
286 }
287 if (!reqs.empty())
288 MPI_Waitall(static_cast<int>(reqs.size()), reqs.data(), MPI_STATUSES_IGNORE);
289 for (std::size_t k = 0; k < t.recvSlot.size(); ++k)
290 x[static_cast<std::size_t>(t.recvSlot[k])] = recvBuf[k];
291 }
292
293 private:
294 DO* d_ = nullptr;
295 Index nLocal_ = 0;
296 std::array<long, Dim> fineOrigin_{}, fineSize_{}, globalFine_{};
297 std::array<bool, Dim> periodic_{};
298 std::map<CoordArr, Index> probeSlot_; // wrapped probe coord → extended slot
299 std::map<CoordArr, Index> anchorSlot_; // canonical covering-leaf anchor → ghost id
300 std::map<CoordArr, Index> misses_; // pending coords (value unused; map for dedup+order)
301 // Miss-registration lock (see resolve()); unique_ptr keeps LeafHalo movable (AmrFlow's
302 // release() move-assigns the owner). Cold: taken only on a MISS, pre-freeze.
303 std::unique_ptr<std::mutex> missMx_ = std::make_unique<std::mutex>();
304 std::vector<CoordArr> ghostCoords_; // ghost id → anchor (global fine lo corner)
305 std::vector<int> ghostLevels_; // ghost id → covering-leaf level
306 typename DO::GatherHaloTopology topo_;
307 bool frozen_ = false;
308};
309
310// ---- device exchanger (Kokkos TUs only; include after a Kokkos-carrying header) ---------------
311#ifdef KOKKOS_INLINE_FUNCTION
312
318 public:
319 template <int Dim, unsigned Bits>
321 const auto& t = h.topology();
322 comm_ = h.comm();
323 nLocal_ = h.numLocal();
324 nSend_ = static_cast<Index>(t.sendLeaf.size());
325 nRecv_ = static_cast<Index>(t.recvSlot.size());
326 d_sendLeaf_ = toDevice(t.sendLeaf, "lh::sendLeaf");
327 d_recvSlot_ = toDevice(t.recvSlot, "lh::recvSlot");
328 sendRanks_ = t.sendRanks;
329 sendCounts_ = t.sendCounts;
330 recvRanks_ = t.recvRanks;
331 recvCounts_ = t.recvCounts;
332 sendOff_.assign(sendCounts_.size() + 1, 0);
333 for (std::size_t k = 0; k < sendCounts_.size(); ++k)
334 sendOff_[k + 1] = sendOff_[k] + sendCounts_[k];
335 recvOff_.assign(recvCounts_.size() + 1, 0);
336 for (std::size_t k = 0; k < recvCounts_.size(); ++k)
337 recvOff_[k + 1] = recvOff_[k] + recvCounts_[k];
338 // Buffers sized for the batched 3-component exchange (single-field uses the first third).
339 d_sendBuf_ = View<double>(Kokkos::view_alloc("lh::sendBuf", Kokkos::WithoutInitializing),
340 static_cast<std::size_t>(nSend_) * 3);
341 d_recvBuf_ = View<double>(Kokkos::view_alloc("lh::recvBuf", Kokkos::WithoutInitializing),
342 static_cast<std::size_t>(nRecv_) * 3);
343 h_sendBuf_ = Kokkos::create_mirror_view(d_sendBuf_);
344 h_recvBuf_ = Kokkos::create_mirror_view(d_recvBuf_);
345 }
346
347 Index numGhosts() const { return nRecv_; }
348
350 void exchange(View<double> x, int tag = 45) const {
351 if (nSend_) {
352 IndexView sl = d_sendLeaf_;
353 View<double> buf = d_sendBuf_;
354 Kokkos::parallel_for(
355 "lh::pack", Kokkos::RangePolicy<ExecSpace>(0, nSend_),
356 KOKKOS_LAMBDA(const Index p) { buf(p) = x(sl(p)); });
357 }
358 transfer(1, tag);
359 if (nRecv_) {
360 IndexView rs = d_recvSlot_;
361 View<double> buf = d_recvBuf_;
362 Kokkos::parallel_for(
363 "lh::scatter", Kokkos::RangePolicy<ExecSpace>(0, nRecv_),
364 KOKKOS_LAMBDA(const Index k) { x(rs(k)) = buf(k); });
365 }
366 Kokkos::fence();
367 }
368
371 if (nSend_) {
372 IndexView sl = d_sendLeaf_;
373 View<double> buf = d_sendBuf_;
374 Kokkos::parallel_for(
375 "lh::pack3", Kokkos::RangePolicy<ExecSpace>(0, nSend_), KOKKOS_LAMBDA(const Index p) {
376 const Index l = sl(p);
377 buf(p * 3 + 0) = x0(l);
378 buf(p * 3 + 1) = x1(l);
379 buf(p * 3 + 2) = x2(l);
380 });
381 }
382 transfer(3, tag);
383 if (nRecv_) {
384 IndexView rs = d_recvSlot_;
385 View<double> buf = d_recvBuf_;
386 Kokkos::parallel_for(
387 "lh::scatter3", Kokkos::RangePolicy<ExecSpace>(0, nRecv_), KOKKOS_LAMBDA(const Index k) {
388 const Index s = rs(k);
389 x0(s) = buf(k * 3 + 0);
390 x1(s) = buf(k * 3 + 1);
391 x2(s) = buf(k * 3 + 2);
392 });
393 }
394 Kokkos::fence();
395 }
396
397 private:
399 void transfer(int width, int tag) const {
400 const bool aware = peclet::core::halo::detail::gpuAwareMpi();
401 if (nSend_ && !aware)
402 Kokkos::deep_copy(h_sendBuf_, d_sendBuf_);
403 Kokkos::fence(); // send buffer (host-staged or device) ready before MPI reads it
404 double* sendBase = aware ? d_sendBuf_.data() : h_sendBuf_.data();
405 double* recvBase = aware ? d_recvBuf_.data() : h_recvBuf_.data();
406 std::vector<MPI_Request> reqs;
407 reqs.reserve(recvRanks_.size() + sendRanks_.size());
408 for (std::size_t k = 0; k < recvRanks_.size(); ++k) {
409 reqs.emplace_back();
410 MPI_Irecv(recvBase + static_cast<std::size_t>(recvOff_[k]) * width,
411 recvCounts_[k] * width * static_cast<int>(sizeof(double)), MPI_BYTE, recvRanks_[k],
412 tag, comm_, &reqs.back());
413 }
414 for (std::size_t k = 0; k < sendRanks_.size(); ++k) {
415 reqs.emplace_back();
416 MPI_Isend(sendBase + static_cast<std::size_t>(sendOff_[k]) * width,
417 sendCounts_[k] * width * static_cast<int>(sizeof(double)), MPI_BYTE, sendRanks_[k],
418 tag, comm_, &reqs.back());
419 }
420 if (!reqs.empty())
421 MPI_Waitall(static_cast<int>(reqs.size()), reqs.data(), MPI_STATUSES_IGNORE);
422 if (nRecv_ && !aware)
423 Kokkos::deep_copy(d_recvBuf_, h_recvBuf_);
424 }
425
426 MPI_Comm comm_ = MPI_COMM_NULL;
427 Index nLocal_ = 0, nSend_ = 0, nRecv_ = 0;
428 IndexView d_sendLeaf_, d_recvSlot_;
429 std::vector<int> sendRanks_, sendCounts_, sendOff_, recvRanks_, recvCounts_, recvOff_;
430 View<double> d_sendBuf_, d_recvBuf_;
431 HostView<double> h_sendBuf_, h_recvBuf_;
432};
433
434#endif // KOKKOS_INLINE_FUNCTION
435
436} // namespace peclet::amr
437
438#endif // PECLET_AMR_LEAF_HALO_HPP
unsigned level(Index i) const
Index find(Code p) const
Leaf containing Morton code p, or -1. Host wrapper over amrLocate.
std::vector< int > coverLevels(const std::vector< std::array< Coord, Dim > > &coords) const
For each global fine coord (already wrapped into the domain), the level of the covering leaf on its o...
const IVec< Dim > & blockFineOrigin() const
const std::array< bool, Dim > & periodic() const
const IVec< Dim > & globalFineSize() const
GatherHaloTopology buildGatherHaloTopology(const FaceGatherPlan &plan) const
Build the value-only gather topology from a FaceGatherPlan: classify each remote coord by owner (owne...
const IVec< Dim > & blockBrick() const
Device-resident value refresh over a finalized LeafHalo: pack the owner's local values as a Kokkos ke...
void init(const LeafHalo< Dim, Bits > &h)
void exchange3(View< double > x0, View< double > x1, View< double > x2, int tag=46) const
Batched 3-component refresh (one message round for u0,u1,u2 — stride-3 packing).
void exchange(View< double > x, int tag=45) const
Refresh x[nLocal, nLocal+nGhost) (x is the extended device field, size >= extendedSize()).
static constexpr Index kNone
resolveGlobal(): exits a non-periodic axis
Definition leaf_halo.hpp:70
Index lookup(const CoordArr &gc) const
Const lookup against the FINALIZED registry (no miss registration): wrapped probe → local leaf or cac...
bool wrap(std::array< long, Dim > &g) const
Wrap an unbounded global fine probe into the domain (in place); false = the probe exits a non-periodi...
Definition leaf_halo.hpp:96
Index resolveGlobal(std::array< long, Dim > g)
Wrap + resolve from unbounded long coords; kNone on a non-periodic exit.
DistributedOctree< Dim, Bits > DO
Definition leaf_halo.hpp:64
static constexpr Index kPending
resolve(): queued as a miss (call resolveMisses)
Definition leaf_halo.hpp:69
MPI_Comm comm() const
Definition leaf_halo.hpp:92
typename DO::M M
Definition leaf_halo.hpp:65
Index numGhosts() const
Definition leaf_halo.hpp:90
Index resolve(const CoordArr &gc)
Resolve a wrapped global fine coordinate to an extended slot: [0, nLocal) = the covering LOCAL leaf; ...
const CoordArr & ghostCoord(Index g) const
Global fine anchor (lo corner) of ghost g in [0, nGhosts) — for SDF sampling / multi-hop probe constr...
Index extendedSize() const
Definition leaf_halo.hpp:91
Index numLocal() const
Definition leaf_halo.hpp:89
void exchangeHost(std::vector< double > &x, int tag=45) const
Host exchange: refresh x[nLocal, nLocal+nGhost) from the owners (x sized extendedSize()).
int level(Index slot) const
Covering-leaf level of an extended slot (local octree level convention: 0 = finest cell).
const DO::GatherHaloTopology & topology() const
typename DO::Coord Coord
Definition leaf_halo.hpp:66
std::array< Coord, Dim > CoordArr
Definition leaf_halo.hpp:67
long resolveMisses()
COLLECTIVE (all ranks together, misses or not): one owner coverLevels round resolving every queued mi...
void finalize()
Freeze the ghost set and establish the owner↔ghost value topology (one NBX round; the owner-side loca...
Index lookupGlobal(std::array< long, Dim > g) const
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