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: 1,
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 (dtype, rank, input_static_shape) = match &node.inputs.first().unwrap().ty {
ArgType::Tensor(tensor) => (tensor.dtype, tensor.rank, tensor.static_shape.clone()),
ArgType::Shape(r) => (crate::ir::DType::I64, 1, Some(vec![Some(*r)])),
ArgType::ScalarTensor(dtype) => (*dtype, 1, Some(vec![Some(1)])),
_ => {
return Err(ProcessError::TypeMismatch {
expected: "Tensor, Shape, or ScalarTensor".to_string(),
actual: format!("{:?}", node.inputs.first().unwrap().ty),
});
}
};
let split_sizes: Option<Vec<usize>> = if node.inputs.len() > 1 {
node.inputs[1]
.value()
.and_then(|v| v.to_vec::<i64>().ok())
.map(|sizes| sizes.into_iter().map(|s| s as usize).collect())
} else {
node.attrs.get("split").map(|v| {
v.clone()
.into_i64s()
.into_iter()
.map(|s| s as usize)
.collect()
})
};
for (i, output_arg) in node.outputs.iter_mut().enumerate() {
let static_shape = if let Some(ref sizes) = split_sizes {
if let Some(ref input_shape) = input_static_shape {
let axis = node
.attrs
.get("axis")
.map(|v| {
let a = v.clone().into_i64();
if a < 0 {
(a + rank as i64) as usize
} else {
a as usize
}
})
.unwrap_or(0);
if i < sizes.len() {
let mut shape = input_shape.clone();
shape[axis] = Some(sizes[i]);
Some(shape)
} else {
None
}
} else {
None
}
} else {
None
};
output_arg.ty = ArgType::Tensor(TensorType {
dtype,
rank,
static_shape,
});
}
Ok(())
}
fn is_noop(&self, node: &RawNode) -> bool {
node.outputs.len() == 1
}
fn extract_config(&self, node: &RawNode, _opset: usize) -> Result<Self::Config, ProcessError> {
let mut axis: i64 = 0;
let split_size: Option<usize> = None;
let mut split_sizes: Option<SplitSizesInput> = None;
let tensor = match &node.inputs.first().unwrap().ty {
ArgType::Tensor(tensor) => tensor.clone(),
ArgType::Shape(rank) => TensorType {
dtype: crate::ir::DType::I64,
rank: 1,
static_shape: Some(vec![Some(*rank)]),
},
ArgType::ScalarTensor(dtype) => TensorType {
dtype: *dtype,
rank: 1,
static_shape: Some(vec![Some(1)]),
},
_ => {
return Err(ProcessError::TypeMismatch {
expected: "Tensor, Shape, or ScalarTensor".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 Some(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 chunk = dim_size.div_ceil(num_outputs);
let mut remaining = dim_size;
let sizes: Vec<usize> = (0..num_outputs)
.map(|_| {
let s = chunk.min(remaining);
remaining = remaining.saturating_sub(chunk);
s
})
.collect();
split_sizes = Some(SplitSizesInput::Static(sizes));
}
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 non-negative, 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 Some(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
}
}
};
} else if let Some(split_attr) = node.attrs.get("split") {
let sizes = split_attr.clone().into_i64s();
if !sizes.is_empty() {
if sizes.iter().any(|&s| s < 0) {
return Err(ProcessError::Custom(
"Split: split sizes must be non-negative".to_string(),
));
}
let usizes: Vec<usize> = sizes.into_iter().map(|s| s 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 Some(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
)));
}
}
split_sizes = Some(SplitSizesInput::Static(usizes));
}
}
if split_sizes.is_none()
&& split_size.is_none()
&& let Some(static_shape) = &tensor.static_shape
&& let Some(dim_size) = static_shape[axis as usize]
{
let n = node.outputs.len();
let chunk = dim_size.div_ceil(n);
let mut remaining = dim_size;
let sizes: Vec<usize> = (0..n)
.map(|_| {
let s = chunk.min(remaining);
remaining = remaining.saturating_sub(chunk);
s
})
.collect();
split_sizes = Some(SplitSizesInput::Static(sizes));
}
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::ScalarNative(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!(config.split_size.is_none());
assert!(
matches!(&config.split_sizes, Some(SplitSizesInput::Static(sizes)) if sizes == &vec![5, 5])
);
}
#[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!(config.split_size.is_none());
assert!(
matches!(&config.split_sizes, Some(SplitSizesInput::Static(sizes)) if sizes == &vec![10, 10])
);
}
#[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!(config.split_size.is_none());
assert!(
matches!(&config.split_sizes, Some(SplitSizesInput::Static(sizes)) if sizes == &vec![10, 10, 10])
);
}
#[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!(config.split_size.is_none());
assert!(
matches!(&config.split_sizes, Some(SplitSizesInput::Static(sizes)) if sizes == &vec![3, 3, 3, 3])
);
}
#[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::ScalarNative(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!(config.split_size.is_none());
assert!(
matches!(&config.split_sizes, Some(SplitSizesInput::Static(sizes)) if sizes == &vec![4, 4, 3])
);
}
#[test]
fn test_split_shape_input() {
let node = TestNodeBuilder::new(NodeType::Split, "test_split")
.input_shape("shape_input", 3)
.input_tensor_i64_data("split_sizes", vec![1, 1, 1], vec![3])
.output_tensor_i64("output_0", 0, None)
.output_tensor_i64("output_1", 0, None)
.output_tensor_i64("output_2", 0, None)
.build_with_graph_data(16);
let mut node = node;
let processor = SplitProcessor;
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
for output in &node.outputs {
match &output.ty {
ArgType::Tensor(t) => {
assert_eq!(t.dtype, DType::I64);
assert_eq!(t.rank, 1);
assert_eq!(t.static_shape, Some(vec![Some(1)]));
}
other => panic!("Expected Tensor, got {:?}", other),
}
}
}
#[test]
fn test_split_zero_sized_parts() {
let node = TestNodeBuilder::new(NodeType::Split, "test_split")
.input_shape("shape_input", 1)
.input_tensor_i64_data("split_sizes", vec![0, 1, 0], vec![3])
.output_tensor_i64("output_0", 0, None)
.output_tensor_i64("output_1", 0, None)
.output_tensor_i64("output_2", 0, None)
.build_with_graph_data(16);
let mut node = node;
let processor = SplitProcessor;
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
let shapes: Vec<_> = node
.outputs
.iter()
.map(|o| match &o.ty {
ArgType::Tensor(t) => t.static_shape.clone(),
_ => panic!("Expected Tensor"),
})
.collect();
assert_eq!(
shapes,
vec![
Some(vec![Some(0)]),
Some(vec![Some(1)]),
Some(vec![Some(0)])
]
);
let config = processor.extract_config(&node, 16).unwrap();
assert!(matches!(
&config.split_sizes,
Some(SplitSizesInput::Static(sizes)) if sizes == &vec![0, 1, 0]
));
}
#[test]
fn test_split_single_output_is_noop() {
let node = create_test_node(3, 1, Some(vec![2, 3, 4]), None, None).build();
assert!(SplitProcessor.is_noop(&node));
}
#[test]
fn test_split_multiple_outputs_is_not_noop() {
let node = create_test_node(3, 2, Some(vec![2, 3, 4]), None, None).build();
assert!(!SplitProcessor.is_noop(&node));
}
#[test]
fn test_split_scalar_tensor_input() {
let mut node = TestNodeBuilder::new(NodeType::Split, "test_split")
.add_input("data", ArgType::ScalarTensor(DType::F32))
.output_default("output_0")
.build();
let processor = SplitProcessor;
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(t) => {
assert_eq!(t.dtype, DType::F32);
assert_eq!(t.rank, 1);
}
other => panic!("Expected Tensor, got {:?}", other),
}
}
}