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, Default)]
pub struct OnnxGraphBuilder {
}
impl OnnxGraphBuilder {
pub fn new() -> Self {
Self::default()
}
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_base_path(&model, base_path)?;
if let Some(path) = path_str {
log::info!("Finished parsing ONNX file: {}", path);
} else {
log::info!("Finished parsing ONNX from bytes");
}
Ok(graph)
}
}
pub fn build_graph_with_base_path(
model: &ModelProto,
base_path: Option<&Path>,
) -> Result<OnnxGraph, Error> {
let opset_version = extract_opset_version(model)?;
build_graph_from_proto_with_base_path(&model.graph, opset_version, base_path)
}
pub fn build_graph_from_proto_with_base_path(
graph: &crate::protos::GraphProto,
opset_version: usize,
base_path: Option<&Path>,
) -> Result<OnnxGraph, Error> {
let graph_builder = build_graph_builder_from_proto(graph, opset_version, None, base_path)?;
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>,
) -> Result<crate::ir::OnnxGraphBuilder, Error> {
build_graph_builder_from_proto_with_outer_scope(
graph,
opset_version,
name_registry,
crate::ir::OuterScopeTypes::new(),
base_path,
)
}
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>,
) -> 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 3: Type Inference ");
type_inference::infer_types(&state_rc, opset_version).map_err(Error::TypeInference)?;
log::debug!(" PHASE 4: Post-processing ");
let (mut nodes, inputs, mut outputs) = post_processing::post_process(&state_rc);
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
}
}