Skip to main content

onnx_runtime_optimizer/
constant_folding.rs

1//! Constant folding: replace a node whose inputs are *all* constant
2//! (initializers) with a precomputed initializer (see `docs/ORT2.md` §18.1).
3//!
4//! ## Boundary (deliberately conservative)
5//!
6//! Fully general constant folding needs a kernel executor — the optimizer has
7//! none — so this pass folds only what the IR can compute *directly and
8//! exactly*, reusing the lessons of the loader's "const-fold-lite"
9//! (`crates/onnx-runtime-loader/src/shape_inference.rs`):
10//!
11//! * **`Constant`** nodes are materialized into initializers (always safe).
12//! * **`Shape`** on a fully-static input becomes an `int64` initializer.
13//! * **Elementwise integer `Add`/`Sub`/`Mul`** on two *same-shape* constant
14//!   `int32`/`int64` tensors are evaluated with **checked** arithmetic; any
15//!   overflow aborts the fold rather than emit a wrong constant.
16//!
17//! Everything else is left untouched. Folding is bounded to
18//! [`MAX_FOLD_ELEMS`] elements so large weight tensors are never materialized,
19//! and dispatch is purely on op type — no model-specific names. The invariant
20//! is: **never produce a wrong constant.** When in doubt, do not fold.
21
22use std::cmp::Reverse;
23use std::collections::{BinaryHeap, HashMap};
24
25use onnx_runtime_ir::{
26    Attribute, DataType, Graph, NodeId, TensorData, ValueId, WeightRef, as_static_shape,
27    is_fully_static, static_shape,
28};
29
30use crate::error::Result;
31use crate::pass::{OptimizationPass, PassContext};
32
33/// Upper bound on the number of elements this pass will materialize. Keeps
34/// folding limited to shape-computation-sized tensors, never model weights.
35const MAX_FOLD_ELEMS: usize = 1024;
36
37/// Folds constant-input nodes into initializers (bounded, integer/shape only).
38#[derive(Clone, Copy, Debug, Default)]
39pub struct ConstantFolding;
40
41impl OptimizationPass for ConstantFolding {
42    fn name(&self) -> &str {
43        "ConstantFolding"
44    }
45
46    fn run(&self, graph: &mut Graph, _ctx: &PassContext) -> Result<()> {
47        let candidates: Vec<NodeId> = graph
48            .nodes
49            .iter()
50            .filter_map(|(nid, node)| is_candidate(node).then_some(nid))
51            .collect();
52        let mut unresolved = HashMap::with_capacity(candidates.len());
53        let mut dependents: HashMap<ValueId, Vec<NodeId>> =
54            HashMap::with_capacity(candidates.len());
55        // Match ascending fixpoint passes: higher IDs made ready during a wave
56        // join it, while lower/equal IDs wait for the next wave.
57        let mut current_wave = BinaryHeap::new();
58        let mut next_wave = BinaryHeap::new();
59
60        for nid in candidates {
61            let node = graph.node(nid);
62            let Some(inputs) = unresolved_inputs(graph, node) else {
63                continue;
64            };
65            let count = inputs.len();
66            unresolved.insert(nid, count);
67            if count == 0 {
68                current_wave.push(Reverse(nid.0));
69            } else {
70                for input in inputs {
71                    dependents.entry(input).or_default().push(nid);
72                }
73            }
74        }
75
76        while !current_wave.is_empty() {
77            while let Some(Reverse(raw_nid)) = current_wave.pop() {
78                let nid = NodeId(raw_nid);
79                if unresolved.remove(&nid).is_none() || !graph.nodes.contains(nid) {
80                    continue;
81                }
82                let (out, folded) = {
83                    let node = graph.node(nid);
84                    let folded = match node.op_type.as_str() {
85                        "Constant" => eval_constant(node),
86                        "Shape" => fold_shape(graph, node),
87                        "Add" | "Sub" | "Mul" => fold_binary_int(graph, node),
88                        _ => None,
89                    };
90                    (node.outputs[0], folded)
91                };
92                let Some(tensor) = folded else { continue };
93
94                // Only fold outputs that are still needed (have a consumer or
95                // are graph outputs); dead outputs are DCE's job and folding
96                // them would leave a stale initializer referencing a GC'd id.
97                let needed = graph.outputs.contains(&out)
98                    || graph.try_value(out).is_some_and(|_| graph.has_uses(out));
99                if !needed {
100                    continue;
101                }
102
103                graph.remove_node(nid);
104                // The output survives because it is needed; retype it to the
105                // folded tensor and back it with an inline initializer.
106                if graph.try_value(out).is_none() {
107                    continue;
108                }
109                let dims = tensor.dims.clone();
110                let dtype = tensor.dtype;
111                let v = graph.value_mut(out);
112                v.dtype = dtype;
113                v.shape = static_shape(dims);
114                graph.set_initializer(out, WeightRef::Inline(tensor));
115
116                for consumer in dependents.remove(&out).unwrap_or_default() {
117                    let Some(count) = unresolved.get_mut(&consumer) else {
118                        continue;
119                    };
120                    *count -= 1;
121                    if *count == 0 {
122                        let wave = if consumer.0 > nid.0 {
123                            &mut current_wave
124                        } else {
125                            &mut next_wave
126                        };
127                        wave.push(Reverse(consumer.0));
128                    }
129                }
130            }
131            std::mem::swap(&mut current_wave, &mut next_wave);
132        }
133        Ok(())
134    }
135}
136
137fn is_candidate(node: &onnx_runtime_ir::Node) -> bool {
138    matches!(node.domain.as_str(), "" | "ai.onnx")
139        && node.outputs.len() == 1
140        && matches!(
141            node.op_type.as_str(),
142            "Constant" | "Shape" | "Add" | "Sub" | "Mul"
143        )
144}
145
146fn unresolved_inputs(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<Vec<ValueId>> {
147    match node.op_type.as_str() {
148        "Constant" => Some(Vec::new()),
149        "Shape" => {
150            if node.attr("start").is_some() || node.attr("end").is_some() {
151                return None;
152            }
153            let input = node.inputs.first().copied().flatten()?;
154            let shape = &graph.try_value(input)?.shape;
155            if shape.len() <= MAX_FOLD_ELEMS && is_fully_static(shape) {
156                Some(Vec::new())
157            } else {
158                Some(vec![input])
159            }
160        }
161        "Add" | "Sub" | "Mul" => {
162            if node.inputs.len() != 2 {
163                return None;
164            }
165            let inputs = [node.inputs[0]?, node.inputs[1]?];
166            Some(
167                inputs
168                    .into_iter()
169                    .filter(|&input| inline_const(graph, input).is_none())
170                    .collect(),
171            )
172        }
173        _ => None,
174    }
175}
176
177/// The inline constant tensor backing `value`, if any (external weights, which
178/// are large, are never folded).
179fn inline_const(graph: &Graph, value: ValueId) -> Option<&TensorData> {
180    match graph.initializers.get(&value)? {
181        WeightRef::Inline(t) => Some(t),
182        WeightRef::External { .. } => None,
183    }
184}
185
186/// Materialize a `Constant` node's value into a concrete [`TensorData`].
187fn eval_constant(node: &onnx_runtime_ir::Node) -> Option<TensorData> {
188    if let Some(Attribute::Tensor(t)) = node.attr("value") {
189        return Some(t.clone());
190    }
191    if let Some(ints) = node.attr("value_ints").and_then(Attribute::as_ints) {
192        let mut data = Vec::with_capacity(ints.len() * 8);
193        for &i in ints {
194            data.extend_from_slice(&i.to_le_bytes());
195        }
196        return Some(TensorData::from_raw(
197            DataType::Int64,
198            vec![ints.len()],
199            data,
200        ));
201    }
202    if let Some(i) = node.attr("value_int").and_then(Attribute::as_int) {
203        return Some(TensorData::from_raw(
204            DataType::Int64,
205            Vec::new(),
206            i.to_le_bytes().to_vec(),
207        ));
208    }
209    None
210}
211
212/// Fold `Shape(x)` when `x` has a fully-static shape into an `int64` vector.
213///
214/// Conservative: bails if `start`/`end` attributes are present (a slice of the
215/// shape) so we never emit a partial result.
216fn fold_shape(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
217    if node.attr("start").is_some() || node.attr("end").is_some() {
218        return None;
219    }
220    let input = node.inputs.first().copied().flatten()?;
221    let shape = &graph.try_value(input)?.shape;
222    let dims = as_static_shape(shape)?;
223    if dims.len() > MAX_FOLD_ELEMS {
224        return None;
225    }
226    let mut data = Vec::with_capacity(dims.len() * 8);
227    for &d in &dims {
228        data.extend_from_slice(&(d as i64).to_le_bytes());
229    }
230    Some(TensorData::from_raw(
231        DataType::Int64,
232        vec![dims.len()],
233        data,
234    ))
235}
236
237/// Fold elementwise integer `Add`/`Sub`/`Mul` on two same-shape constant
238/// tensors. Uses checked arithmetic; overflow aborts (returns `None`).
239fn fold_binary_int(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
240    if node.inputs.len() != 2 {
241        return None;
242    }
243    let a = inline_const(graph, node.inputs[0]?)?;
244    let b = inline_const(graph, node.inputs[1]?)?;
245    if a.dtype != b.dtype || a.dims != b.dims {
246        return None; // no broadcasting / mixed dtype in v1
247    }
248    if !matches!(a.dtype, DataType::Int32 | DataType::Int64) {
249        return None;
250    }
251    let numel = a.numel();
252    if numel > MAX_FOLD_ELEMS {
253        return None;
254    }
255    let op = node.op_type.as_str();
256    let apply = |x: i64, y: i64| -> Option<i64> {
257        match op {
258            "Add" => x.checked_add(y),
259            "Sub" => x.checked_sub(y),
260            "Mul" => x.checked_mul(y),
261            _ => None,
262        }
263    };
264
265    match a.dtype {
266        DataType::Int64 => {
267            let (xs, ys) = (read_i64(a)?, read_i64(b)?);
268            let mut data = Vec::with_capacity(numel * 8);
269            for (x, y) in xs.into_iter().zip(ys) {
270                data.extend_from_slice(&apply(x, y)?.to_le_bytes());
271            }
272            Some(TensorData::from_raw(DataType::Int64, a.dims.clone(), data))
273        }
274        DataType::Int32 => {
275            let (xs, ys) = (read_i32(a)?, read_i32(b)?);
276            let mut data = Vec::with_capacity(numel * 4);
277            for (x, y) in xs.into_iter().zip(ys) {
278                let r = apply(x as i64, y as i64)?;
279                let r32: i32 = r.try_into().ok()?; // must fit back into i32
280                data.extend_from_slice(&r32.to_le_bytes());
281            }
282            Some(TensorData::from_raw(DataType::Int32, a.dims.clone(), data))
283        }
284        _ => None,
285    }
286}
287
288fn read_i64(t: &TensorData) -> Option<Vec<i64>> {
289    if t.data.len() != t.numel() * 8 {
290        return None;
291    }
292    Some(
293        t.data
294            .chunks_exact(8)
295            .map(|c| i64::from_le_bytes(c.try_into().unwrap()))
296            .collect(),
297    )
298}
299
300fn read_i32(t: &TensorData) -> Option<Vec<i32>> {
301    if t.data.len() != t.numel() * 4 {
302        return None;
303    }
304    Some(
305        t.data
306            .chunks_exact(4)
307            .map(|c| i32::from_le_bytes(c.try_into().unwrap()))
308            .collect(),
309    )
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use onnx_runtime_ir::{Node, NodeId};
316    use onnx_runtime_loader::{Model, encode_model};
317
318    fn int64_tensor(dims: Vec<usize>, vals: &[i64]) -> TensorData {
319        let mut data = Vec::new();
320        for &v in vals {
321            data.extend_from_slice(&v.to_le_bytes());
322        }
323        TensorData::from_raw(DataType::Int64, dims, data)
324    }
325
326    fn const_init(graph: &mut Graph, name: &str, dims: Vec<usize>, vals: &[i64]) -> ValueId {
327        let shape = static_shape(dims.clone());
328        let v = graph.create_named_value(name, DataType::Int64, shape);
329        graph.set_initializer(v, WeightRef::Inline(int64_tensor(dims, vals)));
330        v
331    }
332
333    fn run_reference_ascending_fixpoint(graph: &mut Graph) {
334        loop {
335            let mut changed = false;
336            let node_ids: Vec<NodeId> = graph.nodes.keys().collect();
337            for nid in node_ids {
338                if !graph.nodes.contains(nid) {
339                    continue;
340                }
341                let node = graph.node(nid).clone();
342                if !matches!(node.domain.as_str(), "" | "ai.onnx") || node.outputs.len() != 1 {
343                    continue;
344                }
345                let out = node.outputs[0];
346                let folded = match node.op_type.as_str() {
347                    "Constant" => eval_constant(&node),
348                    "Shape" => fold_shape(graph, &node),
349                    "Add" | "Sub" | "Mul" => fold_binary_int(graph, &node),
350                    _ => None,
351                };
352                let Some(tensor) = folded else { continue };
353                let needed = graph.outputs.contains(&out)
354                    || graph.try_value(out).is_some_and(|_| graph.has_uses(out));
355                if !needed {
356                    continue;
357                }
358
359                graph.remove_node(nid);
360                if graph.try_value(out).is_some() {
361                    let dims = tensor.dims.clone();
362                    let dtype = tensor.dtype;
363                    let v = graph.value_mut(out);
364                    v.dtype = dtype;
365                    v.shape = static_shape(dims);
366                    graph.set_initializer(out, WeightRef::Inline(tensor));
367                    changed = true;
368                }
369            }
370            if !changed {
371                break;
372            }
373        }
374    }
375
376    fn serialized(graph: &Graph) -> Vec<u8> {
377        encode_model(&Model::new(graph)).expect("serialize graph")
378    }
379
380    fn schedule_sensitive_chain() -> (Graph, ValueId) {
381        let mut g = Graph::new();
382        g.opset_imports.insert(String::new(), 17);
383        let init = const_init(&mut g, "init", vec![1], &[2]);
384        let a = g.create_named_value("a", DataType::Int64, static_shape([1]));
385        let b = g.create_named_value("b", DataType::Int64, static_shape([1]));
386        let out = g.create_named_value("out", DataType::Int64, static_shape([1]));
387
388        let mut constant = Node::new(NodeId(0), "Constant", vec![], vec![a]);
389        constant.attributes.insert(
390            "value".into(),
391            Attribute::Tensor(int64_tensor(vec![1], &[3])),
392        );
393        g.insert_node(constant);
394        g.insert_node(Node::new(
395            NodeId(0),
396            "Add",
397            vec![Some(a), Some(init)],
398            vec![b],
399        ));
400        g.insert_node(Node::new(NodeId(0), "Shape", vec![Some(b)], vec![out]));
401        g.add_output(out);
402        (g, out)
403    }
404
405    #[test]
406    fn ascending_wave_folds_constant_add_before_shape_consumer() {
407        let (base, out) = schedule_sensitive_chain();
408        let mut reference = base.clone();
409        let mut worklist = base;
410        run_reference_ascending_fixpoint(&mut reference);
411        ConstantFolding
412            .run(&mut worklist, &PassContext::new())
413            .unwrap();
414
415        assert_eq!(worklist.num_nodes(), 0, "Constant, Add, and Shape fold");
416        assert!(!worklist.nodes.values().any(|node| node.op_type == "Add"));
417        assert_eq!(
418            read_i64(inline_const(&worklist, out).unwrap()),
419            Some(vec![1])
420        );
421        assert_eq!(serialized(&worklist), serialized(&reference));
422        assert!(worklist.validate().is_ok());
423    }
424
425    #[test]
426    fn ascending_wave_leaves_lower_dead_producer_unfolded() {
427        let mut base = Graph::new();
428        base.opset_imports.insert(String::new(), 17);
429        let init = const_init(&mut base, "init", vec![1], &[2]);
430        let a = base.create_named_value("a", DataType::Int64, static_shape([1]));
431        let b = base.create_named_value("b", DataType::Int64, static_shape([1]));
432        let out = base.create_named_value("out", DataType::Int64, static_shape([1]));
433
434        base.insert_node(Node::new(
435            NodeId(0),
436            "Add",
437            vec![Some(a), Some(init)],
438            vec![b],
439        ));
440        base.insert_node(Node::new(NodeId(0), "Shape", vec![Some(b)], vec![out]));
441        let mut constant = Node::new(NodeId(0), "Constant", vec![], vec![a]);
442        constant.attributes.insert(
443            "value".into(),
444            Attribute::Tensor(int64_tensor(vec![1], &[3])),
445        );
446        base.insert_node(constant);
447        base.add_output(out);
448
449        let mut reference = base.clone();
450        let mut worklist = base;
451        run_reference_ascending_fixpoint(&mut reference);
452        ConstantFolding
453            .run(&mut worklist, &PassContext::new())
454            .unwrap();
455
456        assert_eq!(worklist.num_nodes(), 1);
457        assert_eq!(worklist.nodes.values().next().unwrap().op_type, "Add");
458        assert!(inline_const(&worklist, b).is_none());
459        assert_eq!(serialized(&worklist), serialized(&reference));
460        assert!(worklist.validate().is_ok());
461    }
462
463    fn seeded_dag(mut seed: u64, nodes: usize) -> Graph {
464        fn next(seed: &mut u64) -> u64 {
465            *seed = seed
466                .wrapping_mul(6_364_136_223_846_793_005)
467                .wrapping_add(1_442_695_040_888_963_407);
468            *seed
469        }
470
471        let mut g = Graph::new();
472        g.opset_imports.insert(String::new(), 17);
473        let init = const_init(&mut g, "init", vec![1], &[1]);
474        let values: Vec<ValueId> = (0..nodes)
475            .map(|i| g.create_named_value(format!("v{i}"), DataType::Int64, static_shape([1])))
476            .collect();
477        let mut definitions = Vec::with_capacity(nodes);
478        for i in 0..nodes {
479            let mut node = if i == 0 || next(&mut seed).is_multiple_of(4) {
480                let mut constant = Node::new(NodeId(0), "Constant", vec![], vec![values[i]]);
481                constant.attributes.insert(
482                    "value".into(),
483                    Attribute::Tensor(int64_tensor(vec![1], &[(next(&mut seed) % 8) as i64])),
484                );
485                constant
486            } else if next(&mut seed).is_multiple_of(3) {
487                let input = values[(next(&mut seed) as usize) % i];
488                Node::new(NodeId(0), "Shape", vec![Some(input)], vec![values[i]])
489            } else {
490                let pick_input = |seed: &mut u64| {
491                    if next(seed).is_multiple_of(4) {
492                        init
493                    } else {
494                        values[(next(seed) as usize) % i]
495                    }
496                };
497                Node::new(
498                    NodeId(0),
499                    "Add",
500                    vec![Some(pick_input(&mut seed)), Some(pick_input(&mut seed))],
501                    vec![values[i]],
502                )
503            };
504            node.name = format!("node_{i}");
505            definitions.push(node);
506        }
507
508        let mut order: Vec<usize> = (0..nodes).collect();
509        for i in (1..nodes).rev() {
510            order.swap(i, (next(&mut seed) as usize) % (i + 1));
511        }
512        for index in order {
513            g.insert_node(definitions[index].clone());
514        }
515        for (i, &value) in values.iter().enumerate() {
516            if i + 1 == nodes || (i > nodes / 2 && next(&mut seed).is_multiple_of(11)) {
517                g.add_output(value);
518            }
519        }
520        g
521    }
522
523    #[test]
524    fn seeded_dags_are_byte_identical_to_ascending_fixpoint() {
525        for seed in 0..32 {
526            let base = seeded_dag(seed, 96);
527            assert!(base.validate().is_ok(), "seed {seed}");
528            let mut reference = base.clone();
529            let mut worklist = base;
530            run_reference_ascending_fixpoint(&mut reference);
531            ConstantFolding
532                .run(&mut worklist, &PassContext::new())
533                .unwrap();
534            assert_eq!(serialized(&worklist), serialized(&reference), "seed {seed}");
535        }
536    }
537
538    #[test]
539    fn folds_add_of_two_const_inputs() {
540        let mut g = Graph::new();
541        g.opset_imports.insert(String::new(), 17);
542        let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
543        let b = const_init(&mut g, "b", vec![3], &[10, 20, 30]);
544        let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
545        g.insert_node(Node::new(
546            NodeId(0),
547            "Add",
548            vec![Some(a), Some(b)],
549            vec![out],
550        ));
551        g.add_output(out);
552
553        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
554
555        assert_eq!(g.num_nodes(), 0, "Add should be folded away");
556        let t = inline_const(&g, out).expect("out is now an initializer");
557        assert_eq!(read_i64(t).unwrap(), vec![11, 22, 33]);
558        assert!(g.validate().is_ok());
559    }
560
561    #[test]
562    fn folds_sub_and_mul() {
563        for (op, expect) in [("Sub", vec![9, 18, 27]), ("Mul", vec![10, 40, 90])] {
564            let mut g = Graph::new();
565            g.opset_imports.insert(String::new(), 17);
566            let a = const_init(&mut g, "a", vec![3], &[10, 20, 30]);
567            let b = const_init(&mut g, "b", vec![3], &[1, 2, 3]);
568            let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
569            g.insert_node(Node::new(NodeId(0), op, vec![Some(a), Some(b)], vec![out]));
570            g.add_output(out);
571
572            ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
573            let t = inline_const(&g, out).unwrap();
574            assert_eq!(read_i64(t).unwrap(), expect, "op {op}");
575        }
576    }
577
578    #[test]
579    fn does_not_fold_when_one_input_is_non_const() {
580        let mut g = Graph::new();
581        g.opset_imports.insert(String::new(), 17);
582        let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
583        // `b` is a graph input, not a constant.
584        let b = g.create_named_value("b", DataType::Int64, static_shape([3]));
585        g.add_input(b);
586        let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
587        g.insert_node(Node::new(
588            NodeId(0),
589            "Add",
590            vec![Some(a), Some(b)],
591            vec![out],
592        ));
593        g.add_output(out);
594
595        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
596        assert_eq!(g.num_nodes(), 1, "must not fold with a non-const input");
597        assert!(inline_const(&g, out).is_none());
598        assert!(g.validate().is_ok());
599    }
600
601    #[test]
602    fn does_not_fold_mismatched_shapes() {
603        let mut g = Graph::new();
604        g.opset_imports.insert(String::new(), 17);
605        let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
606        let b = const_init(&mut g, "b", vec![2], &[10, 20]);
607        let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
608        g.insert_node(Node::new(
609            NodeId(0),
610            "Add",
611            vec![Some(a), Some(b)],
612            vec![out],
613        ));
614        g.add_output(out);
615
616        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
617        assert_eq!(g.num_nodes(), 1, "no broadcasting in v1");
618    }
619
620    #[test]
621    fn does_not_fold_overflow() {
622        let mut g = Graph::new();
623        g.opset_imports.insert(String::new(), 17);
624        let a = const_init(&mut g, "a", vec![1], &[i64::MAX]);
625        let b = const_init(&mut g, "b", vec![1], &[1]);
626        let out = g.create_named_value("out", DataType::Int64, static_shape([1]));
627        g.insert_node(Node::new(
628            NodeId(0),
629            "Add",
630            vec![Some(a), Some(b)],
631            vec![out],
632        ));
633        g.add_output(out);
634
635        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
636        assert_eq!(g.num_nodes(), 1, "overflow must abort the fold");
637    }
638
639    #[test]
640    fn folds_constant_node_to_initializer() {
641        let mut g = Graph::new();
642        g.opset_imports.insert(String::new(), 17);
643        let out = g.create_named_value("c", DataType::Int64, static_shape([2]));
644        let mut node = Node::new(NodeId(0), "Constant", vec![], vec![out]);
645        node.attributes.insert(
646            "value".into(),
647            Attribute::Tensor(int64_tensor(vec![2], &[7, 8])),
648        );
649        g.insert_node(node);
650        // Keep `out` alive with a consumer.
651        let sink = g.create_named_value("sink", DataType::Int64, static_shape([2]));
652        g.insert_node(Node::new(
653            NodeId(0),
654            "Identity",
655            vec![Some(out)],
656            vec![sink],
657        ));
658        g.add_output(sink);
659
660        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
661        assert!(g.try_node(NodeId(0)).is_none(), "Constant folded away");
662        let t = inline_const(&g, out).unwrap();
663        assert_eq!(read_i64(t).unwrap(), vec![7, 8]);
664        assert!(g.validate().is_ok());
665    }
666
667    #[test]
668    fn folds_shape_of_static_input() {
669        let mut g = Graph::new();
670        g.opset_imports.insert(String::new(), 17);
671        let x = g.create_named_value("x", DataType::Float32, static_shape([2, 3, 4]));
672        g.add_input(x);
673        let out = g.create_named_value("s", DataType::Int64, static_shape([3]));
674        g.insert_node(Node::new(NodeId(0), "Shape", vec![Some(x)], vec![out]));
675        g.add_output(out);
676
677        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
678        let t = inline_const(&g, out).expect("Shape folded to initializer");
679        assert_eq!(read_i64(t).unwrap(), vec![2, 3, 4]);
680        assert!(g.validate().is_ok());
681    }
682
683    #[test]
684    fn folds_transitively_to_fixpoint() {
685        // Constant c1, Constant c2, then Add(c1, c2) -> out. All should fold.
686        let mut g = Graph::new();
687        g.opset_imports.insert(String::new(), 17);
688
689        let c1 = g.create_named_value("c1", DataType::Int64, static_shape([2]));
690        let mut n1 = Node::new(NodeId(0), "Constant", vec![], vec![c1]);
691        n1.attributes.insert(
692            "value".into(),
693            Attribute::Tensor(int64_tensor(vec![2], &[1, 2])),
694        );
695        g.insert_node(n1);
696
697        let c2 = g.create_named_value("c2", DataType::Int64, static_shape([2]));
698        let mut n2 = Node::new(NodeId(0), "Constant", vec![], vec![c2]);
699        n2.attributes.insert(
700            "value".into(),
701            Attribute::Tensor(int64_tensor(vec![2], &[3, 4])),
702        );
703        g.insert_node(n2);
704
705        let out = g.create_named_value("out", DataType::Int64, static_shape([2]));
706        g.insert_node(Node::new(
707            NodeId(0),
708            "Add",
709            vec![Some(c1), Some(c2)],
710            vec![out],
711        ));
712        g.add_output(out);
713
714        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
715        assert_eq!(g.num_nodes(), 0, "both constants and the Add fold away");
716        let t = inline_const(&g, out).unwrap();
717        assert_eq!(read_i64(t).unwrap(), vec![4, 6]);
718        assert!(g.validate().is_ok());
719    }
720
721    fn constant_chain(nodes: usize, reverse_node_ids: bool) -> (Graph, ValueId) {
722        let mut g = Graph::new();
723        g.opset_imports.insert(String::new(), 17);
724        let zero = const_init(&mut g, "zero", vec![1], &[0]);
725        let one = const_init(&mut g, "one", vec![1], &[1]);
726        let mut values = Vec::with_capacity(nodes + 1);
727        values.push(zero);
728        for _ in 0..nodes {
729            values.push(g.create_value(DataType::Int64, static_shape([1])));
730        }
731
732        if reverse_node_ids {
733            for i in (1..=nodes).rev() {
734                g.insert_node(Node::new(
735                    NodeId(0),
736                    "Add",
737                    vec![Some(values[i - 1]), Some(one)],
738                    vec![values[i]],
739                ));
740            }
741        } else {
742            for i in 1..=nodes {
743                g.insert_node(Node::new(
744                    NodeId(0),
745                    "Add",
746                    vec![Some(values[i - 1]), Some(one)],
747                    vec![values[i]],
748                ));
749            }
750        }
751
752        let out = values[nodes];
753        g.add_output(out);
754        (g, out)
755    }
756
757    #[test]
758    fn reverse_node_id_chain_matches_forward_order() {
759        let (mut forward, forward_out) = constant_chain(64, false);
760        let (mut reverse, reverse_out) = constant_chain(64, true);
761
762        let reverse_ids = reverse.topological_order().unwrap();
763        assert!(
764            reverse_ids.windows(2).all(|ids| ids[0].0 > ids[1].0),
765            "test graph must have reverse dependency NodeIds"
766        );
767
768        ConstantFolding
769            .run(&mut forward, &PassContext::new())
770            .unwrap();
771        ConstantFolding
772            .run(&mut reverse, &PassContext::new())
773            .unwrap();
774
775        assert_eq!(forward.num_nodes(), 0);
776        assert_eq!(reverse.num_nodes(), 0);
777        assert_eq!(
778            inline_const(&forward, forward_out),
779            inline_const(&reverse, reverse_out)
780        );
781        assert_eq!(
782            read_i64(inline_const(&reverse, reverse_out).unwrap()),
783            Some(vec![64])
784        );
785        assert!(forward.validate().is_ok());
786        assert!(reverse.validate().is_ok());
787    }
788
789    #[test]
790    fn does_not_fold_float_binary() {
791        let mut g = Graph::new();
792        g.opset_imports.insert(String::new(), 17);
793        let mk = |g: &mut Graph, name: &str| {
794            let v = g.create_named_value(name, DataType::Float32, static_shape([2]));
795            g.set_initializer(
796                v,
797                WeightRef::Inline(TensorData::from_raw(
798                    DataType::Float32,
799                    vec![2],
800                    vec![0u8; 8],
801                )),
802            );
803            v
804        };
805        let a = mk(&mut g, "a");
806        let b = mk(&mut g, "b");
807        let out = g.create_named_value("out", DataType::Float32, static_shape([2]));
808        g.insert_node(Node::new(
809            NodeId(0),
810            "Mul",
811            vec![Some(a), Some(b)],
812            vec![out],
813        ));
814        g.add_output(out);
815
816        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
817        assert_eq!(g.num_nodes(), 1, "float folding is out of scope in v1");
818    }
819}