Skip to main content

oxfuse_core/
builder.rs

1//! Builds a graph, inferring each node's type as it is added, so a graph
2//! with an ill-typed node cannot be constructed.
3
4use crate::shape::{check_shape, checked_num_elements, infer};
5use crate::{Error, Graph, Limits, Op, Source, TensorType, Value, ValueId};
6use std::collections::HashMap;
7use std::sync::Arc;
8
9pub struct GraphBuilder {
10    limits: Limits,
11    values: Vec<Value>,
12    inputs: Vec<ValueId>,
13    names: HashMap<String, ValueId>,
14    total_elements: usize,
15}
16
17impl GraphBuilder {
18    pub fn new(limits: Limits) -> Self {
19        Self {
20            limits,
21            values: Vec::new(),
22            inputs: Vec::new(),
23            names: HashMap::new(),
24            total_elements: 0,
25        }
26    }
27
28    pub fn lookup(&self, name: &str) -> Option<ValueId> {
29        self.names.get(name).copied()
30    }
31
32    pub fn input(&mut self, name: &str, shape: Vec<usize>) -> Result<ValueId, Error> {
33        check_shape(name, &shape, &self.limits)?;
34        let id = self.push(name, TensorType::f32(shape), Source::Input)?;
35        self.inputs.push(id);
36        Ok(id)
37    }
38
39    pub fn constant(
40        &mut self,
41        name: &str,
42        shape: Vec<usize>,
43        data: Vec<f32>,
44    ) -> Result<ValueId, Error> {
45        self.constant_shared(name, shape, data.into())
46    }
47
48    /// Like `constant`, but shares the buffer, so rebuilding a graph never
49    /// copies weights.
50    pub fn constant_shared(
51        &mut self,
52        name: &str,
53        shape: Vec<usize>,
54        data: Arc<[f32]>,
55    ) -> Result<ValueId, Error> {
56        let expected = check_shape(name, &shape, &self.limits)?;
57        if data.len() != expected {
58            return Err(Error::DataLength {
59                name: name.to_owned(),
60                shape,
61                expected,
62                found: data.len(),
63            });
64        }
65        self.push(name, TensorType::f32(shape), Source::Constant(data))
66    }
67
68    pub fn node(&mut self, name: &str, op: Op, inputs: &[ValueId]) -> Result<ValueId, Error> {
69        let mut types = Vec::with_capacity(inputs.len());
70        for id in inputs {
71            let value = self.values.get(id.index()).ok_or_else(|| {
72                Error::Verify(format!("node `{name}` uses undefined value %{}", id.0))
73            })?;
74            types.push(&value.ty);
75        }
76        let ty = infer(name, &op, &types)?;
77        check_shape(name, &ty.shape, &self.limits)?;
78        self.push(
79            name,
80            ty,
81            Source::Node {
82                op,
83                inputs: inputs.to_vec(),
84            },
85        )
86    }
87
88    pub fn finish(self, outputs: &[ValueId]) -> Result<Graph, Error> {
89        let names = outputs
90            .iter()
91            .map(|id| match self.values.get(id.index()) {
92                Some(Value {
93                    name: Some(name), ..
94                }) => (name.clone(), *id),
95                _ => (format!("%{}", id.0), *id),
96            })
97            .collect();
98        self.finish_named(names)
99    }
100
101    pub fn finish_named(self, outputs: Vec<(String, ValueId)>) -> Result<Graph, Error> {
102        let (output_names, outputs): (Vec<String>, Vec<ValueId>) = outputs.into_iter().unzip();
103        let graph = Graph {
104            values: self.values,
105            inputs: self.inputs,
106            outputs,
107            output_names,
108        };
109        graph.verify()?;
110        Ok(graph)
111    }
112
113    fn push(&mut self, name: &str, ty: TensorType, source: Source) -> Result<ValueId, Error> {
114        // Ids are u32, so the cap can never exceed u32::MAX whatever the limits say.
115        let cap = self.limits.max_values.min(u32::MAX as usize);
116        if self.values.len() >= cap {
117            return Err(Error::LimitExceeded {
118                what: "values",
119                found: self.values.len() + 1,
120                limit: cap,
121            });
122        }
123        let total = self
124            .total_elements
125            .saturating_add(checked_num_elements(&ty.shape).unwrap_or(usize::MAX));
126        if total > self.limits.max_total_elements {
127            return Err(Error::TensorLimit {
128                name: name.to_owned(),
129                what: "total tensor elements",
130                found: total,
131                limit: self.limits.max_total_elements,
132            });
133        }
134        if self.names.contains_key(name) {
135            return Err(Error::DuplicateName {
136                name: name.to_owned(),
137            });
138        }
139        let id =
140            ValueId(u32::try_from(self.values.len()).expect("the value cap keeps ids within u32"));
141        self.total_elements = total;
142        self.names.insert(name.to_owned(), id);
143        self.values.push(Value {
144            ty,
145            source,
146            name: Some(name.to_owned()),
147        });
148        Ok(id)
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    fn builder() -> GraphBuilder {
157        GraphBuilder::new(Limits::default())
158    }
159
160    #[test]
161    fn builds_input_relu_graph() {
162        let mut b = builder();
163        let x = b.input("x", vec![2, 3]).unwrap();
164        let y = b.node("y", Op::Relu, &[x]).unwrap();
165        let g = b.finish(&[y]).unwrap();
166        assert_eq!(g.values().len(), 2);
167        assert_eq!(g.inputs(), &[x]);
168        assert_eq!(g.outputs(), &[y]);
169        assert_eq!(g.value(y).ty, TensorType::f32(vec![2, 3]));
170        assert_eq!(g.label(y), "y");
171    }
172
173    #[test]
174    fn lookup_finds_defined_names_only() {
175        let mut b = builder();
176        let x = b.input("x", vec![1]).unwrap();
177        assert_eq!(b.lookup("x"), Some(x));
178        assert_eq!(b.lookup("nope"), None);
179    }
180
181    #[test]
182    fn scalar_input_is_allowed() {
183        assert!(builder().input("s", vec![]).is_ok());
184    }
185
186    #[test]
187    fn constant_length_mismatch_is_rejected() {
188        let err = builder()
189            .constant("c", vec![2, 3], vec![0.0; 5])
190            .unwrap_err();
191        assert_eq!(
192            err,
193            Error::DataLength {
194                name: "c".into(),
195                shape: vec![2, 3],
196                expected: 6,
197                found: 5
198            }
199        );
200    }
201
202    #[test]
203    fn duplicate_name_across_kinds_is_rejected() {
204        let mut b = builder();
205        b.input("x", vec![1]).unwrap();
206        let err = b.constant("x", vec![1], vec![0.0]).unwrap_err();
207        assert_eq!(err, Error::DuplicateName { name: "x".into() });
208    }
209
210    #[test]
211    fn wrong_arity_is_rejected() {
212        let mut b = builder();
213        let x = b.input("x", vec![1]).unwrap();
214        let err = b.node("y", Op::Relu, &[x, x]).unwrap_err();
215        assert!(matches!(
216            err,
217            Error::Arity {
218                expected: 1,
219                found: 2,
220                ..
221            }
222        ));
223    }
224
225    #[test]
226    fn undefined_input_value_is_rejected_not_a_panic() {
227        let err = builder().node("y", Op::Relu, &[ValueId(9)]).unwrap_err();
228        assert!(matches!(err, Error::Verify(_)));
229    }
230
231    #[test]
232    fn graph_without_outputs_is_rejected() {
233        let mut b = builder();
234        b.input("x", vec![1]).unwrap();
235        assert_eq!(b.finish(&[]).unwrap_err(), Error::NoOutputs);
236    }
237
238    #[test]
239    fn value_count_limit_is_enforced() {
240        let limits = Limits {
241            max_values: 2,
242            ..Limits::default()
243        };
244        let mut b = GraphBuilder::new(limits);
245        b.input("a", vec![1]).unwrap();
246        b.input("b", vec![1]).unwrap();
247        let err = b.input("c", vec![1]).unwrap_err();
248        assert!(matches!(err, Error::LimitExceeded { what: "values", .. }));
249    }
250
251    #[test]
252    fn huge_shape_is_rejected_before_allocation() {
253        let mut b = builder();
254        let overflow = b.input("a", vec![usize::MAX, 2]).unwrap_err();
255        assert!(matches!(overflow, Error::InvalidShape { .. }));
256        let too_big = b.input("b", vec![1 << 20, 1 << 20]).unwrap_err();
257        assert!(matches!(
258            too_big,
259            Error::TensorLimit {
260                what: "tensor elements",
261                ..
262            }
263        ));
264        let too_deep = b.input("c", vec![1; 9]).unwrap_err();
265        assert!(matches!(
266            too_deep,
267            Error::TensorLimit {
268                what: "tensor rank",
269                ..
270            }
271        ));
272    }
273
274    #[test]
275    fn zero_dimension_is_rejected() {
276        let err = builder().input("x", vec![3, 0]).unwrap_err();
277        assert!(matches!(err, Error::InvalidShape { .. }));
278    }
279
280    #[test]
281    fn scalar_operand_on_either_side_keeps_the_tensor_shape() {
282        let mut b = builder();
283        let t = b.input("t", vec![2, 3]).unwrap();
284        let s = b.input("s", vec![]).unwrap();
285        let left = b.node("l", Op::Add, &[s, t]).unwrap();
286        let right = b.node("r", Op::Mul, &[t, s]).unwrap();
287        let g = b.finish(&[left, right]).unwrap();
288        assert_eq!(g.value(left).ty, TensorType::f32(vec![2, 3]));
289        assert_eq!(g.value(right).ty, TensorType::f32(vec![2, 3]));
290    }
291
292    #[test]
293    fn mismatched_elementwise_shapes_are_rejected() {
294        let mut b = builder();
295        let x = b.input("x", vec![2, 3]).unwrap();
296        let z = b.input("z", vec![3, 2]).unwrap();
297        let err = b.node("y", Op::Add, &[x, z]).unwrap_err();
298        assert!(matches!(err, Error::ShapeMismatch { .. }));
299    }
300
301    #[test]
302    fn matmul_output_over_the_element_limit_is_rejected() {
303        let mut b = builder();
304        let a = b.input("a", vec![1 << 15, 1]).unwrap();
305        let c = b.input("c", vec![1, 1 << 15]).unwrap();
306        let err = b.node("y", Op::MatMul, &[a, c]).unwrap_err();
307        assert!(matches!(
308            err,
309            Error::TensorLimit {
310                what: "tensor elements",
311                ..
312            }
313        ));
314    }
315
316    #[test]
317    fn reshape_beyond_the_rank_limit_is_rejected() {
318        let mut b = builder();
319        let x = b.input("x", vec![512]).unwrap();
320        let op = Op::Reshape { shape: vec![2; 9] };
321        let err = b.node("y", op, &[x]).unwrap_err();
322        assert!(matches!(
323            err,
324            Error::TensorLimit {
325                what: "tensor rank",
326                ..
327            }
328        ));
329    }
330
331    #[test]
332    fn total_allocation_across_values_is_bounded() {
333        let mut b = builder();
334        let a = b.input("a", vec![1 << 14, 1]).unwrap();
335        let c = b.input("c", vec![1, 1 << 14]).unwrap();
336        b.node("y1", Op::MatMul, &[a, c]).unwrap();
337        let err = b.node("y2", Op::MatMul, &[a, c]).unwrap_err();
338        assert_eq!(
339            err,
340            Error::TensorLimit {
341                name: "y2".into(),
342                what: "total tensor elements",
343                found: 2 * (1 << 14) + 2 * (1 << 28),
344                limit: 1 << 29,
345            }
346        );
347    }
348
349    #[test]
350    fn many_small_values_still_fit_the_total_budget() {
351        let mut b = builder();
352        let mut last = b.input("x0", vec![64]).unwrap();
353        for i in 1..200 {
354            last = b.node(&format!("n{i}"), Op::Relu, &[last]).unwrap();
355        }
356        assert!(b.finish(&[last]).is_ok());
357    }
358
359    #[test]
360    fn finish_names_outputs_after_their_values() {
361        let mut b = builder();
362        let x = b.input("x", vec![1]).unwrap();
363        let y = b.node("y", Op::Relu, &[x]).unwrap();
364        let g = b.finish(&[y, x]).unwrap();
365        assert_eq!(g.output_names(), ["y", "x"]);
366    }
367
368    #[test]
369    fn finish_named_keeps_the_given_names_even_for_duplicates() {
370        let mut b = builder();
371        let x = b.input("x", vec![1]).unwrap();
372        let y = b.node("y", Op::Relu, &[x]).unwrap();
373        let g = b
374            .finish_named(vec![("first".to_owned(), y), ("second".to_owned(), y)])
375            .unwrap();
376        assert_eq!(g.output_names(), ["first", "second"]);
377        assert_eq!(g.outputs(), &[y, y]);
378    }
379
380    #[test]
381    fn constant_shared_does_not_copy_the_data() {
382        let mut b = builder();
383        let data: std::sync::Arc<[f32]> = std::sync::Arc::from(vec![1.0, 2.0]);
384        let c = b
385            .constant_shared("c", vec![2], std::sync::Arc::clone(&data))
386            .unwrap();
387        let g = b.finish(&[c]).unwrap();
388        match &g.value(c).source {
389            Source::Constant(stored) => assert!(std::sync::Arc::ptr_eq(stored, &data)),
390            other => panic!("expected a constant, got {other:?}"),
391        }
392    }
393
394    #[test]
395    fn value_limit_error_reports_the_exact_numbers() {
396        let limits = Limits {
397            max_values: 2,
398            ..Limits::default()
399        };
400        let mut b = GraphBuilder::new(limits);
401        b.input("a", vec![1]).unwrap();
402        b.input("b", vec![1]).unwrap();
403        assert_eq!(
404            b.input("c", vec![1]).unwrap_err(),
405            Error::LimitExceeded {
406                what: "values",
407                found: 3,
408                limit: 2
409            }
410        );
411    }
412
413    #[test]
414    fn total_element_budget_accepts_exactly_the_limit_and_rejects_one_more() {
415        let limits = Limits {
416            max_total_elements: 10,
417            ..Limits::default()
418        };
419        let mut b = GraphBuilder::new(limits);
420        b.input("a", vec![4]).unwrap();
421        b.input("b", vec![6]).unwrap();
422        assert_eq!(
423            b.input("c", vec![1]).unwrap_err(),
424            Error::TensorLimit {
425                name: "c".into(),
426                what: "total tensor elements",
427                found: 11,
428                limit: 10
429            }
430        );
431    }
432}