1use std::path::Path;
16use std::sync::Arc;
17
18use onnx_runtime_ir::Graph;
19use onnx_runtime_loader::proto::{
20 ModelProto, decode_model,
21 onnx::{ValueInfoProto, type_proto},
22};
23use onnx_runtime_loader::{
24 Model as EncoderModel, ModelMetadata, WeightStore, encode_model_proto,
25 load_model_bytes_with_weights,
26};
27use prost::Message;
28
29use crate::check::{OnnxChecker, ValidationResult};
30use crate::error::{Error, Result};
31use crate::text::{self, PrintOptions};
32
33pub use onnx_runtime_loader::proto::onnx::{
39 DeviceConfigurationProto, IntIntListEntryProto, NodeDeviceConfigurationProto, ShardedDimProto,
40 ShardingSpecProto, SimpleShardedDimProto, simple_sharded_dim_proto,
41};
42
43pub type OpaqueProto = onnx_runtime_loader::proto::onnx::type_proto::Opaque;
45
46pub struct Model {
53 pub graph: Graph,
55 pub metadata: ModelMetadata,
57 weights: Option<Arc<WeightStore>>,
61 source_proto: Option<ModelProto>,
66}
67
68impl Model {
69 pub fn new(graph: Graph) -> Self {
74 Self {
75 graph,
76 metadata: ModelMetadata::default(),
77 weights: None,
78 source_proto: None,
79 }
80 }
81
82 pub fn with_metadata(graph: Graph, metadata: ModelMetadata) -> Self {
84 Self {
85 graph,
86 metadata,
87 weights: None,
88 source_proto: None,
89 }
90 }
91
92 pub fn set_weights(&mut self, weights: Arc<WeightStore>) {
94 self.weights = Some(weights);
95 }
96
97 pub fn weights(&self) -> Option<&Arc<WeightStore>> {
99 self.weights.as_ref()
100 }
101
102 pub fn from_proto(proto: ModelProto) -> Result<Self> {
107 let metadata = metadata_from_proto(&proto);
108 let bytes = proto.encode_to_vec();
109 let loaded = load_model_bytes_with_weights(&bytes, ".").or_else(|_| {
110 let projection = execution_projection(&proto);
111 load_model_bytes_with_weights(&projection.encode_to_vec(), ".")
112 })?;
113 Ok(Self {
114 graph: loaded.0,
115 metadata,
116 weights: Some(loaded.1),
117 source_proto: Some(proto),
118 })
119 }
120
121 pub fn to_proto(&self) -> Result<ModelProto> {
127 if let Some(proto) = &self.source_proto {
128 return Ok(proto.clone());
129 }
130 let mut encoder = EncoderModel::new(&self.graph).with_metadata(self.metadata.clone());
131 if let Some(weights) = self.weights() {
132 encoder = encoder.with_weights(weights);
133 }
134 Ok(encode_model_proto(&encoder)?)
135 }
136
137 pub fn make_graph_authoritative(&mut self) {
144 self.source_proto = None;
145 }
146
147 pub(crate) fn retained_proto(&self) -> Option<&ModelProto> {
148 self.source_proto.as_ref()
149 }
150
151 pub fn to_text(&self) -> String {
153 text::to_text(self)
154 }
155
156 pub fn to_text_with(&self, opts: &PrintOptions) -> String {
158 text::to_text_with(self, opts)
159 }
160
161 pub fn from_text(source: &str) -> Result<Self> {
163 text::from_text(source)
164 }
165
166 pub fn validate(&self) -> ValidationResult {
168 OnnxChecker::new().check(self)
169 }
170}
171
172pub fn load_model(path: impl AsRef<Path>) -> Result<Model> {
181 let path = path.as_ref();
182 let bytes = std::fs::read(path).map_err(|source| Error::Read {
183 path: path.to_path_buf(),
184 source,
185 })?;
186 let proto = decode_model(&bytes)?;
187 let metadata = metadata_from_proto(&proto);
188 let model_dir = path.parent().unwrap_or_else(|| Path::new("."));
189 let (graph, store) = load_model_bytes_with_weights(&bytes, model_dir).or_else(|_| {
190 load_model_bytes_with_weights(&execution_projection(&proto).encode_to_vec(), model_dir)
191 })?;
192 Ok(Model {
193 graph,
194 metadata,
195 weights: Some(store),
196 source_proto: Some(proto),
197 })
198}
199
200pub fn save_model(model: &Model, path: impl AsRef<Path>) -> Result<()> {
206 let bytes = model.to_proto()?.encode_to_vec();
207 let path = path.as_ref();
208 std::fs::write(path, bytes).map_err(|source| Error::Write {
209 path: path.to_path_buf(),
210 source,
211 })?;
212 Ok(())
213}
214
215fn execution_projection(proto: &ModelProto) -> ModelProto {
222 let mut projection = proto.clone();
223 if let Some(graph) = &mut projection.graph {
224 for sparse in &graph.sparse_initializer {
225 let Some(values) = &sparse.values else {
226 continue;
227 };
228 if values.name.is_empty() || graph.input.iter().any(|input| input.name == values.name) {
229 continue;
230 }
231 graph.input.push(ValueInfoProto {
232 name: values.name.clone(),
233 r#type: Some(onnx_runtime_loader::proto::onnx::TypeProto {
234 value: Some(type_proto::Value::SparseTensorType(
235 type_proto::SparseTensor {
236 elem_type: values.data_type,
237 shape: Some(onnx_runtime_loader::proto::onnx::TensorShapeProto {
238 dim: sparse
239 .dims
240 .iter()
241 .map(|&dim| {
242 onnx_runtime_loader::proto::onnx::tensor_shape_proto::Dimension {
243 value: Some(
244 onnx_runtime_loader::proto::onnx::tensor_shape_proto::dimension::Value::DimValue(dim),
245 ),
246 denotation: String::new(),
247 }
248 })
249 .collect(),
250 }),
251 },
252 )),
253 denotation: String::new(),
254 }),
255 ..Default::default()
256 });
257 }
258 }
259 projection
260}
261
262fn metadata_from_proto(proto: &ModelProto) -> ModelMetadata {
267 ModelMetadata {
268 ir_version: proto.ir_version,
269 producer_name: proto.producer_name.clone(),
270 producer_version: proto.producer_version.clone(),
271 domain: proto.domain.clone(),
272 model_version: proto.model_version,
273 doc_string: if proto.doc_string.is_empty() {
274 None
275 } else {
276 Some(proto.doc_string.clone())
277 },
278 graph_name: proto
279 .graph
280 .as_ref()
281 .map(|g| g.name.clone())
282 .unwrap_or_default(),
283 metadata_props: proto
284 .metadata_props
285 .iter()
286 .map(|entry| (entry.key.clone(), entry.value.clone()))
287 .collect(),
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use onnx_runtime_ir::{DataType, Node, NodeId, static_shape};
295
296 fn add_graph() -> Graph {
298 let mut g = Graph::new();
299 g.opset_imports.insert(String::new(), 21);
300 let x = g.create_named_value("X", DataType::Float32, static_shape([2, 3]));
301 let y = g.create_named_value("Y", DataType::Float32, static_shape([2, 3]));
302 let z = g.create_named_value("Z", DataType::Float32, static_shape([2, 3]));
303 g.add_input(x);
304 g.add_input(y);
305 let mut node = Node::new(NodeId(0), "Add", vec![Some(x), Some(y)], vec![z]);
307 node.name = "add0".to_string();
308 g.insert_node(node);
309 g.add_output(z);
310 g
311 }
312
313 #[test]
314 fn new_wraps_graph_with_default_metadata() {
315 let model = Model::new(add_graph());
316 assert_eq!(model.metadata, ModelMetadata::default());
317 assert_eq!(model.graph.num_nodes(), 1);
318 }
319
320 #[test]
321 fn save_then_load_round_trips_structure_and_metadata() {
322 let meta = ModelMetadata {
323 producer_name: "onnx-std-test".to_string(),
324 graph_name: "g".to_string(),
325 metadata_props: vec![("author".to_string(), "deckard".to_string())],
326 ..Default::default()
327 };
328 let model = Model::with_metadata(add_graph(), meta.clone());
329
330 let dir = std::env::current_dir().unwrap().join("target");
331 std::fs::create_dir_all(&dir).unwrap();
332 let path = dir.join("onnx_std_roundtrip_test.onnx");
333 save_model(&model, &path).unwrap();
334
335 let loaded = load_model(&path).unwrap();
336 assert_eq!(loaded.graph.num_nodes(), 1);
337 assert_eq!(loaded.metadata.producer_name, "onnx-std-test");
338 assert_eq!(loaded.metadata.graph_name, "g");
339 assert_eq!(
340 loaded.metadata.metadata_props,
341 vec![("author".to_string(), "deckard".to_string())]
342 );
343
344 let _ = std::fs::remove_file(&path);
345 }
346
347 #[test]
348 fn load_missing_file_is_read_error() {
349 let result = load_model("definitely-not-a-real-file.onnx");
350 assert!(matches!(result, Err(Error::Read { .. })));
351 }
352}