core 0.5.0
Shared MPI block decomposition + asynchronous ghost-layer exchange (header-only C++20)
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// Header-only, guarded by PECLET_CORE_HAVE_MORTON. The host part compiles without Kokkos; the
36// device exchanger is guarded on KOKKOS_INLINE_FUNCTION (include after a Kokkos-carrying header
37// in device TUs, like ghost_projection.hpp).
38#ifndef PECLET_CORE_AMR_LEAF_HALO_HPP
39#define PECLET_CORE_AMR_LEAF_HALO_HPP
40
41#ifdef PECLET_CORE_HAVE_MORTON
42
43#include <array>
44#include <map>
45#include <stdexcept>
46#include <vector>
47
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
56#include "peclet/core/halo/grid_halo.hpp" // halo::detail::gpuAwareMpi() (GPU-aware opt-in)
57#endif
58
59namespace peclet::core::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 misses_.emplace(gc, kPending);
132 return kPending;
133 }
134
136 Index resolveGlobal(std::array<long, Dim> g) {
137 if (!wrap(g))
138 return kNone;
139 CoordArr gc{};
140 for (int a = 0; a < Dim; ++a)
141 gc[a] = static_cast<Coord>(g[a]);
142 return resolve(gc);
143 }
144
149 Index lookup(const CoordArr& gc) const {
150 bool inBlock = true;
151 for (int a = 0; a < Dim; ++a) {
152 const long v = static_cast<long>(gc[a]) - fineOrigin_[a];
153 if (v < 0 || v >= fineSize_[a]) {
154 inBlock = false;
155 break;
156 }
157 }
158 if (inBlock) {
159 std::array<Coord, Dim> lc{};
160 for (int a = 0; a < Dim; ++a)
161 lc[a] = static_cast<Coord>(static_cast<long>(gc[a]) - fineOrigin_[a]);
162 return d_->local().find(M::encode(lc).code());
163 }
164 auto it = probeSlot_.find(gc);
165 if (it == probeSlot_.end())
166 throw std::runtime_error("amr::LeafHalo::lookup: coord not in the frozen registry");
167 return it->second;
168 }
169 Index lookupGlobal(std::array<long, Dim> g) const {
170 if (!wrap(g))
171 return kNone;
172 CoordArr gc{};
173 for (int a = 0; a < Dim; ++a)
174 gc[a] = static_cast<Coord>(g[a]);
175 return lookup(gc);
176 }
177
183 if (frozen_)
184 throw std::runtime_error("amr::LeafHalo::resolveMisses after finalize()");
185 long localPending = static_cast<long>(misses_.size()), globalPending = 0;
187 if (globalPending == 0)
188 return 0;
189 std::vector<CoordArr> coords;
190 coords.reserve(misses_.size());
191 for (const auto& kv : misses_)
192 coords.push_back(kv.first);
193 // Collective owner request/reply — every rank participates (possibly with zero requests).
194 std::vector<int> lv = d_->coverLevels(coords);
195 for (std::size_t k = 0; k < coords.size(); ++k) {
196 const int L = lv[k];
197 if (L < 0)
198 throw std::runtime_error(
199 "amr::LeafHalo: a wrapped probe has no covering leaf on its owner (octree hole?)");
201 for (int a = 0; a < Dim; ++a)
202 anchor[a] = static_cast<Coord>((coords[k][a] >> L) << L); // covering leaf lo corner
203 Index g;
204 auto it = anchorSlot_.find(anchor);
205 if (it != anchorSlot_.end()) {
206 g = it->second;
207 } else {
208 g = static_cast<Index>(ghostCoords_.size());
209 anchorSlot_.emplace(anchor, g);
210 ghostCoords_.push_back(anchor);
211 ghostLevels_.push_back(L);
212 }
213 probeSlot_[coords[k]] = nLocal_ + g;
214 probeSlot_[anchor] = nLocal_ + g; // a later probe may hit the anchor directly
215 }
216 misses_.clear();
217 return globalPending;
218 }
219
221 int level(Index slot) const {
222 if (slot < nLocal_)
223 return static_cast<int>(d_->local().level(slot));
224 return ghostLevels_[static_cast<std::size_t>(slot - nLocal_)];
225 }
228 const CoordArr& ghostCoord(Index g) const {
229 return ghostCoords_[static_cast<std::size_t>(g)];
230 }
231
234 void finalize() {
235 typename DO::FaceGatherPlan plan;
236 plan.nFaces = extendedSize();
237 plan.remoteCoords = ghostCoords_;
238 plan.remoteSlot.resize(ghostCoords_.size());
239 for (std::size_t g = 0; g < ghostCoords_.size(); ++g)
240 plan.remoteSlot[g] = nLocal_ + static_cast<Index>(g);
241 topo_ = d_->buildGatherHaloTopology(plan);
242 // An out-of-block coord is never self-owned (a rank's ORB region IS its block), so nothing
243 // may fold into the local-fill list — that would silently alias a ghost onto a local leaf.
244 if (!topo_.localSlot.empty())
245 throw std::runtime_error("amr::LeafHalo::finalize: ghost anchor resolved self-owned");
246 for (Index l : topo_.sendLeaf)
247 if (l < 0)
248 throw std::runtime_error(
249 "amr::LeafHalo::finalize: an owner cannot locate a requested ghost leaf");
250 frozen_ = true;
251 }
252
253 const typename DO::GatherHaloTopology& topology() const { return topo_; }
254
257 void exchangeHost(std::vector<double>& x, int tag = 45) const {
258 const auto& t = topo_;
259 std::vector<double> sendBuf(t.sendLeaf.size()), recvBuf(t.recvSlot.size());
260 for (std::size_t k = 0; k < t.sendLeaf.size(); ++k)
261 sendBuf[k] = x[static_cast<std::size_t>(t.sendLeaf[k])];
262 std::vector<MPI_Request> reqs;
263 reqs.reserve(t.recvRanks.size() + t.sendRanks.size());
264 std::size_t off = 0;
265 for (std::size_t k = 0; k < t.recvRanks.size(); ++k) {
266 reqs.emplace_back();
267 MPI_Irecv(recvBuf.data() + off,
268 t.recvCounts[k] * static_cast<int>(sizeof(double)), MPI_BYTE, t.recvRanks[k], tag,
269 d_->comm(), &reqs.back());
270 off += static_cast<std::size_t>(t.recvCounts[k]);
271 }
272 off = 0;
273 for (std::size_t k = 0; k < t.sendRanks.size(); ++k) {
274 reqs.emplace_back();
275 MPI_Isend(sendBuf.data() + off,
276 t.sendCounts[k] * static_cast<int>(sizeof(double)), MPI_BYTE, t.sendRanks[k], tag,
277 d_->comm(), &reqs.back());
278 off += static_cast<std::size_t>(t.sendCounts[k]);
279 }
280 if (!reqs.empty())
281 MPI_Waitall(static_cast<int>(reqs.size()), reqs.data(), MPI_STATUSES_IGNORE);
282 for (std::size_t k = 0; k < t.recvSlot.size(); ++k)
283 x[static_cast<std::size_t>(t.recvSlot[k])] = recvBuf[k];
284 }
285
286 private:
287 DO* d_ = nullptr;
288 Index nLocal_ = 0;
289 std::array<long, Dim> fineOrigin_{}, fineSize_{}, globalFine_{};
290 std::array<bool, Dim> periodic_{};
291 std::map<CoordArr, Index> probeSlot_; // wrapped probe coord → extended slot
292 std::map<CoordArr, Index> anchorSlot_; // canonical covering-leaf anchor → ghost id
293 std::map<CoordArr, Index> misses_; // pending coords (value unused; map for dedup+order)
294 std::vector<CoordArr> ghostCoords_; // ghost id → anchor (global fine lo corner)
295 std::vector<int> ghostLevels_; // ghost id → covering-leaf level
296 typename DO::GatherHaloTopology topo_;
297 bool frozen_ = false;
298};
299
300// ---- device exchanger (Kokkos TUs only; include after a Kokkos-carrying header) ---------------
301#ifdef KOKKOS_INLINE_FUNCTION
302
308 public:
309 template <int Dim, unsigned Bits>
311 const auto& t = h.topology();
312 comm_ = h.comm();
313 nLocal_ = h.numLocal();
314 nSend_ = static_cast<Index>(t.sendLeaf.size());
315 nRecv_ = static_cast<Index>(t.recvSlot.size());
316 d_sendLeaf_ = toDevice(t.sendLeaf, "lh::sendLeaf");
317 d_recvSlot_ = toDevice(t.recvSlot, "lh::recvSlot");
318 sendRanks_ = t.sendRanks;
319 sendCounts_ = t.sendCounts;
320 recvRanks_ = t.recvRanks;
321 recvCounts_ = t.recvCounts;
322 sendOff_.assign(sendCounts_.size() + 1, 0);
323 for (std::size_t k = 0; k < sendCounts_.size(); ++k)
324 sendOff_[k + 1] = sendOff_[k] + sendCounts_[k];
325 recvOff_.assign(recvCounts_.size() + 1, 0);
326 for (std::size_t k = 0; k < recvCounts_.size(); ++k)
327 recvOff_[k + 1] = recvOff_[k] + recvCounts_[k];
328 // Buffers sized for the batched 3-component exchange (single-field uses the first third).
329 d_sendBuf_ = View<double>(Kokkos::view_alloc("lh::sendBuf", Kokkos::WithoutInitializing),
330 static_cast<std::size_t>(nSend_) * 3);
331 d_recvBuf_ = View<double>(Kokkos::view_alloc("lh::recvBuf", Kokkos::WithoutInitializing),
332 static_cast<std::size_t>(nRecv_) * 3);
333 h_sendBuf_ = Kokkos::create_mirror_view(d_sendBuf_);
334 h_recvBuf_ = Kokkos::create_mirror_view(d_recvBuf_);
335 }
336
337 Index numGhosts() const { return nRecv_; }
338
340 void exchange(View<double> x, int tag = 45) const {
341 if (nSend_) {
342 IndexView sl = d_sendLeaf_;
343 View<double> buf = d_sendBuf_;
344 Kokkos::parallel_for(
345 "lh::pack", Kokkos::RangePolicy<ExecSpace>(0, nSend_),
346 KOKKOS_LAMBDA(const Index p) { buf(p) = x(sl(p)); });
347 }
348 transfer(1, tag);
349 if (nRecv_) {
350 IndexView rs = d_recvSlot_;
351 View<double> buf = d_recvBuf_;
352 Kokkos::parallel_for(
353 "lh::scatter", Kokkos::RangePolicy<ExecSpace>(0, nRecv_),
354 KOKKOS_LAMBDA(const Index k) { x(rs(k)) = buf(k); });
355 }
356 Kokkos::fence();
357 }
358
361 if (nSend_) {
362 IndexView sl = d_sendLeaf_;
363 View<double> buf = d_sendBuf_;
364 Kokkos::parallel_for(
365 "lh::pack3", Kokkos::RangePolicy<ExecSpace>(0, nSend_), KOKKOS_LAMBDA(const Index p) {
366 const Index l = sl(p);
367 buf(p * 3 + 0) = x0(l);
368 buf(p * 3 + 1) = x1(l);
369 buf(p * 3 + 2) = x2(l);
370 });
371 }
372 transfer(3, tag);
373 if (nRecv_) {
374 IndexView rs = d_recvSlot_;
375 View<double> buf = d_recvBuf_;
376 Kokkos::parallel_for(
377 "lh::scatter3", Kokkos::RangePolicy<ExecSpace>(0, nRecv_), KOKKOS_LAMBDA(const Index k) {
378 const Index s = rs(k);
379 x0(s) = buf(k * 3 + 0);
380 x1(s) = buf(k * 3 + 1);
381 x2(s) = buf(k * 3 + 2);
382 });
383 }
384 Kokkos::fence();
385 }
386
387 private:
389 void transfer(int width, int tag) const {
391 if (nSend_ && !aware)
392 Kokkos::deep_copy(h_sendBuf_, d_sendBuf_);
393 Kokkos::fence(); // send buffer (host-staged or device) ready before MPI reads it
394 double* sendBase = aware ? d_sendBuf_.data() : h_sendBuf_.data();
395 double* recvBase = aware ? d_recvBuf_.data() : h_recvBuf_.data();
396 std::vector<MPI_Request> reqs;
397 reqs.reserve(recvRanks_.size() + sendRanks_.size());
398 for (std::size_t k = 0; k < recvRanks_.size(); ++k) {
399 reqs.emplace_back();
400 MPI_Irecv(recvBase + static_cast<std::size_t>(recvOff_[k]) * width,
401 recvCounts_[k] * width * static_cast<int>(sizeof(double)), MPI_BYTE, recvRanks_[k],
402 tag, comm_, &reqs.back());
403 }
404 for (std::size_t k = 0; k < sendRanks_.size(); ++k) {
405 reqs.emplace_back();
406 MPI_Isend(sendBase + static_cast<std::size_t>(sendOff_[k]) * width,
407 sendCounts_[k] * width * static_cast<int>(sizeof(double)), MPI_BYTE, sendRanks_[k],
408 tag, comm_, &reqs.back());
409 }
410 if (!reqs.empty())
411 MPI_Waitall(static_cast<int>(reqs.size()), reqs.data(), MPI_STATUSES_IGNORE);
412 if (nRecv_ && !aware)
413 Kokkos::deep_copy(d_recvBuf_, h_recvBuf_);
414 }
415
416 MPI_Comm comm_ = MPI_COMM_NULL;
417 Index nLocal_ = 0, nSend_ = 0, nRecv_ = 0;
418 IndexView d_sendLeaf_, d_recvSlot_;
419 std::vector<int> sendRanks_, sendCounts_, sendOff_, recvRanks_, recvCounts_, recvOff_;
420 View<double> d_sendBuf_, d_recvBuf_;
421 HostView<double> h_sendBuf_, h_recvBuf_;
422};
423
424#endif // KOKKOS_INLINE_FUNCTION
425
426} // namespace peclet::core::amr
427
428#endif // PECLET_CORE_HAVE_MORTON
429#endif // PECLET_CORE_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.
const std::array< bool, Dim > & periodic() 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 > & globalFineSize() const
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 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 kPending
resolve(): queued as a miss (call resolveMisses)
Definition leaf_halo.hpp:69
void exchangeHost(std::vector< double > &x, int tag=45) const
Host exchange: refresh x[nLocal, nLocal+nGhost) from the owners (x sized extendedSize()).
Index lookup(const CoordArr &gc) const
Const lookup against the FINALIZED registry (no miss registration): wrapped probe → local leaf or cac...
Index resolve(const CoordArr &gc)
Resolve a wrapped global fine coordinate to an extended slot: [0, nLocal) = the covering LOCAL leaf; ...
typename DO::Coord Coord
Definition leaf_halo.hpp:66
DistributedOctree< Dim, Bits > DO
Definition leaf_halo.hpp:64
Index lookupGlobal(std::array< long, Dim > g) const
static constexpr Index kNone
resolveGlobal(): exits a non-periodic axis
Definition leaf_halo.hpp:70
int level(Index slot) const
Covering-leaf level of an extended slot (local octree level convention: 0 = finest cell).
const DO::GatherHaloTopology & topology() const
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
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 resolveGlobal(std::array< long, Dim > g)
Wrap + resolve from unbounded long coords; kNone on a non-periodic exit.
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...
std::array< Coord, Dim > CoordArr
Definition leaf_halo.hpp:67
#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_LONG
Definition mpi_stub.hpp:39
int MPI_Irecv(void *, int, MPI_Datatype, int, int, MPI_Comm, MPI_Request *)
Definition mpi_stub.hpp:101
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
bool gpuAwareMpi()
Whether to hand DEVICE pointers straight to MPI (GPU-aware MPI) instead of host-staging.
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