include/pops/mesh/execution/for_each.hpp Source FileΒΆ

adc_cpp: include/pops/mesh/execution/for_each.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
for_each.hpp
Go to the documentation of this file.
1
18
19#pragma once
20
21#include <pops/core/foundation/kokkos_env.hpp> // detail::ensure_kokkos_initialized + device_fence (life cycle)
24
25#include <cstdint> // std::int64_t: cell counts (LLP64 portability, no-op on LP64)
26#include <cstdlib> // getenv / strtol: overridable serial fallback threshold (#165)
27#include <type_traits> // std::is_same_v: compile-time guard host vs device exec space (#165)
28
29#ifndef POPS_HAS_KOKKOS
30// adc_cpp is KOKKOS-ONLY: there is no longer a standalone OpenMP backend nor a manual host loop
31// as a production path. Configure with -DPOPS_USE_KOKKOS=ON (+ -DKokkos_ROOT=...); serial
32// goes through a Kokkos install with Kokkos_ENABLE_SERIAL=ON.
33#error \
34 "adc_cpp is Kokkos-only: for_each_cell requires POPS_HAS_KOKKOS. Configure with -DPOPS_USE_KOKKOS=ON and a Kokkos Serial/OpenMP/Cuda install."
35#endif
36
37#include <Kokkos_Core.hpp>
38
39namespace pops {
40
41// detail::ensure_kokkos_initialized() and device_fence(): defined in pops/core/kokkos_env.hpp
42// (Kokkos life cycle shared with the unified allocator, which must also initialize Kokkos BEFORE
43// its first kokkos_malloc, otherwise the Kokkos build crashes when constructing a Fab).
44
45// SERIAL FALLBACK THRESHOLD for for_each_cell (#165). Under a HOST Kokkos execution space
46// (Serial/OpenMP), launching a Kokkos::parallel_for(MDRangePolicy) on a tiny box pays a
47// fork/join (and the policy construction) that OVERWHELMS the useful work. The multigrid
48// V-cycle descends down to ~2x2/4x4 grids; on those levels the GS smoother, the residual,
49// the restriction/prolongation and the copies chain dozens of parallel_for over a few
50// cells, and this launch overhead DOMINATES the solve time. Below this threshold we run
51// a SEQUENTIAL host loop (internal to the Kokkos path, this is NOT a separate backend),
52// above it we keep Kokkos parallel_for for the fine grids.
53//
54// BIT-IDENTITY. for_each_cell has NO inter-iteration dependency: each f(i, j)
55// writes only cell (i, j) of its destination and reads cells IT DOES NOT WRITE
56// in the same call (the GS smoother is RED-BLACK colored -- one color only reads
57// the other; residual/restriction/prolongation/copies/saxpy write a destination
58// distinct from the source). The result is therefore INDEPENDENT OF the traversal ORDER:
59// the sequential loop yields exactly the same bits as MDRangePolicy<Rank<2>>.
60// The threshold touches ONLY for_each_cell (not the reductions for_each_cell_reduce_*:
61// the Kokkos parallel sum reassociates the addition, so switching them to serial would
62// NOT be bit-identical -- we leave them intact; the max is exact but the smoother itself
63// does go through for_each_cell, where the overhead of the small grids concentrates).
64//
65// Overridable at run time via POPS_FOREACH_SERIAL_THRESHOLD (read once) to
66// resweep the threshold without recompiling; default 4096 (same fork/join vs computation
67// trade-off as the old if() clause of the removed OpenMP path).
68namespace detail {
69inline std::int64_t foreach_serial_threshold() {
70 static const std::int64_t thr = []() -> std::int64_t {
71 if (const char* e = std::getenv("POPS_FOREACH_SERIAL_THRESHOLD")) {
72 char* end = nullptr;
73 const std::int64_t v = std::strtol(e, &end, 10);
74 if (end != e && v >= 0)
75 return v;
76 }
77 return 4096;
78 }();
79 return thr;
80}
81} // namespace detail
82
83// ---------------------------------------------------------------------------
84// Data residency: sync_host() / sync_device(). The COHERENCE seam, the
85// counterpart of for_each_cell for host accesses.
86//
87// Today the Fab storage lives in UNIFIED memory (Kokkos::SharedSpace, cf.
88// allocator.hpp): the same buffer serves the host code (operator(), loops) AND
89// the device kernels. Coherence therefore does NOT require a copy, only
90// ORDERING: a host access must not read/write a buffer while
91// an async kernel still touches it. Until now this ordering was laid down by
92// hand by scattered device_fence() calls, without ever saying WHICH residency one
93// wants to make valid.
94//
95// sync_host()/sync_device() ENCODE that intent:
96// - sync_host(): "I am going to read/write this data FROM THE HOST;
97// make it valid host-side". Under SharedSpace = a
98// targeted device_fence() (wait for in-flight kernels), so
99// host accesses are then race-free (data race).
100// - sync_device(): "I am going to read/write this data FROM THE DEVICE
101// (a kernel); make it valid device-side". Under
102// SharedSpace the preceding host writes are visible
103// from the device without a barrier (no async host pipeline to
104// drain), so it is a REAL no-op today; the
105// function exists to MARK the intent at the call site.
106//
107// SEMANTICS UNDER SHAREDSPACE (current state): these calls are at most a fence,
108// never a copy. The behavior therefore stays BIT-IDENTICAL to the old code
109// (sync_host == the old device_fence() laid before a host access; sync_device
110// == nothing). This is deliberately SCAFFOLDING: under unified memory there is
111// nothing else to do.
112//
113// FUTURE NON-UNIFIED PATH (separate host/device buffers + deep_copy): this is WHERE
114// the migration would plug in. sync_host() would do a Kokkos::deep_copy
115// device->host (and a fence) if the device is the last residency written;
116// sync_device() a deep_copy host->device in the other direction. Tracking "who
117// owns the up-to-date data" (per-residency dirty flag) would live on the MultiFab,
118// not here: this seam stays stateless, the MultiFab overloads carry the state.
119// Since all the host-access sites already go through sync_host(), switching to
120// that path will NOT touch the operators, exactly as for_each_cell
121// isolates the CPU -> GPU switch from the call sites.
122
125inline void sync_host() {
126 device_fence();
127}
128
132inline void sync_device() {}
133
137template <class F>
138void for_each_cell(const Box2D& b, F f) {
139 // SMALL BOXES (#165): under a HOST Kokkos execution space (Serial/OpenMP), the
140 // fork/join of a parallel_for on a tiny grid (coarse V-cycle levels,
141 // ~2x2..32x32) overwhelms the computation. We then run a sequential host loop (internal
142 // to the Kokkos path). BIT-IDENTICAL: no inter-iteration dependency (cf. the threshold note),
143 // so the order affects no bit.
144 //
145 // DEVICE GUARD (if constexpr): the serial fallback is taken ONLY if the default execution space
146 // of Kokkos IS the host space (Serial/OpenMP). Under a DEVICE space (Cuda
147 // on a CUDA device), DefaultExecutionSpace != DefaultHostExecutionSpace: the host loop
148 // would run on the CPU while the preceding device kernels are in flight (no
149 // fence laid here) -- data race. We therefore keep parallel_for on device WHATEVER
150 // THE size -> GPU path STRICTLY unchanged (the if constexpr evaporates at
151 // compile time, zero overhead). Under SharedSpace + host execution, the loop is
152 // race-free: the existing coherence seams (gs_rb_sweep lays its device_fence around the
153 // sweeps, sync_host before the host accesses) stay in place and unchanged.
154 if constexpr (std::is_same_v<Kokkos::DefaultExecutionSpace, Kokkos::DefaultHostExecutionSpace>) {
155 const std::int64_t n_cells =
156 static_cast<std::int64_t>(b.hi[0] - b.lo[0] + 1) * (b.hi[1] - b.lo[1] + 1);
157 if (n_cells < detail::foreach_serial_threshold()) {
158 for (int j = b.lo[1]; j <= b.hi[1]; ++j)
159 for (int i = b.lo[0]; i <= b.hi[0]; ++i)
160 f(i, j);
161 return;
162 }
163 }
164 detail::ensure_kokkos_initialized();
165 // IndexType<int>: SIGNED indices. Ghost boxes have negative low
166 // bounds (e.g. lo = -ng for copy_shifted); without an explicit signed type,
167 // MDRangePolicy rejects the bound -1 (implicit conversion deemed unsafe).
168 Kokkos::parallel_for("pops_for_each_cell",
169 Kokkos::MDRangePolicy<Kokkos::Rank<2>, Kokkos::IndexType<int>>(
170 {b.lo[0], b.lo[1]}, {b.hi[0] + 1, b.hi[1] + 1}),
171 f);
172}
173
174// Device reductions: the reducing counterpart of for_each_cell. Same constraints
175// on the functor (device-callable POD, taken by value, captures a ConstArray4,
176// never the Fab); it receives (i, j) and returns the value to accumulate. The seam carries
177// the device ordering: under Kokkos the scalar is ready on return without a
178// prior device_fence() (parallel_reduce is blocking host-side and orders itself
179// after the parallel_for already submitted in the same space).
180//
181// IMPORTANT FP CHOICE. A true parallel reduction reassociates floating-point
182// addition (non-associative in IEEE754): the result of the sum depends on
183// the traversal order.
184// - SUM: Kokkos::Sum, DETERMINISTIC per-tile reduction (no floating-point
185// atomics). Two calls on identical data return exactly the
186// same bit -> idempotence (sum_unchanged) holds. But the per-tile order
187// DIFFERS from a lexicographic sum: the value is NOT bit-identical to
188// a hand-written (i, j) loop. Since Kokkos is the only backend, this
189// holds for ALL spaces (Serial, OpenMP, Cuda).
190// - MAX: Kokkos::Max, exact everywhere (max is associative/commutative and
191// rounding-free in IEEE754) -> bit-identical across Kokkos spaces.
192// Summary: the SUM is deterministic-per-tile (idempotent) but reassociated; the
193// MAX (norm_inf) is exact.
194
198template <class F>
200 detail::ensure_kokkos_initialized();
201 Real result = 0;
202 Kokkos::parallel_reduce(
203 "pops_reduce_sum",
204 Kokkos::MDRangePolicy<Kokkos::Rank<2>, Kokkos::IndexType<int>>({b.lo[0], b.lo[1]},
205 {b.hi[0] + 1, b.hi[1] + 1}),
206 KOKKOS_LAMBDA(int i, int j, Real& acc) { acc += f(i, j); }, Kokkos::Sum<Real>{result});
207 return result; // blocking host-side: valid on return, without device_fence()
208}
209
212template <class F>
214 detail::ensure_kokkos_initialized();
215 Real result = 0;
216 Kokkos::parallel_reduce(
217 "pops_reduce_max",
218 Kokkos::MDRangePolicy<Kokkos::Rank<2>, Kokkos::IndexType<int>>({b.lo[0], b.lo[1]},
219 {b.hi[0] + 1, b.hi[1] + 1}),
220 KOKKOS_LAMBDA(int i, int j, Real& acc) {
221 const Real v = f(i, j);
222 if (v > acc)
223 acc = v;
224 },
225 Kokkos::Max<Real>{result});
226 return result; // exact max (associative/commutative IEEE754), no fence
227}
228
229// MAX variant with a REDUCING FUNCTOR: @p f is passed DIRECTLY to Kokkos::parallel_reduce and
230// receives (i, j, Real& acc) to update acc (acc = max(acc, value)). Unlike
231// for_each_cell_reduce_max, NO extended lambda wraps @p f: this is the device-clean path
232// for a Model-template kernel instantiated from an EXTERNAL TRANSLATION UNIT (add_compiled_model),
233// where nvcc does not reliably emit an extended lambda (cf. the named functors of spatial_operator.hpp).
234// Determinism and bit-exactness IDENTICAL to for_each_cell_reduce_max (same Kokkos::Max): only the
235// carrier of the computation changes (named functor instead of a lambda wrapper).
239template <class F>
240Real reduce_max_cell(const Box2D& b, F f) {
241 detail::ensure_kokkos_initialized();
242 Real result = 0;
243 Kokkos::parallel_reduce("pops_reduce_max_cell",
244 Kokkos::MDRangePolicy<Kokkos::Rank<2>, Kokkos::IndexType<int>>(
245 {b.lo[0], b.lo[1]}, {b.hi[0] + 1, b.hi[1] + 1}),
246 f, Kokkos::Max<Real>{result});
247 return result;
248}
249
250// MIN variant: exact counterpart of reduce_max_cell for Kokkos::Min (dt_hotspot diagnostic,
251// ADC-182: reduction of the smallest index encoded among the cells that equal the max).
252template <class F>
253Real reduce_min_cell(const Box2D& b, F f) {
254 detail::ensure_kokkos_initialized();
255 Real result = 0;
256 Kokkos::parallel_reduce("pops_reduce_min_cell",
257 Kokkos::MDRangePolicy<Kokkos::Rank<2>, Kokkos::IndexType<int>>(
258 {b.lo[0], b.lo[1]}, {b.hi[0] + 1, b.hi[1] + 1}),
259 f, Kokkos::Min<Real>{result});
260 return result;
261}
262
263// SUM variant with a REDUCING FUNCTOR: exact counterpart of reduce_max_cell for Kokkos::Sum. @p f
264// receives (i, j, Real& acc) and accumulates (acc += value), passed DIRECTLY to parallel_reduce WITHOUT
265// a wrapping extended lambda (unlike for_each_cell_reduce_sum, which lays one). This is
266// the device-clean path required by a kernel instantiated from an EXTERNAL TRANSLATION UNIT (the
267// Krylov solver drawn from the native harness/loader): nvcc does not reliably emit an extended lambda
268// first-instantiated cross-TU (cf. the named functors of mf_arith.hpp / spatial_operator.hpp).
269// Determinism and FP IDENTICAL to for_each_cell_reduce_sum: same per-tile deterministic Kokkos::Sum.
273template <class F>
274Real reduce_sum_cell(const Box2D& b, F f) {
275 detail::ensure_kokkos_initialized();
276 Real result = 0;
277 Kokkos::parallel_reduce("pops_reduce_sum_cell",
278 Kokkos::MDRangePolicy<Kokkos::Rank<2>, Kokkos::IndexType<int>>(
279 {b.lo[0], b.lo[1]}, {b.hi[0] + 1, b.hi[1] + 1}),
280 f, Kokkos::Sum<Real>{result});
281 return result;
282}
283
284} // namespace pops
Box2D: the integer index space of a 2D cell-centered Cartesian grid.
Shared Kokkos lifecycle: lazy init + device barrier.
std::int64_t foreach_serial_threshold()
Definition for_each.hpp:69
Definition amr_hierarchy.hpp:29
Real for_each_cell_reduce_sum(const Box2D &b, F f)
SUM reduction of f(i, j) over box b.
Definition for_each.hpp:199
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
Real reduce_min_cell(const Box2D &b, F f)
Definition for_each.hpp:253
void sync_host()
Makes the HOST residency valid before a host access (read/write from the host).
Definition for_each.hpp:125
Real for_each_cell_reduce_max(const Box2D &b, F f)
MAX reduction of f(i, j) over box b.
Definition for_each.hpp:213
void device_fence()
Device barrier: waits for in-flight kernels to finish before a HOST access to unified memory.
Definition kokkos_env.hpp:43
Real reduce_sum_cell(const Box2D &b, F f)
SUM reduction with a REDUCING FUNCTOR: f receives (i, j, Real& acc) and accumulates,...
Definition for_each.hpp:274
Real reduce_max_cell(const Box2D &b, F f)
MAX reduction with a REDUCING FUNCTOR: f receives (i, j, Real& acc) and updates acc,...
Definition for_each.hpp:240
void sync_device()
Marks a DEVICE residency (upcoming kernel).
Definition for_each.hpp:132
2D integer index space, cell-centered.
Definition box2d.hpp:37
int hi[2]
Definition box2d.hpp:39
int lo[2]
Definition box2d.hpp:38
Base scalar types and the POPS_HD macro (host+device portability).