1use std::collections::HashSet;
49use std::path::Path;
50
51use prost::Message;
52
53use onnx_runtime_ir::{
54 Attribute, DataType, Dim, Graph, Node, Shape, TensorData, TypeProto, ValueId, WeightRef,
55};
56
57use crate::LoaderError;
58use crate::proto::onnx::{
59 self, AttributeProto, GraphProto, ModelProto, NodeProto, OperatorSetIdProto,
60 StringStringEntryProto, TensorProto, TensorShapeProto, ValueInfoProto,
61 attribute_proto::AttributeType, tensor_shape_proto, type_proto,
62};
63use crate::weights::WeightStore;
64
65pub const DEFAULT_IR_VERSION: i64 = 11;
69
70pub const DEFAULT_OPSET_VERSION: i64 = 24;
74
75#[derive(Clone, Debug, PartialEq, Eq)]
81pub struct ModelMetadata {
82 pub ir_version: i64,
84 pub producer_name: String,
86 pub producer_version: String,
88 pub domain: String,
90 pub model_version: i64,
92 pub doc_string: Option<String>,
94 pub graph_name: String,
96 pub metadata_props: Vec<(String, String)>,
98}
99
100impl Default for ModelMetadata {
101 fn default() -> Self {
102 Self {
103 ir_version: DEFAULT_IR_VERSION,
104 producer_name: String::new(),
105 producer_version: String::new(),
106 domain: String::new(),
107 model_version: 0,
108 doc_string: None,
109 graph_name: String::new(),
110 metadata_props: Vec::new(),
111 }
112 }
113}
114
115pub struct Model<'a> {
123 pub graph: &'a Graph,
125 pub metadata: ModelMetadata,
127 pub weights: Option<&'a WeightStore>,
129}
130
131impl<'a> Model<'a> {
132 pub fn new(graph: &'a Graph) -> Self {
141 Self {
142 graph,
143 metadata: ModelMetadata::default(),
144 weights: None,
145 }
146 }
147
148 pub fn with_metadata(mut self, metadata: ModelMetadata) -> Self {
150 self.metadata = metadata;
151 self
152 }
153
154 pub fn with_weights(mut self, weights: &'a WeightStore) -> Self {
156 self.weights = Some(weights);
157 self
158 }
159}
160
161fn reject_unrepresentable_node_versions(graph: &Graph) -> Result<(), LoaderError> {
173 for node in graph.nodes.values() {
174 let Some(node_version) = node.version else {
175 continue;
176 };
177 let graph_version = graph
178 .opset_imports
179 .get(node.domain.as_str())
180 .copied()
181 .unwrap_or(0);
182 if u64::try_from(node_version).is_ok_and(|version| version == graph_version) {
183 continue;
184 }
185 return Err(LoaderError::NodeVersionNotRepresentable {
186 node: if node.name.is_empty() {
187 format!("#{}", node.id.0)
188 } else {
189 node.name.clone()
190 },
191 op_type: node.op_type.clone(),
192 domain: node.domain.clone(),
193 node_version,
194 graph_version,
195 });
196 }
197 Ok(())
198}
199
200pub fn encode_model(model: &Model) -> Result<Vec<u8>, LoaderError> {
201 Ok(encode_model_proto(model)?.encode_to_vec())
202}
203
204pub fn write_model(model: &Model, path: impl AsRef<Path>) -> Result<(), LoaderError> {
206 let bytes = encode_model(model)?;
207 let path = path.as_ref();
208 std::fs::write(path, bytes).map_err(|source| LoaderError::Io {
209 path: path.to_path_buf(),
210 source,
211 })
212}
213
214pub fn encode_model_proto(model: &Model) -> Result<ModelProto, LoaderError> {
218 reject_unrepresentable_node_versions(model.graph)?;
219 let meta = &model.metadata;
220 let graph = encode_graph_proto(model.graph, model.weights, true, &meta.graph_name)?;
221
222 let mut opset_import: Vec<OperatorSetIdProto> = model
224 .graph
225 .opset_imports
226 .iter()
227 .map(|(domain, &version)| OperatorSetIdProto {
228 domain: domain.clone(),
229 version: version as i64,
230 })
231 .collect();
232 if meta.ir_version >= 3
233 && !opset_import
234 .iter()
235 .any(|opset| opset.domain.is_empty() || opset.domain == "ai.onnx")
236 {
237 opset_import.push(OperatorSetIdProto {
239 domain: String::new(),
240 version: DEFAULT_OPSET_VERSION,
241 });
242 }
243 opset_import.sort_by(|a, b| a.domain.cmp(&b.domain));
244
245 let metadata_props = meta
246 .metadata_props
247 .iter()
248 .map(|(key, value)| StringStringEntryProto {
249 key: key.clone(),
250 value: value.clone(),
251 })
252 .collect();
253
254 Ok(ModelProto {
255 ir_version: meta.ir_version,
256 opset_import,
257 producer_name: meta.producer_name.clone(),
258 producer_version: meta.producer_version.clone(),
259 domain: meta.domain.clone(),
260 model_version: meta.model_version,
261 doc_string: meta.doc_string.clone().unwrap_or_default(),
262 graph: Some(graph),
263 metadata_props,
264 ..Default::default()
265 })
266}
267
268fn encode_graph_proto(
271 graph: &Graph,
272 weights: Option<&WeightStore>,
273 _is_top_level: bool,
274 name: &str,
275) -> Result<GraphProto, LoaderError> {
276 let mut init_ids: Vec<ValueId> = graph.initializers.keys().copied().collect();
278 init_ids.sort_by_key(|v| v.0);
279 let mut initializer = Vec::with_capacity(init_ids.len());
280 for vid in &init_ids {
281 let weight = &graph.initializers[vid];
282 let iname = value_name(graph, *vid).unwrap_or_default().to_string();
283 initializer.push(encode_weight(iname, weight, weights)?);
284 }
285
286 let input: Vec<ValueInfoProto> = graph
288 .inputs
289 .iter()
290 .map(|&vid| encode_value_info(graph, vid))
291 .collect();
292 let output: Vec<ValueInfoProto> = graph
293 .outputs
294 .iter()
295 .map(|&vid| encode_value_info(graph, vid))
296 .collect();
297
298 let mut excluded: HashSet<ValueId> = HashSet::new();
302 excluded.extend(graph.inputs.iter().copied());
303 excluded.extend(graph.outputs.iter().copied());
304 excluded.extend(init_ids.iter().copied());
305 let mut value_info = Vec::new();
306 for (vid, value) in graph.values.iter() {
307 if excluded.contains(&vid) {
308 continue;
309 }
310 if value.name.as_deref().is_some_and(|n| !n.is_empty()) {
311 value_info.push(encode_value_info(graph, vid));
312 }
313 }
314
315 let mut node = Vec::with_capacity(graph.num_nodes());
317 for (_, n) in graph.nodes.iter() {
318 node.push(encode_node(graph, n, weights)?);
319 }
320
321 Ok(GraphProto {
322 node,
323 name: name.to_string(),
324 initializer,
325 input,
326 output,
327 value_info,
328 ..Default::default()
329 })
330}
331
332fn encode_node(
334 graph: &Graph,
335 node: &Node,
336 weights: Option<&WeightStore>,
337) -> Result<NodeProto, LoaderError> {
338 let input: Vec<String> = node
339 .inputs
340 .iter()
341 .map(|slot| match slot {
342 Some(vid) => value_name(graph, *vid).unwrap_or_default().to_string(),
343 None => String::new(),
344 })
345 .collect();
346 let output: Vec<String> = node
347 .outputs
348 .iter()
349 .map(|&vid| value_name(graph, vid).unwrap_or_default().to_string())
350 .collect();
351
352 let mut keys: Vec<&String> = node.attributes.keys().collect();
355 keys.sort();
356 let mut attribute = Vec::with_capacity(keys.len());
357 for key in keys {
358 attribute.push(encode_attribute(
359 graph,
360 node,
361 key,
362 &node.attributes[key],
363 weights,
364 )?);
365 }
366
367 Ok(NodeProto {
368 input,
369 output,
370 name: node.name.clone(),
371 op_type: node.op_type.clone(),
372 domain: node.domain.clone(),
373 attribute,
374 doc_string: node.doc_string.clone().unwrap_or_default(),
375 ..Default::default()
376 })
377}
378
379fn encode_attribute(
382 graph: &Graph,
383 node: &Node,
384 name: &str,
385 attr: &Attribute,
386 weights: Option<&WeightStore>,
387) -> Result<AttributeProto, LoaderError> {
388 let mut ap = AttributeProto {
389 name: name.to_string(),
390 ..Default::default()
391 };
392 match attr {
393 Attribute::Int(v) => {
394 ap.i = *v;
395 ap.r#type = AttributeType::Int as i32;
396 }
397 Attribute::Float(v) => {
398 ap.f = *v;
399 ap.r#type = AttributeType::Float as i32;
400 }
401 Attribute::String(s) => {
402 ap.s = s.clone();
403 ap.r#type = AttributeType::String as i32;
404 }
405 Attribute::Ints(v) => {
406 ap.ints = v.clone();
407 ap.r#type = AttributeType::Ints as i32;
408 }
409 Attribute::Floats(v) => {
410 ap.floats = v.clone();
411 ap.r#type = AttributeType::Floats as i32;
412 }
413 Attribute::Strings(v) => {
414 ap.strings = v.clone();
415 ap.r#type = AttributeType::Strings as i32;
416 }
417 Attribute::Tensor(t) => {
418 ap.t = Some(encode_tensor(t));
419 ap.r#type = AttributeType::Tensor as i32;
420 }
421 Attribute::Tensors(tensors) => {
422 ap.tensors = tensors.iter().map(encode_tensor).collect();
423 ap.r#type = AttributeType::Tensors as i32;
424 }
425 Attribute::Graph(inline) => {
426 let subgraph = graph
427 .subgraphs
428 .get(&(node.id, name.to_string()))
429 .unwrap_or(inline);
430 ap.g = Some(encode_graph_proto(subgraph, weights, false, "")?);
431 ap.r#type = AttributeType::Graph as i32;
432 }
433 Attribute::Graphs(inline) => {
434 ap.graphs = inline
435 .iter()
436 .enumerate()
437 .map(|(index, fallback)| {
438 let key = (node.id, format!("{name}[{index}]"));
439 let subgraph = graph.subgraphs.get(&key).unwrap_or(fallback);
440 encode_graph_proto(subgraph, weights, false, "")
441 })
442 .collect::<Result<Vec<_>, _>>()?;
443 ap.r#type = AttributeType::Graphs as i32;
444 }
445 Attribute::TypeProto(tp) => {
446 ap.tp = Some(encode_type_proto(graph, tp));
447 ap.r#type = AttributeType::TypeProto as i32;
448 }
449 Attribute::TypeProtos(types) => {
450 ap.type_protos = types
451 .iter()
452 .map(|value| encode_type_proto(graph, value))
453 .collect();
454 ap.r#type = AttributeType::TypeProtos as i32;
455 }
456 Attribute::SparseTensor(tensor) => {
457 ap.sparse_tensor = Some(encode_sparse_tensor(tensor));
458 ap.r#type = AttributeType::SparseTensor as i32;
459 }
460 Attribute::SparseTensors(tensors) => {
461 ap.sparse_tensors = tensors.iter().map(encode_sparse_tensor).collect();
462 ap.r#type = AttributeType::SparseTensors as i32;
463 }
464 }
465 Ok(ap)
466}
467
468fn encode_tensor(t: &TensorData) -> TensorProto {
471 let mut tp = TensorProto {
472 dims: t.dims.iter().map(|&d| d as i64).collect(),
473 data_type: t.dtype.to_onnx(),
474 name: t.name.clone().unwrap_or_default(),
475 ..Default::default()
476 };
477 if t.dtype == DataType::String {
478 tp.string_data = t.strings.iter().map(|s| s.clone().into_bytes()).collect();
479 } else {
480 tp.raw_data = t.data.clone();
481 }
482
483 tp
484}
485
486fn encode_sparse_tensor(tensor: &onnx_runtime_ir::SparseTensorData) -> onnx::SparseTensorProto {
487 onnx::SparseTensorProto {
488 values: Some(encode_tensor(&tensor.values)),
489 indices: Some(encode_tensor(&tensor.indices)),
490 dims: tensor.dims.iter().map(|&dim| dim as i64).collect(),
491 }
492}
493
494fn encode_weight(
500 name: String,
501 weight: &WeightRef,
502 weights: Option<&WeightStore>,
503) -> Result<TensorProto, LoaderError> {
504 match weight {
505 WeightRef::Inline(t) => {
506 let mut tp = encode_tensor(t);
507 tp.name = name;
508 Ok(tp)
509 }
510 WeightRef::External { dtype, dims, .. } => {
511 if *dtype == DataType::String {
512 return Err(LoaderError::GraphBuild(format!(
513 "external initializer {name:?}: STRING external data is unsupported"
514 )));
515 }
516 let bytes = weights.and_then(|s| s.bytes(weight)).ok_or_else(|| {
517 LoaderError::GraphBuild(format!(
518 "external initializer {name:?}: weight bytes unavailable \
519 (attach a WeightStore via Model::with_weights)"
520 ))
521 })?;
522 Ok(TensorProto {
523 name,
524 data_type: dtype.to_onnx(),
525 dims: dims.iter().map(|&d| d as i64).collect(),
526 raw_data: bytes.to_vec(),
527 ..Default::default()
528 })
529 }
530 }
531}
532
533fn encode_value_info(graph: &Graph, vid: ValueId) -> ValueInfoProto {
535 let value = graph.value(vid);
536 ValueInfoProto {
537 name: value.name.clone().unwrap_or_default(),
538 r#type: Some(encode_tensor_type(graph, value.dtype, &value.shape)),
539 ..Default::default()
540 }
541}
542
543fn encode_tensor_type(graph: &Graph, dtype: DataType, shape: &Shape) -> onnx::TypeProto {
545 onnx::TypeProto {
546 value: Some(type_proto::Value::TensorType(type_proto::Tensor {
547 elem_type: dtype.to_onnx(),
548 shape: Some(encode_shape(graph, shape)),
549 })),
550 ..Default::default()
551 }
552}
553
554fn encode_shape(graph: &Graph, shape: &Shape) -> TensorShapeProto {
558 use tensor_shape_proto::{Dimension, dimension::Value as DV};
559 let dim = shape
560 .iter()
561 .map(|d| {
562 let value = match d {
563 Dim::Static(n) => Some(DV::DimValue(*n as i64)),
564 Dim::Symbolic(sym) => graph
565 .symbol_constraints
566 .get(sym)
567 .and_then(|c| c.name.clone())
568 .map(DV::DimParam),
569 };
570 Dimension {
571 value,
572 ..Default::default()
573 }
574 })
575 .collect();
576 TensorShapeProto { dim }
577}
578
579fn encode_type_proto(graph: &Graph, tp: &TypeProto) -> onnx::TypeProto {
581 let value = match tp {
582 TypeProto::Tensor { dtype, shape } => type_proto::Value::TensorType(type_proto::Tensor {
583 elem_type: dtype.to_onnx(),
584 shape: Some(encode_shape(graph, shape)),
585 }),
586 TypeProto::SparseTensor { dtype, shape } => {
587 type_proto::Value::SparseTensorType(type_proto::SparseTensor {
588 elem_type: dtype.to_onnx(),
589 shape: Some(encode_shape(graph, shape)),
590 })
591 }
592 TypeProto::Sequence(inner) => {
593 type_proto::Value::SequenceType(Box::new(type_proto::Sequence {
594 elem_type: Some(Box::new(encode_type_proto(graph, inner))),
595 }))
596 }
597 TypeProto::Optional(inner) => {
598 type_proto::Value::OptionalType(Box::new(type_proto::Optional {
599 elem_type: Some(Box::new(encode_type_proto(graph, inner))),
600 }))
601 }
602 TypeProto::Map { key, value } => type_proto::Value::MapType(Box::new(type_proto::Map {
603 key_type: key.to_onnx(),
604 value_type: Some(Box::new(encode_type_proto(graph, value))),
605 })),
606 };
607 onnx::TypeProto {
608 value: Some(value),
609 ..Default::default()
610 }
611}
612
613fn value_name(graph: &Graph, vid: ValueId) -> Option<&str> {
615 graph.try_value(vid).and_then(|v| v.name.as_deref())
616}
617
618#[cfg(test)]
619mod node_version_tests {
620 use super::*;
621 use onnx_runtime_ir::{DataType, Dim, Node, NodeId};
622
623 fn graph_with_node_version(version: Option<i64>) -> Graph {
624 let mut graph = Graph::new();
625 graph.opset_imports.insert(String::new(), 13);
626 let x = graph.create_named_value("x", DataType::Float32, vec![Dim::Static(2)]);
627 let y = graph.create_named_value("y", DataType::Float32, vec![Dim::Static(2)]);
628 graph.add_input(x);
629 graph.add_output(y);
630 let mut node = Node::new(NodeId(0), "Swish", vec![Some(x)], vec![y]);
631 node.version = version;
632 node.name = "swish".to_string();
633 graph.insert_node(node);
634 graph
635 }
636
637 #[test]
644 fn refuses_to_write_a_node_whose_version_the_format_cannot_hold() {
645 let graph = graph_with_node_version(Some(24));
646 let error = reject_unrepresentable_node_versions(&graph)
647 .expect_err("a mixed-version graph must not serialise");
648 let text = error.to_string();
649 assert!(text.contains("swish"), "must name the node: {text}");
650 assert!(text.contains("24"), "must give the node's version: {text}");
651 assert!(text.contains("13"), "must give the graph's version: {text}");
652 assert!(
653 text.contains("serialise before") || text.contains("fusion disabled"),
654 "must say how to proceed: {text}"
655 );
656 }
657
658 #[test]
660 fn allows_versions_the_format_can_represent() {
661 reject_unrepresentable_node_versions(&graph_with_node_version(None))
662 .expect("an unversioned node is the ordinary case");
663 reject_unrepresentable_node_versions(&graph_with_node_version(Some(13)))
664 .expect("a node agreeing with the graph loses nothing when written");
665 }
666}