onnx_runtime_ir/node.rs
1//! Graph nodes (operations) and their attributes.
2
3use std::collections::HashMap;
4
5use crate::arena::ArenaKey;
6use crate::device::DeviceId;
7use crate::graph::Graph;
8use crate::shape::Shape;
9use crate::tensor::{SparseTensorData, TensorData, TypeProto};
10use crate::value::ValueId;
11
12/// The operator domain for operators this runtime defines itself.
13///
14/// Anything we invent goes here, and nothing we invent goes in `com.microsoft`
15/// or the default ONNX domain: those namespaces belong to their owners, and an
16/// operator placed in one of them claims a provenance and a specification it
17/// does not have. Consumers reading a graph use the domain to decide whose
18/// definition applies, so getting it wrong is a factual error about the model,
19/// not a naming preference.
20pub const RUNTIME_DOMAIN: &str = "pkg.nxrt";
21
22/// Unique identifier for a [`Node`] within a [`Graph`](crate::Graph).
23#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
24pub struct NodeId(pub u32);
25
26impl ArenaKey for NodeId {
27 fn from_raw(raw: u32) -> Self {
28 NodeId(raw)
29 }
30 fn to_raw(self) -> u32 {
31 self.0
32 }
33}
34
35/// An operation in the graph.
36///
37/// Inputs are `Option<ValueId>` because ONNX ops may have optional (skipped)
38/// inputs represented by empty names; a `None` slot preserves positional
39/// arity. Outputs are always present (SSA values).
40#[derive(Clone, Debug)]
41pub struct Node {
42 pub id: NodeId,
43 /// Optional ONNX node name (`""` means unnamed).
44 pub name: String,
45 pub op_type: String,
46 /// Operator domain (`""` == the default ONNX domain).
47 pub domain: String,
48 /// Opset version of the operator called.
49 ///
50 /// If `None`, the version is unspecified and follows the owning graph's
51 /// `opset_imports`. This property is special to ONNX IR to allow mixed opset
52 /// usage in a graph for more flexible graph transformations; it does not
53 /// exist in the ONNX protobuf spec. For example, a fusion may emit an
54 /// opset-24 `Swish` into a graph whose other default-domain nodes still use
55 /// the graph's older exported opset, avoiding a false graph-wide upgrade.
56 pub version: Option<i64>,
57 pub inputs: Vec<Option<ValueId>>,
58 pub outputs: Vec<ValueId>,
59 pub attributes: HashMap<String, Attribute>,
60 pub doc_string: Option<String>,
61 /// Device placement, filled in by the placement pass.
62 pub device: Option<DeviceId>,
63 /// Position in the final execution schedule, filled in by the scheduler.
64 pub exec_order: Option<usize>,
65}
66
67impl Node {
68 /// A new node with the given op type and edges, and no attributes.
69 pub fn new(
70 id: NodeId,
71 op_type: impl Into<String>,
72 inputs: Vec<Option<ValueId>>,
73 outputs: Vec<ValueId>,
74 ) -> Self {
75 Self {
76 id,
77 name: String::new(),
78 op_type: op_type.into(),
79 domain: String::new(),
80 version: None,
81 inputs,
82 outputs,
83 attributes: HashMap::new(),
84 doc_string: None,
85 device: None,
86 exec_order: None,
87 }
88 }
89
90 /// Iterate over the present (non-skipped) input value ids.
91 pub fn input_values(&self) -> impl Iterator<Item = ValueId> + '_ {
92 self.inputs.iter().filter_map(|slot| *slot)
93 }
94
95 /// Look up an attribute by name.
96 pub fn attr(&self, name: &str) -> Option<&Attribute> {
97 self.attributes.get(name)
98 }
99
100 /// Whether this node belongs to the default ONNX operator domain.
101 ///
102 /// Relies on the post-load invariant that the loader canonicalizes the
103 /// default domain to `""` (see [`crate::normalize_domain`]), so this is a
104 /// simple emptiness test — the `"ai.onnx"` spelling never reaches loaded IR.
105 #[inline]
106 #[must_use]
107 pub fn is_default_domain(&self) -> bool {
108 self.domain.is_empty()
109 }
110
111 /// This node's own opset version, if it names one that could be real.
112 ///
113 /// The single owner of that judgement, so every subsystem reading
114 /// [`Node::version`] agrees about the same node. Values that cannot be an
115 /// opset — negative, zero, or beyond what any opset could plausibly reach —
116 /// yield `None`: a node claiming them describes IR that is already wrong,
117 /// and the graph's own import is the better answer.
118 ///
119 /// Callers with a graph in hand should prefer
120 /// [`Graph::effective_opset`](crate::Graph::effective_opset), which falls
121 /// back to that import. Shape inference has only the node and a map of
122 /// imports, so it uses this directly.
123 #[inline]
124 #[must_use]
125 pub fn local_opset(&self) -> Option<u64> {
126 match self.version {
127 Some(version) if (1..=MAX_PLAUSIBLE_OPSET).contains(&version) => Some(version as u64),
128 _ => None,
129 }
130 }
131}
132
133/// No ONNX opset will plausibly reach this, so a larger `Node::version` is a
134/// mistake rather than a version we do not know yet.
135pub(crate) const MAX_PLAUSIBLE_OPSET: i64 = 1_000;
136
137/// An ONNX operator attribute. Covers all attribute value kinds.
138#[derive(Clone, Debug)]
139pub enum Attribute {
140 Int(i64),
141 Float(f32),
142 /// An ONNX `STRING` attribute. Stored as **raw bytes**, not `String`, so
143 /// that the load/dump path round-trips the payload byte-exactly: ONNX
144 /// `STRING` attributes are arbitrary byte strings (e.g. an opaque compiled
145 /// blob) that are not guaranteed to be valid UTF-8. Use [`Attribute::as_str`]
146 /// to view the bytes as UTF-8 text when that is meaningful.
147 String(Vec<u8>),
148 Ints(Vec<i64>),
149 Floats(Vec<f32>),
150 /// An ONNX `STRINGS` attribute — a list of raw byte strings (see
151 /// [`Attribute::String`] for why bytes rather than `String`).
152 Strings(Vec<Vec<u8>>),
153 Tensor(TensorData),
154 Tensors(Vec<TensorData>),
155 SparseTensor(SparseTensorData),
156 SparseTensors(Vec<SparseTensorData>),
157 /// A subgraph body (control-flow ops: If/Loop/Scan). Stored inline; the
158 /// owning [`Graph`] also indexes it in `subgraphs` for traversal.
159 Graph(Box<Graph>),
160 Graphs(Vec<Graph>),
161 TypeProto(TypeProto),
162 TypeProtos(Vec<TypeProto>),
163}
164
165impl Attribute {
166 /// The `i64` value, if this is an [`Attribute::Int`].
167 pub fn as_int(&self) -> Option<i64> {
168 match self {
169 Attribute::Int(v) => Some(*v),
170 _ => None,
171 }
172 }
173
174 /// The `f32` value, if this is an [`Attribute::Float`].
175 pub fn as_float(&self) -> Option<f32> {
176 match self {
177 Attribute::Float(v) => Some(*v),
178 _ => None,
179 }
180 }
181
182 /// The value as UTF-8 text, if this is an [`Attribute::String`] whose bytes
183 /// are valid UTF-8. Returns `None` for a non-string attribute or for string
184 /// bytes that are not valid UTF-8 (e.g. an opaque binary payload).
185 pub fn as_str(&self) -> Option<&str> {
186 match self {
187 Attribute::String(v) => std::str::from_utf8(v).ok(),
188 _ => None,
189 }
190 }
191
192 /// The raw bytes of an [`Attribute::String`], regardless of whether they are
193 /// valid UTF-8. Returns `None` for any other attribute kind.
194 pub fn as_bytes(&self) -> Option<&[u8]> {
195 match self {
196 Attribute::String(v) => Some(v),
197 _ => None,
198 }
199 }
200
201 /// The `&[i64]` slice, if this is an [`Attribute::Ints`].
202 pub fn as_ints(&self) -> Option<&[i64]> {
203 match self {
204 Attribute::Ints(v) => Some(v),
205 _ => None,
206 }
207 }
208
209 /// Interpret an `Ints` attribute as a shape of static dims.
210 pub fn as_shape(&self) -> Option<Shape> {
211 self.as_ints()
212 .map(|v| v.iter().map(|&d| (d as usize).into()).collect())
213 }
214}