1use std::collections::HashSet;
52use std::path::Path;
53
54use prost::Message;
55
56use onnx_runtime_ir::{
57 Attribute, DataType, Dim, Graph, Node, Shape, TensorData, TypeProto, ValueId, WeightRef,
58};
59
60use crate::proto::onnx::{
61 self, attribute_proto::AttributeType, tensor_shape_proto, type_proto, AttributeProto,
62 GraphProto, ModelProto, NodeProto, OperatorSetIdProto, StringStringEntryProto, TensorProto,
63 TensorShapeProto, ValueInfoProto,
64};
65use crate::weights::WeightStore;
66use crate::LoaderError;
67
68pub const DEFAULT_IR_VERSION: i64 = 10;
71
72#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct ModelMetadata {
79 pub ir_version: i64,
81 pub producer_name: String,
83 pub producer_version: String,
85 pub domain: String,
87 pub model_version: i64,
89 pub doc_string: Option<String>,
91 pub graph_name: String,
93 pub metadata_props: Vec<(String, String)>,
95}
96
97impl Default for ModelMetadata {
98 fn default() -> Self {
99 Self {
100 ir_version: DEFAULT_IR_VERSION,
101 producer_name: String::new(),
102 producer_version: String::new(),
103 domain: String::new(),
104 model_version: 0,
105 doc_string: None,
106 graph_name: String::new(),
107 metadata_props: Vec::new(),
108 }
109 }
110}
111
112pub struct Model<'a> {
120 pub graph: &'a Graph,
122 pub metadata: ModelMetadata,
124 pub weights: Option<&'a WeightStore>,
126}
127
128impl<'a> Model<'a> {
129 pub fn new(graph: &'a Graph) -> Self {
138 Self {
139 graph,
140 metadata: ModelMetadata::default(),
141 weights: None,
142 }
143 }
144
145 pub fn with_metadata(mut self, metadata: ModelMetadata) -> Self {
147 self.metadata = metadata;
148 self
149 }
150
151 pub fn with_weights(mut self, weights: &'a WeightStore) -> Self {
153 self.weights = Some(weights);
154 self
155 }
156}
157
158pub fn encode_model(model: &Model) -> Result<Vec<u8>, LoaderError> {
160 Ok(encode_model_proto(model)?.encode_to_vec())
161}
162
163pub fn write_model(model: &Model, path: impl AsRef<Path>) -> Result<(), LoaderError> {
165 let bytes = encode_model(model)?;
166 let path = path.as_ref();
167 std::fs::write(path, bytes).map_err(|source| LoaderError::Io {
168 path: path.to_path_buf(),
169 source,
170 })
171}
172
173pub fn encode_model_proto(model: &Model) -> Result<ModelProto, LoaderError> {
177 let meta = &model.metadata;
178 let graph = encode_graph_proto(model.graph, model.weights, true, &meta.graph_name)?;
179
180 let mut opset_import: Vec<OperatorSetIdProto> = model
182 .graph
183 .opset_imports
184 .iter()
185 .map(|(domain, &version)| OperatorSetIdProto {
186 domain: domain.clone(),
187 version: version as i64,
188 })
189 .collect();
190 if meta.ir_version >= 3
191 && !opset_import
192 .iter()
193 .any(|opset| opset.domain.is_empty() || opset.domain == "ai.onnx")
194 {
195 opset_import.push(OperatorSetIdProto {
197 domain: String::new(),
198 version: 21,
199 });
200 }
201 opset_import.sort_by(|a, b| a.domain.cmp(&b.domain));
202
203 let metadata_props = meta
204 .metadata_props
205 .iter()
206 .map(|(key, value)| StringStringEntryProto {
207 key: key.clone(),
208 value: value.clone(),
209 })
210 .collect();
211
212 Ok(ModelProto {
213 ir_version: meta.ir_version,
214 opset_import,
215 producer_name: meta.producer_name.clone(),
216 producer_version: meta.producer_version.clone(),
217 domain: meta.domain.clone(),
218 model_version: meta.model_version,
219 doc_string: meta.doc_string.clone().unwrap_or_default(),
220 graph: Some(graph),
221 metadata_props,
222 ..Default::default()
223 })
224}
225
226fn encode_graph_proto(
229 graph: &Graph,
230 weights: Option<&WeightStore>,
231 _is_top_level: bool,
232 name: &str,
233) -> Result<GraphProto, LoaderError> {
234 let mut init_ids: Vec<ValueId> = graph.initializers.keys().copied().collect();
236 init_ids.sort_by_key(|v| v.0);
237 let mut initializer = Vec::with_capacity(init_ids.len());
238 for vid in &init_ids {
239 let weight = &graph.initializers[vid];
240 let iname = value_name(graph, *vid).unwrap_or_default().to_string();
241 initializer.push(encode_weight(iname, weight, weights)?);
242 }
243
244 let input: Vec<ValueInfoProto> = graph
246 .inputs
247 .iter()
248 .map(|&vid| encode_value_info(graph, vid))
249 .collect();
250 let output: Vec<ValueInfoProto> = graph
251 .outputs
252 .iter()
253 .map(|&vid| encode_value_info(graph, vid))
254 .collect();
255
256 let mut excluded: HashSet<ValueId> = HashSet::new();
260 excluded.extend(graph.inputs.iter().copied());
261 excluded.extend(graph.outputs.iter().copied());
262 excluded.extend(init_ids.iter().copied());
263 let mut value_info = Vec::new();
264 for (vid, value) in graph.values.iter() {
265 if excluded.contains(&vid) {
266 continue;
267 }
268 if value.name.as_deref().is_some_and(|n| !n.is_empty()) {
269 value_info.push(encode_value_info(graph, vid));
270 }
271 }
272
273 let mut node = Vec::with_capacity(graph.num_nodes());
275 for (_, n) in graph.nodes.iter() {
276 node.push(encode_node(graph, n)?);
277 }
278
279 Ok(GraphProto {
280 node,
281 name: name.to_string(),
282 initializer,
283 input,
284 output,
285 value_info,
286 ..Default::default()
287 })
288}
289
290fn encode_node(graph: &Graph, node: &Node) -> Result<NodeProto, LoaderError> {
292 let input: Vec<String> = node
293 .inputs
294 .iter()
295 .map(|slot| match slot {
296 Some(vid) => value_name(graph, *vid).unwrap_or_default().to_string(),
297 None => String::new(),
298 })
299 .collect();
300 let output: Vec<String> = node
301 .outputs
302 .iter()
303 .map(|&vid| value_name(graph, vid).unwrap_or_default().to_string())
304 .collect();
305
306 let mut keys: Vec<&String> = node.attributes.keys().collect();
309 keys.sort();
310 let mut attribute = Vec::with_capacity(keys.len());
311 for key in keys {
312 attribute.push(encode_attribute(graph, key, &node.attributes[key])?);
313 }
314
315 Ok(NodeProto {
316 input,
317 output,
318 name: node.name.clone(),
319 op_type: node.op_type.clone(),
320 domain: node.domain.clone(),
321 attribute,
322 doc_string: node.doc_string.clone().unwrap_or_default(),
323 ..Default::default()
324 })
325}
326
327fn encode_attribute(
330 graph: &Graph,
331 name: &str,
332 attr: &Attribute,
333) -> Result<AttributeProto, LoaderError> {
334 let mut ap = AttributeProto {
335 name: name.to_string(),
336 ..Default::default()
337 };
338 match attr {
339 Attribute::Int(v) => {
340 ap.i = *v;
341 ap.r#type = AttributeType::Int as i32;
342 }
343 Attribute::Float(v) => {
344 ap.f = *v;
345 ap.r#type = AttributeType::Float as i32;
346 }
347 Attribute::String(s) => {
348 ap.s = s.clone();
349 ap.r#type = AttributeType::String as i32;
350 }
351 Attribute::Ints(v) => {
352 ap.ints = v.clone();
353 ap.r#type = AttributeType::Ints as i32;
354 }
355 Attribute::Floats(v) => {
356 ap.floats = v.clone();
357 ap.r#type = AttributeType::Floats as i32;
358 }
359 Attribute::Strings(v) => {
360 ap.strings = v.clone();
361 ap.r#type = AttributeType::Strings as i32;
362 }
363 Attribute::Tensor(t) => {
364 ap.t = Some(encode_tensor(t));
365 ap.r#type = AttributeType::Tensor as i32;
366 }
367 Attribute::Graph(_) | Attribute::Graphs(_) => {
368 return Err(LoaderError::GraphBuild(format!(
375 "attribute {name:?}: encoding control-flow subgraph attributes is \
376 unsupported (nested-graph formal I/O is not round-tripped by the \
377 load path)"
378 )));
379 }
380 Attribute::TypeProto(tp) => {
381 ap.tp = Some(encode_type_proto(graph, tp));
382 ap.r#type = AttributeType::TypeProto as i32;
383 }
384 Attribute::SparseTensor(_) => {
388 return Err(LoaderError::GraphBuild(format!(
389 "attribute {name:?}: SparseTensor encoding is unsupported"
390 )));
391 }
392 }
393 Ok(ap)
394}
395
396fn encode_tensor(t: &TensorData) -> TensorProto {
399 let mut tp = TensorProto {
400 dims: t.dims.iter().map(|&d| d as i64).collect(),
401 data_type: t.dtype.to_onnx(),
402 name: t.name.clone().unwrap_or_default(),
403 ..Default::default()
404 };
405 if t.dtype == DataType::String {
406 tp.string_data = t.strings.iter().map(|s| s.clone().into_bytes()).collect();
407 } else {
408 tp.raw_data = t.data.clone();
409 }
410 tp
411}
412
413fn encode_weight(
419 name: String,
420 weight: &WeightRef,
421 weights: Option<&WeightStore>,
422) -> Result<TensorProto, LoaderError> {
423 match weight {
424 WeightRef::Inline(t) => {
425 let mut tp = encode_tensor(t);
426 tp.name = name;
427 Ok(tp)
428 }
429 WeightRef::External { dtype, dims, .. } => {
430 if *dtype == DataType::String {
431 return Err(LoaderError::GraphBuild(format!(
432 "external initializer {name:?}: STRING external data is unsupported"
433 )));
434 }
435 let bytes = weights.and_then(|s| s.bytes(weight)).ok_or_else(|| {
436 LoaderError::GraphBuild(format!(
437 "external initializer {name:?}: weight bytes unavailable \
438 (attach a WeightStore via Model::with_weights)"
439 ))
440 })?;
441 Ok(TensorProto {
442 name,
443 data_type: dtype.to_onnx(),
444 dims: dims.iter().map(|&d| d as i64).collect(),
445 raw_data: bytes.to_vec(),
446 ..Default::default()
447 })
448 }
449 }
450}
451
452fn encode_value_info(graph: &Graph, vid: ValueId) -> ValueInfoProto {
454 let value = graph.value(vid);
455 ValueInfoProto {
456 name: value.name.clone().unwrap_or_default(),
457 r#type: Some(encode_tensor_type(graph, value.dtype, &value.shape)),
458 ..Default::default()
459 }
460}
461
462fn encode_tensor_type(graph: &Graph, dtype: DataType, shape: &Shape) -> onnx::TypeProto {
464 onnx::TypeProto {
465 value: Some(type_proto::Value::TensorType(type_proto::Tensor {
466 elem_type: dtype.to_onnx(),
467 shape: Some(encode_shape(graph, shape)),
468 })),
469 ..Default::default()
470 }
471}
472
473fn encode_shape(graph: &Graph, shape: &Shape) -> TensorShapeProto {
477 use tensor_shape_proto::{dimension::Value as DV, Dimension};
478 let dim = shape
479 .iter()
480 .map(|d| {
481 let value = match d {
482 Dim::Static(n) => Some(DV::DimValue(*n as i64)),
483 Dim::Symbolic(sym) => graph
484 .symbol_constraints
485 .get(sym)
486 .and_then(|c| c.name.clone())
487 .map(DV::DimParam),
488 };
489 Dimension {
490 value,
491 ..Default::default()
492 }
493 })
494 .collect();
495 TensorShapeProto { dim }
496}
497
498fn encode_type_proto(graph: &Graph, tp: &TypeProto) -> onnx::TypeProto {
500 let value = match tp {
501 TypeProto::Tensor { dtype, shape } => type_proto::Value::TensorType(type_proto::Tensor {
502 elem_type: dtype.to_onnx(),
503 shape: Some(encode_shape(graph, shape)),
504 }),
505 TypeProto::SparseTensor { dtype, shape } => {
506 type_proto::Value::SparseTensorType(type_proto::SparseTensor {
507 elem_type: dtype.to_onnx(),
508 shape: Some(encode_shape(graph, shape)),
509 })
510 }
511 TypeProto::Sequence(inner) => type_proto::Value::SequenceType(Box::new(type_proto::Sequence {
512 elem_type: Some(Box::new(encode_type_proto(graph, inner))),
513 })),
514 TypeProto::Optional(inner) => type_proto::Value::OptionalType(Box::new(type_proto::Optional {
515 elem_type: Some(Box::new(encode_type_proto(graph, inner))),
516 })),
517 TypeProto::Map { key, value } => type_proto::Value::MapType(Box::new(type_proto::Map {
518 key_type: key.to_onnx(),
519 value_type: Some(Box::new(encode_type_proto(graph, value))),
520 })),
521 };
522 onnx::TypeProto {
523 value: Some(value),
524 ..Default::default()
525 }
526}
527
528fn value_name(graph: &Graph, vid: ValueId) -> Option<&str> {
530 graph.try_value(vid).and_then(|v| v.name.as_deref())
531}