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 = 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 if meta.ir_version >= 3
188 && !opset_import
189 .iter()
190 .any(|opset| opset.domain.is_empty() || opset.domain == "ai.onnx")
191 {
192 opset_import.push(OperatorSetIdProto {
194 domain: String::new(),
195 version: 21,
196 });
197 }
198 opset_import.sort_by(|a, b| a.domain.cmp(&b.domain));
199
200 let metadata_props = meta
201 .metadata_props
202 .iter()
203 .map(|(key, value)| StringStringEntryProto {
204 key: key.clone(),
205 value: value.clone(),
206 })
207 .collect();
208
209 Ok(ModelProto {
210 ir_version: meta.ir_version,
211 opset_import,
212 producer_name: meta.producer_name.clone(),
213 producer_version: meta.producer_version.clone(),
214 domain: meta.domain.clone(),
215 model_version: meta.model_version,
216 doc_string: meta.doc_string.clone().unwrap_or_default(),
217 graph: Some(graph),
218 metadata_props,
219 ..Default::default()
220 })
221}
222
223fn encode_graph_proto(
226 graph: &Graph,
227 weights: Option<&WeightStore>,
228 _is_top_level: bool,
229 name: &str,
230) -> Result<GraphProto, LoaderError> {
231 let mut init_ids: Vec<ValueId> = graph.initializers.keys().copied().collect();
233 init_ids.sort_by_key(|v| v.0);
234 let mut initializer = Vec::with_capacity(init_ids.len());
235 for vid in &init_ids {
236 let weight = &graph.initializers[vid];
237 let iname = value_name(graph, *vid).unwrap_or_default().to_string();
238 initializer.push(encode_weight(iname, weight, weights)?);
239 }
240
241 let input: Vec<ValueInfoProto> = graph
243 .inputs
244 .iter()
245 .map(|&vid| encode_value_info(graph, vid))
246 .collect();
247 let output: Vec<ValueInfoProto> = graph
248 .outputs
249 .iter()
250 .map(|&vid| encode_value_info(graph, vid))
251 .collect();
252
253 let mut excluded: HashSet<ValueId> = HashSet::new();
257 excluded.extend(graph.inputs.iter().copied());
258 excluded.extend(graph.outputs.iter().copied());
259 excluded.extend(init_ids.iter().copied());
260 let mut value_info = Vec::new();
261 for (vid, value) in graph.values.iter() {
262 if excluded.contains(&vid) {
263 continue;
264 }
265 if value.name.as_deref().is_some_and(|n| !n.is_empty()) {
266 value_info.push(encode_value_info(graph, vid));
267 }
268 }
269
270 let mut node = Vec::with_capacity(graph.num_nodes());
272 for (_, n) in graph.nodes.iter() {
273 node.push(encode_node(graph, n, weights)?);
274 }
275
276 Ok(GraphProto {
277 node,
278 name: name.to_string(),
279 initializer,
280 input,
281 output,
282 value_info,
283 ..Default::default()
284 })
285}
286
287fn encode_node(
289 graph: &Graph,
290 node: &Node,
291 weights: Option<&WeightStore>,
292) -> Result<NodeProto, LoaderError> {
293 let input: Vec<String> = node
294 .inputs
295 .iter()
296 .map(|slot| match slot {
297 Some(vid) => value_name(graph, *vid).unwrap_or_default().to_string(),
298 None => String::new(),
299 })
300 .collect();
301 let output: Vec<String> = node
302 .outputs
303 .iter()
304 .map(|&vid| value_name(graph, vid).unwrap_or_default().to_string())
305 .collect();
306
307 let mut keys: Vec<&String> = node.attributes.keys().collect();
310 keys.sort();
311 let mut attribute = Vec::with_capacity(keys.len());
312 for key in keys {
313 attribute.push(encode_attribute(
314 graph,
315 node,
316 key,
317 &node.attributes[key],
318 weights,
319 )?);
320 }
321
322 Ok(NodeProto {
323 input,
324 output,
325 name: node.name.clone(),
326 op_type: node.op_type.clone(),
327 domain: node.domain.clone(),
328 attribute,
329 doc_string: node.doc_string.clone().unwrap_or_default(),
330 ..Default::default()
331 })
332}
333
334fn encode_attribute(
337 graph: &Graph,
338 node: &Node,
339 name: &str,
340 attr: &Attribute,
341 weights: Option<&WeightStore>,
342) -> Result<AttributeProto, LoaderError> {
343 let mut ap = AttributeProto {
344 name: name.to_string(),
345 ..Default::default()
346 };
347 match attr {
348 Attribute::Int(v) => {
349 ap.i = *v;
350 ap.r#type = AttributeType::Int as i32;
351 }
352 Attribute::Float(v) => {
353 ap.f = *v;
354 ap.r#type = AttributeType::Float as i32;
355 }
356 Attribute::String(s) => {
357 ap.s = s.clone();
358 ap.r#type = AttributeType::String as i32;
359 }
360 Attribute::Ints(v) => {
361 ap.ints = v.clone();
362 ap.r#type = AttributeType::Ints as i32;
363 }
364 Attribute::Floats(v) => {
365 ap.floats = v.clone();
366 ap.r#type = AttributeType::Floats as i32;
367 }
368 Attribute::Strings(v) => {
369 ap.strings = v.clone();
370 ap.r#type = AttributeType::Strings as i32;
371 }
372 Attribute::Tensor(t) => {
373 ap.t = Some(encode_tensor(t));
374 ap.r#type = AttributeType::Tensor as i32;
375 }
376 Attribute::Tensors(tensors) => {
377 ap.tensors = tensors.iter().map(encode_tensor).collect();
378 ap.r#type = AttributeType::Tensors as i32;
379 }
380 Attribute::Graph(inline) => {
381 let subgraph = graph
382 .subgraphs
383 .get(&(node.id, name.to_string()))
384 .unwrap_or(inline);
385 ap.g = Some(encode_graph_proto(subgraph, weights, false, "")?);
386 ap.r#type = AttributeType::Graph as i32;
387 }
388 Attribute::Graphs(inline) => {
389 ap.graphs = inline
390 .iter()
391 .enumerate()
392 .map(|(index, fallback)| {
393 let key = (node.id, format!("{name}[{index}]"));
394 let subgraph = graph.subgraphs.get(&key).unwrap_or(fallback);
395 encode_graph_proto(subgraph, weights, false, "")
396 })
397 .collect::<Result<Vec<_>, _>>()?;
398 ap.r#type = AttributeType::Graphs as i32;
399 }
400 Attribute::TypeProto(tp) => {
401 ap.tp = Some(encode_type_proto(graph, tp));
402 ap.r#type = AttributeType::TypeProto as i32;
403 }
404 Attribute::TypeProtos(types) => {
405 ap.type_protos = types
406 .iter()
407 .map(|value| encode_type_proto(graph, value))
408 .collect();
409 ap.r#type = AttributeType::TypeProtos as i32;
410 }
411 Attribute::SparseTensor(tensor) => {
412 ap.sparse_tensor = Some(encode_sparse_tensor(tensor));
413 ap.r#type = AttributeType::SparseTensor as i32;
414 }
415 Attribute::SparseTensors(tensors) => {
416 ap.sparse_tensors = tensors.iter().map(encode_sparse_tensor).collect();
417 ap.r#type = AttributeType::SparseTensors as i32;
418 }
419 }
420 Ok(ap)
421}
422
423fn encode_tensor(t: &TensorData) -> TensorProto {
426 let mut tp = TensorProto {
427 dims: t.dims.iter().map(|&d| d as i64).collect(),
428 data_type: t.dtype.to_onnx(),
429 name: t.name.clone().unwrap_or_default(),
430 ..Default::default()
431 };
432 if t.dtype == DataType::String {
433 tp.string_data = t.strings.iter().map(|s| s.clone().into_bytes()).collect();
434 } else {
435 tp.raw_data = t.data.clone();
436 }
437
438 tp
439}
440
441fn encode_sparse_tensor(tensor: &onnx_runtime_ir::SparseTensorData) -> onnx::SparseTensorProto {
442 onnx::SparseTensorProto {
443 values: Some(encode_tensor(&tensor.values)),
444 indices: Some(encode_tensor(&tensor.indices)),
445 dims: tensor.dims.iter().map(|&dim| dim as i64).collect(),
446 }
447}
448
449fn encode_weight(
455 name: String,
456 weight: &WeightRef,
457 weights: Option<&WeightStore>,
458) -> Result<TensorProto, LoaderError> {
459 match weight {
460 WeightRef::Inline(t) => {
461 let mut tp = encode_tensor(t);
462 tp.name = name;
463 Ok(tp)
464 }
465 WeightRef::External { dtype, dims, .. } => {
466 if *dtype == DataType::String {
467 return Err(LoaderError::GraphBuild(format!(
468 "external initializer {name:?}: STRING external data is unsupported"
469 )));
470 }
471 let bytes = weights.and_then(|s| s.bytes(weight)).ok_or_else(|| {
472 LoaderError::GraphBuild(format!(
473 "external initializer {name:?}: weight bytes unavailable \
474 (attach a WeightStore via Model::with_weights)"
475 ))
476 })?;
477 Ok(TensorProto {
478 name,
479 data_type: dtype.to_onnx(),
480 dims: dims.iter().map(|&d| d as i64).collect(),
481 raw_data: bytes.to_vec(),
482 ..Default::default()
483 })
484 }
485 }
486}
487
488fn encode_value_info(graph: &Graph, vid: ValueId) -> ValueInfoProto {
490 let value = graph.value(vid);
491 ValueInfoProto {
492 name: value.name.clone().unwrap_or_default(),
493 r#type: Some(encode_tensor_type(graph, value.dtype, &value.shape)),
494 ..Default::default()
495 }
496}
497
498fn encode_tensor_type(graph: &Graph, dtype: DataType, shape: &Shape) -> onnx::TypeProto {
500 onnx::TypeProto {
501 value: Some(type_proto::Value::TensorType(type_proto::Tensor {
502 elem_type: dtype.to_onnx(),
503 shape: Some(encode_shape(graph, shape)),
504 })),
505 ..Default::default()
506 }
507}
508
509fn encode_shape(graph: &Graph, shape: &Shape) -> TensorShapeProto {
513 use tensor_shape_proto::{Dimension, dimension::Value as DV};
514 let dim = shape
515 .iter()
516 .map(|d| {
517 let value = match d {
518 Dim::Static(n) => Some(DV::DimValue(*n as i64)),
519 Dim::Symbolic(sym) => graph
520 .symbol_constraints
521 .get(sym)
522 .and_then(|c| c.name.clone())
523 .map(DV::DimParam),
524 };
525 Dimension {
526 value,
527 ..Default::default()
528 }
529 })
530 .collect();
531 TensorShapeProto { dim }
532}
533
534fn encode_type_proto(graph: &Graph, tp: &TypeProto) -> onnx::TypeProto {
536 let value = match tp {
537 TypeProto::Tensor { dtype, shape } => type_proto::Value::TensorType(type_proto::Tensor {
538 elem_type: dtype.to_onnx(),
539 shape: Some(encode_shape(graph, shape)),
540 }),
541 TypeProto::SparseTensor { dtype, shape } => {
542 type_proto::Value::SparseTensorType(type_proto::SparseTensor {
543 elem_type: dtype.to_onnx(),
544 shape: Some(encode_shape(graph, shape)),
545 })
546 }
547 TypeProto::Sequence(inner) => {
548 type_proto::Value::SequenceType(Box::new(type_proto::Sequence {
549 elem_type: Some(Box::new(encode_type_proto(graph, inner))),
550 }))
551 }
552 TypeProto::Optional(inner) => {
553 type_proto::Value::OptionalType(Box::new(type_proto::Optional {
554 elem_type: Some(Box::new(encode_type_proto(graph, inner))),
555 }))
556 }
557 TypeProto::Map { key, value } => type_proto::Value::MapType(Box::new(type_proto::Map {
558 key_type: key.to_onnx(),
559 value_type: Some(Box::new(encode_type_proto(graph, value))),
560 })),
561 };
562 onnx::TypeProto {
563 value: Some(value),
564 ..Default::default()
565 }
566}
567
568fn value_name(graph: &Graph, vid: ValueId) -> Option<&str> {
570 graph.try_value(vid).and_then(|v| v.name.as_deref())
571}