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::proto::onnx::{
58 self, attribute_proto::AttributeType, tensor_shape_proto, type_proto, AttributeProto,
59 GraphProto, ModelProto, NodeProto, OperatorSetIdProto, StringStringEntryProto, TensorProto,
60 TensorShapeProto, ValueInfoProto,
61};
62use crate::weights::WeightStore;
63use crate::LoaderError;
64
65pub const DEFAULT_IR_VERSION: i64 = 10;
68
69#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct ModelMetadata {
76 pub ir_version: i64,
78 pub producer_name: String,
80 pub producer_version: String,
82 pub domain: String,
84 pub model_version: i64,
86 pub doc_string: Option<String>,
88 pub graph_name: String,
90 pub metadata_props: Vec<(String, String)>,
92}
93
94impl Default for ModelMetadata {
95 fn default() -> Self {
96 Self {
97 ir_version: DEFAULT_IR_VERSION,
98 producer_name: String::new(),
99 producer_version: String::new(),
100 domain: String::new(),
101 model_version: 0,
102 doc_string: None,
103 graph_name: String::new(),
104 metadata_props: Vec::new(),
105 }
106 }
107}
108
109pub struct Model<'a> {
117 pub graph: &'a Graph,
119 pub metadata: ModelMetadata,
121 pub weights: Option<&'a WeightStore>,
123}
124
125impl<'a> Model<'a> {
126 pub fn new(graph: &'a Graph) -> Self {
135 Self {
136 graph,
137 metadata: ModelMetadata::default(),
138 weights: None,
139 }
140 }
141
142 pub fn with_metadata(mut self, metadata: ModelMetadata) -> Self {
144 self.metadata = metadata;
145 self
146 }
147
148 pub fn with_weights(mut self, weights: &'a WeightStore) -> Self {
150 self.weights = Some(weights);
151 self
152 }
153}
154
155pub fn encode_model(model: &Model) -> Result<Vec<u8>, LoaderError> {
157 Ok(encode_model_proto(model)?.encode_to_vec())
158}
159
160pub fn write_model(model: &Model, path: impl AsRef<Path>) -> Result<(), LoaderError> {
162 let bytes = encode_model(model)?;
163 let path = path.as_ref();
164 std::fs::write(path, bytes).map_err(|source| LoaderError::Io {
165 path: path.to_path_buf(),
166 source,
167 })
168}
169
170pub fn encode_model_proto(model: &Model) -> Result<ModelProto, LoaderError> {
174 let meta = &model.metadata;
175 let graph = encode_graph_proto(model.graph, model.weights, true, &meta.graph_name)?;
176
177 let mut opset_import: Vec<OperatorSetIdProto> = model
179 .graph
180 .opset_imports
181 .iter()
182 .map(|(domain, &version)| OperatorSetIdProto {
183 domain: domain.clone(),
184 version: version as i64,
185 })
186 .collect();
187 opset_import.sort_by(|a, b| a.domain.cmp(&b.domain));
188
189 let metadata_props = meta
190 .metadata_props
191 .iter()
192 .map(|(key, value)| StringStringEntryProto {
193 key: key.clone(),
194 value: value.clone(),
195 })
196 .collect();
197
198 Ok(ModelProto {
199 ir_version: meta.ir_version,
200 opset_import,
201 producer_name: meta.producer_name.clone(),
202 producer_version: meta.producer_version.clone(),
203 domain: meta.domain.clone(),
204 model_version: meta.model_version,
205 doc_string: meta.doc_string.clone().unwrap_or_default(),
206 graph: Some(graph),
207 metadata_props,
208 ..Default::default()
209 })
210}
211
212fn encode_graph_proto(
215 graph: &Graph,
216 weights: Option<&WeightStore>,
217 _is_top_level: bool,
218 name: &str,
219) -> Result<GraphProto, LoaderError> {
220 let mut init_ids: Vec<ValueId> = graph.initializers.keys().copied().collect();
222 init_ids.sort_by_key(|v| v.0);
223 let mut initializer = Vec::with_capacity(init_ids.len());
224 for vid in &init_ids {
225 let weight = &graph.initializers[vid];
226 let iname = value_name(graph, *vid).unwrap_or_default().to_string();
227 initializer.push(encode_weight(iname, weight, weights)?);
228 }
229
230 let input: Vec<ValueInfoProto> = graph
232 .inputs
233 .iter()
234 .map(|&vid| encode_value_info(graph, vid))
235 .collect();
236 let output: Vec<ValueInfoProto> = graph
237 .outputs
238 .iter()
239 .map(|&vid| encode_value_info(graph, vid))
240 .collect();
241
242 let mut excluded: HashSet<ValueId> = HashSet::new();
246 excluded.extend(graph.inputs.iter().copied());
247 excluded.extend(graph.outputs.iter().copied());
248 excluded.extend(init_ids.iter().copied());
249 let mut value_info = Vec::new();
250 for (vid, value) in graph.values.iter() {
251 if excluded.contains(&vid) {
252 continue;
253 }
254 if value.name.as_deref().is_some_and(|n| !n.is_empty()) {
255 value_info.push(encode_value_info(graph, vid));
256 }
257 }
258
259 let mut node = Vec::with_capacity(graph.num_nodes());
261 for (_, n) in graph.nodes.iter() {
262 node.push(encode_node(graph, n)?);
263 }
264
265 Ok(GraphProto {
266 node,
267 name: name.to_string(),
268 initializer,
269 input,
270 output,
271 value_info,
272 ..Default::default()
273 })
274}
275
276fn encode_node(graph: &Graph, node: &Node) -> Result<NodeProto, LoaderError> {
278 let input: Vec<String> = node
279 .inputs
280 .iter()
281 .map(|slot| match slot {
282 Some(vid) => value_name(graph, *vid).unwrap_or_default().to_string(),
283 None => String::new(),
284 })
285 .collect();
286 let output: Vec<String> = node
287 .outputs
288 .iter()
289 .map(|&vid| value_name(graph, vid).unwrap_or_default().to_string())
290 .collect();
291
292 let mut keys: Vec<&String> = node.attributes.keys().collect();
295 keys.sort();
296 let mut attribute = Vec::with_capacity(keys.len());
297 for key in keys {
298 attribute.push(encode_attribute(graph, key, &node.attributes[key])?);
299 }
300
301 Ok(NodeProto {
302 input,
303 output,
304 name: String::new(),
305 op_type: node.op_type.clone(),
306 domain: node.domain.clone(),
307 attribute,
308 doc_string: node.doc_string.clone().unwrap_or_default(),
309 ..Default::default()
310 })
311}
312
313fn encode_attribute(
316 graph: &Graph,
317 name: &str,
318 attr: &Attribute,
319) -> Result<AttributeProto, LoaderError> {
320 let mut ap = AttributeProto {
321 name: name.to_string(),
322 ..Default::default()
323 };
324 match attr {
325 Attribute::Int(v) => {
326 ap.i = *v;
327 ap.r#type = AttributeType::Int as i32;
328 }
329 Attribute::Float(v) => {
330 ap.f = *v;
331 ap.r#type = AttributeType::Float as i32;
332 }
333 Attribute::String(s) => {
334 ap.s = s.clone();
335 ap.r#type = AttributeType::String as i32;
336 }
337 Attribute::Ints(v) => {
338 ap.ints = v.clone();
339 ap.r#type = AttributeType::Ints as i32;
340 }
341 Attribute::Floats(v) => {
342 ap.floats = v.clone();
343 ap.r#type = AttributeType::Floats as i32;
344 }
345 Attribute::Strings(v) => {
346 ap.strings = v.clone();
347 ap.r#type = AttributeType::Strings as i32;
348 }
349 Attribute::Tensor(t) => {
350 ap.t = Some(encode_tensor(t));
351 ap.r#type = AttributeType::Tensor as i32;
352 }
353 Attribute::Graph(_) | Attribute::Graphs(_) => {
354 return Err(LoaderError::GraphBuild(format!(
361 "attribute {name:?}: encoding control-flow subgraph attributes is \
362 unsupported (nested-graph formal I/O is not round-tripped by the \
363 load path)"
364 )));
365 }
366 Attribute::TypeProto(tp) => {
367 ap.tp = Some(encode_type_proto(graph, tp));
368 ap.r#type = AttributeType::TypeProto as i32;
369 }
370 Attribute::SparseTensor(_) => {
374 return Err(LoaderError::GraphBuild(format!(
375 "attribute {name:?}: SparseTensor encoding is unsupported"
376 )));
377 }
378 }
379 Ok(ap)
380}
381
382fn encode_tensor(t: &TensorData) -> TensorProto {
385 let mut tp = TensorProto {
386 dims: t.dims.iter().map(|&d| d as i64).collect(),
387 data_type: t.dtype.to_onnx(),
388 name: t.name.clone().unwrap_or_default(),
389 ..Default::default()
390 };
391 if t.dtype == DataType::String {
392 tp.string_data = t.strings.iter().map(|s| s.clone().into_bytes()).collect();
393 } else {
394 tp.raw_data = t.data.clone();
395 }
396 tp
397}
398
399fn encode_weight(
405 name: String,
406 weight: &WeightRef,
407 weights: Option<&WeightStore>,
408) -> Result<TensorProto, LoaderError> {
409 match weight {
410 WeightRef::Inline(t) => {
411 let mut tp = encode_tensor(t);
412 tp.name = name;
413 Ok(tp)
414 }
415 WeightRef::External { dtype, dims, .. } => {
416 if *dtype == DataType::String {
417 return Err(LoaderError::GraphBuild(format!(
418 "external initializer {name:?}: STRING external data is unsupported"
419 )));
420 }
421 let bytes = weights.and_then(|s| s.bytes(weight)).ok_or_else(|| {
422 LoaderError::GraphBuild(format!(
423 "external initializer {name:?}: weight bytes unavailable \
424 (attach a WeightStore via Model::with_weights)"
425 ))
426 })?;
427 Ok(TensorProto {
428 name,
429 data_type: dtype.to_onnx(),
430 dims: dims.iter().map(|&d| d as i64).collect(),
431 raw_data: bytes.to_vec(),
432 ..Default::default()
433 })
434 }
435 }
436}
437
438fn encode_value_info(graph: &Graph, vid: ValueId) -> ValueInfoProto {
440 let value = graph.value(vid);
441 ValueInfoProto {
442 name: value.name.clone().unwrap_or_default(),
443 r#type: Some(encode_tensor_type(graph, value.dtype, &value.shape)),
444 ..Default::default()
445 }
446}
447
448fn encode_tensor_type(graph: &Graph, dtype: DataType, shape: &Shape) -> onnx::TypeProto {
450 onnx::TypeProto {
451 value: Some(type_proto::Value::TensorType(type_proto::Tensor {
452 elem_type: dtype.to_onnx(),
453 shape: Some(encode_shape(graph, shape)),
454 })),
455 ..Default::default()
456 }
457}
458
459fn encode_shape(graph: &Graph, shape: &Shape) -> TensorShapeProto {
463 use tensor_shape_proto::{dimension::Value as DV, Dimension};
464 let dim = shape
465 .iter()
466 .map(|d| {
467 let value = match d {
468 Dim::Static(n) => Some(DV::DimValue(*n as i64)),
469 Dim::Symbolic(sym) => graph
470 .symbol_constraints
471 .get(sym)
472 .and_then(|c| c.name.clone())
473 .map(DV::DimParam),
474 };
475 Dimension {
476 value,
477 ..Default::default()
478 }
479 })
480 .collect();
481 TensorShapeProto { dim }
482}
483
484fn encode_type_proto(graph: &Graph, tp: &TypeProto) -> onnx::TypeProto {
486 let value = match tp {
487 TypeProto::Tensor { dtype, shape } => type_proto::Value::TensorType(type_proto::Tensor {
488 elem_type: dtype.to_onnx(),
489 shape: Some(encode_shape(graph, shape)),
490 }),
491 TypeProto::SparseTensor { dtype, shape } => {
492 type_proto::Value::SparseTensorType(type_proto::SparseTensor {
493 elem_type: dtype.to_onnx(),
494 shape: Some(encode_shape(graph, shape)),
495 })
496 }
497 TypeProto::Sequence(inner) => type_proto::Value::SequenceType(Box::new(type_proto::Sequence {
498 elem_type: Some(Box::new(encode_type_proto(graph, inner))),
499 })),
500 TypeProto::Optional(inner) => type_proto::Value::OptionalType(Box::new(type_proto::Optional {
501 elem_type: Some(Box::new(encode_type_proto(graph, inner))),
502 })),
503 TypeProto::Map { key, value } => type_proto::Value::MapType(Box::new(type_proto::Map {
504 key_type: key.to_onnx(),
505 value_type: Some(Box::new(encode_type_proto(graph, value))),
506 })),
507 };
508 onnx::TypeProto {
509 value: Some(value),
510 ..Default::default()
511 }
512}
513
514fn value_name(graph: &Graph, vid: ValueId) -> Option<&str> {
516 graph.try_value(vid).and_then(|v| v.name.as_deref())
517}