Eigen-Contrib  5.0.1
 
Loading...
Searching...
No Matches
TensorDeviceThreadPool.h
1// This file is part of Eigen, a lightweight C++ template library
2// for linear algebra.
3//
4// Copyright (C) 2014 Benoit Steiner <benoit.steiner.goog@gmail.com>
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#if defined(EIGEN_USE_THREADS) && !defined(EIGEN_TENSOR_TENSOR_DEVICE_THREAD_POOL_H)
12#define EIGEN_TENSOR_TENSOR_DEVICE_THREAD_POOL_H
13
14// IWYU pragma: private
15#include "./InternalHeaderCheck.h"
16
17namespace Eigen {
18
19// An abstract interface to a device specific memory allocator.
20class Allocator {
21 public:
22 virtual ~Allocator() = default;
23 virtual void* allocate(size_t num_bytes) const = 0;
24 virtual void deallocate(void* buffer) const = 0;
25};
26
27// Build a thread pool device on top of an existing pool of threads.
28struct ThreadPoolDevice {
29 // The ownership of the thread pool remains with the caller.
30 ThreadPoolDevice(ThreadPoolInterface* pool, int num_cores, Allocator* allocator = nullptr)
31 : pool_(pool), num_threads_(num_cores), allocator_(allocator) {}
32
33 EIGEN_STRONG_INLINE void* allocate(size_t num_bytes) const {
34 return allocator_ ? allocator_->allocate(num_bytes) : internal::aligned_malloc(num_bytes);
35 }
36
37 EIGEN_STRONG_INLINE void deallocate(void* buffer) const {
38 if (allocator_) {
39 allocator_->deallocate(buffer);
40 } else {
41 internal::aligned_free(buffer);
42 }
43 }
44
45 EIGEN_STRONG_INLINE void* allocate_temp(size_t num_bytes) const { return allocate(num_bytes); }
46
47 EIGEN_STRONG_INLINE void deallocate_temp(void* buffer) const { deallocate(buffer); }
48
49 template <typename Type>
50 EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Type get(Type data) const {
51 return data;
52 }
53
54 EIGEN_STRONG_INLINE void memcpy(void* dst, const void* src, size_t n) const {
55#ifdef __ANDROID__
56 ::memcpy(dst, src, n);
57#else
58 // TODO(rmlarsen): Align blocks on cache lines.
59 const size_t kMinBlockSize = 32768;
60 // Pure memory op (zero compute) — the cost model's bandwidth saturation
61 // cap will limit threads appropriately.
62 const size_t num_threads = CostModel::numThreads(n, TensorOpCost(1.0, 1.0, 0), static_cast<int>(numThreads()));
63 if (n <= kMinBlockSize || num_threads < 2) {
64 ::memcpy(dst, src, n);
65 } else {
66 const char* src_ptr = static_cast<const char*>(src);
67 char* dst_ptr = static_cast<char*>(dst);
68 const size_t blocksize = (n + (num_threads - 1)) / num_threads;
69 Barrier barrier(static_cast<int>(num_threads - 1));
70 // Schedule blocks 1..num_threads-1 on worker threads.
71 for (size_t i = 1; i < num_threads; ++i) {
72 pool_->Schedule([n, i, src_ptr, dst_ptr, blocksize, &barrier] {
73 ::memcpy(dst_ptr + i * blocksize, src_ptr + i * blocksize, numext::mini(blocksize, n - (i * blocksize)));
74 barrier.Notify();
75 });
76 }
77 // Run the first block on the calling thread.
78 ::memcpy(dst_ptr, src_ptr, blocksize);
79 barrier.Wait();
80 }
81#endif
82 }
83 EIGEN_STRONG_INLINE void memcpyHostToDevice(void* dst, const void* src, size_t n) const { memcpy(dst, src, n); }
84 EIGEN_STRONG_INLINE void memcpyDeviceToHost(void* dst, const void* src, size_t n) const { memcpy(dst, src, n); }
85
86 EIGEN_STRONG_INLINE void memset(void* buffer, int c, size_t n) const { ::memset(buffer, c, n); }
87
88 template <typename T>
89 EIGEN_STRONG_INLINE void fill(T* begin, T* end, const T& value) const {
90 std::fill(begin, end, value);
91 }
92
93 EIGEN_STRONG_INLINE int numThreads() const { return num_threads_; }
94
95 // Number of threads available in the underlying thread pool. This number can
96 // be different from the value returned by numThreads().
97 EIGEN_STRONG_INLINE int numThreadsInPool() const { return pool_->NumThreads(); }
98
99 EIGEN_STRONG_INLINE size_t firstLevelCacheSize() const { return l1CacheSize(); }
100
101 EIGEN_STRONG_INLINE size_t lastLevelCacheSize() const {
102 // The l3 cache size is shared between all the cores.
103 return l3CacheSize() / num_threads_;
104 }
105
106 EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE void synchronize() const {
107 // Nothing. Threadpool device operations are synchronous.
108 }
109
110 EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE int majorDeviceVersion() const {
111 // Should return an enum that encodes the ISA supported by the CPU
112 return 1;
113 }
114
115 // TODO(rmlarsen): Remove this deprecated interface when all users have been converted.
116 template <class Function, class... Args>
117 EIGEN_STRONG_INLINE void enqueueNoNotification(Function&& f, Args&&... args) const {
118 enqueue(std::forward<Function>(f), std::forward<Args>(args)...);
119 }
120
121 template <class Function, class... Args>
122 EIGEN_STRONG_INLINE void enqueue(Function&& f, Args&&... args) const {
123 if (sizeof...(args) > 0) {
124#if EIGEN_COMP_CXXVER >= 20
125 // C++20 pack-expansion init capture decay-copies args into a lambda
126 // small enough to fit in std::function's SBO for typical arg counts.
127 pool_->Schedule([f = std::forward<Function>(f), ... args = std::forward<Args>(args)]() { f(args...); });
128#else
129 // Pre-C++20 fallback: std::bind decay-copies the arguments so they
130 // outlive the deferred call even when the caller passes temporaries.
131 // The closure exceeds std::function's SBO and forces a heap allocation
132 // that the C++20 path above avoids.
133 pool_->Schedule(std::bind(std::forward<Function>(f), std::forward<Args>(args)...));
134#endif
135 } else {
136 pool_->Schedule(std::forward<Function>(f));
137 }
138 }
139
140 // Returns a logical thread index between 0 and pool_->NumThreads() - 1 if
141 // called from one of the threads in pool_. Returns -1 otherwise.
142 EIGEN_STRONG_INLINE int currentThreadId() const { return pool_->CurrentThreadId(); }
143
144 // WARNING: This function is synchronous and will block the calling thread.
145 //
146 // Synchronous parallelFor executes f with [0, n) arguments in parallel and
147 // waits for completion. F accepts a half-open interval [first, last). Block
148 // size is chosen based on the iteration cost and resulting parallel
149 // efficiency. If block_align is not nullptr, it is called to round up the
150 // block size.
151 void parallelFor(Index n, const TensorOpCost& cost, std::function<Index(Index)> block_align,
152 std::function<void(Index, Index)> f) const {
153 if (EIGEN_PREDICT_FALSE(n <= 0)) {
154 return;
155 }
156 // Compute small problems directly in the caller thread.
157 if (n == 1 || numThreads() == 1 || CostModel::numThreads(n, cost, static_cast<int>(numThreads())) == 1) {
158 f(0, n);
159 return;
160 }
161
162 // Compute block size and total count of blocks.
163 ParallelForBlock block = CalculateParallelForBlock(n, cost, block_align);
164
165 // Recursively divide size into halves until we reach block_size.
166 // Division code rounds mid to block_size, so we are guaranteed to get
167 // block_count leaves that do actual computations.
168 Barrier barrier(static_cast<unsigned int>(block.count));
169 if (block.count <= numThreads()) {
170 // Avoid a thread hop by running the root of the tree and one block on the
171 // main thread.
172 handleRange(0, n, block.size, &barrier, pool_, f);
173 } else {
174 // Execute the root in the thread pool to avoid running work on more than
175 // numThreads() threads.
176 pool_->Schedule([this, n, &block, &barrier, &f]() { handleRange(0, n, block.size, &barrier, pool_, f); });
177 }
178
179 barrier.Wait();
180 }
181
182 // Convenience wrapper for parallelFor that does not align blocks.
183 void parallelFor(Index n, const TensorOpCost& cost, std::function<void(Index, Index)> f) const {
184 parallelFor(n, cost, nullptr, std::move(f));
185 }
186
187 // WARNING: This function is asynchronous and will not block the calling thread.
188 //
189 // Asynchronous parallelFor executes f with [0, n) arguments in parallel
190 // without waiting for completion. When the last block finished, it will call
191 // 'done' callback. F accepts a half-open interval [first, last). Block size
192 // is chosen based on the iteration cost and resulting parallel efficiency. If
193 // block_align is not nullptr, it is called to round up the block size.
194 void parallelForAsync(Index n, const TensorOpCost& cost, std::function<Index(Index)> block_align,
195 std::function<void(Index, Index)> f, std::function<void()> done) const {
196 // Compute small problems directly in the caller thread.
197 if (n <= 1 || numThreads() == 1 || CostModel::numThreads(n, cost, static_cast<int>(numThreads())) == 1) {
198 f(0, n);
199 done();
200 return;
201 }
202
203 // Compute block size and total count of blocks.
204 ParallelForBlock block = CalculateParallelForBlock(n, cost, block_align);
205
206 ParallelForAsyncContext* const ctx =
207 new ParallelForAsyncContext(block.count, block.size, pool_, std::move(f), std::move(done));
208
209 if (block.count <= numThreads()) {
210 // Avoid a thread hop by running the root of the tree and one block on the
211 // main thread.
212 handleRangeAsync(ctx, 0, n);
213 } else {
214 // Execute the root in the thread pool to avoid running work on more than
215 // numThreads() threads.
216 pool_->Schedule([ctx, n]() { handleRangeAsync(ctx, 0, n); });
217 }
218 }
219
220 // Convenience wrapper for parallelForAsync that does not align blocks.
221 void parallelForAsync(Index n, const TensorOpCost& cost, std::function<void(Index, Index)> f,
222 std::function<void()> done) const {
223 parallelForAsync(n, cost, nullptr, std::move(f), std::move(done));
224 }
225
226 // Thread pool accessor.
227 ThreadPoolInterface* getPool() const { return pool_; }
228
229 // Allocator accessor.
230 Allocator* allocator() const { return allocator_; }
231
232 private:
233 typedef TensorCostModel<ThreadPoolDevice> CostModel;
234
235 static void handleRange(Index firstIdx, Index lastIdx, Index granularity, Barrier* barrier, ThreadPoolInterface* pool,
236 const std::function<void(Index, Index)>& f) {
237 while (lastIdx - firstIdx > granularity) {
238 // Split into halves and schedule the second half on a different thread.
239 const Index midIdx = firstIdx + numext::div_ceil((lastIdx - firstIdx) / 2, granularity) * granularity;
240 pool->Schedule([=, &f]() { handleRange(midIdx, lastIdx, granularity, barrier, pool, f); });
241 lastIdx = midIdx;
242 }
243 // Single block or less, execute directly.
244 f(firstIdx, lastIdx);
245 barrier->Notify();
246 }
247
248 // For parallelForAsync we must keep passed in closures on the heap, and
249 // delete them only after `done` callback finished.
250 struct ParallelForAsyncContext {
251 ParallelForAsyncContext(Index block_count, Index block_size, ThreadPoolInterface* p,
252 std::function<void(Index, Index)> block_f, std::function<void()> done_callback)
253 : count(block_count), granularity(block_size), pool(p), f(std::move(block_f)), done(std::move(done_callback)) {}
254 ~ParallelForAsyncContext() { done(); }
255
256 std::atomic<Index> count;
257 Index granularity;
258 ThreadPoolInterface* pool;
259 std::function<void(Index, Index)> f;
260 std::function<void()> done;
261 };
262
263 // Async counterpart of handleRange: no Barrier, deletes the heap-allocated
264 // context once the final block has run.
265 static void handleRangeAsync(ParallelForAsyncContext* ctx, Index firstIdx, Index lastIdx) {
266 while (lastIdx - firstIdx > ctx->granularity) {
267 const Index midIdx = firstIdx + numext::div_ceil((lastIdx - firstIdx) / 2, ctx->granularity) * ctx->granularity;
268 ctx->pool->Schedule([ctx, midIdx, lastIdx]() { handleRangeAsync(ctx, midIdx, lastIdx); });
269 lastIdx = midIdx;
270 }
271 ctx->f(firstIdx, lastIdx);
272 if (ctx->count.fetch_sub(1) == 1) delete ctx;
273 }
274
275 struct ParallelForBlock {
276 Index size; // block size
277 Index count; // number of blocks
278 };
279
280 // Calculates block size based on (1) the iteration cost and (2) parallel
281 // efficiency. We want blocks to be not too small to mitigate parallelization
282 // overheads; not too large to mitigate tail effect and potential load
283 // imbalance and we also want number of blocks to be evenly dividable across
284 // threads.
285 ParallelForBlock CalculateParallelForBlock(const Index n, const TensorOpCost& cost,
286 std::function<Index(Index)> block_align) const {
287 const double block_size_f = 1.0 / CostModel::taskSize(1, cost);
288 const Index max_oversharding_factor = 4;
289 Index block_size = numext::mini(
290 n, numext::maxi<Index>(numext::div_ceil<Index>(n, max_oversharding_factor * numThreads()), block_size_f));
291 const Index max_block_size = numext::mini(n, 2 * block_size);
292
293 if (block_align) {
294 Index new_block_size = block_align(block_size);
295 eigen_assert(new_block_size >= block_size);
296 block_size = numext::mini(n, new_block_size);
297 }
298
299 Index block_count = numext::div_ceil(n, block_size);
300
301 // Calculate parallel efficiency as fraction of total CPU time used for
302 // computations:
303 double max_efficiency =
304 static_cast<double>(block_count) / (numext::div_ceil<Index>(block_count, numThreads()) * numThreads());
305
306 // Now try to increase block size up to max_block_size as long as it
307 // doesn't decrease parallel efficiency.
308 for (Index prev_block_count = block_count; max_efficiency < 1.0 && prev_block_count > 1;) {
309 // This is the next block size that divides size into a smaller number
310 // of blocks than the current block_size.
311 Index coarser_block_size = numext::div_ceil(n, prev_block_count - 1);
312 if (block_align) {
313 Index new_block_size = block_align(coarser_block_size);
314 eigen_assert(new_block_size >= coarser_block_size);
315 coarser_block_size = numext::mini(n, new_block_size);
316 }
317 if (coarser_block_size > max_block_size) {
318 break; // Reached max block size. Stop.
319 }
320 // Recalculate parallel efficiency.
321 const Index coarser_block_count = numext::div_ceil(n, coarser_block_size);
322 eigen_assert(coarser_block_count < prev_block_count);
323 prev_block_count = coarser_block_count;
324 const double coarser_efficiency = static_cast<double>(coarser_block_count) /
325 (numext::div_ceil<Index>(coarser_block_count, numThreads()) * numThreads());
326 if (coarser_efficiency + 0.01 >= max_efficiency) {
327 // Taking it.
328 block_size = coarser_block_size;
329 block_count = coarser_block_count;
330 if (max_efficiency < coarser_efficiency) {
331 max_efficiency = coarser_efficiency;
332 }
333 }
334 }
335
336 return {block_size, block_count};
337 }
338
339 ThreadPoolInterface* pool_;
340 int num_threads_;
341 Allocator* allocator_;
342};
343
344} // end namespace Eigen
345
346#endif // EIGEN_TENSOR_TENSOR_DEVICE_THREAD_POOL_H
Namespace containing all symbols from the Eigen library.