use ndarray::{Array4, Axis};
use rand::{thread_rng, Rng};
use std::fmt;
use ocl_convolution::{Convolution, FeatureMap, FeatureMapShape, I8Params, Params};
#[derive(Debug)]
enum Error {
Shape(ndarray::ShapeError),
Ocl(ocl::Error),
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Shape(e) => fmt::Display::fmt(e, formatter),
Self::Ocl(e) => fmt::Display::fmt(e, formatter),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Shape(e) => Some(e),
Self::Ocl(_) => None,
}
}
}
impl From<ndarray::ShapeError> for Error {
fn from(error: ndarray::ShapeError) -> Self {
Self::Shape(error)
}
}
impl From<ocl::Error> for Error {
fn from(error: ocl::Error) -> Self {
Self::Ocl(error)
}
}
#[allow(clippy::cast_precision_loss)] fn create_signal(width: usize, height: usize) -> Array4<f32> {
Array4::from_shape_vec(
[1, width, height, 1],
(0..(width * height)).map(|i| i as f32).collect(),
)
.unwrap()
}
#[test]
fn basics() -> Result<(), Error> {
let convolution = Convolution::f32(3)?.build(Params::default())?;
let signal = create_signal(5, 5);
let filter = Array4::from_shape_vec([1, 3, 3, 1], vec![1.0; 9])?;
let c = convolution.compute(FeatureMap::nhwc(&signal), &filter)?;
assert_eq!(
c,
Array4::from_shape_vec(
[1, 3, 3, 1],
vec![54., 63., 72., 99., 108., 117., 144., 153., 162.],
)?,
);
let signal = Array4::from_shape_vec(
[1, 1, 5, 5],
vec![
0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14., 15., 16., 17., 18.,
19., 20., 21., 22., 23., 24.,
],
)?;
let c = convolution.compute(FeatureMap::nchw(&signal), &filter)?;
assert_eq!(
c,
Array4::from_shape_vec(
[1, 1, 3, 3],
vec![54., 63., 72., 99., 108., 117., 144., 153., 162.],
)?,
);
Ok(())
}
#[test]
#[allow(clippy::cast_precision_loss)]
fn f32_convolution_with_filters() -> Result<(), Error> {
let filters = Array4::from_elem([1, 3, 3, 1], 1.0);
let convolution = Convolution::f32(3)?
.build(Params::default())?
.with_filters(filters.view())?;
let signal = create_signal(5, 5);
let c = convolution.compute(FeatureMap::nhwc(&signal))?;
assert_eq!(
c,
Array4::from_shape_vec(
[1, 3, 3, 1],
vec![54., 63., 72., 99., 108., 117., 144., 153., 162.],
)?,
);
for i in 1..=5 {
let signal = Array4::from_elem([1, 5 + i, 5 + i, 1], i as f32);
assert!(convolution.compute(FeatureMap::nhwc(&signal)).is_ok());
}
let pinned = convolution.pin(FeatureMapShape {
batch_size: 1,
width: 5,
height: 5,
channels: 1,
})?;
let c = pinned.compute(FeatureMap::nhwc(&signal))?;
assert_eq!(
c,
Array4::from_shape_vec(
[1, 3, 3, 1],
vec![54., 63., 72., 99., 108., 117., 144., 153., 162.],
)?,
);
for i in 1..=5 {
let signal = Array4::from_elem([1, 5, 5, 1], i as f32);
assert!(pinned.compute(FeatureMap::nhwc(&signal)).is_ok());
}
Ok(())
}
#[test]
fn f32_convolution_with_filters_and_biases() -> Result<(), Error> {
let filters = Array4::from_elem([1, 3, 3, 1], 1.0);
let convolution = Convolution::f32(3)?
.build(Params::default())?
.with_biased_filters(filters.view(), &[-100.0])?;
let signal = create_signal(5, 5);
let c = convolution.compute(FeatureMap::nhwc(&signal))?;
assert_eq!(
c,
Array4::from_shape_vec(
[1, 3, 3, 1],
vec![-46., -37., -28., -1., 8., 17., 44., 53., 62.],
)?,
);
Ok(())
}
#[test]
fn grouped_convolution() -> Result<(), Error> {
let convolution = Convolution::f32(3)?.build(Params {
strides: [1, 1],
pads: [0; 4],
dilation: [1, 1],
groups: 2,
})?;
let signal = Array4::from_shape_vec(
[1, 3, 3, 4],
vec![
1., 2., 3., 4., 1., 2., 3., 4., 1., 2., 3., 4., 1., 2., 3., 4., 1., 2., 3., 4., 1., 2.,
3., 4., 1., 2., 3., 4., 1., 2., 3., 4., 1., 2., 3., 4.,
],
)?;
let filters = Array4::from_shape_vec(
[2, 3, 3, 2],
vec![
1., -1., 1., -1., 1., -1., 1., -1., 1., -1., 1., -1., 1., -1., 1., -1., 1., -1.,
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
],
)?;
let expected_output = Array4::from_shape_vec(
[1, 1, 1, 2],
vec![
-9.0, 63.0, ],
)?;
let output = convolution.compute(FeatureMap::nhwc(&signal), &filters)?;
assert_eq!(output, expected_output);
Ok(())
}
#[test]
fn grouped_i8_convolution() -> Result<(), Error> {
let convolution = Convolution::i8(3)?.build(I8Params {
common: Params {
strides: [1, 1],
pads: [0; 4],
dilation: [1, 1],
groups: 4,
},
bit_shift: 12,
scale: I8Params::convert_scale(12, 1.0),
output_bias: 0,
signal_bias: 0,
filter_bias: 0,
})?;
let signal = Array4::from_shape_vec(
[1, 3, 3, 4],
vec![
1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1,
2, 3, 4, 1, 2, 3, 4,
],
)?;
let filters = Array4::from_shape_vec(
[4, 3, 3, 1],
vec![
1, -1, 1, -1, 1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, 1, -1, 1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ],
)?;
let expected_output = Array4::from_shape_vec(
[1, 1, 1, 4],
vec![
1, 18, 3, 36, ],
)?;
let output = convolution.compute(FeatureMap::nhwc(&signal), &filters)?;
assert_eq!(output, expected_output);
Ok(())
}
#[test]
fn with_padding() -> Result<(), Error> {
let convolution = Convolution::f32(3)?.build(Params {
strides: [1, 1],
pads: [1; 4],
dilation: [1, 1],
groups: 1,
})?;
let signal = create_signal(5, 5);
let filter = Array4::from_shape_vec([1, 3, 3, 1], vec![1.0; 9])?;
let c = convolution.compute(FeatureMap::nhwc(&signal), &filter)?;
assert_eq!(
c,
Array4::from_shape_vec(
[1, 5, 5, 1],
vec![
12., 21., 27., 33., 24., 33., 54., 63., 72., 51., 63., 99., 108., 117., 81., 93.,
144., 153., 162., 111., 72., 111., 117., 123., 84.,
],
)?,
);
Ok(())
}
#[test]
fn with_strides() -> Result<(), Error> {
let convolution = Convolution::f32(3)?.build(Params {
strides: [2, 2],
pads: [0; 4],
dilation: [1, 1],
groups: 1,
})?;
let signal = create_signal(7, 5);
let filter = Array4::from_shape_vec([1, 3, 3, 1], vec![1.; 9])?;
let expected_output =
Array4::from_shape_vec([1, 3, 2, 1], vec![54., 72., 144., 162., 234., 252.])?;
assert_eq!(
convolution.compute(FeatureMap::nhwc(&signal), &filter)?,
expected_output
);
Ok(())
}
#[test]
fn with_strides_and_padding() -> Result<(), Error> {
let convolution = Convolution::f32(3)?.build(Params {
strides: [2, 2],
pads: [1; 4],
dilation: [1, 1],
groups: 1,
})?;
let signal = create_signal(7, 5);
let filter = Array4::from_shape_vec([1, 3, 3, 1], vec![1.; 9])?;
let expected_output = Array4::from_shape_vec(
[1, 4, 3, 1],
vec![
12., 27., 24., 63., 108., 81., 123., 198., 141., 112., 177., 124.,
],
)?;
assert_eq!(
convolution.compute(FeatureMap::nhwc(&signal), &filter)?,
expected_output
);
Ok(())
}
#[test]
#[allow(clippy::cast_precision_loss)]
fn with_several_input_channels() -> Result<(), Error> {
let convolution = Convolution::f32(3)?.build(Params {
strides: [1, 1],
pads: [1; 4],
dilation: [1, 1],
groups: 1,
})?;
let mut signal = vec![0.0; 100];
for (i, val) in signal.iter_mut().enumerate() {
*val = (i / 4) as f32; }
let signal = Array4::from_shape_vec([1, 5, 5, 4], signal)?;
let filter = Array4::from_shape_vec([1, 3, 3, 4], vec![1.; 36])?;
let output = convolution.compute(FeatureMap::nhwc(&signal), &filter)?;
assert!((output[[0, 0, 0, 0]] - 48.0).abs() < f32::EPSILON);
Ok(())
}
#[test]
fn with_dilation() -> Result<(), Error> {
let mut convolution = Convolution::f32(3)?.build(Params {
strides: [1, 1],
pads: [0; 4],
groups: 1,
dilation: [2, 2],
})?;
let signal = Array4::from_shape_vec(
[1, 5, 5, 1],
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, ],
)?;
let filters = Array4::from_elem([1, 3, 3, 1], 1.0);
let expected_output = Array4::from_elem([1, 1, 1, 1], 117.0);
assert_eq!(
convolution.compute(FeatureMap::nhwc(&signal), &filters)?,
expected_output
);
convolution.set_params(Params {
strides: [1, 1],
pads: [1; 4],
groups: 1,
dilation: [2, 2],
})?;
let expected_output = Array4::from_shape_vec(
[1, 3, 3, 1],
vec![
52.0, 78.0, 52.0, 78.0, 117.0, 78.0, 52.0, 78.0, 52.0, ],
)?;
assert_eq!(
convolution.compute(FeatureMap::nhwc(&signal), &filters)?,
expected_output
);
Ok(())
}
#[test]
fn rounding_in_i8_convolution() -> Result<(), Error> {
const BIT_SHIFT: u8 = 8;
let params = I8Params {
common: Params::default(),
bit_shift: BIT_SHIFT,
scale: I8Params::convert_scale(BIT_SHIFT, 0.5),
output_bias: 0,
signal_bias: 0,
filter_bias: 0,
};
let convolution = Convolution::i8(1)?.build(params)?;
let signal = Array4::from_shape_vec([1, 2, 3, 1], vec![-7, -6, -5, 5, 6, 7])?;
let filter = Array4::from_shape_vec([1, 1, 1, 1], vec![1])?;
let output = convolution.compute(FeatureMap::nhwc(&signal), &filter)?;
let expected_output = Array4::from_shape_vec([1, 2, 3, 1], vec![-4, -3, -2, 2, 3, 4])?;
assert_eq!(output, expected_output);
Ok(())
}
#[test]
fn i8_convolution() -> Result<(), Error> {
const BIT_SHIFT: u8 = 8;
let params = I8Params {
common: Params::default(),
bit_shift: BIT_SHIFT,
scale: I8Params::convert_scale(BIT_SHIFT, 1.0),
output_bias: 0,
signal_bias: 0,
filter_bias: 0,
};
let mut convolution = Convolution::i8(3)?.build(params)?;
let signal = vec![
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, -5, -6, -7, -8, -9, 0, -1, -2, -3, -4, ];
let signal = Array4::from_shape_vec([1, 5, 5, 1], signal)?;
let filter = Array4::from_shape_vec([1, 3, 3, 1], vec![1; 9])?;
let expected_output = vec![
54, 63, 72, 33, 36, 39, 12, 9, 6, ];
let expected_output = Array4::from_shape_vec([1, 3, 3, 1], expected_output)?;
let output = convolution.compute(FeatureMap::nhwc(&signal), &filter)?;
assert_eq!(output, expected_output);
let expected_output = vec![
6, 9, 12, -1, 0, 1, -8, -9, -10, ];
let expected_output = Array4::from_shape_vec([1, 3, 3, 1], expected_output)?;
convolution.set_params(I8Params {
common: Params::default(),
bit_shift: BIT_SHIFT,
scale: I8Params::convert_scale(BIT_SHIFT, 1.0 / 3.0),
output_bias: -12,
signal_bias: 0,
filter_bias: 0,
})?;
let output = convolution.compute(FeatureMap::nhwc(&signal), &filter)?;
assert_eq!(output, expected_output);
let signal = vec![
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, -5, -6, -7, -8, -9, 0, -1, -2, -3, -4, ];
let signal = Array4::from_shape_vec([1, 5, 5, 1], signal)? - 7;
let filter = Array4::from_shape_vec([1, 3, 3, 1], vec![0; 9])?;
convolution.set_params(I8Params {
common: Params::default(),
output_bias: -12,
filter_bias: 1,
signal_bias: 7,
bit_shift: BIT_SHIFT,
scale: I8Params::convert_scale(BIT_SHIFT, 1.0 / 3.0),
})?;
let output = convolution.compute(FeatureMap::nhwc(&signal), &filter)?;
assert_eq!(output, expected_output);
Ok(())
}
#[test]
fn i8_convolution_with_filter_bias() -> Result<(), Error> {
const BIT_SHIFT: u8 = 8;
const MULTIPLIER: i32 = 1 << (BIT_SHIFT as i32);
let params = I8Params {
common: Params::default(),
bit_shift: BIT_SHIFT,
scale: I8Params::convert_scale(BIT_SHIFT, 1.0 / 3.0),
output_bias: 0,
signal_bias: 0,
filter_bias: 0,
};
let convolution = Convolution::i8(3)?.build(params)?;
let signal = vec![
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, -5, -6, -7, -8, -9, 0, -1, -2, -3, -4, ];
let signal = Array4::from_shape_vec([1, 5, 5, 1], signal)?;
let signal = FeatureMap::nhwc(&signal);
let filter = Array4::from_shape_vec([2, 3, 3, 1], vec![1; 18])?;
let expected_output = vec![
6, 9, 12, -1, 0, 1, -8, -9, -10, 17, 20, 23, 10, 11, 12, 3, 2, 1, ];
let expected_output =
Array4::from_shape_vec([1, 2, 3, 3], expected_output)?.permuted_axes([0, 2, 3, 1]);
let biases = &[-12 * MULTIPLIER, -MULTIPLIER];
let output = convolution
.compute_with_biases(signal, &filter, biases)
.unwrap();
assert_eq!(output, expected_output);
let convolution = convolution.with_biased_filters(&filter, biases)?;
let output = convolution.compute(signal)?;
assert_eq!(output, expected_output);
let convolution = convolution.pin(FeatureMapShape {
batch_size: 1,
width: 5,
height: 5,
channels: 1,
})?;
let output = convolution.compute(signal)?;
assert_eq!(output, expected_output);
Ok(())
}
#[test]
#[allow(clippy::deref_addrof)] fn f32_batching() -> Result<(), Error> {
use ndarray::{concatenate, s};
let mut rng = thread_rng();
let conv = Convolution::f32(3)?.build(Params::default())?;
let filters = Array4::from_shape_fn([2, 3, 3, 4], |_| rng.gen_range(-1.0..=1.0));
let conv = conv.with_filters(&filters)?;
for batch_size in 2..8 {
let signal_shape = if batch_size % 2 == 0 {
[batch_size, 5, 5, 4]
} else {
[batch_size, 4, 5, 5]
};
let to_map = if batch_size % 2 == 0 {
FeatureMap::nhwc
} else {
FeatureMap::nchw
};
let signal = Array4::from_shape_fn(signal_shape, |_| rng.gen_range(-1.0..=1.0));
let batched_output = conv.compute(to_map(signal.view()))?;
let sample_outputs: Vec<_> = (0..batch_size)
.map(|i| {
let sample_signal = signal.slice(s![i..=i, .., .., ..]);
conv.compute(to_map(sample_signal))
})
.collect::<Result<_, _>>()?;
let sample_outputs: Vec<_> = sample_outputs.iter().map(Array4::view).collect();
let stitched_output = concatenate(Axis(0), &sample_outputs)?;
let max_diff = (batched_output.clone() - stitched_output.clone())
.mapv(f32::abs)
.fold(0.0, |acc, &x| if x > acc { x } else { acc });
assert!(
max_diff < f32::EPSILON,
"batched={}, stitched={}",
batched_output,
stitched_output
);
}
Ok(())
}