use std::path::Path;
use std::sync::Arc;
use onnx_runtime_ir::Graph;
use onnx_runtime_loader::proto::{
ModelProto, decode_model,
onnx::{ValueInfoProto, type_proto},
};
use onnx_runtime_loader::{
Model as EncoderModel, ModelMetadata, WeightStore, encode_model_proto,
load_model_bytes_with_weights,
};
use prost::Message;
use crate::check::{OnnxChecker, ValidationResult};
use crate::error::{Error, Result};
use crate::text::{self, PrintOptions};
pub use onnx_runtime_loader::proto::onnx::{
DeviceConfigurationProto, IntIntListEntryProto, NodeDeviceConfigurationProto, ShardedDimProto,
ShardingSpecProto, SimpleShardedDimProto, simple_sharded_dim_proto,
};
pub type OpaqueProto = onnx_runtime_loader::proto::onnx::type_proto::Opaque;
pub struct Model {
pub graph: Graph,
pub metadata: ModelMetadata,
weights: Option<Arc<WeightStore>>,
source_proto: Option<ModelProto>,
}
impl Model {
pub fn new(graph: Graph) -> Self {
Self {
graph,
metadata: ModelMetadata::default(),
weights: None,
source_proto: None,
}
}
pub fn with_metadata(graph: Graph, metadata: ModelMetadata) -> Self {
Self {
graph,
metadata,
weights: None,
source_proto: None,
}
}
pub fn set_weights(&mut self, weights: Arc<WeightStore>) {
self.weights = Some(weights);
}
pub fn weights(&self) -> Option<&Arc<WeightStore>> {
self.weights.as_ref()
}
pub fn from_proto(proto: ModelProto) -> Result<Self> {
let metadata = metadata_from_proto(&proto);
let bytes = proto.encode_to_vec();
let loaded = load_model_bytes_with_weights(&bytes, ".").or_else(|_| {
let projection = execution_projection(&proto);
load_model_bytes_with_weights(&projection.encode_to_vec(), ".")
})?;
Ok(Self {
graph: loaded.0,
metadata,
weights: Some(loaded.1),
source_proto: Some(proto),
})
}
pub fn to_proto(&self) -> Result<ModelProto> {
if let Some(proto) = &self.source_proto {
return Ok(proto.clone());
}
let mut encoder = EncoderModel::new(&self.graph).with_metadata(self.metadata.clone());
if let Some(weights) = self.weights() {
encoder = encoder.with_weights(weights);
}
Ok(encode_model_proto(&encoder)?)
}
pub fn make_graph_authoritative(&mut self) {
self.source_proto = None;
}
pub(crate) fn retained_proto(&self) -> Option<&ModelProto> {
self.source_proto.as_ref()
}
pub fn to_text(&self) -> String {
text::to_text(self)
}
pub fn to_text_with(&self, opts: &PrintOptions) -> String {
text::to_text_with(self, opts)
}
pub fn from_text(source: &str) -> Result<Self> {
text::from_text(source)
}
pub fn validate(&self) -> ValidationResult {
OnnxChecker::new().check(self)
}
}
pub fn load_model(path: impl AsRef<Path>) -> Result<Model> {
let path = path.as_ref();
let bytes = std::fs::read(path).map_err(|source| Error::Read {
path: path.to_path_buf(),
source,
})?;
let proto = decode_model(&bytes)?;
let metadata = metadata_from_proto(&proto);
let model_dir = path.parent().unwrap_or_else(|| Path::new("."));
let (graph, store) = load_model_bytes_with_weights(&bytes, model_dir).or_else(|_| {
load_model_bytes_with_weights(&execution_projection(&proto).encode_to_vec(), model_dir)
})?;
Ok(Model {
graph,
metadata,
weights: Some(store),
source_proto: Some(proto),
})
}
pub fn save_model(model: &Model, path: impl AsRef<Path>) -> Result<()> {
let bytes = model.to_proto()?.encode_to_vec();
let path = path.as_ref();
std::fs::write(path, bytes).map_err(|source| Error::Write {
path: path.to_path_buf(),
source,
})?;
Ok(())
}
fn execution_projection(proto: &ModelProto) -> ModelProto {
let mut projection = proto.clone();
if let Some(graph) = &mut projection.graph {
for sparse in &graph.sparse_initializer {
let Some(values) = &sparse.values else {
continue;
};
if values.name.is_empty() || graph.input.iter().any(|input| input.name == values.name) {
continue;
}
graph.input.push(ValueInfoProto {
name: values.name.clone(),
r#type: Some(onnx_runtime_loader::proto::onnx::TypeProto {
value: Some(type_proto::Value::SparseTensorType(
type_proto::SparseTensor {
elem_type: values.data_type,
shape: Some(onnx_runtime_loader::proto::onnx::TensorShapeProto {
dim: sparse
.dims
.iter()
.map(|&dim| {
onnx_runtime_loader::proto::onnx::tensor_shape_proto::Dimension {
value: Some(
onnx_runtime_loader::proto::onnx::tensor_shape_proto::dimension::Value::DimValue(dim),
),
denotation: String::new(),
}
})
.collect(),
}),
},
)),
denotation: String::new(),
}),
..Default::default()
});
}
}
projection
}
fn metadata_from_proto(proto: &ModelProto) -> ModelMetadata {
ModelMetadata {
ir_version: proto.ir_version,
producer_name: proto.producer_name.clone(),
producer_version: proto.producer_version.clone(),
domain: proto.domain.clone(),
model_version: proto.model_version,
doc_string: if proto.doc_string.is_empty() {
None
} else {
Some(proto.doc_string.clone())
},
graph_name: proto
.graph
.as_ref()
.map(|g| g.name.clone())
.unwrap_or_default(),
metadata_props: proto
.metadata_props
.iter()
.map(|entry| (entry.key.clone(), entry.value.clone()))
.collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use onnx_runtime_ir::{DataType, Node, NodeId, static_shape};
fn add_graph() -> Graph {
let mut g = Graph::new();
g.opset_imports.insert(String::new(), 21);
let x = g.create_named_value("X", DataType::Float32, static_shape([2, 3]));
let y = g.create_named_value("Y", DataType::Float32, static_shape([2, 3]));
let z = g.create_named_value("Z", DataType::Float32, static_shape([2, 3]));
g.add_input(x);
g.add_input(y);
let mut node = Node::new(NodeId(0), "Add", vec![Some(x), Some(y)], vec![z]);
node.name = "add0".to_string();
g.insert_node(node);
g.add_output(z);
g
}
#[test]
fn new_wraps_graph_with_default_metadata() {
let model = Model::new(add_graph());
assert_eq!(model.metadata, ModelMetadata::default());
assert_eq!(model.graph.num_nodes(), 1);
}
#[test]
fn save_then_load_round_trips_structure_and_metadata() {
let meta = ModelMetadata {
producer_name: "onnx-std-test".to_string(),
graph_name: "g".to_string(),
metadata_props: vec![("author".to_string(), "deckard".to_string())],
..Default::default()
};
let model = Model::with_metadata(add_graph(), meta.clone());
let dir = std::env::current_dir().unwrap().join("target");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("onnx_std_roundtrip_test.onnx");
save_model(&model, &path).unwrap();
let loaded = load_model(&path).unwrap();
assert_eq!(loaded.graph.num_nodes(), 1);
assert_eq!(loaded.metadata.producer_name, "onnx-std-test");
assert_eq!(loaded.metadata.graph_name, "g");
assert_eq!(
loaded.metadata.metadata_props,
vec![("author".to_string(), "deckard".to_string())]
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn load_missing_file_is_read_error() {
let result = load_model("definitely-not-a-real-file.onnx");
assert!(matches!(result, Err(Error::Read { .. })));
}
}