use std::{cell::RefCell, collections::HashMap, iter::Peekable, rc::Rc, slice::Iter};
use crate::{
graph_state::GraphState,
ir::{ArgType, AttributeValue, NodeType, RawNode, TensorData, TensorDataExt},
pipeline::Error,
processor::get_processor_registry,
proto_conversion::convert_node_proto,
protos::{GraphProto, NodeProto},
};
pub(crate) fn convert_nodes_from_graph(
graph: &GraphProto,
state_rc: &Rc<RefCell<GraphState>>,
opset_version: usize,
) -> Result<(), Error> {
convert_nodes_impl(&graph.node, state_rc, opset_version)
}
fn convert_nodes_impl(
nodes: &[NodeProto],
state_rc: &Rc<RefCell<GraphState>>,
opset_version: usize,
) -> Result<(), Error> {
let mut node_name_counter: HashMap<NodeType, usize> = HashMap::new();
let name_registry = {
let state = state_rc.borrow();
if let Some(registry) = state.name_registry() {
Some(registry.clone())
} else {
let constant_count = state
.processed_nodes
.iter()
.filter(|n| n.node_type == NodeType::Constant)
.count();
if constant_count > 0 {
node_name_counter.insert(NodeType::Constant, constant_count);
}
None
}
};
let mut node_iter = nodes.iter().peekable();
while let Some(node_proto) = node_iter.next() {
let mut node = convert_node_proto(node_proto, &state_rc.borrow());
if matches!(
node.node_type,
NodeType::If | NodeType::Loop | NodeType::Scan
) {
let outer_refs =
crate::proto_conversion::extract_node_outer_scope_references(node_proto);
if !outer_refs.is_empty() {
log::debug!(
"Found {} outer-scope references for {} node {}: {:?}",
outer_refs.len(),
node.node_type,
node.name,
outer_refs
);
let state = state_rc.borrow();
let mut scope_ref_names: Vec<String> = Vec::new();
for ref_name in outer_refs {
if node.inputs.iter().any(|i| i.name == ref_name) {
log::debug!("Skipping '{}' - already in node inputs", ref_name);
continue;
}
let arg = state.init_in(&ref_name);
log::debug!(
"Adding outer-scope reference '{}' -> arg.name='{}' (type: {:?}) as input to {} node",
ref_name,
arg.name,
arg.ty,
node.node_type
);
node.inputs.push(arg);
scope_ref_names.push(ref_name);
}
let onnx_input_count = node.inputs.len() - scope_ref_names.len();
node.attrs.insert(
"__onnx_input_count".to_string(),
AttributeValue::Int64(onnx_input_count as i64),
);
node.attrs.insert(
"__scope_ref_names".to_string(),
AttributeValue::Strings(scope_ref_names),
);
}
let parent_registry = if let Some(registry) = state_rc.borrow().name_registry().cloned()
{
registry
} else {
let registry = crate::graph_state::NameRegistry::new();
for (node_type, count) in &node_name_counter {
registry.set_initial_counter(node_type, count + 1);
}
registry.set_initial_counter(&node.node_type, 1);
registry
};
let base_path = state_rc.borrow().base_path().map(|p| p.to_path_buf());
let graph_attrs = crate::proto_conversion::convert_graph_attributes(
node_proto,
opset_version,
Some(parent_registry),
base_path.as_deref(),
);
for (key, value) in graph_attrs {
node.attrs.insert(key, value);
}
}
attach_value_stores(&mut node, state_rc);
if node.node_type == NodeType::Constant {
extract_constant_from_attributes(&mut node, state_rc);
}
remap_node_type(&mut node);
rename_node(&mut node, &mut node_name_counter, name_registry.as_ref());
let node_type_before = node.node_type.clone();
coalesce(&mut node, &mut node_iter, &mut state_rc.borrow_mut());
attach_value_stores(&mut node, state_rc);
if node.node_type != node_type_before {
rename_node(&mut node, &mut node_name_counter, name_registry.as_ref());
}
let registry = get_processor_registry();
let processor = registry.get(&node.node_type);
processor
.lift_constants(&mut node, opset_version)
.unwrap_or_else(|e| {
panic!(
"Failed to lift constants for node {} (type: {:?}): {:?}",
node.name, node.node_type, e
)
});
state_rc.borrow_mut().add_node(node);
}
Ok(())
}
fn extract_constant_from_attributes(node: &mut RawNode, state_rc: &Rc<RefCell<GraphState>>) {
let keys = [
"value",
"value_float",
"value_floats",
"value_int",
"value_ints",
"value_string",
"value_strings",
];
if let Some(attr_key) = keys.iter().find(|&key| node.attrs.contains_key(*key))
&& let Some(attr_value) = node.attrs.get(*attr_key)
{
let tensor_data_opt: Option<TensorData> = match attr_value {
AttributeValue::Tensor(tensor) => Some(tensor.clone()),
AttributeValue::Float32(val) => Some(TensorData::new(vec![*val], [0usize; 0])),
AttributeValue::Float32s(vals) => Some(TensorData::new(vals.clone(), [vals.len()])),
AttributeValue::Int64(val) => Some(TensorData::new(vec![*val], [0usize; 0])),
AttributeValue::Int64s(vals) => Some(TensorData::new(vals.clone(), [vals.len()])),
_ => None,
};
if let Some(tensor_data) = tensor_data_opt {
let data_id = {
let mut state = state_rc.borrow_mut();
state.store_tensor_data(tensor_data.clone().into())
};
let ty = if tensor_data.shape.is_empty() {
crate::ir::ArgType::ScalarNative(tensor_data.elem_type())
} else {
crate::ir::ArgType::Tensor(crate::ir::TensorType {
dtype: tensor_data.elem_type(),
rank: tensor_data.shape.len(),
static_shape: Some(tensor_data.shape.iter().map(|&d| Some(d)).collect()),
})
};
let mut input_arg = crate::ir::Argument {
name: String::new(),
ty: ty.clone(),
value_source: crate::ir::ValueSource::Static(data_id),
value_store: None,
};
let value_store = state_rc.borrow().build_value_store();
input_arg.set_value_store(value_store);
node.inputs.push(input_arg);
if !node.outputs.is_empty() {
node.outputs[0].value_source = crate::ir::ValueSource::Constant;
node.outputs[0].ty = ty;
}
node.attrs.remove(*attr_key);
}
}
}
fn attach_value_stores(node: &mut RawNode, state_rc: &Rc<RefCell<GraphState>>) {
let value_store = state_rc.borrow().build_value_store();
for arg in &mut node.inputs {
let should_preserve = match arg.value_source {
crate::ir::ValueSource::Constant => {
arg.value_store
.as_ref()
.map(|store| store.get_constant_data_id(&arg.name).is_some())
.unwrap_or(false)
}
crate::ir::ValueSource::Static(data_id) => {
arg.value_store
.as_ref()
.map(|store| store.get_tensor_data(data_id).is_some())
.unwrap_or(false)
}
_ => false,
};
if should_preserve {
log::debug!(
"Preserving outer-scope value_store for '{}' ({:?})",
arg.name,
arg.value_source
);
} else {
arg.set_value_store(value_store.clone());
}
}
for arg in &mut node.outputs {
arg.set_value_store(value_store.clone());
}
}
fn rename_node(
node: &mut RawNode,
counters: &mut HashMap<NodeType, usize>,
name_registry: Option<&crate::graph_state::NameRegistry>,
) {
if let Some(registry) = name_registry {
let old_name = node.name.clone();
node.name = registry.generate_node_name(&node.node_type);
log::debug!(
"Renamed node: '{}' -> '{}' (type: {:?})",
old_name,
node.name,
node.node_type
);
} else {
counters
.entry(node.node_type.clone())
.and_modify(|e| *e += 1)
.or_insert(1);
let new_name = format!("{}{}", node.node_type, counters[&node.node_type]).to_lowercase();
node.name = new_name;
}
}
fn remap_node_with_kernel_shape<F>(node: &mut RawNode, new_node_type: F)
where
F: FnOnce(usize) -> NodeType,
{
let spatial_dims = match node.attrs.get("kernel_shape") {
Some(AttributeValue::Int64s(ints)) => ints.len(),
None if [NodeType::Conv, NodeType::ConvTranspose].contains(&node.node_type) => {
if let ArgType::Tensor(weight) = &node.inputs[1].ty {
weight.rank - 2
} else {
panic!("Cannot infer kernel spatial dims");
}
}
_ => panic!("Cannot infer kernel shape"),
};
node.node_type = new_node_type(spatial_dims);
}
fn remap_node_type(node: &mut RawNode) {
match node.node_type {
NodeType::Conv => remap_node_with_kernel_shape(node, |spatial_dims| match spatial_dims {
1 => NodeType::Conv1d,
2 => NodeType::Conv2d,
3 => NodeType::Conv3d,
_ => panic!("Only conv 1d, 2d and 3d are supported"),
}),
NodeType::ConvTranspose => {
remap_node_with_kernel_shape(node, |spatial_dims| match spatial_dims {
1 => NodeType::ConvTranspose1d,
2 => NodeType::ConvTranspose2d,
3 => NodeType::ConvTranspose3d,
_ => panic!("Only conv_transpose 1d, 2d and 3d are supported"),
})
}
NodeType::MaxPool => {
remap_node_with_kernel_shape(node, |spatial_dims| match spatial_dims {
1 => NodeType::MaxPool1d,
2 => NodeType::MaxPool2d,
3 => NodeType::MaxPool3d,
_ => panic!("Only max_pool 1d, 2d, and 3d are supported"),
})
}
NodeType::AveragePool => {
remap_node_with_kernel_shape(node, |spatial_dims| match spatial_dims {
1 => NodeType::AveragePool1d,
2 => NodeType::AveragePool2d,
3 => NodeType::AveragePool3d,
_ => panic!("Only avg_pool 1d, 2d, and 3d are supported"),
})
}
NodeType::LpPool => remap_node_with_kernel_shape(node, |spatial_dims| match spatial_dims {
1 => NodeType::LpPool1d,
2 => NodeType::LpPool2d,
_ => panic!("Only lp_pool 1d and 2d are supported"),
}),
_ => (),
}
}
fn coalesce(
node: &mut RawNode,
nodes_iter: &mut Peekable<Iter<NodeProto>>,
graph_data: &mut GraphState,
) {
#[allow(clippy::single_match)]
match node.node_type {
NodeType::Gemm => convert_gemm_to_linear(node),
NodeType::MatMul => {
convert_matmul_to_linear(node, nodes_iter, graph_data);
}
_ => {}
}
}
fn convert_gemm_to_linear(node: &mut RawNode) {
if node.outputs.len() != 1 {
panic!("Gemm node must have 1 output");
}
let trans_a = node
.attrs
.get("transA")
.map(|v| matches!(v, AttributeValue::Int64(1)))
.unwrap_or(false);
if trans_a {
log::debug!(
"Keeping Gemm node {} (transA=1 not supported for Linear conversion)",
node.name
);
return;
}
let straight_linear = match (
node.attrs.get("alpha"),
node.attrs.get("beta"),
node.attrs.get("transB"),
) {
(
Some(AttributeValue::Float32(alpha)),
Some(AttributeValue::Float32(beta)),
Some(AttributeValue::Int64(trans_b)),
) => *alpha == 1.0 && *beta == 1.0 && *trans_b == 1,
_ => false,
};
if straight_linear {
log::debug!("Fusing Gemm → Linear for node {}", node.name);
node.node_type = NodeType::Linear;
node.attrs.remove("alpha");
node.attrs.remove("beta");
node.attrs.remove("transA");
node.attrs.remove("transB");
node.attrs
.insert("transpose_weight".to_string(), AttributeValue::Int64(1));
} else {
log::debug!(
"Keeping Gemm node {} (alpha={:?}, beta={:?}, transB={:?} don't match Linear pattern)",
node.name,
node.attrs.get("alpha"),
node.attrs.get("beta"),
node.attrs.get("transB")
);
}
}
fn convert_matmul_to_linear(
node: &mut RawNode,
iter_mut: &mut Peekable<Iter<NodeProto>>,
graph_data: &mut GraphState,
) {
if node.inputs.len() != 2 {
panic!("MatMul node must have 2 inputs");
}
if !graph_data.has_value(&node.inputs[1].name) {
log::debug!(
"Keeping MatMul node {} (second input is not a constant weight)",
node.name
);
return;
}
match &node.inputs[1].ty {
ArgType::Tensor(tensor_type) if tensor_type.rank == 2 => {
}
ArgType::Tensor(tensor_type) => {
log::debug!(
"Keeping MatMul node {} (weight is {}D, not 2D)",
node.name,
tensor_type.rank
);
return;
}
_ => {
log::debug!("Keeping MatMul node {} (weight is not a tensor)", node.name);
return;
}
}
node.node_type = NodeType::Linear;
log::debug!("Converting MatMul → Linear for node {}", node.name);
if let Some(peek_node) = iter_mut.peek() {
let peek_node = convert_node_proto(peek_node, graph_data);
if is_add_node_with_bias(&peek_node, node, graph_data) {
convert_and_remove_add_node(&peek_node, node);
log::debug!("Fused Add bias into Linear node {}", node.name);
let _ = iter_mut.next();
}
}
}
fn is_add_node_with_bias(
peek_node: &RawNode,
current_node: &RawNode,
graph_data: &GraphState,
) -> bool {
if peek_node.node_type != NodeType::Add || peek_node.inputs.len() != 2 {
return false;
}
let bias_input = if peek_node.inputs[0].name == current_node.outputs[0].name
&& graph_data.has_value(&peek_node.inputs[1].name)
{
&peek_node.inputs[1]
} else if peek_node.inputs[1].name == current_node.outputs[0].name
&& graph_data.has_value(&peek_node.inputs[0].name)
{
&peek_node.inputs[0]
} else {
return false;
};
let weight_input = ¤t_node.inputs[1];
match (&bias_input.ty, &weight_input.ty) {
(ArgType::Tensor(bias_ty), ArgType::Tensor(weight_ty)) => {
if bias_ty.rank != 1 {
log::debug!(
"Skipping Add fusion: bias rank is {}, expected 1",
bias_ty.rank
);
return false;
}
if let (Some(bias_shape), Some(weight_shape)) =
(&bias_ty.static_shape, &weight_ty.static_shape)
{
let out_features = weight_shape.get(1).copied().flatten();
let bias_dim = bias_shape.first().copied().flatten();
if let (Some(out_f), Some(bias_d)) = (out_features, bias_dim)
&& out_f != bias_d
{
log::debug!(
"Skipping Add fusion: bias size {} != out_features {}",
bias_d,
out_f
);
return false;
}
} else {
log::debug!("Add fusion: static shapes not fully known, proceeding optimistically");
}
true
}
_ => {
log::debug!(
"Skipping Add fusion: expected (Tensor, Tensor), got ({}, {})",
bias_input.ty,
weight_input.ty
);
false
}
}
}
fn convert_and_remove_add_node(bias_node: &RawNode, current_node: &mut RawNode) {
let bias_input = if bias_node.inputs[0].name == current_node.outputs[0].name {
bias_node.inputs[1].clone()
} else {
bias_node.inputs[0].clone()
};
current_node.inputs.push(bias_input);
current_node.outputs[0]
.name
.clone_from(&bias_node.outputs[0].name);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::node::test_utils::TestNodeBuilder;
#[test]
fn should_infer_conv2d_node_from_weights_rank() {
let weight_data = vec![0.0; 16];
let weight_shape = vec![2, 2, 2, 2];
let mut node = TestNodeBuilder::new(NodeType::Conv, "test_conv2d")
.input_tensor_f32("data", 4, None)
.input_tensor_f32_data("weight", weight_data.clone(), weight_shape)
.output_tensor_f32("output", 4, None)
.attr_ints("strides", vec![1, 1])
.attr_ints("pads", vec![0, 0, 0, 0])
.attr_ints("dilations", vec![1, 1])
.attr_int("group", 1)
.build();
assert_eq!(node.node_type, NodeType::Conv);
remap_node_type(&mut node);
assert_eq!(node.node_type, NodeType::Conv2d);
}
#[test]
fn should_infer_conv_transpose1d_node_from_weights_rank() {
let weight_data = vec![0.0; 16];
let weight_shape = vec![2, 2, 4];
let mut node = TestNodeBuilder::new(NodeType::ConvTranspose, "test_conv2d")
.input_tensor_f32("data", 3, None)
.input_tensor_f32_data("weight", weight_data, weight_shape)
.output_tensor_f32("output", 3, None)
.build();
assert_eq!(node.node_type, NodeType::ConvTranspose);
remap_node_type(&mut node);
assert_eq!(node.node_type, NodeType::ConvTranspose1d);
}
#[test]
fn should_skip_matmul_to_linear_for_1d_weight() {
use crate::ir::TensorData;
let weight_data = vec![1.0, 2.0, 3.0, 4.0];
let weight_shape = vec![4];
let node = TestNodeBuilder::new(NodeType::MatMul, "test_matmul")
.input_tensor_f32("input", 2, None)
.input_tensor_f32_data("weight", weight_data.clone(), weight_shape.clone())
.output_tensor_f32("output", 1, None)
.build();
let mut graph_state = crate::graph_state::GraphState::new(&[], &[], &[], &[]);
graph_state.register_test_constant(
"weight".to_string(),
TensorData::new(weight_data, weight_shape),
);
let mut node = node;
let empty_nodes: Vec<NodeProto> = vec![];
let mut iter = empty_nodes.iter().peekable();
convert_matmul_to_linear(&mut node, &mut iter, &mut graph_state);
assert_eq!(node.node_type, NodeType::MatMul);
}
fn make_matmul_add_pair(
out_features: usize,
bias_shape: Vec<usize>,
) -> (RawNode, RawNode, crate::graph_state::GraphState) {
use crate::ir::{TensorData, TensorType};
let in_features = 4;
let weight_data = vec![0.0; in_features * out_features];
let weight_shape = vec![in_features, out_features];
let matmul_node = TestNodeBuilder::new(NodeType::MatMul, "matmul")
.input_tensor_f32("input", 2, Some(vec![8, in_features]))
.input_tensor_f32_data("weight", weight_data.clone(), weight_shape.clone())
.output_tensor_f32("matmul_out", 2, Some(vec![8, out_features]))
.build();
let mut matmul_node = matmul_node;
matmul_node.inputs[1].ty = ArgType::Tensor(TensorType {
dtype: burn_tensor::DType::F32,
rank: 2,
static_shape: Some(weight_shape.iter().map(|&d| Some(d)).collect()),
});
let bias_data = vec![0.0; bias_shape.iter().product::<usize>()];
let mut add_node = TestNodeBuilder::new(NodeType::Add, "add")
.input_tensor_f32("matmul_out", 2, Some(vec![8, out_features]))
.input_tensor_f32_data("bias", bias_data.clone(), bias_shape.clone())
.output_tensor_f32("add_out", 2, Some(vec![8, out_features]))
.build();
add_node.inputs[1].ty = ArgType::Tensor(TensorType {
dtype: burn_tensor::DType::F32,
rank: 1,
static_shape: Some(bias_shape.iter().map(|&d| Some(d)).collect()),
});
let mut graph_state = crate::graph_state::GraphState::new(&[], &[], &[], &[]);
graph_state.register_test_constant(
"weight".to_string(),
TensorData::new(weight_data, weight_shape),
);
graph_state
.register_test_constant("bias".to_string(), TensorData::new(bias_data, bias_shape));
(matmul_node, add_node, graph_state)
}
#[test]
fn should_fuse_add_bias_when_shape_matches() {
let (matmul_node, add_node, graph_state) = make_matmul_add_pair(32, vec![32]);
assert!(is_add_node_with_bias(&add_node, &matmul_node, &graph_state));
}
#[test]
fn should_skip_add_fusion_when_bias_is_scalar() {
let (matmul_node, add_node, graph_state) = make_matmul_add_pair(32, vec![1]);
assert!(!is_add_node_with_bias(
&add_node,
&matmul_node,
&graph_state
));
}
#[test]
fn should_skip_add_fusion_when_bias_rank_is_2d() {
use crate::ir::{TensorData, TensorType};
let out_features = 32;
let in_features = 4;
let weight_data = vec![0.0; in_features * out_features];
let weight_shape = vec![in_features, out_features];
let matmul_node = {
let mut n = TestNodeBuilder::new(NodeType::MatMul, "matmul")
.input_tensor_f32("input", 2, Some(vec![8, in_features]))
.input_tensor_f32_data("weight", weight_data.clone(), weight_shape.clone())
.output_tensor_f32("matmul_out", 2, Some(vec![8, out_features]))
.build();
n.inputs[1].ty = ArgType::Tensor(TensorType {
dtype: burn_tensor::DType::F32,
rank: 2,
static_shape: Some(weight_shape.iter().map(|&d| Some(d)).collect()),
});
n
};
let bias_data = vec![0.0; out_features];
let bias_shape = vec![1, out_features];
let mut add_node = TestNodeBuilder::new(NodeType::Add, "add")
.input_tensor_f32("matmul_out", 2, Some(vec![8, out_features]))
.input_tensor_f32_data("bias", bias_data.clone(), bias_shape.clone())
.output_tensor_f32("add_out", 2, Some(vec![8, out_features]))
.build();
add_node.inputs[1].ty = ArgType::Tensor(TensorType {
dtype: burn_tensor::DType::F32,
rank: 2,
static_shape: Some(bias_shape.iter().map(|&d| Some(d)).collect()),
});
let mut graph_state = crate::graph_state::GraphState::new(&[], &[], &[], &[]);
graph_state.register_test_constant(
"weight".to_string(),
TensorData::new(weight_data, weight_shape),
);
graph_state
.register_test_constant("bias".to_string(), TensorData::new(bias_data, bias_shape));
assert!(!is_add_node_with_bias(
&add_node,
&matmul_node,
&graph_state
));
}
#[test]
fn should_fuse_add_bias_with_reversed_operand_order() {
use crate::ir::{TensorData, TensorType};
let out_features = 32;
let in_features = 4;
let weight_data = vec![0.0; in_features * out_features];
let weight_shape = vec![in_features, out_features];
let matmul_node = {
let mut n = TestNodeBuilder::new(NodeType::MatMul, "matmul")
.input_tensor_f32("input", 2, Some(vec![8, in_features]))
.input_tensor_f32_data("weight", weight_data.clone(), weight_shape.clone())
.output_tensor_f32("matmul_out", 2, Some(vec![8, out_features]))
.build();
n.inputs[1].ty = ArgType::Tensor(TensorType {
dtype: burn_tensor::DType::F32,
rank: 2,
static_shape: Some(weight_shape.iter().map(|&d| Some(d)).collect()),
});
n
};
let bias_data = vec![0.0; out_features];
let bias_shape = vec![out_features];
let mut add_node = TestNodeBuilder::new(NodeType::Add, "add")
.input_tensor_f32_data("bias", bias_data.clone(), bias_shape.clone())
.input_tensor_f32("matmul_out", 2, Some(vec![8, out_features]))
.output_tensor_f32("add_out", 2, Some(vec![8, out_features]))
.build();
add_node.inputs[0].ty = ArgType::Tensor(TensorType {
dtype: burn_tensor::DType::F32,
rank: 1,
static_shape: Some(bias_shape.iter().map(|&d| Some(d)).collect()),
});
let mut graph_state = crate::graph_state::GraphState::new(&[], &[], &[], &[]);
graph_state.register_test_constant(
"weight".to_string(),
TensorData::new(weight_data, weight_shape),
);
graph_state
.register_test_constant("bias".to_string(), TensorData::new(bias_data, bias_shape));
assert!(is_add_node_with_bias(&add_node, &matmul_node, &graph_state));
}
#[test]
fn should_fuse_add_bias_when_static_shapes_unknown() {
use crate::ir::TensorData;
let matmul_node = TestNodeBuilder::new(NodeType::MatMul, "matmul")
.input_tensor_f32("input", 2, None)
.input_tensor_f32_data("weight", vec![0.0; 128], vec![4, 32])
.output_tensor_f32("matmul_out", 2, None)
.build();
let add_node = TestNodeBuilder::new(NodeType::Add, "add")
.input_tensor_f32("matmul_out", 2, None)
.input_tensor_f32_data("bias", vec![0.0; 32], vec![32])
.output_tensor_f32("add_out", 2, None)
.build();
let mut graph_state = crate::graph_state::GraphState::new(&[], &[], &[], &[]);
graph_state.register_test_constant(
"weight".to_string(),
TensorData::new(vec![0.0; 128], vec![4, 32]),
);
graph_state
.register_test_constant("bias".to_string(), TensorData::new(vec![0.0; 32], vec![32]));
assert!(is_add_node_with_bias(&add_node, &matmul_node, &graph_state));
}
}