use super::PaddingType;
use crate::error::Error;
use crate::math::matmul::{gemm_par_auto, gemm_par_switch};
use crate::neural_network::Tensor;
use ndarray::{Array2, Array3, ArrayD, ArrayView2, ArrayViewMut2, Axis, IxDyn};
use rayon::prelude::*;
tunable_gate! {
pub(crate) CONV_PARALLEL_MIN_FLOPS => conv_parallel_min_flops / set_conv_parallel_min_flops = 4_000_000
}
const CONV_MIN_CHUNK_COLS: usize = 64;
pub(super) struct ConvGradients {
pub weight_grad: Vec<f32>,
pub bias_grad: Vec<f32>,
pub input_grad: Tensor,
}
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 map_indexed<R, F>(n: usize, parallel: bool, f: F) -> Vec<R>
where
R: Send,
F: Fn(usize) -> R + Sync + Send,
{
if parallel {
(0..n).into_par_iter().map(f).collect()
} else {
(0..n).map(f).collect()
}
}
type ConvGeometry = (Vec<usize>, Vec<usize>, Vec<usize>);
fn conv_geometry(
sp: &[usize],
k_dims: &[usize],
strides: &[usize],
padding: PaddingType,
) -> Result<ConvGeometry, Error> {
let r = sp.len();
match padding {
PaddingType::Valid => {
if let Some(d) = (0..r).find(|&d| sp[d] < k_dims[d]) {
return Err(Error::invalid_input(format!(
"Valid-padding convolution requires every input spatial dimension to be at \
least the kernel size: axis {d} has input size {} < kernel size {}",
sp[d], k_dims[d]
)));
}
let out_sp: Vec<usize> = (0..r)
.map(|d| (sp[d] - k_dims[d]) / strides[d] + 1)
.collect();
Ok((out_sp, vec![0; r], sp.to_vec()))
}
PaddingType::Same => {
let out_sp: Vec<usize> = (0..r).map(|d| sp[d].div_ceil(strides[d])).collect();
let pad_before: Vec<usize> = (0..r)
.map(|d| (((out_sp[d] - 1) * strides[d] + k_dims[d]).saturating_sub(sp[d])) / 2)
.collect();
let padded_sp: Vec<usize> = (0..r)
.map(|d| ((out_sp[d] - 1) * strides[d] + k_dims[d]).max(sp[d]))
.collect();
Ok((out_sp, pad_before, padded_sp))
}
}
}
fn build_padded(
in_flat: &[f32],
bc: usize,
sp: &[usize],
padded_sp: &[usize],
pad_before: &[usize],
) -> Vec<f32> {
let r = sp.len();
let in_plane: usize = sp.iter().product();
let padded_plane: usize = padded_sp.iter().product();
let padded_strides = row_major_strides(padded_sp);
let mut out = vec![0.0f32; bc * padded_plane];
for chan in 0..bc {
let in_base = chan * in_plane;
let pad_base = chan * padded_plane;
let mut si = vec![0usize; r];
let mut si_flat = 0usize;
loop {
let mut pidx = 0usize;
for d in 0..r {
pidx += (si[d] + pad_before[d]) * padded_strides[d];
}
out[pad_base + pidx] = in_flat[in_base + si_flat];
si_flat += 1;
if !increment_index(&mut si, sp) {
break;
}
}
}
out
}
fn crop_padded(
padded: &[f32],
bc: usize,
sp: &[usize],
padded_sp: &[usize],
pad_before: &[usize],
) -> Vec<f32> {
let r = sp.len();
let in_plane: usize = sp.iter().product();
let padded_plane: usize = padded_sp.iter().product();
let padded_strides = row_major_strides(padded_sp);
let mut out = vec![0.0f32; bc * in_plane];
for chan in 0..bc {
let in_base = chan * in_plane;
let pad_base = chan * padded_plane;
let mut si = vec![0usize; r];
let mut si_flat = 0usize;
loop {
let mut pidx = 0usize;
for d in 0..r {
pidx += (si[d] + pad_before[d]) * padded_strides[d];
}
out[in_base + si_flat] = padded[pad_base + pidx];
si_flat += 1;
if !increment_index(&mut si, sp) {
break;
}
}
}
out
}
fn im2col_offsets(
out_sp: &[usize],
k_dims: &[usize],
strides: &[usize],
padded_strides: &[usize],
) -> Vec<usize> {
let r = out_sp.len();
let out_plane: usize = out_sp.iter().product();
let k_plane: usize = k_dims.iter().product();
let mut offsets = vec![0usize; k_plane * out_plane];
let mut o = vec![0usize; r];
let mut o_flat = 0usize;
loop {
let mut kk = vec![0usize; r];
let mut kk_flat = 0usize;
loop {
let mut pidx = 0usize;
for d in 0..r {
pidx += (o[d] * strides[d] + kk[d]) * padded_strides[d];
}
offsets[kk_flat * out_plane + o_flat] = pidx;
kk_flat += 1;
if !increment_index(&mut kk, k_dims) {
break;
}
}
o_flat += 1;
if !increment_index(&mut o, out_sp) {
break;
}
}
offsets
}
struct ColContext<'a> {
padded: &'a [f32],
cin: usize,
padded_plane: usize,
k_plane: usize,
out_plane: usize,
offsets: &'a [usize],
}
fn build_col_range(ctx: &ColContext, b: usize, c0: usize, c1: usize) -> Vec<f32> {
let cols = c1 - c0;
let mut col = vec![0.0f32; ctx.cin * ctx.k_plane * cols];
let b_base = b * ctx.cin * ctx.padded_plane;
for c in 0..ctx.cin {
let pc = b_base + c * ctx.padded_plane;
for kk in 0..ctx.k_plane {
let krow = (c * ctx.k_plane + kk) * cols;
let off = kk * ctx.out_plane;
for (i, o) in (c0..c1).enumerate() {
col[krow + i] = ctx.padded[pc + ctx.offsets[off + o]];
}
}
}
col
}
pub(super) fn conv_forward(
input: &Tensor,
weights: &[f32],
weight_shape: &[usize],
bias: &[f32],
strides: &[usize],
padding: PaddingType,
) -> Result<Tensor, Error> {
conv_forward_impl(input, weights, weight_shape, bias, strides, padding, None)
}
pub fn conv_forward_impl(
input: &Tensor,
weights: &[f32],
weight_shape: &[usize],
bias: &[f32],
strides: &[usize],
padding: PaddingType,
force_parallel: Option<bool>,
) -> Result<Tensor, Error> {
let in_shape = input.shape();
let (batch, cin) = (in_shape[0], in_shape[1]);
let sp = &in_shape[2..];
let r = sp.len();
let filters = weight_shape[0];
let k_dims = &weight_shape[2..];
let k_plane: usize = k_dims.iter().product();
let (out_sp, pad_before, padded_sp) = conv_geometry(sp, k_dims, strides, padding)?;
let out_plane: usize = out_sp.iter().product();
let padded_plane: usize = padded_sp.iter().product();
let padded_strides = row_major_strides(&padded_sp);
let input_std = input.as_standard_layout();
let in_flat = input_std
.as_slice()
.expect("standard-layout array is contiguous");
let padded_storage = if padded_sp.as_slice() != sp {
Some(build_padded(
in_flat,
batch * cin,
sp,
&padded_sp,
&pad_before,
))
} else {
None
};
let padded: &[f32] = padded_storage.as_deref().unwrap_or(in_flat);
let k_total = cin * k_plane;
let offsets = im2col_offsets(&out_sp, k_dims, strides, &padded_strides);
let w_mat = ArrayView2::from_shape((filters, k_total), weights)
.expect("weights length matches [F, Cin*k]");
let ctx = ColContext {
padded,
cin,
padded_plane,
k_plane,
out_plane,
offsets: &offsets,
};
let fill_block = |b: usize, c0: usize, mut blk: ArrayViewMut2<f32>| {
let cols = blk.ncols();
let col = build_col_range(&ctx, b, c0, c0 + cols);
let col_mat = ArrayView2::from_shape((k_total, cols), &col)
.expect("col block length matches [Cin*k, cols]");
let mut prod = w_mat.dot(&col_mat); for (f, mut row) in prod.outer_iter_mut().enumerate() {
row += bias[f];
}
blk.assign(&prod);
};
let gemm_flops = 2usize
.saturating_mul(batch)
.saturating_mul(filters)
.saturating_mul(out_plane)
.saturating_mul(k_total);
let parallel = force_parallel.unwrap_or(gemm_flops >= conv_parallel_min_flops());
let mut out3 = Array3::<f32>::zeros((batch, filters, out_plane));
if parallel {
let chunks_per_item = rayon::current_num_threads().div_ceil(batch);
let chunk_cols = out_plane.div_ceil(chunks_per_item).max(CONV_MIN_CHUNK_COLS);
out3.axis_iter_mut(Axis(0))
.into_par_iter()
.enumerate()
.for_each(|(b, mut out_b)| {
out_b
.axis_chunks_iter_mut(Axis(1), chunk_cols)
.into_par_iter()
.enumerate()
.for_each(|(ci, blk)| fill_block(b, ci * chunk_cols, blk));
});
} else {
for (b, mut out_b) in out3.axis_iter_mut(Axis(0)).enumerate() {
fill_block(b, 0, out_b.view_mut());
}
}
let mut out_shape = Vec::with_capacity(2 + r);
out_shape.push(batch);
out_shape.push(filters);
out_shape.extend_from_slice(&out_sp);
Ok(out3
.into_shape_with_order(IxDyn(&out_shape))
.expect("conv output length matches shape"))
}
pub(super) fn conv_backward(
grad_output: &Tensor,
input: &Tensor,
weights: &[f32],
weight_shape: &[usize],
strides: &[usize],
padding: PaddingType,
) -> Result<ConvGradients, Error> {
let in_shape = input.shape();
let (batch, cin) = (in_shape[0], in_shape[1]);
let sp = &in_shape[2..];
let r = sp.len();
let filters = weight_shape[0];
let k_dims = &weight_shape[2..];
let k_plane: usize = k_dims.iter().product();
let (out_sp, pad_before, padded_sp) = conv_geometry(sp, k_dims, strides, padding)?;
let out_plane: usize = out_sp.iter().product();
let in_plane: usize = sp.iter().product();
let padded_plane: usize = padded_sp.iter().product();
let padded_strides = row_major_strides(&padded_sp);
let input_std = input.as_standard_layout();
let in_flat = input_std
.as_slice()
.expect("standard-layout array is contiguous");
let padded_storage = if padded_sp.as_slice() != sp {
Some(build_padded(
in_flat,
batch * cin,
sp,
&padded_sp,
&pad_before,
))
} else {
None
};
let padded: &[f32] = padded_storage.as_deref().unwrap_or(in_flat);
let grad_std = grad_output.as_standard_layout();
let grad_flat = grad_std
.as_slice()
.expect("standard-layout array is contiguous");
let k_total = cin * k_plane;
let offsets = im2col_offsets(&out_sp, k_dims, strides, &padded_strides);
let w_mat = ArrayView2::from_shape((filters, k_total), weights)
.expect("weights length matches [F, Cin*k]");
let ctx = ColContext {
padded,
cin,
padded_plane,
k_plane,
out_plane,
offsets: &offsets,
};
let process_b = |b: usize, serial_gemm: bool| -> (Array2<f32>, Vec<f32>, Vec<f32>) {
let col = build_col_range(&ctx, b, 0, out_plane);
let col_mat = ArrayView2::from_shape((k_total, out_plane), &col)
.expect("col length matches [Cin*k, out_plane]");
let g_slice = &grad_flat[b * filters * out_plane..(b + 1) * filters * out_plane];
let g_mat = ArrayView2::from_shape((filters, out_plane), g_slice)
.expect("grad slice matches [F, out_plane]");
let (wg, dcol): (Array2<f32>, Array2<f32>) = if serial_gemm {
(
gemm_par_switch(&g_mat, &col_mat.t(), false),
gemm_par_switch(&w_mat.t(), &g_mat, false),
)
} else {
(
gemm_par_auto(&g_mat, &col_mat.t()),
gemm_par_auto(&w_mat.t(), &g_mat),
)
};
let bias_p: Vec<f32> = g_mat.outer_iter().map(|row| row.sum()).collect();
let dcol = dcol.as_slice().expect("matmul result is standard layout");
let mut pad_grad = vec![0.0f32; cin * padded_plane];
for c in 0..cin {
let pc = c * padded_plane;
for kk in 0..k_plane {
let krow = (c * k_plane + kk) * out_plane;
let off = kk * out_plane;
for o in 0..out_plane {
pad_grad[pc + offsets[off + o]] += dcol[krow + o];
}
}
}
let input_grad_b = if padded_sp.as_slice() != sp {
crop_padded(&pad_grad, cin, sp, &padded_sp, &pad_before)
} else {
pad_grad
};
(wg, bias_p, input_grad_b)
};
let gemm_flops = 4usize
.saturating_mul(batch)
.saturating_mul(filters)
.saturating_mul(out_plane)
.saturating_mul(k_total);
let parallel = gemm_flops >= conv_parallel_min_flops();
let serial_gemm = parallel && batch >= rayon::current_num_threads();
let per_b = map_indexed(batch, parallel, |b| process_b(b, serial_gemm));
let mut weight_grad_arr = Array2::<f32>::zeros((filters, k_total));
let mut bias_grad = vec![0.0f32; filters];
let mut in_grad_flat = Vec::with_capacity(batch * cin * in_plane);
for (wg, bias_p, ig_b) in per_b {
weight_grad_arr += &wg;
for (acc, v) in bias_grad.iter_mut().zip(bias_p) {
*acc += v;
}
in_grad_flat.extend(ig_b);
}
let weight_grad = weight_grad_arr.into_raw_vec_and_offset().0;
let mut ig_shape = Vec::with_capacity(2 + r);
ig_shape.push(batch);
ig_shape.push(cin);
ig_shape.extend_from_slice(sp);
let input_grad =
ArrayD::from_shape_vec(IxDyn(&ig_shape), in_grad_flat).expect("input grad matches shape");
Ok(ConvGradients {
weight_grad,
bias_grad,
input_grad,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_row_major_strides_3d() {
let got = row_major_strides(&[2, 3, 4]);
assert_eq!(got, vec![12, 4, 1]);
}
#[test]
fn test_row_major_strides_1d() {
let got = row_major_strides(&[5]);
assert_eq!(got, vec![1]);
}
#[test]
fn test_row_major_strides_empty() {
let got = row_major_strides(&[]);
assert_eq!(got, Vec::<usize>::new());
}
#[test]
fn test_increment_index_2d() {
let dims = [2usize, 3];
let mut idx = vec![0usize, 0];
assert!(increment_index(&mut idx, &dims));
assert_eq!(idx, vec![0, 1]);
assert!(increment_index(&mut idx, &dims));
assert_eq!(idx, vec![0, 2]);
assert!(increment_index(&mut idx, &dims));
assert_eq!(idx, vec![1, 0]);
assert!(increment_index(&mut idx, &dims));
assert_eq!(idx, vec![1, 1]);
assert!(increment_index(&mut idx, &dims));
assert_eq!(idx, vec![1, 2]);
assert!(!increment_index(&mut idx, &dims));
assert_eq!(idx, vec![0, 0]);
}
#[test]
fn test_increment_index_1d() {
let dims = [3usize];
let mut idx = vec![0usize];
assert!(increment_index(&mut idx, &dims));
assert_eq!(idx, vec![1]);
assert!(increment_index(&mut idx, &dims));
assert_eq!(idx, vec![2]);
assert!(!increment_index(&mut idx, &dims));
}
#[test]
fn test_conv_geometry_valid_1d() {
let (out_sp, pad_before, padded_sp) =
conv_geometry(&[5], &[3], &[1], PaddingType::Valid).unwrap();
assert_eq!(out_sp, vec![3]);
assert_eq!(pad_before, vec![0]);
assert_eq!(padded_sp, vec![5]);
}
#[test]
fn test_conv_geometry_valid_input_smaller_than_kernel_errors() {
let result = conv_geometry(&[2], &[3], &[1], PaddingType::Valid);
assert!(
matches!(result, Err(Error::InvalidInput(_))),
"expected InvalidInput, got {:?}",
result
);
}
#[test]
fn test_conv_geometry_same_1d() {
let (out_sp, pad_before, padded_sp) =
conv_geometry(&[7], &[3], &[2], PaddingType::Same).unwrap();
assert_eq!(out_sp, vec![4]);
assert_eq!(pad_before, vec![1]);
assert_eq!(padded_sp, vec![9]);
}
#[test]
fn test_conv_geometry_same_2d() {
let (out_sp, pad_before, padded_sp) =
conv_geometry(&[4, 4], &[3, 3], &[1, 1], PaddingType::Same).unwrap();
assert_eq!(out_sp, vec![4, 4]);
assert_eq!(pad_before, vec![1, 1]);
assert_eq!(padded_sp, vec![6, 6]);
}
#[test]
fn test_build_padded_2x2_into_4x4() {
let in_flat = [1.0f32, 2.0, 3.0, 4.0];
let got = build_padded(&in_flat, 1, &[2, 2], &[4, 4], &[1, 1]);
assert_eq!(got.len(), 16, "padded buffer should have 16 elements");
assert_eq!(got[5], 1.0, "padded[5] should be in[0,0]=1.0");
assert_eq!(got[6], 2.0, "padded[6] should be in[0,1]=2.0");
assert_eq!(got[9], 3.0, "padded[9] should be in[1,0]=3.0");
assert_eq!(got[10], 4.0, "padded[10] should be in[1,1]=4.0");
let non_zero_positions = [5usize, 6, 9, 10];
for (i, &val) in got.iter().enumerate() {
if !non_zero_positions.contains(&i) {
assert_eq!(val, 0.0, "padded[{i}] should be 0.0 (border), got {val}");
}
}
}
#[test]
fn test_build_padded_two_channels() {
let in_flat = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let got = build_padded(&in_flat, 2, &[2, 2], &[4, 4], &[1, 1]);
assert_eq!(got.len(), 32);
assert_eq!(got[5], 1.0);
assert_eq!(got[6], 2.0);
assert_eq!(got[9], 3.0);
assert_eq!(got[10], 4.0);
assert_eq!(got[16 + 5], 5.0);
assert_eq!(got[16 + 6], 6.0);
assert_eq!(got[16 + 9], 7.0);
assert_eq!(got[16 + 10], 8.0);
}
#[test]
fn test_build_padded_1d() {
let in_flat = [10.0f32, 20.0, 30.0];
let got = build_padded(&in_flat, 1, &[3], &[5], &[1]);
assert_eq!(got.len(), 5);
assert_eq!(got[0], 0.0, "leading pad must be 0");
assert_eq!(got[1], 10.0);
assert_eq!(got[2], 20.0);
assert_eq!(got[3], 30.0);
assert_eq!(got[4], 0.0, "trailing pad must be 0");
}
#[test]
fn test_build_padded_no_padding() {
let in_flat = [5.0f32, 6.0, 7.0, 8.0];
let got = build_padded(&in_flat, 1, &[2, 2], &[2, 2], &[0, 0]);
assert_eq!(got, vec![5.0f32, 6.0, 7.0, 8.0]);
}
#[test]
fn test_crop_padded_roundtrip() {
let in_flat = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let padded = build_padded(&in_flat, 2, &[2, 2], &[4, 4], &[1, 1]);
let got = crop_padded(&padded, 2, &[2, 2], &[4, 4], &[1, 1]);
assert_eq!(got, in_flat.to_vec());
}
#[test]
fn test_crop_padded_1d() {
let padded = [0.0f32, 10.0, 20.0, 30.0, 0.0];
let got = crop_padded(&padded, 1, &[3], &[5], &[1]);
assert_eq!(got, vec![10.0f32, 20.0, 30.0]);
}
#[test]
fn test_im2col_offsets_1d() {
let offsets = im2col_offsets(&[3], &[3], &[1], &[1]);
assert_eq!(
offsets,
vec![
0, 1, 2, 1, 2, 3, 2, 3, 4
]
);
}
}