Skip to main content

onnx_runtime_ir/
graph.rs

1//! The mutable graph model: node/value arenas, edge-consistent mutation,
2//! topological ordering, and validation (see `docs/ORT2.md` §3.3 and §3.5).
3
4use std::collections::{BinaryHeap, HashMap, HashSet};
5
6use crate::arena::Arena;
7use crate::dtype::DataType;
8use crate::error::GraphError;
9use crate::node::{Node, NodeId};
10use crate::shape::{Shape, SymbolConstraints, SymbolId};
11use crate::tensor::WeightRef;
12use crate::value::{Value, ValueId};
13
14/// A computation graph in SSA form.
15///
16/// Nodes and values live in [`Arena`]s keyed by [`NodeId`] / [`ValueId`]. The
17/// mutation API keeps producer/consumer edges consistent, so optimization
18/// passes can rewrite the graph and then [`Graph::validate`] it.
19#[derive(Clone, Debug, Default)]
20pub struct Graph {
21    pub nodes: Arena<NodeId, Node>,
22    pub values: Arena<ValueId, Value>,
23    /// Graph inputs, in order. These have no producer.
24    pub inputs: Vec<ValueId>,
25    /// Graph outputs, in order.
26    pub outputs: Vec<ValueId>,
27    /// Constant initializer weights, keyed by the value they populate.
28    pub initializers: HashMap<ValueId, WeightRef>,
29    /// Constraints on symbolic dimensions.
30    pub symbol_constraints: HashMap<SymbolId, SymbolConstraints>,
31    /// Imported opsets: domain → version.
32    pub opset_imports: HashMap<String, u64>,
33    /// Subgraph bodies for control-flow ops, keyed by `(node, attr_name)`.
34    pub subgraphs: HashMap<(NodeId, String), Graph>,
35
36    next_symbol: u32,
37    symbol_names: HashMap<String, SymbolId>,
38}
39
40impl Graph {
41    /// An empty graph.
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    // === Query API ===
47
48    /// Borrow a node. Panics if `id` is not live; use
49    /// [`Graph::try_node`] for a checked lookup.
50    pub fn node(&self, id: NodeId) -> &Node {
51        self.nodes.get(id).expect("node id not live in graph")
52    }
53
54    /// Mutably borrow a node. Panics if `id` is not live.
55    pub fn node_mut(&mut self, id: NodeId) -> &mut Node {
56        self.nodes.get_mut(id).expect("node id not live in graph")
57    }
58
59    /// Checked node lookup.
60    pub fn try_node(&self, id: NodeId) -> Option<&Node> {
61        self.nodes.get(id)
62    }
63
64    /// Borrow a value. Panics if `id` is not live; use
65    /// [`Graph::try_value`] for a checked lookup.
66    pub fn value(&self, id: ValueId) -> &Value {
67        self.values.get(id).expect("value id not live in graph")
68    }
69
70    /// Mutably borrow a value. Panics if `id` is not live.
71    pub fn value_mut(&mut self, id: ValueId) -> &mut Value {
72        self.values.get_mut(id).expect("value id not live in graph")
73    }
74
75    /// Checked value lookup.
76    pub fn try_value(&self, id: ValueId) -> Option<&Value> {
77        self.values.get(id)
78    }
79
80    /// Number of live nodes.
81    pub fn num_nodes(&self) -> usize {
82        self.nodes.len()
83    }
84
85    /// Number of live values.
86    pub fn num_values(&self) -> usize {
87        self.values.len()
88    }
89
90    // === Symbolic dimensions ===
91
92    /// Allocate a fresh symbolic dimension with an optional name (no dedup).
93    pub fn create_symbol(&mut self, name: Option<String>) -> SymbolId {
94        let id = SymbolId(self.next_symbol);
95        self.next_symbol += 1;
96        self.symbol_constraints
97            .insert(id, SymbolConstraints::new(id, name.clone()));
98        if let Some(n) = name {
99            self.symbol_names.insert(n, id);
100        }
101        id
102    }
103
104    /// Intern a symbolic dimension by protobuf dim-param name: repeated names
105    /// resolve to the same [`SymbolId`] (graph-construction invariant §3.5.4).
106    pub fn intern_symbol(&mut self, name: &str) -> SymbolId {
107        if let Some(id) = self.symbol_names.get(name) {
108            return *id;
109        }
110        self.create_symbol(Some(name.to_string()))
111    }
112
113    // === Construction helpers ===
114
115    /// Create a new anonymous value with a contiguous default layout.
116    pub fn create_value(&mut self, dtype: DataType, shape: Shape) -> ValueId {
117        self.values
118            .insert_with(|vid| Value::new(vid, dtype, shape.clone()))
119    }
120
121    /// Create a new named value.
122    pub fn create_named_value(
123        &mut self,
124        name: impl Into<String>,
125        dtype: DataType,
126        shape: Shape,
127    ) -> ValueId {
128        let id = self.create_value(dtype, shape);
129        self.value_mut(id).name = Some(name.into());
130        id
131    }
132
133    /// Register `value` as a graph input (also clears any producer link).
134    pub fn add_input(&mut self, value: ValueId) {
135        self.inputs.push(value);
136    }
137
138    /// Register `value` as a graph output.
139    pub fn add_output(&mut self, value: ValueId) {
140        self.outputs.push(value);
141    }
142
143    /// Attach initializer weights to `value`.
144    pub fn set_initializer(&mut self, value: ValueId, weight: WeightRef) {
145        self.initializers.insert(value, weight);
146    }
147
148    // === Traversal ===
149
150    /// Direct predecessors: nodes that produce this node's inputs.
151    pub fn predecessors(&self, node: NodeId) -> Vec<NodeId> {
152        let mut out = Vec::new();
153        let mut seen = HashSet::new();
154        for v in self.node(node).input_values() {
155            if let Some(val) = self.values.get(v)
156                && let Some(prod) = val.producer
157                && seen.insert(prod)
158            {
159                out.push(prod);
160            }
161        }
162        out
163    }
164
165    /// Direct successors: nodes that consume this node's outputs.
166    pub fn successors(&self, node: NodeId) -> Vec<NodeId> {
167        let mut out = Vec::new();
168        let mut seen = HashSet::new();
169        for &v in &self.node(node).outputs {
170            if let Some(val) = self.values.get(v) {
171                for &c in &val.consumers {
172                    if seen.insert(c) {
173                        out.push(c);
174                    }
175                }
176            }
177        }
178        out
179    }
180
181    /// All nodes that lie on a path between `inputs` and `outputs`.
182    ///
183    /// Walks backwards from `outputs` via producer edges, stopping at any value
184    /// in `inputs`. Used to extract subgraphs for EP capability claims (§3.4).
185    pub fn nodes_between(&self, inputs: &[ValueId], outputs: &[ValueId]) -> Vec<NodeId> {
186        let boundary: HashSet<ValueId> = inputs.iter().copied().collect();
187        let mut nodes = Vec::new();
188        let mut seen_nodes = HashSet::new();
189        let mut seen_values = HashSet::new();
190        let mut stack: Vec<ValueId> = outputs.to_vec();
191        while let Some(v) = stack.pop() {
192            if boundary.contains(&v) || !seen_values.insert(v) {
193                continue;
194            }
195            let Some(val) = self.values.get(v) else { continue };
196            if let Some(prod) = val.producer {
197                if seen_nodes.insert(prod) {
198                    nodes.push(prod);
199                }
200                for iv in self.node(prod).input_values() {
201                    stack.push(iv);
202                }
203            }
204        }
205        nodes
206    }
207
208    /// Topological order of nodes via Kahn's algorithm.
209    ///
210    /// Ties are broken by ascending [`NodeId`] for deterministic output.
211    /// Returns [`GraphError::CycleDetected`] if the graph has a cycle.
212    pub fn topological_order(&self) -> Result<Vec<NodeId>, GraphError> {
213        let mut in_degree: HashMap<NodeId, usize> =
214            self.nodes.keys().map(|k| (k, 0usize)).collect();
215        let mut adj: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
216
217        for (nid, node) in self.nodes.iter() {
218            for v in node.input_values() {
219                if let Some(val) = self.values.get(v)
220                    && let Some(prod) = val.producer
221                    && self.nodes.contains(prod)
222                {
223                    adj.entry(prod).or_default().push(nid);
224                    *in_degree.entry(nid).or_insert(0) += 1;
225                }
226            }
227        }
228
229        // Min-heap on raw id for deterministic ordering.
230        let mut ready: BinaryHeap<std::cmp::Reverse<u32>> = in_degree
231            .iter()
232            .filter(|(_, d)| **d == 0)
233            .map(|(k, _)| std::cmp::Reverse(k.0))
234            .collect();
235
236        let mut order = Vec::with_capacity(self.nodes.len());
237        while let Some(std::cmp::Reverse(raw)) = ready.pop() {
238            let nid = NodeId(raw);
239            order.push(nid);
240            if let Some(succs) = adj.get(&nid) {
241                for &s in succs {
242                    let d = in_degree.get_mut(&s).expect("successor tracked");
243                    *d -= 1;
244                    if *d == 0 {
245                        ready.push(std::cmp::Reverse(s.0));
246                    }
247                }
248            }
249        }
250
251        if order.len() != self.nodes.len() {
252            return Err(GraphError::CycleDetected);
253        }
254        Ok(order)
255    }
256
257    // === Mutation API ===
258
259    /// Insert a node, wiring its producer/consumer edges. The node's `id`
260    /// field is overwritten with the freshly allocated [`NodeId`].
261    pub fn insert_node(&mut self, node: Node) -> NodeId {
262        let id = self.nodes.insert_with(|nid| {
263            let mut node = node;
264            node.id = nid;
265            node
266        });
267        self.connect_edges(id);
268        id
269    }
270
271    /// Remove a node, disconnecting its edges. Output values left with no
272    /// consumers (and not graph I/O or initializers) are deleted.
273    pub fn remove_node(&mut self, id: NodeId) {
274        if !self.nodes.contains(id) {
275            return;
276        }
277        self.disconnect_edges(id);
278        let outputs = self.node(id).outputs.clone();
279        self.nodes.remove(id);
280        for v in outputs {
281            self.gc_value_if_orphan(v);
282        }
283    }
284
285    /// Replace node `old` in place with `new`, preserving the [`NodeId`].
286    ///
287    /// The old node's edges are disconnected and the new node's edges are
288    /// connected. Values that were outputs of `old` but not of `new` are left
289    /// in place (producer cleared); the caller may prune them.
290    pub fn replace_node(&mut self, old: NodeId, new: Node) -> NodeId {
291        assert!(self.nodes.contains(old), "replace_node: old id not live");
292        self.disconnect_edges(old);
293        {
294            let slot = self.nodes.get_mut(old).expect("old live");
295            let mut new = new;
296            new.id = old;
297            *slot = new;
298        }
299        self.connect_edges(old);
300        old
301    }
302
303    /// Splice `new_node` onto the edge feeding out of `value`:
304    /// `producer(value) → [new_node] → consumers(value)`.
305    ///
306    /// `new_node`'s single input becomes `value`, and it produces a fresh value
307    /// that replaces `value` in all of `value`'s original consumers.
308    pub fn insert_on_edge(&mut self, value: ValueId, new_node: Node) -> NodeId {
309        let (dtype, shape) = {
310            let v = self.value(value);
311            (v.dtype, v.shape.clone())
312        };
313        let new_value = self.create_value(dtype, shape);
314        // Redirect existing consumers onto the new value first (before the new
315        // node itself becomes a consumer of `value`).
316        self.replace_all_uses(value, new_value);
317
318        let mut new_node = new_node;
319        new_node.inputs = vec![Some(value)];
320        new_node.outputs = vec![new_value];
321        self.insert_node(new_node)
322    }
323
324    /// Replace every use of `old_value` with `new_value` in consumer nodes and
325    /// in the graph output list, moving consumer edges accordingly.
326    pub fn replace_all_uses(&mut self, old_value: ValueId, new_value: ValueId) {
327        if old_value == new_value {
328            return;
329        }
330        let consumers = match self.values.get(old_value) {
331            Some(v) => v.consumers.clone(),
332            None => return,
333        };
334        for nid in &consumers {
335            if let Some(node) = self.nodes.get_mut(*nid) {
336                for slot in node.inputs.iter_mut() {
337                    if *slot == Some(old_value) {
338                        *slot = Some(new_value);
339                    }
340                }
341            }
342        }
343        // Move consumer entries from old to new.
344        let mut moved = std::mem::take(&mut self.value_mut(old_value).consumers);
345        if let Some(nv) = self.values.get_mut(new_value) {
346            nv.consumers.append(&mut moved);
347        }
348        // Rewrite graph outputs.
349        for out in self.outputs.iter_mut() {
350            if *out == old_value {
351                *out = new_value;
352            }
353        }
354    }
355
356    // === Validation ===
357
358    /// Verify structural invariants (§3.3). Returns every defect found.
359    pub fn validate(&self) -> Result<(), Vec<GraphError>> {
360        let mut errors = Vec::new();
361
362        // 1. Node edges reference live values; collect produced values.
363        let mut produced: HashMap<ValueId, NodeId> = HashMap::new();
364        for (nid, node) in self.nodes.iter() {
365            for v in node.input_values() {
366                if !self.values.contains(v) {
367                    errors.push(GraphError::DanglingValue(v));
368                }
369            }
370            for &v in &node.outputs {
371                if !self.values.contains(v) {
372                    errors.push(GraphError::DanglingValue(v));
373                    continue;
374                }
375                if produced.insert(v, nid).is_some() {
376                    errors.push(GraphError::DuplicateOutput(v));
377                }
378            }
379        }
380
381        // 2/3. Producer/consumer link consistency.
382        for (vid, val) in self.values.iter() {
383            if let Some(p) = val.producer {
384                if !self.nodes.contains(p) {
385                    errors.push(GraphError::DanglingNode(p));
386                } else if !self.node(p).outputs.contains(&vid) {
387                    errors.push(GraphError::ProducerLinkMismatch(vid));
388                }
389            }
390            for &c in &val.consumers {
391                if !self.nodes.contains(c) {
392                    errors.push(GraphError::DanglingNode(c));
393                } else if !self.node(c).input_values().any(|iv| iv == vid) {
394                    errors.push(GraphError::ConsumerLinkMismatch(vid));
395                }
396            }
397        }
398
399        // 4. Graph inputs must be sources.
400        for &inp in &self.inputs {
401            if let Some(val) = self.values.get(inp) {
402                if val.producer.is_some() {
403                    errors.push(GraphError::InputHasProducer(inp));
404                }
405            } else {
406                errors.push(GraphError::DanglingValue(inp));
407            }
408        }
409
410        // 5. Graph outputs must be produced (unless they are graph inputs or
411        //    initializers passed straight through).
412        for &out in &self.outputs {
413            match self.values.get(out) {
414                Some(val) => {
415                    let is_source = self.inputs.contains(&out)
416                        || self.initializers.contains_key(&out);
417                    if val.producer.is_none() && !is_source {
418                        errors.push(GraphError::MissingProducer(out));
419                    }
420                }
421                None => errors.push(GraphError::DanglingValue(out)),
422            }
423        }
424
425        // 6. No cycles.
426        if let Err(e) = self.topological_order() {
427            errors.push(e);
428        }
429
430        // 7. Opset imports must have non-zero versions.
431        for (domain, &version) in &self.opset_imports {
432            if version == 0 {
433                errors.push(GraphError::InvalidOpsetImport {
434                    domain: domain.clone(),
435                    version,
436                });
437            }
438        }
439
440        // 8. Subgraphs validate recursively.
441        for sub in self.subgraphs.values() {
442            if let Err(mut sub_errors) = sub.validate() {
443                errors.append(&mut sub_errors);
444            }
445        }
446
447        if errors.is_empty() {
448            Ok(())
449        } else {
450            Err(errors)
451        }
452    }
453
454    // === Private edge maintenance ===
455
456    /// Wire a live node's edges into its input/output values.
457    fn connect_edges(&mut self, id: NodeId) {
458        let inputs: Vec<ValueId> = self.node(id).input_values().collect();
459        let outputs = self.node(id).outputs.clone();
460        for v in inputs {
461            if let Some(val) = self.values.get_mut(v) {
462                val.consumers.push(id);
463            }
464        }
465        for v in outputs {
466            if let Some(val) = self.values.get_mut(v) {
467                val.producer = Some(id);
468            }
469        }
470    }
471
472    /// Remove a live node's edges from its input/output values, without
473    /// deleting the values or the node itself.
474    fn disconnect_edges(&mut self, id: NodeId) {
475        let inputs: Vec<ValueId> = self.node(id).input_values().collect();
476        let outputs = self.node(id).outputs.clone();
477        for v in inputs {
478            if let Some(val) = self.values.get_mut(v) {
479                val.consumers.retain(|&c| c != id);
480            }
481        }
482        for v in outputs {
483            if let Some(val) = self.values.get_mut(v)
484                && val.producer == Some(id)
485            {
486                val.producer = None;
487            }
488        }
489    }
490
491    /// Delete `value` if it has no producer, no consumers, and is not part of
492    /// the graph's I/O or initializers.
493    fn gc_value_if_orphan(&mut self, value: ValueId) {
494        let orphan = match self.values.get(value) {
495            Some(v) => {
496                v.producer.is_none()
497                    && v.consumers.is_empty()
498                    && !self.inputs.contains(&value)
499                    && !self.outputs.contains(&value)
500                    && !self.initializers.contains_key(&value)
501            }
502            None => false,
503        };
504        if orphan {
505            self.values.remove(value);
506        }
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use crate::shape::static_shape;
514
515    /// Build `a -> Relu -> b -> Add(b, c) -> d`, returning ids.
516    fn sample_graph() -> Graph {
517        let mut g = Graph::new();
518        g.opset_imports.insert(String::new(), 17);
519        let a = g.create_named_value("a", DataType::Float32, static_shape([4]));
520        let c = g.create_named_value("c", DataType::Float32, static_shape([4]));
521        g.add_input(a);
522        g.add_input(c);
523
524        let b = g.create_value(DataType::Float32, static_shape([4]));
525        let relu = Node::new(NodeId(0), "Relu", vec![Some(a)], vec![b]);
526        g.insert_node(relu);
527
528        let d = g.create_named_value("d", DataType::Float32, static_shape([4]));
529        let add = Node::new(NodeId(0), "Add", vec![Some(b), Some(c)], vec![d]);
530        g.insert_node(add);
531        g.add_output(d);
532        g
533    }
534
535    #[test]
536    fn edges_are_wired_on_insert() {
537        let g = sample_graph();
538        assert_eq!(g.num_nodes(), 2);
539        // b has a producer (Relu) and a consumer (Add)
540        let b = g.value(ValueId(2));
541        assert!(b.producer.is_some());
542        assert_eq!(b.consumers.len(), 1);
543    }
544
545    #[test]
546    fn topo_order_is_valid_and_deterministic() {
547        let g = sample_graph();
548        let order = g.topological_order().unwrap();
549        assert_eq!(order.len(), 2);
550        // Relu (NodeId 0) must come before Add (NodeId 1)
551        assert_eq!(order, vec![NodeId(0), NodeId(1)]);
552    }
553
554    #[test]
555    fn validate_accepts_wellformed_graph() {
556        let g = sample_graph();
557        assert!(g.validate().is_ok());
558    }
559
560    #[test]
561    fn predecessors_and_successors() {
562        let g = sample_graph();
563        assert_eq!(g.successors(NodeId(0)), vec![NodeId(1)]);
564        assert_eq!(g.predecessors(NodeId(1)), vec![NodeId(0)]);
565    }
566
567    #[test]
568    fn nodes_between_walks_back() {
569        let g = sample_graph();
570        let between = g.nodes_between(&[ValueId(0), ValueId(1)], &[ValueId(3)]);
571        assert!(between.contains(&NodeId(0)));
572        assert!(between.contains(&NodeId(1)));
573        assert_eq!(between.len(), 2);
574    }
575
576    #[test]
577    fn replace_all_uses_redirects_consumers() {
578        let mut g = sample_graph();
579        // New constant value replaces `b` as Add's input.
580        let e = g.create_value(DataType::Float32, static_shape([4]));
581        g.replace_all_uses(ValueId(2), e);
582        // Add now consumes `e`, not `b`.
583        let add = g.node(NodeId(1));
584        assert!(add.input_values().any(|v| v == e));
585        assert!(!add.input_values().any(|v| v == ValueId(2)));
586        assert_eq!(g.value(e).consumers, vec![NodeId(1)]);
587        assert!(g.value(ValueId(2)).consumers.is_empty());
588    }
589
590    #[test]
591    fn insert_on_edge_splices_node() {
592        let mut g = sample_graph();
593        // Insert an Identity between b (ValueId 2) and its consumer Add.
594        let ident = Node::new(NodeId(0), "Identity", vec![], vec![]);
595        let nid = g.insert_on_edge(ValueId(2), ident);
596        // Add now consumes the new value produced by Identity.
597        let new_out = g.node(nid).outputs[0];
598        assert_eq!(g.value(new_out).producer, Some(nid));
599        assert!(g.node(NodeId(1)).input_values().any(|v| v == new_out));
600        assert!(g.validate().is_ok());
601    }
602
603    #[test]
604    fn remove_node_disconnects_and_gcs() {
605        let mut g = sample_graph();
606        g.remove_node(NodeId(1)); // remove Add
607        assert_eq!(g.num_nodes(), 1);
608        // `d` was Add's only output and a graph output -> kept, producer cleared
609        assert!(g.value(ValueId(3)).producer.is_none());
610        // b lost its consumer
611        assert!(g.value(ValueId(2)).consumers.is_empty());
612    }
613
614    #[test]
615    fn replace_node_preserves_id() {
616        let mut g = sample_graph();
617        let d = g.node(NodeId(1)).outputs[0];
618        let b = g.node(NodeId(1)).inputs[0];
619        let c = g.node(NodeId(1)).inputs[1];
620        let sub = Node::new(NodeId(0), "Sub", vec![b, c], vec![d]);
621        let id = g.replace_node(NodeId(1), sub);
622        assert_eq!(id, NodeId(1));
623        assert_eq!(g.node(NodeId(1)).op_type, "Sub");
624        assert!(g.validate().is_ok());
625    }
626
627    #[test]
628    fn cycle_is_detected() {
629        let mut g = Graph::new();
630        let v0 = g.create_value(DataType::Float32, static_shape([1]));
631        let v1 = g.create_value(DataType::Float32, static_shape([1]));
632        // n0: v1 -> v0 ; n1: v0 -> v1  (cycle)
633        g.insert_node(Node::new(NodeId(0), "A", vec![Some(v1)], vec![v0]));
634        g.insert_node(Node::new(NodeId(0), "B", vec![Some(v0)], vec![v1]));
635        assert_eq!(g.topological_order(), Err(GraphError::CycleDetected));
636        assert!(g.validate().is_err());
637    }
638
639    #[test]
640    fn intern_symbol_dedups_by_name() {
641        let mut g = Graph::new();
642        let s1 = g.intern_symbol("batch");
643        let s2 = g.intern_symbol("batch");
644        let s3 = g.intern_symbol("seq");
645        assert_eq!(s1, s2);
646        assert_ne!(s1, s3);
647    }
648}