onnx-runtime-ep-cpu 0.1.0-dev.3

CPU execution provider for the ORT 2.0 runtime
Documentation
//! `SpaceToDepth` rearranges spatial blocks into the channel dimension.

use onnx_runtime_ep_api::{EpError, Kernel, KernelFactory, Result, TensorMut, TensorView};
use onnx_runtime_ir::{Attribute, Node};

use super::{check_arity, elem_size, to_dense_bytes, write_dense_bytes};

pub struct SpaceToDepthKernel {
    blocksize: usize,
}

pub struct SpaceToDepthFactory;

impl KernelFactory for SpaceToDepthFactory {
    fn create(&self, node: &Node, _shapes: &[Vec<usize>]) -> Result<Box<dyn Kernel>> {
        let blocksize = node
            .attr("blocksize")
            .and_then(Attribute::as_int)
            .ok_or_else(|| EpError::KernelFailed("SpaceToDepth: blocksize is required".into()))?;
        if blocksize <= 0 {
            return Err(EpError::KernelFailed(
                "SpaceToDepth: blocksize must be positive".into(),
            ));
        }
        Ok(Box::new(SpaceToDepthKernel {
            blocksize: blocksize as usize,
        }))
    }
}

impl Kernel for SpaceToDepthKernel {
    fn execute(&self, inputs: &[TensorView], outputs: &mut [TensorMut]) -> Result<()> {
        check_arity("SpaceToDepth", inputs, outputs, 1, 1, 1)?;
        let input = &inputs[0];
        if input.shape.len() != 4 {
            return Err(EpError::KernelFailed(
                "SpaceToDepth: input must have rank 4".into(),
            ));
        }
        let [n, c, h, w] = <[usize; 4]>::try_from(input.shape).unwrap();
        let block = self.blocksize;
        if h % block != 0 || w % block != 0 {
            return Err(EpError::KernelFailed(format!(
                "SpaceToDepth: spatial dimensions {h}x{w} must be divisible by blocksize {block}"
            )));
        }
        let output_shape = [n, c * block * block, h / block, w / block];
        if outputs[0].dtype != input.dtype || outputs[0].shape != output_shape {
            return Err(EpError::KernelFailed(format!(
                "SpaceToDepth: output must have dtype {:?} and shape {output_shape:?}",
                input.dtype
            )));
        }

        let element_size = elem_size(input.dtype)?;
        let input_bytes = to_dense_bytes(input)?;
        let mut output_bytes = vec![0; input_bytes.len()];
        let output_h = h / block;
        let output_w = w / block;
        for batch in 0..n {
            for channel in 0..c {
                for block_h in 0..block {
                    for block_w in 0..block {
                        let output_channel = (block_h * block + block_w) * c + channel;
                        for output_y in 0..output_h {
                            for output_x in 0..output_w {
                                let input_y = output_y * block + block_h;
                                let input_x = output_x * block + block_w;
                                let input_index =
                                    ((batch * c + channel) * h + input_y) * w + input_x;
                                let output_index = ((batch * output_shape[1] + output_channel)
                                    * output_h
                                    + output_y)
                                    * output_w
                                    + output_x;
                                output_bytes[output_index * element_size
                                    ..(output_index + 1) * element_size]
                                    .copy_from_slice(
                                        &input_bytes[input_index * element_size
                                            ..(input_index + 1) * element_size],
                                    );
                            }
                        }
                    }
                }
            }
        }
        write_dense_bytes(&mut outputs[0], &output_bytes)
    }

    fn supports_strided_input(&self, _input_idx: usize) -> bool {
        true
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::kernels::testutil::Owned;

    #[test]
    fn rearranges_each_spatial_offset_into_channels() {
        let input = Owned::f32(
            &[1, 1, 4, 4],
            &(1..=16).map(|value| value as f32).collect::<Vec<_>>(),
        );
        let mut output = Owned::zeros_f32(&[1, 4, 2, 2]);
        SpaceToDepthKernel { blocksize: 2 }
            .execute(&[input.view()], &mut [output.view_mut()])
            .unwrap();
        assert_eq!(
            output.to_f32(),
            vec![
                1., 3., 9., 11., 2., 4., 10., 12., 5., 7., 13., 15., 6., 8., 14., 16.
            ]
        );
    }

    #[test]
    fn interleaves_channels_within_each_spatial_offset() {
        let input = Owned::i64(&[1, 2, 2, 2], &[1, 2, 3, 4, 11, 12, 13, 14]);
        let mut output = Owned::zeros(onnx_runtime_ir::DataType::Int64, &[1, 8, 1, 1]);
        SpaceToDepthKernel { blocksize: 2 }
            .execute(&[input.view()], &mut [output.view_mut()])
            .unwrap();
        assert_eq!(output.to_i64(), vec![1, 11, 2, 12, 3, 13, 4, 14]);
    }
}