ryujin 2.1.1 revision 71cdc42292164f8095c0bd62c4ea75ebcfac58fa
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 <concepts>
19#include <mutex>
20#include <string>
21#include <type_traits>
22#include <vector>
23
24#ifdef WITH_OPENMP
25#include <omp.h>
26#endif
27
28namespace ryujin
29{
47 template <typename ScalarNumber, typename Functor, typename... Args>
48 inline void cpu_simd_loop(const std::string &region_name [[maybe_unused]],
49 const Functor &body,
50 const unsigned int left,
51 const unsigned int internal,
52 const unsigned int right,
53 Args &&...args)
54 {
55 Assert(left <= internal && internal <= right,
56 dealii::ExcMessage("Invalid index range: it must hold left <= "
57 "internal, internal <= right"));
58
59 if (!region_name.empty()) {
60 LIKWID_MARKER_START(region_name.c_str());
61 }
62
63 using VA = dealii::VectorizedArray<ScalarNumber>;
64
65 constexpr unsigned int stride_size = get_stride_size<VA>;
66 const unsigned int regular =
67 left + (internal - left) / stride_size * stride_size;
68
69#if defined(WITH_OPENMP)
70 /* Variant using OpenMP: */
71
72 RYUJIN_PRAGMA(omp parallel default(shared))
73 {
74 /* SIMD vectorized loop: */
75 RYUJIN_PRAGMA(omp for nowait)
76 for (unsigned int i = left; i < regular; i += stride_size)
77 body(VA(), std::forward<Args>(args)..., i);
78
79 /* Serial loop: */
80 RYUJIN_PRAGMA(omp for)
81 for (unsigned int i = regular; i < right; i += 1)
82 body(ScalarNumber(), std::forward<Args>(args)..., i);
83 }
84
85#elif defined(WITH_DEAL_II_THREADS)
86 /* Variant using dealii's parallel for: */
87 {
88 /*
89 * We have to ensure that the deal.II routine only schedules a
90 * workload that is divisible by stride_size.
91 */
92 Assert((regular - left) % stride_size == 0, dealii::ExcInternalError());
93 dealii::parallel::apply_to_subranges(
94 0,
95 (regular - left) / stride_size,
96 [&](const unsigned int begin, const unsigned int end) {
97 /* SIMD vectorized loop: */
98 for (unsigned int i = begin; i < end; ++i)
99 body(VA(), std::forward<Args>(args)..., left + stride_size * i);
100 },
101 1000);
102
103 dealii::parallel::apply_to_subranges(
104 regular,
105 right,
106 [&](const unsigned int begin, const unsigned int end) {
107 /* Serial loop: */
108 for (unsigned int i = begin; i < end; ++i)
109 body(ScalarNumber(), std::forward<Args>(args)..., i);
110 },
111 1000);
112 }
113
114#else
115 /* Execute loops in serial: */
116 {
117 /* SIMD vectorized loop: */
118 for (unsigned int i = left; i < regular; i += stride_size)
119 body(VA(), std::forward<Args>(args)..., i);
120
121 /* Serial loop: */
122 for (unsigned int i = regular; i < right; i += 1)
123 body(ScalarNumber(), std::forward<Args>(args)..., i);
124 }
125#endif
126
127 if (!region_name.empty()) {
128 LIKWID_MARKER_STOP(region_name.c_str());
129 }
130 }
131
132
157 template <typename ScalarNumber, typename Functor, typename... Args>
158 inline void gpu_loop(const std::string &region_name,
159 const Functor &body,
160 const unsigned int left,
161 const unsigned int internal [[maybe_unused]],
162 const unsigned int right,
163 Args &&...args)
164 {
165 DeviceTimer::Scope scope;
166
167 Assert(left <= internal && internal <= right,
168 dealii::ExcMessage("Invalid index range: it must hold left <= "
169 "internal, internal <= right"));
170
171 using MemorySpace = dealii::MemorySpace::Default;
172 using ExecutionSpace = typename MemorySpace::kokkos_space::execution_space;
173 using Policy =
174 Kokkos::RangePolicy<ExecutionSpace, Kokkos::IndexType<unsigned int>>;
175
176 const auto exec = ExecutionSpace{};
177
178 if (!region_name.empty()) {
179 NVTX_MARKER_START(region_name.c_str());
180 }
181
182 Kokkos::parallel_for(
183 region_name,
184 Policy(exec, left, right),
185 KOKKOS_LAMBDA(const unsigned int i) {
186 body(ScalarNumber(), args..., i);
187 });
188
189 exec.fence();
190
191 if (!region_name.empty()) {
192 NVTX_MARKER_STOP(region_name.c_str());
193 }
194 }
195
196
208 template <typename MemorySpace,
209 typename ScalarNumber,
210 typename Functor,
211 typename... Args>
212 inline void loop(const std::string &region_name,
213 const Functor &body,
214 const unsigned int left,
215 const unsigned int internal,
216 const unsigned int right,
217 Args &&...args)
218 {
219 using HostSpace = dealii::MemorySpace::Host;
220 using DefaultSpace = dealii::MemorySpace::Default;
221 static_assert(std::is_same_v<MemorySpace, HostSpace> ||
222 std::is_same_v<MemorySpace, DefaultSpace>,
223 "Unexpected memory space");
224
225 if constexpr (std::is_same_v<MemorySpace, HostSpace>) {
226 cpu_simd_loop<ScalarNumber>(region_name,
227 body,
228 left,
229 internal,
230 right,
231 std::forward<Args>(args)...);
232 } else {
233 gpu_loop<ScalarNumber>(region_name,
234 body,
235 left,
236 internal,
237 right,
238 std::forward<Args>(args)...);
239 }
240 }
241
242
258 template <typename ElementReducer>
260 using scalar_type = typename ElementReducer::value_type;
262
263 const unsigned int value_count;
264
265 ArrayReducer(scalar_type *data, const unsigned int n)
266 : value_count(n)
267 , data_(data)
268 , element_reducer_(typename ElementReducer::result_view_type())
269 {
270 }
271
272 explicit ArrayReducer(std::vector<scalar_type> &values)
273 : ArrayReducer(values.data(), static_cast<unsigned int>(values.size()))
274 {
275 }
276
277 KOKKOS_INLINE_FUNCTION
278 void init(scalar_type *values) const
279 {
280 for (unsigned int k = 0; k < value_count; ++k)
281 element_reducer_.init(values[k]);
282 }
283
284 KOKKOS_INLINE_FUNCTION
285 void join(scalar_type *destination, const scalar_type *source) const
286 {
287 for (unsigned int k = 0; k < value_count; ++k)
288 element_reducer_.join(destination[k], source[k]);
289 }
290
291 template <typename Contribution>
292 requires std::invocable<const Contribution &, unsigned int>
293 KOKKOS_INLINE_FUNCTION void join(scalar_type *destination,
294 const Contribution &contribution) const
295 {
296 for (unsigned int k = 0; k < value_count; ++k)
297 element_reducer_.join(destination[k], contribution(k));
298 }
299
301 {
302 return data_;
303 }
304
305 private:
306 scalar_type *const data_;
307
308 /*
309 * The element reducer is only used for its join() and init()
310 * operations; it is constructed with an empty result view.
311 */
312 const ElementReducer element_reducer_;
313 };
314
315
316 namespace internal
317 {
324 template <typename Reducer>
325 struct LocalResult {
326 static constexpr bool is_array =
327 std::is_array_v<typename Reducer::value_type>;
328 using scalar_type = std::remove_extent_t<typename Reducer::value_type>;
329
330 std::conditional_t<is_array, std::vector<scalar_type>, scalar_type>
331 storage;
332
333 /* Initialize our storage element: */
334 LocalResult(const Reducer &reducer)
335 {
336 if constexpr (is_array)
337 storage.resize(reducer.value_count);
338 reducer.init(get());
339 }
340
341 /* Return the stored data: */
342 std::conditional_t<is_array, scalar_type *, scalar_type &> get()
343 {
344 if constexpr (is_array)
345 return storage.data();
346 else
347 return storage;
348 }
349
350 /* Construct a view for our storage element: */
351 auto view()
352 {
353 using Unmanaged = Kokkos::MemoryTraits<Kokkos::Unmanaged>;
354 if constexpr (is_array)
355 return Kokkos::View<scalar_type *, Kokkos::HostSpace, Unmanaged>(
356 storage.data(), storage.size());
357 else
358 return Kokkos::View<scalar_type, Kokkos::HostSpace, Unmanaged>(
359 &storage);
360 }
361 };
362
363
369 template <typename Reducer, typename Body>
370 struct ReductionFunctor : Reducer {
371 const Body body;
372
373 ReductionFunctor(const Reducer &reducer, const Body &body)
374 : Reducer(reducer)
375 , body(body)
376 {
377 }
378
379 KOKKOS_INLINE_FUNCTION
380 void operator()(const unsigned int i, auto &&local_result) const
381 {
382 Reducer::join(local_result, body(i));
383 }
384 };
385 } // namespace internal
386
387
388 /*
389 * A thread-parallelized reduction loop running on the CPU. The loop
390 * traverses the index range [left, right) and reduces the contributions
391 * returned by the loop body with the supplied @p reducer into the result
392 * storage that the reducer references, see reduction_loop().
393 *
394 * @note In contrast to cpu_simd_loop() the loop is not SIMD vectorized:
395 * the loop body is always called with a scalar sentinel type.
396 *
397 * @note Here, @p body is a functor that must accept a "sentinel" type as
398 * first argument and the current index i as last argument, and that
399 * returns its contribution to the reduction. Additional `args` may be
400 * specified in the cpu_reduction_loop() invocation that will be
401 * forwarded to the loop body:
402 * `body(Number(), std::forward<Args>(args)..., i)`
403 */
404 template <typename Reducer, typename Functor, typename... Args>
405 inline void cpu_reduction_loop(const std::string &region_name
406 [[maybe_unused]],
407 const Functor &body,
408 const Reducer &reducer,
409 const unsigned int left,
410 const unsigned int right,
411 Args &&...args)
412 {
413 Assert(
414 left <= right,
415 dealii::ExcMessage("Invalid index range: it must hold left <= right"));
416
417 if (!region_name.empty()) {
418 LIKWID_MARKER_START(region_name.c_str());
419 }
420
421 using scalar_type = std::remove_extent_t<typename Reducer::value_type>;
422
423#if defined(WITH_OPENMP)
424 /* Variant using OpenMP: */
425
426 RYUJIN_PRAGMA(omp parallel default(shared))
427 {
428 internal::LocalResult<Reducer> local_result(reducer);
429
430 RYUJIN_PRAGMA(omp for nowait)
431 for (unsigned int i = left; i < right; ++i)
432 reducer.join(local_result.get(),
433 body(scalar_type(), std::forward<Args>(args)..., i));
434
435 RYUJIN_PRAGMA(omp critical)
436 reducer.join(reducer.reference(), local_result.get());
437 }
438
439#elif defined(WITH_DEAL_II_THREADS)
440 /* Variant using dealii's parallel for: */
441 {
442 std::mutex mutex;
443
444 dealii::parallel::apply_to_subranges(
445 left,
446 right,
447 [&](const unsigned int begin, const unsigned int end) {
448 /* per thread */
449 internal::LocalResult<Reducer> local_result(reducer);
450
451 for (unsigned int i = begin; i < end; ++i)
452 reducer.join(local_result.get(),
453 body(scalar_type(), std::forward<Args>(args)..., i));
454
455 std::lock_guard<std::mutex> lock(mutex);
456 reducer.join(reducer.reference(), local_result.get());
457 },
458 1000);
459 }
460
461#else
462 /* Execute loop in serial: */
463 {
464 for (unsigned int i = left; i < right; ++i)
465 reducer.join(reducer.reference(),
466 body(scalar_type(), std::forward<Args>(args)..., i));
467 }
468#endif
469
470 if (!region_name.empty()) {
471 LIKWID_MARKER_STOP(region_name.c_str());
472 }
473 }
474
475
476 /*
477 * A reduction loop running on the device (i.e., in the default memory
478 * space). The loop traverses the index range [left, right) with a
479 * Kokkos::parallel_reduce using a range policy and reduces the
480 * contributions returned by the loop body with the supplied @p reducer
481 * into the result storage that the reducer references, see reduction_loop().
482 *
483 * @note Here, @p body is a functor that must accept a "sentinel" type as
484 * first argument and the current index i as last argument, and that
485 * returns its contribution to the reduction. Additional `args` may be
486 * specified in the gpu_reduction_loop() invocation that will be
487 * forwarded to the loop body:
488 * `body(Number(), args..., i)`
489 *
490 * @note The loop body (and everything it references) has to be callable
491 * on the device, see the discussion in gpu_loop().
492 *
493 * @note The function fences the execution space before returning. It
494 * thus has the same (synchronous) semantics as cpu_reduction_loop().
495 */
496 template <typename Reducer, typename Functor, typename... Args>
497 inline void gpu_reduction_loop(const std::string &region_name,
498 const Functor &body,
499 const Reducer &reducer,
500 const unsigned int left,
501 const unsigned int right,
502 Args &&...args)
503 {
504 DeviceTimer::Scope scope;
505
506 Assert(
507 left <= right,
508 dealii::ExcMessage("Invalid index range: it must hold left <= right"));
509
510 using scalar_type = std::remove_extent_t<typename Reducer::value_type>;
511
512 using MemorySpace = dealii::MemorySpace::Default;
513 using ExecutionSpace = typename MemorySpace::kokkos_space::execution_space;
514 using Policy =
515 Kokkos::RangePolicy<ExecutionSpace, Kokkos::IndexType<unsigned int>>;
516
517 const auto exec = ExecutionSpace{};
518
519 const auto kernel = KOKKOS_LAMBDA(const unsigned int i)
520 {
521 return body(scalar_type(), args..., i);
522 };
523
524 const auto functor =
525 internal::ReductionFunctor<Reducer, decltype(kernel)>(reducer, kernel);
526
527 internal::LocalResult<Reducer> result(reducer);
528
529 if (!region_name.empty()) {
530 NVTX_MARKER_START(region_name.c_str());
531 }
532
533 Kokkos::parallel_reduce(
534 region_name, Policy(exec, left, right), functor, result.view());
535
536 exec.fence();
537
538 if (!region_name.empty()) {
539 NVTX_MARKER_STOP(region_name.c_str());
540 }
541
542 reducer.join(reducer.reference(), result.get());
543 }
544
545
546 /*
547 * A reduction loop running either on the CPU, or on the device depending
548 * on the selected memory space: For dealii::MemorySpace::Host the loop is
549 * dispatched to cpu_reduction_loop(), and for dealii::MemorySpace::Default to
550 * gpu_reduction_loop().
551 *
552 * The loop body computes and returns a contribution for every index. The
553 * reduction operation itself is selected with a @p reducer object (such
554 * as Kokkos::Min, Kokkos::Max, or Kokkos::Sum) that folds all
555 * contributions into the result storage it references. The value_type of
556 * the reducer also determines the number type of the loop. The initial
557 * contents of the result storage take part in the reduction:
558 * ```
559 * const auto body = [=](auto, unsigned int i) -> Number {
560 * // ...
561 * return local_contribution;
562 * };
563 *
564 * reduction_loop<MemorySpace>(
565 * "loop name", body, Kokkos::Min<Number>(value), 0, n_owned);
566 * ```
567 * For reducing an array of values elementwise use the ArrayReducer, in
568 * which case the loop body returns a callable `j -> Number` that the
569 * reducer evaluates for all j < `value_count`:
570 * ```
571 * const auto body = [=](auto, unsigned int i) {
572 * // ...
573 * return [=](unsigned int j) { return local_contribution[j]; };
574 * };
575 *
576 * std::vector<Number> sums(n_values, Number(0.));
577 * reduction_loop<MemorySpace>(
578 * "loop name", body, ArrayReducer<Kokkos::Sum<Number>>(sums), 0,
579 * n_owned);
580 * ```
581 *
582 * @note Here, @p body is a functor that must accept a "sentinel" type as
583 * first argument and the current index i as last argument, and that
584 * returns its contribution to the reduction. Additional `args` may be
585 * specified in the reduction_loop() invocation that will be forwarded to
586 * the loop body.
587 */
588 template <typename MemorySpace,
589 typename Reducer,
590 typename Functor,
591 typename... Args>
592 inline void reduction_loop(const std::string &region_name,
593 const Functor &body,
594 const Reducer &reducer,
595 const unsigned int left,
596 const unsigned int right,
597 Args &&...args)
598 {
599 using HostSpace = dealii::MemorySpace::Host;
600 using DefaultSpace = dealii::MemorySpace::Default;
601 static_assert(std::is_same_v<MemorySpace, HostSpace> ||
602 std::is_same_v<MemorySpace, DefaultSpace>,
603 "Unexpected memory space");
604
605 if constexpr (std::is_same_v<MemorySpace, HostSpace>) {
607 region_name, body, reducer, left, right, std::forward<Args>(args)...);
608 } else {
610 region_name, body, reducer, left, right, std::forward<Args>(args)...);
611 }
612 }
613} // 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)
void gpu_reduction_loop(const std::string &region_name, const Functor &body, const Reducer &reducer, const unsigned int left, const unsigned int right, Args &&...args)
Definition loop.h:497
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:158
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:212
void reduction_loop(const std::string &region_name, const Functor &body, const Reducer &reducer, const unsigned int left, const unsigned int right, Args &&...args)
Definition loop.h:592
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:48
void cpu_reduction_loop(const std::string &region_name, const Functor &body, const Reducer &reducer, const unsigned int left, const unsigned int right, Args &&...args)
Definition loop.h:405
typename ElementReducer::value_type scalar_type
Definition loop.h:260
ArrayReducer(scalar_type *data, const unsigned int n)
Definition loop.h:265
scalar_type * reference() const
Definition loop.h:300
scalar_type[] value_type
Definition loop.h:261
KOKKOS_INLINE_FUNCTION void init(scalar_type *values) const
Definition loop.h:278
ArrayReducer(std::vector< scalar_type > &values)
Definition loop.h:272
const unsigned int value_count
Definition loop.h:263
KOKKOS_INLINE_FUNCTION void join(scalar_type *destination, const scalar_type *source) const
Definition loop.h:285
KOKKOS_INLINE_FUNCTION void join(scalar_type *destination, const Contribution &contribution) const
Definition loop.h:293