1use onnx_runtime_ir::{
23 Attribute, DataType, Graph, NodeId, TensorData, ValueId, WeightRef, as_static_shape,
24 static_shape,
25};
26
27use crate::error::Result;
28use crate::pass::{OptimizationPass, PassContext};
29
30const MAX_FOLD_ELEMS: usize = 1024;
33
34#[derive(Clone, Copy, Debug, Default)]
36pub struct ConstantFolding;
37
38impl OptimizationPass for ConstantFolding {
39 fn name(&self) -> &str {
40 "ConstantFolding"
41 }
42
43 fn run(&self, graph: &mut Graph, _ctx: &PassContext) -> Result<()> {
44 loop {
47 let mut changed = false;
48 let node_ids: Vec<NodeId> = graph.nodes.keys().collect();
49 for nid in node_ids {
50 if !graph.nodes.contains(nid) {
51 continue;
52 }
53 let node = graph.node(nid).clone();
54 if !matches!(node.domain.as_str(), "" | "ai.onnx") {
55 continue;
56 }
57 if node.outputs.len() != 1 {
58 continue;
59 }
60 let out = node.outputs[0];
61
62 let folded: Option<TensorData> = match node.op_type.as_str() {
63 "Constant" => eval_constant(&node),
64 "Shape" => fold_shape(graph, &node),
65 "Add" | "Sub" | "Mul" => fold_binary_int(graph, &node),
66 _ => None,
67 };
68
69 let Some(tensor) = folded else { continue };
70
71 let needed = graph.outputs.contains(&out)
75 || graph
76 .try_value(out)
77 .is_some_and(|v| !v.consumers.is_empty());
78 if !needed {
79 continue;
80 }
81
82 graph.remove_node(nid);
83 if graph.try_value(out).is_some() {
86 let dims = tensor.dims.clone();
87 let dtype = tensor.dtype;
88 let v = graph.value_mut(out);
89 v.dtype = dtype;
90 v.shape = static_shape(dims);
91 graph.set_initializer(out, WeightRef::Inline(tensor));
92 changed = true;
93 }
94 }
95 if !changed {
96 break;
97 }
98 }
99 Ok(())
100 }
101}
102
103fn inline_const(graph: &Graph, value: ValueId) -> Option<&TensorData> {
106 match graph.initializers.get(&value)? {
107 WeightRef::Inline(t) => Some(t),
108 WeightRef::External { .. } => None,
109 }
110}
111
112fn eval_constant(node: &onnx_runtime_ir::Node) -> Option<TensorData> {
114 if let Some(Attribute::Tensor(t)) = node.attr("value") {
115 return Some(t.clone());
116 }
117 if let Some(ints) = node.attr("value_ints").and_then(Attribute::as_ints) {
118 let mut data = Vec::with_capacity(ints.len() * 8);
119 for &i in ints {
120 data.extend_from_slice(&i.to_le_bytes());
121 }
122 return Some(TensorData::from_raw(DataType::Int64, vec![ints.len()], data));
123 }
124 if let Some(i) = node.attr("value_int").and_then(Attribute::as_int) {
125 return Some(TensorData::from_raw(
126 DataType::Int64,
127 Vec::new(),
128 i.to_le_bytes().to_vec(),
129 ));
130 }
131 None
132}
133
134fn fold_shape(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
139 if node.attr("start").is_some() || node.attr("end").is_some() {
140 return None;
141 }
142 let input = node.inputs.first().copied().flatten()?;
143 let shape = &graph.try_value(input)?.shape;
144 let dims = as_static_shape(shape)?;
145 if dims.len() > MAX_FOLD_ELEMS {
146 return None;
147 }
148 let mut data = Vec::with_capacity(dims.len() * 8);
149 for &d in &dims {
150 data.extend_from_slice(&(d as i64).to_le_bytes());
151 }
152 Some(TensorData::from_raw(DataType::Int64, vec![dims.len()], data))
153}
154
155fn fold_binary_int(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
158 if node.inputs.len() != 2 {
159 return None;
160 }
161 let a = inline_const(graph, node.inputs[0]?)?;
162 let b = inline_const(graph, node.inputs[1]?)?;
163 if a.dtype != b.dtype || a.dims != b.dims {
164 return None; }
166 if !matches!(a.dtype, DataType::Int32 | DataType::Int64) {
167 return None;
168 }
169 let numel = a.numel();
170 if numel > MAX_FOLD_ELEMS {
171 return None;
172 }
173 let op = node.op_type.as_str();
174 let apply = |x: i64, y: i64| -> Option<i64> {
175 match op {
176 "Add" => x.checked_add(y),
177 "Sub" => x.checked_sub(y),
178 "Mul" => x.checked_mul(y),
179 _ => None,
180 }
181 };
182
183 match a.dtype {
184 DataType::Int64 => {
185 let (xs, ys) = (read_i64(a)?, read_i64(b)?);
186 let mut data = Vec::with_capacity(numel * 8);
187 for (x, y) in xs.into_iter().zip(ys) {
188 data.extend_from_slice(&apply(x, y)?.to_le_bytes());
189 }
190 Some(TensorData::from_raw(DataType::Int64, a.dims.clone(), data))
191 }
192 DataType::Int32 => {
193 let (xs, ys) = (read_i32(a)?, read_i32(b)?);
194 let mut data = Vec::with_capacity(numel * 4);
195 for (x, y) in xs.into_iter().zip(ys) {
196 let r = apply(x as i64, y as i64)?;
197 let r32: i32 = r.try_into().ok()?; data.extend_from_slice(&r32.to_le_bytes());
199 }
200 Some(TensorData::from_raw(DataType::Int32, a.dims.clone(), data))
201 }
202 _ => None,
203 }
204}
205
206fn read_i64(t: &TensorData) -> Option<Vec<i64>> {
207 if t.data.len() != t.numel() * 8 {
208 return None;
209 }
210 Some(
211 t.data
212 .chunks_exact(8)
213 .map(|c| i64::from_le_bytes(c.try_into().unwrap()))
214 .collect(),
215 )
216}
217
218fn read_i32(t: &TensorData) -> Option<Vec<i32>> {
219 if t.data.len() != t.numel() * 4 {
220 return None;
221 }
222 Some(
223 t.data
224 .chunks_exact(4)
225 .map(|c| i32::from_le_bytes(c.try_into().unwrap()))
226 .collect(),
227 )
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use onnx_runtime_ir::{Node, NodeId};
234
235 fn int64_tensor(dims: Vec<usize>, vals: &[i64]) -> TensorData {
236 let mut data = Vec::new();
237 for &v in vals {
238 data.extend_from_slice(&v.to_le_bytes());
239 }
240 TensorData::from_raw(DataType::Int64, dims, data)
241 }
242
243 fn const_init(graph: &mut Graph, name: &str, dims: Vec<usize>, vals: &[i64]) -> ValueId {
244 let shape = static_shape(dims.clone());
245 let v = graph.create_named_value(name, DataType::Int64, shape);
246 graph.set_initializer(v, WeightRef::Inline(int64_tensor(dims, vals)));
247 v
248 }
249
250 #[test]
251 fn folds_add_of_two_const_inputs() {
252 let mut g = Graph::new();
253 g.opset_imports.insert(String::new(), 17);
254 let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
255 let b = const_init(&mut g, "b", vec![3], &[10, 20, 30]);
256 let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
257 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(a), Some(b)], vec![out]));
258 g.add_output(out);
259
260 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
261
262 assert_eq!(g.num_nodes(), 0, "Add should be folded away");
263 let t = inline_const(&g, out).expect("out is now an initializer");
264 assert_eq!(read_i64(t).unwrap(), vec![11, 22, 33]);
265 assert!(g.validate().is_ok());
266 }
267
268 #[test]
269 fn folds_sub_and_mul() {
270 for (op, expect) in [("Sub", vec![9, 18, 27]), ("Mul", vec![10, 40, 90])] {
271 let mut g = Graph::new();
272 g.opset_imports.insert(String::new(), 17);
273 let a = const_init(&mut g, "a", vec![3], &[10, 20, 30]);
274 let b = const_init(&mut g, "b", vec![3], &[1, 2, 3]);
275 let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
276 g.insert_node(Node::new(NodeId(0), op, vec![Some(a), Some(b)], vec![out]));
277 g.add_output(out);
278
279 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
280 let t = inline_const(&g, out).unwrap();
281 assert_eq!(read_i64(t).unwrap(), expect, "op {op}");
282 }
283 }
284
285 #[test]
286 fn does_not_fold_when_one_input_is_non_const() {
287 let mut g = Graph::new();
288 g.opset_imports.insert(String::new(), 17);
289 let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
290 let b = g.create_named_value("b", DataType::Int64, static_shape([3]));
292 g.add_input(b);
293 let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
294 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(a), Some(b)], vec![out]));
295 g.add_output(out);
296
297 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
298 assert_eq!(g.num_nodes(), 1, "must not fold with a non-const input");
299 assert!(inline_const(&g, out).is_none());
300 assert!(g.validate().is_ok());
301 }
302
303 #[test]
304 fn does_not_fold_mismatched_shapes() {
305 let mut g = Graph::new();
306 g.opset_imports.insert(String::new(), 17);
307 let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
308 let b = const_init(&mut g, "b", vec![2], &[10, 20]);
309 let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
310 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(a), Some(b)], vec![out]));
311 g.add_output(out);
312
313 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
314 assert_eq!(g.num_nodes(), 1, "no broadcasting in v1");
315 }
316
317 #[test]
318 fn does_not_fold_overflow() {
319 let mut g = Graph::new();
320 g.opset_imports.insert(String::new(), 17);
321 let a = const_init(&mut g, "a", vec![1], &[i64::MAX]);
322 let b = const_init(&mut g, "b", vec![1], &[1]);
323 let out = g.create_named_value("out", DataType::Int64, static_shape([1]));
324 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(a), Some(b)], vec![out]));
325 g.add_output(out);
326
327 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
328 assert_eq!(g.num_nodes(), 1, "overflow must abort the fold");
329 }
330
331 #[test]
332 fn folds_constant_node_to_initializer() {
333 let mut g = Graph::new();
334 g.opset_imports.insert(String::new(), 17);
335 let out = g.create_named_value("c", DataType::Int64, static_shape([2]));
336 let mut node = Node::new(NodeId(0), "Constant", vec![], vec![out]);
337 node.attributes
338 .insert("value".into(), Attribute::Tensor(int64_tensor(vec![2], &[7, 8])));
339 g.insert_node(node);
340 let sink = g.create_named_value("sink", DataType::Int64, static_shape([2]));
342 g.insert_node(Node::new(NodeId(0), "Identity", vec![Some(out)], vec![sink]));
343 g.add_output(sink);
344
345 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
346 assert!(g.try_node(NodeId(0)).is_none(), "Constant folded away");
347 let t = inline_const(&g, out).unwrap();
348 assert_eq!(read_i64(t).unwrap(), vec![7, 8]);
349 assert!(g.validate().is_ok());
350 }
351
352 #[test]
353 fn folds_shape_of_static_input() {
354 let mut g = Graph::new();
355 g.opset_imports.insert(String::new(), 17);
356 let x = g.create_named_value("x", DataType::Float32, static_shape([2, 3, 4]));
357 g.add_input(x);
358 let out = g.create_named_value("s", DataType::Int64, static_shape([3]));
359 g.insert_node(Node::new(NodeId(0), "Shape", vec![Some(x)], vec![out]));
360 g.add_output(out);
361
362 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
363 let t = inline_const(&g, out).expect("Shape folded to initializer");
364 assert_eq!(read_i64(t).unwrap(), vec![2, 3, 4]);
365 assert!(g.validate().is_ok());
366 }
367
368 #[test]
369 fn folds_transitively_to_fixpoint() {
370 let mut g = Graph::new();
372 g.opset_imports.insert(String::new(), 17);
373
374 let c1 = g.create_named_value("c1", DataType::Int64, static_shape([2]));
375 let mut n1 = Node::new(NodeId(0), "Constant", vec![], vec![c1]);
376 n1.attributes
377 .insert("value".into(), Attribute::Tensor(int64_tensor(vec![2], &[1, 2])));
378 g.insert_node(n1);
379
380 let c2 = g.create_named_value("c2", DataType::Int64, static_shape([2]));
381 let mut n2 = Node::new(NodeId(0), "Constant", vec![], vec![c2]);
382 n2.attributes
383 .insert("value".into(), Attribute::Tensor(int64_tensor(vec![2], &[3, 4])));
384 g.insert_node(n2);
385
386 let out = g.create_named_value("out", DataType::Int64, static_shape([2]));
387 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(c1), Some(c2)], vec![out]));
388 g.add_output(out);
389
390 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
391 assert_eq!(g.num_nodes(), 0, "both constants and the Add fold away");
392 let t = inline_const(&g, out).unwrap();
393 assert_eq!(read_i64(t).unwrap(), vec![4, 6]);
394 assert!(g.validate().is_ok());
395 }
396
397 #[test]
398 fn does_not_fold_float_binary() {
399 let mut g = Graph::new();
400 g.opset_imports.insert(String::new(), 17);
401 let mk = |g: &mut Graph, name: &str| {
402 let v = g.create_named_value(name, DataType::Float32, static_shape([2]));
403 g.set_initializer(
404 v,
405 WeightRef::Inline(TensorData::from_raw(
406 DataType::Float32,
407 vec![2],
408 vec![0u8; 8],
409 )),
410 );
411 v
412 };
413 let a = mk(&mut g, "a");
414 let b = mk(&mut g, "b");
415 let out = g.create_named_value("out", DataType::Float32, static_shape([2]));
416 g.insert_node(Node::new(NodeId(0), "Mul", vec![Some(a), Some(b)], vec![out]));
417 g.add_output(out);
418
419 ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
420 assert_eq!(g.num_nodes(), 1, "float folding is out of scope in v1");
421 }
422}