nk 1.0.0
C++ Numerical Computing Runtime
Loading...
Searching...
No Matches
parallel_for.hpp
1// core/include/nk/ops/parallel_for.hpp
2//
3// `parallel_for(n, threshold, fn)` — call `fn(start, end)` over a
4// non-overlapping cover of [0, n).
5//
6// * NK_WITH_THREADS undefined → always sequential:
7// `fn(0, n)` on the caller's thread.
8// * NK_WITH_THREADS defined AND n ≥ threshold AND the global
9// thread pool has more than one worker → split [0, n) evenly
10// across workers, block until all chunks finish.
11// * Otherwise (n < threshold or single-worker pool) → still
12// `fn(0, n)` sequentially. Threshold lets per-kernel callers
13// opt out of threading on small inputs where the hand-off
14// would dominate.
15//
16// SIMD kernels in *_highway.cpp TUs (src/ops/src/ and co-located module backends) use this exclusively
17// for parallelism. Output buffers must be disjoint per chunk and
18// inputs read-only — both already true for our element-wise SIMD
19// kernels.
20
21#pragma once
22
23#include <cstddef>
24
25#if defined(NK_WITH_THREADS)
26#include <nk/ops/thread_pool.hpp>
27#endif
28
29namespace nk::detail {
30
31// Suggested per-kernel parallel thresholds. Chosen so the per-call
32// overhead of the pool dispatch (~ a few µs of mutex + condvar on
33// native, ~30-100 µs per Web Worker dispatch on WASM) is well under
34// the kernel's expected wall time at that N. Tune later if profiling
35// tells us otherwise.
36//
37// `kCheapElementwiseThreshold` is for the cheapest memory-bound ops
38// (abs / + / - / .* / ./). Each element costs essentially one SIMD
39// instruction; the kernel saturates DRAM bandwidth at modest N and
40// parallelism doesn't help past ~6 workers.
41//
42// The threshold is platform-dependent because pool dispatch latency
43// is wildly different:
44// * native std::thread + condvar: a few µs per dispatch
45// * Emscripten pthread (Web Workers + SharedArrayBuffer atomics):
46// ~30-100 µs per Worker signal × 6 Workers ≈ 200-600 µs per
47// parallel_for call.
48// At 200+ µs of dispatch overhead per call, the abs kernel at N=1M
49// (~0.5 ms single-thread) gets slower with threading. Raising the
50// WASM threshold to 4 M keeps practical-size calls on the caller's
51// thread there.
52//
53// `kTranscendentalThreshold` is much smaller because sin/cos/exp/log
54// have high arithmetic intensity per element (Highway's polynomial
55// approximations) — the kernel pays for the dispatch even on small N.
56inline constexpr std::size_t kElementwiseThreshold = 16 * 1024;
57#if defined(__EMSCRIPTEN__)
58inline constexpr std::size_t kCheapElementwiseThreshold = 4 * 1024 * 1024;
59#else
60inline constexpr std::size_t kCheapElementwiseThreshold = 256 * 1024;
61#endif
62inline constexpr std::size_t kTranscendentalThreshold = 4 * 1024;
63
64// Below this N the cheapest element-wise ops (+ - .* ./) skip the Highway
65// dynamic-dispatch path and run an inline scalar loop the compiler
66// auto-vectorizes. On native (MSVC) the indirect HWY_DYNAMIC_DISPATCH call
67// into the non-inlinable per-target body loses to inline autovec at
68// cache-resident sizes — up to ~8x slower (see
69// bugs/ops/cheap-elementwise-simd-small-n). On WASM the dispatch is
70// static/inlinable and already wins on small inputs, so the gate is disabled
71// (0 → `n < 0` never trips). Set at the measured native break-even (~256K).
72#if defined(__EMSCRIPTEN__)
73inline constexpr std::size_t kSimdInlineThreshold = 0;
74#else
75inline constexpr std::size_t kSimdInlineThreshold = 256 * 1024;
76#endif
77
78// Worker caps for memory-bandwidth-bound kernels. On dual-channel
79// DDR5-6400 (≈ 100 GB/s aggregate), 4-6 workers already saturate
80// the bus on `z = x op y` and friends; more workers add signaling
81// noise without helping throughput. Compute-bound kernels (mtimes,
82// transcendentals) don't need a cap and benefit from every core.
83inline constexpr int kElementwiseMaxWorkers = 6;
84
85template <typename F>
86inline void parallel_for(std::size_t n, std::size_t threshold, F &&fn,
87 int max_workers = 0)
88{
89#if defined(NK_WITH_THREADS)
90 auto &pool = ThreadPool::global();
91 if (n >= threshold && pool.workers() > 1) {
92 pool.run(n, std::forward<F>(fn), max_workers);
93 return;
94 }
95#else
96 (void)threshold;
97 (void)max_workers;
98#endif
99 if (n > 0)
100 fn(static_cast<std::size_t>(0), n);
101}
102
103} // namespace nk::detail