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