use onnx_ir_derive::NodeBuilder;
use crate::ir::{
ArgType, Argument, DType, Node, RawNode, RuntimeInputRef, TensorDataExt, TensorType,
};
use crate::processor::{
InputSpec, NodeProcessor, NodeSpec, OutputPreferences, OutputSpec, ProcessError,
};
#[derive(Debug, Clone, NodeBuilder)]
pub struct ExpandNode {
pub name: String,
pub inputs: Vec<Argument>,
pub outputs: Vec<Argument>,
pub config: ExpandConfig,
}
#[derive(Debug, Clone)]
pub enum ExpandConfig {
Static(Vec<i64>),
Runtime(RuntimeInputRef),
}
pub(crate) struct ExpandProcessor;
impl NodeProcessor for ExpandProcessor {
type Config = ExpandConfig;
fn spec(&self) -> NodeSpec {
NodeSpec {
min_opset: 8,
max_opset: None,
inputs: InputSpec::Exact(2),
outputs: OutputSpec::Exact(1),
}
}
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> {
match &node.inputs[1].ty {
ArgType::Tensor(tensor) => {
if tensor.rank != 1 {
return Err(ProcessError::Custom(
"Expand: shape tensor must be 1D".to_string(),
));
}
if !matches!(tensor.dtype, DType::I64) {
return Err(ProcessError::Custom(
"Expand: shape tensor must have element type int64".to_string(),
));
}
}
ArgType::Shape(_) => {
}
ArgType::ScalarTensor(_) | ArgType::ScalarNative(_) => {
}
}
let config = self
.extract_config(node, opset)
.expect("Config extraction failed");
let input_elem_type = match &node.inputs[0].ty {
ArgType::Tensor(tensor) => tensor.dtype,
ArgType::ScalarTensor(dtype) | ArgType::ScalarNative(dtype) => *dtype,
ArgType::Shape(_) => DType::I64,
};
match config {
ExpandConfig::Static(shape) => {
if shape.is_empty() {
node.outputs[0].ty = ArgType::ScalarTensor(input_elem_type);
} else {
node.outputs[0].ty = ArgType::Tensor(TensorType {
dtype: input_elem_type,
rank: shape.len(),
static_shape: Some(shape.iter().map(|&dim| Some(dim as usize)).collect()),
});
}
}
ExpandConfig::Runtime(_) => {
let output_rank = match &node.inputs[1].ty {
ArgType::ScalarTensor(_) | ArgType::ScalarNative(_) => 0,
ArgType::Shape(rank) => *rank,
ArgType::Tensor(tensor) => {
if let Some(static_shape) = &tensor.static_shape
&& let Some(Some(rank)) = static_shape.first()
{
*rank
} else {
match &node.outputs[0].ty {
ArgType::Tensor(TensorType { rank, .. }) if *rank > 0 => *rank,
_ => {
match &node.inputs[0].ty {
ArgType::Tensor(t) => t.rank,
ArgType::Shape(_) => 1,
other => {
return Err(ProcessError::Custom(format!(
"Cannot determine output rank for Expand node {} \
with fully dynamic shape tensor and input type {:?}",
node.name, other
)));
}
}
}
}
}
}
};
if output_rank == 0 {
node.outputs[0].ty = ArgType::ScalarTensor(input_elem_type);
} else {
node.outputs[0].ty = ArgType::Tensor(TensorType {
dtype: input_elem_type,
rank: output_rank,
static_shape: None,
});
}
}
}
Ok(())
}
fn is_noop(&self, node: &RawNode) -> bool {
if let (ArgType::Tensor(in_t), ArgType::Tensor(out_t)) =
(&node.inputs[0].ty, &node.outputs[0].ty)
&& let (Some(in_shape), Some(out_shape)) = (&in_t.static_shape, &out_t.static_shape)
{
return in_shape == out_shape;
}
false
}
fn extract_config(&self, node: &RawNode, _opset: usize) -> Result<Self::Config, ProcessError> {
let config = match node.inputs[1].value() {
Some(tensor_data) => match tensor_data.to_i64_vec() {
Ok(shape) => ExpandConfig::Static(shape),
Err(_) => {
return Err(ProcessError::Custom(
"Expand: shape data type must be int32 or int64".to_string(),
));
}
},
None => {
ExpandConfig::Runtime(RuntimeInputRef::new(node.inputs[1].name.clone(), 1))
}
};
Ok(config)
}
fn build_node(&self, builder: RawNode, opset: usize) -> Node {
let config = self
.extract_config(&builder, opset)
.expect("Config extraction failed");
Node::Expand(ExpandNode {
name: builder.name,
inputs: builder.inputs,
outputs: builder.outputs,
config,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::{BoolStore, DType, NodeType};
use crate::node::test_utils::TestNodeBuilder;
fn create_test_node(
input_rank: usize,
shape_value: Option<Vec<i64>>,
shape_type: Option<ArgType>,
) -> TestNodeBuilder {
let mut builder = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_tensor_f32("input", input_rank, None)
.output_tensor_f32("output", 0, None);
if let Some(shape) = shape_value {
builder = builder.input_tensor_i64_data("shape", shape.clone(), vec![shape.len()]);
} else if let Some(st) = shape_type {
builder = builder.add_input("shape", st);
} else {
builder = builder.input_tensor_i64("shape", 1, Some(vec![3]));
}
builder
}
#[test]
fn test_expand_with_constant_shape() {
let mut node = create_test_node(2, Some(vec![2, 3, 4]), None).build_with_graph_data(16);
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(tensor.dtype, DType::F32);
assert_eq!(tensor.rank, 3);
assert_eq!(tensor.static_shape, Some(vec![Some(2), Some(3), Some(4)]));
}
_ => panic!("Expected tensor output"),
}
}
#[test]
fn test_expand_with_dynamic_shape() {
let mut node = create_test_node(2, None, None).build();
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(tensor.dtype, DType::F32);
assert_eq!(tensor.rank, 3);
assert_eq!(tensor.static_shape, None);
}
_ => panic!("Expected tensor output"),
}
}
#[test]
fn test_expand_with_incorrect_inputs() {
let mut node = create_test_node(2, Some(vec![2, 3, 4]), None).build_with_graph_data(16);
node.inputs.pop();
let processor = ExpandProcessor;
let spec = processor.spec();
let result = crate::processor::validate_node_spec(&node, 16, &spec);
assert!(matches!(
result,
Err(ProcessError::InvalidInputCount {
expected: 2,
actual: 1
})
));
}
#[test]
fn test_expand_config_with_static_shape() {
let node = create_test_node(2, Some(vec![2, 3, 4]), None).build_with_graph_data(16);
let mut node = node;
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match config {
ExpandConfig::Static(shape) => {
assert_eq!(*shape, vec![2, 3, 4]);
}
ExpandConfig::Runtime(_) => panic!("Expected Static config, got Runtime"),
}
}
#[test]
fn test_expand_config_with_runtime_shape() {
let node = create_test_node(2, None, None).build();
let mut node = node;
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match config {
ExpandConfig::Static(_) => panic!("Expected Runtime config, got Static"),
ExpandConfig::Runtime(name) => {
assert_eq!(name.name, "shape");
}
}
}
#[test]
fn test_expand_config_with_shape_type() {
let shape_type = ArgType::Shape(3);
let node = create_test_node(2, None, Some(shape_type)).build();
let mut node = node;
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match config {
ExpandConfig::Static(_) => panic!("Expected Runtime config, got Static"),
ExpandConfig::Runtime(name) => {
assert_eq!(name.name, "shape");
}
}
}
#[test]
fn test_expand_config_with_invalid_shape_rank() {
let invalid_shape_type = ArgType::Tensor(TensorType {
dtype: DType::I64,
rank: 2, static_shape: None,
});
let node = create_test_node(2, None, Some(invalid_shape_type)).build();
let mut node = node;
let processor = ExpandProcessor;
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::Custom(_))));
}
#[test]
fn test_expand_config_with_invalid_shape_type() {
let invalid_shape_type = ArgType::Tensor(TensorType {
dtype: DType::F32, rank: 1,
static_shape: None,
});
let node = create_test_node(2, None, Some(invalid_shape_type)).build();
let mut node = node;
let processor = ExpandProcessor;
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::Custom(_))));
}
#[test]
fn test_expand_scalar_native_shape_outputs_scalar() {
let shape_type = ArgType::ScalarNative(DType::I64);
let node = create_test_node(2, None, Some(shape_type)).build();
let mut node = node;
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(node.outputs[0].ty, ArgType::ScalarTensor(DType::F32));
}
#[test]
fn test_expand_config_with_invalid_value_type() {
let node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_tensor_f32("input", 2, None)
.input_tensor_f32_data("shape", vec![2.0, 3.0, 4.0], vec![3]) .output_tensor_f32("output", 0, None)
.build_with_graph_data(16);
let node = node;
let processor = ExpandProcessor;
let result = processor.extract_config(&node, 16);
match result {
Err(ProcessError::Custom(_)) => {}
_ => panic!("Expected ProcessError::Custom for invalid shape data type"),
}
}
#[test]
fn test_expand_update_outputs_with_shape_input() {
let mut node = create_test_node(2, None, Some(ArgType::Shape(4))).build();
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(tensor.dtype, DType::F32);
assert_eq!(tensor.rank, 4); assert_eq!(tensor.static_shape, None); }
_ => panic!("Expected tensor output"),
}
}
#[test]
fn test_expand_update_outputs_with_shape_input_static_value() {
let mut node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_tensor_f32("input", 2, None)
.input_tensor_i64_data("shape", vec![5, 10, 15], vec![3]) .output_tensor_f32("output", 0, None)
.build_with_graph_data(16);
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(tensor.dtype, DType::F32);
assert_eq!(tensor.rank, 3);
assert_eq!(tensor.static_shape, Some(vec![Some(5), Some(10), Some(15)]));
}
_ => panic!("Expected tensor output"),
}
}
#[test]
fn test_expand_preserves_input_element_type() {
{
let mut node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_tensor_f32("input", 2, None)
.input_tensor_i64_data("shape", vec![2, 3, 4], vec![3])
.output_tensor_f32("output", 0, None)
.build_with_graph_data(16);
node.outputs[0].ty = ArgType::Tensor(TensorType {
dtype: DType::I64, rank: 0,
static_shape: None,
});
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(
tensor.dtype,
DType::F32,
"Expand should preserve Float32 input type"
);
assert_eq!(tensor.rank, 3);
}
_ => panic!("Expected tensor output"),
}
}
{
let mut node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_tensor_i64("input", 2, None)
.input_tensor_i64_data("shape", vec![2, 3, 4], vec![3])
.output_tensor_i64("output", 0, None)
.build_with_graph_data(16);
node.outputs[0].ty = ArgType::Tensor(TensorType {
dtype: DType::F32, rank: 0,
static_shape: None,
});
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(
tensor.dtype,
DType::I64,
"Expand should preserve Int64 input type"
);
assert_eq!(tensor.rank, 3);
}
_ => panic!("Expected tensor output"),
}
}
{
let mut node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_tensor_bool("input", 2, None)
.input_tensor_i64_data("shape", vec![2, 3, 4], vec![3])
.output_tensor_bool("output", 0, None)
.build_with_graph_data(16);
node.outputs[0].ty = ArgType::Tensor(TensorType {
dtype: DType::F32, rank: 0,
static_shape: None,
});
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(
tensor.dtype,
DType::Bool(BoolStore::Native),
"Expand should preserve Bool input type"
);
assert_eq!(tensor.rank, 3);
}
_ => panic!("Expected tensor output"),
}
}
}
#[test]
fn test_expand_with_mismatched_output_type() {
let mut node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_tensor_i64("input", 2, None) .input_tensor_i64_data("shape", vec![2, 3], vec![2])
.output_tensor_f32("output", 0, None) .build_with_graph_data(16);
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let _config = processor.extract_config(&node, 16).unwrap();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(
tensor.dtype,
DType::I64,
"Expand should use input type (Int64) not initial output type (Float32)"
);
assert_eq!(tensor.rank, 2);
assert_eq!(tensor.static_shape, Some(vec![Some(2), Some(3)]));
}
_ => panic!("Expected tensor output"),
}
}
#[test]
fn test_expand_shape_input_static_target() {
let mut node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.add_input("input", ArgType::Shape(2))
.input_tensor_i64_data("shape", vec![2, 3], vec![2])
.output_tensor_i64("output", 0, None)
.build_with_graph_data(16);
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(tensor.dtype, DType::I64);
assert_eq!(tensor.rank, 2);
assert_eq!(tensor.static_shape, Some(vec![Some(2), Some(3)]));
}
_ => panic!("Expected tensor output"),
}
}
#[test]
fn test_expand_scalar_input_static_shape() {
let mut node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_scalar_f32("input")
.input_tensor_i64_data("shape", vec![2, 3], vec![2])
.output_tensor_f32("output", 0, None)
.build_with_graph_data(16);
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(tensor.dtype, DType::F32);
assert_eq!(tensor.rank, 2);
assert_eq!(tensor.static_shape, Some(vec![Some(2), Some(3)]));
}
_ => panic!("Expected tensor output"),
}
}
#[test]
fn test_expand_scalar_input_runtime_shape() {
let mut node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_scalar_i64("input")
.input_tensor_i64("shape", 1, Some(vec![4]))
.output_tensor_i64("output", 0, None)
.build();
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(tensor.dtype, DType::I64);
assert_eq!(tensor.rank, 4);
assert_eq!(tensor.static_shape, None);
}
_ => panic!("Expected tensor output"),
}
}
#[test]
fn test_expand_same_static_shape_is_noop() {
let node = TestNodeBuilder::new(NodeType::Expand, "test")
.input_tensor_f32("input", 3, Some(vec![2, 3, 4]))
.input_tensor_i64("shape", 1, Some(vec![3]))
.output_tensor_f32("output", 3, Some(vec![2, 3, 4]))
.build();
assert!(ExpandProcessor.is_noop(&node));
}
#[test]
fn test_expand_different_static_shape_is_not_noop() {
let node = TestNodeBuilder::new(NodeType::Expand, "test")
.input_tensor_f32("input", 3, Some(vec![1, 3, 4]))
.input_tensor_i64("shape", 1, Some(vec![3]))
.output_tensor_f32("output", 3, Some(vec![2, 3, 4]))
.build();
assert!(!ExpandProcessor.is_noop(&node));
}
#[test]
fn test_expand_no_static_shape_is_not_noop() {
let node = TestNodeBuilder::new(NodeType::Expand, "test")
.input_tensor_f32("input", 3, None)
.input_tensor_i64("shape", 1, Some(vec![3]))
.output_tensor_f32("output", 3, None)
.build();
assert!(!ExpandProcessor.is_noop(&node));
}
#[test]
fn test_expand_with_fully_dynamic_shape_tensor() {
let shape_type = ArgType::Tensor(TensorType {
dtype: DType::I64,
rank: 1,
static_shape: None,
});
let mut node = create_test_node(2, None, Some(shape_type)).build();
node.outputs[0].ty = ArgType::Tensor(TensorType {
dtype: DType::F32,
rank: 0,
static_shape: None,
});
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
match &node.outputs[0].ty {
ArgType::Tensor(tensor) => {
assert_eq!(tensor.dtype, DType::F32);
assert_eq!(tensor.rank, 2);
assert_eq!(tensor.static_shape, None);
}
_ => panic!("Expected tensor output"),
}
}
#[test]
fn test_expand_scalar_input_fully_dynamic_shape_errors() {
let shape_type = ArgType::Tensor(TensorType {
dtype: DType::I64,
rank: 1,
static_shape: None,
});
let mut node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_scalar_f32("input")
.add_input("shape", shape_type)
.output_tensor_f32("output", 0, None)
.build();
node.outputs[0].ty = ArgType::Tensor(TensorType {
dtype: DType::F32,
rank: 0,
static_shape: None,
});
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
let result = processor.infer_types(&mut node, 16, &prefs);
assert!(matches!(result, Err(ProcessError::Custom(_))));
}
#[test]
fn test_expand_static_empty_shape_outputs_scalar() {
let mut node = TestNodeBuilder::new(NodeType::Expand, "test_expand")
.input_scalar_f32("input")
.input_tensor_i64_data("shape", vec![], vec![0])
.output_tensor_f32("output", 0, None)
.build_with_graph_data(16);
let processor = ExpandProcessor;
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert_eq!(node.outputs[0].ty, ArgType::ScalarTensor(DType::F32));
}
}