use crate::layouts::{Backend, ScratchArena};
pub trait TaskExecutor: Send + Sync + 'static {
const IS_PARALLEL: bool;
fn is_parallel() -> bool {
Self::IS_PARALLEL
}
fn max_parallelism() -> usize {
1
}
fn join<A, B, RA, RB>(left: A, right: B) -> (RA, RB)
where
A: FnOnce() -> RA + Send,
B: FnOnce() -> RB + Send,
RA: Send,
RB: Send;
fn for_each<F>(count: usize, task: F)
where
F: Fn(usize) + Send + Sync,
{
for index in 0..count {
task(index);
}
}
fn for_each_chunked<T, F>(count: usize, scratch: &mut [T], per_worker: usize, task: F)
where
T: Send,
F: Fn(&mut [T], usize) + Send + Sync,
{
assert!(
scratch.len() >= per_worker,
"worker scratch: {} < per_worker {per_worker}",
scratch.len()
);
let worker = &mut scratch[..per_worker];
for index in 0..count {
task(worker, index);
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct SerialTaskExecutor;
impl TaskExecutor for SerialTaskExecutor {
const IS_PARALLEL: bool = false;
fn join<A, B, RA, RB>(left: A, right: B) -> (RA, RB)
where
A: FnOnce() -> RA + Send,
B: FnOnce() -> RB + Send,
RA: Send,
RB: Send,
{
(left(), right())
}
}
pub fn worker_count<E: TaskExecutor>(requested: usize, tasks: usize) -> usize {
assert!(requested > 0, "parallel worker count must be non-zero");
if tasks == 0 {
0
} else if E::is_parallel() {
requested.min(E::max_parallelism()).min(tasks).max(1)
} else {
1
}
}
pub trait ScratchWorkers {
const PREPARE: usize = 1;
const APPLY: usize = 1;
const VMP: usize = 1;
const IDFT: usize = 1;
}
pub fn scratch_workers<E: TaskExecutor>(cap: usize) -> usize {
if E::IS_PARALLEL { cap.max(1) } else { 1 }
}
pub fn scratch_workers_within<E: TaskExecutor>(cap: usize, per_worker: usize, available: usize) -> usize {
if per_worker == 0 {
return 1;
}
scratch_workers::<E>(cap).min(available / per_worker).max(1)
}
pub fn worker_scratch_bytes<B: Backend>(bytes: usize) -> usize {
assert!(B::SCRATCH_ALIGN > 0, "backend scratch alignment must be non-zero");
bytes.next_multiple_of(B::SCRATCH_ALIGN)
}
pub fn for_each_with_scratch<E, B, T, F>(items: &mut [T], base_index: usize, scratch: Vec<ScratchArena<'_, B>>, task: &F)
where
E: TaskExecutor,
B: Backend,
T: Send,
F: Fn(usize, &mut T, &mut ScratchArena<'_, B>) + Send + Sync,
{
assert!(!items.is_empty());
assert!(!scratch.is_empty());
assert!(scratch.len() <= items.len());
if scratch.len() == 1 {
let mut scratch = scratch.into_iter().next().unwrap();
for (offset, item) in items.iter_mut().enumerate() {
task(base_index + offset, item, &mut scratch);
}
return;
}
let left_workers = scratch.len() / 2;
let split = items.len() * left_workers / scratch.len();
let (left_items, right_items) = items.split_at_mut(split);
let mut left_scratch = scratch;
let right_scratch = left_scratch.split_off(left_workers);
E::join(
|| for_each_with_scratch::<E, B, T, F>(left_items, base_index, left_scratch, task),
|| for_each_with_scratch::<E, B, T, F>(right_items, base_index + split, right_scratch, task),
);
}
#[cfg(test)]
mod tests {
use super::{SerialTaskExecutor, TaskExecutor, scratch_workers, scratch_workers_within, worker_count};
#[test]
fn serial_executor_always_uses_one_worker() {
assert!(!SerialTaskExecutor::is_parallel());
assert_eq!(worker_count::<SerialTaskExecutor>(8, 32), 1);
assert_eq!(worker_count::<SerialTaskExecutor>(8, 0), 0);
assert_eq!(scratch_workers::<SerialTaskExecutor>(32), 1);
assert_eq!(scratch_workers_within::<SerialTaskExecutor>(32, 64, 4096), 1);
}
#[test]
fn scratch_workers_are_bounded_by_the_arena() {
assert_eq!(scratch_workers_within::<SerialTaskExecutor>(32, 64, 4096), 1);
assert_eq!(scratch_workers_within::<SerialTaskExecutor>(32, 64, 0), 1);
assert_eq!(scratch_workers_within::<SerialTaskExecutor>(32, 0, 4096), 1);
}
#[test]
fn serial_for_each_chunked_visits_every_index_once() {
use std::sync::atomic::{AtomicUsize, Ordering};
let mut scratch = [0usize; 4];
let visits: Vec<AtomicUsize> = (0..7).map(|_| AtomicUsize::new(0)).collect();
SerialTaskExecutor::for_each_chunked(7, &mut scratch, 4, |worker, i| {
assert_eq!(worker.len(), 4);
visits[i].fetch_add(1, Ordering::Relaxed);
});
assert!(visits.iter().all(|v| v.load(Ordering::Relaxed) == 1));
}
#[test]
fn for_each_chunked_accepts_zero_tasks() {
let mut scratch = [0usize; 4];
SerialTaskExecutor::for_each_chunked(0, &mut scratch, 4, |_, _| panic!("no task expected"));
}
#[test]
#[should_panic(expected = "worker scratch")]
fn for_each_chunked_rejects_undersized_scratch() {
let mut scratch = [0usize; 2];
SerialTaskExecutor::for_each_chunked(1, &mut scratch, 4, |_, _| {});
}
}