use alloc::vec::Vec;
#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
use core::arch::x86_64::{__m512i, _mm_sfence, _mm512_loadu_si512, _mm512_stream_si512};
use core::ptr;
use p3_maybe_rayon::prelude::*;
use p3_util::DisjointMutPtr;
const PAGE_BYTES: usize = 1 << 12;
const PREFAULT_BYTES: usize = 1 << 21;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum Store {
Cached,
Streamed,
}
pub(crate) trait Staged: Copy + Send + Sync {
unsafe fn stream(target: &mut [Self], source: &[Self]) {
target.copy_from_slice(source);
}
fn fence() {}
}
impl Staged for u128 {
#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
unsafe fn stream(target: &mut [Self], source: &[Self]) {
const WORDS_PER_LINE: usize = 64 / size_of::<u128>();
assert_eq!(target.len(), source.len(), "a run is copied whole");
if target.is_empty() {
return;
}
let prefix = target.as_ptr().align_offset(64).min(target.len());
let (target_prefix, target) = target.split_at_mut(prefix);
let (source_prefix, source) = source.split_at(prefix);
target_prefix.copy_from_slice(source_prefix);
let body = source.len() / WORDS_PER_LINE * WORDS_PER_LINE;
let (source_body, source_tail) = source.split_at(body);
let (target_body, target_tail) = target.split_at_mut(body);
for offset in (0..body).step_by(WORDS_PER_LINE) {
unsafe {
let source_line = source_body.as_ptr().add(offset).cast::<__m512i>();
let target_line = target_body.as_mut_ptr().add(offset).cast::<__m512i>();
let value = _mm512_loadu_si512(source_line);
_mm512_stream_si512(target_line, value);
}
}
target_tail.copy_from_slice(source_tail);
}
#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
fn fence() {
unsafe { _mm_sfence() };
}
}
struct FenceOnDrop<'a, F: Fn()> {
fence: &'a F,
}
impl<F: Fn()> Drop for FenceOnDrop<'_, F> {
fn drop(&mut self) {
(self.fence)();
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) enum Dispatch {
Serial,
Parallel {
min_len: usize,
},
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct StagedRuns {
run: usize,
log_stride: usize,
depth: usize,
}
impl StagedRuns {
pub(crate) const fn new(run: usize, log_stride: usize, depth: usize) -> Self {
assert!(
depth < usize::BITS as usize && log_stride < usize::BITS as usize - depth,
"staging shifts exceed the word size"
);
Self {
run,
log_stride,
depth,
}
}
#[inline]
pub(crate) const fn block(&self, index: usize) -> usize {
index >> self.log_stride
}
#[inline]
pub(crate) const fn run_index(&self, index: usize, k: usize) -> usize {
let offset = index & ((1 << self.log_stride) - 1);
(self.block(index) << (self.log_stride + self.depth)) + offset + (k << self.log_stride)
}
}
pub(crate) fn for_each_staged_tile<T, P>(
values: &mut [T],
runs: StagedRuns,
dispatch: Dispatch,
process: P,
) where
T: Copy + Send + Sync,
P: Fn(&mut [T], usize) + Send + Sync,
{
let StagedRuns { run, depth, .. } = runs;
let len = values.len();
let rows = 1 << depth;
let tile_len = run << depth;
let tiles = len / tile_len;
let count = len / run;
assert_eq!(tiles * tile_len, len, "tiles do not partition the matrix");
let base = DisjointMutPtr::new(values);
let task = move |tile: &mut Vec<T>, index: usize| {
assert!(
runs.run_index(index, rows - 1) < count,
"staged row walk leaves the matrix"
);
tile.clear();
for k in 0..rows {
let source = unsafe { base.slice(runs.run_index(index, k) * run, run) };
tile.extend_from_slice(source);
}
debug_assert_eq!(tile.len(), tile_len, "the gather left the tile short");
process(tile.as_mut_slice(), runs.block(index));
for (k, source) in tile.chunks_exact(run).enumerate() {
let target = unsafe { base.slice_mut(runs.run_index(index, k) * run, run) };
target.copy_from_slice(source);
}
};
let new_tile = || Vec::with_capacity(tile_len);
match dispatch {
Dispatch::Parallel { min_len } => (0..tiles)
.into_par_iter()
.with_min_len(min_len)
.for_each_init(new_tile, task),
Dispatch::Serial => {
let mut tile = new_tile();
for index in 0..tiles {
task(&mut tile, index);
}
}
}
}
pub(crate) fn prefault<T: Copy + Send + Sync>(values: &mut [T], zero: T) {
let size = size_of::<T>().max(1);
let page = (PAGE_BYTES / size).max(1);
let touch = |chunk: &mut [T]| {
for element in chunk.iter_mut().step_by(page) {
unsafe { ptr::write_volatile(element, zero) };
}
};
let head = values
.as_ptr()
.align_offset(PREFAULT_BYTES)
.min(values.len());
let (head, body) = values.split_at_mut(head);
touch(head);
body.par_chunks_mut((PREFAULT_BYTES / size).max(1))
.for_each(touch);
}
pub(crate) fn for_each_staged_tile_into_cosets<T, Q, P>(
values: &mut [T],
source: Option<&[T]>,
coset_len: usize,
runs: StagedRuns,
dispatch: Dispatch,
prepare: Q,
process: P,
) where
T: Copy + Send + Sync,
Q: Fn(&mut [T]) + Send + Sync,
P: Fn(&mut [T], usize, usize) + Send + Sync,
{
for_each_staged_tile_into_cosets_with(
values,
source,
coset_len,
runs,
dispatch,
prepare,
process,
|target, source| target.copy_from_slice(source),
|| {},
);
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn for_each_staged_tile_into_cosets_stored<T, Q, P>(
values: &mut [T],
source: Option<&[T]>,
coset_len: usize,
runs: StagedRuns,
dispatch: Dispatch,
store: Store,
prepare: Q,
process: P,
) where
T: Staged,
Q: Fn(&mut [T]) + Send + Sync,
P: Fn(&mut [T], usize, usize) + Send + Sync,
{
for_each_staged_tile_into_cosets_with(
values,
source,
coset_len,
runs,
dispatch,
prepare,
process,
move |target, source| match store {
Store::Cached => target.copy_from_slice(source),
Store::Streamed => {
unsafe { T::stream(target, source) };
}
},
move || {
if store == Store::Streamed {
T::fence();
}
},
);
}
#[allow(clippy::too_many_arguments)]
fn for_each_staged_tile_into_cosets_with<T, Q, P, W, F>(
values: &mut [T],
source: Option<&[T]>,
coset_len: usize,
runs: StagedRuns,
dispatch: Dispatch,
prepare: Q,
process: P,
write: W,
fence: F,
) where
T: Copy + Send + Sync,
Q: Fn(&mut [T]) + Send + Sync,
P: Fn(&mut [T], usize, usize) + Send + Sync,
W: Fn(&mut [T], &[T]) + Send + Sync,
F: Fn() + Send + Sync,
{
let StagedRuns { run, depth, .. } = runs;
let cosets = values.len() / coset_len;
let rows = 1 << depth;
let tile_len = run << depth;
let tiles = coset_len / tile_len;
let count = coset_len / run;
assert_eq!(
cosets * coset_len,
values.len(),
"cosets do not partition the matrix"
);
assert!(
source.is_none_or(|source| source.len() == coset_len),
"the source is not one coset long"
);
assert_eq!(
tiles * tile_len,
coset_len,
"tiles do not partition the coset"
);
let base = DisjointMutPtr::new(values);
let slice_of = move |index: usize, k: usize, coset: usize| {
coset * coset_len + runs.run_index(index, k) * run
};
let task = move |(tile, copy): &mut (Vec<T>, Vec<T>), index: usize| {
let _fence = FenceOnDrop { fence: &fence };
assert!(
runs.run_index(index, rows - 1) < count,
"staged row walk leaves the coset"
);
tile.clear();
for k in 0..rows {
let start = slice_of(index, k, 0);
let gathered = source.map_or_else(
|| unsafe { base.slice(start, run) },
|source| &source[start..start + run],
);
tile.extend_from_slice(gathered);
}
debug_assert_eq!(tile.len(), tile_len, "the gather left the tile short");
prepare(tile.as_mut_slice());
let block = runs.block(index);
for coset in (0..cosets).rev() {
let staged = if coset == 0 {
&mut *tile
} else {
copy.clear();
copy.extend_from_slice(&tile[..]);
&mut *copy
};
process(staged.as_mut_slice(), block, coset);
for (k, source) in staged.chunks_exact(run).enumerate() {
let target = unsafe { base.slice_mut(slice_of(index, k, coset), run) };
write(target, source);
}
}
};
let new_tiles = || (Vec::with_capacity(tile_len), Vec::with_capacity(tile_len));
match dispatch {
Dispatch::Parallel { min_len } => (0..tiles)
.into_par_iter()
.with_min_len(min_len)
.for_each_init(new_tiles, task),
Dispatch::Serial => {
let mut buffers = new_tiles();
for index in 0..tiles {
task(&mut buffers, index);
}
}
}
}
#[cfg(test)]
mod tests {
extern crate std;
use alloc::vec::Vec;
use alloc::{format, vec};
use core::sync::atomic::{AtomicUsize, Ordering};
use std::panic::{AssertUnwindSafe, catch_unwind};
use super::{
Dispatch, FenceOnDrop, Staged, StagedRuns, Store, for_each_staged_tile,
for_each_staged_tile_into_cosets, for_each_staged_tile_into_cosets_stored,
for_each_staged_tile_into_cosets_with, prefault,
};
#[test]
fn streamed_u128_runs_match_cached_copies_at_every_alignment_and_length() {
for target_offset in 0..4 {
for source_offset in 0..4 {
for len in [0usize, 1, 3, 4, 5, 7, 8, 9, 13] {
let source: Vec<u128> = (0..len + 4)
.map(|i| (i as u128).wrapping_mul(0x101_0000_0001))
.collect();
let mut actual = vec![u128::MAX; len + 8];
let mut expected = actual.clone();
let source = &source[source_offset..source_offset + len];
expected[target_offset..target_offset + len].copy_from_slice(source);
{
let fence = <u128 as Staged>::fence;
let _fence = FenceOnDrop { fence: &fence };
unsafe {
<u128 as Staged>::stream(
&mut actual[target_offset..target_offset + len],
source,
);
}
}
assert_eq!(
actual, expected,
"target={target_offset} source={source_offset} len={len}"
);
}
}
}
}
#[test]
fn a_task_fences_prior_writes_while_unwinding() {
let writes = AtomicUsize::new(0);
let fences = AtomicUsize::new(0);
let mut values = vec![0u8; 8];
let result = catch_unwind(AssertUnwindSafe(|| {
for_each_staged_tile_into_cosets_with(
&mut values,
None,
4,
StagedRuns::new(4, 0, 0),
Dispatch::Serial,
|_| {},
|_, _, coset| assert_ne!(coset, 0, "stop after one coset was written"),
|target, source| {
target.copy_from_slice(source);
writes.fetch_add(1, Ordering::Relaxed);
},
|| {
fences.fetch_add(1, Ordering::Relaxed);
},
);
}));
assert!(result.is_err());
assert_eq!(writes.load(Ordering::Relaxed), 1);
assert_eq!(fences.load(Ordering::Relaxed), 1);
}
#[test]
fn the_tiles_partition_the_runs() {
for log_runs in 0..=8usize {
for depth in 0..=log_runs {
for log_stride in 0..=log_runs - depth {
let runs = StagedRuns::new(1, log_stride, depth);
let label =
format!("log_runs={log_runs} log_stride={log_stride} depth={depth}");
let mut seen = vec![false; 1 << log_runs];
for index in 0..1usize << (log_runs - depth) {
for k in 0..1usize << depth {
let run = runs.run_index(index, k);
let hit = seen
.get_mut(run)
.unwrap_or_else(|| panic!("run {run} out of range, {label}"));
assert!(!*hit, "run {run} staged twice, {label}");
*hit = true;
assert_eq!(
run >> (log_stride + depth),
runs.block(index),
"index={index} k={k} {label}"
);
}
}
assert!(seen.iter().all(|&hit| hit), "a run was left out, {label}");
}
}
}
}
#[test]
fn a_pass_writes_back_what_the_callback_left() {
let dispatches = [
Dispatch::Serial,
Dispatch::Parallel { min_len: 1 },
Dispatch::Parallel { min_len: 4 },
];
for dispatch in dispatches {
for run in [1usize, 3] {
for (log_runs, log_stride, depth) in [(6, 2, 3), (6, 0, 6), (6, 6, 0), (5, 1, 2)] {
let len = run << log_runs;
let mut values: Vec<u64> = (0..len as u64).collect();
let shift = len as u64;
let label = format!(
"{dispatch:?} run={run} log_runs={log_runs} log_stride={log_stride} \
depth={depth}"
);
for_each_staged_tile(
&mut values,
StagedRuns::new(run, log_stride, depth),
dispatch,
|tile, block| {
for (k, staged) in tile.chunks_exact_mut(run).enumerate() {
let r = staged[0] as usize / run;
for (j, &value) in staged.iter().enumerate() {
assert_eq!(value, (r * run + j) as u64, "{label}");
}
assert_eq!(r >> (log_stride + depth), block, "{label}");
assert_eq!((r >> log_stride) & ((1 << depth) - 1), k, "{label}");
for value in staged.iter_mut() {
*value += shift;
}
}
},
);
for (i, &value) in values.iter().enumerate() {
assert_eq!(value, i as u64 + shift, "i={i} {label}");
}
}
}
}
}
#[test]
fn every_coset_receives_its_own_result_of_the_leading_one() {
let dispatches = [
Dispatch::Serial,
Dispatch::Parallel { min_len: 1 },
Dispatch::Parallel { min_len: 4 },
];
for dispatch in dispatches {
for cosets in [1usize, 2, 4] {
for run in [1usize, 3] {
for (log_runs, log_stride, depth) in
[(6, 2, 3), (6, 0, 6), (6, 6, 0), (5, 1, 2)]
{
let coset_len = run << log_runs;
let label = format!(
"{dispatch:?} cosets={cosets} run={run} log_runs={log_runs} \
log_stride={log_stride} depth={depth}"
);
let mut values: Vec<u64> = (0..coset_len as u64)
.chain(core::iter::repeat_n(u64::MAX, (cosets - 1) * coset_len))
.collect();
for_each_staged_tile_into_cosets(
&mut values,
None,
coset_len,
StagedRuns::new(run, log_stride, depth),
dispatch,
|tile| tile.iter_mut().for_each(|value| *value *= 2),
|tile, block, coset| {
for (k, staged) in tile.chunks_exact_mut(run).enumerate() {
let r = staged[0] as usize / (2 * run);
for (j, &value) in staged.iter().enumerate() {
assert_eq!(value, 2 * (r * run + j) as u64, "{label}");
}
assert_eq!(r >> (log_stride + depth), block, "{label}");
assert_eq!(
(r >> log_stride) & ((1 << depth) - 1),
k,
"{label}"
);
for value in staged.iter_mut() {
*value += (coset * coset_len) as u64;
}
}
},
);
for (i, &value) in values.iter().enumerate() {
let (coset, offset) = (i / coset_len, i % coset_len);
let want = 2 * offset as u64 + (coset * coset_len) as u64;
assert_eq!(value, want, "i={i} {label}");
}
}
}
}
}
}
#[test]
fn a_prefault_leaves_a_zeroed_region_zero() {
for len in [
0usize,
1,
255,
256,
257,
3 * 256 + 5,
(1 << 17) + 3,
3 << 17,
] {
let mut values = vec![0u128; len];
prefault(&mut values, 0);
prefault(&mut values[len / 3..], 0);
assert!(values.iter().all(|&value| value == 0), "len={len}");
let mut words = vec![0u64; len];
prefault(&mut words, 0);
prefault(&mut words[len / 3..], 0);
assert!(words.iter().all(|&word| word == 0), "words len={len}");
}
}
#[test]
fn a_separate_source_stands_in_for_the_leading_coset() {
let dispatches = [Dispatch::Serial, Dispatch::Parallel { min_len: 1 }];
let cosets = 4;
for dispatch in dispatches {
for (run, log_runs, log_stride, depth) in [(1, 6, 2, 3), (3, 5, 1, 2), (1, 6, 0, 6)] {
let coset_len = run << log_runs;
let runs = StagedRuns::new(run, log_stride, depth);
let label = format!("{dispatch:?} run={run} log_runs={log_runs} depth={depth}");
let source: Vec<u64> = (0..coset_len as u64).map(|v| 7 * v + 1).collect();
let prepare = |tile: &mut [u64]| tile.iter_mut().for_each(|value| *value ^= 0x55);
let process = |tile: &mut [u64], block: usize, coset: usize| {
for (k, value) in tile.iter_mut().enumerate() {
*value = 3 * *value + (1000 * block + 100 * coset + k) as u64;
}
};
let mut in_place: Vec<u64> = source
.iter()
.copied()
.chain(core::iter::repeat_n(u64::MAX, (cosets - 1) * coset_len))
.collect();
for_each_staged_tile_into_cosets(
&mut in_place,
None,
coset_len,
runs,
dispatch,
prepare,
process,
);
let mut separate = vec![u64::MAX; cosets * coset_len];
for_each_staged_tile_into_cosets(
&mut separate,
Some(source.as_slice()),
coset_len,
runs,
dispatch,
prepare,
process,
);
assert_eq!(separate, in_place, "{label}");
}
}
}
#[test]
fn streamed_runs_land_where_cached_ones_do() {
let dispatches = [Dispatch::Serial, Dispatch::Parallel { min_len: 1 }];
for dispatch in dispatches {
for (run, log_runs, log_stride, depth) in
[(1, 6, 2, 3), (3, 5, 1, 2), (4, 7, 0, 7), (7, 6, 1, 3)]
{
let coset_len = run << log_runs;
let runs = StagedRuns::new(run, log_stride, depth);
let label = format!("{dispatch:?} run={run} log_runs={log_runs} depth={depth}");
let source: Vec<u128> = (0..coset_len as u128).map(|v| (v << 64) ^ 0x5a).collect();
let prepare = |tile: &mut [u128]| tile.iter_mut().for_each(|value| *value ^= 3);
let process = |tile: &mut [u128], block: usize, coset: usize| {
for (k, value) in tile.iter_mut().enumerate() {
*value = value.rotate_left(7) ^ (1000 * block + 100 * coset + k) as u128;
}
};
let staged = |store| {
let mut values = vec![u128::MAX; 2 * coset_len];
for_each_staged_tile_into_cosets_stored(
&mut values,
Some(source.as_slice()),
coset_len,
runs,
dispatch,
store,
prepare,
process,
);
values
};
assert_eq!(staged(Store::Streamed), staged(Store::Cached), "{label}");
}
}
}
#[test]
#[should_panic = "the source is not one coset long"]
fn a_source_of_another_length_is_refused() {
let mut values = vec![0u8; 16];
for_each_staged_tile_into_cosets(
&mut values,
Some(&[0u8; 4][..]),
8,
StagedRuns::new(1, 1, 2),
Dispatch::Serial,
|_| {},
|_, _, _| {},
);
}
#[test]
#[should_panic = "cosets do not partition the matrix"]
fn cosets_that_do_not_partition_the_matrix_are_refused() {
let mut values = vec![0u8; 12];
for_each_staged_tile_into_cosets(
&mut values,
None,
8,
StagedRuns::new(1, 1, 2),
Dispatch::Serial,
|_| {},
|_, _, _| {},
);
}
#[test]
#[should_panic = "tiles do not partition the matrix"]
fn a_tile_longer_than_the_matrix_is_refused() {
let mut values = vec![0u8; 3];
for_each_staged_tile(
&mut values,
StagedRuns::new(1, 0, 2),
Dispatch::Serial,
|_, _| {},
);
}
#[test]
#[should_panic = "staged row walk leaves the matrix"]
fn a_walk_past_the_matrix_is_refused() {
let mut values = vec![0u8; 6];
for_each_staged_tile(
&mut values,
StagedRuns::new(1, 1, 1),
Dispatch::Serial,
|_, _| {},
);
}
}