use derive_new::new;
use onnx_ir_derive::NodeBuilder;
use crate::ir::{ArgType, Argument, Node, RawNode, TensorType};
use crate::processor::{
InputPreferences, InputSpec, NodeProcessor, NodeSpec, OutputPreferences, OutputSpec,
ProcessError,
};
#[derive(Debug, Clone, new)]
pub struct GatherConfig {
pub axis: usize,
}
#[derive(Debug, Clone, NodeBuilder)]
pub struct GatherNode {
pub name: String,
pub inputs: Vec<Argument>,
pub outputs: Vec<Argument>,
pub config: GatherConfig,
}
pub(crate) struct GatherProcessor;
impl NodeProcessor for GatherProcessor {
type Config = GatherConfig;
fn spec(&self) -> NodeSpec {
NodeSpec {
min_opset: 1,
max_opset: None,
inputs: InputSpec::Exact(2),
outputs: OutputSpec::Exact(1),
}
}
fn is_noop(&self, node: &RawNode) -> bool {
matches!(node.inputs[0].ty, ArgType::ScalarNative(_))
}
fn input_preferences(
&self,
node: &RawNode,
_opset: usize,
) -> Result<Option<InputPreferences>, ProcessError> {
use crate::processor::ArgPreference;
if node.inputs.len() < 2 {
return Ok(None);
}
if node.inputs[0].ty.is_shape() {
let pref = if node.inputs[1].ty.is_scalar() {
ArgPreference::ScalarNative
} else {
ArgPreference::Shape
};
Ok(Some(
InputPreferences::new().add(&node.inputs[1].name, pref),
))
} else {
Ok(None)
}
}
fn infer_types(
&self,
node: &mut RawNode,
_opset: usize,
_output_preferences: &OutputPreferences,
) -> Result<(), ProcessError> {
let input_dim = match &node.inputs[0].ty {
ArgType::Tensor(tensor) => tensor.rank as i64,
ArgType::Shape(shape_rank) => *shape_rank as i64,
ArgType::ScalarTensor(_) | ArgType::ScalarNative(_) => 1,
};
let mut axis: i64 = 0;
for (key, value) in node.attrs.iter() {
match key.as_str() {
"axis" => axis = value.clone().into_i64(),
_ => {
return Err(ProcessError::InvalidAttribute {
name: key.clone(),
reason: format!("Unexpected attribute for Gather: {}", key),
});
}
}
}
if axis < 0 {
axis += input_dim;
}
if axis < 0 || axis >= input_dim {
return Err(ProcessError::InvalidAttribute {
name: "axis".to_string(),
reason: format!("axis {} is out of bounds for rank {}", axis, input_dim),
});
}
let indices_rank = match &node.inputs[1].ty {
ArgType::Tensor(tensor) => tensor.rank,
ArgType::ScalarTensor(_) | ArgType::ScalarNative(_) => 0,
ArgType::Shape(_shape_rank) => {
1 }
};
match &node.inputs[0].ty {
ArgType::Tensor(input_tensor) => {
let output_rank = indices_rank + input_tensor.rank - 1;
if output_rank == 0 {
node.outputs[0].ty = ArgType::ScalarTensor(input_tensor.dtype);
} else {
node.outputs[0].ty = ArgType::Tensor(TensorType {
dtype: input_tensor.dtype,
rank: output_rank,
static_shape: None,
});
}
}
ArgType::Shape(_shape_rank) => {
if indices_rank == 0 {
node.outputs[0].ty = ArgType::ScalarNative(crate::ir::DType::I64);
} else {
let output_shape_rank = match &node.inputs[1].ty {
ArgType::Shape(shape_rank) => *shape_rank,
ArgType::Tensor(t) => {
t.static_shape_known()
.and_then(|s| s.first().copied())
.unwrap_or(indices_rank)
}
_ => indices_rank,
};
node.outputs[0].ty = ArgType::Shape(output_shape_rank);
}
}
ArgType::ScalarTensor(dtype) => {
node.outputs[0].ty = ArgType::ScalarTensor(*dtype);
}
ArgType::ScalarNative(dtype) => {
node.outputs[0].ty = ArgType::ScalarNative(*dtype);
}
}
let is_scalar_index = match &node.inputs[1].ty {
ArgType::ScalarTensor(_) | ArgType::ScalarNative(_) => true,
ArgType::Tensor(t) => t.rank == 0,
_ => false,
};
if node.inputs.len() > 1 && node.inputs[1].is_constant() && is_scalar_index {
node.inputs[1].to_static()?;
}
Ok(())
}
fn extract_config(&self, node: &RawNode, _opset: usize) -> Result<Self::Config, ProcessError> {
let input_dim = match &node.inputs[0].ty {
ArgType::Tensor(tensor) => tensor.rank as i64,
ArgType::Shape(shape_rank) => *shape_rank as i64,
ArgType::ScalarTensor(_) | ArgType::ScalarNative(_) => 1,
};
let mut axis: i64 = 0;
for (key, value) in node.attrs.iter() {
if key.as_str() == "axis" {
axis = value.clone().into_i64()
}
}
if axis < 0 {
axis += input_dim;
}
let config = GatherConfig {
axis: 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::Gather(GatherNode {
name: builder.name,
inputs: builder.inputs,
outputs: builder.outputs,
config,
})
}
}
#[cfg(test)]
mod tests {
use burn_tensor::DType;
use super::*;
use crate::ir::NodeType;
use crate::node::test_utils::TestNodeBuilder;
fn create_test_node(axis: i64, input_rank: usize, is_shape: bool) -> TestNodeBuilder {
let mut builder =
TestNodeBuilder::new(NodeType::Gather, "test_gather").attr_int("axis", axis);
if is_shape {
builder = builder.add_input("data", ArgType::Shape(input_rank));
} else {
builder = builder.input_tensor_f32("data", input_rank, None);
}
builder
.input_tensor_i64("indices", 1, None)
.output_tensor_f32("output", input_rank, None)
}
#[test]
fn test_gather_config_basic() {
let node = create_test_node(0, 3, false).build();
let mut node = node;
let processor = GatherProcessor;
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);
}
#[test]
fn test_gather_config_negative_axis() {
let node = create_test_node(-2, 3, false).build();
let mut node = node;
let processor = GatherProcessor;
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); }
#[test]
fn test_gather_config_shape_input() {
let node = create_test_node(0, 4, true).build(); let mut node = node;
let processor = GatherProcessor;
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);
}
#[test]
fn test_gather_config_missing_index() {
let mut node = create_test_node(0, 3, false).build();
node.inputs.pop(); let processor = GatherProcessor;
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_gather_update_outputs_scalar_result() {
let mut node = TestNodeBuilder::new(NodeType::Gather, "test_scalar_gather")
.attr_int("axis", 0)
.input_tensor_f32("data", 1, None)
.add_input("indices", ArgType::ScalarNative(crate::ir::DType::I64))
.output_tensor_f32("output", 1, None)
.build();
let processor = GatherProcessor;
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::ScalarTensor(elem_type) => {
assert_eq!(*elem_type, crate::ir::DType::F32);
}
other => panic!("Expected ScalarTensor output, got {:?}", other),
}
}
#[test]
fn test_gather_update_outputs_tensor_result() {
let mut node = TestNodeBuilder::new(NodeType::Gather, "test_tensor_gather")
.attr_int("axis", 0)
.input_tensor_f32("data", 2, None)
.input_tensor_i64("indices", 1, None)
.output_tensor_f32("output", 2, None)
.build();
let processor = GatherProcessor;
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.rank, 2);
assert_eq!(tensor.dtype, crate::ir::DType::F32);
}
other => panic!("Expected tensor output, got {:?}", other),
}
}
#[test]
fn test_gather_update_outputs_shape_indices() {
let mut node = TestNodeBuilder::new(NodeType::Gather, "test_gather_shape_indices")
.attr_int("axis", 0)
.input_shape("data", 3) .add_input("indices", ArgType::Shape(1)) .output_shape("output", 1) .build();
let processor = GatherProcessor;
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::Shape(rank) => {
assert_eq!(*rank, 1);
}
other => panic!("Expected Shape output, got {:?}", other),
}
}
#[test]
fn test_gather_update_outputs_shape_scalar_indices() {
let mut node = TestNodeBuilder::new(NodeType::Gather, "test_gather_shape_scalar")
.attr_int("axis", 0)
.input_shape("data", 2) .add_input("indices", ArgType::ScalarNative(crate::ir::DType::I64)) .output_tensor_i64("output", 0, None) .build();
let processor = GatherProcessor;
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::ScalarNative(elem_type) => {
assert_eq!(*elem_type, crate::ir::DType::I64);
}
other => panic!("Expected scalar output, got {:?}", other),
}
}
#[test]
fn test_gather_update_outputs_shape_with_shape_indices_rank_2() {
let mut node = TestNodeBuilder::new(NodeType::Gather, "test_gather_shape_shape_2")
.attr_int("axis", 0)
.input_shape("data", 4) .add_input("indices", ArgType::Shape(2)) .output_shape("output", 1) .build();
let processor = GatherProcessor;
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::Shape(rank) => {
assert_eq!(*rank, 2, "Expected Shape(2) output for Shape(2) indices");
}
other => panic!("Expected Shape(2) output, got {:?}", other),
}
}
#[test]
fn test_gather_update_outputs_shape_with_shape_indices_rank_3() {
let mut node = TestNodeBuilder::new(NodeType::Gather, "test_gather_shape_shape_3")
.attr_int("axis", 0)
.input_shape("data", 5) .add_input("indices", ArgType::Shape(3)) .output_shape("output", 1) .build();
let processor = GatherProcessor;
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::Shape(rank) => {
assert_eq!(*rank, 3, "Expected Shape(3) output for Shape(3) indices");
}
other => panic!("Expected Shape(3) output, got {:?}", other),
}
}
#[test]
fn test_gather_update_outputs_shape_with_tensor_indices() {
let mut node = TestNodeBuilder::new(NodeType::Gather, "test_gather_shape_tensor")
.attr_int("axis", 0)
.input_shape("data", 4) .input_tensor_i64("indices", 1, None) .output_shape("output", 1) .build();
let processor = GatherProcessor;
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::Shape(rank) => {
assert_eq!(*rank, 1, "Expected Shape(1) output for 1D tensor indices");
}
other => panic!("Expected Shape(1) output, got {:?}", other),
}
}
#[test]
fn test_gather_scalar_input() {
let mut node = TestNodeBuilder::new(NodeType::Gather, "test_gather_scalar")
.attr_int("axis", 0)
.add_input("data", ArgType::ScalarNative(crate::ir::DType::I64)) .add_input("indices", ArgType::ScalarNative(crate::ir::DType::I64)) .add_output(
"output",
ArgType::Tensor(TensorType::new(crate::ir::DType::I64, 1, None)),
) .build();
let processor = GatherProcessor;
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::ScalarNative(dtype) => {
assert_eq!(*dtype, crate::ir::DType::I64);
}
other => panic!("Expected Scalar output, got {:?}", other),
}
}
#[test]
fn test_gather_is_noop_scalar_input() {
let mut node = TestNodeBuilder::new(NodeType::Gather, "test_gather_noop")
.attr_int("axis", 0)
.add_input("data", ArgType::ScalarNative(crate::ir::DType::I64))
.add_input("indices", ArgType::ScalarNative(crate::ir::DType::I64))
.add_output(
"output",
ArgType::Tensor(TensorType::new(crate::ir::DType::I64, 1, None)),
)
.build();
let processor = GatherProcessor;
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(processor.is_noop(&node));
}
#[test]
fn test_gather_is_not_noop_tensor_input() {
let mut node = create_test_node(0, 3, false).build();
let processor = GatherProcessor;
let prefs = OutputPreferences::new();
processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(!processor.is_noop(&node));
}
#[test]
fn test_gather_infer_types_lifts_constant_rank0_tensor_index() {
let mut node = TestNodeBuilder::new(NodeType::Gather, "test_gather_rank0_tensor")
.attr_int("axis", 0)
.input_tensor_f32("data", 3, None)
.input_scalar_tensor_i64("indices", Some(2))
.output_tensor_f32("output", 2, None)
.build_with_graph_data(16);
node.inputs[1].ty = ArgType::Tensor(TensorType::new(DType::I64, 0, None));
let processor = GatherProcessor;
let prefs = OutputPreferences::new();
assert!(node.inputs[1].is_constant()); processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(node.inputs[1].is_static()); }
#[test]
fn test_gather_infer_types_lifts_constant_scalar_tensor_index() {
let mut node = TestNodeBuilder::new(NodeType::Gather, "test_gather_scalar_tensor")
.attr_int("axis", 0)
.input_tensor_f32("data", 3, None)
.input_scalar_tensor_i64("indices", Some(2))
.output_tensor_f32("output", 2, None)
.build_with_graph_data(16);
node.inputs[1].ty = ArgType::ScalarTensor(DType::I64);
let processor = GatherProcessor;
let prefs = OutputPreferences::new();
assert!(node.inputs[1].is_constant()); processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(node.inputs[1].is_static()); }
#[test]
fn test_gather_infer_types_lifts_constant_scalar_index() {
let mut node = TestNodeBuilder::new(NodeType::Gather, "test_gather_scalar")
.attr_int("axis", 0)
.input_tensor_f32("data", 3, None)
.input_scalar_tensor_i64("indices", Some(2))
.output_tensor_f32("output", 2, None)
.build_with_graph_data(16);
node.inputs[1].ty = ArgType::ScalarNative(DType::I64);
let processor = GatherProcessor;
let prefs = OutputPreferences::new();
assert!(node.inputs[1].is_constant()); processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(node.inputs[1].is_static()); }
#[test]
fn test_gather_infer_types_dynamic_index() {
let mut node = TestNodeBuilder::new(NodeType::Gather, "test_gather_dynamic")
.attr_int("axis", 0)
.input_tensor_f32("data", 3, None)
.input_scalar_tensor_i64("indices", None)
.output_tensor_f32("output", 2, None)
.build_with_graph_data(16);
let processor = GatherProcessor;
let prefs = OutputPreferences::new();
assert!(!node.inputs[1].is_constant()); processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(!node.inputs[1].is_static()); }
#[test]
fn test_gather_infer_types_constant_2d_index() {
let mut node = TestNodeBuilder::new(NodeType::Gather, "test_gather_constant_2d")
.attr_int("axis", 0)
.input_tensor_f32("data", 3, None)
.input_scalar_tensor_i64("indices", Some(2))
.output_tensor_f32("output", 2, None)
.build_with_graph_data(16);
node.inputs[1].ty = ArgType::Tensor(TensorType::new(DType::I64, 2, None));
let processor = GatherProcessor;
let prefs = OutputPreferences::new();
assert!(node.inputs[1].is_constant()); processor.infer_types(&mut node, 16, &prefs).unwrap();
assert!(!node.inputs[1].is_static()); }
}