use std::path::Path;
use std::str::{FromStr, from_utf8};
use super::graph_state::GraphState;
use super::ir::{
ArgType, Argument, AttributeValue, Attributes, NodeType, RawNode, TensorData, TensorDataExt,
TensorType,
};
use super::protos::{
AttributeProto, NodeProto, TensorProto, TensorShapeProto, ValueInfoProto,
attribute_proto::AttributeType,
tensor_proto::{DataLocation, DataType as DT},
tensor_shape_proto::dimension::Value,
};
use crate::external_data::ExternalDataInfo;
use crate::tensor_store::TensorDataRef;
use burn_tensor::DType;
use protobuf::Enum;
pub const DEFAULT_OPSET_VERSION: usize = 16;
#[derive(Debug)]
pub enum ParseError {
VariantNotFound(String),
}
pub fn sanitize_name(name: &str) -> String {
if name.is_empty() {
return String::new();
}
let mut result = String::with_capacity(name.len() * 2);
let mut prev_is_lower = false;
let mut prev_is_underscore = false;
for (i, c) in name.chars().enumerate() {
if c == '_' {
if !prev_is_underscore || i == 0 {
result.push('_');
prev_is_underscore = true;
}
prev_is_lower = false;
} else if c.is_ascii_alphanumeric() {
if c.is_ascii_uppercase() && prev_is_lower && !prev_is_underscore {
result.push('_');
}
result.push(c.to_ascii_lowercase());
prev_is_lower = c.is_ascii_lowercase();
prev_is_underscore = false;
} else {
if !prev_is_underscore && i > 0 {
result.push('_');
prev_is_underscore = true;
}
prev_is_lower = false;
}
}
while result.ends_with('_') && result.len() > 1 {
let check = result.trim_end_matches('_');
if check.is_empty() {
break;
}
result.pop();
}
if !result.is_empty() && !result.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') {
result = format!("_{result}");
}
result
}
pub fn element_type_from_proto(dt_i32: i32) -> Result<DType, String> {
match DT::from_i32(dt_i32).ok_or_else(|| format!("unknown dtype {}", dt_i32))? {
DT::FLOAT => Ok(DType::F32),
DT::DOUBLE => Ok(DType::F64),
DT::FLOAT16 => Ok(DType::F16),
DT::BFLOAT16 => Ok(DType::BF16),
DT::INT64 => Ok(DType::I64),
DT::INT32 => Ok(DType::I32),
DT::INT16 => Ok(DType::I16),
DT::INT8 => Ok(DType::I8),
DT::UINT64 => Ok(DType::U64),
DT::UINT32 => Ok(DType::U32),
DT::UINT16 => Ok(DType::U16),
DT::UINT8 => Ok(DType::U8),
DT::BOOL => Ok(DType::Bool),
DT::STRING => Err("String tensors not supported".to_string()),
other => Err(format!("unsupported dtype {:?}", other)),
}
}
pub fn argument_from_initializer(initializer: &TensorProto) -> (Argument, TensorData) {
use crate::ir::ValueSource;
let name = initializer.name.clone();
match TensorData::try_from(initializer.clone()) {
Ok(td) => {
let arg = if td.shape.is_empty() {
Argument {
name,
ty: ArgType::Scalar(td.elem_type()),
value_source: ValueSource::Constant, value_store: None,
}
} else {
Argument {
name,
ty: ArgType::Tensor(TensorType {
dtype: td.elem_type(),
rank: td.shape.len(),
static_shape: Some(td.shape.to_vec()),
}),
value_source: ValueSource::Constant, value_store: None,
}
};
(arg, td)
}
Err(orig_err) => {
let dims: Vec<i64> = initializer.dims.clone();
if dims.iter().any(|&d| d < 0) {
panic!(
"invalid tensor shape (negative dims) for initializer '{}': {:?}",
name, dims
);
}
let dim_elems: usize = if dims.is_empty() {
1
} else {
dims.iter().map(|&d| d as usize).product()
};
let payload_len = {
let i32n = initializer.int32_data.len();
let i64n = initializer.int64_data.len();
let f32n = initializer.float_data.len();
let f64n = initializer.double_data.len();
let sn = initializer.string_data.len();
let typed = *[i32n, i64n, f32n, f64n, sn].iter().max().unwrap_or(&0);
if typed > 0 {
typed
} else {
if !initializer.raw_data.is_empty() && dim_elems == 1 {
1
} else {
0
}
}
};
let looks_scalar = dims.is_empty() || (dims.len() == 1 && dims[0] == 1);
if looks_scalar && payload_len == 1 {
let td = TensorData::try_from(initializer.clone()).unwrap_or_else(|_| {
panic!(
"failed to decode scalar initializer '{}': dims={:?}",
name, dims
)
});
let arg = Argument {
name,
ty: ArgType::Scalar(td.elem_type()),
value_source: ValueSource::Constant, value_store: None,
};
return (arg, td);
}
if dim_elems == 0 && payload_len == 0 && !dims.is_empty() {
let dtype = element_type_from_proto(initializer.data_type).unwrap_or_else(|e| {
panic!(
"unsupported empty-tensor data_type={} for '{}': {}",
initializer.data_type, name, e
)
});
let shape_usize: Vec<usize> = dims.iter().map(|&d| d as usize).collect();
let td = match dtype {
DType::F32 => TensorData::new(Vec::<f32>::new(), shape_usize.clone()),
DType::F64 => TensorData::new(Vec::<f64>::new(), shape_usize.clone()),
DType::F16 => TensorData::new(Vec::<half::f16>::new(), shape_usize.clone()),
DType::I32 => TensorData::new(Vec::<i32>::new(), shape_usize.clone()),
DType::I64 => TensorData::new(Vec::<i64>::new(), shape_usize.clone()),
DType::U16 => TensorData::new(Vec::<u16>::new(), shape_usize.clone()),
DType::U8 => TensorData::new(Vec::<u8>::new(), shape_usize.clone()),
DType::I8 => TensorData::new(Vec::<i8>::new(), shape_usize.clone()),
DType::Bool => TensorData::new(Vec::<bool>::new(), shape_usize.clone()),
_ => panic!("Unsupported dtype {:?} for empty tensor", dtype),
};
let arg = Argument {
name,
ty: ArgType::Tensor(TensorType {
dtype,
rank: shape_usize.len(),
static_shape: Some(shape_usize),
}),
value_source: ValueSource::Constant, value_store: None,
};
return (arg, td);
}
panic!(
"invalid tensor '{}' (dims {:?} => {} elems) with payload {} elems; original error: {:?}",
name, dims, dim_elems, payload_len, orig_err
);
}
}
}
pub fn argument_from_initializer_lazy_with_context(
initializer: TensorProto,
base_path: Option<&Path>,
) -> Result<(Argument, TensorDataRef), ParseError> {
use crate::ir::ValueSource;
let name = initializer.name.clone();
let data_ref = tensor_data_ref_from_proto(initializer, base_path)?;
let arg = if data_ref.shape().is_empty() {
Argument {
name,
ty: ArgType::Scalar(data_ref.dtype()),
value_source: ValueSource::Constant,
value_store: None,
}
} else {
Argument {
name,
ty: ArgType::Tensor(TensorType {
dtype: data_ref.dtype(),
rank: data_ref.shape().len(),
static_shape: Some(data_ref.shape().to_vec()),
}),
value_source: ValueSource::Constant,
value_store: None,
}
};
Ok((arg, data_ref))
}
impl TryFrom<TensorProto> for TensorDataRef {
type Error = ParseError;
fn try_from(tensor: TensorProto) -> Result<TensorDataRef, Self::Error> {
tensor_data_ref_from_proto(tensor, None)
}
}
pub fn tensor_data_ref_from_proto(
tensor: TensorProto,
base_path: Option<&Path>,
) -> Result<TensorDataRef, ParseError> {
let shape = convert_shape(tensor.dims.clone());
let elem = element_type_from_proto(tensor.data_type).map_err(ParseError::VariantNotFound)?;
if tensor.data_location.enum_value() == Ok(DataLocation::EXTERNAL) {
return create_external_data_ref(&tensor, base_path, shape, elem);
}
if !tensor.raw_data.is_empty() {
match elem {
DType::F32
| DType::F64
| DType::F16
| DType::BF16
| DType::I64
| DType::I32
| DType::I16
| DType::I8
| DType::U64
| DType::U32
| DType::U16
| DType::U8
| DType::Bool => Ok(TensorDataRef::new(tensor.raw_data, shape, elem)),
_ => Err(ParseError::VariantNotFound(format!(
"Unsupported dtype {:?}",
elem
))),
}
} else {
let raw_bytes = match elem {
DType::F32 => vec_to_bytes(&tensor.float_data),
DType::F64 => vec_to_bytes(&tensor.double_data),
DType::I64 => vec_to_bytes(&tensor.int64_data),
DType::I32 => vec_to_bytes(&tensor.int32_data),
DType::I16 => {
let data: Vec<i16> = tensor.int32_data.iter().map(|&x| x as i16).collect();
vec_to_bytes(&data)
}
DType::I8 => {
let data: Vec<i8> = tensor.int32_data.iter().map(|&x| x as i8).collect();
vec_to_bytes(&data)
}
DType::U64 => vec_to_bytes(&tensor.uint64_data),
DType::U32 => {
let data: Vec<u32> = tensor.uint64_data.iter().map(|&x| x as u32).collect();
vec_to_bytes(&data)
}
DType::U16 => {
let data: Vec<u16> = tensor.int32_data.iter().map(|&x| x as u16).collect();
vec_to_bytes(&data)
}
DType::U8 => {
let data: Vec<u8> = tensor.int32_data.iter().map(|&x| x as u8).collect();
bytes::Bytes::from(data)
}
DType::Bool => {
let data: Vec<u8> = tensor.int32_data.iter().map(|&x| (x != 0) as u8).collect();
bytes::Bytes::from(data)
}
DType::F16 => {
let data: Vec<u16> = tensor.int32_data.iter().map(|&x| x as u16).collect();
vec_to_bytes(&data)
}
DType::BF16 => {
let data: Vec<u16> = tensor.int32_data.iter().map(|&x| x as u16).collect();
vec_to_bytes(&data)
}
_ => {
return Err(ParseError::VariantNotFound(format!(
"empty/unsupported payload for {:?}",
elem
)));
}
};
Ok(TensorDataRef::new(raw_bytes, shape, elem))
}
}
fn create_external_data_ref(
tensor: &TensorProto,
base_path: Option<&Path>,
shape: Vec<usize>,
dtype: DType,
) -> Result<TensorDataRef, ParseError> {
let entries = tensor
.external_data
.iter()
.map(|e| (e.key.as_str(), e.value.as_str()));
let external_info = ExternalDataInfo::from_proto_entries(entries).map_err(|e| {
ParseError::VariantNotFound(format!(
"Failed to parse external_data for tensor '{}': {}",
tensor.name, e
))
})?;
let base = base_path.ok_or_else(|| {
ParseError::VariantNotFound(format!(
"Tensor '{}' uses external data but no base_path provided. \
External data requires loading from a file path, not bytes.",
tensor.name
))
})?;
let file_path = external_info
.resolve_path(base)
.map_err(ParseError::VariantNotFound)?;
let length = external_info.length.unwrap_or_else(|| {
let num_elements: usize = if shape.is_empty() {
1 } else {
shape.iter().product()
};
(num_elements * dtype.size()) as u64
});
log::debug!(
"Creating external data ref for '{}': file={}, offset={}, length={}",
tensor.name,
file_path.display(),
external_info.offset,
length
);
Ok(TensorDataRef::new_external(
file_path,
external_info.offset,
length,
shape,
dtype,
))
}
fn vec_to_bytes<T: bytemuck::Pod>(data: &[T]) -> bytes::Bytes {
bytes::Bytes::copy_from_slice(bytemuck::cast_slice(data))
}
impl TryFrom<TensorProto> for TensorData {
type Error = ParseError;
fn try_from(tensor: TensorProto) -> Result<TensorData, Self::Error> {
let data_ref = TensorDataRef::try_from(tensor)?;
Ok(data_ref.to_tensor_data())
}
}
impl TryFrom<TensorShapeProto> for Vec<usize> {
type Error = ParseError;
fn try_from(shape: TensorShapeProto) -> Result<Vec<usize>, Self::Error> {
let mut result = Vec::new();
for dim in shape.dim {
if let Value::DimValue(value) = dim.value.unwrap() {
result.push(value as usize);
}
}
Ok(result)
}
}
fn convert_vec_tensor_proto(tensors: Vec<TensorProto>) -> Result<Vec<TensorData>, ParseError> {
let mut result = Vec::new();
for tensor in tensors {
result.push(TensorData::try_from(tensor)?);
}
Ok(result)
}
impl TryFrom<AttributeProto> for AttributeValue {
type Error = ParseError;
fn try_from(attr: AttributeProto) -> Result<AttributeValue, Self::Error> {
let value = match attr.type_.unwrap() {
AttributeType::FLOAT => AttributeValue::Float32(attr.f),
AttributeType::INT => AttributeValue::Int64(attr.i),
AttributeType::STRING => AttributeValue::String(to_string(attr.s)),
AttributeType::TENSOR => AttributeValue::Tensor(TensorData::try_from(attr.t.unwrap())?),
AttributeType::GRAPH => {
panic!(
"Graph attributes should be converted during node processing, not during proto conversion"
)
}
AttributeType::FLOATS => AttributeValue::Float32s(attr.floats),
AttributeType::INTS => AttributeValue::Int64s(attr.ints),
AttributeType::STRINGS => AttributeValue::Strings(to_string_vec(attr.strings)),
AttributeType::TENSORS => {
AttributeValue::Tensors(convert_vec_tensor_proto(attr.tensors)?)
}
AttributeType::GRAPHS => {
panic!(
"Graphs attributes should be converted during node processing, not during proto conversion"
)
}
attribute_type => {
return Err(ParseError::VariantNotFound(format!("{attribute_type:?}")));
}
};
Ok(value)
}
}
pub fn convert_vec_attrs_proto(attrs: Vec<AttributeProto>) -> Attributes {
let mut result = Attributes::new();
for attr in attrs {
if let Ok(attr_type) = attr.type_.enum_value()
&& (attr_type == AttributeType::GRAPH || attr_type == AttributeType::GRAPHS)
{
continue;
}
result.insert(attr.name.clone(), AttributeValue::try_from(attr).unwrap());
}
result
}
pub fn convert_node_proto(node: &NodeProto, graph_data: &GraphState) -> RawNode {
let name = sanitize_name(&node.name);
let inputs = node.input.iter().map(|x| graph_data.init_in(x)).collect();
let outputs = node
.output
.iter()
.map(|output_name| {
let mut arg = Argument::from_name(sanitize_name(output_name));
if let Some(graph_output_type) = graph_data.get_output_type(output_name) {
arg.ty = graph_output_type.clone();
} else if let Some(value_info_type) = graph_data.get_value_info_type(output_name) {
arg.ty = value_info_type.clone();
}
arg
})
.collect();
let attrs = convert_vec_attrs_proto(node.attribute.clone());
let node_type = NodeType::from_str(&node.op_type).expect("Unknown node type");
RawNode {
node_type,
name,
inputs,
outputs,
attrs,
}
}
fn to_string(bytes: bytes::Bytes) -> String {
from_utf8(&bytes).unwrap().to_string()
}
fn to_string_vec(bytes: Vec<bytes::Bytes>) -> Vec<String> {
bytes.into_iter().map(to_string).collect()
}
fn convert_shape(shape: Vec<i64>) -> Vec<usize> {
shape.iter().map(|s| *s as usize).collect()
}
pub fn extract_outer_scope_references(
graph_proto: &crate::protos::GraphProto,
) -> std::collections::HashSet<String> {
use std::collections::HashSet;
let initializer_names: HashSet<String> = graph_proto
.initializer
.iter()
.filter(|i| !i.name.is_empty())
.map(|i| sanitize_name(&i.name))
.collect();
let is_outer_scope_input = |name: &str| -> bool {
!name.is_empty() && !initializer_names.contains(&sanitize_name(name))
};
let mut defined_names: HashSet<String> = HashSet::new();
for input in &graph_proto.input {
if !input.name.is_empty() && !is_outer_scope_input(&input.name) {
defined_names.insert(sanitize_name(&input.name));
}
}
for init in &graph_proto.initializer {
if !init.name.is_empty() {
defined_names.insert(sanitize_name(&init.name));
}
}
for node in &graph_proto.node {
for output in &node.output {
if !output.is_empty() {
defined_names.insert(sanitize_name(output));
}
}
}
let mut referenced_names: HashSet<String> = HashSet::new();
for input in &graph_proto.input {
if is_outer_scope_input(&input.name) {
referenced_names.insert(sanitize_name(&input.name));
}
}
for node in &graph_proto.node {
for input in &node.input {
if !input.is_empty() {
referenced_names.insert(sanitize_name(input));
}
}
let is_loop_or_scan = node.op_type == "Loop" || node.op_type == "Scan";
for attr in &node.attribute {
if let Ok(attr_type) = attr.type_.enum_value() {
use crate::protos::attribute_proto::AttributeType;
match attr_type {
AttributeType::GRAPH => {
if let Some(nested_graph) = attr.g.as_ref() {
let loop_provided_names: std::collections::HashSet<String> =
if is_loop_or_scan && attr.name == "body" {
nested_graph
.input
.iter()
.filter(|i| !i.name.is_empty())
.map(|i| sanitize_name(&i.name))
.collect()
} else {
std::collections::HashSet::new()
};
let nested_refs = extract_outer_scope_references(nested_graph);
for name in nested_refs {
if !defined_names.contains(&name)
&& !loop_provided_names.contains(&name)
{
referenced_names.insert(name);
}
}
}
}
AttributeType::GRAPHS => {
for nested_graph in &attr.graphs {
let nested_refs = extract_outer_scope_references(nested_graph);
for name in nested_refs {
if !defined_names.contains(&name) {
referenced_names.insert(name);
}
}
}
}
_ => {}
}
}
}
}
for output in &graph_proto.output {
if !output.name.is_empty() {
referenced_names.insert(sanitize_name(&output.name));
}
}
referenced_names
.difference(&defined_names)
.cloned()
.collect()
}
pub fn extract_node_outer_scope_references(
node_proto: &NodeProto,
) -> std::collections::HashSet<String> {
use std::collections::HashSet;
let mut all_refs: HashSet<String> = HashSet::new();
for attr in &node_proto.attribute {
if let Ok(attr_type) = attr.type_.enum_value() {
match attr_type {
AttributeType::GRAPH => {
if let Some(graph_proto) = attr.g.as_ref() {
let refs = extract_outer_scope_references(graph_proto);
all_refs.extend(refs);
}
}
AttributeType::GRAPHS => {
for graph_proto in &attr.graphs {
let refs = extract_outer_scope_references(graph_proto);
all_refs.extend(refs);
}
}
_ => {}
}
}
}
all_refs
}
pub fn convert_graph_attributes(
node_proto: &NodeProto,
opset_version: usize,
parent_registry: Option<crate::graph_state::NameRegistry>,
base_path: Option<&Path>,
) -> Attributes {
use crate::ir::DeferredGraph;
use std::sync::Arc;
let mut result = Attributes::new();
let name_registry = parent_registry.unwrap_or_default();
for attr in &node_proto.attribute {
if let Ok(attr_type) = attr.type_.enum_value() {
match attr_type {
AttributeType::GRAPH => {
if let Some(graph_proto) = attr.g.as_ref() {
let deferred = DeferredGraph {
proto: Arc::new(graph_proto.clone()),
opset_version,
name_registry: Some(name_registry.clone()),
base_path: base_path.map(|p| p.to_path_buf()),
};
result.insert(attr.name.clone(), AttributeValue::DeferredGraph(deferred));
}
}
AttributeType::GRAPHS => {
let deferred_graphs: Vec<_> = attr
.graphs
.iter()
.map(|graph_proto| DeferredGraph {
proto: Arc::new(graph_proto.clone()),
opset_version,
name_registry: Some(name_registry.clone()),
base_path: base_path.map(|p| p.to_path_buf()),
})
.collect();
result.insert(
attr.name.clone(),
AttributeValue::DeferredGraphs(deferred_graphs),
);
}
_ => {}
}
}
}
result
}
impl TryFrom<ValueInfoProto> for Argument {
type Error = ParseError;
fn try_from(value: ValueInfoProto) -> Result<Argument, Self::Error> {
let name = sanitize_name(&value.name);
let proto_type = value
.type_
.as_ref()
.ok_or(ParseError::VariantNotFound("missing type".into()))?;
if !proto_type.has_tensor_type() {
return Err(ParseError::VariantNotFound(format!(
"Unsupported argument type: no tensor_type in {:?}",
proto_type
)));
}
let tensor_proto = proto_type.tensor_type();
let elem_type =
element_type_from_proto(tensor_proto.elem_type).map_err(ParseError::VariantNotFound)?;
let ty = if tensor_proto.shape.dim.is_empty() {
ArgType::Scalar(elem_type)
} else {
let has_unknown_dim = tensor_proto.shape.dim.iter().any(|dim| match &dim.value {
None | Some(Value::DimParam(_)) => true,
Some(Value::DimValue(_)) => false,
});
let static_shape = if has_unknown_dim {
None
} else {
let shape: Vec<usize> = tensor_proto
.shape
.dim
.iter()
.filter_map(|d| {
if let Some(Value::DimValue(v)) = &d.value {
Some(*v as usize)
} else {
None
}
})
.collect();
Some(shape)
};
ArgType::Tensor(TensorType {
rank: tensor_proto.shape.dim.len(),
dtype: elem_type,
static_shape,
})
};
Ok(Argument {
ty,
name,
value_source: crate::ir::ValueSource::Dynamic, value_store: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitize_name_basic() {
assert_eq!(sanitize_name("valid_name"), "valid_name");
assert_eq!(sanitize_name("_underscore"), "_underscore");
assert_eq!(sanitize_name("a"), "a");
assert_eq!(sanitize_name("ValidName123"), "valid_name123");
assert_eq!(sanitize_name("MyVariable"), "my_variable");
assert_eq!(sanitize_name("HTTPResponse"), "httpresponse");
}
#[test]
fn test_sanitize_name_special_chars() {
assert_eq!(sanitize_name("input:0"), "input_0");
assert_eq!(sanitize_name("layer/weight"), "layer_weight");
assert_eq!(sanitize_name("jax2tf/model:0"), "jax2tf_model_0");
assert_eq!(sanitize_name("bert.encoder.layer"), "bert_encoder_layer");
assert_eq!(sanitize_name("layer-norm"), "layer_norm");
assert_eq!(
sanitize_name("jax2tf_rhs_/pjit_silu_/Const_2:0"),
"jax2tf_rhs_pjit_silu_const_2_0"
);
}
#[test]
fn test_sanitize_name_camel_to_snake() {
assert_eq!(
sanitize_name("onnx__GlobalAveragePool_0"),
"onnx_global_average_pool_0"
);
assert_eq!(sanitize_name("onnx__Gemm_0"), "onnx_gemm_0");
assert_eq!(sanitize_name("onnx__Greater_0"), "onnx_greater_0");
assert_eq!(sanitize_name("MyClassName"), "my_class_name");
assert_eq!(sanitize_name("HTTPSConnection"), "httpsconnection");
}
#[test]
fn test_sanitize_name_starts_with_digit() {
assert_eq!(sanitize_name("123tensor"), "_123tensor");
assert_eq!(sanitize_name("0input"), "_0input");
}
#[test]
fn test_sanitize_name_unicode() {
assert_eq!(sanitize_name("tensor™"), "tensor");
assert_eq!(sanitize_name("input€output"), "input_output");
}
#[test]
fn test_sanitize_name_empty_and_edge_cases() {
assert_eq!(sanitize_name(""), "");
assert_eq!(sanitize_name("_"), "_");
assert_eq!(sanitize_name("___"), "_");
assert_eq!(sanitize_name("a__b"), "a_b");
assert_eq!(sanitize_name(":/:"), "_");
assert_eq!(sanitize_name("a:::b"), "a_b");
assert_eq!(sanitize_name("name_:"), "name");
}
}