use crate::inline::InlineVec;
use crate::kernel::for_each_inner_block_preordered;
use crate::Result;
use core::num::NonZeroUsize;
use std::ops::Range;
use strided_exec::Exec;
type SVec<T> = InlineVec<T>;
const ONE: NonZeroUsize = NonZeroUsize::MIN;
pub struct SendPtr<T>(pub *mut T);
impl<T> Clone for SendPtr<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for SendPtr<T> {}
unsafe impl<T> Send for SendPtr<T> {}
unsafe impl<T> Sync for SendPtr<T> {}
impl<T> SendPtr<T> {
pub fn as_ptr(self) -> *mut T {
self.0
}
pub fn as_const(self) -> *const T {
self.0 as *const T
}
}
impl<T> core::fmt::Debug for SendPtr<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("SendPtr").field(&self.0).finish()
}
}
pub use strided_perm::MINTHREADLENGTH;
fn scheduler_blocks(dims: &[usize], blocks: &[usize]) -> SVec<usize> {
dims.iter()
.zip(blocks)
.map(|(&dim, &block)| {
if dim > 1 && dim <= block.min(1024) {
(dim / 2).max(1)
} else {
block
}
})
.collect()
}
pub fn parallel_threads_for_len(len: usize, budget: usize) -> usize {
if len <= MINTHREADLENGTH {
1
} else {
budget.max(1)
}
}
pub(crate) fn for_len(exec: Exec<'_>, len: usize) -> Exec<'_> {
let width = parallel_threads_for_len(len, exec.budget().get());
exec.limit(NonZeroUsize::new(width).unwrap_or(ONE))
}
pub(crate) fn is_parallel(len: usize, exec: &Exec<'_>) -> bool {
len > MINTHREADLENGTH && !exec.is_serial()
}
fn fork2<R: Send>(
exec: Exec<'_>,
left: impl Fn(Exec<'_>) -> R + Sync,
right: impl Fn(Exec<'_>) -> R + Sync,
) -> (R, R) {
struct Slot<T>(core::cell::UnsafeCell<Option<T>>);
unsafe impl<T: Send> Sync for Slot<T> {}
let slots = [Slot(Default::default()), Slot(Default::default())];
exec.fork(2, |i, child| {
let value = if i == 0 { left(child) } else { right(child) };
unsafe { *slots[i].0.get() = Some(value) };
});
let [left, right] = slots.map(|slot| {
slot.0
.into_inner()
.expect("every fork job stores its result")
});
(left, right)
}
pub fn parallel_map_reduce<R, Map, Reduce>(
range: Range<usize>,
exec: Exec<'_>,
map: &Map,
reduce: &Reduce,
) -> R
where
R: Send,
Map: Fn(Range<usize>) -> R + Sync,
Reduce: Fn(R, R) -> R + Sync,
{
let len = range.end - range.start;
let budget = exec.budget().get();
if budget <= 1 || len <= 1 {
return map(range);
}
let left_threads = budget.div_ceil(2);
let left_len = len * left_threads / budget;
let middle = range.start + left_len.max(1).min(len - 1);
let left_range = range.start..middle;
let right_range = middle..range.end;
let (left, right) = fork2(
exec,
|child| parallel_map_reduce(left_range.clone(), child, map, reduce),
|child| parallel_map_reduce(right_range.clone(), child, map, reduce),
);
reduce(left, right)
}
pub(crate) fn parallel_for_each<F>(range: Range<usize>, exec: Exec<'_>, operation: &F)
where
F: Fn(Range<usize>) + Sync,
{
let start = range.start;
exec.for_ranges(range.end - range.start, ONE, |chunk| {
operation(start + chunk.start..start + chunk.end)
});
}
pub(crate) unsafe fn copy_contiguous<T: Copy + Send + Sync>(
src: *const T,
dst: *mut T,
len: usize,
exec: Exec<'_>,
) {
if is_parallel(len, &exec) {
let src = SendPtr(src as *mut T);
let dst = SendPtr(dst);
parallel_for_each(0..len, exec, &|range| {
unsafe {
core::ptr::copy_nonoverlapping(
src.as_const().add(range.start),
dst.as_ptr().add(range.start),
range.len(),
);
}
});
return;
}
unsafe { core::ptr::copy_nonoverlapping(src, dst, len) };
}
pub(crate) fn fill_contiguous<T: Copy + Send + Sync>(dst: &mut [T], value: T, exec: Exec<'_>) {
let len = dst.len();
if is_parallel(len, &exec) {
let dst = SendPtr(dst.as_mut_ptr());
parallel_for_each(0..len, exec, &|range| {
let chunk = unsafe {
core::slice::from_raw_parts_mut(dst.as_ptr().add(range.start), range.len())
};
chunk.fill(value);
});
return;
}
dst.fill(value);
}
pub(crate) unsafe fn fill_contiguous_uninit<T: Copy + Send + Sync>(
dst: *mut T,
len: usize,
value: T,
exec: Exec<'_>,
) {
let span =
unsafe { core::slice::from_raw_parts_mut(dst.cast::<core::mem::MaybeUninit<T>>(), len) };
fill_contiguous(span, core::mem::MaybeUninit::new(value), exec);
}
pub(crate) fn copy_permuted<T: Copy + Send + Sync + 'static>(
dest: &mut crate::StridedViewMut<T>,
src: &crate::StridedView<T>,
exec: Exec<'_>,
) -> crate::Result<()> {
let total = crate::kernel::total_len(dest.dims())?;
strided_perm::copy_into(dest, src, for_len(exec, total))
}
pub(crate) fn copy_permuted_uninit<T: Copy + Send + Sync + 'static>(
dest: &mut crate::StridedViewMut<core::mem::MaybeUninit<T>>,
src: &crate::StridedView<T>,
exec: Exec<'_>,
) -> crate::Result<()> {
let total = crate::kernel::total_len(dest.dims())?;
strided_perm::copy_into_uninit(dest, src, for_len(exec, total))
}
fn split_axis(dims: &[usize], blocks: &[usize], costs: &[isize]) -> Option<usize> {
let (i, _) = dims.iter().zip(costs.iter()).enumerate().fold(
(0, isize::MIN),
|(best_i, best_v), (idx, (&d, &c))| {
let score = (d as isize - 1) * c;
if score >= best_v {
(idx, score)
} else {
(best_i, best_v)
}
},
);
(costs[i] != 0 && dims[i] > blocks[i].min(1024)).then_some(i)
}
fn halves(
dims: &[usize],
offsets: &[isize],
strides_list: &[&[isize]],
i: usize,
) -> (SVec<usize>, SVec<usize>, SVec<isize>) {
let di = dims[i];
let ndi = di / 2;
let mut left_dims: SVec<usize> = dims.iter().copied().collect();
left_dims[i] = ndi;
let mut right_dims: SVec<usize> = dims.iter().copied().collect();
right_dims[i] = di - ndi;
let mut right_offsets: SVec<isize> = offsets.iter().copied().collect();
for (k, strides) in strides_list.iter().enumerate() {
right_offsets[k] += ndi as isize * strides[i];
}
(left_dims, right_dims, right_offsets)
}
pub(crate) fn mapreduce_threaded<S: AsRef<[isize]>, F>(
dims: &[usize],
blocks: &[usize],
strides_list: &[S],
offsets: &[isize],
costs: &[isize],
exec: Exec<'_>,
f: &F,
) -> Result<()>
where
F: Fn(&[usize], &[usize], &[&[isize]], &[isize]) -> Result<()> + Sync,
{
let refs: SVec<&[isize]> = strided_slices(strides_list);
mapreduce_threaded_dyn(dims, blocks, &refs, offsets, costs, exec, f)
}
fn strided_slices<S: AsRef<[isize]>>(strides_list: &[S]) -> SVec<&[isize]> {
strides_list.iter().map(|s| s.as_ref()).collect()
}
type LeafFn<'a> = &'a (dyn Fn(&[usize], &[usize], &[&[isize]], &[isize]) -> Result<()> + Sync);
#[inline(never)]
fn mapreduce_threaded_dyn(
dims: &[usize],
blocks: &[usize],
strides_list: &[&[isize]],
offsets: &[isize],
costs: &[isize],
exec: Exec<'_>,
f: LeafFn<'_>,
) -> Result<()> {
let blocks = scheduler_blocks(dims, blocks);
ThreadedMapReduce {
blocks: &blocks,
strides_list,
costs,
operation: f,
}
.run(dims, offsets, exec)
}
struct ThreadedMapReduce<'a> {
blocks: &'a [usize],
strides_list: &'a [&'a [isize]],
costs: &'a [isize],
operation: LeafFn<'a>,
}
impl ThreadedMapReduce<'_> {
fn run(&self, dims: &[usize], offsets: &[isize], exec: Exec<'_>) -> Result<()> {
let total = crate::kernel::total_len(dims)?;
if exec.is_serial() || total <= MINTHREADLENGTH {
return (self.operation)(dims, self.blocks, self.strides_list, offsets);
}
let Some(i) = split_axis(dims, self.blocks, self.costs) else {
return (self.operation)(dims, self.blocks, self.strides_list, offsets);
};
let (left_dims, right_dims, right_offsets) = halves(dims, offsets, self.strides_list, i);
let (r1, r2) = fork2(
exec,
|child| self.run(&left_dims, offsets, child),
|child| self.run(&right_dims, &right_offsets, child),
);
r1?;
r2?;
Ok(())
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn mapreduce_threaded_fold<S, F, C, R>(
dims: &[usize],
blocks: &[usize],
strides_list: &[S],
offsets: &[isize],
costs: &[isize],
exec: Exec<'_>,
f: &F,
combine: &C,
) -> Result<R>
where
S: AsRef<[isize]>,
F: Fn(&[usize], &[usize], &[&[isize]], &[isize]) -> Result<R> + Sync,
C: Fn(R, R) -> R + Sync,
R: Send,
{
let refs: SVec<&[isize]> = strided_slices(strides_list);
let blocks = scheduler_blocks(dims, blocks);
FoldMapReduce {
blocks: &blocks,
strides_list: &refs,
costs,
f,
combine,
}
.run(dims, offsets, exec)
}
struct FoldMapReduce<'a, F, C> {
blocks: &'a [usize],
strides_list: &'a [&'a [isize]],
costs: &'a [isize],
f: &'a F,
combine: &'a C,
}
impl<F, C> FoldMapReduce<'_, F, C> {
fn run<R: Send>(&self, dims: &[usize], offsets: &[isize], exec: Exec<'_>) -> Result<R>
where
F: Fn(&[usize], &[usize], &[&[isize]], &[isize]) -> Result<R> + Sync,
C: Fn(R, R) -> R + Sync,
{
let total = crate::kernel::total_len(dims)?;
if exec.is_serial() || total <= MINTHREADLENGTH {
return (self.f)(dims, self.blocks, self.strides_list, offsets);
}
let Some(i) = split_axis(dims, self.blocks, self.costs) else {
return (self.f)(dims, self.blocks, self.strides_list, offsets);
};
let (left_dims, right_dims, right_offsets) = halves(dims, offsets, self.strides_list, i);
let (r1, r2) = fork2(
exec,
|child| self.run(&left_dims, offsets, child),
|child| self.run(&right_dims, &right_offsets, child),
);
Ok((self.combine)(r1?, r2?))
}
}
pub(crate) fn for_each_inner_block_with_offsets<S: AsRef<[isize]>, F>(
dims: &[usize],
blocks: &[usize],
strides_list: &[S],
initial_offsets: &[isize],
f: F,
) -> Result<()>
where
F: FnMut(&[isize], usize, &[isize]) -> Result<()>,
{
for_each_inner_block_preordered(dims, blocks, strides_list, initial_offsets, f)
}
#[cfg(test)]
pub(crate) fn test_pool(threads: usize) -> strided_exec::Pool {
strided_exec::Pool::builder()
.threads(threads)
.build()
.unwrap()
}
#[cfg(test)]
pub(crate) fn run_serial_and_parallel<R: Send>(operation: impl Fn(Exec<'_>) -> R + Sync) -> (R, R) {
let serial = operation(Exec::serial());
let pool = test_pool(4);
let exec = Exec::new(&pool, 4).unwrap();
assert_eq!(
parallel_threads_for_len(MINTHREADLENGTH + 1, exec.budget().get()),
exec.budget().get()
);
assert_eq!(parallel_threads_for_len(MINTHREADLENGTH, 4), 1);
let parallel = operation(exec);
(serial, parallel)
}
#[cfg(test)]
#[path = "threading/tests/tests.rs"]
mod tests;