Eigen  5.0.1
 
Loading...
Searching...
No Matches
Parallelizer.h
1// This file is part of Eigen, a lightweight C++ template library
2// for linear algebra.
3//
4// Copyright (C) 2010 Gael Guennebaud <gael.guennebaud@inria.fr>
5//
6// This Source Code Form is subject to the terms of the Mozilla
7// Public License v. 2.0. If a copy of the MPL was not distributed
8// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
9// SPDX-License-Identifier: MPL-2.0
10
11#ifndef EIGEN_PARALLELIZER_H
12#define EIGEN_PARALLELIZER_H
13
14// IWYU pragma: private
15#include "../InternalHeaderCheck.h"
16
17// Note that in the following, there are 3 different uses of the concept
18// "number of threads":
19// 1. Max number of threads used by OpenMP or ThreadPool.
20// * For OpenMP this is typically the value set by the OMP_NUM_THREADS
21// environment variable, or by a call to omp_set_num_threads() prior to
22// calling Eigen.
23// * For ThreadPool, this is the number of threads in the ThreadPool.
24// 2. Max number of threads currently allowed to be used by parallel Eigen
25// operations. This is set by setNbThreads(), and cannot exceed the value
26// in 1.
27// 3. The actual number of threads used for a given parallel Eigen operation.
28// This is typically computed on the fly using a cost model and cannot exceed
29// the value in 2.
30// * For OpenMP, this is typically the number of threads specified in individual
31// "omp parallel" pragmas associated with an Eigen operation.
32// * For ThreadPool, it is the number of concurrent tasks scheduled in the
33// threadpool for a given Eigen operation. Notice that since the threadpool
34// uses task stealing, there is no way to limit the number of concurrently
35// executing tasks to below the number in 1. except by limiting the total
36// number of tasks in flight.
37
38#if defined(EIGEN_HAS_OPENMP) && defined(EIGEN_GEMM_THREADPOOL)
39#error "EIGEN_HAS_OPENMP and EIGEN_GEMM_THREADPOOL may not both be defined."
40#endif
41
42namespace Eigen {
43
44namespace internal {
45inline void manage_multi_threading(Action action, int* v);
46}
47
48// Public APIs.
49
51EIGEN_DEPRECATED_WITH_REASON("Initialization is no longer needed.") inline void initParallel() {}
52
55inline int nbThreads() {
56 int ret;
57 internal::manage_multi_threading(GetAction, &ret);
58 return ret;
59}
60
63inline void setNbThreads(int v) { internal::manage_multi_threading(SetAction, &v); }
64
65#ifdef EIGEN_VECTORIZE_SME
66#ifndef EIGEN_SME_MIN_TASK_SIZE
67#define EIGEN_SME_MIN_TASK_SIZE (double(1 << 20))
68#endif
69namespace internal {
70// Time of one multiply-add on the SME kernel in fp32 multiply-adds: an FP64 outer product covers a quarter of the
71// elements of an FP32 one at the same vector length, and a complex multiply-add is four real ones.
72template <typename Scalar>
73struct sme_madd_cost {
74 static constexpr double value = (std::is_same<typename NumTraits<Scalar>::Real, double>::value ? 4.0 : 1.0) *
75 (NumTraits<Scalar>::IsComplex ? 4.0 : 1.0);
76};
77// Apple silicon shares one SME unit per core cluster, so more threads than clusters only contend for the units; other
78// SME implementations may have one per core. The count comes from macOS's performance-cluster topology and is 0 (no
79// cap) everywhere else, where EIGEN_SME_UNITS or setNbSmeUnits() supply it.
80inline int detect_sme_units() {
81#if defined(EIGEN_SME_UNITS)
82 return EIGEN_SME_UNITS;
83#elif EIGEN_OS_MAC
84 // Performance clusters only (a cluster shares one L2): the work is split evenly over the threads,
85 // so a thread on the efficiency cluster's slower unit would set the finishing time.
86 int32_t cores = 0, per_l2 = 0;
87 size_t sz = sizeof(cores);
88 if (sysctlbyname("hw.perflevel0.physicalcpu", &cores, &sz, nullptr, 0) != 0 || cores <= 0) return 0;
89 sz = sizeof(per_l2);
90 if (sysctlbyname("hw.perflevel0.cpusperl2", &per_l2, &sz, nullptr, 0) != 0 || per_l2 <= 0) return 0;
91 return numext::maxi<int32_t>(1, cores / per_l2);
92#else
93 return 0;
94#endif
95}
96inline void manage_sme_units(Action action, int* v) {
97 static int m_units = detect_sme_units();
98 if (action == SetAction)
99 m_units = *v;
100 else
101 *v = m_units;
102}
103} // namespace internal
104#endif
105
109inline int nbSmeUnits() {
110#ifdef EIGEN_VECTORIZE_SME
111 int ret;
112 internal::manage_sme_units(GetAction, &ret);
113 return ret;
114#else
115 return 0;
116#endif
117}
123inline void setNbSmeUnits(int v) {
124#ifdef EIGEN_VECTORIZE_SME
125 internal::manage_sme_units(SetAction, &v);
126#else
127 EIGEN_UNUSED_VARIABLE(v);
128#endif
129}
130
131#ifdef EIGEN_GEMM_THREADPOOL
132// Sets the ThreadPool used by Eigen parallel Gemm.
133//
134// NOTICE: This function has a known race condition with
135// parallelize_gemm below, and should not be called while
136// an instance of that function is running.
137//
138// TODO(rmlarsen): Make the device API available instead of
139// storing a local static pointer variable to avoid this issue.
140inline ThreadPool* setGemmThreadPool(ThreadPool* new_pool) {
141 static ThreadPool* pool = nullptr;
142 if (new_pool != nullptr) {
143 // This only replaces the stored pointer: work already scheduled on the old
144 // ThreadPool is not waited for, and the old pool is not destroyed. Since
145 // this returns the new pool, the caller must keep its own pointer to the
146 // old one to dispose of it.
147 pool = new_pool;
148 // Reset the number of threads to the number of threads on the new pool.
149 setNbThreads(pool->NumThreads());
150 }
151 return pool;
152}
153
154// Gets the ThreadPool used by Eigen parallel Gemm.
155inline ThreadPool* getGemmThreadPool() { return setGemmThreadPool(nullptr); }
156#endif
157
158namespace internal {
159
160// Implementation.
161
162// Granularity of the column split. Aligning to Traits::nr instead would avoid a partial leading
163// rhs panel per thread, but nr is 8 on several backends and the coarser split balances worse: on a
164// 72-core Neoverse V2, nr-alignment cost 3.5-4.5% at high thread counts (8192x8192 float, 72
165// threads: 5132 vs 5315 GFLOP/s). gebp handles a partial panel, so prefer the finer balance.
166constexpr int kGemmColGrain = 4;
167
174template <typename Index>
175EIGEN_ALWAYS_INLINE void balanced_gemm_range(Index total, Index parts, Index grain, Index part, Index& start,
176 Index& length) {
177 const Index chunks = numext::div_ceil(total, grain);
178 const Index base = chunks / parts;
179 const Index extra = chunks % parts;
180 const Index first_chunk = part * base + numext::mini(part, extra);
181 const Index chunk_count = base + (part < extra ? 1 : 0);
182 start = numext::mini(first_chunk * grain, total);
183 length = numext::mini(chunk_count * grain, total - start);
184}
185
186#if defined(EIGEN_USE_BLAS) || (!defined(EIGEN_HAS_OPENMP) && !defined(EIGEN_GEMM_THREADPOOL))
187
188inline void manage_multi_threading(Action action, int* v) {
189 if (action == SetAction) {
190 eigen_internal_assert(v != nullptr);
191 } else if (action == GetAction) {
192 eigen_internal_assert(v != nullptr);
193 *v = 1;
194 } else {
195 eigen_internal_assert(false);
196 }
197}
198template <typename Index>
199struct GemmParallelInfo {};
200template <bool Condition, typename Functor, typename Index>
201EIGEN_STRONG_INLINE void parallelize_gemm(const Functor& func, Index rows, Index cols, Index /*unused*/,
202 bool /*unused*/) {
203 func(0, rows, 0, cols);
204}
205
206#else
207
208template <typename Index>
209struct GemmParallelTaskInfo {
210 GemmParallelTaskInfo() {}
211 std::atomic<Index> sync{Index(-1)};
212 std::atomic<int> users{0};
213 Index lhs_start = 0;
214 Index lhs_length = 0;
215};
216
217template <typename Index>
218struct GemmParallelInfo {
219 const int logical_thread_id;
220 const int num_threads;
221 GemmParallelTaskInfo<Index>* task_info;
222
223 GemmParallelInfo(int logical_thread_id_, int num_threads_, GemmParallelTaskInfo<Index>* task_info_)
224 : logical_thread_id(logical_thread_id_), num_threads(num_threads_), task_info(task_info_) {}
225};
226
227inline void manage_multi_threading(Action action, int* v) {
228 static int m_maxThreads = -1;
229 if (action == SetAction) {
230 eigen_internal_assert(v != nullptr);
231#if defined(EIGEN_HAS_OPENMP)
232 // Calling action == SetAction and *v = 0 means
233 // restoring m_maxThreads to the maximum number of threads specified
234 // for OpenMP.
235 eigen_internal_assert(*v >= 0);
236 int omp_threads = omp_get_max_threads();
237 m_maxThreads = (*v == 0 ? omp_threads : std::min<int>(*v, omp_threads));
238#elif defined(EIGEN_GEMM_THREADPOOL)
239 // Calling action == SetAction and *v = 0 means
240 // restoring m_maxThreads to the number of threads in the ThreadPool,
241 // which defaults to 1 if no pool was provided.
242 eigen_internal_assert(*v >= 0);
243 ThreadPool* pool = getGemmThreadPool();
244 int pool_threads = pool != nullptr ? pool->NumThreads() : 1;
245 m_maxThreads = (*v == 0 ? pool_threads : numext::mini(pool_threads, *v));
246#endif
247 } else if (action == GetAction) {
248 eigen_internal_assert(v != nullptr);
249#if defined(EIGEN_HAS_OPENMP)
250 if (m_maxThreads > 0)
251 *v = m_maxThreads;
252 else
253 *v = omp_get_max_threads();
254#else
255 *v = m_maxThreads;
256#endif
257 } else {
258 eigen_internal_assert(false);
259 }
260}
261
262template <bool Condition, typename Functor, typename Index>
263EIGEN_STRONG_INLINE void parallelize_gemm(const Functor& func, Index rows, Index cols, Index depth, bool transpose) {
264 // Dynamically check whether we should even try to execute in parallel.
265 // The conditions are:
266 // - the max number of threads we can create is greater than 1
267 // - we are not already in a parallel code
268 // - the sizes are large enough
269
270 // compute the maximal number of threads from the size of the product:
271 // This first heuristic takes into account that the product kernel is fully optimized when working with nr columns at
272 // once.
273 Index size = transpose ? rows : cols;
274 Index pb_max_threads = std::max<Index>(1, size / Functor::Traits::nr);
275
276 // compute the maximal number of threads from the total amount of work:
277 double work = static_cast<double>(rows) * static_cast<double>(cols) * static_cast<double>(depth);
278 double kMinTaskSize = 50000; // FIXME: tune this minimum task-size heuristic based on architecture and scalar type.
279#ifdef EIGEN_VECTORIZE_SME
280 // With a known SME unit count (one unit per core cluster), the SME kernel does far more work per unit of time than
281 // the generic one, so a thread needs more of it to repay the handoff; elsewhere the size is not measured and the
282 // generic one applies. With no preallocated buffers, each thread runs a disjoint part of the result (below).
283 constexpr bool kSme =
284 sme_has_gebp_kernel<typename Functor::Traits::LhsScalar, typename Functor::Traits::RhsScalar>::value;
285 const bool sme_units_known = kSme && nbSmeUnits() > 0;
286 const bool sme_disjoint = sme_units_known && func.ownsNoBuffers();
287 if (sme_units_known)
288 kMinTaskSize = EIGEN_SME_MIN_TASK_SIZE / sme_madd_cost<typename Functor::Traits::LhsScalar>::value;
289 if (sme_disjoint) {
290 const Index kernel_rows = transpose ? cols : rows, kernel_cols = transpose ? rows : cols;
291 pb_max_threads =
292 std::max<Index>(1, (std::max)(kernel_rows / Functor::Traits::mr, kernel_cols / Functor::Traits::nr));
293 }
294#endif
295 pb_max_threads = std::max<Index>(1, std::min<Index>(pb_max_threads, static_cast<Index>(work / kMinTaskSize)));
296
297 // compute the number of threads we are going to use
298 int threads = std::min<int>(nbThreads(), static_cast<int>(pb_max_threads));
299#ifdef EIGEN_VECTORIZE_SME
300 // The SME kernels share one unit per cluster; more threads than units only contend.
301 EIGEN_IF_CONSTEXPR (kSme) {
302 const int units = nbSmeUnits();
303 if (units > 0) threads = std::min<int>(threads, units);
304 }
305#endif
306
307 // if multi-threading is explicitly disabled, not useful, or if we already are
308 // inside a parallel session, then abort multi-threading
309 bool dont_parallelize = (!Condition) || (threads <= 1);
310#if defined(EIGEN_HAS_OPENMP)
311 // don't parallelize if we are executing in a parallel context already.
312 dont_parallelize |= omp_get_num_threads() > 1;
313#elif defined(EIGEN_GEMM_THREADPOOL)
314 // don't parallelize if we have a trivial threadpool or the current thread id
315 // is != -1, indicating that we are already executing on a thread inside the pool.
316 // In other words, we do not allow nested parallelism, since this would lead to
317 // deadlocks due to the workstealing nature of the threadpool.
318 ThreadPool* pool = getGemmThreadPool();
319 dont_parallelize |= (pool == nullptr || pool->CurrentThreadId() != -1);
320#endif
321 if (dont_parallelize) return func(0, rows, 0, cols);
322
323#ifdef EIGEN_VECTORIZE_SME
324 // A known unit count stands for one SME unit per core cluster, each with its own L2 (see nbSmeUnits): the
325 // cooperative session below would read the other cluster's packed LHS and wait for it at every depth step, so each
326 // thread runs a disjoint part of the result instead.
327 EIGEN_IF_CONSTEXPR (kSme) {
328 if (sme_disjoint) {
329 // On the kernel's problem (the transposed one for a row-major result), a row split repacks the whole RHS per
330 // thread and a column split the whole LHS (a copy, or nothing when read in place): split the rows only when
331 // they are twice the columns and give 8 LHS panels per part, since a narrower result already runs close to peak.
332 const Index kernel_rows = transpose ? cols : rows, kernel_cols = transpose ? rows : cols;
333 const Index row_parts = kernel_rows / (8 * Functor::Traits::mr);
334 const bool split_kernel_rows = kernel_rows >= 2 * kernel_cols && row_parts >= 2;
335 const bool split_rows = transpose ? !split_kernel_rows : split_kernel_rows;
336 if (split_kernel_rows) threads = static_cast<int>((std::min)(Index(threads), row_parts));
337 // The result's rows are the kernel's columns for a row-major result. No part may be empty.
338 const Index row_grain = transpose ? Functor::Traits::nr : Functor::Traits::mr;
339 const Index col_grain = transpose ? Functor::Traits::mr : Functor::Traits::nr;
340 const Index chunks = split_rows ? (rows + row_grain - 1) / row_grain : (cols + col_grain - 1) / col_grain;
341 threads = static_cast<int>((std::min)(Index(threads), chunks));
342 if (threads <= 1) return func(0, rows, 0, cols);
343 auto part = [&func, rows, cols, threads, split_rows, row_grain, col_grain](int i) {
344 Index start, length;
345 if (split_rows) {
346 balanced_gemm_range<Index>(rows, threads, row_grain, i, start, length);
347 if (length > 0) func(start, length, 0, cols);
348 } else {
349 balanced_gemm_range<Index>(cols, threads, col_grain, i, start, length);
350 if (length > 0) func(0, rows, start, length);
351 }
352 };
353#if defined(EIGEN_HAS_OPENMP)
354#pragma omp parallel for num_threads(threads) schedule(static, 1)
355 for (int i = 0; i < threads; ++i) part(i);
356#elif defined(EIGEN_GEMM_THREADPOOL)
357 // The parts take about as long as each other, so the caller spins for the others (tens of microseconds, less than
358 // a blocking wake-up costs a short product) before it blocks on the barrier, which it always passes, also
359 // before an exception from its own part leaves this frame.
360 std::atomic<int> pending(threads - 1);
361 Barrier done(threads - 1);
362 for (int i = 0; i < threads - 1; ++i)
363 pool->Schedule([&part, &pending, &done, i] {
364 part(i);
365 pending.fetch_sub(1, std::memory_order_release);
366 done.Notify();
367 });
368 const auto wait = [&pending, &done] {
369 for (int spin = 0; spin < 65536 && pending.load(std::memory_order_acquire) != 0; ++spin) {
370 }
371 done.Wait();
372 };
373 EIGEN_TRY { part(threads - 1); }
374 EIGEN_CATCH(...) {
375 wait();
376 EIGEN_THROW;
377 }
378 wait();
379#endif
380 return;
381 }
382 }
383#endif
384
385 func.initParallelSession(threads);
386
387 if (transpose) std::swap(rows, cols);
388
389 ei_declare_aligned_stack_constructed_variable(GemmParallelTaskInfo<Index>, task_info, threads, 0);
390
391#if defined(EIGEN_HAS_OPENMP)
392#pragma omp parallel num_threads(threads)
393 {
394 Index i = omp_get_thread_num();
395 // Note that the actual number of threads might be lower than the number of
396 // requested ones
397 Index actual_threads = omp_get_num_threads();
398 GemmParallelInfo<Index> info(static_cast<int>(i), static_cast<int>(actual_threads), task_info);
399
400 Index r0, actualBlockRows;
401 balanced_gemm_range<Index>(rows, actual_threads, Index(Functor::Traits::mr), i, r0, actualBlockRows);
402
403 Index c0, actualBlockCols;
404 balanced_gemm_range<Index>(cols, actual_threads, Index(kGemmColGrain), i, c0, actualBlockCols);
405
406 info.task_info[i].lhs_start = r0;
407 info.task_info[i].lhs_length = actualBlockRows;
408
409 if (transpose)
410 func(c0, actualBlockCols, 0, rows, &info);
411 else
412 func(0, rows, c0, actualBlockCols, &info);
413 }
414
415#elif defined(EIGEN_GEMM_THREADPOOL)
416 Barrier barrier(threads);
417 auto task = [=, &func, &barrier, &task_info](int i) {
418 Index actual_threads = threads;
419 GemmParallelInfo<Index> info(i, static_cast<int>(actual_threads), task_info);
420 Index r0, actualBlockRows;
421 balanced_gemm_range<Index>(rows, actual_threads, Index(Functor::Traits::mr), i, r0, actualBlockRows);
422
423 Index c0, actualBlockCols;
424 balanced_gemm_range<Index>(cols, actual_threads, Index(kGemmColGrain), i, c0, actualBlockCols);
425
426 info.task_info[i].lhs_start = r0;
427 info.task_info[i].lhs_length = actualBlockRows;
428
429 if (transpose)
430 func(c0, actualBlockCols, 0, rows, &info);
431 else
432 func(0, rows, c0, actualBlockCols, &info);
433
434 barrier.Notify();
435 };
436 // Notice that we do not schedule more than "threads" tasks, which allows us to
437 // limit number of running threads, even if the threadpool itself was constructed
438 // with a larger number of threads.
439 for (int i = 0; i < threads - 1; ++i) {
440 pool->Schedule([=, task = std::move(task)] { task(i); });
441 }
442 task(threads - 1);
443 barrier.Wait();
444#endif
445}
446
447#endif
448
449} // end namespace internal
450} // end namespace Eigen
451
452#endif // EIGEN_PARALLELIZER_H