use derive_new::new;
use onnx_ir_derive::NodeBuilder;
use crate::ir::{ArgType, Argument, Node, RawNode, RuntimeInputRef, TensorType};
use crate::processor::{
InputSpec, NodeProcessor, NodeSpec, OutputPreferences, OutputSpec, ProcessError,
};
#[derive(Debug, Clone)]
pub enum SplitSizesInput {
Static(Vec<usize>),
Runtime(RuntimeInputRef),
}
impl Default for SplitSizesInput {
fn default() -> Self {
SplitSizesInput::Static(vec![])
}
}
#[derive(Clone, Debug, new)]
pub struct SplitConfig {
pub axis: usize,
pub split_size: Option<usize>,
pub split_sizes: Option<SplitSizesInput>,
pub num_outputs: Option<usize>,
}
#[derive(Debug, Clone, NodeBuilder)]
pub struct SplitNode {
pub name: String,
pub inputs: Vec<Argument>,
pub outputs: Vec<Argument>,
pub config: SplitConfig,
}
pub(crate) struct SplitProcessor;
impl NodeProcessor for SplitProcessor {
type Config = SplitConfig;
fn spec(&self) -> NodeSpec {
NodeSpec {
min_opset: 11,
max_opset: None,
inputs: InputSpec::AtLeast(1),
outputs: OutputSpec::Range(1, 2147483647),
}
}
fn lift_constants(&self, node: &mut RawNode, _opset: usize) -> Result<(), ProcessError> {
if node.inputs.len() > 1 && node.inputs[1].is_constant() {
node.inputs[1].to_static()?;
}
Ok(())
}
fn infer_types(
&self,
node: &mut RawNode,
_opset: usize,
_output_preferences: &OutputPreferences,
) -> Result<(), ProcessError> {
let tensor = match &node.inputs.first().unwrap().ty {
ArgType::Tensor(tensor) => tensor,
_ => {
return Err(ProcessError::TypeMismatch {
expected: "Tensor".to_string(),
actual: format!("{:?}", node.inputs.first().unwrap().ty),
});
}
};
for output_arg in node.outputs.iter_mut() {
output_arg.ty = ArgType::Tensor(TensorType {
dtype: tensor.dtype,
rank: tensor.rank,
static_shape: None,
});
}
Ok(())
}
fn extract_config(&self, node: &RawNode, _opset: usize) -> Result<Self::Config, ProcessError> {
let mut axis: i64 = 0;
let mut split_size: Option<usize> = None;
let mut split_sizes: Option<SplitSizesInput> = None;
let tensor = match &node.inputs.first().unwrap().ty {
ArgType::Tensor(tensor) => tensor,
_ => {
return Err(ProcessError::TypeMismatch {
expected: "Tensor".to_string(),
actual: format!("{:?}", node.inputs.first().unwrap().ty),
});
}
};
let mut num_outputs: Option<usize> = None;
for (key, value) in node.attrs.iter() {
match key.as_str() {
"axis" => axis = value.clone().into_i64(),
"num_outputs" => num_outputs = Some(value.clone().into_i64() as usize),
_ => {}
}
}
let rank = tensor.rank as i64;
if axis < -rank || axis >= rank {
return Err(ProcessError::InvalidAttribute {
name: "axis".to_string(),
reason: format!(
"Split: axis {} is out of range for tensor of rank {} (valid range: [{}, {}])",
axis,
rank,
-rank,
rank - 1
),
});
}
if axis < 0 {
axis += rank;
}
if let Some(num) = num_outputs
&& num == 0
{
return Err(ProcessError::InvalidAttribute {
name: "num_outputs".to_string(),
reason: "Split: num_outputs must be greater than 0".to_string(),
});
}
if let Some(num_outputs) = num_outputs
&& let Some(static_shape) = &tensor.static_shape
{
let dim_size = static_shape[axis as usize];
if dim_size == 0 {
return Err(ProcessError::Custom(format!(
"Split: cannot split dimension of size 0 into {} outputs",
num_outputs
)));
}
let calculated_split_size =
dim_size / (num_outputs - (dim_size % num_outputs != 0) as usize);
split_size = Some(calculated_split_size);
}
if node.inputs.len() > 1 {
match &node.inputs[1].ty {
ArgType::Tensor(t) => {
if t.rank != 1 {
return Err(ProcessError::Custom(format!(
"Split: split sizes tensor must be 1D, got rank {}",
t.rank
)));
}
if t.dtype != crate::ir::DType::I64 {
return Err(ProcessError::TypeMismatch {
expected: "Split sizes tensor with dtype I64".to_string(),
actual: format!("Split sizes tensor with dtype {:?}", t.dtype),
});
}
}
_ => {
return Err(ProcessError::TypeMismatch {
expected: "Tensor for split sizes input".to_string(),
actual: format!("{:?}", node.inputs[1].ty),
});
}
}
split_sizes = match node.inputs[1].value() {
None => {
Some(SplitSizesInput::Runtime(RuntimeInputRef::new(
node.inputs[1].name.clone(),
1,
)))
}
Some(tensor_data) => {
let sizes: Vec<i64> = tensor_data.to_vec().unwrap();
for (i, &size) in sizes.iter().enumerate() {
if size <= 0 {
return Err(ProcessError::Custom(format!(
"Split: split size at index {} must be positive, got {}",
i, size
)));
}
}
let usizes: Vec<usize> = sizes.into_iter().map(|x| x as usize).collect();
if usizes.len() != node.outputs.len() {
return Err(ProcessError::Custom(format!(
"Split: number of split sizes ({}) must match number of outputs ({})",
usizes.len(),
node.outputs.len()
)));
}
if let Some(static_shape) = &tensor.static_shape {
let dim_size = static_shape[axis as usize];
let total_size: usize = usizes.iter().sum();
if total_size != dim_size {
return Err(ProcessError::Custom(format!(
"Split: sum of split sizes ({}) must equal dimension size ({}) along axis {}",
total_size, dim_size, axis
)));
}
}
if !usizes.is_empty() {
Some(SplitSizesInput::Static(usizes))
} else {
None
}
}
};
}
if split_sizes.is_none()
&& split_size.is_none()
&& let Some(static_shape) = &tensor.static_shape
{
let inferred_num_outputs = node.outputs.len();
let dim_size = static_shape[axis as usize];
let calculated_split_size =
dim_size / (inferred_num_outputs - (dim_size % inferred_num_outputs != 0) as usize);
split_size = Some(calculated_split_size);
}
let runtime_num_outputs = if split_size.is_none() && split_sizes.is_none() {
Some(num_outputs.unwrap_or(node.outputs.len()))
} else {
None
};
let config = SplitConfig {
axis: axis as usize,
split_size,
split_sizes,
num_outputs: runtime_num_outputs,
};
Ok(config)
}
fn build_node(&self, builder: RawNode, opset: usize) -> Node {
let config = self
.extract_config(&builder, opset)
.expect("Config extraction failed");
Node::Split(SplitNode {
name: builder.name,
inputs: builder.inputs,
outputs: builder.outputs,
config,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::{ArgType, AttributeValue, DType, NodeType};
use crate::node::test_utils::TestNodeBuilder;
use std::collections::HashMap;
fn create_test_node(
input_rank: usize,
num_outputs: usize,
static_shape: Option<Vec<usize>>,
attrs: Option<HashMap<String, AttributeValue>>,
split_sizes_input: Option<Vec<i64>>,
) -> TestNodeBuilder {
let mut builder = TestNodeBuilder::new(NodeType::Split, "test_split").input_tensor_f32(
"input",
input_rank,
static_shape,
);
if let Some(sizes) = split_sizes_input {
builder = builder.input_tensor_i64_data("split", sizes.clone(), vec![sizes.len()]);
}
for i in 0..num_outputs {
builder = builder.output_tensor_f32(
&format!("output_{i}"),
0, None,
);
}
if let Some(attributes) = attrs {
for (key, value) in attributes {
builder = match key.as_str() {
"axis" => builder.attr_int("axis", value.into_i64()),
"num_outputs" => builder.attr_int("num_outputs", value.into_i64()),
_ => builder,
};
}
}
builder
}
#[test]
fn test_split_single_output() {
let mut node = create_test_node(3, 1, Some(vec![10, 20, 30]), None, None).build();
let processor = SplitProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(node.outputs.len(), 1);
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(tensor.dtype, DType::F32);
assert_eq!(tensor.rank, 3);
}
_ => panic!("Expected tensor output"),
}
}
#[test]
fn test_split_multiple_outputs() {
let mut node = create_test_node(4, 3, Some(vec![12, 15, 18, 21]), None, None).build();
let processor = SplitProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(node.outputs.len(), 3);
for output in &node.outputs {
match &output.ty {
ArgType::Tensor(tensor) => {
assert_eq!(tensor.dtype, DType::F32);
assert_eq!(tensor.rank, 4);
}
_ => panic!("Expected tensor output"),
}
}
}
#[test]
fn test_split_invalid_input() {
let mut node = create_test_node(3, 2, Some(vec![10, 20, 30]), None, None).build();
node.inputs[0].ty = ArgType::Scalar(DType::F32);
let processor = SplitProcessor;
let _prefs = OutputPreferences::new();
let result = processor.extract_config(&node, 16);
assert!(matches!(result, Err(ProcessError::TypeMismatch { .. })));
}
#[test]
fn test_split_config_default_axis() {
let static_shape = Some(vec![10, 20, 30]);
let node = create_test_node(3, 2, static_shape, None, None).build();
let mut node = node;
let processor = SplitProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(config.axis, 0);
assert_eq!(config.split_size, Some(5)); assert!(config.split_sizes.is_none());
}
#[test]
fn test_split_config_specified_axis() {
let static_shape = Some(vec![10, 20, 30]);
let mut attrs = HashMap::new();
attrs.insert("axis".to_string(), AttributeValue::Int64(1));
let node = create_test_node(3, 2, static_shape, Some(attrs), None).build();
let mut node = node;
let processor = SplitProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(config.axis, 1);
assert_eq!(config.split_size, Some(10)); assert!(config.split_sizes.is_none());
}
#[test]
fn test_split_config_negative_axis() {
let static_shape = Some(vec![10, 20, 30]);
let mut attrs = HashMap::new();
attrs.insert("axis".to_string(), AttributeValue::Int64(-1));
let node = create_test_node(3, 3, static_shape, Some(attrs), None).build();
let mut node = node;
let processor = SplitProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(config.axis, 2); assert_eq!(config.split_size, Some(10)); assert!(config.split_sizes.is_none());
}
#[test]
fn test_split_config_num_outputs_attr() {
let static_shape = Some(vec![12, 24, 36]);
let mut attrs = HashMap::new();
attrs.insert("num_outputs".to_string(), AttributeValue::Int64(4));
let node = create_test_node(3, 4, static_shape, Some(attrs), None).build();
let mut node = node;
let processor = SplitProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(config.axis, 0);
assert_eq!(config.split_size, Some(3)); assert!(config.split_sizes.is_none());
}
#[test]
fn test_split_config_with_split_sizes_input() {
let static_shape = Some(vec![10, 20, 30]);
let split_sizes = vec![3, 7];
let node = create_test_node(3, 2, static_shape, None, Some(split_sizes.clone()))
.build_with_graph_data(16);
let mut node = node;
let processor = SplitProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(config.axis, 0);
assert_eq!(config.split_size, None);
assert!(
matches!(&config.split_sizes, Some(SplitSizesInput::Static(sizes)) if sizes == &vec![3, 7])
);
}
#[test]
fn test_split_config_both_splits_and_num_outputs() {
let static_shape = Some(vec![10, 20, 30]);
let mut attrs = HashMap::new();
attrs.insert("num_outputs".to_string(), AttributeValue::Int64(2));
let split_sizes = vec![3, 7];
let node = create_test_node(3, 2, static_shape, Some(attrs), Some(split_sizes))
.build_with_graph_data(16);
let mut node = node;
let processor = SplitProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
}
#[test]
fn test_split_config_zero_num_outputs() {
let static_shape = Some(vec![10, 20, 30]);
let mut attrs = HashMap::new();
attrs.insert("num_outputs".to_string(), AttributeValue::Int64(0));
let node = create_test_node(3, 0, static_shape, Some(attrs), None).build();
assert_eq!(node.outputs.len(), 0);
}
#[test]
fn test_split_config_invalid_num_outputs() {
let static_shape = Some(vec![5, 10, 15]);
let mut attrs = HashMap::new();
attrs.insert("num_outputs".to_string(), AttributeValue::Int64(10));
let node = create_test_node(3, 10, static_shape, Some(attrs), None).build();
let mut node = node;
let processor = SplitProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
}
#[test]
fn test_split_config_no_static_shape() {
let mut attrs = HashMap::new();
attrs.insert("num_outputs".to_string(), AttributeValue::Int64(2));
let node = create_test_node(3, 2, None, Some(attrs), None).build();
let node = node;
let processor = SplitProcessor;
let config = processor.extract_config(&node, 16).unwrap();
assert_eq!(config.axis, 0);
assert!(config.split_size.is_none());
assert!(config.split_sizes.is_none());
assert_eq!(config.num_outputs, Some(2));
}
#[test]
fn test_split_config_invalid_input_type() {
let mut node = create_test_node(3, 2, Some(vec![10, 20, 30]), None, None).build();
node.inputs[0].ty = ArgType::Scalar(DType::F32);
let node = node;
let processor = SplitProcessor;
let result = processor.extract_config(&node, 16);
assert!(matches!(result, Err(ProcessError::TypeMismatch { .. })));
}
#[test]
fn test_split_config_with_runtime_split_sizes() {
let static_shape = Some(vec![20, 30, 40]);
let node = TestNodeBuilder::new(NodeType::Split, "test_split")
.input_tensor_f32("input", 3, static_shape)
.input_tensor_i64("split", 1, Some(vec![2])) .output_tensor_f32("output_0", 0, None)
.output_tensor_f32("output_1", 0, None)
.build();
let mut node = node;
let processor = SplitProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(config.axis, 0);
assert_eq!(config.split_size, None);
assert!(
matches!(&config.split_sizes, Some(SplitSizesInput::Runtime(arg)) if arg.name == "split")
);
}
#[test]
fn test_split_config_non_even_split() {
let static_shape = Some(vec![11, 22, 33]); let mut attrs = HashMap::new();
attrs.insert("axis".to_string(), AttributeValue::Int64(0));
let node = create_test_node(3, 3, static_shape, Some(attrs), None).build();
let mut node = node;
let processor = SplitProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(config.split_size, Some(5));
}
}