Clustering
C++20 header-only: DBSCAN, HDBSCAN, k-means.
Loading...
Searching...
No Matches
thread.h
Go to the documentation of this file.
1#pragma once
2
3#include <citor/cancellation.h>
4#include <citor/hints.h>
5#include <citor/thread_pool.h>
6
7#include <algorithm>
8#include <cstddef>
9#include <cstdint>
10#include <memory>
11#include <mutex>
12#include <span>
13#include <thread>
14#include <unordered_map>
15#include <utility>
16#include <vector>
17
18#include "clustering/math/detail/coherence_cache.h"
19
20namespace clustering::math {
21
28using OwnedPool = citor::ThreadPool;
29
46inline OwnedPool &sharedPool(std::size_t nJobs);
47
59inline std::size_t clampedJobCount(std::size_t nJobs) noexcept {
60 if (nJobs == 0) {
61 const std::size_t hw = std::thread::hardware_concurrency();
62 return hw == 0 ? std::size_t{1} : hw;
63 }
64 return nJobs;
65}
66
82inline bool shouldSpawnPool(std::size_t totalOps, std::size_t nJobs,
83 std::size_t minOpsPerWorker = std::size_t{1} << 15) noexcept {
84 if (nJobs <= 1) {
85 return false;
86 }
87 return (totalOps / nJobs) >= minOpsPerWorker;
88}
89
109struct Pool {
111 OwnedPool *pool = nullptr;
112
118 [[nodiscard]] std::size_t workerCount() const noexcept {
119 if (pool == nullptr) {
120 return std::size_t{1};
121 }
122 return pool->participants();
123 }
124
131 [[nodiscard]] static std::size_t workerIndex() noexcept {
132 return citor::ThreadPool::workerIndex();
133 }
134
147 [[nodiscard]] bool shouldParallelize(std::size_t totalWork, std::size_t minChunk,
148 std::size_t minTasksPerWorker = 2) const noexcept {
149 if (pool == nullptr || minChunk == 0) {
150 return false;
151 }
152 return (totalWork / minChunk) >= (workerCount() * minTasksPerWorker);
153 }
154
168 [[nodiscard]] bool shouldParallelizeWork(std::size_t totalOps,
169 std::size_t minOpsPerWorker = std::size_t{1}
170 << 15) const noexcept {
171 if (pool == nullptr) {
172 return false;
173 }
174 return (totalOps / workerCount()) >= minOpsPerWorker;
175 }
176
191 [[nodiscard]] std::size_t stealBlocks(std::size_t n,
192 std::size_t minRowsPerBlock = 256) const noexcept {
193 const std::size_t workers = workerCount();
194 if (workers <= 1 || n == 0) {
195 return workers;
196 }
197 const std::size_t byRows =
198 (minRowsPerBlock == 0) ? n : std::max<std::size_t>(1, n / minRowsPerBlock);
199 const std::size_t blocks = std::min(workers * 8, byRows);
200 return std::max(blocks, workers);
201 }
202
212 template <class HintsT = citor::HintsDefaults, class FnA, class FnB>
213 void forkJoin2(FnA &&a, FnB &&b) const {
214 if (pool == nullptr) {
215 a();
216 b();
217 return;
218 }
219 pool->template forkJoin<HintsT>(std::forward<FnA>(a), std::forward<FnB>(b));
220 }
221
238 template <class HintsT = citor::HintsDefaults, class Body>
239 void parallelForBlocks(std::size_t first, std::size_t last, std::size_t numBlocks, Body body) {
240 if (pool == nullptr || first >= last) {
241 body(first, last);
242 return;
243 }
244 if (numBlocks != 0) {
245 parallelForExactBlocks<HintsT>(first, last, numBlocks, std::move(body));
246 return;
247 }
248 pool->template parallelFor<HintsT>(first, last, body);
249 }
250
267 template <class HintsT = citor::HintsDefaults, class Body>
268 void parallelForExactBlocks(std::size_t first, std::size_t last, std::size_t numBlocks,
269 Body body) {
270 if (first >= last || numBlocks == 0) {
271 return;
272 }
273 if (pool == nullptr || numBlocks == 1) {
274 body(first, last);
275 return;
276 }
277 const std::size_t span = last - first;
278 auto sliceLo = [first, span, numBlocks](std::size_t s) noexcept {
279 return first + ((span * s) / numBlocks);
280 };
281 pool->template parallelFor<HintsT>(std::size_t{0}, numBlocks,
282 [&](std::size_t loSlot, std::size_t hiSlot) {
283 for (std::size_t s = loSlot; s < hiSlot; ++s) {
284 const std::size_t blockLo = sliceLo(s);
285 const std::size_t blockHi = sliceLo(s + 1);
286 body(blockLo, blockHi);
287 }
288 });
289 }
290
306 template <class HintsT = citor::HintsDefaults, class Body>
307 void parallelForExactBlocksWithSlot(std::size_t first, std::size_t last, std::size_t numBlocks,
308 Body body) {
309 if (first >= last || numBlocks == 0) {
310 return;
311 }
312 if (pool == nullptr || numBlocks == 1) {
313 body(first, last, std::size_t{0});
314 return;
315 }
316 const std::size_t span = last - first;
317 auto sliceLo = [first, span, numBlocks](std::size_t s) noexcept {
318 return first + ((span * s) / numBlocks);
319 };
320 pool->template parallelFor<HintsT>(std::size_t{0}, numBlocks,
321 [&](std::size_t loSlot, std::size_t hiSlot) {
322 for (std::size_t s = loSlot; s < hiSlot; ++s) {
323 body(sliceLo(s), sliceLo(s + 1), s);
324 }
325 });
326 }
327
341 template <class HintsT = citor::HintsDefaults, class Body>
342 void parallelForChunks(std::size_t numChunks, Body body) {
343 if (pool == nullptr || numChunks == 0) {
344 for (std::size_t c = 0; c < numChunks; ++c) {
345 body(c);
346 }
347 return;
348 }
349 pool->template parallelFor<HintsT>(std::size_t{0}, numChunks,
350 [&](std::size_t lo, std::size_t hi) {
351 for (std::size_t c = lo; c < hi; ++c) {
352 body(c);
353 }
354 });
355 }
356
375 template <class HintsT = citor::HintsDefaults, class T, class Map, class Combine>
376 [[nodiscard]] T parallelReduce(std::size_t first, std::size_t last, T init, Map map,
377 Combine combine) {
378 if (first >= last) {
379 return init;
380 }
381 if (pool == nullptr) {
382 return combine(std::move(init), map(first, last));
383 }
384 return pool->template parallelReduce<HintsT>(first, last, std::move(init), std::move(map),
385 std::move(combine));
386 }
387
410 template <class HintsT = citor::HintsDefaults, class Phase>
411 void parallelRunPlex(std::size_t nPhases, std::size_t n, Phase phaseFn) {
412 auto noPrePhase = [](std::size_t /*phaseIdx*/) noexcept {};
413 parallelRunPlex<HintsT>(nPhases, n, std::move(phaseFn), noPrePhase);
414 }
415
443 template <class HintsT = citor::HintsDefaults, class T, class BodyFn, class PrefixFn>
444 T parallelScan(std::size_t n, T identity, BodyFn body, PrefixFn prefix) {
445 if (n == 0) {
446 return identity;
447 }
448 if (pool == nullptr) {
449 T partial = body(std::size_t{0}, std::size_t{0}, n, identity, static_cast<T *>(nullptr));
450 return prefix(std::move(identity), std::move(partial));
451 }
452 return pool->template parallelScan<HintsT>(n, std::move(identity), std::move(body),
453 std::move(prefix));
454 }
455
472 template <class HintsT = citor::HintsDefaults, class T, class PrefixFn>
473 [[nodiscard]] T inclusiveScan(std::span<const T> in, std::span<T> out, T identity,
474 PrefixFn prefix) {
475 if (pool == nullptr) {
476 T acc = identity;
477 for (std::size_t i = 0; i < in.size(); ++i) {
478 acc = prefix(acc, in[i]);
479 out[i] = acc;
480 }
481 return acc;
482 }
483 return pool->template inclusiveScan<HintsT>(in, out, std::move(identity), std::move(prefix));
484 }
485
493 template <class HintsT = citor::HintsDefaults, class Phase, class PrePhase>
494 void parallelRunPlex(std::size_t nPhases, std::size_t n, Phase phaseFn, PrePhase prePhaseFn,
495 citor::CancellationToken tok = citor::CancellationToken{}) {
496 if (nPhases == 0) {
497 return;
498 }
499 if (pool == nullptr) {
500 // Single-slot inline emulation when no pool is attached; mirrors the backend's
501 // phase-boundary token check.
502 for (std::size_t p = 0; p < nPhases; ++p) {
503 if (tok.stop_requested()) {
504 return;
505 }
506 prePhaseFn(p);
507 phaseFn(p, std::uint32_t{0}, std::size_t{0}, n, static_cast<void *>(nullptr));
508 }
509 return;
510 }
511 pool->template runPlex<HintsT>(nPhases, n, std::forward<Phase>(phaseFn),
512 std::forward<PrePhase>(prePhaseFn), std::move(tok));
513 }
514};
515
516inline OwnedPool &sharedPool(std::size_t nJobs) {
517 const std::size_t effective = clampedJobCount(nJobs);
518 // Inline-static map: shared across translation units thanks to the inline keyword on the
519 // enclosing function. Mutex protects the registry against first-time-init races; lookups
520 // are O(1) and gated by Python's GIL or other caller-side serialization in practice.
521 static std::unordered_map<std::size_t, std::unique_ptr<OwnedPool>> registry;
522 static std::mutex registryMutex;
523 const std::scoped_lock guard{registryMutex};
524 auto &slot = registry[effective];
525 if (!slot) {
526 // Seed citor's coherence-probe cache from disk before constructing the pool
527 // so a short-lived or single-fit process skips the live inter-core
528 // calibration. On a cold cache the ctor runs the probe; persist the fresh
529 // result so the next process replays it.
530 const bool seeded = detail::importPersistedCoherenceProbe(effective);
531 slot = std::make_unique<OwnedPool>(effective);
532 if (!seeded) {
533 detail::exportPersistedCoherenceProbe(*slot, effective);
534 }
535 }
536 return *slot;
537}
538
539} // namespace clustering::math
std::size_t clampedJobCount(std::size_t nJobs) noexcept
Clamp a caller-supplied nJobs to a valid worker count.
Definition thread.h:59
bool shouldSpawnPool(std::size_t totalOps, std::size_t nJobs, std::size_t minOpsPerWorker=std::size_t{1}<< 15) noexcept
Decide whether spawning a pool with nJobs workers is worth it for totalOps of arithmetic work.
Definition thread.h:82
citor::ThreadPool OwnedPool
Type alias for the owning pool the algorithm wrappers (KMeans, DBSCAN, HDBSCAN) hold inside an std::o...
Definition thread.h:28
OwnedPool & sharedPool(std::size_t nJobs)
Process-wide pool registry, keyed by worker count.
Definition thread.h:516
Thin compile-time-templated wrapper around the underlying OwnedPool.
Definition thread.h:109
static std::size_t workerIndex() noexcept
Stable index of the calling worker thread within the owning pool.
Definition thread.h:131
void parallelRunPlex(std::size_t nPhases, std::size_t n, Phase phaseFn)
Run phaseFn for nPhases persistent-worker phases over [0, n).
Definition thread.h:411
OwnedPool * pool
Underlying pool, or nullptr to force serial execution.
Definition thread.h:111
void parallelForExactBlocks(std::size_t first, std::size_t last, std::size_t numBlocks, Body body)
Run body in parallel with exactly numBlocks contiguous ranges.
Definition thread.h:268
std::size_t workerCount() const noexcept
Number of worker threads available, or 1 in serial mode.
Definition thread.h:118
void parallelForChunks(std::size_t numChunks, Body body)
Run body once per chunk over [0, numChunks) in parallel.
Definition thread.h:342
void forkJoin2(FnA &&a, FnB &&b) const
Run two independent tasks as a fork-join pair.
Definition thread.h:213
void parallelRunPlex(std::size_t nPhases, std::size_t n, Phase phaseFn, PrePhase prePhaseFn, citor::CancellationToken tok=citor::CancellationToken{})
Pre-phase form of parallelRunPlex with cooperative cancellation.
Definition thread.h:494
void parallelForBlocks(std::size_t first, std::size_t last, std::size_t numBlocks, Body body)
Run body in parallel over [first, last) partitioned into numBlocks blocks.
Definition thread.h:239
std::size_t stealBlocks(std::size_t n, std::size_t minRowsPerBlock=256) const noexcept
Block count for a fan-out over n rows that lets work-stealing balance heterogeneous cores.
Definition thread.h:191
T inclusiveScan(std::span< const T > in, std::span< T > out, T identity, PrefixFn prefix)
Buffer-to-buffer inclusive prefix scan of in into out.
Definition thread.h:473
bool shouldParallelize(std::size_t totalWork, std::size_t minChunk, std::size_t minTasksPerWorker=2) const noexcept
Decide whether totalWork warrants parallel dispatch.
Definition thread.h:147
bool shouldParallelizeWork(std::size_t totalOps, std::size_t minOpsPerWorker=std::size_t{1}<< 15) const noexcept
Decide whether totalOps warrants parallel dispatch, based on work volume.
Definition thread.h:168
void parallelForExactBlocksWithSlot(std::size_t first, std::size_t last, std::size_t numBlocks, Body body)
Slot-aware variant of parallelForExactBlocks.
Definition thread.h:307
T parallelScan(std::size_t n, T identity, BodyFn body, PrefixFn prefix)
Two-pass exclusive-prefix scan over [0, n).
Definition thread.h:444
T parallelReduce(std::size_t first, std::size_t last, T init, Map map, Combine combine)
Reduce [first, last) with the backend's reduction primitive.
Definition thread.h:376