NeoN
A framework for CFD software
Loading...
Searching...
No Matches
parallelAlgorithms.hpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2023 - 2026 NeoN authors
2//
3// SPDX-License-Identifier: MIT
4
5#pragma once
6
7#include <Kokkos_Core.hpp>
8#include <type_traits>
9
10#include "NeoN/core/logging.hpp"
14
15#ifdef NN_WITH_KOKKOS
16#define NEON_LAMBDA KOKKOS_LAMBDA
17#define NEON_INLINE_FUNCTION KOKKOS_INLINE_FUNCTION
18namespace NeoN
19{
20// just pull Kokkos::atomic_* functions into NeoN namespace
21using Kokkos::atomic_add;
22using Kokkos::atomic_sub;
23}
24#else
25#define NEON_LAMBDA [&]
26namespace NeoN
27{
28// using atomic_add = [](auto& a, auto b){a+b;};
29// using atomic_sub = [](auto& a, auto b){a-b;};
30}
31#endif
32
33namespace NeoN
34{
35
36
37template<typename ValueType>
38class Vector;
39
40
41// Concept to check if a callable is compatible with void(const NeoN::sizeType)
42template<typename Kernel>
43concept parallelForKernel = requires(Kernel t, sizeType i) {
44 {
45 t(i)
46 } -> std::same_as<void>;
47};
48
49
50namespace detail
51{
52/* @brief call f with the concrete executor held by exec
53 *
54 * Deliberately not std::visit: its dispatch table is a GNU_UNIQUE object the dynamic linker
55 * merges process-wide, so a table instantiated in another DSO can route a kernel built here into
56 * that DSO's copy of the code. For nvcc extended lambdas (NEON_LAMBDA) the call slot is TU-local,
57 * so the foreign copy calls 0x0 (exasim-project/NeoFOAM#410). Direct calls stay in the calling
58 * DSO when it is linked with -Bsymbolic-functions.
59 */
60template<typename F>
61void visitExecutor(const Executor& exec, F&& f)
62{
63 if (const auto* serial = std::get_if<SerialExecutor>(&exec))
64 {
65 f(*serial);
66 }
67 else if (const auto* cpu = std::get_if<CPUExecutor>(&exec))
68 {
69 f(*cpu);
70 }
71 else
72 {
73 f(std::get<GPUExecutor>(exec));
74 }
75}
76}
77
78/* @brief calls fence if a logger is set */
79template<typename ExecutorType>
80void fenceIfLogger(const ExecutorType& exec)
81{
82 auto logger = getLogger(exec);
83 if (logger != nullptr)
84 {
85 fence(exec);
86 }
87}
88
89/* @brief execute parallelFor with concrete executor */
90template<typename ExecutorType, parallelForKernel Kernel>
92 const ExecutorType&, std::pair<localIdx, localIdx> range, const Kernel& kernel, std::string name
93)
94{
95 auto [start, end] = range;
96
97 if constexpr (std::is_same<std::remove_reference_t<ExecutorType>, SerialExecutor>::value)
98 {
99 for (localIdx i = start; i < end; i++)
100 {
101 kernel(i);
102 }
103 }
104 else
105 {
106 using runOn = typename ExecutorType::exec;
107 // Pass `kernel` DIRECTLY to Kokkos (do not wrap it in another NEON_LAMBDA). The wrapper
108 // makes `kernel`'s type only ever HOST-copied (into the wrapper's closure) and never the
109 // type actually device-launched, so nvcc emits no — or, under -O3, a NULL — host copy
110 // trampoline (__nv_hdl_wrapper_t::do_copy) for it; the host-side copy then jumps to 0x0
111 // and SIGSEGVs. Launching `kernel` directly makes it the device-launched type, forcing
112 // nvcc to emit a correct trampoline. See neon-nvcc-extended-lambda-trampoline.
113 Kokkos::parallel_for(
114 name, Kokkos::RangePolicy<runOn, Kokkos::IndexType<localIdx>>(start, end), kernel
115 );
116 }
117}
118
119
120/* @brief dispatch parallelFor based on executor variant type */
121template<parallelForKernel Kernel>
123 const NeoN::Executor& exec,
124 std::pair<localIdx, localIdx> range,
125 const Kernel& kernel,
126 std::string name = "parallelFor"
127)
128{
129 detail::visitExecutor(exec, [&](const auto& e) { parallelFor(e, range, kernel, name); });
130}
131
132// Concept to check if a callable is compatible with ValueType(const NeoN::sizeType)
133template<typename Kernel, typename ValueType>
134concept parallelForContainerKernel = requires(Kernel t, ValueType val, sizeType i) {
135 {
136 t(i)
137 } -> std::same_as<ValueType>;
138};
139
140template<
141 typename Executor,
142 template<typename>
143 class ContType,
144 typename ValueType,
147 const Executor&,
148 ContType<ValueType>& container,
149 const Kernel& kernel,
150 std::string name = "parallelFor"
151)
152{
153 auto view = container.view();
154 const auto viewSize = toLocalIdx(view.size());
155 if constexpr (std::is_same<std::remove_reference_t<Executor>, SerialExecutor>::value)
156 {
157 for (localIdx i = 0; i < viewSize; i++)
158 {
159 view[i] = kernel(i);
160 }
161 }
162 else
163 {
164 using runOn = typename Executor::exec;
165 Kokkos::parallel_for(
166 name,
167 Kokkos::RangePolicy<runOn>(0, viewSize),
168 NEON_LAMBDA(const localIdx i) { view[i] = kernel(i); }
169 );
170 }
171}
172
173template<
174 template<typename>
175 class ContType,
176 typename ValueType,
177 parallelForContainerKernel<ValueType> Kernel>
178void parallelFor(ContType<ValueType>& cont, const Kernel& kernel, std::string name = "parallelFor")
179{
180 detail::visitExecutor(cont.exec(), [&](const auto& e) { parallelFor(e, cont, kernel, name); });
181}
182
183template<typename Executor, typename Kernel, typename T>
185 [[maybe_unused]] const Executor& exec,
186 std::pair<localIdx, localIdx> range,
187 const Kernel& kernel,
188 T& value
189)
190{
191 auto [start, end] = range;
192 if constexpr (std::is_same<std::remove_reference_t<Executor>, SerialExecutor>::value)
193 {
194 for (localIdx i = start; i < end; i++)
195 {
196 if constexpr (Kokkos::is_reducer<T>::value)
197 {
198 kernel(i, value.reference());
199 }
200 else
201 {
202 kernel(i, value);
203 }
204 }
205 }
206 else
207 {
208 using runOn = typename Executor::exec;
209 Kokkos::parallel_reduce(
210 "parallelReduce", Kokkos::RangePolicy<runOn>(start, end), kernel, value
211 );
212 }
213}
214
215template<typename Kernel, typename T>
217 const NeoN::Executor& exec, std::pair<localIdx, localIdx> range, const Kernel& kernel, T& value
218)
219{
220 detail::visitExecutor(exec, [&](const auto& e) { parallelReduce(e, range, kernel, value); });
221}
222
223
224template<typename Executor, typename ValueType, typename Kernel, typename T>
226 [[maybe_unused]] const Executor& exec, Vector<ValueType>& field, const Kernel& kernel, T& value
227)
228{
229 if constexpr (std::is_same<std::remove_reference_t<Executor>, SerialExecutor>::value)
230 {
231 localIdx fieldSize = field.size();
232 for (localIdx i = 0; i < fieldSize; i++)
233 {
234 if constexpr (Kokkos::is_reducer<T>::value)
235 {
236 kernel(i, value.reference());
237 }
238 else
239 {
240 kernel(i, value);
241 }
242 }
243 }
244 else
245 {
246 using runOn = typename Executor::exec;
247 Kokkos::parallel_reduce(
248 "parallelReduce", Kokkos::RangePolicy<runOn>(0, field.size()), kernel, value
249 );
250 }
251}
252
253template<typename ValueType, typename Kernel, typename T>
254void parallelReduce(Vector<ValueType>& field, const Kernel& kernel, T& value)
255{
257 field.exec(), [&](const auto& e) { parallelReduce(e, field, kernel, value); }
258 );
259}
260
261template<typename Executor, typename Kernel>
263 [[maybe_unused]] const Executor& exec, std::pair<localIdx, localIdx> range, const Kernel& kernel
264)
265{
266 auto [start, end] = range;
267 using runOn = typename Executor::exec;
268 Kokkos::parallel_scan("parallelScan", Kokkos::RangePolicy<runOn>(start, end), kernel);
269}
270
271template<typename Kernel>
273 const NeoN::Executor& exec, std::pair<localIdx, localIdx> range, const Kernel& kernel
274)
275{
276 detail::visitExecutor(exec, [&](const auto& e) { parallelScan(e, range, kernel); });
277}
278
279template<typename Executor, typename Kernel, typename ReturnType>
281 [[maybe_unused]] const Executor& exec,
282 std::pair<localIdx, localIdx> range,
283 const Kernel& kernel,
284 ReturnType& returnValue
285)
286{
287 auto [start, end] = range;
288 using runOn = typename Executor::exec;
289 Kokkos::parallel_scan(
290 "parallelScan", Kokkos::RangePolicy<runOn>(start, end), kernel, returnValue
291 );
292}
293
294template<typename Kernel, typename ReturnType>
296 const NeoN::Executor& exec,
297 std::pair<localIdx, localIdx> range,
298 const Kernel& kernel,
299 ReturnType& returnValue
300)
301{
303 exec, [&](const auto& e) { parallelScan(e, range, kernel, returnValue); }
304 );
305}
306
307} // namespace NeoN
Reference executor for serial CPU execution.
An executor-aware contiguous container for numerical data with vector-style arithmetic semantics.
Definition vector.hpp:40
sizeType size() const
Returns the number of elements in the Vector.
Definition vector.hpp:332
const Executor & exec() const
Returns the executor associated with the Vector.
Definition vector.hpp:325
void visitExecutor(const Executor &exec, F &&f)
Integer types used throughout NeoN.
Definition array.hpp:18
std::variant< SerialExecutor, CPUExecutor, GPUExecutor > Executor
Definition executor.hpp:21
void fenceIfLogger(const ExecutorType &exec)
void fence(const Executor &exec)
Definition executor.hpp:24
localIdx toLocalIdx(sizeType value)
std::shared_ptr< const Logging::BaseLogger > getLogger(const Executor &exec)
Definition executor.hpp:72
void parallelScan(const Executor &exec, std::pair< localIdx, localIdx > range, const Kernel &kernel)
std::size_t sizeType
Definition label.hpp:56
void parallelFor(const ExecutorType &, std::pair< localIdx, localIdx > range, const Kernel &kernel, std::string name)
const std::string & name(const NeoN::Document &doc)
Retrieves the name of a Document.
int32_t localIdx
Definition label.hpp:50
void parallelReduce(const Executor &exec, std::pair< localIdx, localIdx > range, const Kernel &kernel, T &value)
#define NEON_LAMBDA