Skip to main content

onnx_runtime_optimizer/
dead_node.rs

1//! Dead-node elimination: remove nodes whose outputs are not transitively
2//! consumed by any graph output (see `docs/ORT2.md` §18.1).
3
4use std::collections::HashSet;
5
6use onnx_runtime_ir::{Graph, NodeId, ValueId};
7
8use crate::error::Result;
9use crate::pass::{OptimizationPass, PassContext};
10
11/// Removes nodes that do not contribute to any graph output.
12///
13/// A node is *live* if any of its outputs is (transitively) required to compute
14/// a value in [`Graph::outputs`]. Liveness is found by walking backwards from
15/// the graph outputs through producer edges; every node not reached is removed
16/// via [`Graph::remove_node`], which keeps producer/consumer edges consistent
17/// and garbage-collects orphaned values. Graph inputs and initializers are left
18/// untouched.
19#[derive(Clone, Copy, Debug, Default)]
20pub struct DeadNodeElimination;
21
22impl DeadNodeElimination {
23    /// Node ids reachable (via producer edges) from the graph outputs.
24    fn live_nodes(graph: &Graph) -> HashSet<NodeId> {
25        let mut live: HashSet<NodeId> = HashSet::new();
26        let mut seen_values: HashSet<ValueId> = HashSet::new();
27        let mut stack: Vec<ValueId> = graph.outputs.clone();
28
29        while let Some(v) = stack.pop() {
30            if !seen_values.insert(v) {
31                continue;
32            }
33            let Some(val) = graph.try_value(v) else {
34                continue;
35            };
36            if let Some(prod) = val.producer
37                && live.insert(prod)
38            {
39                for iv in graph.node(prod).input_values() {
40                    stack.push(iv);
41                }
42            }
43        }
44        live
45    }
46}
47
48impl OptimizationPass for DeadNodeElimination {
49    fn name(&self) -> &str {
50        "DeadNodeElimination"
51    }
52
53    fn run(&self, graph: &mut Graph, _ctx: &PassContext) -> Result<()> {
54        let live = Self::live_nodes(graph);
55        let dead: Vec<NodeId> = graph
56            .nodes
57            .keys()
58            .filter(|nid| !live.contains(nid))
59            .collect();
60        for nid in dead {
61            graph.remove_node(nid);
62        }
63        Ok(())
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use onnx_runtime_ir::{DataType, Node, NodeId, static_shape};
71
72    /// `a -> Relu -> b -> Add(b, c) -> d(out)`, plus a dangling
73    /// `a -> Neg -> e` branch whose result `e` feeds nothing.
74    fn graph_with_dangling_branch() -> (Graph, NodeId, NodeId) {
75        let mut g = Graph::new();
76        g.opset_imports.insert(String::new(), 17);
77        let a = g.create_named_value("a", DataType::Float32, static_shape([4]));
78        let c = g.create_named_value("c", DataType::Float32, static_shape([4]));
79        g.add_input(a);
80        g.add_input(c);
81
82        let b = g.create_value(DataType::Float32, static_shape([4]));
83        let relu = g.insert_node(Node::new(NodeId(0), "Relu", vec![Some(a)], vec![b]));
84
85        let d = g.create_named_value("d", DataType::Float32, static_shape([4]));
86        g.insert_node(Node::new(NodeId(0), "Add", vec![Some(b), Some(c)], vec![d]));
87        g.add_output(d);
88
89        // Dangling branch: consumes `a`, produces `e`, which nothing reads.
90        let e = g.create_value(DataType::Float32, static_shape([4]));
91        let neg = g.insert_node(Node::new(NodeId(0), "Neg", vec![Some(a)], vec![e]));
92
93        (g, relu, neg)
94    }
95
96    #[test]
97    fn removes_dangling_branch() {
98        let (mut g, _relu, neg) = graph_with_dangling_branch();
99        assert_eq!(g.num_nodes(), 3);
100        DeadNodeElimination
101            .run(&mut g, &PassContext::new())
102            .unwrap();
103        assert_eq!(g.num_nodes(), 2);
104        assert!(g.try_node(neg).is_none());
105        assert!(g.validate().is_ok());
106    }
107
108    #[test]
109    fn keeps_live_nodes() {
110        let (mut g, relu, _neg) = graph_with_dangling_branch();
111        DeadNodeElimination
112            .run(&mut g, &PassContext::new())
113            .unwrap();
114        // Relu and Add remain.
115        assert!(g.try_node(relu).is_some());
116        assert!(g.validate().is_ok());
117    }
118
119    #[test]
120    fn dead_value_is_garbage_collected() {
121        let (mut g, _relu, _neg) = graph_with_dangling_branch();
122        let before = g.num_values();
123        DeadNodeElimination
124            .run(&mut g, &PassContext::new())
125            .unwrap();
126        // `e` had no other consumer, so removing Neg should GC it.
127        assert!(g.num_values() < before);
128    }
129
130    #[test]
131    fn noop_on_all_live_graph() {
132        let mut g = Graph::new();
133        g.opset_imports.insert(String::new(), 17);
134        let a = g.create_named_value("a", DataType::Float32, static_shape([2]));
135        g.add_input(a);
136        let b = g.create_named_value("b", DataType::Float32, static_shape([2]));
137        g.insert_node(Node::new(NodeId(0), "Relu", vec![Some(a)], vec![b]));
138        g.add_output(b);
139
140        DeadNodeElimination
141            .run(&mut g, &PassContext::new())
142            .unwrap();
143        assert_eq!(g.num_nodes(), 1);
144        assert!(g.validate().is_ok());
145    }
146
147    #[test]
148    fn removes_transitively_dead_chain() {
149        // a(in) -> Relu -> b -> Neg -> e ; and a -> Abs -> out. b/e are dead.
150        let mut g = Graph::new();
151        g.opset_imports.insert(String::new(), 17);
152        let a = g.create_named_value("a", DataType::Float32, static_shape([2]));
153        g.add_input(a);
154
155        let b = g.create_value(DataType::Float32, static_shape([2]));
156        g.insert_node(Node::new(NodeId(0), "Relu", vec![Some(a)], vec![b]));
157        let e = g.create_value(DataType::Float32, static_shape([2]));
158        g.insert_node(Node::new(NodeId(0), "Neg", vec![Some(b)], vec![e]));
159
160        let out = g.create_named_value("out", DataType::Float32, static_shape([2]));
161        g.insert_node(Node::new(NodeId(0), "Abs", vec![Some(a)], vec![out]));
162        g.add_output(out);
163
164        DeadNodeElimination
165            .run(&mut g, &PassContext::new())
166            .unwrap();
167        assert_eq!(g.num_nodes(), 1); // only Abs survives
168        assert!(g.validate().is_ok());
169    }
170}