use derive_new::new;
use onnx_ir_derive::NodeBuilder;
use crate::ir::Argument;
use crate::ir::{ArgType, Node, RawNode, TensorType};
use crate::processor::{
InputPreferences, InputSpec, NodeProcessor, NodeSpec, OutputPreferences, OutputSpec,
ProcessError,
};
#[derive(Debug, Clone, new)]
pub struct ConcatConfig {
pub axis: usize,
}
#[derive(Debug, Clone, NodeBuilder)]
pub struct ConcatNode {
pub name: String,
pub inputs: Vec<Argument>,
pub outputs: Vec<Argument>,
pub config: ConcatConfig,
}
pub(crate) struct ConcatProcessor;
impl NodeProcessor for ConcatProcessor {
type Config = ConcatConfig;
fn spec(&self) -> NodeSpec {
NodeSpec {
min_opset: 1,
max_opset: None,
inputs: InputSpec::AtLeast(1),
outputs: OutputSpec::Exact(1),
}
}
fn input_preferences(
&self,
node: &RawNode,
_opset: usize,
) -> Result<Option<InputPreferences>, ProcessError> {
use crate::processor::ArgPreference;
if node.inputs.is_empty() {
return Ok(None);
}
let mut prefs = InputPreferences::new();
let has_shape = node.inputs.iter().any(|input| input.ty.is_shape());
for input in &node.inputs {
if has_shape && matches!(&input.ty, ArgType::Tensor(t) if t.rank == 1) {
prefs = prefs.add(&input.name, ArgPreference::Shape);
}
if input.ty.is_scalar() {
prefs = prefs.add(&input.name, ArgPreference::ScalarNative);
}
}
Ok(Some(prefs))
}
fn infer_types(
&self,
node: &mut RawNode,
opset: usize,
_output_preferences: &OutputPreferences,
) -> Result<(), ProcessError> {
let _config = self
.extract_config(node, opset)
.expect("Config extraction failed");
let has_shape = node
.inputs
.iter()
.any(|i| matches!(i.ty, ArgType::Shape(_)));
let has_rank1_tensor = node
.inputs
.iter()
.any(|i| matches!(&i.ty, ArgType::Tensor(t) if t.rank == 1));
let has_scalar = node.inputs.iter().any(|i| i.ty.is_scalar());
if has_scalar {
let mut total_length = 0usize;
for (i, input) in node.inputs.iter().enumerate() {
match &input.ty {
ArgType::ScalarNative(_) | ArgType::ScalarTensor(_) => {
total_length += 1; }
ArgType::Tensor(t) if t.rank == 1 => {
let len = t
.static_shape_known()
.map(|s| s[0])
.or_else(|| input.value().as_ref().map(|v| v.shape[0]))
.unwrap_or(1);
total_length += len;
}
ArgType::Shape(rank) => {
total_length += rank;
}
_ => {
return Err(ProcessError::TypeMismatch {
expected: "Scalar, rank-1 Tensor, or Shape".to_string(),
actual: format!("{:?} at input {}", input.ty, i),
});
}
}
}
if has_shape {
node.outputs[0].ty = ArgType::Shape(total_length);
} else {
let first_dtype = node
.inputs
.iter()
.find_map(|input| match &input.ty {
ArgType::ScalarNative(dtype) | ArgType::ScalarTensor(dtype) => Some(*dtype),
ArgType::Tensor(t) => Some(t.dtype),
_ => None,
})
.unwrap_or(crate::ir::DType::I64);
for (i, input) in node.inputs.iter().enumerate() {
let dtype = match &input.ty {
ArgType::ScalarNative(d) | ArgType::ScalarTensor(d) => Some(*d),
ArgType::Tensor(t) => Some(t.dtype),
_ => None,
};
if let Some(d) = dtype
&& d != first_dtype
{
return Err(ProcessError::TypeMismatch {
expected: format!("{:?}", first_dtype),
actual: format!("{:?} at input {}", d, i),
});
}
}
node.outputs[0].ty = ArgType::Tensor(TensorType {
dtype: first_dtype,
rank: 1,
static_shape: Some(vec![Some(total_length)]),
});
}
return Ok(());
}
if !has_shape && !has_rank1_tensor {
let first_dtype = match &node.inputs[0].ty {
ArgType::Tensor(t) => t.dtype,
_ => {
return Err(ProcessError::TypeMismatch {
expected: "Tensor".to_string(),
actual: format!("{:?}", node.inputs[0].ty),
});
}
};
for (i, input) in node.inputs.iter().enumerate().skip(1) {
match &input.ty {
ArgType::Tensor(t) => {
if t.dtype != first_dtype {
return Err(ProcessError::TypeMismatch {
expected: format!("Tensor with dtype {:?}", first_dtype),
actual: format!("Tensor with dtype {:?} at input {}", t.dtype, i),
});
}
}
_ => {
return Err(ProcessError::TypeMismatch {
expected: "Tensor".to_string(),
actual: format!("{:?} at input {}", input.ty, i),
});
}
}
}
}
if has_shape && has_rank1_tensor {
let mut provisional_rank: usize = 0;
for input in &node.inputs {
match &input.ty {
ArgType::Shape(rank) => {
provisional_rank += rank;
}
ArgType::Tensor(t) if t.rank == 1 => {
let contribution = input
.value()
.as_ref()
.map(|v| v.shape[0])
.or_else(|| t.static_shape_known().map(|s| s[0]))
.unwrap_or(1);
provisional_rank += contribution;
}
_ => {
return Err(ProcessError::TypeMismatch {
expected: "Shape or rank-1 Tensor".to_string(),
actual: format!("{:?}", input.ty),
});
}
}
}
node.outputs[0].ty = ArgType::Shape(provisional_rank);
return Ok(());
}
let first_input_type = &node.inputs[0].ty;
match first_input_type {
ArgType::Tensor(tensor) => {
node.outputs[0].ty = ArgType::Tensor(TensorType {
dtype: tensor.dtype,
rank: tensor.rank,
static_shape: None,
});
}
ArgType::Shape(_) => {
let total_rank: usize = node
.inputs
.iter()
.map(|input| match &input.ty {
ArgType::Shape(rank) => Ok(*rank),
_ => Err(ProcessError::TypeMismatch {
expected: "Shape".to_string(),
actual: format!("{:?}", input.ty),
}),
})
.collect::<Result<Vec<_>, _>>()?
.iter()
.sum();
node.outputs[0].ty = ArgType::Shape(total_rank);
}
_ => {
return Err(ProcessError::TypeMismatch {
expected: "Tensor or Shape".to_string(),
actual: format!("{:?}", first_input_type),
});
}
}
Ok(())
}
fn is_noop(&self, node: &RawNode) -> bool {
node.inputs.len() == 1
}
fn extract_config(&self, node: &RawNode, _opset: usize) -> Result<Self::Config, ProcessError> {
let mut axis: Option<i64> = None;
for (key, value) in node.attrs.iter() {
if key.as_str() == "axis" {
axis = Some(value.clone().into_i64());
break;
}
}
let axis = axis.ok_or_else(|| ProcessError::MissingAttribute("axis".to_string()))?;
let rank = match &node.inputs.first().unwrap().ty {
ArgType::Tensor(tensor) => tensor.rank as i64,
ArgType::Shape(_) => 1, ArgType::ScalarTensor(_) | ArgType::ScalarNative(_) => 0, };
let normalized_axis = if axis < 0 { axis + rank } else { axis };
let config = ConcatConfig {
axis: normalized_axis as usize,
};
Ok(config)
}
fn build_node(&self, builder: RawNode, opset: usize) -> Node {
let config = self
.extract_config(&builder, opset)
.expect("Config extraction failed");
Node::Concat(ConcatNode {
name: builder.name,
inputs: builder.inputs,
outputs: builder.outputs,
config,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::NodeType;
use crate::node::test_utils::TestNodeBuilder;
fn create_test_node(axis: i64, input_rank: usize, num_inputs: usize) -> TestNodeBuilder {
TestNodeBuilder::new(NodeType::Concat, "test_concat")
.input_tensors_f32("data", num_inputs, input_rank, None)
.output_tensor_f32("output", input_rank, None)
.attr_int("axis", axis)
}
#[test]
fn test_concat_config_basic() {
let node = create_test_node(1, 3, 2).process(ConcatProcessor, 16);
let processor = ConcatProcessor;
let config = processor.extract_config(&node, 16).unwrap();
assert_eq!(config.axis, 1);
}
#[test]
fn test_concat_config_negative_axis() {
let node = create_test_node(-2, 3, 2).process(ConcatProcessor, 16);
let processor = ConcatProcessor;
let config = processor.extract_config(&node, 16).unwrap();
assert_eq!(config.axis, 1); }
#[test]
fn test_concat_config_shape_input() {
let node = TestNodeBuilder::new(NodeType::Concat, "test_concat_shape")
.input_shape("shape1", 2)
.input_shape("shape2", 3)
.output_shape("output", 5)
.attr_int("axis", 0) .process(ConcatProcessor, 16);
let processor = ConcatProcessor;
let config = processor.extract_config(&node, 16).unwrap();
assert_eq!(config.axis, 0); }
#[test]
fn test_concat_config_missing_axis() {
let node = TestNodeBuilder::new(NodeType::Concat, "test_concat")
.input_tensor_f32("data1", 3, None)
.input_tensor_f32("data2", 3, None)
.output_tensor_f32("output", 3, None)
.build();
let node = node;
let processor = ConcatProcessor;
let result = processor.extract_config(&node, 16);
assert!(matches!(result, Err(ProcessError::MissingAttribute(_))));
}
#[test]
fn test_concat_config_axis_out_of_bounds() {
let node = TestNodeBuilder::new(NodeType::Concat, "test_concat")
.input_tensor_f32("data1", 3, None)
.input_tensor_f32("data2", 3, None)
.output_tensor_f32("output", 3, None)
.attr_int("axis", 3)
.build();
let processor = ConcatProcessor;
let result = processor.extract_config(&node, 16);
assert!(result.is_ok()); }
#[test]
fn test_concat_update_outputs_shape() {
let node = TestNodeBuilder::new(NodeType::Concat, "test_concat_shape")
.input_shape("shape1", 2)
.input_shape("shape2", 3)
.input_shape("shape3", 1)
.output_shape("output", 0) .attr_int("axis", 0) .process(ConcatProcessor, 16);
match &node.outputs[0].ty {
ArgType::Shape(rank) => assert_eq!(*rank, 6), _ => panic!("Expected Shape output"),
}
}
#[test]
fn test_concat_config_shape_negative_axis() {
let node = TestNodeBuilder::new(NodeType::Concat, "test_concat_shape")
.input_shape("shape1", 2)
.input_shape("shape2", 3)
.output_shape("output", 5)
.attr_int("axis", -1) .process(ConcatProcessor, 16);
let processor = ConcatProcessor;
let config = processor.extract_config(&node, 16).unwrap();
assert_eq!(config.axis, 0); }
#[test]
fn test_concat_config_shape_invalid_axis() {
let node = TestNodeBuilder::new(NodeType::Concat, "test_concat_shape")
.input_shape("shape1", 2)
.input_shape("shape2", 3)
.output_shape("output", 5)
.attr_int("axis", 1)
.build();
let processor = ConcatProcessor;
let result = processor.extract_config(&node, 16);
assert!(result.is_ok()); }
#[test]
fn test_concat_mixed_inputs() {
let mut node = TestNodeBuilder::new(NodeType::Concat, "test_concat_mixed")
.input_shape("shape1", 2)
.input_tensor_f32("tensor1", 3, None)
.output_shape("output", 0)
.attr_int("axis", 0)
.build();
let processor = ConcatProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
let result = processor.infer_types(&mut node, 16, &prefs);
assert!(matches!(result, Err(ProcessError::TypeMismatch { .. })));
}
#[test]
fn test_concat_scalar_inputs() {
use burn_tensor::DType;
let node = TestNodeBuilder::new(NodeType::Concat, "test_concat_scalar")
.input_scalar_i64("scalar1")
.input_scalar_i64("scalar2")
.output_tensor_i64("output", 1, None)
.attr_int("axis", 0)
.process(ConcatProcessor, 16);
match &node.outputs[0].ty {
ArgType::Tensor(t) => {
assert_eq!(t.rank, 1);
assert_eq!(t.dtype, DType::I64);
assert_eq!(t.static_shape, Some(vec![Some(2)])); }
_ => panic!("Expected Tensor output, got {:?}", node.outputs[0].ty),
}
}
#[test]
fn test_concat_scalar_config_extraction() {
let node = TestNodeBuilder::new(NodeType::Concat, "test_concat_scalar")
.input_scalar_i64("scalar1")
.input_scalar_i64("scalar2")
.output_tensor_i64("output", 1, None)
.attr_int("axis", 0)
.build();
let processor = ConcatProcessor;
let config = processor.extract_config(&node, 16).unwrap();
assert_eq!(config.axis, 0); }
#[test]
fn test_concat_scalar_dtype_mismatch() {
use burn_tensor::DType;
let mut node = TestNodeBuilder::new(NodeType::Concat, "test_concat_dtype_mismatch")
.input_scalar_i64("s1")
.input_scalar("s2", DType::F32)
.output_tensor_i64("output", 1, None)
.attr_int("axis", 0)
.build();
let processor = ConcatProcessor;
let prefs = OutputPreferences::new();
let result = processor.infer_types(&mut node, 16, &prefs);
assert!(matches!(result, Err(ProcessError::TypeMismatch { .. })));
}
#[test]
fn test_concat_multiple_scalars() {
use burn_tensor::DType;
let node = TestNodeBuilder::new(NodeType::Concat, "test_concat_multi_scalar")
.input_scalar_i64("s1")
.input_scalar_i64("s2")
.input_scalar_i64("s3")
.input_scalar_i64("s4")
.output_tensor_i64("output", 1, None)
.attr_int("axis", 0)
.process(ConcatProcessor, 16);
match &node.outputs[0].ty {
ArgType::Tensor(t) => {
assert_eq!(t.rank, 1);
assert_eq!(t.dtype, DType::I64);
assert_eq!(t.static_shape, Some(vec![Some(4)])); }
_ => panic!("Expected Tensor output"),
}
}
#[test]
fn test_concat_mixed_shape_and_rank1_tensor_with_static_shape() {
let mut node = TestNodeBuilder::new(NodeType::Concat, "test_concat_mixed_static")
.input_shape("shape1", 2)
.input_tensor_i64("tensor1", 1, Some(vec![0])) .output_shape("output", 0) .attr_int("axis", 0)
.build();
let processor = ConcatProcessor;
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Shape(rank) => assert_eq!(*rank, 2),
_ => panic!("Expected Shape output, got {:?}", node.outputs[0].ty),
}
}
#[test]
fn test_concat_single_input_is_noop() {
let node = create_test_node(0, 3, 1).build();
assert!(ConcatProcessor.is_noop(&node));
}
#[test]
fn test_concat_multiple_inputs_is_not_noop() {
let node = create_test_node(0, 3, 2).build();
assert!(!ConcatProcessor.is_noop(&node));
}
}