1use 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
33const MAX_FOLD_ELEMS: usize = 1024;
36
37#[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 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 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 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
177fn 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
186fn 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
212fn 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
237fn 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; }
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()?; 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 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 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 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}