ryujin 2.1.1 revision ee5cbcbf2346c1299c942d0e1f13b46449973c18
Loading...
Searching...
No Matches
loop.h
Go to the documentation of this file.
1//
2// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception or LGPL-2.1-or-later
3// Copyright (C) 2026 by the ryujin authors
4//
5
6#pragma once
7
8#include <compile_time_options.h>
9#include <computing_timer.h>
10#include <convenience_macros.h>
11#include <instrumentation.h>
12#include <simd.h>
13
14#include <deal.II/base/config.h>
15#include <deal.II/base/memory_space.h>
16#include <deal.II/base/parallel.h>
17
18#include <mutex>
19#include <string>
20#include <tuple>
21
22#ifdef WITH_OPENMP
23#include <omp.h>
24#endif
25
26namespace ryujin
27{
28 /*
29 * A thread-parallelized and vectorized loop running on the CPU. The loop
30 * traverses the index range [left, internal) SIMD vectorized stepping
31 * forward with a stride size equal to the number of packed
32 * doubles/singles that the loop body operates on at the same time. For
33 * the remainder of the index range, i.e., [internal, right) a serial
34 * loop is invoked.
35 *
36 * @note the index internal is rounded down to the next integer multiple
37 * of the SIMD stride size.
38 *
39 * @note Here, @p body is a functor that must accept a "sentinel" type as
40 * first argument and the current index i as last argument. Additional
41 * `args` may be specified in the cpu_simd_loop() invocation that will be
42 * forwarded to the loop body:
43 * `body(Number(), std::forward<Args>(args)..., i);`
44 */
45 template <typename ScalarNumber, typename Functor, typename... Args>
46 inline void cpu_simd_loop(const std::string &region_name [[maybe_unused]],
47 const Functor &body,
48 const unsigned int left,
49 const unsigned int internal,
50 const unsigned int right,
51 Args &&...args)
52 {
53 Assert(left <= internal && internal <= right,
54 dealii::ExcMessage("Invalid index range: it must hold left <= "
55 "internal, internal <= right"));
56
57 if (!region_name.empty()) {
58 LIKWID_MARKER_START(region_name.c_str());
59 }
60
61 using VA = dealii::VectorizedArray<ScalarNumber>;
62
63 constexpr unsigned int stride_size = get_stride_size<VA>;
64 const unsigned int regular =
65 left + (internal - left) / stride_size * stride_size;
66
67#if defined(WITH_OPENMP)
68 /* Variant using OpenMP: */
69
70 RYUJIN_PRAGMA(omp parallel default(shared))
71 {
72 /* SIMD vectorized loop: */
73 RYUJIN_PRAGMA(omp for nowait)
74 for (unsigned int i = left; i < regular; i += stride_size)
75 body(VA(), std::forward<Args>(args)..., i);
76
77 /* Serial loop: */
78 RYUJIN_PRAGMA(omp for)
79 for (unsigned int i = regular; i < right; i += 1)
80 body(ScalarNumber(), std::forward<Args>(args)..., i);
81 }
82
83#elif defined(WITH_DEAL_II_THREADS)
84 /* Variant using dealii's parallel for: */
85 {
86 /*
87 * We have to ensure that the deal.II routine only schedules a
88 * workload that is divisible by stride_size.
89 */
90 Assert((regular - left) % stride_size == 0, dealii::ExcInternalError());
91 dealii::parallel::apply_to_subranges(
92 0,
93 (regular - left) / stride_size,
94 [&](const unsigned int begin, const unsigned int end) {
95 /* SIMD vectorized loop: */
96 for (unsigned int i = begin; i < end; ++i)
97 body(VA(), std::forward<Args>(args)..., left + stride_size * i);
98 },
99 1000);
100
101 dealii::parallel::apply_to_subranges(
102 regular,
103 right,
104 [&](const unsigned int begin, const unsigned int end) {
105 /* Serial loop: */
106 for (unsigned int i = begin; i < end; ++i)
107 body(ScalarNumber(), std::forward<Args>(args)..., i);
108 },
109 1000);
110 }
111
112#else
113 /* Execute loops in serial: */
114 {
115 /* SIMD vectorized loop: */
116 for (unsigned int i = left; i < regular; i += stride_size)
117 body(VA(), std::forward<Args>(args)..., i);
118
119 /* Serial loop: */
120 for (unsigned int i = regular; i < right; i += 1)
121 body(ScalarNumber(), std::forward<Args>(args)..., i);
122 }
123#endif
124
125 if (!region_name.empty()) {
126 LIKWID_MARKER_STOP(region_name.c_str());
127 }
128 }
129
130
131 /*
132 * A loop running on the device (i.e., in the default memory space). The
133 * loop traverses the index range [left, right) with a suitable Kokkos
134 * parallel_for using a range policy.
135 *
136 * @note The index range [left, internal) that is used for SIMD
137 * vectorization in cpu_simd_loop() is currently ignored: On the device
138 * every "lane" operates on a scalar value and the loop body is thus
139 * always called with a scalar sentinel type.
140 *
141 * @note Here, @p body is a functor that must accept a "sentinel" type as
142 * first argument and the current index i as last argument. Additional
143 * `args` may be specified in the gpu_loop() invocation that will be
144 * forwarded to the loop body:
145 * `body(ScalarNumber(), std::forward<Args>(args)..., i);`
146 *
147 * @note The loop body (and everything it references) has to be callable
148 * on the device. In particular, @p body and all @p args are copied into
149 * the kernel, meaning that the functor must capture by value and must be
150 * trivially copyable.
151 *
152 * @note The function fences the execution space before returning. It
153 * thus has the same (synchronous) semantics as cpu_simd_loop().
154 */
155 template <typename ScalarNumber, typename Functor, typename... Args>
156 inline void gpu_loop(const std::string &region_name,
157 const Functor &body,
158 const unsigned int left,
159 const unsigned int internal [[maybe_unused]],
160 const unsigned int right,
161 Args &&...args)
162 {
163 DeviceTimer::Scope scope;
164
165 Assert(left <= internal && internal <= right,
166 dealii::ExcMessage("Invalid index range: it must hold left <= "
167 "internal, internal <= right"));
168
169 using MemorySpace = dealii::MemorySpace::Default;
170 using ExecutionSpace = typename MemorySpace::kokkos_space::execution_space;
171 using Policy =
172 Kokkos::RangePolicy<ExecutionSpace, Kokkos::IndexType<unsigned int>>;
173
174 const auto exec = ExecutionSpace{};
175
176 /*
177 * Note: nvcc does not allow an extended __host__ __device__ lambda to
178 * capture an element of a parameter pack. We thus pack all arguments
179 * into a tuple and unpack them again in the loop body.
180 */
181 const auto packed_args = std::make_tuple(std::forward<Args>(args)...);
182
183 if (!region_name.empty()) {
184 NVTX_MARKER_START(region_name.c_str());
185 }
186
187 Kokkos::parallel_for(
188 region_name,
189 Policy(exec, left, right),
190 KOKKOS_LAMBDA(const unsigned int i) {
191 std::apply(
192 [&](const auto &...unpacked) {
193 body(ScalarNumber(), unpacked..., i);
194 },
195 packed_args);
196 });
197
198 exec.fence();
199
200 if (!region_name.empty()) {
201 NVTX_MARKER_STOP(region_name.c_str());
202 }
203 }
204
205
206 /*
207 * A loop running either on the CPU, or on the device depending on the
208 * selected memory space: For dealii::MemorySpace::Host the loop is
209 * dispatched to cpu_simd_loop(), and for dealii::MemorySpace::Default to
210 * gpu_loop().
211 *
212 * @note Here, @p body is a functor that must accept a "sentinel" type as
213 * first argument and the current index i as last argument. Additional
214 * `args` may be specified in the loop() invocation that will be forwarded
215 * to the loop body.
216 */
217 template <typename MemorySpace,
218 typename ScalarNumber,
219 typename Functor,
220 typename... Args>
221 inline void loop(const std::string &region_name,
222 const Functor &body,
223 const unsigned int left,
224 const unsigned int internal,
225 const unsigned int right,
226 Args &&...args)
227 {
228 using HostSpace = dealii::MemorySpace::Host;
229 using DefaultSpace = dealii::MemorySpace::Default;
230 static_assert(std::is_same_v<MemorySpace, HostSpace> ||
231 std::is_same_v<MemorySpace, DefaultSpace>,
232 "Unexpected memory space");
233
234 if constexpr (std::is_same_v<MemorySpace, HostSpace>) {
235 cpu_simd_loop<ScalarNumber>(region_name,
236 body,
237 left,
238 internal,
239 right,
240 std::forward<Args>(args)...);
241 } else {
242 gpu_loop<ScalarNumber>(region_name,
243 body,
244 left,
245 internal,
246 right,
247 std::forward<Args>(args)...);
248 }
249 }
250
251
252 /*
253 * A thread-parallelized reduction loop running on the CPU. The loop
254 * traverses the index range [left, right) and reduces the contributions
255 * of the loop body with the supplied Kokkos @p Reducer (such as
256 * Kokkos::Min, Kokkos::Max, or Kokkos::Sum) into a single value that is
257 * returned.
258 *
259 * @note In contrast to cpu_simd_loop() the loop is not SIMD vectorized:
260 * the loop body is always called with a scalar sentinel type.
261 *
262 * @note Here, @p body is a functor that must accept a "sentinel" type as
263 * first argument, the current index i, and a reference to a thread-local
264 * accumulator as last arguments. Additional `args` may be specified in
265 * the cpu_reduction_loop() invocation that will be forwarded to the loop
266 * body:
267 * `body(ValueType(), std::forward<Args>(args)..., i, local_result);`
268 */
269 template <typename Reducer, typename Functor, typename... Args>
270 inline typename Reducer::value_type
271 cpu_reduction_loop(const std::string &region_name [[maybe_unused]],
272 const Functor &body,
273 const typename Reducer::value_type initial_value,
274 const unsigned int left,
275 const unsigned int right,
276 Args &&...args)
277 {
278 Assert(
279 left <= right,
280 dealii::ExcMessage("Invalid index range: it must hold left <= right"));
281
282 if (!region_name.empty()) {
283 LIKWID_MARKER_START(region_name.c_str());
284 }
285
286 using ValueType = typename Reducer::value_type;
287
288 ValueType result = initial_value;
289
290#if defined(WITH_OPENMP)
291 /* Variant using OpenMP: */
292
293 RYUJIN_PRAGMA(omp parallel default(shared))
294 {
295 ValueType local_result;
296 Reducer(local_result).init(local_result);
297
298 RYUJIN_PRAGMA(omp for nowait)
299 for (unsigned int i = left; i < right; ++i)
300 body(ValueType(), std::forward<Args>(args)..., i, local_result);
301
302 RYUJIN_PRAGMA(omp critical)
303 Reducer(result).join(result, local_result);
304 }
305
306#elif defined(WITH_DEAL_II_THREADS)
307 /* Variant using dealii's parallel for: */
308 {
309 std::mutex mutex;
310
311 dealii::parallel::apply_to_subranges(
312 left,
313 right,
314 [&](const unsigned int begin, const unsigned int end) {
315 ValueType local_result; /* per thread */
316 Reducer(local_result).init(local_result);
317
318 for (unsigned int i = begin; i < end; ++i)
319 body(ValueType(), std::forward<Args>(args)..., i, local_result);
320
321 std::lock_guard<std::mutex> lock(mutex);
322 Reducer(result).join(result, local_result);
323 },
324 1000);
325 }
326
327#else
328 /* Execute loop in serial: */
329 {
330 for (unsigned int i = left; i < right; ++i)
331 body(ValueType(), std::forward<Args>(args)..., i, result);
332 }
333#endif
334
335 if (!region_name.empty()) {
336 LIKWID_MARKER_STOP(region_name.c_str());
337 }
338
339 return result;
340 }
341
342
343 /*
344 * A reduction loop running on the device (i.e., in the default memory
345 * space). The loop traverses the index range [left, right) with a
346 * Kokkos::parallel_reduce using a range policy and the supplied Kokkos
347 * @p Reducer (such as Kokkos::Min, Kokkos::Max, or Kokkos::Sum).
348 *
349 * @note Here, @p body is a functor that must accept a "sentinel" type as
350 * first argument, the current index i, and a reference to a thread-local
351 * accumulator as last arguments. Additional `args` may be specified in
352 * the gpu_reduction_loop() invocation that will be forwarded to the loop
353 * body:
354 * `body(ValueType(), args..., i, local_result);`
355 *
356 * @note The loop body (and everything it references) has to be callable
357 * on the device, see the discussion in gpu_loop().
358 *
359 * @note Kokkos::parallel_reduce() fences the execution space before
360 * returning. The function thus has the same (synchronous) semantics as
361 * cpu_reduction_loop().
362 */
363 template <typename Reducer, typename Functor, typename... Args>
364 inline typename Reducer::value_type
365 gpu_reduction_loop(const std::string &region_name,
366 const Functor &body,
367 const typename Reducer::value_type initial_value,
368 const unsigned int left,
369 const unsigned int right,
370 Args &&...args)
371 {
372 DeviceTimer::Scope scope;
373
374 Assert(
375 left <= right,
376 dealii::ExcMessage("Invalid index range: it must hold left <= right"));
377
378 using ValueType = typename Reducer::value_type;
379 using MemorySpace = dealii::MemorySpace::Default;
380 using ExecutionSpace = typename MemorySpace::kokkos_space::execution_space;
381 using Policy =
382 Kokkos::RangePolicy<ExecutionSpace, Kokkos::IndexType<unsigned int>>;
383
384 const auto exec = ExecutionSpace{};
385
386 ValueType result;
387
388 /*
389 * Note: nvcc does not allow an extended __host__ __device__ lambda to
390 * capture an element of a parameter pack. We thus pack all arguments
391 * into a tuple and unpack them again in the loop body.
392 */
393 const auto packed_args = std::make_tuple(std::forward<Args>(args)...);
394
395 if (!region_name.empty()) {
396 NVTX_MARKER_START(region_name.c_str());
397 }
398
399 Kokkos::parallel_reduce(
400 region_name,
401 Policy(exec, left, right),
402 KOKKOS_LAMBDA(const unsigned int i, ValueType &local_result) {
403 std::apply(
404 [&](const auto &...unpacked) {
405 body(ValueType(), unpacked..., i, local_result);
406 },
407 packed_args);
408 },
409 Reducer(result));
410
411 if (!region_name.empty()) {
412 NVTX_MARKER_STOP(region_name.c_str());
413 }
414
415 ValueType combined = initial_value;
416 Reducer(combined).join(combined, result);
417 return combined;
418 }
419
420
421 /*
422 * A reduction loop running either on the CPU, or on the device depending
423 * on the selected memory space: For dealii::MemorySpace::Host the loop is
424 * dispatched to cpu_reduction_loop(), and for dealii::MemorySpace::Default to
425 * gpu_reduction_loop().
426 *
427 * The operation is selected with a @p Reducer (such as Kokkos::Min,
428 * Kokkos::Max, or Kokkos::Sum) whose value_type also determines the
429 * number type that the loop body accumulates into:
430 * ```
431 * const auto body = [=](auto, unsigned int i, Number &result) {
432 * // ...
433 * result = std::min(result, local_contribution);
434 * };
435 *
436 * value = reduction_loop<MemorySpace, Kokkos::Min<Number>>(
437 * "loop name", body, value, 0, n_owned);
438 * ```
439 *
440 * @note Here, @p body is a functor that must accept a "sentinel" type as
441 * first argument, the current index i, and a reference to a thread-local
442 * accumulator as last argument. Additional `args` may be specified in
443 * the reduction_loop() invocation that will be forwarded to the loop body.
444 */
445 template <typename MemorySpace,
446 typename Reducer,
447 typename Functor,
448 typename... Args>
449 inline typename Reducer::value_type
450 reduction_loop(const std::string &region_name,
451 const Functor &body,
452 const typename Reducer::value_type initial_value,
453 const unsigned int left,
454 const unsigned int right,
455 Args &&...args)
456 {
457 using HostSpace = dealii::MemorySpace::Host;
458 using DefaultSpace = dealii::MemorySpace::Default;
459 static_assert(std::is_same_v<MemorySpace, HostSpace> ||
460 std::is_same_v<MemorySpace, DefaultSpace>,
461 "Unexpected memory space");
462
463 if constexpr (std::is_same_v<MemorySpace, HostSpace>) {
464 return cpu_reduction_loop<Reducer>(region_name,
465 body,
466 initial_value,
467 left,
468 right,
469 std::forward<Args>(args)...);
470 } else {
471 return gpu_reduction_loop<Reducer>(region_name,
472 body,
473 initial_value,
474 left,
475 right,
476 std::forward<Args>(args)...);
477 }
478 }
479} // namespace ryujin
#define RYUJIN_PRAGMA(x)
#define LIKWID_MARKER_START(opt)
#define LIKWID_MARKER_STOP(opt)
#define NVTX_MARKER_START(opt)
#define NVTX_MARKER_STOP(opt)
Reducer::value_type reduction_loop(const std::string &region_name, const Functor &body, const typename Reducer::value_type initial_value, const unsigned int left, const unsigned int right, Args &&...args)
Definition loop.h:450
Reducer::value_type cpu_reduction_loop(const std::string &region_name, const Functor &body, const typename Reducer::value_type initial_value, const unsigned int left, const unsigned int right, Args &&...args)
Definition loop.h:271
void gpu_loop(const std::string &region_name, const Functor &body, const unsigned int left, const unsigned int internal, const unsigned int right, Args &&...args)
Definition loop.h:156
void loop(const std::string &region_name, const Functor &body, const unsigned int left, const unsigned int internal, const unsigned int right, Args &&...args)
Definition loop.h:221
Reducer::value_type gpu_reduction_loop(const std::string &region_name, const Functor &body, const typename Reducer::value_type initial_value, const unsigned int left, const unsigned int right, Args &&...args)
Definition loop.h:365
void cpu_simd_loop(const std::string &region_name, const Functor &body, const unsigned int left, const unsigned int internal, const unsigned int right, Args &&...args)
Definition loop.h:46