1use std::cmp::Reverse;
40use std::collections::{BinaryHeap, HashMap};
41
42use onnx_runtime_ir::{
43 Attribute, DataType, Graph, NodeId, TensorData, ValueId, WeightRef, as_static_shape,
44 checked_numel, is_fully_static, read_vec_le, static_shape,
45};
46
47use crate::error::Result;
48use crate::pass::{OptimizationPass, PassContext};
49
50const MAX_FOLD_ELEMS: usize = 1024;
54
55const MAX_WEIGHT_FOLD_ELEMS: usize = 1 << 30;
71
72#[derive(Clone, Copy, Debug, Default)]
74pub struct ConstantFolding;
75
76impl OptimizationPass for ConstantFolding {
77 fn name(&self) -> &str {
78 "ConstantFolding"
79 }
80
81 fn run(&self, graph: &mut Graph, _ctx: &PassContext) -> Result<()> {
82 let candidates: Vec<NodeId> = graph
83 .nodes
84 .iter()
85 .filter_map(|(nid, node)| is_candidate(node).then_some(nid))
86 .collect();
87 let mut unresolved = HashMap::with_capacity(candidates.len());
88 let mut dependents: HashMap<ValueId, Vec<NodeId>> =
89 HashMap::with_capacity(candidates.len());
90 let mut current_wave = BinaryHeap::new();
93 let mut next_wave = BinaryHeap::new();
94
95 for nid in candidates {
96 let node = graph.node(nid);
97 let Some(inputs) = unresolved_inputs(graph, node) else {
98 continue;
99 };
100 let count = inputs.len();
101 unresolved.insert(nid, count);
102 if count == 0 {
103 current_wave.push(Reverse(nid.0));
104 } else {
105 for input in inputs {
106 dependents.entry(input).or_default().push(nid);
107 }
108 }
109 }
110
111 while !current_wave.is_empty() {
112 while let Some(Reverse(raw_nid)) = current_wave.pop() {
113 let nid = NodeId(raw_nid);
114 if unresolved.remove(&nid).is_none() || !graph.nodes.contains(nid) {
115 continue;
116 }
117 let (out, folded) = {
118 let node = graph.node(nid);
119 let folded = match node.op_type.as_str() {
120 "Constant" => eval_constant(node),
121 "Shape" => fold_shape(graph, node),
122 "Add" | "Sub" | "Mul" => fold_binary_int(graph, node),
123 "Concat" => fold_concat(graph, node),
124 "Reshape" => fold_reshape(graph, node),
125 "Transpose" => fold_transpose(graph, node),
126 _ => None,
127 };
128 (node.outputs[0], folded)
129 };
130 let Some(tensor) = folded else { continue };
131
132 let needed = graph.outputs.contains(&out)
136 || graph.try_value(out).is_some_and(|_| graph.has_uses(out));
137 if !needed {
138 continue;
139 }
140
141 graph.remove_node(nid);
142 if graph.try_value(out).is_none() {
145 continue;
146 }
147 let dims = tensor.dims.clone();
148 let dtype = tensor.dtype;
149 let v = graph.value_mut(out);
150 v.dtype = dtype;
151 v.shape = static_shape(dims);
152 graph.set_initializer(out, WeightRef::Inline(tensor));
153
154 for consumer in dependents.remove(&out).unwrap_or_default() {
155 let Some(count) = unresolved.get_mut(&consumer) else {
156 continue;
157 };
158 *count -= 1;
159 if *count == 0 {
160 let wave = if consumer.0 > nid.0 {
161 &mut current_wave
162 } else {
163 &mut next_wave
164 };
165 wave.push(Reverse(consumer.0));
166 }
167 }
168 }
169 std::mem::swap(&mut current_wave, &mut next_wave);
170 }
171 Ok(())
172 }
173}
174
175fn is_candidate(node: &onnx_runtime_ir::Node) -> bool {
176 matches!(node.domain.as_str(), "" | "ai.onnx")
177 && node.outputs.len() == 1
178 && matches!(
179 node.op_type.as_str(),
180 "Constant" | "Shape" | "Add" | "Sub" | "Mul" | "Concat" | "Reshape" | "Transpose"
181 )
182}
183
184fn unresolved_inputs(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<Vec<ValueId>> {
185 match node.op_type.as_str() {
186 "Constant" => Some(Vec::new()),
187 "Shape" => {
188 let input = node.inputs.first().copied().flatten()?;
189 let shape = &graph.try_value(input)?.shape;
190 if shape.len() <= MAX_FOLD_ELEMS && is_fully_static(shape) {
191 Some(Vec::new())
192 } else {
193 Some(vec![input])
194 }
195 }
196 "Add" | "Sub" | "Mul" => {
197 if node.inputs.len() != 2 {
198 return None;
199 }
200 let inputs = [node.inputs[0]?, node.inputs[1]?];
201 Some(
202 inputs
203 .into_iter()
204 .filter(|&input| inline_const(graph, input).is_none())
205 .collect(),
206 )
207 }
208 "Concat" => {
209 if node.inputs.is_empty() {
210 return None;
211 }
212 let inputs: Vec<ValueId> = node.inputs.iter().copied().collect::<Option<_>>()?;
213 Some(
214 inputs
215 .into_iter()
216 .filter(|&input| inline_const(graph, input).is_none())
217 .collect(),
218 )
219 }
220 "Reshape" => {
221 if node.attr("allowzero").and_then(Attribute::as_int) == Some(1) {
222 return None; }
224 let data = node.inputs.first().copied().flatten()?;
225 let shape = node.inputs.get(1).copied().flatten()?;
226 Some(
227 [data, shape]
228 .into_iter()
229 .filter(|&input| inline_const(graph, input).is_none())
230 .collect(),
231 )
232 }
233 "Transpose" => {
234 let data = node.inputs.first().copied().flatten()?;
235 Some(
236 [data]
237 .into_iter()
238 .filter(|&input| inline_const(graph, input).is_none())
239 .collect(),
240 )
241 }
242 _ => None,
243 }
244}
245
246fn inline_const(graph: &Graph, value: ValueId) -> Option<&TensorData> {
249 match graph.initializers.get(&value)? {
250 WeightRef::Inline(t) => Some(t),
251 WeightRef::External { .. } => None,
252 }
253}
254
255fn eval_constant(node: &onnx_runtime_ir::Node) -> Option<TensorData> {
257 if let Some(Attribute::Tensor(t)) = node.attr("value") {
258 return Some(t.clone());
259 }
260 if let Some(ints) = node.attr("value_ints").and_then(Attribute::as_ints) {
261 let mut data = Vec::with_capacity(ints.len() * 8);
262 for &i in ints {
263 data.extend_from_slice(&i.to_le_bytes());
264 }
265 return Some(TensorData::from_raw(
266 DataType::Int64,
267 vec![ints.len()],
268 data,
269 ));
270 }
271 if let Some(i) = node.attr("value_int").and_then(Attribute::as_int) {
272 return Some(TensorData::from_raw(
273 DataType::Int64,
274 Vec::new(),
275 i.to_le_bytes().to_vec(),
276 ));
277 }
278 None
279}
280
281fn fold_shape(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
286 let input = node.inputs.first().copied().flatten()?;
287 let shape = &graph.try_value(input)?.shape;
288 let dims = as_static_shape(shape)?;
289 if dims.len() > MAX_FOLD_ELEMS {
290 return None;
291 }
292 let rank = dims.len() as i64;
293 let clamp = |v: i64| -> usize { v.clamp(0, rank) as usize };
294 let start = node
295 .attr("start")
296 .and_then(Attribute::as_int)
297 .map_or(0, |v| clamp(if v < 0 { v + rank } else { v }));
298 let end = node
299 .attr("end")
300 .and_then(Attribute::as_int)
301 .map_or(dims.len(), |v| clamp(if v < 0 { v + rank } else { v }));
302 let sliced = if start < end { &dims[start..end] } else { &[] };
303 let mut data = Vec::with_capacity(sliced.len() * 8);
304 for &d in sliced {
305 data.extend_from_slice(&(d as i64).to_le_bytes());
306 }
307 Some(TensorData::from_raw(
308 DataType::Int64,
309 vec![sliced.len()],
310 data,
311 ))
312}
313
314fn fold_binary_int(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
317 if node.inputs.len() != 2 {
318 return None;
319 }
320 let a = inline_const(graph, node.inputs[0]?)?;
321 let b = inline_const(graph, node.inputs[1]?)?;
322 if a.dtype != b.dtype || a.dims != b.dims {
323 return None; }
325 if !matches!(a.dtype, DataType::Int32 | DataType::Int64) {
326 return None;
327 }
328 let numel = a.numel();
329 if numel > MAX_FOLD_ELEMS {
330 return None;
331 }
332 let op = node.op_type.as_str();
333 let apply = |x: i64, y: i64| -> Option<i64> {
334 match op {
335 "Add" => x.checked_add(y),
336 "Sub" => x.checked_sub(y),
337 "Mul" => x.checked_mul(y),
338 _ => None,
339 }
340 };
341
342 match a.dtype {
343 DataType::Int64 => {
344 let (xs, ys) = (read_i64(a)?, read_i64(b)?);
345 let mut data = Vec::with_capacity(numel * 8);
346 for (x, y) in xs.into_iter().zip(ys) {
347 data.extend_from_slice(&apply(x, y)?.to_le_bytes());
348 }
349 Some(TensorData::from_raw(DataType::Int64, a.dims.clone(), data))
350 }
351 DataType::Int32 => {
352 let (xs, ys) = (read_i32(a)?, read_i32(b)?);
353 let mut data = Vec::with_capacity(numel * 4);
354 for (x, y) in xs.into_iter().zip(ys) {
355 let r = apply(x as i64, y as i64)?;
356 let r32: i32 = r.try_into().ok()?; data.extend_from_slice(&r32.to_le_bytes());
358 }
359 Some(TensorData::from_raw(DataType::Int32, a.dims.clone(), data))
360 }
361 _ => None,
362 }
363}
364
365fn fold_concat(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
371 let axis_attr = node.attr("axis").and_then(Attribute::as_int)?;
372 let inputs: Vec<&TensorData> = node
373 .inputs
374 .iter()
375 .map(|slot| inline_const(graph, (*slot)?))
376 .collect::<Option<_>>()?;
377 let first = *inputs.first()?;
378 if first.dtype == DataType::String || !first.strings.is_empty() {
379 return None;
380 }
381 let elem_size = first.dtype.byte_size();
382 if elem_size == 0 {
383 return None; }
385 let rank = first.dims.len();
386 if rank == 0 {
387 return None;
388 }
389 let axis = normalize_axis(axis_attr, rank)?;
390 let mut axis_sum = 0usize;
391 for t in &inputs {
392 if t.dtype != first.dtype || t.dims.len() != rank {
393 return None;
394 }
395 for (i, (&a, &b)) in t.dims.iter().zip(first.dims.iter()).enumerate() {
396 if i != axis && a != b {
397 return None;
398 }
399 }
400 axis_sum = axis_sum.checked_add(t.dims[axis])?;
401 }
402 let mut out_dims = first.dims.clone();
403 out_dims[axis] = axis_sum;
404 let numel = checked_numel(&out_dims)?;
405 if numel > MAX_FOLD_ELEMS {
406 return None;
407 }
408 let outer: usize = out_dims[..axis].iter().product();
409 let inner: usize = out_dims[axis + 1..].iter().product();
410 let mut out_bytes = vec![0u8; numel.checked_mul(elem_size)?];
411 let mut dst = 0usize;
412 for o in 0..outer {
413 for t in &inputs {
414 let slab_len = t.dims[axis].checked_mul(inner)?.checked_mul(elem_size)?;
415 let src_off = o.checked_mul(slab_len)?;
416 let src = t.data.get(src_off..src_off + slab_len)?;
417 out_bytes[dst..dst + slab_len].copy_from_slice(src);
418 dst += slab_len;
419 }
420 }
421 Some(TensorData::from_raw(first.dtype, out_dims, out_bytes))
422}
423
424fn normalize_axis(axis: i64, rank: usize) -> Option<usize> {
425 let r = rank as i64;
426 let a = if axis < 0 { axis + r } else { axis };
427 (0..r).contains(&a).then_some(a as usize)
428}
429
430fn fold_reshape(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
435 let data_id = node.inputs.first().copied().flatten()?;
436 let shape_id = node.inputs.get(1).copied().flatten()?;
437 let data = inline_const(graph, data_id)?;
438 let shape_tensor = inline_const(graph, shape_id)?;
439 if shape_tensor.dtype != DataType::Int64 {
440 return None;
441 }
442 if data.dtype == DataType::String || !data.strings.is_empty() {
443 return None;
444 }
445 let numel = data.checked_numel()?;
446 if numel > MAX_WEIGHT_FOLD_ELEMS {
447 return None;
448 }
449 let shape_vals = read_i64(shape_tensor)?;
450 let resolved = resolve_reshape_dims(&data.dims, &shape_vals)?;
451 if checked_numel(&resolved)? != numel {
452 return None;
453 }
454 Some(TensorData::from_raw(
455 data.dtype,
456 resolved,
457 data.data.clone(),
458 ))
459}
460
461fn resolve_reshape_dims(input_dims: &[usize], shape_vals: &[i64]) -> Option<Vec<usize>> {
465 let mut resolved = Vec::with_capacity(shape_vals.len());
466 let mut infer_at: Option<usize> = None;
467 for (i, &v) in shape_vals.iter().enumerate() {
468 if v == -1 {
469 if infer_at.is_some() {
470 return None; }
472 infer_at = Some(i);
473 resolved.push(0); } else if v == 0 {
475 resolved.push(*input_dims.get(i)?);
476 } else {
477 resolved.push(usize::try_from(v).ok()?);
478 }
479 }
480 if let Some(i) = infer_at {
481 let known = resolved
482 .iter()
483 .enumerate()
484 .filter(|&(idx, _)| idx != i)
485 .try_fold(1usize, |acc, (_, &d)| acc.checked_mul(d))?;
486 if known == 0 {
487 return None; }
489 let total = checked_numel(input_dims)?;
490 if total % known != 0 {
491 return None;
492 }
493 resolved[i] = total / known;
494 }
495 Some(resolved)
496}
497
498fn fold_transpose(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
503 let input = node.inputs.first().copied().flatten()?;
504 let data = inline_const(graph, input)?;
505 if data.dtype == DataType::String || !data.strings.is_empty() {
506 return None;
507 }
508 let elem_size = data.dtype.byte_size();
509 if elem_size == 0 {
510 return None;
511 }
512 let rank = data.dims.len();
513 let perm: Vec<usize> = match node.attr("perm").and_then(Attribute::as_ints) {
514 Some(ints) => {
515 if ints.len() != rank {
516 return None;
517 }
518 let mut p = Vec::with_capacity(rank);
519 for &v in ints {
520 let v = usize::try_from(v).ok()?;
521 if v >= rank {
522 return None;
523 }
524 p.push(v);
525 }
526 p
527 }
528 None => (0..rank).rev().collect(),
529 };
530 let mut seen = vec![false; rank];
531 for &p in &perm {
532 if std::mem::replace(&mut seen[p], true) {
533 return None; }
535 }
536 let numel = data.checked_numel()?;
537 if numel > MAX_WEIGHT_FOLD_ELEMS {
538 return None;
539 }
540 if data.data.len() != numel.checked_mul(elem_size)? {
541 return None; }
543 let out_dims: Vec<usize> = perm.iter().map(|&p| data.dims[p]).collect();
544 let mut in_strides = vec![1usize; rank];
545 for i in (0..rank.saturating_sub(1)).rev() {
546 in_strides[i] = in_strides[i + 1].checked_mul(data.dims[i + 1])?;
547 }
548 let mut out_strides = vec![1usize; rank];
549 for i in (0..rank.saturating_sub(1)).rev() {
550 out_strides[i] = out_strides[i + 1].checked_mul(out_dims[i + 1])?;
551 }
552 let mut out_bytes = vec![0u8; numel.checked_mul(elem_size)?];
553 let mut idx = vec![0usize; rank];
554 for o in 0..numel {
555 let mut rem = o;
556 for (k, stride) in out_strides.iter().enumerate() {
557 idx[k] = rem / stride;
558 rem %= stride;
559 }
560 let mut in_flat = 0usize;
561 for (k, &p) in perm.iter().enumerate() {
562 in_flat += idx[k] * in_strides[p];
563 }
564 let src = in_flat * elem_size;
565 let dst = o * elem_size;
566 out_bytes[dst..dst + elem_size].copy_from_slice(&data.data[src..src + elem_size]);
567 }
568 Some(TensorData::from_raw(data.dtype, out_dims, out_bytes))
569}
570
571fn read_i64(t: &TensorData) -> Option<Vec<i64>> {
572 if t.data.len() != t.numel() * 8 {
573 return None;
574 }
575 read_vec_le(&t.data).ok()
576}
577
578fn read_i32(t: &TensorData) -> Option<Vec<i32>> {
579 if t.data.len() != t.numel() * 4 {
580 return None;
581 }
582 read_vec_le(&t.data).ok()
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588 use crate::DeadNodeElimination;
589 use onnx_runtime_ir::{Node, NodeId};
590 use onnx_runtime_loader::{Model, encode_model};
591
592 fn int64_tensor(dims: Vec<usize>, vals: &[i64]) -> TensorData {
593 let mut data = Vec::new();
594 for &v in vals {
595 data.extend_from_slice(&v.to_le_bytes());
596 }
597 TensorData::from_raw(DataType::Int64, dims, data)
598 }
599
600 fn const_init(graph: &mut Graph, name: &str, dims: Vec<usize>, vals: &[i64]) -> ValueId {
601 let shape = static_shape(dims.clone());
602 let v = graph.create_named_value(name, DataType::Int64, shape);
603 graph.set_initializer(v, WeightRef::Inline(int64_tensor(dims, vals)));
604 v
605 }
606
607 fn run_reference_ascending_fixpoint(graph: &mut Graph) {
608 loop {
609 let mut changed = false;
610 let node_ids: Vec<NodeId> = graph.nodes.keys().collect();
611 for nid in node_ids {
612 if !graph.nodes.contains(nid) {
613 continue;
614 }
615 let node = graph.node(nid).clone();
616 if !matches!(node.domain.as_str(), "" | "ai.onnx") || node.outputs.len() != 1 {
617 continue;
618 }
619 let out = node.outputs[0];
620 let folded = match node.op_type.as_str() {
621 "Constant" => eval_constant(&node),
622 "Shape" => fold_shape(graph, &node),
623 "Add" | "Sub" | "Mul" => fold_binary_int(graph, &node),
624 "Concat" => fold_concat(graph, &node),
625 "Reshape" => fold_reshape(graph, &node),
626 "Transpose" => fold_transpose(graph, &node),
627 _ => None,
628 };
629 let Some(tensor) = folded else { continue };
630 let needed = graph.outputs.contains(&out)
631 || graph.try_value(out).is_some_and(|_| graph.has_uses(out));
632 if !needed {
633 continue;
634 }
635
636 graph.remove_node(nid);
637 if graph.try_value(out).is_some() {
638 let dims = tensor.dims.clone();
639 let dtype = tensor.dtype;
640 let v = graph.value_mut(out);
641 v.dtype = dtype;
642 v.shape = static_shape(dims);
643 graph.set_initializer(out, WeightRef::Inline(tensor));
644 changed = true;
645 }
646 }
647 if !changed {
648 break;
649 }
650 }
651 }
652
653 fn serialized(graph: &Graph) -> Vec<u8> {
654 encode_model(&Model::new(graph)).expect("serialize graph")
655 }
656
657 fn schedule_sensitive_chain() -> (Graph, ValueId) {
658 let mut g = Graph::new();
659 g.opset_imports.insert(String::new(), 17);
660 let init = const_init(&mut g, "init", vec![1], &[2]);
661 let a = g.create_named_value("a", DataType::Int64, static_shape([1]));
662 let b = g.create_named_value("b", DataType::Int64, static_shape([1]));
663 let out = g.create_named_value("out", DataType::Int64, static_shape([1]));
664
665 let mut constant = Node::new(NodeId(0), "Constant", vec![], vec![a]);
666 constant.attributes.insert(
667 "value".into(),
668 Attribute::Tensor(int64_tensor(vec![1], &[3])),
669 );
670 g.insert_node(constant);
671 g.insert_node(Node::new(
672 NodeId(0),
673 "Add",
674 vec![Some(a), Some(init)],
675 vec![b],
676 ));
677 g.insert_node(Node::new(NodeId(0), "Shape", vec![Some(b)], vec![out]));
678 g.add_output(out);
679 (g, out)
680 }
681
682 #[test]
683 fn ascending_wave_folds_constant_add_before_shape_consumer() {
684 let (base, out) = schedule_sensitive_chain();
685 let mut reference = base.clone();
686 let mut worklist = base;
687 run_reference_ascending_fixpoint(&mut reference);
688 ConstantFolding
689 .run(&mut worklist, &PassContext::new())
690 .unwrap();
691
692 assert_eq!(worklist.num_nodes(), 0, "Constant, Add, and Shape fold");
693 assert!(!worklist.nodes.values().any(|node| node.op_type == "Add"));
694 assert_eq!(
695 read_i64(inline_const(&worklist, out).unwrap()),
696 Some(vec![1])
697 );
698 assert_eq!(serialized(&worklist), serialized(&reference));
699 assert!(worklist.validate().is_ok());
700 }
701
702 #[test]
703 fn ascending_wave_leaves_lower_dead_producer_unfolded() {
704 let mut base = Graph::new();
705 base.opset_imports.insert(String::new(), 17);
706 let init = const_init(&mut base, "init", vec![1], &[2]);
707 let a = base.create_named_value("a", DataType::Int64, static_shape([1]));
708 let b = base.create_named_value("b", DataType::Int64, static_shape([1]));
709 let out = base.create_named_value("out", DataType::Int64, static_shape([1]));
710
711 base.insert_node(Node::new(
712 NodeId(0),
713 "Add",
714 vec![Some(a), Some(init)],
715 vec![b],
716 ));
717 base.insert_node(Node::new(NodeId(0), "Shape", vec![Some(b)], vec![out]));
718 let mut constant = Node::new(NodeId(0), "Constant", vec![], vec![a]);
719 constant.attributes.insert(
720 "value".into(),
721 Attribute::Tensor(int64_tensor(vec![1], &[3])),
722 );
723 base.insert_node(constant);
724 base.add_output(out);
725
726 let mut reference = base.clone();
727 let mut worklist = base;
728 run_reference_ascending_fixpoint(&mut reference);
729 ConstantFolding
730 .run(&mut worklist, &PassContext::new())
731 .unwrap();
732
733 assert_eq!(worklist.num_nodes(), 1);
734 assert_eq!(worklist.nodes.values().next().unwrap().op_type, "Add");
735 assert!(inline_const(&worklist, b).is_none());
736 assert_eq!(serialized(&worklist), serialized(&reference));
737 assert!(worklist.validate().is_ok());
738 }
739
740 fn seeded_dag(mut seed: u64, nodes: usize) -> Graph {
741 fn next(seed: &mut u64) -> u64 {
742 *seed = seed
743 .wrapping_mul(6_364_136_223_846_793_005)
744 .wrapping_add(1_442_695_040_888_963_407);
745 *seed
746 }
747
748 let mut g = Graph::new();
749 g.opset_imports.insert(String::new(), 17);
750 let init = const_init(&mut g, "init", vec![1], &[1]);
751 let values: Vec<ValueId> = (0..nodes)
752 .map(|i| g.create_named_value(format!("v{i}"), DataType::Int64, static_shape([1])))
753 .collect();
754 let mut definitions = Vec::with_capacity(nodes);
755 for i in 0..nodes {
756 let mut node = if i == 0 || next(&mut seed).is_multiple_of(4) {
757 let mut constant = Node::new(NodeId(0), "Constant", vec![], vec![values[i]]);
758 constant.attributes.insert(
759 "value".into(),
760 Attribute::Tensor(int64_tensor(vec![1], &[(next(&mut seed) % 8) as i64])),
761 );
762 constant
763 } else if next(&mut seed).is_multiple_of(3) {
764 let input = values[(next(&mut seed) as usize) % i];
765 Node::new(NodeId(0), "Shape", vec![Some(input)], vec![values[i]])
766 } else {
767 let pick_input = |seed: &mut u64| {
768 if next(seed).is_multiple_of(4) {
769 init
770 } else {
771 values[(next(seed) as usize) % i]
772 }
773 };
774 Node::new(
775 NodeId(0),
776 "Add",
777 vec![Some(pick_input(&mut seed)), Some(pick_input(&mut seed))],
778 vec![values[i]],
779 )
780 };
781 node.name = format!("node_{i}");
782 definitions.push(node);
783 }
784
785 let mut order: Vec<usize> = (0..nodes).collect();
786 for i in (1..nodes).rev() {
787 order.swap(i, (next(&mut seed) as usize) % (i + 1));
788 }
789 for index in order {
790 g.insert_node(definitions[index].clone());
791 }
792 for (i, &value) in values.iter().enumerate() {
793 if i + 1 == nodes || (i > nodes / 2 && next(&mut seed).is_multiple_of(11)) {
794 g.add_output(value);
795 }
796 }
797 g
798 }
799
800 #[test]
801 fn seeded_dags_are_byte_identical_to_ascending_fixpoint() {
802 for seed in 0..32 {
803 let base = seeded_dag(seed, 96);
804 assert!(base.validate().is_ok(), "seed {seed}");
805 let mut reference = base.clone();
806 let mut worklist = base;
807 run_reference_ascending_fixpoint(&mut reference);
808 ConstantFolding
809 .run(&mut worklist, &PassContext::new())
810 .unwrap();
811 assert_eq!(serialized(&worklist), serialized(&reference), "seed {seed}");
812 }
813 }
814
815 #[test]
816 fn folds_add_of_two_const_inputs() {
817 let mut g = Graph::new();
818 g.opset_imports.insert(String::new(), 17);
819 let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
820 let b = const_init(&mut g, "b", vec![3], &[10, 20, 30]);
821 let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
822 g.insert_node(Node::new(
823 NodeId(0),
824 "Add",
825 vec![Some(a), Some(b)],
826 vec![out],
827 ));
828 g.add_output(out);
829
830 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
831
832 assert_eq!(g.num_nodes(), 0, "Add should be folded away");
833 let t = inline_const(&g, out).expect("out is now an initializer");
834 assert_eq!(read_i64(t).unwrap(), vec![11, 22, 33]);
835 assert!(g.validate().is_ok());
836 }
837
838 #[test]
839 fn folds_sub_and_mul() {
840 for (op, expect) in [("Sub", vec![9, 18, 27]), ("Mul", vec![10, 40, 90])] {
841 let mut g = Graph::new();
842 g.opset_imports.insert(String::new(), 17);
843 let a = const_init(&mut g, "a", vec![3], &[10, 20, 30]);
844 let b = const_init(&mut g, "b", vec![3], &[1, 2, 3]);
845 let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
846 g.insert_node(Node::new(NodeId(0), op, vec![Some(a), Some(b)], vec![out]));
847 g.add_output(out);
848
849 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
850 let t = inline_const(&g, out).unwrap();
851 assert_eq!(read_i64(t).unwrap(), expect, "op {op}");
852 }
853 }
854
855 #[test]
856 fn does_not_fold_when_one_input_is_non_const() {
857 let mut g = Graph::new();
858 g.opset_imports.insert(String::new(), 17);
859 let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
860 let b = g.create_named_value("b", DataType::Int64, static_shape([3]));
862 g.add_input(b);
863 let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
864 g.insert_node(Node::new(
865 NodeId(0),
866 "Add",
867 vec![Some(a), Some(b)],
868 vec![out],
869 ));
870 g.add_output(out);
871
872 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
873 assert_eq!(g.num_nodes(), 1, "must not fold with a non-const input");
874 assert!(inline_const(&g, out).is_none());
875 assert!(g.validate().is_ok());
876 }
877
878 #[test]
879 fn does_not_fold_mismatched_shapes() {
880 let mut g = Graph::new();
881 g.opset_imports.insert(String::new(), 17);
882 let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
883 let b = const_init(&mut g, "b", vec![2], &[10, 20]);
884 let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
885 g.insert_node(Node::new(
886 NodeId(0),
887 "Add",
888 vec![Some(a), Some(b)],
889 vec![out],
890 ));
891 g.add_output(out);
892
893 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
894 assert_eq!(g.num_nodes(), 1, "no broadcasting in v1");
895 }
896
897 #[test]
898 fn does_not_fold_overflow() {
899 let mut g = Graph::new();
900 g.opset_imports.insert(String::new(), 17);
901 let a = const_init(&mut g, "a", vec![1], &[i64::MAX]);
902 let b = const_init(&mut g, "b", vec![1], &[1]);
903 let out = g.create_named_value("out", DataType::Int64, static_shape([1]));
904 g.insert_node(Node::new(
905 NodeId(0),
906 "Add",
907 vec![Some(a), Some(b)],
908 vec![out],
909 ));
910 g.add_output(out);
911
912 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
913 assert_eq!(g.num_nodes(), 1, "overflow must abort the fold");
914 }
915
916 #[test]
917 fn folds_constant_node_to_initializer() {
918 let mut g = Graph::new();
919 g.opset_imports.insert(String::new(), 17);
920 let out = g.create_named_value("c", DataType::Int64, static_shape([2]));
921 let mut node = Node::new(NodeId(0), "Constant", vec![], vec![out]);
922 node.attributes.insert(
923 "value".into(),
924 Attribute::Tensor(int64_tensor(vec![2], &[7, 8])),
925 );
926 g.insert_node(node);
927 let sink = g.create_named_value("sink", DataType::Int64, static_shape([2]));
929 g.insert_node(Node::new(
930 NodeId(0),
931 "Identity",
932 vec![Some(out)],
933 vec![sink],
934 ));
935 g.add_output(sink);
936
937 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
938 assert!(g.try_node(NodeId(0)).is_none(), "Constant folded away");
939 let t = inline_const(&g, out).unwrap();
940 assert_eq!(read_i64(t).unwrap(), vec![7, 8]);
941 assert!(g.validate().is_ok());
942 }
943
944 #[test]
945 fn folds_shape_of_static_input() {
946 let mut g = Graph::new();
947 g.opset_imports.insert(String::new(), 17);
948 let x = g.create_named_value("x", DataType::Float32, static_shape([2, 3, 4]));
949 g.add_input(x);
950 let out = g.create_named_value("s", DataType::Int64, static_shape([3]));
951 g.insert_node(Node::new(NodeId(0), "Shape", vec![Some(x)], vec![out]));
952 g.add_output(out);
953
954 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
955 let t = inline_const(&g, out).expect("Shape folded to initializer");
956 assert_eq!(read_i64(t).unwrap(), vec![2, 3, 4]);
957 assert!(g.validate().is_ok());
958 }
959
960 #[test]
961 fn folds_transitively_to_fixpoint() {
962 let mut g = Graph::new();
964 g.opset_imports.insert(String::new(), 17);
965
966 let c1 = g.create_named_value("c1", DataType::Int64, static_shape([2]));
967 let mut n1 = Node::new(NodeId(0), "Constant", vec![], vec![c1]);
968 n1.attributes.insert(
969 "value".into(),
970 Attribute::Tensor(int64_tensor(vec![2], &[1, 2])),
971 );
972 g.insert_node(n1);
973
974 let c2 = g.create_named_value("c2", DataType::Int64, static_shape([2]));
975 let mut n2 = Node::new(NodeId(0), "Constant", vec![], vec![c2]);
976 n2.attributes.insert(
977 "value".into(),
978 Attribute::Tensor(int64_tensor(vec![2], &[3, 4])),
979 );
980 g.insert_node(n2);
981
982 let out = g.create_named_value("out", DataType::Int64, static_shape([2]));
983 g.insert_node(Node::new(
984 NodeId(0),
985 "Add",
986 vec![Some(c1), Some(c2)],
987 vec![out],
988 ));
989 g.add_output(out);
990
991 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
992 assert_eq!(g.num_nodes(), 0, "both constants and the Add fold away");
993 let t = inline_const(&g, out).unwrap();
994 assert_eq!(read_i64(t).unwrap(), vec![4, 6]);
995 assert!(g.validate().is_ok());
996 }
997
998 fn constant_chain(nodes: usize, reverse_node_ids: bool) -> (Graph, ValueId) {
999 let mut g = Graph::new();
1000 g.opset_imports.insert(String::new(), 17);
1001 let zero = const_init(&mut g, "zero", vec![1], &[0]);
1002 let one = const_init(&mut g, "one", vec![1], &[1]);
1003 let mut values = Vec::with_capacity(nodes + 1);
1004 values.push(zero);
1005 for _ in 0..nodes {
1006 values.push(g.create_value(DataType::Int64, static_shape([1])));
1007 }
1008
1009 if reverse_node_ids {
1010 for i in (1..=nodes).rev() {
1011 g.insert_node(Node::new(
1012 NodeId(0),
1013 "Add",
1014 vec![Some(values[i - 1]), Some(one)],
1015 vec![values[i]],
1016 ));
1017 }
1018 } else {
1019 for i in 1..=nodes {
1020 g.insert_node(Node::new(
1021 NodeId(0),
1022 "Add",
1023 vec![Some(values[i - 1]), Some(one)],
1024 vec![values[i]],
1025 ));
1026 }
1027 }
1028
1029 let out = values[nodes];
1030 g.add_output(out);
1031 (g, out)
1032 }
1033
1034 #[test]
1035 fn reverse_node_id_chain_matches_forward_order() {
1036 let (mut forward, forward_out) = constant_chain(64, false);
1037 let (mut reverse, reverse_out) = constant_chain(64, true);
1038
1039 let reverse_ids = reverse.topological_order().unwrap();
1040 assert!(
1041 reverse_ids.windows(2).all(|ids| ids[0].0 > ids[1].0),
1042 "test graph must have reverse dependency NodeIds"
1043 );
1044
1045 ConstantFolding
1046 .run(&mut forward, &PassContext::new())
1047 .unwrap();
1048 ConstantFolding
1049 .run(&mut reverse, &PassContext::new())
1050 .unwrap();
1051
1052 assert_eq!(forward.num_nodes(), 0);
1053 assert_eq!(reverse.num_nodes(), 0);
1054 assert_eq!(
1055 inline_const(&forward, forward_out),
1056 inline_const(&reverse, reverse_out)
1057 );
1058 assert_eq!(
1059 read_i64(inline_const(&reverse, reverse_out).unwrap()),
1060 Some(vec![64])
1061 );
1062 assert!(forward.validate().is_ok());
1063 assert!(reverse.validate().is_ok());
1064 }
1065
1066 #[test]
1067 fn does_not_fold_float_binary() {
1068 let mut g = Graph::new();
1069 g.opset_imports.insert(String::new(), 17);
1070 let mk = |g: &mut Graph, name: &str| {
1071 let v = g.create_named_value(name, DataType::Float32, static_shape([2]));
1072 g.set_initializer(
1073 v,
1074 WeightRef::Inline(TensorData::from_raw(
1075 DataType::Float32,
1076 vec![2],
1077 vec![0u8; 8],
1078 )),
1079 );
1080 v
1081 };
1082 let a = mk(&mut g, "a");
1083 let b = mk(&mut g, "b");
1084 let out = g.create_named_value("out", DataType::Float32, static_shape([2]));
1085 g.insert_node(Node::new(
1086 NodeId(0),
1087 "Mul",
1088 vec![Some(a), Some(b)],
1089 vec![out],
1090 ));
1091 g.add_output(out);
1092
1093 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
1094 assert_eq!(g.num_nodes(), 1, "float folding is out of scope in v1");
1095 }
1096
1097 fn raw_const_init(
1098 graph: &mut Graph,
1099 name: &str,
1100 dtype: DataType,
1101 dims: Vec<usize>,
1102 data: Vec<u8>,
1103 ) -> ValueId {
1104 let shape = static_shape(dims.clone());
1105 let v = graph.create_named_value(name, dtype, shape);
1106 graph.set_initializer(
1107 v,
1108 WeightRef::Inline(TensorData::from_raw(dtype, dims, data)),
1109 );
1110 v
1111 }
1112
1113 fn ints_attr_node(op_type: &str, inputs: Vec<Option<ValueId>>, outputs: Vec<ValueId>) -> Node {
1114 Node::new(NodeId(0), op_type, inputs, outputs)
1115 }
1116
1117 #[test]
1118 fn folds_reshape_with_literal_shape() {
1119 let mut g = Graph::new();
1120 g.opset_imports.insert(String::new(), 17);
1121 let data = raw_const_init(&mut g, "x", DataType::Uint8, vec![2, 3], (0u8..6).collect());
1122 let shape = const_init(&mut g, "shape", vec![1], &[6]);
1123 let out = g.create_named_value("out", DataType::Uint8, static_shape([6]));
1124 g.insert_node(ints_attr_node(
1125 "Reshape",
1126 vec![Some(data), Some(shape)],
1127 vec![out],
1128 ));
1129 g.add_output(out);
1130
1131 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
1132 let t = inline_const(&g, out).expect("Reshape folded to initializer");
1133 assert_eq!(t.dims, vec![6]);
1134 assert_eq!(t.data, (0u8..6).collect::<Vec<_>>());
1135 assert!(g.validate().is_ok());
1136 }
1137
1138 #[test]
1139 fn folds_reshape_with_inferred_dim() {
1140 let mut g = Graph::new();
1141 g.opset_imports.insert(String::new(), 17);
1142 let data = raw_const_init(&mut g, "x", DataType::Uint8, vec![2, 3], (0u8..6).collect());
1143 let shape = const_init(&mut g, "shape", vec![2], &[3, -1]);
1144 let out = g.create_named_value("out", DataType::Uint8, static_shape([3, 2]));
1145 g.insert_node(ints_attr_node(
1146 "Reshape",
1147 vec![Some(data), Some(shape)],
1148 vec![out],
1149 ));
1150 g.add_output(out);
1151
1152 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
1153 let t = inline_const(&g, out).expect("Reshape folded to initializer");
1154 assert_eq!(t.dims, vec![3, 2]);
1155 assert_eq!(t.data, (0u8..6).collect::<Vec<_>>());
1156 }
1157
1158 #[test]
1159 fn folds_reshape_beyond_shape_fold_bound() {
1160 let numel = MAX_FOLD_ELEMS * 4;
1163 let mut g = Graph::new();
1164 g.opset_imports.insert(String::new(), 17);
1165 let data = raw_const_init(&mut g, "x", DataType::Uint8, vec![numel], vec![0u8; numel]);
1166 let shape = const_init(&mut g, "shape", vec![2], &[2, (numel / 2) as i64]);
1167 let out = g.create_named_value("out", DataType::Uint8, static_shape([2, numel / 2]));
1168 g.insert_node(ints_attr_node(
1169 "Reshape",
1170 vec![Some(data), Some(shape)],
1171 vec![out],
1172 ));
1173 g.add_output(out);
1174
1175 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
1176 let t = inline_const(&g, out).expect("large Reshape must still fold");
1177 assert_eq!(t.dims, vec![2, numel / 2]);
1178 }
1179
1180 #[test]
1181 fn folds_transpose_permutes_bytes() {
1182 let mut g = Graph::new();
1183 g.opset_imports.insert(String::new(), 17);
1184 let data = raw_const_init(
1187 &mut g,
1188 "x",
1189 DataType::Uint8,
1190 vec![2, 3],
1191 vec![0, 1, 2, 3, 4, 5],
1192 );
1193 let out = g.create_named_value("out", DataType::Uint8, static_shape([3, 2]));
1194 let mut node = ints_attr_node("Transpose", vec![Some(data)], vec![out]);
1195 node.attributes
1196 .insert("perm".into(), Attribute::Ints(vec![1, 0]));
1197 g.insert_node(node);
1198 g.add_output(out);
1199
1200 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
1201 let t = inline_const(&g, out).expect("Transpose folded to initializer");
1202 assert_eq!(t.dims, vec![3, 2]);
1203 assert_eq!(t.data, vec![0, 3, 1, 4, 2, 5]);
1204 }
1205
1206 #[test]
1207 fn folds_concat_along_axis_zero() {
1208 let mut g = Graph::new();
1209 g.opset_imports.insert(String::new(), 17);
1210 let a = const_init(&mut g, "a", vec![2], &[1, 2]);
1211 let b = const_init(&mut g, "b", vec![3], &[3, 4, 5]);
1212 let out = g.create_named_value("out", DataType::Int64, static_shape([5]));
1213 let mut node = ints_attr_node("Concat", vec![Some(a), Some(b)], vec![out]);
1214 node.attributes.insert("axis".into(), Attribute::Int(0));
1215 g.insert_node(node);
1216 g.add_output(out);
1217
1218 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
1219 let t = inline_const(&g, out).expect("Concat folded to initializer");
1220 assert_eq!(read_i64(t).unwrap(), vec![1, 2, 3, 4, 5]);
1221 }
1222
1223 #[test]
1224 fn folds_gate_up_interleave_chain_to_a_literal_initializer() {
1225 let num_experts = 2usize;
1231 let half = 2usize;
1232 let fc1_out = 2 * half;
1233 let trailing = 3usize;
1234 let numel = num_experts * fc1_out * trailing;
1235
1236 let mut g = Graph::new();
1237 g.opset_imports.insert(String::new(), 17);
1238 let tensor = raw_const_init(
1239 &mut g,
1240 "fc1_experts_weights",
1241 DataType::Uint8,
1242 vec![num_experts, fc1_out, trailing],
1243 (0u8..numel as u8).collect(),
1244 );
1245
1246 let trailing_shape = g.create_named_value("trailing", DataType::Int64, static_shape([1]));
1247 let mut shape_node = ints_attr_node("Shape", vec![Some(tensor)], vec![trailing_shape]);
1248 shape_node
1249 .attributes
1250 .insert("start".into(), Attribute::Int(2));
1251 g.insert_node(shape_node);
1252
1253 let split_const = g.create_named_value("split_const", DataType::Int64, static_shape([3]));
1254 let mut split_const_node = ints_attr_node("Constant", vec![], vec![split_const]);
1255 split_const_node.attributes.insert(
1256 "value_ints".into(),
1257 Attribute::Ints(vec![num_experts as i64, 2, half as i64]),
1258 );
1259 g.insert_node(split_const_node);
1260
1261 let split_shape = g.create_named_value("split_shape", DataType::Int64, static_shape([4]));
1262 let mut split_concat = ints_attr_node(
1263 "Concat",
1264 vec![Some(split_const), Some(trailing_shape)],
1265 vec![split_shape],
1266 );
1267 split_concat
1268 .attributes
1269 .insert("axis".into(), Attribute::Int(0));
1270 g.insert_node(split_concat);
1271
1272 let reshaped = g.create_named_value(
1273 "reshaped",
1274 DataType::Uint8,
1275 static_shape([num_experts, 2, half, trailing]),
1276 );
1277 g.insert_node(ints_attr_node(
1278 "Reshape",
1279 vec![Some(tensor), Some(split_shape)],
1280 vec![reshaped],
1281 ));
1282
1283 let transposed = g.create_named_value(
1284 "transposed",
1285 DataType::Uint8,
1286 static_shape([num_experts, half, 2, trailing]),
1287 );
1288 let mut transpose_node =
1289 ints_attr_node("Transpose", vec![Some(reshaped)], vec![transposed]);
1290 transpose_node
1291 .attributes
1292 .insert("perm".into(), Attribute::Ints(vec![0, 2, 1, 3]));
1293 g.insert_node(transpose_node);
1294
1295 let merge_const = g.create_named_value("merge_const", DataType::Int64, static_shape([2]));
1296 let mut merge_const_node = ints_attr_node("Constant", vec![], vec![merge_const]);
1297 merge_const_node.attributes.insert(
1298 "value_ints".into(),
1299 Attribute::Ints(vec![num_experts as i64, fc1_out as i64]),
1300 );
1301 g.insert_node(merge_const_node);
1302
1303 let merge_shape = g.create_named_value("merge_shape", DataType::Int64, static_shape([3]));
1304 let mut merge_concat = ints_attr_node(
1305 "Concat",
1306 vec![Some(merge_const), Some(trailing_shape)],
1307 vec![merge_shape],
1308 );
1309 merge_concat
1310 .attributes
1311 .insert("axis".into(), Attribute::Int(0));
1312 g.insert_node(merge_concat);
1313
1314 let result = g.create_named_value(
1315 "result",
1316 DataType::Uint8,
1317 static_shape([num_experts, fc1_out, trailing]),
1318 );
1319 g.insert_node(ints_attr_node(
1320 "Reshape",
1321 vec![Some(transposed), Some(merge_shape)],
1322 vec![result],
1323 ));
1324 g.add_output(result);
1325
1326 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
1327 DeadNodeElimination
1328 .run(&mut g, &PassContext::new())
1329 .unwrap();
1330
1331 assert_eq!(
1332 g.num_nodes(),
1333 0,
1334 "the entire interleave chain must fold to a literal initializer"
1335 );
1336 let t = inline_const(&g, result).expect("result must be a literal initializer");
1337 assert_eq!(t.dims, vec![num_experts, fc1_out, trailing]);
1338 assert_eq!(
1342 t.data,
1343 vec![
1344 0, 1, 2, 6, 7, 8, 3, 4, 5, 9, 10, 11, 12, 13, 14, 18, 19, 20, 15, 16, 17, 21, 22, 23,
1346 ]
1347 );
1348 assert!(g.validate().is_ok());
1349 }
1350}