use strided_basic::{reduce_axis_into, StridedError, StridedView, StridedViewMut};
struct Layout {
dims: Vec<usize>,
strides: Vec<isize>,
offset: isize,
len: usize,
}
fn layout(dims: &[usize], strides: &[isize], offset: isize, len: usize) -> Layout {
Layout {
dims: dims.to_vec(),
strides: strides.to_vec(),
offset,
len,
}
}
fn for_each_index(dims: &[usize], mut f: impl FnMut(&[usize])) {
if dims.contains(&0) {
return;
}
let mut index = vec![0usize; dims.len()];
loop {
f(&index);
let mut axis = 0;
loop {
if axis == dims.len() {
return;
}
index[axis] += 1;
if index[axis] < dims[axis] {
break;
}
index[axis] = 0;
axis += 1;
}
}
}
fn offset_of(layout: &Layout, index: &[usize]) -> usize {
let mut offset = layout.offset;
for (&i, &s) in index.iter().zip(&layout.strides) {
offset += i as isize * s;
}
offset as usize
}
fn expected_values(
src_data: &[i64],
src: &Layout,
dest: &Layout,
axis: usize,
init: i64,
) -> Vec<i64> {
let mut expected = Vec::new();
for_each_index(&dest.dims, |kept| {
let mut acc = init;
for k in 0..src.dims[axis] {
let mut index = kept.to_vec();
index.insert(axis, k);
acc = acc.wrapping_add(src_data[offset_of(src, &index)]);
}
expected.push(acc);
});
expected
}
fn assert_matches(dest_data: &[i64], dest: &Layout, expected: &[i64], label: &str) {
let mut iter = expected.iter();
for_each_index(&dest.dims, |kept| {
let offset = offset_of(dest, kept);
assert_eq!(
dest_data[offset],
*iter.next().unwrap(),
"{label}: destination index {kept:?}"
);
});
assert!(
iter.next().is_none(),
"{label}: expected longer than layout"
);
}
fn src_data_for(src: &Layout) -> Vec<i64> {
(0..src.len)
.map(|i| (i as i64).wrapping_mul(7).wrapping_sub(3))
.collect()
}
fn check_case(src: &Layout, dest: &Layout, axis: usize, init: i64, label: &str) {
let src_data = src_data_for(src);
let src_view = StridedView::<i64>::new(&src_data, &src.dims, &src.strides, src.offset).unwrap();
let expected = expected_values(&src_data, src, dest, axis, init);
let mut dest_data = vec![i64::MIN; dest.len];
{
let mut dest_view =
StridedViewMut::<i64>::new(&mut dest_data, &dest.dims, &dest.strides, dest.offset)
.unwrap();
reduce_axis_into(
&mut dest_view,
&src_view,
axis,
|x| x,
i64::wrapping_add,
init,
)
.unwrap();
}
assert_matches(&dest_data, dest, &expected, label);
}
#[test]
fn reduce_axis_into_rejects_axis_out_of_range() {
let src_data = [0i64; 6];
let src = StridedView::<i64>::new(&src_data, &[2, 3], &[3, 1], 0).unwrap();
let mut dest_data = [0i64; 3];
let mut dest = StridedViewMut::new(&mut dest_data, &[3], &[1], 0).unwrap();
let error = reduce_axis_into(&mut dest, &src, 2, |x| x, i64::wrapping_add, 0).unwrap_err();
assert!(matches!(
error,
StridedError::InvalidAxis { axis: 2, rank: 2 }
));
}
#[test]
fn reduce_axis_into_rejects_multi_element_full_reduction() {
let src_data = [1i64, 2, 3];
let src = StridedView::<i64>::new(&src_data, &[3], &[1], 0).unwrap();
let mut dest_data = [0i64; 2];
let mut dest = StridedViewMut::new(&mut dest_data, &[2], &[1], 0).unwrap();
let error = reduce_axis_into(&mut dest, &src, 0, |x| x, i64::wrapping_add, 0).unwrap_err();
assert!(matches!(
error,
StridedError::ShapeMismatch(ref dims, ref expected)
if dims == &[2] && expected == &[1]
));
}
#[test]
fn reduce_axis_into_rejects_destination_rank_mismatch() {
let src_data = [0i64; 24];
let src = StridedView::<i64>::new(&src_data, &[2, 3, 4], &[1, 2, 6], 0).unwrap();
let mut dest_data = [0i64; 3];
let mut dest = StridedViewMut::new(&mut dest_data, &[3], &[1], 0).unwrap();
let error = reduce_axis_into(&mut dest, &src, 0, |x| x, i64::wrapping_add, 0).unwrap_err();
assert!(matches!(error, StridedError::RankMismatch(1, 2)));
}
#[test]
fn reduce_axis_into_rejects_destination_shape_mismatch() {
let src_data = [0i64; 6];
let src = StridedView::<i64>::new(&src_data, &[2, 3], &[3, 1], 0).unwrap();
let mut dest_data = [0i64; 4];
let mut dest = StridedViewMut::new(&mut dest_data, &[4], &[1], 0).unwrap();
let error = reduce_axis_into(&mut dest, &src, 0, |x| x, i64::wrapping_add, 0).unwrap_err();
assert!(matches!(
error,
StridedError::ShapeMismatch(ref dims, ref expected)
if dims == &[4] && expected == &[3]
));
}
#[test]
fn reduce_axis_into_rejects_non_injective_destination() {
let src_data = [0i64; 6];
let src = StridedView::<i64>::new(&src_data, &[2, 3], &[3, 1], 0).unwrap();
let mut dest_data = [0i64; 1];
let mut dest = StridedViewMut::new(&mut dest_data, &[3], &[0], 0).unwrap();
let error = reduce_axis_into(&mut dest, &src, 0, |x| x, i64::wrapping_add, 0).unwrap_err();
assert!(matches!(error, StridedError::NonInjectiveOutputLayout));
}
#[test]
fn strided_destination_sweeps_contiguous_columns_rank3() {
let src = layout(&[16, 8, 4], &[1, 16, 128], 0, 16 * 8 * 4);
let dest = layout(&[16, 4], &[1, 19], 0, 15 + 3 * 19 + 1);
check_case(&src, &dest, 1, 0, "sweep rank3");
}
#[test]
fn strided_destination_folds_each_output_rank3() {
let src = layout(&[16, 8, 4], &[1, 16, 128], 0, 16 * 8 * 4);
let dest = layout(&[16, 4], &[2, 35], 0, 15 * 2 + 3 * 35 + 1);
check_case(&src, &dest, 1, 0, "fold rank3");
}
#[test]
fn strided_destination_folds_each_output_rank2() {
let src = layout(&[16, 8], &[1, 16], 0, 16 * 8);
let dest = layout(&[16], &[3], 0, 15 * 3 + 1);
check_case(&src, &dest, 1, 0, "fold rank2");
}
#[test]
fn strided_destination_fold_over_unit_stride_axis() {
let src = layout(&[8, 16], &[1, 8], 0, 8 * 16);
let dest = layout(&[16], &[3], 0, 15 * 3 + 1);
check_case(&src, &dest, 0, 0, "fold unit axis");
check_case(&src, &dest, 0, 12345, "fold unit axis init");
}
#[test]
fn strided_destination_carries_across_kept_axes() {
let src = layout(&[3, 5, 4], &[1, 3, 15], 0, 3 * 5 * 4);
let dest = layout(&[3, 4], &[1, 7], 0, 2 + 3 * 7 + 1);
check_case(&src, &dest, 1, 0, "carry rank3");
}
#[test]
fn strided_destination_spills_cursor_above_inline_rank() {
let src = layout(
&[1, 2, 2, 2, 2, 2, 2, 2, 2, 2],
&[1, 1, 2, 4, 8, 16, 32, 64, 128, 256],
0,
512,
);
let dest_strides = [1isize, 1000, 2000, 4000, 8000, 16000, 32000, 64000, 128000];
let dest_len: usize = 1 + 1000 + 2000 + 4000 + 8000 + 16000 + 32000 + 64000 + 128000 + 1;
let dest = layout(&[2; 9], &dest_strides, 0, dest_len);
check_case(&src, &dest, 0, 0, "spill rank9");
}
#[test]
fn empty_reduced_axis_skips_an_empty_destination() {
let src_data: [i64; 0] = [];
let src = StridedView::<i64>::new(&src_data, &[0, 0], &[0, 1], 0).unwrap();
let mut dest_data: [i64; 0] = [];
let mut dest = StridedViewMut::new(&mut dest_data, &[0], &[1], 0).unwrap();
reduce_axis_into(&mut dest, &src, 0, |x| x, i64::wrapping_add, 9).unwrap();
}
#[test]
fn empty_reduced_axis_fills_a_rank0_destination() {
let src_data: [i64; 0] = [];
let src = StridedView::<i64>::new(&src_data, &[0], &[1], 0).unwrap();
let mut dest_data = [123i64];
let mut dest = StridedViewMut::new(&mut dest_data, &[], &[], 0).unwrap();
reduce_axis_into(&mut dest, &src, 0, |x| x, i64::wrapping_add, 77).unwrap();
assert_eq!(dest_data, [77]);
}
#[test]
fn empty_reduced_axis_spills_the_fill_cursor_above_inline_rank() {
let src_data: [i64; 0] = [];
let src =
StridedView::<i64>::new(&src_data, &[0, 2, 2, 2, 2, 2, 2, 2, 2, 2], &[1; 10], 0).unwrap();
let mut dest_data = vec![0i64; 512];
let dest_dims = [2usize; 9];
let mut dest_strides = [0isize; 9];
let mut step = 1isize;
for (stride, &dim) in dest_strides.iter_mut().zip(dest_dims.iter()) {
*stride = step;
step *= dim as isize;
}
let mut dest = StridedViewMut::new(&mut dest_data, &dest_dims, &dest_strides, 0).unwrap();
reduce_axis_into(&mut dest, &src, 0, |x| x, i64::wrapping_add, 5).unwrap();
assert!(dest_data.iter().all(|&value| value == 5));
}
#[cfg(feature = "parallel")]
#[test]
fn strided_destination_splits_across_two_workers() {
use std::num::NonZeroUsize;
use strided_basic::{with_execution_policy, ExecutionPolicy};
let (n, m, k) = (32usize, 64usize, 32usize);
let src = layout(&[n, m, k], &[1, n as isize, (n * m) as isize], 0, n * m * k);
let dest = layout(
&[n, k],
&[1, (n + 3) as isize],
0,
(n - 1) + (k - 1) * (n + 3) + 1,
);
assert!(dest.dims[0] * dest.dims[1] * m > 1 << 15);
let src_data = src_data_for(&src);
let src_view = StridedView::<i64>::new(&src_data, &src.dims, &src.strides, src.offset).unwrap();
let expected = expected_values(&src_data, &src, &dest, 1, 0);
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(2)
.build()
.unwrap();
let mut dest_data = vec![i64::MIN; dest.len];
pool.install(|| {
with_execution_policy(
ExecutionPolicy::Rayon {
max_threads: NonZeroUsize::new(2).unwrap(),
},
|| {
let mut dest_view =
StridedViewMut::<i64>::new(&mut dest_data, &dest.dims, &dest.strides, 0)
.unwrap();
reduce_axis_into(&mut dest_view, &src_view, 1, |x| x, i64::wrapping_add, 0)
.unwrap();
},
);
});
assert_matches(&dest_data, &dest, &expected, "parallel strided sweep");
}