Skip to main content

rustymath_core/
parallel.rs

1//! Splitting work across threads. [`pieces`] cuts a call's items into pieces of the sizes its caller gives, and [`ranges`] into as many pieces as the time of its first unit justifies. The pieces run on a Rayon pool with Rayon's default number of threads: `RAYON_NUM_THREADS` where it's set, and otherwise one for each core. [`set_threads`] changes that number for the whole process. A call made on one of a pool's threads, such as a function that another call's piece runs, doesn't split again. In the browser build, which has no threads, every call runs its pieces in turn on the calling thread.
2use crate::slice::{Arg, CHUNK};
3use crate::{error::Error, interrupt::check};
4/// The clock that `parallel`'s rounds and the speed tests' timings read: the browser's in a browser build, where `std::time::Instant` panics.
5#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
6pub use std::time::Instant;
7use std::{mem::MaybeUninit, ops::Range, sync::atomic::{AtomicUsize, Ordering::Relaxed}, time::Duration};
8#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
9pub use web_time::Instant;
10
11/// The number of threads that calls split across, for the whole process, or 0 for the default pool's number.
12static THREADS: AtomicUsize = AtomicUsize::new(0);
13
14/// Sets how many threads calls split their work across, for the whole process, and gives the number it replaces. 0 restores the default. 1 runs every call on its calling thread, as the speed tests do for their budgets.
15pub fn set_threads(n: usize) -> usize { THREADS.swap(n, Relaxed) }
16
17/// The number that [`set_threads`] last set. It starts at 0, the default.
18pub fn thread_setting() -> usize { THREADS.load(Relaxed) }
19
20/// How many threads the calls made on this thread split their work across: the process's [`set_threads`] count, or 1 on a pool's worker thread.
21pub fn threads() -> usize { requested().map_or(1, |n| if n == 0 { pool::get(0).current_num_threads() } else { n }) }
22
23/// Whether calls made on this thread run serially: after `set_threads(1)`, or on a pool's worker thread.
24pub(crate) fn serial() -> bool { requested().is_none() }
25
26/// The pool that calls made on this thread split across, as its number of threads, 0 for the default. `None` where they run on this thread.
27fn requested() -> Option<usize> { let n = THREADS.load(Relaxed); (n != 1 && !pool::inside()).then_some(n) }
28
29/// Runs `piece` on each range that cuts `0..n` into pieces of `size` items, the last one shorter, and gives whether every call said `true`. Every piece runs, whatever another one gives. The pieces run across at most `most` of this thread's [`threads`], on a smaller pool where `most` or the number of pieces is below the pool's threads, as [`sized`] chooses. Where either is 1, they run in order on this thread.
30pub(crate) fn pieces(n: usize, size: usize, most: usize, piece: &(dyn Fn(Range<usize>) -> bool + Sync)) -> bool {
31    let (count, range) = (n.div_ceil(size), |p: usize| p * size..n.min((p + 1) * size));
32    if count.min(most) > 1 && let Some(threads) = requested() { return pool::run(sized(threads, count.min(most)), count, &|p| piece(range(p))); }
33    (0..count).fold(true, |ok, p| piece(range(p)) & ok)
34}
35
36/// The pool that `count` pieces run on, where this thread's calls split across the pool of `threads` threads, 0 for the default. Where there are fewer pieces than that pool's threads, it's the pool of the largest power of two up to `count`. On an M5 Max, `-v` on a million items, cut into about 4 pieces, took 0.33 copies on a 4-thread pool and 0.5 to 0.8 on pools of 12 to 18 threads.
37fn sized(threads: usize, count: usize) -> usize {
38    let all = if threads == 0 { pool::get(0).current_num_threads() } else { threads };
39    if count >= all { threads } else { 1 << count.ilog2() }
40}
41
42/// Runs `write` on each piece of `size` items that cuts `out`, the last one shorter, across at most `most` threads as [`pieces`] runs its ranges, and gives whether every call said `true`. `write(start, items)` writes `items`, the piece of `out` from `start`.
43pub fn pieces_of<B: Send>(out: &mut [B], size: usize, most: usize, write: &(dyn Fn(usize, &mut [B]) -> bool + Sync)) -> bool {
44    let (shared, n) = (Shared(out.as_mut_ptr()), out.len());
45    // SAFETY: `pieces` gives each call its own range within `0..n`.
46    pieces(n, size, most, &|range| write(range.start, unsafe { shared.part(range) }))
47}
48
49/// An output slice shared only through disjoint ranges.
50pub struct Shared<B>(*mut B);
51// SAFETY: each piece reaches only the items in its own range, so no two threads reach the same item.
52unsafe impl<B: Send> Sync for Shared<B> {}
53impl<B> Shared<B> {
54    /// The items in `range`. SAFETY: the range must lie within the output, and no other caller may hold any of these items at the same time.
55    #[inline]
56    unsafe fn part(&self, range: Range<usize>) -> &mut [B] { unsafe { std::slice::from_raw_parts_mut(self.0.add(range.start), range.len()) } }
57}
58
59/// The time on one thread that each piece of a [`ranges`] call is given at least. A call whose pieces would be shorter splits into fewer of them, because waking a thread for a shorter piece costs more than the piece saves. On an M5 Max, `v+w` on a million items, about 200 µs on one thread, took 0.51 copies split across 4 threads and 0.81 across all 18.
60pub const PIECE_TIME: Duration = Duration::from_micros(30);
61
62/// Runs `run` on disjoint ranges covering `0..n`, and gives whether every call said `true`. Every range runs, even after a failure. Each range contains whole units of `unit` items, except that the last unit may be shorter. A call of at most one unit, or one whose calls run on this thread, runs whole on this thread. A longer call times its first unit on this thread, then cuts the rest into as many pieces as that estimate gives [`PIECE_TIME`] each, at most four for each of this thread's [`threads`]. It runs them as [`pieces`] does, or runs the rest in one call on this thread where that gives fewer than two pieces. This non-generic, out-of-line scheduler keeps Rayon's machinery out of its callers. An empty call runs `0..0` once. Panics if `unit` is zero.
63#[inline(never)]
64pub fn ranges(n: usize, unit: usize, run: &(dyn Fn(Range<usize>) -> bool + Sync)) -> bool {
65    assert!(unit > 0, "a work unit must contain at least one item");
66    if n <= unit || serial() { return run(0..n); }
67    let start = Instant::now();
68    let ok = run(0..unit);
69    let units = (n - unit).div_ceil(unit);
70    let count = ((start.elapsed().as_secs_f64() * units as f64 / PIECE_TIME.as_secs_f64()) as usize).min(4 * threads()).min(units);
71    if count < 2 { return ok & run(unit..n); }
72    ok & pieces(n - unit, units.div_ceil(count) * unit, usize::MAX, &|range| run(unit + range.start..unit + range.end))
73}
74
75/// Runs `write(start, items)` over disjoint pieces of `out`, using [`ranges`] in units of `unit` items, and gives whether every call said `true`. An empty output calls `write` once. Panics if `unit` is zero.
76#[inline(never)]
77pub fn split<B: Send>(out: &mut [B], unit: usize, write: &(dyn Fn(usize, &mut [B]) -> bool + Sync)) -> bool {
78    let (shared, n) = (Shared(out.as_mut_ptr()), out.len());
79    // SAFETY: `ranges` gives each call its own range within `0..n`.
80    ranges(n, unit, &|range| write(range.start, unsafe { shared.part(range) }))
81}
82
83/// Adds the sums of every piece of `0..n` into `sums`, where `piece(range, sums)` adds the sums of the piece `range` into the `sums` it's given. `0..n` splits into one piece of at least `least` items for each of this thread's [`threads`]. With too few items for two pieces, `0..n` runs as one piece on this thread. Otherwise the first piece adds into `sums`, each later one into its own part of `parts`, and the parts then add into `sums` on this thread.
84pub fn summed<T: Copy + Send + num_traits::Zero>(n: usize, least: usize, sums: &mut [T], parts: &mut Vec<T>, piece: &(dyn Fn(Range<usize>, &mut [T]) + Sync)) {
85    let pieces = (n / least).min(threads());
86    if pieces <= 1 { return piece(0..n, sums); }
87    let per = n.div_ceil(pieces);
88    let (len, width) = (sums.len(), sums.len().max(1));
89    parts.clear();
90    parts.resize((n.div_ceil(per) - 1) * width, T::zero());
91    {
92        let mut work: Vec<&mut [T]> = std::iter::once(&mut *sums).chain(parts.chunks_mut(width).map(|part| &mut part[..len])).collect();
93        pieces_of(&mut work, 1, usize::MAX, &|p, work| { piece(p * per..n.min((p + 1) * per), &mut work[0]); true });
94    }
95    for part in parts.chunks(width) { for (s, &x) in sums.iter_mut().zip(part) { *s = *s + x } }
96}
97
98/// [`summed`] over the units of `items`, each of `unit` items, where `piece(first, items, sums)` gets the items of the piece whose first unit is `first`.
99pub fn summed_of<B: Send, T: Copy + Send + num_traits::Zero>(
100    items: &mut [B], unit: usize, least: usize, sums: &mut [T], parts: &mut Vec<T>, piece: &(dyn Fn(usize, &mut [B], &mut [T]) + Sync)) {
101    let shared = Shared(items.as_mut_ptr());
102    // SAFETY: `summed` gives each call its own range of units, within `items`.
103    summed(items.len() / unit, least, sums, parts, &|units, sums| piece(units.start, unsafe { shared.part(units.start * unit..units.end * unit) }, sums))
104}
105
106/// The most cells that [`each_cell`] runs alone at its start, to time a cell.
107const CELLS: usize = 8;
108
109/// The pieces that each thread is given in a round of an [`each_cell`] call, so that a thread that finishes early takes more of the round.
110const PIECES: usize = 4;
111
112/// The time that each round of an [`each_cell`] call gives each thread, where its cells are that short. A stack stops within about a round of an interrupt, or within one cell where a cell takes longer.
113const ROUND_TIME: Duration = Duration::from_millis(20);
114
115/// Runs `write(i, scratch)` on each cell `i` of a stack of `count` cells, and gives whether every call said `true`. Cells that run in turn on one thread share one `S` for their scratch buffers, such as an `LuWork`. It checks for an interrupt on this thread between rounds, and an interrupt's error stops the call. It makes no check before the first cell, which a caller's own check covers, so a stack of one small cell costs no check.
116///
117/// The first cells run alone on this thread, to time a cell: up to `CELLS` of them, or [`PIECE_TIME`] of work, because one cell alone reads high from a cold start. Each later round holds about `ROUND_TIME` of cells for each of this thread's [`threads`], at least one each. A round's cells split into about `PIECES` pieces for each thread, each of at least `PIECE_TIME` of work. A cell that runs on this thread can split its own work, and one on another thread doesn't. On all 18 threads of an M5 Max, splitting stacks of 8 to 16 matrices of 128² to 512² was 1.5 to 5 times as fast as running their cells in turn, each split across the threads. Three inverses of 1024² were 25% slower. In the browser build, which has one thread, each round's cells run in turn. A stack of one cell runs alone, with nothing to time.
118fn each_cell<S: Default>(count: usize, write: impl Fn(usize, &mut S) -> bool + Sync) -> Result<bool, Error> {
119    let mut scratch = S::default();
120    if count <= 1 { return Ok(count == 0 || write(0, &mut scratch)); }
121    let threads = threads();
122    let start = Instant::now();
123    let mut ok = write(0, &mut scratch);
124    // The timed cells after a short first cell skip the clock read between them. A stack of 64 tiny products spent about a sixth of its time reading the clock.
125    let short = start.elapsed() * CELLS as u32 <= PIECE_TIME;
126    let mut timed = 1;
127    while timed < count.min(CELLS) && (short || start.elapsed() < PIECE_TIME) {
128        ok &= write(timed, &mut scratch);
129        timed += 1;
130    }
131    let cell = start.elapsed().as_secs_f64().max(1e-9) / timed as f64;
132    let round = ((ROUND_TIME.as_secs_f64() / cell) as usize).max(1).saturating_mul(threads);
133    let least = (PIECE_TIME.as_secs_f64() / cell).ceil() as usize;
134    for first in (timed..count).step_by(round) {
135        check()?;
136        let cells = round.min(count - first);
137        if threads == 1 {
138            ok &= (first..first + cells).fold(true, |ok, i| write(i, &mut scratch) & ok);
139        } else {
140            ok &= pieces(cells, cells.div_ceil(threads * PIECES).max(least).max(1), usize::MAX, &|range| {
141                let mut scratch = S::default();
142                range.map(|c| first + c).fold(true, |ok, i| write(i, &mut scratch) & ok)
143            });
144        }
145    }
146    Ok(ok)
147}
148
149/// A stack's outputs: a tuple of one to three `Vec`s, each holding its own number of items for every cell in turn. [`stack`] gives each cell its part of each output, as a QR stack writes Q and R and an SVD stack U, S and V.
150pub trait Outputs: Sized {
151    /// The items that each cell writes in each output.
152    type Per: Copy + Sync;
153    /// One cell's part of each output.
154    type Cell<'a>
155    where
156        Self: 'a;
157    /// The outputs' items, shared with the threads that write their cells.
158    type Shared: Sync;
159    /// The outputs of `count` cells, each item at its default. `vec!` asks the allocator for zeroed memory where that default is zero bits, as for real numbers and integers, so no pass writes a large output before its cells do.
160    fn new(count: usize, per: Self::Per) -> Self;
161    /// Pointers to the outputs' items.
162    fn shared(&mut self) -> Self::Shared;
163    /// Cell `i`'s part of each output. SAFETY: `i` must be below the outputs' number of cells, and no other caller may hold cell `i`'s part at the same time.
164    unsafe fn cell<'a>(shared: &'a Self::Shared, per: Self::Per, i: usize) -> Self::Cell<'a> where Self: 'a;
165}
166
167macro_rules! outputs {
168    ($n:literal: $($t:ident $k:tt),+) => {
169        impl<$($t: Clone + Default + Send + 'static),+> Outputs for ($(Vec<$t>,)+) {
170            type Per = [usize; $n];
171            type Cell<'a> = ($(&'a mut [$t],)+) where Self: 'a;
172            type Shared = ($(Shared<$t>,)+);
173            fn new(count: usize, per: [usize; $n]) -> Self { ($(vec![<$t>::default(); count * per[$k]],)+) }
174            fn shared(&mut self) -> Self::Shared { ($(Shared(self.$k.as_mut_ptr()),)+) }
175            unsafe fn cell<'a>(shared: &'a Self::Shared, per: [usize; $n], i: usize) -> Self::Cell<'a> where Self: 'a {
176                // SAFETY: as the caller promises.
177                ($(unsafe { shared.$k.part(i * per[$k]..(i + 1) * per[$k]) },)+)
178            }
179        }
180    };
181}
182outputs!(1: A 0);
183outputs!(2: A 0, B 1);
184outputs!(3: A 0, B 1, C 2);
185
186/// Runs `write(i, parts, scratch)` on each cell `i` of a stack of `count` cells, as [`each_cell`] runs them, and gives the outputs with whether every call said `true`. `parts` is cell `i`'s part of each output `k`: `per[k]` items from `i * per[k]`.
187pub fn stack<O: Outputs + 'static, S: Default>(
188    count: usize, per: O::Per, write: &(dyn Fn(usize, O::Cell<'_>, &mut S) -> bool + Sync)) -> Result<(O, bool), Error> {
189    let mut out = O::new(count, per);
190    let shared = out.shared();
191    // SAFETY: `each_cell` gives each cell below `count` to one call.
192    let ok = each_cell(count, |i, scratch| write(i, unsafe { O::cell(&shared, per, i) }, scratch))?;
193    Ok((out, ok))
194}
195
196/// `f(i, scratch)` of each cell `i` of a stack of `count` cells, in order, as [`each_cell`] runs them: [`stack`] for a result of any type, one for each cell.
197pub fn each<R: Send, S: Default>(count: usize, f: impl Fn(usize, &mut S) -> R + Sync) -> Result<Vec<R>, Error> {
198    let mut out: Vec<Option<R>> = (0..count).map(|_| None).collect();
199    let shared = Shared(out.as_mut_ptr());
200    // SAFETY: `each_cell` gives each cell below `count` to one call.
201    each_cell(count, |i, scratch| {
202        let cell = unsafe { shared.part(i..i + 1) };
203        cell[0] = Some(f(i, scratch));
204        true
205    })?;
206    Ok(out.into_iter().map(|c| c.expect("`each_cell` runs every cell")).collect())
207}
208
209/// Appends `n` items to `out`, and gives whether every call of `write` said `true`. `write(range, slots)` fills `slots` with the items in `range`, in order. The calls split across threads as [`split`] splits a call, in units of `unit` items. A call that leaves any of its slots unfilled panics, and `out` keeps its length.
210pub fn extended<B: Copy + Send>(out: &mut Vec<B>, n: usize, unit: usize, write: &(dyn Fn(Range<usize>, &mut Slots<'_, B>) -> bool + Sync)) -> bool {
211    unsafe {
212        crate::slice::appended(out, n, |slots| {
213            split(slots, unit, &|start, slots| {
214                let end = start + slots.len();
215                filled(slots, |slots| write(start..end, slots))
216            })
217        })
218    }
219}
220
221/// `write`'s result for `slots`, which it fills in order through one [`Slots`]. Panics where it leaves any unfilled.
222fn filled<B, T>(slots: &mut [MaybeUninit<B>], write: impl FnOnce(&mut Slots<'_, B>) -> T) -> T {
223    let mut slots = Slots { slots, written: 0 };
224    let result = write(&mut slots);
225    assert_eq!(slots.written, slots.slots.len(), "a writer fills every slot it gets");
226    result
227}
228
229/// The slots of a vector that a writer fills in order: one call's part of an [`extended`] vector, or the items that [`Slots::append`] adds.
230pub struct Slots<'a, B> { slots: &'a mut [MaybeUninit<B>], written: usize }
231
232impl<B> Slots<'_, B> {
233    /// Appends `n` items to `out` on this thread, and gives `write`'s result. `write` fills the slots in order. Where it leaves any unfilled, `append` panics and `out` keeps its length.
234    pub fn append<T>(out: &mut Vec<B>, n: usize, write: impl FnOnce(&mut Slots<'_, B>) -> T) -> T {
235        unsafe { crate::slice::appended(out, n, |slots| filled(slots, write)) }
236    }
237    /// Writes the `n` results of [`pairs`](crate::pairs) after the items already written, on this thread, and gives whether every result succeeded. Panics where they don't fit.
238    pub fn pairs<X: Copy, Y: Copy>(&mut self, x: Arg<'_, X>, y: Arg<'_, Y>, n: usize, f: impl Fn(X, Y) -> (B, bool)) -> bool {
239        unsafe { self.fill(n, |slots| crate::slice::pairs_into(x, y, n, slots, f)) }
240    }
241    /// Writes `items` after the items already written, on this thread. Panics where they don't fit.
242    pub fn copy(&mut self, items: &[B]) where B: Copy {
243        unsafe {
244            self.fill(items.len(), |slots| {
245                for (slot, &item) in slots.iter_mut().zip(items) { slot.write(item); }
246                true
247            })
248        };
249    }
250    /// `write`'s result for the next `n` slots, which it must fill. They then count as written. Panics where they don't fit.
251    ///
252    /// # Safety
253    /// `write` must initialize every slot it receives before returning, whether it returns `true` or `false`.
254    pub unsafe fn fill(&mut self, n: usize, write: impl FnOnce(&mut [MaybeUninit<B>]) -> bool) -> bool {
255        let ok = write(&mut self.slots[self.written..self.written + n]);
256        self.written += n;
257        ok
258    }
259}
260
261/// Runs `write` on each `CHUNK` of `slots` in turn, the results for the items from `start`, and gives whether every call said `true`. A slice form that makes several passes over buffers of its own keeps them in cache this way.
262#[inline]
263pub(crate) fn in_chunks<B>(start: usize, slots: &mut [MaybeUninit<B>], write: impl Fn(usize, &mut [MaybeUninit<B>]) -> bool) -> bool {
264    let (n, mut ok) = (slots.len(), true);
265    for at in (0..n).step_by(CHUNK) { ok &= write(start + at, &mut slots[at..n.min(at + CHUNK)]) }
266    ok
267}
268
269#[cfg(not(target_arch = "wasm32"))]
270mod pool {
271    use std::sync::{Arc, Mutex};
272
273    /// The pools built in this process, each with its number of threads, 0 for the default, and the id of the process that built them. A child that `fork` creates has none of its parent's threads, so it builds pools of its own.
274    static POOLS: Mutex<(u32, Vec<(usize, Arc<rayon::ThreadPool>)>)> = Mutex::new((0, Vec::new()));
275
276    /// The pool of `n` threads, 0 for Rayon's default number, built the first time a call needs it.
277    pub(super) fn get(n: usize) -> Arc<rayon::ThreadPool> {
278        let mut pools = POOLS.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
279        let id = std::process::id();
280        if pools.0 != id {
281            // Dropping a parent's pool would signal threads that this process doesn't have.
282            std::mem::forget(std::mem::take(&mut pools.1));
283            pools.0 = id;
284        }
285        if let Some((_, pool)) = pools.1.iter().find(|(k, _)| *k == n) { return pool.clone(); }
286        let pool = Arc::new(rayon::ThreadPoolBuilder::new().num_threads(n).build().expect("a thread pool"));
287        pools.1.push((n, pool.clone()));
288        pool
289    }
290
291    /// Whether this thread is one of a pool's.
292    pub(super) fn inside() -> bool { rayon::current_thread_index().is_some() }
293
294    /// Runs `f` on each of `count` pieces across the pool of `threads` threads, and gives whether every call said `true`.
295    pub(super) fn run(threads: usize, count: usize, f: &(dyn Fn(usize) -> bool + Sync)) -> bool {
296        use rayon::prelude::*;
297        let error = crate::error::ErrorSlot::current();
298        get(threads).install(|| {
299            (0..count).into_par_iter().map(|item| match &error { Some(slot) => slot.within(|| f(item)), None => f(item) }).reduce(|| true, |a, b| a & b)
300        })
301    }
302}
303
304#[cfg(target_arch = "wasm32")]
305mod pool {
306    pub(super) struct Pool;
307    impl Pool { pub(super) fn current_num_threads(&self) -> usize { 1 } }
308    pub(super) fn get(_: usize) -> Pool { Pool }
309    pub(super) fn inside() -> bool { true }
310    pub(super) fn run(_: usize, count: usize, f: &dyn Fn(usize) -> bool) -> bool { (0..count).fold(true, |ok, p| f(p) & ok) }
311}