use super::rebuild::Rebuilder;
use super::{Pass, PassOutput};
use crate::shape::checked_num_elements;
use crate::{Error, Graph, Limits, Op, Source, TensorType, ValueId};
use std::sync::Arc;
pub trait ConstEvaluator {
fn eval(&self, op: &Op, inputs: &[(&TensorType, &[f32])]) -> Result<Vec<f32>, Error>;
}
pub struct ConstantFolding {
evaluator: Arc<dyn ConstEvaluator>,
limits: Limits,
max_elements: usize,
}
impl ConstantFolding {
pub const DEFAULT_MAX_ELEMENTS: usize = 1 << 20;
pub fn new(evaluator: Arc<dyn ConstEvaluator>, limits: Limits) -> Self {
Self {
evaluator,
limits,
max_elements: Self::DEFAULT_MAX_ELEMENTS,
}
}
pub fn with_max_elements(mut self, max_elements: usize) -> Self {
self.max_elements = max_elements;
self
}
fn within_cap(&self, shape: &[usize]) -> bool {
checked_num_elements(shape).is_some_and(|n| n <= self.max_elements)
}
}
impl Pass for ConstantFolding {
fn name(&self) -> &'static str {
"fold"
}
fn run(&self, graph: &Graph) -> Result<PassOutput, Error> {
let mut rebuild = Rebuilder::new(graph, &self.limits);
let mut constants: Vec<Option<Arc<[f32]>>> = vec![None; graph.values().len()];
let mut folded = 0;
for (index, value) in graph.values().iter().enumerate() {
let old = ValueId(index as u32);
match &value.source {
Source::Constant(data) => {
constants[index] = Some(Arc::clone(data));
rebuild.copy(old)?;
}
Source::Node { op, inputs }
if inputs.iter().all(|id| constants[id.index()].is_some())
&& self.within_cap(value.ty.shape.as_slice())
&& inputs
.iter()
.all(|id| self.within_cap(&graph.value(*id).ty.shape)) =>
{
let args: Vec<(&TensorType, &[f32])> = inputs
.iter()
.map(|id| {
let data = constants[id.index()].as_deref();
(
&graph.value(*id).ty,
data.expect("all inputs were checked to be constants"),
)
})
.collect();
let data: Arc<[f32]> = self.evaluator.eval(op, &args)?.into();
rebuild.replace_with_constant(old, Arc::clone(&data))?;
constants[index] = Some(data);
folded += 1;
}
_ => {
rebuild.copy(old)?;
}
}
}
Ok(PassOutput {
graph: rebuild.finish()?,
changed: folded,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{GraphBuilder, Op, Source, ValueId};
struct Eval;
impl ConstEvaluator for Eval {
fn eval(&self, op: &Op, inputs: &[(&TensorType, &[f32])]) -> Result<Vec<f32>, Error> {
match op {
Op::Relu => Ok(inputs[0]
.1
.iter()
.map(|&x| if x > 0.0 { x } else { 0.0 })
.collect()),
Op::Add => Ok(inputs[0]
.1
.iter()
.zip(inputs[1].1)
.map(|(a, b)| a + b)
.collect()),
_ => Err(Error::Verify(
"test evaluator does not support this op".into(),
)),
}
}
}
fn fold() -> ConstantFolding {
ConstantFolding::new(std::sync::Arc::new(Eval), Limits::default())
}
fn constant_data(graph: &Graph, name: &str) -> Vec<f32> {
let id = (0..graph.values().len())
.map(|i| ValueId(i as u32))
.find(|&id| graph.label(id) == name)
.unwrap();
match &graph.value(id).source {
Source::Constant(data) => data.to_vec(),
other => panic!("`{name}` is not a constant: {other:?}"),
}
}
fn builder() -> GraphBuilder {
GraphBuilder::new(Limits::default())
}
#[test]
fn folds_a_constant_chain_and_leaves_input_dependent_nodes() {
let mut b = builder();
let x = b.input("x", vec![2]).unwrap();
let a = b.constant("a", vec![2], vec![1.0, -2.0]).unwrap();
let c = b.constant("b", vec![2], vec![3.0, 4.0]).unwrap();
let s = b.node("s", Op::Add, &[a, c]).unwrap();
let r = b.node("r", Op::Relu, &[s]).unwrap();
let y = b.node("y", Op::Add, &[x, r]).unwrap();
let g = b.finish(&[y]).unwrap();
let out = fold().run(&g).unwrap();
assert_eq!(out.changed, 2);
assert_eq!(constant_data(&out.graph, "s"), [4.0, 2.0]);
assert_eq!(constant_data(&out.graph, "r"), [4.0, 2.0]);
assert!(matches!(
out.graph.value(out.graph.outputs()[0]).source,
Source::Node { op: Op::Add, .. }
));
assert_eq!(out.graph.values().len(), g.values().len());
}
#[test]
fn folded_output_keeps_its_name() {
let mut b = builder();
let a = b.constant("a", vec![1], vec![-5.0]).unwrap();
let r = b.node("r", Op::Relu, &[a]).unwrap();
let g = b.finish_named(vec![("result".to_owned(), r)]).unwrap();
let out = fold().run(&g).unwrap();
assert_eq!(out.graph.output_names(), ["result"]);
assert_eq!(constant_data(&out.graph, "r"), [0.0]);
}
#[test]
fn nothing_to_fold_rebuilds_the_same_graph() {
let mut b = builder();
let x = b.input("x", vec![2]).unwrap();
let y = b.node("y", Op::Relu, &[x]).unwrap();
let g = b.finish(&[y]).unwrap();
let out = fold().run(&g).unwrap();
assert_eq!(out.changed, 0);
assert_eq!(out.graph, g);
}
#[test]
fn non_finite_results_fold_like_runtime() {
let mut b = builder();
let pos = b.constant("pos", vec![1], vec![f32::INFINITY]).unwrap();
let neg = b.constant("neg", vec![1], vec![f32::NEG_INFINITY]).unwrap();
let sum = b.node("sum", Op::Add, &[pos, neg]).unwrap();
let g = b.finish(&[sum]).unwrap();
let out = fold().run(&g).unwrap();
assert_eq!(out.changed, 1);
assert!(constant_data(&out.graph, "sum")[0].is_nan());
}
#[test]
fn large_results_are_left_for_run_time() {
let mut b = builder();
let a = b
.constant("a", vec![4], vec![1.0, -1.0, 2.0, -2.0])
.unwrap();
let r = b.node("r", Op::Relu, &[a]).unwrap();
let g = b.finish(&[r]).unwrap();
let small = fold().with_max_elements(3);
let out = small.run(&g).unwrap();
assert_eq!(out.changed, 0);
assert_eq!(out.graph, g);
let exact = fold().with_max_elements(4);
assert_eq!(exact.run(&g).unwrap().changed, 1);
}
#[test]
fn evaluator_errors_propagate() {
let mut b = builder();
let a = b.constant("a", vec![1], vec![1.0]).unwrap();
let s = b.node("s", Op::Sigmoid, &[a]).unwrap();
let g = b.finish(&[s]).unwrap();
let err = fold().run(&g).err().unwrap();
assert!(matches!(err, Error::Verify(_)));
}
#[test]
fn wrong_length_results_are_rejected() {
struct Short;
impl ConstEvaluator for Short {
fn eval(&self, _: &Op, _: &[(&TensorType, &[f32])]) -> Result<Vec<f32>, Error> {
Ok(vec![0.0])
}
}
let mut b = builder();
let a = b.constant("a", vec![2], vec![1.0, 2.0]).unwrap();
let r = b.node("r", Op::Relu, &[a]).unwrap();
let g = b.finish(&[r]).unwrap();
let folding = ConstantFolding::new(std::sync::Arc::new(Short), Limits::default());
let err = folding.run(&g).err().unwrap();
assert!(matches!(
err,
Error::DataLength {
expected: 2,
found: 1,
..
}
));
}
#[test]
fn large_inputs_are_not_folded_even_when_the_output_is_small() {
let mut b = builder();
let a = b.constant("a", vec![1, 4], vec![1.0; 4]).unwrap();
let c = b.constant("c", vec![4, 1], vec![1.0; 4]).unwrap();
let p = b.node("p", Op::MatMul, &[a, c]).unwrap();
let g = b.finish(&[p]).unwrap();
let capped = fold().with_max_elements(3);
let out = capped.run(&g).unwrap();
assert_eq!(out.changed, 0);
assert_eq!(out.graph, g);
}
}