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
99 .try_value(out)
100 .is_some_and(|_| graph.has_uses(out));
101 if !needed {
102 continue;
103 }
104
105 graph.remove_node(nid);
106 if graph.try_value(out).is_none() {
109 continue;
110 }
111 let dims = tensor.dims.clone();
112 let dtype = tensor.dtype;
113 let v = graph.value_mut(out);
114 v.dtype = dtype;
115 v.shape = static_shape(dims);
116 graph.set_initializer(out, WeightRef::Inline(tensor));
117
118 for consumer in dependents.remove(&out).unwrap_or_default() {
119 let Some(count) = unresolved.get_mut(&consumer) else {
120 continue;
121 };
122 *count -= 1;
123 if *count == 0 {
124 let wave = if consumer.0 > nid.0 {
125 &mut current_wave
126 } else {
127 &mut next_wave
128 };
129 wave.push(Reverse(consumer.0));
130 }
131 }
132 }
133 std::mem::swap(&mut current_wave, &mut next_wave);
134 }
135 Ok(())
136 }
137}
138
139fn is_candidate(node: &onnx_runtime_ir::Node) -> bool {
140 matches!(node.domain.as_str(), "" | "ai.onnx")
141 && node.outputs.len() == 1
142 && matches!(
143 node.op_type.as_str(),
144 "Constant" | "Shape" | "Add" | "Sub" | "Mul"
145 )
146}
147
148fn unresolved_inputs(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<Vec<ValueId>> {
149 match node.op_type.as_str() {
150 "Constant" => Some(Vec::new()),
151 "Shape" => {
152 if node.attr("start").is_some() || node.attr("end").is_some() {
153 return None;
154 }
155 let input = node.inputs.first().copied().flatten()?;
156 let shape = &graph.try_value(input)?.shape;
157 if shape.len() <= MAX_FOLD_ELEMS && is_fully_static(shape) {
158 Some(Vec::new())
159 } else {
160 Some(vec![input])
161 }
162 }
163 "Add" | "Sub" | "Mul" => {
164 if node.inputs.len() != 2 {
165 return None;
166 }
167 let inputs = [node.inputs[0]?, node.inputs[1]?];
168 Some(
169 inputs
170 .into_iter()
171 .filter(|&input| inline_const(graph, input).is_none())
172 .collect(),
173 )
174 }
175 _ => None,
176 }
177}
178
179fn inline_const(graph: &Graph, value: ValueId) -> Option<&TensorData> {
182 match graph.initializers.get(&value)? {
183 WeightRef::Inline(t) => Some(t),
184 WeightRef::External { .. } => None,
185 }
186}
187
188fn eval_constant(node: &onnx_runtime_ir::Node) -> Option<TensorData> {
190 if let Some(Attribute::Tensor(t)) = node.attr("value") {
191 return Some(t.clone());
192 }
193 if let Some(ints) = node.attr("value_ints").and_then(Attribute::as_ints) {
194 let mut data = Vec::with_capacity(ints.len() * 8);
195 for &i in ints {
196 data.extend_from_slice(&i.to_le_bytes());
197 }
198 return Some(TensorData::from_raw(DataType::Int64, vec![ints.len()], data));
199 }
200 if let Some(i) = node.attr("value_int").and_then(Attribute::as_int) {
201 return Some(TensorData::from_raw(
202 DataType::Int64,
203 Vec::new(),
204 i.to_le_bytes().to_vec(),
205 ));
206 }
207 None
208}
209
210fn fold_shape(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
215 if node.attr("start").is_some() || node.attr("end").is_some() {
216 return None;
217 }
218 let input = node.inputs.first().copied().flatten()?;
219 let shape = &graph.try_value(input)?.shape;
220 let dims = as_static_shape(shape)?;
221 if dims.len() > MAX_FOLD_ELEMS {
222 return None;
223 }
224 let mut data = Vec::with_capacity(dims.len() * 8);
225 for &d in &dims {
226 data.extend_from_slice(&(d as i64).to_le_bytes());
227 }
228 Some(TensorData::from_raw(DataType::Int64, vec![dims.len()], data))
229}
230
231fn fold_binary_int(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
234 if node.inputs.len() != 2 {
235 return None;
236 }
237 let a = inline_const(graph, node.inputs[0]?)?;
238 let b = inline_const(graph, node.inputs[1]?)?;
239 if a.dtype != b.dtype || a.dims != b.dims {
240 return None; }
242 if !matches!(a.dtype, DataType::Int32 | DataType::Int64) {
243 return None;
244 }
245 let numel = a.numel();
246 if numel > MAX_FOLD_ELEMS {
247 return None;
248 }
249 let op = node.op_type.as_str();
250 let apply = |x: i64, y: i64| -> Option<i64> {
251 match op {
252 "Add" => x.checked_add(y),
253 "Sub" => x.checked_sub(y),
254 "Mul" => x.checked_mul(y),
255 _ => None,
256 }
257 };
258
259 match a.dtype {
260 DataType::Int64 => {
261 let (xs, ys) = (read_i64(a)?, read_i64(b)?);
262 let mut data = Vec::with_capacity(numel * 8);
263 for (x, y) in xs.into_iter().zip(ys) {
264 data.extend_from_slice(&apply(x, y)?.to_le_bytes());
265 }
266 Some(TensorData::from_raw(DataType::Int64, a.dims.clone(), data))
267 }
268 DataType::Int32 => {
269 let (xs, ys) = (read_i32(a)?, read_i32(b)?);
270 let mut data = Vec::with_capacity(numel * 4);
271 for (x, y) in xs.into_iter().zip(ys) {
272 let r = apply(x as i64, y as i64)?;
273 let r32: i32 = r.try_into().ok()?; data.extend_from_slice(&r32.to_le_bytes());
275 }
276 Some(TensorData::from_raw(DataType::Int32, a.dims.clone(), data))
277 }
278 _ => None,
279 }
280}
281
282fn read_i64(t: &TensorData) -> Option<Vec<i64>> {
283 if t.data.len() != t.numel() * 8 {
284 return None;
285 }
286 Some(
287 t.data
288 .chunks_exact(8)
289 .map(|c| i64::from_le_bytes(c.try_into().unwrap()))
290 .collect(),
291 )
292}
293
294fn read_i32(t: &TensorData) -> Option<Vec<i32>> {
295 if t.data.len() != t.numel() * 4 {
296 return None;
297 }
298 Some(
299 t.data
300 .chunks_exact(4)
301 .map(|c| i32::from_le_bytes(c.try_into().unwrap()))
302 .collect(),
303 )
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use onnx_runtime_ir::{Node, NodeId};
310 use onnx_runtime_loader::{Model, encode_model};
311
312 fn int64_tensor(dims: Vec<usize>, vals: &[i64]) -> TensorData {
313 let mut data = Vec::new();
314 for &v in vals {
315 data.extend_from_slice(&v.to_le_bytes());
316 }
317 TensorData::from_raw(DataType::Int64, dims, data)
318 }
319
320 fn const_init(graph: &mut Graph, name: &str, dims: Vec<usize>, vals: &[i64]) -> ValueId {
321 let shape = static_shape(dims.clone());
322 let v = graph.create_named_value(name, DataType::Int64, shape);
323 graph.set_initializer(v, WeightRef::Inline(int64_tensor(dims, vals)));
324 v
325 }
326
327 fn run_reference_ascending_fixpoint(graph: &mut Graph) {
328 loop {
329 let mut changed = false;
330 let node_ids: Vec<NodeId> = graph.nodes.keys().collect();
331 for nid in node_ids {
332 if !graph.nodes.contains(nid) {
333 continue;
334 }
335 let node = graph.node(nid).clone();
336 if !matches!(node.domain.as_str(), "" | "ai.onnx") || node.outputs.len() != 1 {
337 continue;
338 }
339 let out = node.outputs[0];
340 let folded = match node.op_type.as_str() {
341 "Constant" => eval_constant(&node),
342 "Shape" => fold_shape(graph, &node),
343 "Add" | "Sub" | "Mul" => fold_binary_int(graph, &node),
344 _ => None,
345 };
346 let Some(tensor) = folded else { continue };
347 let needed = graph.outputs.contains(&out)
348 || graph
349 .try_value(out)
350 .is_some_and(|_| graph.has_uses(out));
351 if !needed {
352 continue;
353 }
354
355 graph.remove_node(nid);
356 if graph.try_value(out).is_some() {
357 let dims = tensor.dims.clone();
358 let dtype = tensor.dtype;
359 let v = graph.value_mut(out);
360 v.dtype = dtype;
361 v.shape = static_shape(dims);
362 graph.set_initializer(out, WeightRef::Inline(tensor));
363 changed = true;
364 }
365 }
366 if !changed {
367 break;
368 }
369 }
370 }
371
372 fn serialized(graph: &Graph) -> Vec<u8> {
373 encode_model(&Model::new(graph)).expect("serialize graph")
374 }
375
376 fn schedule_sensitive_chain() -> (Graph, ValueId) {
377 let mut g = Graph::new();
378 g.opset_imports.insert(String::new(), 17);
379 let init = const_init(&mut g, "init", vec![1], &[2]);
380 let a = g.create_named_value("a", DataType::Int64, static_shape([1]));
381 let b = g.create_named_value("b", DataType::Int64, static_shape([1]));
382 let out = g.create_named_value("out", DataType::Int64, static_shape([1]));
383
384 let mut constant = Node::new(NodeId(0), "Constant", vec![], vec![a]);
385 constant.attributes.insert(
386 "value".into(),
387 Attribute::Tensor(int64_tensor(vec![1], &[3])),
388 );
389 g.insert_node(constant);
390 g.insert_node(Node::new(
391 NodeId(0),
392 "Add",
393 vec![Some(a), Some(init)],
394 vec![b],
395 ));
396 g.insert_node(Node::new(NodeId(0), "Shape", vec![Some(b)], vec![out]));
397 g.add_output(out);
398 (g, out)
399 }
400
401 #[test]
402 fn ascending_wave_folds_constant_add_before_shape_consumer() {
403 let (base, out) = schedule_sensitive_chain();
404 let mut reference = base.clone();
405 let mut worklist = base;
406 run_reference_ascending_fixpoint(&mut reference);
407 ConstantFolding
408 .run(&mut worklist, &PassContext::new())
409 .unwrap();
410
411 assert_eq!(worklist.num_nodes(), 0, "Constant, Add, and Shape fold");
412 assert!(!worklist.nodes.values().any(|node| node.op_type == "Add"));
413 assert_eq!(
414 read_i64(inline_const(&worklist, out).unwrap()),
415 Some(vec![1])
416 );
417 assert_eq!(serialized(&worklist), serialized(&reference));
418 assert!(worklist.validate().is_ok());
419 }
420
421 #[test]
422 fn ascending_wave_leaves_lower_dead_producer_unfolded() {
423 let mut base = Graph::new();
424 base.opset_imports.insert(String::new(), 17);
425 let init = const_init(&mut base, "init", vec![1], &[2]);
426 let a = base.create_named_value("a", DataType::Int64, static_shape([1]));
427 let b = base.create_named_value("b", DataType::Int64, static_shape([1]));
428 let out = base.create_named_value("out", DataType::Int64, static_shape([1]));
429
430 base.insert_node(Node::new(
431 NodeId(0),
432 "Add",
433 vec![Some(a), Some(init)],
434 vec![b],
435 ));
436 base.insert_node(Node::new(NodeId(0), "Shape", vec![Some(b)], vec![out]));
437 let mut constant = Node::new(NodeId(0), "Constant", vec![], vec![a]);
438 constant.attributes.insert(
439 "value".into(),
440 Attribute::Tensor(int64_tensor(vec![1], &[3])),
441 );
442 base.insert_node(constant);
443 base.add_output(out);
444
445 let mut reference = base.clone();
446 let mut worklist = base;
447 run_reference_ascending_fixpoint(&mut reference);
448 ConstantFolding
449 .run(&mut worklist, &PassContext::new())
450 .unwrap();
451
452 assert_eq!(worklist.num_nodes(), 1);
453 assert_eq!(worklist.nodes.values().next().unwrap().op_type, "Add");
454 assert!(inline_const(&worklist, b).is_none());
455 assert_eq!(serialized(&worklist), serialized(&reference));
456 assert!(worklist.validate().is_ok());
457 }
458
459 fn seeded_dag(mut seed: u64, nodes: usize) -> Graph {
460 fn next(seed: &mut u64) -> u64 {
461 *seed = seed
462 .wrapping_mul(6_364_136_223_846_793_005)
463 .wrapping_add(1_442_695_040_888_963_407);
464 *seed
465 }
466
467 let mut g = Graph::new();
468 g.opset_imports.insert(String::new(), 17);
469 let init = const_init(&mut g, "init", vec![1], &[1]);
470 let values: Vec<ValueId> = (0..nodes)
471 .map(|i| g.create_named_value(format!("v{i}"), DataType::Int64, static_shape([1])))
472 .collect();
473 let mut definitions = Vec::with_capacity(nodes);
474 for i in 0..nodes {
475 let mut node = if i == 0 || next(&mut seed).is_multiple_of(4) {
476 let mut constant = Node::new(NodeId(0), "Constant", vec![], vec![values[i]]);
477 constant.attributes.insert(
478 "value".into(),
479 Attribute::Tensor(int64_tensor(vec![1], &[(next(&mut seed) % 8) as i64])),
480 );
481 constant
482 } else if next(&mut seed).is_multiple_of(3) {
483 let input = values[(next(&mut seed) as usize) % i];
484 Node::new(NodeId(0), "Shape", vec![Some(input)], vec![values[i]])
485 } else {
486 let pick_input = |seed: &mut u64| {
487 if next(seed).is_multiple_of(4) {
488 init
489 } else {
490 values[(next(seed) as usize) % i]
491 }
492 };
493 Node::new(
494 NodeId(0),
495 "Add",
496 vec![Some(pick_input(&mut seed)), Some(pick_input(&mut seed))],
497 vec![values[i]],
498 )
499 };
500 node.name = format!("node_{i}");
501 definitions.push(node);
502 }
503
504 let mut order: Vec<usize> = (0..nodes).collect();
505 for i in (1..nodes).rev() {
506 order.swap(i, (next(&mut seed) as usize) % (i + 1));
507 }
508 for index in order {
509 g.insert_node(definitions[index].clone());
510 }
511 for (i, &value) in values.iter().enumerate() {
512 if i + 1 == nodes || (i > nodes / 2 && next(&mut seed).is_multiple_of(11)) {
513 g.add_output(value);
514 }
515 }
516 g
517 }
518
519 #[test]
520 fn seeded_dags_are_byte_identical_to_ascending_fixpoint() {
521 for seed in 0..32 {
522 let base = seeded_dag(seed, 96);
523 assert!(base.validate().is_ok(), "seed {seed}");
524 let mut reference = base.clone();
525 let mut worklist = base;
526 run_reference_ascending_fixpoint(&mut reference);
527 ConstantFolding
528 .run(&mut worklist, &PassContext::new())
529 .unwrap();
530 assert_eq!(serialized(&worklist), serialized(&reference), "seed {seed}");
531 }
532 }
533
534 #[test]
535 fn folds_add_of_two_const_inputs() {
536 let mut g = Graph::new();
537 g.opset_imports.insert(String::new(), 17);
538 let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
539 let b = const_init(&mut g, "b", vec![3], &[10, 20, 30]);
540 let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
541 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(a), Some(b)], vec![out]));
542 g.add_output(out);
543
544 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
545
546 assert_eq!(g.num_nodes(), 0, "Add should be folded away");
547 let t = inline_const(&g, out).expect("out is now an initializer");
548 assert_eq!(read_i64(t).unwrap(), vec![11, 22, 33]);
549 assert!(g.validate().is_ok());
550 }
551
552 #[test]
553 fn folds_sub_and_mul() {
554 for (op, expect) in [("Sub", vec![9, 18, 27]), ("Mul", vec![10, 40, 90])] {
555 let mut g = Graph::new();
556 g.opset_imports.insert(String::new(), 17);
557 let a = const_init(&mut g, "a", vec![3], &[10, 20, 30]);
558 let b = const_init(&mut g, "b", vec![3], &[1, 2, 3]);
559 let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
560 g.insert_node(Node::new(NodeId(0), op, vec![Some(a), Some(b)], vec![out]));
561 g.add_output(out);
562
563 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
564 let t = inline_const(&g, out).unwrap();
565 assert_eq!(read_i64(t).unwrap(), expect, "op {op}");
566 }
567 }
568
569 #[test]
570 fn does_not_fold_when_one_input_is_non_const() {
571 let mut g = Graph::new();
572 g.opset_imports.insert(String::new(), 17);
573 let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
574 let b = g.create_named_value("b", DataType::Int64, static_shape([3]));
576 g.add_input(b);
577 let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
578 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(a), Some(b)], vec![out]));
579 g.add_output(out);
580
581 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
582 assert_eq!(g.num_nodes(), 1, "must not fold with a non-const input");
583 assert!(inline_const(&g, out).is_none());
584 assert!(g.validate().is_ok());
585 }
586
587 #[test]
588 fn does_not_fold_mismatched_shapes() {
589 let mut g = Graph::new();
590 g.opset_imports.insert(String::new(), 17);
591 let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
592 let b = const_init(&mut g, "b", vec![2], &[10, 20]);
593 let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
594 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(a), Some(b)], vec![out]));
595 g.add_output(out);
596
597 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
598 assert_eq!(g.num_nodes(), 1, "no broadcasting in v1");
599 }
600
601 #[test]
602 fn does_not_fold_overflow() {
603 let mut g = Graph::new();
604 g.opset_imports.insert(String::new(), 17);
605 let a = const_init(&mut g, "a", vec![1], &[i64::MAX]);
606 let b = const_init(&mut g, "b", vec![1], &[1]);
607 let out = g.create_named_value("out", DataType::Int64, static_shape([1]));
608 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(a), Some(b)], vec![out]));
609 g.add_output(out);
610
611 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
612 assert_eq!(g.num_nodes(), 1, "overflow must abort the fold");
613 }
614
615 #[test]
616 fn folds_constant_node_to_initializer() {
617 let mut g = Graph::new();
618 g.opset_imports.insert(String::new(), 17);
619 let out = g.create_named_value("c", DataType::Int64, static_shape([2]));
620 let mut node = Node::new(NodeId(0), "Constant", vec![], vec![out]);
621 node.attributes
622 .insert("value".into(), Attribute::Tensor(int64_tensor(vec![2], &[7, 8])));
623 g.insert_node(node);
624 let sink = g.create_named_value("sink", DataType::Int64, static_shape([2]));
626 g.insert_node(Node::new(NodeId(0), "Identity", vec![Some(out)], vec![sink]));
627 g.add_output(sink);
628
629 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
630 assert!(g.try_node(NodeId(0)).is_none(), "Constant folded away");
631 let t = inline_const(&g, out).unwrap();
632 assert_eq!(read_i64(t).unwrap(), vec![7, 8]);
633 assert!(g.validate().is_ok());
634 }
635
636 #[test]
637 fn folds_shape_of_static_input() {
638 let mut g = Graph::new();
639 g.opset_imports.insert(String::new(), 17);
640 let x = g.create_named_value("x", DataType::Float32, static_shape([2, 3, 4]));
641 g.add_input(x);
642 let out = g.create_named_value("s", DataType::Int64, static_shape([3]));
643 g.insert_node(Node::new(NodeId(0), "Shape", vec![Some(x)], vec![out]));
644 g.add_output(out);
645
646 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
647 let t = inline_const(&g, out).expect("Shape folded to initializer");
648 assert_eq!(read_i64(t).unwrap(), vec![2, 3, 4]);
649 assert!(g.validate().is_ok());
650 }
651
652 #[test]
653 fn folds_transitively_to_fixpoint() {
654 let mut g = Graph::new();
656 g.opset_imports.insert(String::new(), 17);
657
658 let c1 = g.create_named_value("c1", DataType::Int64, static_shape([2]));
659 let mut n1 = Node::new(NodeId(0), "Constant", vec![], vec![c1]);
660 n1.attributes
661 .insert("value".into(), Attribute::Tensor(int64_tensor(vec![2], &[1, 2])));
662 g.insert_node(n1);
663
664 let c2 = g.create_named_value("c2", DataType::Int64, static_shape([2]));
665 let mut n2 = Node::new(NodeId(0), "Constant", vec![], vec![c2]);
666 n2.attributes
667 .insert("value".into(), Attribute::Tensor(int64_tensor(vec![2], &[3, 4])));
668 g.insert_node(n2);
669
670 let out = g.create_named_value("out", DataType::Int64, static_shape([2]));
671 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(c1), Some(c2)], vec![out]));
672 g.add_output(out);
673
674 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
675 assert_eq!(g.num_nodes(), 0, "both constants and the Add fold away");
676 let t = inline_const(&g, out).unwrap();
677 assert_eq!(read_i64(t).unwrap(), vec![4, 6]);
678 assert!(g.validate().is_ok());
679 }
680
681 fn constant_chain(nodes: usize, reverse_node_ids: bool) -> (Graph, ValueId) {
682 let mut g = Graph::new();
683 g.opset_imports.insert(String::new(), 17);
684 let zero = const_init(&mut g, "zero", vec![1], &[0]);
685 let one = const_init(&mut g, "one", vec![1], &[1]);
686 let mut values = Vec::with_capacity(nodes + 1);
687 values.push(zero);
688 for _ in 0..nodes {
689 values.push(g.create_value(DataType::Int64, static_shape([1])));
690 }
691
692 if reverse_node_ids {
693 for i in (1..=nodes).rev() {
694 g.insert_node(Node::new(
695 NodeId(0),
696 "Add",
697 vec![Some(values[i - 1]), Some(one)],
698 vec![values[i]],
699 ));
700 }
701 } else {
702 for i in 1..=nodes {
703 g.insert_node(Node::new(
704 NodeId(0),
705 "Add",
706 vec![Some(values[i - 1]), Some(one)],
707 vec![values[i]],
708 ));
709 }
710 }
711
712 let out = values[nodes];
713 g.add_output(out);
714 (g, out)
715 }
716
717 #[test]
718 fn reverse_node_id_chain_matches_forward_order() {
719 let (mut forward, forward_out) = constant_chain(64, false);
720 let (mut reverse, reverse_out) = constant_chain(64, true);
721
722 let reverse_ids = reverse.topological_order().unwrap();
723 assert!(
724 reverse_ids.windows(2).all(|ids| ids[0].0 > ids[1].0),
725 "test graph must have reverse dependency NodeIds"
726 );
727
728 ConstantFolding
729 .run(&mut forward, &PassContext::new())
730 .unwrap();
731 ConstantFolding
732 .run(&mut reverse, &PassContext::new())
733 .unwrap();
734
735 assert_eq!(forward.num_nodes(), 0);
736 assert_eq!(reverse.num_nodes(), 0);
737 assert_eq!(
738 inline_const(&forward, forward_out),
739 inline_const(&reverse, reverse_out)
740 );
741 assert_eq!(
742 read_i64(inline_const(&reverse, reverse_out).unwrap()),
743 Some(vec![64])
744 );
745 assert!(forward.validate().is_ok());
746 assert!(reverse.validate().is_ok());
747 }
748
749 #[test]
750 fn does_not_fold_float_binary() {
751 let mut g = Graph::new();
752 g.opset_imports.insert(String::new(), 17);
753 let mk = |g: &mut Graph, name: &str| {
754 let v = g.create_named_value(name, DataType::Float32, static_shape([2]));
755 g.set_initializer(
756 v,
757 WeightRef::Inline(TensorData::from_raw(
758 DataType::Float32,
759 vec![2],
760 vec![0u8; 8],
761 )),
762 );
763 v
764 };
765 let a = mk(&mut g, "a");
766 let b = mk(&mut g, "b");
767 let out = g.create_named_value("out", DataType::Float32, static_shape([2]));
768 g.insert_node(Node::new(NodeId(0), "Mul", vec![Some(a), Some(b)], vec![out]));
769 g.add_output(out);
770
771 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
772 assert_eq!(g.num_nodes(), 1, "float folding is out of scope in v1");
773 }
774}