1use super::onnx_proto::{AttributeProto, GraphProto, ModelProto, NodeProto, TensorProto};
4use crate::{Error, Result};
5use protobuf::Message;
6use std::{
7 collections::{HashMap, HashSet},
8 path::Path,
9};
10
11pub mod proto {
13 pub use super::super::onnx_proto::*;
14 pub use protobuf::Message;
15}
16
17pub struct Model {
19 name: String,
20 inputs: Vec<String>,
21 input_shapes: HashMap<String, Vec<Option<usize>>>,
22 outputs: Vec<String>,
23 opsets: Vec<(String, i64)>,
24 nodes: Vec<Node>,
25 initializers: HashMap<String, Tensor>,
26 initializer_order: Vec<String>,
27 producers: HashMap<String, usize>,
28 consumers: HashMap<String, Vec<usize>>,
29}
30
31impl Model {
32 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
34 let path = path.as_ref();
35 let bytes = std::fs::read(path)
36 .map_err(|error| Error::Io(error).context(format!("reading {}", path.display())))?;
37 Self::from_bytes(&bytes)
38 .map_err(|error| error.context(format!("parsing {}", path.display())))
39 }
40
41 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
43 let mut protobuf = ModelProto::parse_from_bytes(bytes)
44 .map_err(|error| Error::Message(format!("invalid ONNX protobuf: {error}")))?;
45 let opsets = protobuf
46 .opset_import
47 .iter()
48 .map(|opset| (opset.domain.clone(), opset.version))
49 .collect();
50 let graph = protobuf
51 .graph
52 .take()
53 .ok_or_else(|| Error::Message("missing ONNX graph".into()))?;
54 Self::from_graph(graph, opsets)
55 }
56
57 fn from_graph(mut graph: GraphProto, opsets: Vec<(String, i64)>) -> Result<Self> {
58 let mut initializers = HashMap::new();
59 let mut initializer_order = Vec::new();
60 for protobuf in std::mem::take(&mut graph.initializer) {
61 if protobuf.name.is_empty() {
62 return Err(Error::Message("unnamed ONNX initializer".into()));
63 }
64 let name = protobuf.name.clone();
65 if initializers
66 .insert(name.clone(), Tensor { protobuf })
67 .is_some()
68 {
69 return Err(Error::Message(format!("duplicate ONNX initializer {name}")));
70 }
71 initializer_order.push(name);
72 }
73 let nodes = std::mem::take(&mut graph.node)
74 .into_iter()
75 .map(|protobuf| Node { protobuf })
76 .collect::<Vec<_>>();
77 let mut producers = HashMap::new();
78 let mut consumers = HashMap::<String, Vec<usize>>::new();
79 for (index, node) in nodes.iter().enumerate() {
80 for output in node.outputs().iter().filter(|name| !name.is_empty()) {
81 if producers.insert(output.clone(), index).is_some() {
82 return Err(Error::Message(format!("duplicate ONNX value {output}")));
83 }
84 }
85 for input in node.inputs().iter().filter(|name| !name.is_empty()) {
86 consumers.entry(input.clone()).or_default().push(index);
87 }
88 }
89 let mut inputs = Vec::with_capacity(graph.input.len());
90 let mut input_shapes = HashMap::new();
91 for value in graph.input {
92 let shape = value
93 .type_
94 .as_ref()
95 .map(|kind| kind.tensor_type())
96 .and_then(|tensor| tensor.shape.as_ref())
97 .map(|shape| {
98 shape
99 .dim
100 .iter()
101 .map(|dimension| {
102 usize::try_from(dimension.dim_value())
103 .ok()
104 .filter(|&value| value > 0)
105 })
106 .collect()
107 })
108 .unwrap_or_default();
109 input_shapes.insert(value.name.clone(), shape);
110 inputs.push(value.name);
111 }
112 Ok(Self {
113 name: graph.name,
114 inputs,
115 input_shapes,
116 outputs: graph.output.into_iter().map(|value| value.name).collect(),
117 opsets,
118 nodes,
119 initializers,
120 initializer_order,
121 producers,
122 consumers,
123 })
124 }
125
126 #[must_use]
128 pub fn name(&self) -> &str {
129 &self.name
130 }
131
132 #[must_use]
134 pub fn nodes(&self) -> &[Node] {
135 &self.nodes
136 }
137
138 #[must_use]
140 pub fn inputs(&self) -> &[String] {
141 &self.inputs
142 }
143
144 pub fn input_shape(&self, name: &str) -> Result<&[Option<usize>]> {
146 self.input_shapes
147 .get(name)
148 .map(Vec::as_slice)
149 .ok_or_else(|| Error::Message(format!("missing ONNX input {name}")))
150 }
151
152 #[must_use]
154 pub fn opsets(&self) -> &[(String, i64)] {
155 &self.opsets
156 }
157
158 #[must_use]
160 pub fn outputs(&self) -> &[String] {
161 &self.outputs
162 }
163
164 pub fn initializer(&self, name: &str) -> Result<&Tensor> {
166 self.initializers
167 .get(name)
168 .ok_or_else(|| Error::Message(format!("missing ONNX initializer {name}")))
169 }
170
171 pub fn initializers(&self) -> impl Iterator<Item = (&str, &Tensor)> {
173 self.initializer_order.iter().map(|name| {
174 (
175 name.as_str(),
176 self.initializers
177 .get(name)
178 .expect("initializer order and index agree"),
179 )
180 })
181 }
182
183 #[must_use]
185 pub fn producer(&self, value: &str) -> Option<&Node> {
186 self.producers.get(value).map(|&index| &self.nodes[index])
187 }
188
189 pub fn consumers<'a>(&'a self, value: &'a str) -> impl Iterator<Item = &'a Node> + 'a {
191 self.consumers
192 .get(value)
193 .into_iter()
194 .flatten()
195 .map(|&index| &self.nodes[index])
196 }
197}
198
199pub struct Node {
201 protobuf: NodeProto,
202}
203
204impl Node {
205 #[must_use]
207 pub fn name(&self) -> &str {
208 &self.protobuf.name
209 }
210
211 #[must_use]
213 pub fn op_type(&self) -> &str {
214 &self.protobuf.op_type
215 }
216
217 #[must_use]
219 pub fn domain(&self) -> &str {
220 &self.protobuf.domain
221 }
222
223 #[must_use]
225 pub fn inputs(&self) -> &[String] {
226 &self.protobuf.input
227 }
228
229 #[must_use]
231 pub fn outputs(&self) -> &[String] {
232 &self.protobuf.output
233 }
234
235 pub fn output(&self) -> Result<&str> {
237 match self.outputs() {
238 [output] if !output.is_empty() => Ok(output),
239 _ => Err(Error::Message(format!(
240 "{} must have one output",
241 self.op_type()
242 ))),
243 }
244 }
245
246 #[must_use]
248 pub fn has(&self, name: &str) -> bool {
249 self.attribute(name).is_some()
250 }
251
252 pub fn validate_attributes(&self, allowed: &[&str]) -> Result<()> {
254 let mut names = HashSet::new();
255 if self.protobuf.attribute.iter().any(|attribute| {
256 !allowed.contains(&attribute.name.as_str()) || !names.insert(attribute.name.as_str())
257 }) {
258 return Err(Error::Message(format!(
259 "{} has an unknown or duplicate attribute",
260 self.op_type()
261 )));
262 }
263 Ok(())
264 }
265
266 #[must_use]
268 pub fn integer(&self, name: &str, default: i64) -> i64 {
269 self.attribute(name)
270 .map_or(default, |attribute| attribute.i)
271 }
272
273 #[must_use]
275 pub fn float(&self, name: &str, default: f64) -> f64 {
276 self.attribute(name)
277 .map_or(default, |attribute| f64::from(attribute.f))
278 }
279
280 #[must_use]
282 pub fn integers(&self, name: &str, default: &[i64]) -> Vec<i64> {
283 self.attribute(name)
284 .map_or_else(|| default.to_vec(), |attribute| attribute.ints.clone())
285 }
286
287 pub fn text(&self, name: &str, default: &str) -> Result<String> {
289 match self.attribute(name) {
290 None => Ok(default.to_owned()),
291 Some(attribute) => String::from_utf8(attribute.s.clone())
292 .map_err(|_| Error::Message(format!("{name} is not UTF-8"))),
293 }
294 }
295
296 pub fn tensor(&self, name: &str) -> Result<TensorView<'_>> {
298 self.attribute(name)
299 .and_then(|attribute| attribute.t.as_ref())
300 .map(|protobuf| TensorView { protobuf })
301 .ok_or_else(|| Error::Message(format!("missing tensor attribute {name}")))
302 }
303
304 fn attribute(&self, name: &str) -> Option<&AttributeProto> {
305 self.protobuf
306 .attribute
307 .iter()
308 .find(|attribute| attribute.name == name)
309 }
310}
311
312pub struct Tensor {
314 protobuf: TensorProto,
315}
316
317impl Tensor {
318 #[must_use]
320 pub fn view(&self) -> TensorView<'_> {
321 TensorView {
322 protobuf: &self.protobuf,
323 }
324 }
325 #[must_use]
327 pub fn name(&self) -> &str {
328 &self.protobuf.name
329 }
330
331 pub fn shape(&self) -> Result<Vec<usize>> {
336 self.protobuf
337 .dims
338 .iter()
339 .map(|&dimension| {
340 usize::try_from(dimension).ok().ok_or_else(|| {
341 Error::Message(format!("{} has an invalid dimension", self.name()))
342 })
343 })
344 .collect()
345 }
346
347 pub fn count(&self) -> Result<usize> {
349 self.shape()?
350 .into_iter()
351 .try_fold(1usize, |count, dimension| {
352 count.checked_mul(dimension).ok_or_else(|| {
353 Error::Message(format!("{} element count overflow", self.name()))
354 })
355 })
356 }
357
358 pub fn f32s(&self) -> Result<Vec<f32>> {
360 if self.protobuf.data_type != 1 || self.protobuf.data_location.value() != 0 {
361 return Err(Error::Message(format!(
362 "{} is not embedded float32",
363 self.name()
364 )));
365 }
366 let count = self.count()?;
367 let values = if self.protobuf.raw_data.is_empty() {
368 self.protobuf.float_data.clone()
369 } else {
370 let bytes = count
371 .checked_mul(4)
372 .ok_or_else(|| Error::Message("ONNX tensor byte count overflow".into()))?;
373 if self.protobuf.raw_data.len() != bytes {
374 return Err(Error::Message(format!("{} is truncated", self.name())));
375 }
376 self.protobuf
377 .raw_data
378 .as_chunks::<4>()
379 .0
380 .iter()
381 .map(|bytes| f32::from_le_bytes(*bytes))
382 .collect()
383 };
384 if values.len() != count || values.iter().any(|value| !value.is_finite()) {
385 return Err(Error::Message(format!(
386 "{} has invalid float values",
387 self.name()
388 )));
389 }
390 Ok(values)
391 }
392
393 pub fn f64s(&self) -> Result<Vec<f64>> {
395 Ok(self.f32s()?.into_iter().map(f64::from).collect())
396 }
397
398 pub fn i64s(&self) -> Result<Vec<i64>> {
400 if self.protobuf.data_type != 7 || self.protobuf.data_location.value() != 0 {
401 return Err(Error::Message(format!(
402 "{} is not embedded int64",
403 self.name()
404 )));
405 }
406 let count = self.count()?;
407 let values = if self.protobuf.raw_data.is_empty() {
408 self.protobuf.int64_data.clone()
409 } else {
410 let bytes = count
411 .checked_mul(8)
412 .ok_or_else(|| Error::Message("ONNX tensor byte count overflow".into()))?;
413 if self.protobuf.raw_data.len() != bytes {
414 return Err(Error::Message(format!("{} is truncated", self.name())));
415 }
416 self.protobuf
417 .raw_data
418 .as_chunks::<8>()
419 .0
420 .iter()
421 .map(|bytes| i64::from_le_bytes(*bytes))
422 .collect()
423 };
424 if values.len() != count {
425 return Err(Error::Message(format!("{} is truncated", self.name())));
426 }
427 Ok(values)
428 }
429}
430
431#[derive(Clone, Copy)]
433pub struct TensorView<'a> {
434 protobuf: &'a TensorProto,
435}
436
437impl TensorView<'_> {
438 #[must_use]
440 pub fn name(&self) -> &str {
441 &self.protobuf.name
442 }
443
444 pub fn shape(&self) -> Result<Vec<usize>> {
449 self.protobuf
450 .dims
451 .iter()
452 .map(|&dimension| {
453 usize::try_from(dimension).ok().ok_or_else(|| {
454 Error::Message(format!("{} has an invalid dimension", self.name()))
455 })
456 })
457 .collect()
458 }
459
460 pub fn count(&self) -> Result<usize> {
462 self.shape()?
463 .into_iter()
464 .try_fold(1usize, |count, dimension| {
465 count.checked_mul(dimension).ok_or_else(|| {
466 Error::Message(format!("{} element count overflow", self.name()))
467 })
468 })
469 }
470
471 pub fn f32s(&self) -> Result<Vec<f32>> {
473 if self.protobuf.data_type != 1 || self.protobuf.data_location.value() != 0 {
474 return Err(Error::Message(format!(
475 "{} is not embedded float32",
476 self.name()
477 )));
478 }
479 let count = self.count()?;
480 let values = if self.protobuf.raw_data.is_empty() {
481 self.protobuf.float_data.clone()
482 } else {
483 let bytes = count
484 .checked_mul(4)
485 .ok_or_else(|| Error::Message("ONNX tensor byte count overflow".into()))?;
486 if self.protobuf.raw_data.len() != bytes {
487 return Err(Error::Message(format!("{} is truncated", self.name())));
488 }
489 self.protobuf
490 .raw_data
491 .as_chunks::<4>()
492 .0
493 .iter()
494 .map(|bytes| f32::from_le_bytes(*bytes))
495 .collect()
496 };
497 if values.len() != count || values.iter().any(|value| !value.is_finite()) {
498 return Err(Error::Message(format!(
499 "{} has invalid float values",
500 self.name()
501 )));
502 }
503 Ok(values)
504 }
505
506 pub fn i64s(&self) -> Result<Vec<i64>> {
508 if self.protobuf.data_type != 7 || self.protobuf.data_location.value() != 0 {
509 return Err(Error::Message(format!(
510 "{} is not embedded int64",
511 self.name()
512 )));
513 }
514 let count = self.count()?;
515 let values = if self.protobuf.raw_data.is_empty() {
516 self.protobuf.int64_data.clone()
517 } else {
518 let bytes = count
519 .checked_mul(8)
520 .ok_or_else(|| Error::Message("ONNX tensor byte count overflow".into()))?;
521 if self.protobuf.raw_data.len() != bytes {
522 return Err(Error::Message(format!("{} is truncated", self.name())));
523 }
524 self.protobuf
525 .raw_data
526 .as_chunks::<8>()
527 .0
528 .iter()
529 .map(|bytes| i64::from_le_bytes(*bytes))
530 .collect()
531 };
532 if values.len() != count {
533 return Err(Error::Message(format!("{} is truncated", self.name())));
534 }
535 Ok(values)
536 }
537}
538
539pub fn axis(value: i64, rank: usize) -> Result<usize> {
541 let rank_i64 = i64::try_from(rank).map_err(|_| Error::Message("rank overflow".into()))?;
542 let resolved = if value < 0 { value + rank_i64 } else { value };
543 usize::try_from(resolved)
544 .ok()
545 .filter(|&axis| axis < rank)
546 .ok_or_else(|| Error::Message(format!("axis {value} is invalid for rank {rank}")))
547}
548
549pub fn strides(shape: &[usize]) -> Result<Vec<usize>> {
551 let mut stride = 1usize;
552 let mut output = vec![0; shape.len()];
553 for (index, &dimension) in shape.iter().enumerate().rev() {
554 output[index] = stride;
555 stride = stride
556 .checked_mul(dimension)
557 .ok_or_else(|| Error::Message("shape stride overflow".into()))?;
558 }
559 Ok(output)
560}
561
562pub fn broadcast(left: &[usize], right: &[usize]) -> Result<Vec<usize>> {
564 let rank = left.len().max(right.len());
565 (0..rank)
566 .map(|index| {
567 let from_right = rank - index;
568 let a = left
569 .len()
570 .checked_sub(from_right)
571 .and_then(|position| left.get(position))
572 .copied()
573 .unwrap_or(1);
574 let b = right
575 .len()
576 .checked_sub(from_right)
577 .and_then(|position| right.get(position))
578 .copied()
579 .unwrap_or(1);
580 if a == b || a == 1 || b == 1 {
581 Ok(a.max(b))
582 } else {
583 Err(Error::Message(format!(
584 "cannot broadcast {left:?} and {right:?}"
585 )))
586 }
587 })
588 .collect()
589}
590
591pub fn broadcast_index(index: usize, output: &[usize], input: &[usize]) -> Result<usize> {
593 if input.len() > output.len() {
594 return Err(Error::Message(
595 "broadcast input rank exceeds output rank".into(),
596 ));
597 }
598 let count = output.iter().try_fold(1usize, |count, dimension| {
599 count
600 .checked_mul(*dimension)
601 .ok_or_else(|| Error::Message("broadcast output size overflow".into()))
602 })?;
603 if index >= count {
604 return Err(Error::Message("broadcast index exceeds output".into()));
605 }
606 let output_strides = strides(output)?;
607 let input_strides = strides(input)?;
608 let offset = output.len() - input.len();
609 let mut result = 0usize;
610 for dimension in 0..input.len() {
611 let output_dimension = output[offset + dimension];
612 let input_dimension = input[dimension];
613 if input_dimension != 1 && input_dimension != output_dimension {
614 return Err(Error::Message("input is not broadcast-compatible".into()));
615 }
616 let coordinate = (index / output_strides[offset + dimension]) % output_dimension;
617 if input_dimension != 1 {
618 result = result
619 .checked_add(
620 coordinate
621 .checked_mul(input_strides[dimension])
622 .ok_or_else(|| Error::Message("broadcast index overflow".into()))?,
623 )
624 .ok_or_else(|| Error::Message("broadcast index overflow".into()))?;
625 }
626 }
627 Ok(result)
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633
634 fn roundtrip_tensor(tensor: TensorProto) -> Model {
635 let model = ModelProto {
636 graph: Some(GraphProto {
637 initializer: vec![tensor.clone()],
638 node: vec![NodeProto {
639 op_type: "Constant".into(),
640 output: vec!["constant".into()],
641 attribute: vec![AttributeProto {
642 name: "value".into(),
643 t: Some(tensor).into(),
644 ..Default::default()
645 }],
646 ..Default::default()
647 }],
648 ..Default::default()
649 })
650 .into(),
651 ..Default::default()
652 };
653 Model::from_bytes(&model.write_to_bytes().unwrap()).unwrap()
654 }
655
656 #[test]
657 fn empty_initializers_and_tensor_attributes_decode() {
658 for dims in [vec![0], vec![2, 0, 3]] {
659 for data_type in [1, 7] {
660 let model = roundtrip_tensor(TensorProto {
661 name: "onnx::Resize_787".into(),
662 dims: dims.clone(),
663 data_type,
664 ..Default::default()
665 });
666 let tensor = model.initializer("onnx::Resize_787").unwrap();
667 let expected: Vec<usize> = dims.iter().map(|&dim| dim as usize).collect();
668 assert_eq!(tensor.shape().unwrap(), expected);
669 assert_eq!(tensor.count().unwrap(), 0);
670 if data_type == 1 {
671 assert!(tensor.f32s().unwrap().is_empty());
672 assert!(tensor.f64s().unwrap().is_empty());
673 } else {
674 assert!(tensor.i64s().unwrap().is_empty());
675 }
676 for view in [tensor.view(), model.nodes()[0].tensor("value").unwrap()] {
677 assert_eq!(view.shape().unwrap(), expected);
678 assert_eq!(view.count().unwrap(), 0);
679 if data_type == 1 {
680 assert!(view.f32s().unwrap().is_empty());
681 } else {
682 assert!(view.i64s().unwrap().is_empty());
683 }
684 }
685 }
686 }
687 }
688
689 #[test]
690 fn zero_dimensions_do_not_relax_data_or_negative_dimension_checks() {
691 for dims in [vec![0], vec![-1], vec![0, -1]] {
692 let model = roundtrip_tensor(TensorProto {
693 name: "invalid".into(),
694 dims: dims.clone(),
695 data_type: 1,
696 float_data: vec![1.0],
697 ..Default::default()
698 });
699 let tensor = model.initializer("invalid").unwrap();
700 assert!(tensor.f32s().is_err());
701 assert!(tensor.view().f32s().is_err());
702 if dims.contains(&-1) {
703 assert!(tensor.shape().is_err());
704 assert!(tensor.view().shape().is_err());
705 }
706 }
707 }
708
709 #[test]
710 fn rank_zero_tensors_remain_scalars() {
711 let model = roundtrip_tensor(TensorProto {
712 name: "scalar".into(),
713 data_type: 1,
714 float_data: vec![2.5],
715 ..Default::default()
716 });
717 let tensor = model.initializer("scalar").unwrap();
718 assert!(tensor.shape().unwrap().is_empty());
719 assert_eq!(tensor.count().unwrap(), 1);
720 assert_eq!(tensor.f32s().unwrap(), [2.5]);
721 let view = model.nodes()[0].tensor("value").unwrap();
722 assert!(view.shape().unwrap().is_empty());
723 assert_eq!(view.count().unwrap(), 1);
724 assert_eq!(view.f32s().unwrap(), [2.5]);
725 }
726
727 #[test]
728 fn axes_and_broadcasts_are_checked() {
729 assert_eq!(axis(-1, 4).unwrap(), 3);
730 assert!(axis(4, 4).is_err());
731 assert_eq!(broadcast(&[2, 1, 4], &[3, 4]).unwrap(), [2, 3, 4]);
732 assert!(broadcast(&[2], &[3]).is_err());
733 assert_eq!(broadcast_index(23, &[2, 3, 4], &[3, 1]).unwrap(), 2);
734 assert!(broadcast_index(24, &[2, 3, 4], &[3, 1]).is_err());
735 }
736}