Skip to main content

oxfuse_core/
verify.rs

1//! Independent re-check of every IR invariant. The builder cannot produce an
2//! invalid graph, so this exists to catch bugs in passes that rebuild graphs.
3
4use crate::shape::{check_shape, checked_num_elements, infer};
5use crate::{Error, Graph, Limits, Source, ValueId};
6use std::collections::HashSet;
7
8impl Graph {
9    pub fn verify(&self) -> Result<(), Error> {
10        if self.outputs.is_empty() {
11            return Err(Error::NoOutputs);
12        }
13        if self.output_names.len() != self.outputs.len() {
14            return Err(Error::Verify(
15                "output names do not match the outputs".to_owned(),
16            ));
17        }
18        let mut names: HashSet<&str> = HashSet::new();
19        let mut input_ids = Vec::new();
20        for (index, value) in self.values.iter().enumerate() {
21            let id = ValueId(index as u32);
22            let label = self.label(id);
23            if value.ty.shape.contains(&0) {
24                return Err(Error::InvalidShape {
25                    name: label,
26                    shape: value.ty.shape.clone(),
27                    reason: "dimensions must be at least 1",
28                });
29            }
30            if let Some(name) = &value.name
31                && !names.insert(name.as_str())
32            {
33                return Err(Error::DuplicateName { name: name.clone() });
34            }
35            match &value.source {
36                Source::Input => input_ids.push(id),
37                Source::Constant(data) => {
38                    let expected = checked_num_elements(&value.ty.shape).ok_or_else(|| {
39                        Error::Verify(format!("`{label}` element count overflows"))
40                    })?;
41                    if data.len() != expected {
42                        return Err(Error::DataLength {
43                            name: label,
44                            shape: value.ty.shape.clone(),
45                            expected,
46                            found: data.len(),
47                        });
48                    }
49                }
50                Source::Node { op, inputs } => {
51                    let mut types = Vec::with_capacity(inputs.len());
52                    for input in inputs {
53                        if input.index() >= index {
54                            return Err(Error::Verify(format!(
55                                "node `{label}` uses value %{} defined at or after it",
56                                input.0
57                            )));
58                        }
59                        types.push(&self.values[input.index()].ty);
60                    }
61                    let inferred = infer(&label, op, &types)?;
62                    if inferred != value.ty {
63                        return Err(Error::Verify(format!(
64                            "node `{label}` stored type {:?} differs from inferred {:?}",
65                            value.ty, inferred
66                        )));
67                    }
68                }
69            }
70        }
71        if input_ids != self.inputs {
72            return Err(Error::Verify(
73                "input list does not match the input values".to_owned(),
74            ));
75        }
76        for output in &self.outputs {
77            if output.index() >= self.values.len() {
78                return Err(Error::Verify(format!(
79                    "output %{} is out of range",
80                    output.0
81                )));
82            }
83        }
84        Ok(())
85    }
86
87    /// Re-checks the resource limits the builder enforced. The builder cannot
88    /// build a graph that breaks them, so this exists for the pass manager:
89    /// a pass must not grow a graph past the limits it started under.
90    pub fn verify_limits(&self, limits: &Limits) -> Result<(), Error> {
91        if self.values.len() > limits.max_values {
92            return Err(Error::LimitExceeded {
93                what: "values",
94                found: self.values.len(),
95                limit: limits.max_values,
96            });
97        }
98        let mut total = 0usize;
99        for (index, value) in self.values.iter().enumerate() {
100            let name = self.label(ValueId(index as u32));
101            total = total.saturating_add(check_shape(&name, &value.ty.shape, limits)?);
102            if total > limits.max_total_elements {
103                return Err(Error::TensorLimit {
104                    name,
105                    what: "total tensor elements",
106                    found: total,
107                    limit: limits.max_total_elements,
108                });
109            }
110        }
111        Ok(())
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use crate::{Error, Graph, GraphBuilder, Limits, Op, Source, TensorType, ValueId};
118    use std::sync::Arc;
119
120    /// x -> y = Relu(x), plus a constant c, output y.
121    fn valid() -> Graph {
122        let mut b = GraphBuilder::new(Limits::default());
123        let x = b.input("x", vec![2, 3]).unwrap();
124        b.constant("c", vec![2], vec![1.0, 2.0]).unwrap();
125        let y = b.node("y", Op::Relu, &[x]).unwrap();
126        b.finish(&[y]).unwrap()
127    }
128
129    #[test]
130    fn builder_output_verifies() {
131        assert_eq!(valid().verify(), Ok(()));
132    }
133
134    #[test]
135    fn rejects_forward_reference() {
136        let mut g = valid();
137        g.values[2].source = Source::Node {
138            op: Op::Relu,
139            inputs: vec![ValueId(2)],
140        };
141        assert!(matches!(g.verify(), Err(Error::Verify(m)) if m.contains("defined at or after")));
142    }
143
144    #[test]
145    fn rejects_stored_type_that_differs_from_inferred() {
146        let mut g = valid();
147        g.values[2].ty = TensorType::f32(vec![9]);
148        assert!(matches!(g.verify(), Err(Error::Verify(m)) if m.contains("differs from inferred")));
149    }
150
151    #[test]
152    fn rejects_empty_outputs() {
153        let mut g = valid();
154        g.outputs.clear();
155        assert_eq!(g.verify(), Err(Error::NoOutputs));
156    }
157
158    #[test]
159    fn rejects_output_out_of_range() {
160        let mut g = valid();
161        g.outputs = vec![ValueId(7)];
162        assert!(matches!(g.verify(), Err(Error::Verify(m)) if m.contains("out of range")));
163    }
164
165    #[test]
166    fn rejects_constant_with_wrong_data_length() {
167        let mut g = valid();
168        g.values[1].source = Source::Constant(Arc::from(vec![1.0f32]));
169        assert!(matches!(
170            g.verify(),
171            Err(Error::DataLength {
172                expected: 2,
173                found: 1,
174                ..
175            })
176        ));
177    }
178
179    #[test]
180    fn rejects_input_list_that_disagrees_with_values() {
181        let mut g = valid();
182        g.inputs.clear();
183        assert!(matches!(g.verify(), Err(Error::Verify(m)) if m.contains("input list")));
184    }
185
186    #[test]
187    fn rejects_duplicate_names() {
188        let mut g = valid();
189        g.values[1].name = Some("x".into());
190        assert_eq!(g.verify(), Err(Error::DuplicateName { name: "x".into() }));
191    }
192
193    #[test]
194    fn rejects_output_names_that_do_not_match_outputs() {
195        let mut g = valid();
196        g.output_names.pop();
197        assert!(matches!(g.verify(), Err(Error::Verify(m)) if m.contains("output names")));
198    }
199
200    #[test]
201    fn rejects_a_zero_dimension_in_a_stored_type() {
202        let mut g = valid();
203        g.values[0].ty = TensorType::f32(vec![2, 0]);
204        assert!(matches!(g.verify(), Err(Error::InvalidShape { .. })));
205    }
206
207    #[test]
208    fn verify_limits_accepts_a_builder_graph() {
209        assert_eq!(valid().verify_limits(&Limits::default()), Ok(()));
210    }
211
212    #[test]
213    fn verify_limits_rejects_a_graph_built_under_looser_limits() {
214        let loose = Limits {
215            max_tensor_elements: 1 << 20,
216            ..Limits::default()
217        };
218        let mut b = GraphBuilder::new(loose);
219        let x = b.input("x", vec![1000, 1000]).unwrap();
220        let g = b.finish(&[x]).unwrap();
221        let strict = Limits {
222            max_tensor_elements: 1000,
223            ..Limits::default()
224        };
225        assert!(matches!(
226            g.verify_limits(&strict),
227            Err(Error::TensorLimit {
228                what: "tensor elements",
229                ..
230            })
231        ));
232    }
233
234    #[test]
235    fn verify_limits_checks_the_value_count_and_the_total() {
236        let g = valid();
237        let few = Limits {
238            max_values: 2,
239            ..Limits::default()
240        };
241        assert!(matches!(
242            g.verify_limits(&few),
243            Err(Error::LimitExceeded { what: "values", .. })
244        ));
245        let small_total = Limits {
246            max_total_elements: 7,
247            ..Limits::default()
248        };
249        assert!(matches!(
250            g.verify_limits(&small_total),
251            Err(Error::TensorLimit {
252                what: "total tensor elements",
253                ..
254            })
255        ));
256    }
257
258    #[test]
259    fn verify_limits_accepts_a_graph_exactly_at_the_limits() {
260        // valid() has 3 values holding 6 + 2 + 6 = 14 elements.
261        let exactly = Limits {
262            max_values: 3,
263            max_total_elements: 14,
264            ..Limits::default()
265        };
266        assert_eq!(valid().verify_limits(&exactly), Ok(()));
267        let one_less = Limits {
268            max_total_elements: 13,
269            ..exactly
270        };
271        assert!(valid().verify_limits(&one_less).is_err());
272    }
273}