include/pops/mesh/boundary/fill_boundary.hpp Source FileΒΆ

adc_cpp: include/pops/mesh/boundary/fill_boundary.hpp Source File
adc_cpp 0.3.0
Model-free C++23 core for coupled hyperbolic-elliptic systems on adaptive (AMR) meshes, with MPI and GPU (Kokkos) backends
fill_boundary.hpp
Go to the documentation of this file.
1
17
18#pragma once
19
27
28#include <cstdint>
29#include <memory>
30#include <utility>
31#include <vector>
32
33namespace pops {
34
38 bool x = false;
39 bool y = false;
40};
41
42namespace detail {
43
44// NAMED FUNCTORS (not POPS_HD lambdas) for the halo-exchange kernels. Same reasons as the rest of the
45// elliptic/mesh path (#93, recipe #64): fill_boundary is first instantiated from the MG V-cycle pulled
46// from an external TU; an extended lambda there stalls device kernel emission under nvcc (-O Release
47// without -g). Strictly identical body -> bit-identical CPU and device.
51 int sx, sy, c;
52 POPS_HD void operator()(int i, int j) const { d(i, j, c) = s(i - sx, j - sy, c); }
53};
54
55// dst(i, j, c) = src(i - sx, j - sy, c) for (i, j) in region.
56inline void copy_shifted(Fab2D& dst, const Fab2D& src, const Box2D& region, int sx, int sy,
57 int ncomp) {
58 Array4 d = dst.array();
59 ConstArray4 s = src.const_array();
60 for (int c = 0; c < ncomp; ++c)
61 for_each_cell(region, CopyShiftedKernel{d, s, sx, sy, c});
62}
63
64// Pack of a send job: sb[b0 + c*rsz + off] = s(i - sx, jc - sy, c), off = (jc-lo1)*rnx + (i-lo0).
65struct PackKernel {
68 std::int64_t b0, rsz;
69 int lo0, lo1, rnx, sx, sy, ncl;
70 POPS_HD void operator()(int i, int jc) const {
71 const std::int64_t off = static_cast<std::int64_t>(jc - lo1) * rnx + (i - lo0);
72 for (int c = 0; c < ncl; ++c)
73 sb[b0 + static_cast<std::int64_t>(c) * rsz + off] = s(i - sx, jc - sy, c);
74 }
75};
76
77// Unpack of a receive job: d(i, jc, c) = rb[b0 + c*rsz + off], off = (jc-lo1)*rnx + (i-lo0).
79 const Real* rb;
81 std::int64_t b0, rsz;
82 int lo0, lo1, rnx, ncl;
83 POPS_HD void operator()(int i, int jc) const {
84 const std::int64_t off = static_cast<std::int64_t>(jc - lo1) * rnx + (i - lo0);
85 for (int c = 0; c < ncl; ++c)
86 d(i, jc, c) = rb[b0 + static_cast<std::int64_t>(c) * rsz + off];
87 }
88};
89
90// Enumerates the halo schedule for (mf layout, per, domain): the BoxHash build + the local (dst AND
91// src local) and, under MPI with n_ranks()>1, the global (cross-rank send/recv) job lists. This is
92// the per-call work that ADC-260 hoists out of fill_boundary_begin; it runs ONCE per distinct
93// (layout, Periodicity, domain) and is then replayed from the cache. Jobs are produced in the SAME
94// deterministic order as the legacy inline loops (local: li x shifts x sorted gB; global: gF x
95// shifts x sorted gB), so the packed buffers stay bit-identical and the per-rank send/recv lists
96// stay aligned. Bumps the build counter (cache-engagement test hook).
97inline void build_halo_schedule(const MultiFab& mf, const Box2D& domain, Periodicity per,
98 HaloSchedule& sched) {
99 ++halo_schedule_build_counter();
100 const int ng = mf.n_grow();
101 const int Lx = domain.nx();
102 const int Ly = domain.ny();
103 const BoxArray& ba = mf.box_array();
104
105 std::vector<int> sxv = {0};
106 if (per.x) {
107 sxv.push_back(Lx);
108 sxv.push_back(-Lx);
109 }
110 std::vector<int> syv = {0};
111 if (per.y) {
112 syv.push_back(Ly);
113 syv.push_back(-Ly);
114 }
115 std::vector<std::pair<int, int>> shifts;
116 for (int sx : sxv)
117 for (int sy : syv)
118 shifts.push_back({sx, sy});
119
120 // spatial hash: restricts the neighbor-box search (see box_hash.hpp).
121 const BoxHash hash(ba, suggest_bin(ba));
122
123 // --- local jobs (local dst AND local src) ---
124 for (int li = 0; li < mf.local_size(); ++li) {
125 const int gF = mf.global_index(li);
126 const Box2D gbox = mf.fab(li).box().grow(ng);
127 for (auto [sx, sy] : shifts) {
128 const Box2D Q = gbox.shift(0, -sx).shift(1, -sy);
129 for (int gB : hash.query(Q)) {
130 if (gB == gF && sx == 0 && sy == 0)
131 continue; // self, without shift
132 const int srcLocal = mf.local_index_of(gB);
133 if (srcLocal < 0)
134 continue; // non-local src -> MPI below
135 const Box2D region = gbox.intersect(ba[gB].shift(0, sx).shift(1, sy));
136 if (region.empty())
137 continue;
138 sched.local.push_back({gB, gF, sx, sy, region});
139 }
140 }
141 }
142
143#ifdef POPS_HAS_MPI
144 const int np = n_ranks();
145 if (np > 1) {
146 const int me = my_rank();
147 const DistributionMapping& dm = mf.dmap();
148 sched.send.assign(np, {});
149 sched.recv.assign(np, {});
150 // deterministic global enumeration: (dst gF) x shifts x hash candidates (gB sorted). Identical
151 // on all ranks -> aligned send/recv lists.
152 for (int gF = 0; gF < ba.size(); ++gF) {
153 const int od = dm[gF];
154 const Box2D gbox = ba[gF].grow(ng);
155 for (auto [sx, sy] : shifts) {
156 const Box2D Q = gbox.shift(0, -sx).shift(1, -sy);
157 for (int gB : hash.query(Q)) {
158 if (gB == gF && sx == 0 && sy == 0)
159 continue;
160 const int os = dm[gB];
161 if (od != me && os != me)
162 continue;
163 if (od == me && os == me)
164 continue;
165 const Box2D region = gbox.intersect(ba[gB].shift(0, sx).shift(1, sy));
166 if (region.empty())
167 continue;
168 if (os == me)
169 sched.send[od].push_back({gB, gF, sx, sy, region});
170 else
171 sched.recv[os].push_back({gB, gF, sx, sy, region});
172 }
173 }
174 }
175 }
176#endif
177}
178
179// Returns the cached schedule for (mf layout, per, domain), building and memoizing it on first use.
180inline std::shared_ptr<const HaloSchedule> get_halo_schedule(const MultiFab& mf,
181 const Box2D& domain, Periodicity per) {
182 HaloScheduleCache& cache = mf.halo_cache();
183 if (std::shared_ptr<const HaloSchedule> hit = cache.find(per.x, per.y, domain))
184 return hit;
185 std::shared_ptr<HaloSchedule> s = cache.add();
186 s->per_x = per.x;
187 s->per_y = per.y;
188 s->domain = domain;
189 build_halo_schedule(mf, domain, per, *s);
190 return s;
191}
192
193} // namespace detail
194
202 std::shared_ptr<const HaloSchedule> sched; // replayed plan (null if mf has no ghost)
203#ifdef POPS_HAS_MPI
204 // Buffers in PINNED HOST memory (comm_allocator = Kokkos::SharedHostPinnedSpace under Kokkos,
205 // std::allocator otherwise), NOT managed. The pack/unpack in for_each (device under Kokkos)
206 // writes/reads directly into them since pinned host is device-accessible; BUT the pointer passed to
207 // MPI is seen as HOST (cuPointerGetAttribute = HOST), so a CUDA-aware MPI (BTL smcuda) does NOT
208 // attempt CUDA IPC on it. A managed/UVM pointer, on the other hand, triggered IPC, which DEADLOCKS
209 // between two GPUs isolated by cgroup (srun --gpus-per-task=1: each rank sees only its GPU as device
210 // 0, cuIpcOpenMemHandle of the peer's buffer impossible). See core/allocator.hpp (comm_allocator).
211 std::vector<std::vector<Real, comm_allocator<Real>>> sbuf, rbuf; // alive until end
212 std::vector<MPI_Request> reqs;
213 int nc = 0;
214#endif
215};
216
220inline HaloExchange fill_boundary_begin(MultiFab& mf, const Box2D& domain, Periodicity per = {}) {
221 HaloExchange h;
222 const int ng = mf.n_grow();
223 if (ng == 0)
224 return h;
225 const int nc = mf.ncomp();
226 // memoized schedule (BoxHash + enumeration) for this (layout, Periodicity, domain).
227 const std::shared_ptr<const HaloSchedule> sched = detail::get_halo_schedule(mf, domain, per);
228 h.sched = sched;
229
230 // --- local copies (local dst AND local src), replayed from the cached plan ---
231 for (const HaloJob& j : sched->local) {
232 Fab2D& dst = mf.fab(mf.local_index_of(j.dst));
233 const Fab2D& src = mf.fab(mf.local_index_of(j.src));
234 detail::copy_shifted(dst, src, j.region, j.sx, j.sy, nc);
235 }
236
237#ifdef POPS_HAS_MPI
238 if (n_ranks() <= 1)
239 return h;
240 const int np = n_ranks();
241 h.nc = nc;
242
243 auto buf_size = [&](const std::vector<HaloJob>& js) {
244 std::int64_t n = 0;
245 for (const auto& j : js)
246 n += j.region.num_cells() * nc;
247 return n;
248 };
249 h.sbuf.assign(np, {});
250 h.rbuf.assign(np, {});
251 // device PACK (for_each, parallel under Kokkos) into the pinned host buffers. Per-job layout:
252 // c-major then (jj, ii), IDENTICAL to the old k++ order -> buffer bit-identical to the host path
253 // (the CPU MPI ctests stay bit-identical at np=1/2/4). The peer rank enumerates in the same order,
254 // so sbuf[A->B] and rbuf[B<-A] align without negotiating sizes.
255 for (int r = 0; r < np; ++r) {
256 const std::vector<HaloJob>& send_r = sched->send[r];
257 if (send_r.empty())
258 continue;
259 h.sbuf[r].resize(buf_size(send_r));
260 Real* sb = h.sbuf[r].data();
261 std::int64_t base = 0;
262 for (const auto& jb : send_r) {
263 const ConstArray4 s = mf.fab(mf.local_index_of(jb.src)).const_array();
264 const int lo0 = jb.region.lo[0], lo1 = jb.region.lo[1], rnx = jb.region.nx();
265 const std::int64_t rsz = static_cast<std::int64_t>(rnx) * jb.region.ny();
266 const int sx = jb.sx, sy = jb.sy, ncl = nc;
267 const std::int64_t b0 = base;
268 for_each_cell(jb.region, detail::PackKernel{sb, s, b0, rsz, lo0, lo1, rnx, sx, sy, ncl});
269 base += rsz * nc;
270 }
271 }
272 for (int r = 0; r < np; ++r) // allocate the receive buffers
273 if (!sched->recv[r].empty())
274 h.rbuf[r].resize(buf_size(sched->recv[r]));
275 device_fence(); // the pack kernels (and the local copies) must finish before MPI reads sbuf
276 for (
277 int r = 0; r < np;
278 ++r) { // non-blocking posting; MPI receives PINNED HOST pointers (seen HOST, no GPUDirect/CUDA IPC)
279 if (!h.sbuf[r].empty()) {
280 h.reqs.emplace_back();
281 MPI_Isend(h.sbuf[r].data(), static_cast<int>(h.sbuf[r].size()), MPI_DOUBLE, r, 0,
282 MPI_COMM_WORLD, &h.reqs.back());
283 }
284 if (!h.rbuf[r].empty()) {
285 h.reqs.emplace_back();
286 MPI_Irecv(h.rbuf[r].data(), static_cast<int>(h.rbuf[r].size()), MPI_DOUBLE, r, 0,
287 MPI_COMM_WORLD, &h.reqs.back());
288 }
289 }
290#endif
291 return h;
292}
293
298#ifdef POPS_HAS_MPI
299 if (h.reqs.empty())
300 return;
301 MPI_Waitall(static_cast<int>(h.reqs.size()), h.reqs.data(), MPI_STATUSES_IGNORE);
302 // device UNPACK (for_each) from the received PINNED HOST buffers. Waitall guarantees the transfer is
303 // complete; the kernel launched next reads the pinned host (device-accessible, coherent). Replayed
304 // from the SAME cached recv list begin used (h.sched), so base offsets match the sender's layout.
305 const HaloSchedule& sched = *h.sched;
306 for (std::size_t r = 0; r < sched.recv.size(); ++r) {
307 if (h.rbuf[r].empty())
308 continue;
309 const Real* rb = h.rbuf[r].data();
310 std::int64_t base = 0;
311 for (const auto& jb : sched.recv[r]) {
312 Array4 d = mf.fab(mf.local_index_of(jb.dst)).array();
313 const int lo0 = jb.region.lo[0], lo1 = jb.region.lo[1], rnx = jb.region.nx();
314 const std::int64_t rsz = static_cast<std::int64_t>(rnx) * jb.region.ny();
315 const int ncl = h.nc;
316 const std::int64_t b0 = base;
317 for_each_cell(jb.region, detail::UnpackKernel{rb, d, b0, rsz, lo0, lo1, rnx, ncl});
318 base += rsz * ncl;
319 }
320 }
321 // The unpack kernels above are ASYNC (device) and read h.rbuf; comm_allocator (pinned host) frees
322 // IMMEDIATELY on destruction of h (no deferred free like the ManagedArena). So we drain the device
323 // BEFORE the pinned buffers are freed -> no use-after-free.
324 device_fence();
325#else
326 (void)mf;
327 (void)h;
328#endif
329}
330
333inline void fill_boundary(MultiFab& mf, const Box2D& domain, Periodicity per = {}) {
334 HaloExchange h = fill_boundary_begin(mf, domain, per);
335 fill_boundary_end(mf, h);
336}
337
338} // namespace pops
Box2D: the integer index space of a 2D cell-centered Cartesian grid.
BoxHash: spatial hash for fast lookup of boxes intersecting a query.
Ordered list of boxes tiling a level.
Definition box_array.hpp:22
Spatial index of a BoxArray's boxes via a bin grid.
Definition box_hash.hpp:26
Single-grid data on a Box2D: VALID box + ng ghost layers, ncomp components, component-slow layout.
Definition fab2d.hpp:59
ConstArray4 const_array() const
READ handle (POD device-copyable) over this Fab. Valid as long as the Fab lives.
Definition fab2d.hpp:96
Array4 array()
WRITE handle (POD device-copyable) over this Fab. Valid as long as the Fab lives.
Definition fab2d.hpp:91
Small per-MultiFab cache of halo schedules, one entry per distinct (Periodicity, domain).
Definition halo_schedule.hpp:57
std::shared_ptr< HaloSchedule > add()
Appends a fresh, empty schedule and returns it for the caller to populate.
Definition halo_schedule.hpp:70
std::shared_ptr< const HaloSchedule > find(bool px, bool py, const Box2D &dom) const
Existing schedule for (px, py, dom), or nullptr if none is cached yet.
Definition halo_schedule.hpp:60
Field distributed over a level: decomposition (BoxArray) + distribution (DistributionMapping) + ncomp...
Definition multifab.hpp:33
Fab2D & fab(int li)
Local fab at index li (0 <= li < local_size()), for writing.
Definition multifab.hpp:67
int local_index_of(int global) const
LOCAL index of the global box global, or -1 if it is not owned by this rank.
Definition multifab.hpp:75
int ncomp() const
Number of components.
Definition multifab.hpp:60
HaloScheduleCache & halo_cache() const
Internal (ADC-260): memoized halo-exchange schedule used by fill_boundary.
Definition multifab.hpp:100
int n_grow() const
Number of ghost layers.
Definition multifab.hpp:62
const BoxArray & box_array() const
GLOBAL decomposition of the level (all boxes, all ranks).
Definition multifab.hpp:56
Parallel seam: minimal MPI abstraction (rank/size + collectives) with serial fallback.
Fab2D: single-grid data on a Box2D (in-house equivalent of AMReX's FArrayBox); Array4 / ConstArray4: ...
for_each_cell and reductions: the parallelism SEAM over the cells of a Box2D; sync_host / sync_device...
HaloSchedule: memoized intra-level halo-exchange plan for fill_boundary (ADC-260).
MultiFab: a field DISTRIBUTED over a level (equivalent of AMReX's MultiFab).
void copy_shifted(Fab2D &dst, const Fab2D &src, const Box2D &region, int sx, int sy, int ncomp)
Definition fill_boundary.hpp:56
std::shared_ptr< const HaloSchedule > get_halo_schedule(const MultiFab &mf, const Box2D &domain, Periodicity per)
Definition fill_boundary.hpp:180
void build_halo_schedule(const MultiFab &mf, const Box2D &domain, Periodicity per, HaloSchedule &sched)
Definition fill_boundary.hpp:97
Definition amr_hierarchy.hpp:29
void for_each_cell(const Box2D &b, F f)
Applies f to EACH cell (i, j) of box b (bounds inclusive), via Kokkos::parallel_for (Serial / OpenMP ...
Definition for_each.hpp:138
double Real
Definition types.hpp:30
int n_ranks()
Definition comm.hpp:139
void device_fence()
Device barrier: waits for in-flight kernels to finish before a HOST access to unified memory.
Definition kokkos_env.hpp:43
void fill_boundary(MultiFab &mf, const Box2D &domain, Periodicity per={})
BLOCKING halo exchange: begin then end immediately (no overlap).
Definition fill_boundary.hpp:333
void fill_boundary_end(MultiFab &mf, HaloExchange &h)
Phase 2 (blocking): MPI_Waitall on the transfers posted by begin, then unpacks the received buffers i...
Definition fill_boundary.hpp:297
int my_rank()
Definition comm.hpp:136
HaloExchange fill_boundary_begin(MultiFab &mf, const Box2D &domain, Periodicity per={})
Phase 1 (non-blocking): does the LOCAL halo copies and posts the Isend/Irecv of the distant halos.
Definition fill_boundary.hpp:220
int suggest_bin(const BoxArray &ba)
Recommended bin size for a BoxArray: the largest box extent (at least 1), so that neighboring boxes f...
Definition box_hash.hpp:72
WRITE POD handle (raw pointer + strides) over a Fab2D buffer, indexed by (i, j, c) IN GLOBAL INDICES ...
Definition fab2d.hpp:29
2D integer index space, cell-centered.
Definition box2d.hpp:37
bool empty() const
true if the box contains no cell (hi < lo in one direction).
Definition box2d.hpp:58
Box2D grow(int n) const
Grows the box by n cells in ALL directions (uniform ghost layer).
Definition box2d.hpp:69
POPS_HD int nx() const
Width (direction 0). POPS_HD (called from Geometry::dx() in a device kernel).
Definition box2d.hpp:50
POPS_HD int ny() const
Height (direction 1). POPS_HD (called from Geometry::dy() in a device kernel).
Definition box2d.hpp:52
Box2D shift(int d, int s) const
Translates the box by s cells in direction d (lo and hi shifted by the same s).
Definition box2d.hpp:78
Box2D intersect(const Box2D &o) const
Intersection of the two boxes (possibly empty: hi < lo if they do not overlap).
Definition box2d.hpp:95
READ-only handle (const counterpart of Array4): same layout and same contract (POD device-copyable,...
Definition fab2d.hpp:44
Opaque state of an in-flight halo exchange, returned by fill_boundary_begin and consumed by fill_boun...
Definition fill_boundary.hpp:201
std::shared_ptr< const HaloSchedule > sched
Definition fill_boundary.hpp:202
Memoized schedule for ONE (Periodicity, domain) over a fixed layout.
Definition halo_schedule.hpp:40
std::vector< std::vector< HaloJob > > recv
Definition halo_schedule.hpp:46
Per-direction periodicity: halo wrapping in x and/or y during the exchange (false = open edge,...
Definition fill_boundary.hpp:37
bool x
Definition fill_boundary.hpp:38
bool y
Definition fill_boundary.hpp:39
Definition fill_boundary.hpp:48
int sy
Definition fill_boundary.hpp:51
Array4 d
Definition fill_boundary.hpp:49
int c
Definition fill_boundary.hpp:51
ConstArray4 s
Definition fill_boundary.hpp:50
int sx
Definition fill_boundary.hpp:51
POPS_HD void operator()(int i, int j) const
Definition fill_boundary.hpp:52
Definition fill_boundary.hpp:65
ConstArray4 s
Definition fill_boundary.hpp:67
int rnx
Definition fill_boundary.hpp:69
int ncl
Definition fill_boundary.hpp:69
int lo1
Definition fill_boundary.hpp:69
std::int64_t b0
Definition fill_boundary.hpp:68
std::int64_t rsz
Definition fill_boundary.hpp:68
int sy
Definition fill_boundary.hpp:69
int lo0
Definition fill_boundary.hpp:69
POPS_HD void operator()(int i, int jc) const
Definition fill_boundary.hpp:70
int sx
Definition fill_boundary.hpp:69
Real * sb
Definition fill_boundary.hpp:66
Definition fill_boundary.hpp:78
std::int64_t b0
Definition fill_boundary.hpp:81
POPS_HD void operator()(int i, int jc) const
Definition fill_boundary.hpp:83
int lo1
Definition fill_boundary.hpp:82
const Real * rb
Definition fill_boundary.hpp:79
int rnx
Definition fill_boundary.hpp:82
std::int64_t rsz
Definition fill_boundary.hpp:81
int lo0
Definition fill_boundary.hpp:82
int ncl
Definition fill_boundary.hpp:82
Array4 d
Definition fill_boundary.hpp:80
#define POPS_HD
Definition types.hpp:25