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], vec![])),
AttributeValue::Float32s(vals) => Some(TensorData::new(vals.clone(), vec![vals.len()])),
AttributeValue::Int64(val) => Some(TensorData::new(vec![*val], vec![])),
AttributeValue::Int64s(vals) => Some(TensorData::new(vals.clone(), vec![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::Scalar(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.to_vec()),
})
};
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,
_ => panic!("Only max_pool 1d and 2d are supported"),
})
}
NodeType::AveragePool => {
remap_node_with_kernel_shape(node, |spatial_dims| match spatial_dims {
1 => NodeType::AveragePool1d,
2 => NodeType::AveragePool2d,
_ => panic!("Only avg_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;
}
if let ArgType::Tensor(ref tensor_type) = node.inputs[1].ty {
assert_eq!(tensor_type.rank, 2, "Weight must be a 2D tensor");
} else {
panic!("Tensor input is expected");
}
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;
}
(peek_node.inputs[0].name == current_node.outputs[0].name
&& graph_data.has_value(&peek_node.inputs[1].name))
|| (peek_node.inputs[1].name == current_node.outputs[0].name
&& graph_data.has_value(&peek_node.inputs[0].name))
}
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);
}
}