use crate::neural_network::Tensor;
use crate::neural_network::layers::convolution::PaddingType;
use ndarray::{ArrayD, IxDyn};
use rayon::prelude::*;
tunable_gate! {
pub(crate) POOL_PARALLEL_MIN_OPS => pool_parallel_min_ops / set_pool_parallel_min_ops = 12_000
}
fn pool_geometry(
sp: &[usize],
pool: &[usize],
strides: &[usize],
padding: PaddingType,
) -> (Vec<usize>, Vec<usize>) {
let r = sp.len();
match padding {
PaddingType::Valid => {
let out_sp = (0..r).map(|k| (sp[k] - pool[k]) / strides[k] + 1).collect();
(out_sp, vec![0; r])
}
PaddingType::Same => {
let out_sp: Vec<usize> = (0..r).map(|k| sp[k].div_ceil(strides[k])).collect();
let pad_before = (0..r)
.map(|k| (((out_sp[k] - 1) * strides[k] + pool[k]).saturating_sub(sp[k])) / 2)
.collect();
(out_sp, pad_before)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PoolKind {
Max,
Average,
}
fn row_major_strides(shape: &[usize]) -> Vec<usize> {
let mut strides = vec![1usize; shape.len()];
for k in (0..shape.len().saturating_sub(1)).rev() {
strides[k] = strides[k + 1] * shape[k + 1];
}
strides
}
#[inline]
fn increment_index(idx: &mut [usize], dims: &[usize]) -> bool {
for k in (0..idx.len()).rev() {
idx[k] += 1;
if idx[k] < dims[k] {
return true;
}
idx[k] = 0;
}
false
}
fn decode_index(mut flat: usize, dims: &[usize]) -> Vec<usize> {
let mut idx = vec![0usize; dims.len()];
for k in (0..dims.len()).rev() {
if dims[k] > 0 {
idx[k] = flat % dims[k];
flat /= dims[k];
}
}
idx
}
const POOL_MIN_CHUNK_OUT: usize = 1024;
fn map_planes<R, F>(
bc_total: usize,
work_per_plane: usize,
force_parallel: Option<bool>,
f: F,
) -> Vec<R>
where
R: Send,
F: Fn(usize) -> R + Sync + Send,
{
let parallel = force_parallel
.unwrap_or(bc_total.saturating_mul(work_per_plane) >= pool_parallel_min_ops());
if parallel {
(0..bc_total).into_par_iter().map(f).collect()
} else {
(0..bc_total).map(f).collect()
}
}
pub(super) fn windowed_pool_forward(
input: &Tensor,
pool: &[usize],
strides: &[usize],
kind: PoolKind,
padding: PaddingType,
) -> (Tensor, Option<Vec<usize>>) {
windowed_pool_forward_impl(input, pool, strides, kind, padding, None)
}
pub fn windowed_pool_forward_impl(
input: &Tensor,
pool: &[usize],
strides: &[usize],
kind: PoolKind,
padding: PaddingType,
force_parallel: Option<bool>,
) -> (Tensor, Option<Vec<usize>>) {
let shape = input.shape();
let (batch, channels) = (shape[0], shape[1]);
let sp = &shape[2..];
let r = sp.len();
let (out_sp, pad_before) = pool_geometry(sp, pool, strides, padding);
let plane_in: usize = sp.iter().product();
let plane_out: usize = out_sp.iter().product();
let in_strides = row_major_strides(sp);
let bc_total = batch * channels;
let track = kind == PoolKind::Max;
let input_std = input.as_standard_layout();
let in_flat = input_std
.as_slice()
.expect("standard-layout array is contiguous");
let process_range = |bc: usize, c0: usize, len: usize| -> (Vec<f32>, Vec<usize>) {
let plane = &in_flat[bc * plane_in..(bc + 1) * plane_in];
let mut out_chunk = vec![0.0f32; len];
let mut arg_chunk = if track { vec![0usize; len] } else { Vec::new() };
let mut o = decode_index(c0, &out_sp);
let mut w = vec![0usize; r];
for i in 0..len {
w.iter_mut().for_each(|x| *x = 0);
let mut sum = 0.0f32;
let mut count = 0usize;
let mut max_val = f32::NEG_INFINITY;
let mut max_idx = 0usize;
loop {
let mut in_idx = 0usize;
let mut in_bounds = true;
for k in 0..r {
let p = (o[k] * strides[k] + w[k]) as isize - pad_before[k] as isize;
if p < 0 || p as usize >= sp[k] {
in_bounds = false;
break;
}
in_idx += p as usize * in_strides[k];
}
if in_bounds {
let v = plane[in_idx];
match kind {
PoolKind::Max => {
if v.is_nan() {
if !max_val.is_nan() {
max_val = v;
max_idx = in_idx;
}
} else if v > max_val {
max_val = v;
max_idx = in_idx;
}
}
PoolKind::Average => {
sum += v;
count += 1;
}
}
}
if !increment_index(&mut w, pool) {
break;
}
}
match kind {
PoolKind::Max => {
out_chunk[i] = max_val;
arg_chunk[i] = max_idx;
}
PoolKind::Average => {
out_chunk[i] = if count > 0 { sum / count as f32 } else { 0.0 };
}
}
increment_index(&mut o, &out_sp);
}
(out_chunk, arg_chunk)
};
let total_ops = bc_total
.saturating_mul(plane_out)
.saturating_mul(pool.iter().product::<usize>());
let parallel = force_parallel.unwrap_or(total_ops >= pool_parallel_min_ops());
let chunk_len = if parallel && bc_total > 0 && plane_out > 0 {
let chunks_per_plane = rayon::current_num_threads().div_ceil(bc_total);
plane_out.div_ceil(chunks_per_plane).max(POOL_MIN_CHUNK_OUT)
} else {
plane_out.max(1)
};
let tasks: Vec<(usize, usize, usize)> = (0..bc_total)
.flat_map(|bc| {
(0..plane_out)
.step_by(chunk_len.max(1))
.map(move |c0| (bc, c0, chunk_len.min(plane_out - c0)))
})
.collect();
let results: Vec<(Vec<f32>, Vec<usize>)> = if parallel {
tasks
.par_iter()
.map(|&(bc, c0, len)| process_range(bc, c0, len))
.collect()
} else {
tasks
.iter()
.map(|&(bc, c0, len)| process_range(bc, c0, len))
.collect()
};
let mut out_flat = vec![0.0f32; bc_total * plane_out];
let mut argmax = if track {
vec![0usize; bc_total * plane_out]
} else {
Vec::new()
};
for (&(bc, c0, len), (out_chunk, arg_chunk)) in tasks.iter().zip(results) {
let base = bc * plane_out + c0;
out_flat[base..base + len].copy_from_slice(&out_chunk);
if track {
argmax[base..base + len].copy_from_slice(&arg_chunk);
}
}
let mut out_shape = Vec::with_capacity(2 + r);
out_shape.push(batch);
out_shape.push(channels);
out_shape.extend_from_slice(&out_sp);
let output = ArrayD::from_shape_vec(IxDyn(&out_shape), out_flat)
.expect("pool output length matches its shape");
(output, if track { Some(argmax) } else { None })
}
pub(super) fn windowed_pool_backward(
grad_output: &Tensor,
input_shape: &[usize],
pool: &[usize],
strides: &[usize],
kind: PoolKind,
argmax: Option<&[usize]>,
padding: PaddingType,
) -> Tensor {
let (batch, channels) = (input_shape[0], input_shape[1]);
let sp = &input_shape[2..];
let r = sp.len();
let out_sp = &grad_output.shape()[2..];
let (_, pad_before) = pool_geometry(sp, pool, strides, padding);
let plane_in: usize = sp.iter().product();
let plane_out: usize = out_sp.iter().product();
let in_strides = row_major_strides(sp);
let bc_total = batch * channels;
let grad_std = grad_output.as_standard_layout();
let grad_flat = grad_std
.as_slice()
.expect("standard-layout array is contiguous");
let process_bc = |bc: usize| -> Vec<f32> {
let grad_out_plane = &grad_flat[bc * plane_out..(bc + 1) * plane_out];
let mut grad_in_plane = vec![0.0f32; plane_in];
match kind {
PoolKind::Max => {
let arg = argmax.expect("max pooling backward requires arg-max positions");
let arg_plane = &arg[bc * plane_out..(bc + 1) * plane_out];
for (o_flat, &g) in grad_out_plane.iter().enumerate() {
grad_in_plane[arg_plane[o_flat]] += g;
}
}
PoolKind::Average => {
let mut o = vec![0usize; r];
let mut w = vec![0usize; r];
let mut o_flat = 0usize;
loop {
w.iter_mut().for_each(|x| *x = 0);
let mut count = 0usize;
loop {
if (0..r).all(|k| {
let p = (o[k] * strides[k] + w[k]) as isize - pad_before[k] as isize;
p >= 0 && (p as usize) < sp[k]
}) {
count += 1;
}
if !increment_index(&mut w, pool) {
break;
}
}
if count > 0 {
let grad_per_element = grad_out_plane[o_flat] / count as f32;
w.iter_mut().for_each(|x| *x = 0);
loop {
let mut in_idx = 0usize;
let mut in_bounds = true;
for k in 0..r {
let p =
(o[k] * strides[k] + w[k]) as isize - pad_before[k] as isize;
if p < 0 || p as usize >= sp[k] {
in_bounds = false;
break;
}
in_idx += p as usize * in_strides[k];
}
if in_bounds {
grad_in_plane[in_idx] += grad_per_element;
}
if !increment_index(&mut w, pool) {
break;
}
}
}
o_flat += 1;
if !increment_index(&mut o, out_sp) {
break;
}
}
}
}
grad_in_plane
};
let planes = map_planes(
bc_total,
plane_out * pool.iter().product::<usize>(),
None,
process_bc,
);
let mut grad_in = Vec::with_capacity(bc_total * plane_in);
for plane in planes {
grad_in.extend(plane);
}
ArrayD::from_shape_vec(IxDyn(input_shape), grad_in)
.expect("grad-input length matches the input shape")
}
pub(super) fn global_pool_forward(input: &Tensor, kind: PoolKind) -> (Tensor, Option<Vec<usize>>) {
let shape = input.shape();
let (batch, channels) = (shape[0], shape[1]);
let plane_in: usize = shape[2..].iter().product();
let bc_total = batch * channels;
let track = kind == PoolKind::Max;
let input_std = input.as_standard_layout();
let in_flat = input_std
.as_slice()
.expect("standard-layout array is contiguous");
let process_bc = |bc: usize| -> (f32, usize) {
let plane = &in_flat[bc * plane_in..(bc + 1) * plane_in];
match kind {
PoolKind::Max => {
let mut max_val = f32::NEG_INFINITY;
let mut max_idx = 0usize;
for (i, &v) in plane.iter().enumerate() {
if v.is_nan() {
if !max_val.is_nan() {
max_val = v;
max_idx = i;
}
} else if v > max_val {
max_val = v;
max_idx = i;
}
}
(max_val, max_idx)
}
PoolKind::Average => {
let sum: f32 = plane.iter().sum();
(sum / plane_in as f32, 0)
}
}
};
let results = map_planes(bc_total, plane_in, None, process_bc);
let mut out_flat = Vec::with_capacity(bc_total);
let mut argmax = if track {
Vec::with_capacity(bc_total)
} else {
Vec::new()
};
for (val, idx) in results {
out_flat.push(val);
if track {
argmax.push(idx);
}
}
let output = ArrayD::from_shape_vec(IxDyn(&[batch, channels]), out_flat)
.expect("global-pool output length matches [batch, channels]");
(output, if track { Some(argmax) } else { None })
}
pub(super) fn global_pool_backward(
grad_output: &Tensor,
input_shape: &[usize],
kind: PoolKind,
argmax: Option<&[usize]>,
) -> Tensor {
let (batch, channels) = (input_shape[0], input_shape[1]);
let plane_in: usize = input_shape[2..].iter().product();
let bc_total = batch * channels;
let grad_std = grad_output.as_standard_layout();
let grad_flat = grad_std
.as_slice()
.expect("standard-layout array is contiguous");
let process_bc = |bc: usize| -> Vec<f32> {
let g = grad_flat[bc];
let mut plane = vec![0.0f32; plane_in];
match kind {
PoolKind::Max => {
let arg = argmax.expect("global max pooling backward requires arg-max positions");
plane[arg[bc]] += g;
}
PoolKind::Average => {
let grad_per_element = g / plane_in as f32;
plane.iter_mut().for_each(|x| *x = grad_per_element);
}
}
plane
};
let planes = map_planes(bc_total, plane_in, None, process_bc);
let mut grad_in = Vec::with_capacity(bc_total * plane_in);
for plane in planes {
grad_in.extend(plane);
}
ArrayD::from_shape_vec(IxDyn(input_shape), grad_in)
.expect("grad-input length matches the input shape")
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_row_major_strides_3d() {
let s = row_major_strides(&[3, 4, 5]);
assert_eq!(s, vec![20, 5, 1]);
}
#[test]
fn test_row_major_strides_2d() {
let s = row_major_strides(&[2, 3]);
assert_eq!(s, vec![3, 1]);
}
#[test]
fn test_row_major_strides_1d() {
let s = row_major_strides(&[7]);
assert_eq!(s, vec![1]);
}
#[test]
fn test_row_major_strides_empty() {
let s = row_major_strides(&[]);
assert_eq!(s, Vec::<usize>::new());
}
#[test]
fn test_increment_index_normal() {
let mut idx = vec![0usize, 0];
let more = increment_index(&mut idx, &[2, 3]);
assert!(more);
assert_eq!(idx, vec![0, 1]);
}
#[test]
fn test_increment_index_carry() {
let mut idx = vec![0usize, 2];
let more = increment_index(&mut idx, &[2, 3]);
assert!(more);
assert_eq!(idx, vec![1, 0]);
}
#[test]
fn test_increment_index_exhausted() {
let mut idx = vec![1usize, 2];
let more = increment_index(&mut idx, &[2, 3]);
assert!(!more);
assert_eq!(idx, vec![0, 0]);
}
#[test]
fn test_increment_index_1d_last_step() {
let mut idx = vec![3usize];
let more = increment_index(&mut idx, &[4]);
assert!(!more);
assert_eq!(idx, vec![0]);
}
#[test]
fn test_increment_index_1d_mid() {
let mut idx = vec![2usize];
let more = increment_index(&mut idx, &[4]);
assert!(more);
assert_eq!(idx, vec![3]);
}
#[test]
fn test_windowed_pool_forward_1d_max() {
let data = ArrayD::from_shape_vec(IxDyn(&[1, 1, 4]), vec![3.0f32, 1.0, 4.0, 1.0]).unwrap();
let (out, argmax) =
windowed_pool_forward(&data, &[2], &[2], PoolKind::Max, PaddingType::Valid);
assert_eq!(out.shape(), &[1, 1, 2]);
let flat: Vec<f32> = out.iter().copied().collect();
assert_abs_diff_eq!(flat[0], 3.0, epsilon = 1e-6);
assert_abs_diff_eq!(flat[1], 4.0, epsilon = 1e-6);
let am = argmax.expect("Max pool must return argmax");
assert_eq!(am, vec![0, 2]);
}
#[test]
fn test_windowed_pool_forward_2d_max() {
let data =
ArrayD::from_shape_vec(IxDyn(&[1, 1, 2, 2]), vec![1.0f32, 2.0, 3.0, 4.0]).unwrap();
let (out, argmax) =
windowed_pool_forward(&data, &[2, 2], &[2, 2], PoolKind::Max, PaddingType::Valid);
assert_eq!(out.shape(), &[1, 1, 1, 1]);
let flat: Vec<f32> = out.iter().copied().collect();
assert_abs_diff_eq!(flat[0], 4.0, epsilon = 1e-6);
let am = argmax.expect("Max pool must return argmax");
assert_eq!(am, vec![3]);
}
#[test]
fn test_windowed_pool_forward_1d_avg() {
let data = ArrayD::from_shape_vec(IxDyn(&[1, 1, 4]), vec![3.0f32, 1.0, 4.0, 1.0]).unwrap();
let (out, argmax) =
windowed_pool_forward(&data, &[2], &[2], PoolKind::Average, PaddingType::Valid);
assert_eq!(out.shape(), &[1, 1, 2]);
let flat: Vec<f32> = out.iter().copied().collect();
assert_abs_diff_eq!(flat[0], 2.0, epsilon = 1e-6);
assert_abs_diff_eq!(flat[1], 2.5, epsilon = 1e-6);
assert!(argmax.is_none(), "Average pool must not return argmax");
}
#[test]
fn test_windowed_pool_forward_2d_avg() {
let data =
ArrayD::from_shape_vec(IxDyn(&[1, 1, 2, 2]), vec![1.0f32, 2.0, 3.0, 4.0]).unwrap();
let (out, argmax) = windowed_pool_forward(
&data,
&[2, 2],
&[2, 2],
PoolKind::Average,
PaddingType::Valid,
);
assert_eq!(out.shape(), &[1, 1, 1, 1]);
let flat: Vec<f32> = out.iter().copied().collect();
assert_abs_diff_eq!(flat[0], 2.5, epsilon = 1e-6);
assert!(argmax.is_none(), "Average pool must not return argmax");
}
#[test]
fn test_windowed_pool_backward_1d_max_nonoverlapping() {
let grad_out = ArrayD::from_shape_vec(IxDyn(&[1, 1, 2]), vec![1.0f32, 1.0]).unwrap();
let argmax = vec![0usize, 2];
let grad_in = windowed_pool_backward(
&grad_out,
&[1, 1, 4],
&[2],
&[2],
PoolKind::Max,
Some(&argmax),
PaddingType::Valid,
);
assert_eq!(grad_in.shape(), &[1, 1, 4]);
let flat: Vec<f32> = grad_in.iter().copied().collect();
assert_abs_diff_eq!(flat[0], 1.0, epsilon = 1e-6); assert_abs_diff_eq!(flat[1], 0.0, epsilon = 1e-6); assert_abs_diff_eq!(flat[2], 1.0, epsilon = 1e-6); assert_abs_diff_eq!(flat[3], 0.0, epsilon = 1e-6); }
#[test]
fn test_windowed_pool_backward_1d_max_varied_grads() {
let grad_out = ArrayD::from_shape_vec(IxDyn(&[1, 1, 2]), vec![2.0f32, 5.0]).unwrap();
let argmax = vec![1usize, 3];
let grad_in = windowed_pool_backward(
&grad_out,
&[1, 1, 4],
&[2],
&[2],
PoolKind::Max,
Some(&argmax),
PaddingType::Valid,
);
let flat: Vec<f32> = grad_in.iter().copied().collect();
assert_abs_diff_eq!(flat[0], 0.0, epsilon = 1e-6);
assert_abs_diff_eq!(flat[1], 2.0, epsilon = 1e-6);
assert_abs_diff_eq!(flat[2], 0.0, epsilon = 1e-6);
assert_abs_diff_eq!(flat[3], 5.0, epsilon = 1e-6);
}
#[test]
fn test_windowed_pool_backward_1d_avg_overlapping() {
let grad_out = ArrayD::from_shape_vec(IxDyn(&[1, 1, 3]), vec![1.0f32, 1.0, 1.0]).unwrap();
let grad_in = windowed_pool_backward(
&grad_out,
&[1, 1, 4],
&[2],
&[1],
PoolKind::Average,
None,
PaddingType::Valid,
);
assert_eq!(grad_in.shape(), &[1, 1, 4]);
let flat: Vec<f32> = grad_in.iter().copied().collect();
assert_abs_diff_eq!(flat[0], 0.5, epsilon = 1e-6); assert_abs_diff_eq!(flat[1], 1.0, epsilon = 1e-6); assert_abs_diff_eq!(flat[2], 1.0, epsilon = 1e-6); assert_abs_diff_eq!(flat[3], 0.5, epsilon = 1e-6); }
#[test]
fn test_windowed_pool_backward_1d_avg_nonoverlapping() {
let grad_out = ArrayD::from_shape_vec(IxDyn(&[1, 1, 2]), vec![1.0f32, 1.0]).unwrap();
let grad_in = windowed_pool_backward(
&grad_out,
&[1, 1, 4],
&[2],
&[2],
PoolKind::Average,
None,
PaddingType::Valid,
);
assert_eq!(grad_in.shape(), &[1, 1, 4]);
let flat: Vec<f32> = grad_in.iter().copied().collect();
assert_abs_diff_eq!(flat[0], 0.5, epsilon = 1e-6);
assert_abs_diff_eq!(flat[1], 0.5, epsilon = 1e-6);
assert_abs_diff_eq!(flat[2], 0.5, epsilon = 1e-6);
assert_abs_diff_eq!(flat[3], 0.5, epsilon = 1e-6);
}
#[test]
fn test_windowed_pool_output_shape_1d() {
let data = ArrayD::from_shape_vec(IxDyn(&[1, 1, 6]), vec![1.0f32; 6]).unwrap();
let (out, _) =
windowed_pool_forward(&data, &[3], &[2], PoolKind::Average, PaddingType::Valid);
assert_eq!(out.shape(), &[1, 1, 2]);
}
#[test]
fn test_windowed_pool_output_shape_2d_batched() {
let data =
ArrayD::from_shape_vec(IxDyn(&[2, 3, 4, 4]), vec![1.0f32; 2 * 3 * 4 * 4]).unwrap();
let (out, _) =
windowed_pool_forward(&data, &[2, 2], &[2, 2], PoolKind::Max, PaddingType::Valid);
assert_eq!(out.shape(), &[2, 3, 2, 2]);
}
#[test]
fn test_windowed_pool_forward_2d_max_tie_breaks_to_first() {
let data =
ArrayD::from_shape_vec(IxDyn(&[1, 1, 2, 2]), vec![5.0f32, 5.0, 1.0, 2.0]).unwrap();
let (out, argmax) =
windowed_pool_forward(&data, &[2, 2], &[2, 2], PoolKind::Max, PaddingType::Valid);
assert_eq!(out.shape(), &[1, 1, 1, 1]);
let flat: Vec<f32> = out.iter().copied().collect();
assert_abs_diff_eq!(flat[0], 5.0, epsilon = 1e-6);
let am = argmax.expect("Max pool must return argmax");
assert_eq!(
am,
vec![0],
"tie must resolve to the FIRST maximum (flat index 0)"
);
}
#[test]
fn test_global_pool_forward_max_tie_breaks_to_first() {
let data = ArrayD::from_shape_vec(IxDyn(&[1, 1, 4]), vec![5.0f32, 5.0, 1.0, 2.0]).unwrap();
let (out, argmax) = global_pool_forward(&data, PoolKind::Max);
assert_eq!(out.shape(), &[1, 1]);
let flat: Vec<f32> = out.iter().copied().collect();
assert_abs_diff_eq!(flat[0], 5.0, epsilon = 1e-6);
let am = argmax.expect("Global max pool must return argmax");
assert_eq!(
am,
vec![0],
"tie must resolve to the FIRST maximum (index 0)"
);
}
}