Skip to main content

strided_basic/
threading.rs

1//! Parallel execution for strided operations.
2//!
3//! Faithfully ports Julia Strided.jl's `_mapreduce_threaded!` recursive
4//! dimension-splitting strategy. Every split goes through
5//! [`threadteam::Exec::fork`] or [`threadteam::Exec::for_ranges`]: the
6//! token carries the budget and the pool, so this module holds no placement
7//! state of its own.
8
9use crate::cost::KernelClass;
10pub(crate) use crate::cost::{for_len, is_parallel};
11use crate::inline::InlineVec;
12use crate::kernel::for_each_inner_block_preordered;
13use crate::Result;
14use core::num::NonZeroUsize;
15use std::ops::Range;
16use threadteam::Exec;
17
18/// Stack-allocated Vec for dims/offsets in recursive threading.
19/// 8 elements covers up to 8-dimensional arrays (after fusion, typically 2-4).
20type SVec<T> = InlineVec<T>;
21
22const ONE: NonZeroUsize = NonZeroUsize::MIN;
23
24/// A raw pointer wrapper that is `Send` + `Sync`.
25///
26/// # Safety
27/// The caller must guarantee that the pointed-to data is valid for the
28/// lifetime of any parallel operation and that no data races occur
29/// (e.g., different threads write to disjoint regions).
30/// Raw address transport for scoped kernel workers, without dereference or ownership.
31/// Storage liveness, aliasing and cross-thread access must be established at use.
32///
33/// # Examples
34/// ```
35/// use strided_basic::execution::SendPtr;
36/// let mut value = 1_i32;
37/// let address = &mut value as *mut i32;
38/// assert_eq!(SendPtr(address).as_ptr(), address);
39/// ```
40pub struct SendPtr<T>(pub *mut T);
41
42impl<T> Clone for SendPtr<T> {
43    fn clone(&self) -> Self {
44        *self
45    }
46}
47
48impl<T> Copy for SendPtr<T> {}
49
50unsafe impl<T> Send for SendPtr<T> {}
51unsafe impl<T> Sync for SendPtr<T> {}
52
53impl<T> SendPtr<T> {
54    pub fn as_ptr(self) -> *mut T {
55        self.0
56    }
57
58    pub fn as_const(self) -> *const T {
59        self.0 as *const T
60    }
61}
62
63impl<T> core::fmt::Debug for SendPtr<T> {
64    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
65        f.debug_tuple("SendPtr").field(&self.0).finish()
66    }
67}
68
69/// Block sizes handed to the parallel splitter.
70///
71/// [`split_axis`] declines to split a dimension that fits in one cache block
72/// (`dims[i] <= blocks[i].min(1024)`), so a region whose block covers a small
73/// whole tensor (for example `1024x64` with `block == dims`) never opens even
74/// at four threads. Halving such a block lets the splitter open; the leaf
75/// merely walks two tiles instead of one. Dimensions that already exceed the
76/// guard are left unchanged so other shapes keep their tiles.
77fn scheduler_blocks(dims: &[usize], blocks: &[usize]) -> SVec<usize> {
78    dims.iter()
79        .zip(blocks)
80        .map(|(&dim, &block)| {
81            if dim > 1 && dim <= block.min(1024) {
82                (dim / 2).max(1)
83            } else {
84                block
85            }
86        })
87        .collect()
88}
89
90/// Runs `left` and `right` as the two halves of `exec` and returns both results.
91///
92/// Each half receives a child token carrying its share of the budget.
93fn fork2<R: Send>(
94    exec: Exec<'_>,
95    left: impl Fn(Exec<'_>) -> R + Sync,
96    right: impl Fn(Exec<'_>) -> R + Sync,
97) -> (R, R) {
98    // Mutex-free result slots: a `Mutex` allocates its lock on first use on
99    // some platforms, which made every call allocate.
100    struct Slot<T>(core::cell::UnsafeCell<Option<T>>);
101    // SAFETY: job `i` is the only writer of slot `i`, and the slots are read only
102    // after `fork` has returned, which waits for every job.
103    unsafe impl<T: Send> Sync for Slot<T> {}
104    let slots = [Slot(Default::default()), Slot(Default::default())];
105    exec.fork(2, |i, child| {
106        let value = if i == 0 { left(child) } else { right(child) };
107        // SAFETY: see `Slot`; each index is written by exactly one job.
108        unsafe { *slots[i].0.get() = Some(value) };
109    });
110    let [left, right] = slots.map(|slot| {
111        slot.0
112            .into_inner()
113            .expect("every fork job stores its result")
114    });
115    (left, right)
116}
117
118/// Recursive halving map-reduce over `range` within the budget of `exec`.
119///
120/// `map` runs on the leaf ranges and `reduce` combines two neighbouring
121/// results left to right, so the combination order depends only on the budget.
122/// A serial token, a budget of one or a single-element range runs `map` on the
123/// whole range on the calling thread.
124///
125/// # Examples
126///
127/// ```
128/// use strided_basic::execution::parallel_map_reduce;
129/// use strided_basic::Exec;
130///
131/// let sum = parallel_map_reduce(0..100, Exec::serial(), &|r| r.sum::<usize>(), &|a, b| a + b);
132/// assert_eq!(sum, 4950);
133/// ```
134pub fn parallel_map_reduce<R, Map, Reduce>(
135    range: Range<usize>,
136    exec: Exec<'_>,
137    map: &Map,
138    reduce: &Reduce,
139) -> R
140where
141    R: Send,
142    Map: Fn(Range<usize>) -> R + Sync,
143    Reduce: Fn(R, R) -> R + Sync,
144{
145    let len = range.end - range.start;
146    let budget = exec.budget().get();
147    if budget <= 1 || len <= 1 {
148        return map(range);
149    }
150
151    let left_threads = budget.div_ceil(2);
152    let left_len = len * left_threads / budget;
153    let middle = range.start + left_len.max(1).min(len - 1);
154    let left_range = range.start..middle;
155    let right_range = middle..range.end;
156
157    let (left, right) = fork2(
158        exec,
159        |child| parallel_map_reduce(left_range.clone(), child, map, reduce),
160        |child| parallel_map_reduce(right_range.clone(), child, map, reduce),
161    );
162    reduce(left, right)
163}
164
165/// Runs `operation` over contiguous sub-ranges of `range`, one per slot of
166/// `exec` at most.
167pub(crate) fn parallel_for_each<F>(range: Range<usize>, exec: Exec<'_>, operation: &F)
168where
169    F: Fn(Range<usize>) + Sync,
170{
171    let start = range.start;
172    exec.for_ranges(range.end - range.start, ONE, |chunk| {
173        operation(start + chunk.start..start + chunk.end)
174    });
175}
176
177/// Copy `len` contiguous elements, chunked across the budget of `exec` when
178/// one section pays for a copy of that size (`KernelClass::Copy`) and as one
179/// direct `copy_nonoverlapping` otherwise.
180///
181/// # Safety
182///
183/// `src..src + len` must be readable and `dst..dst + len` writable for the
184/// whole call, the two ranges must not overlap, and no other thread may
185/// access the destination range concurrently.
186pub(crate) unsafe fn copy_contiguous<T: Copy + Send + Sync>(
187    src: *const T,
188    dst: *mut T,
189    len: usize,
190    exec: Exec<'_>,
191) {
192    let exec = for_len(exec, KernelClass::Copy, len);
193    if !exec.is_serial() {
194        let src = SendPtr(src as *mut T);
195        let dst = SendPtr(dst);
196        parallel_for_each(0..len, exec, &|range| {
197            // SAFETY: `range` lies inside `0..len`, and worker ranges are
198            // disjoint, so each worker copies an exclusive destination
199            // chunk from the caller-validated source range.
200            unsafe {
201                core::ptr::copy_nonoverlapping(
202                    src.as_const().add(range.start),
203                    dst.as_ptr().add(range.start),
204                    range.len(),
205                );
206            }
207        });
208        return;
209    }
210    // SAFETY: the caller guarantees both ranges are valid and disjoint.
211    unsafe { core::ptr::copy_nonoverlapping(src, dst, len) };
212}
213
214/// Fill a contiguous slice, chunked across the budget of `exec` when a section
215/// pays for it (`KernelClass::Copy`) and as one serial `fill` otherwise.
216pub(crate) fn fill_contiguous<T: Copy + Send + Sync>(dst: &mut [T], value: T, exec: Exec<'_>) {
217    let len = dst.len();
218    let exec = for_len(exec, KernelClass::Copy, len);
219    if !exec.is_serial() {
220        let dst = SendPtr(dst.as_mut_ptr());
221        parallel_for_each(0..len, exec, &|range| {
222            // SAFETY: `range` lies inside the exclusively borrowed slice
223            // and worker ranges are disjoint.
224            let chunk = unsafe {
225                core::slice::from_raw_parts_mut(dst.as_ptr().add(range.start), range.len())
226            };
227            chunk.fill(value);
228        });
229        return;
230    }
231    dst.fill(value);
232}
233
234/// Fill `len` contiguous destination elements that may be uninitialized.
235///
236/// [`fill_contiguous`] forms a `&mut [T]`, which is undefined behaviour over
237/// uninitialized storage (for example a fresh `bool` buffer). This sibling
238/// views the same span as `[MaybeUninit<T>]`, which has no validity invariant,
239/// and reuses the existing chunking.
240///
241/// # Safety
242///
243/// `dst..dst + len` must be writable for the whole call and no other thread
244/// may access the destination range concurrently.
245pub(crate) unsafe fn fill_contiguous_uninit<T: Copy + Send + Sync>(
246    dst: *mut T,
247    len: usize,
248    value: T,
249    exec: Exec<'_>,
250) {
251    // SAFETY: `MaybeUninit<T>` has no validity invariant, so the span is a
252    // valid `[MaybeUninit<T>]`; `fill_contiguous` writes every element.
253    let span =
254        unsafe { core::slice::from_raw_parts_mut(dst.cast::<core::mem::MaybeUninit<T>>(), len) };
255    fill_contiguous(span, core::mem::MaybeUninit::new(value), exec);
256}
257
258/// Permuting copy into a destination of any layout, split across `exec`.
259///
260/// The permutation engine decides the width itself (copy class, one section),
261/// so this is the only route: a serial token (or a copy too small to repay a
262/// section) takes its calling-thread path.
263pub(crate) fn copy_permuted<T: Copy + Send + Sync + 'static>(
264    dest: &mut crate::StridedViewMut<T>,
265    src: &crate::StridedView<T>,
266    exec: Exec<'_>,
267) -> crate::Result<()> {
268    strided_perm::copy_into(dest, src, exec)
269}
270
271/// [`copy_permuted`] into an uninitialized destination.
272///
273/// The permutation engine is entered on the source element type, so its
274/// native `f32`/`f64` paths apply: a destination reinterpreted as
275/// `MaybeUninit<T>` falls to the generic element path, measured 2.5x slower
276/// on a 4096x4096 `f64` transpose.
277pub(crate) fn copy_permuted_uninit<T: Copy + Send + Sync + 'static>(
278    dest: &mut crate::StridedViewMut<core::mem::MaybeUninit<T>>,
279    src: &crate::StridedView<T>,
280    exec: Exec<'_>,
281) -> crate::Result<()> {
282    strided_perm::copy_into_uninit(dest, src, exec)
283}
284
285/// Dimension to halve: `_lastargmax((dims .- 1) .* costs)`, or `None` when
286/// that dimension is not worth splitting.
287///
288/// Ties break towards the last index, matching Julia's `_lastargmax`. A
289/// dimension is not split when its cost is zero or it fits in one block
290/// (`dims[i] <= min(blocks[i], 1024)`).
291fn split_axis(dims: &[usize], blocks: &[usize], costs: &[isize]) -> Option<usize> {
292    let (i, _) = dims.iter().zip(costs.iter()).enumerate().fold(
293        (0, isize::MIN),
294        |(best_i, best_v), (idx, (&d, &c))| {
295            let score = (d as isize - 1) * c;
296            if score >= best_v {
297                (idx, score)
298            } else {
299                (best_i, best_v)
300            }
301        },
302    );
303    (costs[i] != 0 && dims[i] > blocks[i].min(1024)).then_some(i)
304}
305
306/// The two halves of `dims` split at axis `i`: left dims, right dims and the
307/// right offsets advanced by the left extent.
308fn halves(
309    dims: &[usize],
310    offsets: &[isize],
311    strides_list: &[&[isize]],
312    i: usize,
313) -> (SVec<usize>, SVec<usize>, SVec<isize>) {
314    let di = dims[i];
315    let ndi = di / 2;
316    let mut left_dims: SVec<usize> = dims.iter().copied().collect();
317    left_dims[i] = ndi;
318    let mut right_dims: SVec<usize> = dims.iter().copied().collect();
319    right_dims[i] = di - ndi;
320    let mut right_offsets: SVec<isize> = offsets.iter().copied().collect();
321    for (k, strides) in strides_list.iter().enumerate() {
322        right_offsets[k] += ndi as isize * strides[i];
323    }
324    (left_dims, right_dims, right_offsets)
325}
326
327/// Recursive dimension-splitting parallel execution.
328///
329/// Faithfully ports Julia's `_mapreduce_threaded!` (mapreduce.jl L195-227).
330///
331/// Parameters:
332/// - `dims`: Ordered dimensions (after fuse/order/block)
333/// - `blocks`: Block sizes per dimension
334/// - `strides_list`: Per-array strides, ordered by plan
335/// - `offsets`: Per-array byte offsets into the data
336/// - `costs`: Per-dimension splitting costs
337/// - `exec`: Placement token; its budget is the number of slots the whole
338///   region may occupy, divided between the halves at every split
339/// - `f`: Leaf function — called when we've reached a single-slot region
340///
341/// The leaf function `f` receives `(dims, blocks, strides_list, offsets)` describing
342/// the sub-region to process.
343pub(crate) fn mapreduce_threaded<S: AsRef<[isize]>, F>(
344    dims: &[usize],
345    blocks: &[usize],
346    strides_list: &[S],
347    offsets: &[isize],
348    costs: &[isize],
349    class: KernelClass,
350    exec: Exec<'_>,
351    f: &F,
352) -> Result<()>
353where
354    F: Fn(&[usize], &[usize], &[&[isize]], &[isize]) -> Result<()> + Sync,
355{
356    // The recursive splitter is compiled once; the leaf is `dyn`, called once
357    // per leaf region. Borrowed per-operand stride slices are built once here
358    // so the splitter and its leaves share them without owning a
359    // `Vec<Vec<isize>>` (#306 P1).
360    let refs: SVec<&[isize]> = strided_slices(strides_list);
361    mapreduce_threaded_dyn(dims, blocks, &refs, offsets, costs, class, exec, f)
362}
363
364fn strided_slices<S: AsRef<[isize]>>(strides_list: &[S]) -> SVec<&[isize]> {
365    strides_list.iter().map(|s| s.as_ref()).collect()
366}
367
368/// The leaf callback of a threaded region.
369type LeafFn<'a> = &'a (dyn Fn(&[usize], &[usize], &[&[isize]], &[isize]) -> Result<()> + Sync);
370
371#[inline(never)]
372fn mapreduce_threaded_dyn(
373    dims: &[usize],
374    blocks: &[usize],
375    strides_list: &[&[isize]],
376    offsets: &[isize],
377    costs: &[isize],
378    class: KernelClass,
379    exec: Exec<'_>,
380    f: LeafFn<'_>,
381) -> Result<()> {
382    // Normalize before the splitter: the guard in `split_axis` uses these tile
383    // sizes, so a block that covers its whole dimension would otherwise
384    // suppress the split. The shared layer applies this for every map, zip and
385    // fused caller.
386    let blocks = scheduler_blocks(dims, blocks);
387    ThreadedMapReduce {
388        blocks: &blocks,
389        strides_list,
390        costs,
391        class,
392        operation: f,
393    }
394    .run(dims, offsets, exec)
395}
396
397struct ThreadedMapReduce<'a> {
398    blocks: &'a [usize],
399    strides_list: &'a [&'a [isize]],
400    costs: &'a [isize],
401    class: KernelClass,
402    operation: LeafFn<'a>,
403}
404
405impl ThreadedMapReduce<'_> {
406    fn run(&self, dims: &[usize], offsets: &[isize], exec: Exec<'_>) -> Result<()> {
407        let total = crate::kernel::total_len(dims)?;
408
409        // Base case: a single slot, or a fork of this region does not pay.
410        if !is_parallel(self.class, total, &exec) {
411            return (self.operation)(dims, self.blocks, self.strides_list, offsets);
412        }
413        let Some(i) = split_axis(dims, self.blocks, self.costs) else {
414            return (self.operation)(dims, self.blocks, self.strides_list, offsets);
415        };
416
417        let (left_dims, right_dims, right_offsets) = halves(dims, offsets, self.strides_list, i);
418        let (r1, r2) = fork2(
419            exec,
420            |child| self.run(&left_dims, offsets, child),
421            |child| self.run(&right_dims, &right_offsets, child),
422        );
423        r1?;
424        r2?;
425        Ok(())
426    }
427}
428
429/// Recursive dimension-splitting parallel fold.
430///
431/// Like [`mapreduce_threaded`], but the leaf returns a partial result and each
432/// split combines the two children with `combine`. A full reduction therefore
433/// never needs a worker-count-sized output-slot vector (#306 scope addition).
434/// `f` must reduce the whole sub-region it is handed, using the supplied
435/// `for_each_inner_block_with_offsets` traversal.
436#[allow(clippy::too_many_arguments)]
437pub(crate) fn mapreduce_threaded_fold<S, F, C, R>(
438    dims: &[usize],
439    blocks: &[usize],
440    strides_list: &[S],
441    offsets: &[isize],
442    costs: &[isize],
443    class: KernelClass,
444    exec: Exec<'_>,
445    f: &F,
446    combine: &C,
447) -> Result<R>
448where
449    S: AsRef<[isize]>,
450    F: Fn(&[usize], &[usize], &[&[isize]], &[isize]) -> Result<R> + Sync,
451    C: Fn(R, R) -> R + Sync,
452    R: Send,
453{
454    let refs: SVec<&[isize]> = strided_slices(strides_list);
455    // Normalize whole-dimension blocks before splitting, matching the
456    // `mapreduce_threaded` path (#313): a block that covers its dimension would
457    // otherwise suppress the split for a small whole tensor.
458    let blocks = scheduler_blocks(dims, blocks);
459    FoldMapReduce {
460        blocks: &blocks,
461        strides_list: &refs,
462        costs,
463        class,
464        f,
465        combine,
466    }
467    .run(dims, offsets, exec)
468}
469
470struct FoldMapReduce<'a, F, C> {
471    blocks: &'a [usize],
472    strides_list: &'a [&'a [isize]],
473    costs: &'a [isize],
474    class: KernelClass,
475    f: &'a F,
476    combine: &'a C,
477}
478
479impl<F, C> FoldMapReduce<'_, F, C> {
480    fn run<R: Send>(&self, dims: &[usize], offsets: &[isize], exec: Exec<'_>) -> Result<R>
481    where
482        F: Fn(&[usize], &[usize], &[&[isize]], &[isize]) -> Result<R> + Sync,
483        C: Fn(R, R) -> R + Sync,
484    {
485        let total = crate::kernel::total_len(dims)?;
486
487        // Base case: a single slot, or a fork of this region does not pay.
488        if !is_parallel(self.class, total, &exec) {
489            return (self.f)(dims, self.blocks, self.strides_list, offsets);
490        }
491        let Some(i) = split_axis(dims, self.blocks, self.costs) else {
492            return (self.f)(dims, self.blocks, self.strides_list, offsets);
493        };
494
495        // Split dimension i in half and combine the two partial results.
496        let (left_dims, right_dims, right_offsets) = halves(dims, offsets, self.strides_list, i);
497        let (r1, r2) = fork2(
498            exec,
499            |child| self.run(&left_dims, offsets, child),
500            |child| self.run(&right_dims, &right_offsets, child),
501        );
502        Ok((self.combine)(r1?, r2?))
503    }
504}
505
506/// Execute the kernel on a sub-region defined by initial offsets.
507///
508/// Delegates to `for_each_inner_block_preordered` which directly calls
509/// kernel functions with the initial offsets, avoiding redundant re-ordering
510/// and per-callback `Vec` allocation.
511pub(crate) fn for_each_inner_block_with_offsets<S: AsRef<[isize]>, F>(
512    dims: &[usize],
513    blocks: &[usize],
514    strides_list: &[S],
515    initial_offsets: &[isize],
516    f: F,
517) -> Result<()>
518where
519    F: FnMut(&[isize], usize, &[isize]) -> Result<()>,
520{
521    for_each_inner_block_preordered(dims, blocks, strides_list, initial_offsets, f)
522}
523
524/// A pool of `threads` workers for unit tests.
525#[cfg(test)]
526pub(crate) fn test_pool(threads: usize) -> threadteam::Pool {
527    threadteam::Pool::builder()
528        .threads(threads)
529        .dispatch_costs(test_dispatch_costs())
530        .build()
531        .unwrap()
532}
533
534/// Dispatch costs of the unit-test pools: 100 nanoseconds per section whatever the
535/// token, so that regions of a few thousand elements open the parallel branches
536/// the tests compare against the serial ones.
537#[cfg(test)]
538pub(crate) fn test_dispatch_costs() -> threadteam::DispatchCosts {
539    use threadteam::{DispatchKind, WidthCosts};
540    let cheap = WidthCosts::flat(core::time::Duration::from_nanos(100));
541    threadteam::DispatchCosts::for_class(threadteam::HostClass::Generic)
542        .with(DispatchKind::Team, cheap)
543        .with(DispatchKind::InPool, cheap)
544        .with(DispatchKind::Outside, cheap)
545}
546
547/// Run `operation` once with a serial token and once with a four-slot token of
548/// a four-worker pool, and return both results.
549///
550/// The parallel run asserts that the gate actually opens for a region of a
551/// million elements, so equivalence tests with such sizes exercise the parallel
552/// branches. Without the `parallel` feature the pool has
553/// width one and both runs are serial.
554#[cfg(test)]
555pub(crate) fn run_serial_and_parallel<R: Send>(operation: impl Fn(Exec<'_>) -> R + Sync) -> (R, R) {
556    let serial = operation(Exec::serial());
557    let pool = test_pool(4);
558    let exec = Exec::new(&pool, 4).unwrap();
559    assert_eq!(
560        crate::cost::parallel_width(&exec, KernelClass::Map, 1 << 20).get(),
561        exec.budget().get()
562    );
563    let parallel = operation(exec);
564    (serial, parallel)
565}
566
567#[cfg(test)]
568#[path = "threading/tests/tests.rs"]
569mod tests;