use std::io::Read;
use std::{fmt, fs::File, path::Path};
use protobuf::Message;
use crate::{ir::OnnxGraph, processor::ProcessError, protos::ModelProto};
use super::phases::{
finalization, initialization, node_conversion, post_processing, type_inference,
};
#[derive(Debug)]
pub enum Error {
Io { path: String, error: std::io::Error },
InvalidFormat { path: Option<String>, error: String },
InvalidGraphStructure { reason: String },
MissingOpsetVersion,
TypeInference(ProcessError),
Processing(ProcessError),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Io { path, error } => {
write!(f, "Failed to open ONNX file '{}': {}", path, error)
}
Error::InvalidFormat { path, error } => {
if let Some(p) = path {
write!(f, "Invalid ONNX format in '{}': {}", p, error)
} else {
write!(f, "Invalid ONNX format: {}", error)
}
}
Error::InvalidGraphStructure { reason } => {
write!(f, "Invalid ONNX graph structure: {}", reason)
}
Error::MissingOpsetVersion => {
write!(
f,
"ONNX model must specify opset version for default domain"
)
}
Error::TypeInference(e) => {
write!(f, "Type inference failed: {e}")
}
Error::Processing(e) => {
write!(f, "Processing error: {e}")
}
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io { error, .. } => Some(error),
_ => None,
}
}
}
impl From<ProcessError> for Error {
fn from(error: ProcessError) -> Self {
Error::Processing(error)
}
}
#[derive(Debug, Clone)]
pub struct OnnxGraphBuilder {
simplify: bool,
}
impl Default for OnnxGraphBuilder {
fn default() -> Self {
Self { simplify: true }
}
}
impl OnnxGraphBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn simplify(mut self, simplify: bool) -> Self {
self.simplify = simplify;
self
}
pub fn parse_file(self, path: impl AsRef<Path>) -> Result<OnnxGraph, Error> {
let path = path.as_ref();
log::info!("Parsing ONNX file: {}", path.display());
#[cfg(feature = "mmap")]
let buffer = {
let file = File::open(path).map_err(|error| Error::Io {
path: path.display().to_string(),
error,
})?;
let mmap = unsafe { memmap2::Mmap::map(&file) }.map_err(|error| Error::Io {
path: path.display().to_string(),
error,
})?;
log::debug!("Memory-mapped ONNX file ({} bytes)", mmap.len());
bytes::Bytes::from_owner(mmap)
};
#[cfg(not(feature = "mmap"))]
let buffer = {
let mut file = File::open(path).map_err(|error| Error::Io {
path: path.display().to_string(),
error,
})?;
let mut buf = Vec::new();
file.read_to_end(&mut buf).map_err(|error| Error::Io {
path: path.display().to_string(),
error,
})?;
log::debug!("Read ONNX file into memory ({} bytes)", buf.len());
bytes::Bytes::from(buf)
};
self.parse_buffer(buffer, Some(path))
}
pub fn parse_bytes(self, data: &[u8]) -> Result<OnnxGraph, Error> {
let buffer = bytes::Bytes::copy_from_slice(data);
self.parse_buffer(buffer, None)
}
pub fn parse_reader<R: Read>(self, mut reader: R) -> Result<OnnxGraph, Error> {
let mut buf = Vec::new();
reader.read_to_end(&mut buf).map_err(|error| Error::Io {
path: "<reader>".to_string(),
error,
})?;
log::debug!("Read ONNX from reader ({} bytes)", buf.len());
let buffer = bytes::Bytes::from(buf);
self.parse_buffer(buffer, None)
}
fn parse_buffer(
self,
buffer: bytes::Bytes,
source_path: Option<&Path>,
) -> Result<OnnxGraph, Error> {
let path_str = source_path.map(|p| p.display().to_string());
let base_path = source_path.and_then(|p| p.parent());
let model: ModelProto =
Message::parse_from_tokio_bytes(&buffer).map_err(|e| Error::InvalidFormat {
path: path_str.clone(),
error: e.to_string(),
})?;
if !model.graph.node.is_top_sorted() {
return Err(Error::InvalidGraphStructure {
reason: "Nodes are not topologically sorted (ONNX spec violation)".to_string(),
});
}
let graph = build_graph_with_options(&model, base_path, self.simplify)?;
if let Some(path) = path_str {
log::info!("Finished parsing ONNX file: {}", path);
} else {
log::info!("Finished parsing ONNX from bytes");
}
Ok(graph)
}
}
fn build_graph_with_options(
model: &ModelProto,
base_path: Option<&Path>,
simplify: bool,
) -> Result<OnnxGraph, Error> {
let opset_version = extract_opset_version(model)?;
let graph_builder =
build_graph_builder_from_proto(&model.graph, opset_version, None, base_path, simplify)?;
log::debug!(" PHASE 6: Node Conversion (RawNode -> Node) ");
Ok(graph_builder.convert_to_graph(opset_version))
}
pub(crate) fn build_graph_builder_from_proto(
graph: &crate::protos::GraphProto,
opset_version: usize,
name_registry: Option<crate::graph_state::NameRegistry>,
base_path: Option<&Path>,
simplify: bool,
) -> Result<crate::ir::OnnxGraphBuilder, Error> {
build_graph_builder_from_proto_with_outer_scope(
graph,
opset_version,
name_registry,
crate::ir::OuterScopeTypes::new(),
base_path,
simplify,
)
}
pub(crate) fn build_graph_builder_from_proto_with_outer_scope(
graph: &crate::protos::GraphProto,
opset_version: usize,
name_registry: Option<crate::graph_state::NameRegistry>,
outer_scope: crate::ir::OuterScopeTypes,
base_path: Option<&Path>,
simplify: bool,
) -> Result<crate::ir::OnnxGraphBuilder, Error> {
log::debug!(" PHASE 1: Initialization ");
let state_rc = initialization::initialize_from_graph_with_registry_and_outer_scope(
graph,
name_registry,
outer_scope,
base_path,
);
log::debug!(" PHASE 2: Node Conversion (Proto -> RawNode) ");
node_conversion::convert_nodes_from_graph(graph, &state_rc, opset_version)?;
log::debug!(" PHASE 2b: Early Constant Folding ");
{
let mut state = state_rc.borrow_mut();
let mut nodes = std::mem::take(&mut state.processed_nodes);
drop(state);
let max_iterations = 10;
let mut converged = false;
for _ in 0..max_iterations {
let const_count_before = nodes
.iter()
.filter(|n| n.node_type == crate::ir::NodeType::Constant)
.count();
nodes = crate::simplify::constant_fold::fold_weight_rearrangements(nodes, &state_rc);
let const_count_after = nodes
.iter()
.filter(|n| n.node_type == crate::ir::NodeType::Constant)
.count();
if const_count_after == const_count_before {
converged = true;
break;
}
}
if !converged {
log::debug!(
"Early constant folding: reached max iterations ({max_iterations}) without converging"
);
}
let mut state = state_rc.borrow_mut();
state.processed_nodes = nodes;
}
log::debug!(" PHASE 3: Type Inference ");
type_inference::infer_types(&state_rc, opset_version).map_err(Error::TypeInference)?;
log::debug!(" PHASE 4: Post-processing ");
let (nodes, inputs, outputs) = post_processing::post_process(&state_rc, simplify);
let (mut nodes, inputs, mut outputs) = if simplify {
log::debug!(" PHASE 4b: Simplification ");
crate::simplify::simplify_graph(nodes, inputs, outputs, &state_rc)
} else {
(nodes, inputs, outputs)
};
log::debug!(" PHASE 5: Finalization ");
Ok(finalization::finalize(
&mut nodes,
inputs,
&mut outputs,
state_rc,
))
}
fn extract_opset_version(model: &ModelProto) -> Result<usize, Error> {
model
.opset_import
.iter()
.find(|opset| opset.domain.is_empty())
.map(|opset| opset.version as usize)
.ok_or(Error::MissingOpsetVersion)
}
pub(crate) trait TopologicalSortable {
fn is_top_sorted(&self) -> bool;
}
use crate::protos::NodeProto;
impl TopologicalSortable for Vec<NodeProto> {
fn is_top_sorted(&self) -> bool {
for (node_position, node) in self.iter().enumerate() {
for output in &node.output {
if output.is_empty() {
continue;
}
for (other_node_position, other_node) in self.iter().enumerate() {
if other_node.input.contains(output) {
if node_position > other_node_position {
return false;
}
}
}
}
}
true
}
}