Skip to main content

oxfuse_core/passes/
layout.rs

1//! Layout elision: a pair of `Transpose` nodes becomes one `Transpose` (or
2//! nothing, when the permutations cancel), a pair of `Reshape` nodes becomes
3//! one `Reshape`, and a `Transpose` with the identity permutation or a
4//! `Reshape` to the shape it already has is aliased to its input. Both ops
5//! only move bits, so the result is bit-identical and the intermediate value,
6//! if nothing else reads it, is left for dead-code elimination.
7
8use super::rebuild::Rebuilder;
9use super::{Pass, PassOutput};
10use crate::{Error, Graph, Limits, Op, Source, ValueId};
11use std::collections::HashMap;
12
13pub struct LayoutElision {
14    limits: Limits,
15}
16
17impl LayoutElision {
18    pub fn new(limits: Limits) -> Self {
19        Self { limits }
20    }
21}
22
23/// A `Transpose` or `Reshape` node of the new graph: the op, the value it
24/// reads, and that value's shape.
25struct Link {
26    op: Op,
27    source: ValueId,
28    source_shape: Vec<usize>,
29}
30
31/// The single permutation equal to transposing by `inner` and then by `outer`:
32/// output dimension `i` of the pair is input dimension `inner[outer[i]]`.
33fn compose(inner: &[usize], outer: &[usize]) -> Vec<usize> {
34    let mut composed = vec![0; outer.len()];
35    compose_into(inner, outer, &mut composed);
36    composed
37}
38
39/// `compose` into `out`, which must be as long as `outer`. Split out so the
40/// model checker can run it without an allocation.
41fn compose_into(inner: &[usize], outer: &[usize], out: &mut [usize]) {
42    for (slot, &i) in out.iter_mut().zip(outer) {
43        *slot = inner[i];
44    }
45}
46
47impl Pass for LayoutElision {
48    fn name(&self) -> &'static str {
49        "layout"
50    }
51
52    fn run(&self, graph: &Graph) -> Result<PassOutput, Error> {
53        let mut rebuild = Rebuilder::new(graph, &self.limits);
54        let mut links: HashMap<ValueId, Link> = HashMap::new();
55        let mut changed = 0;
56        for (index, value) in graph.values().iter().enumerate() {
57            let old = ValueId(index as u32);
58            let Source::Node { op, inputs } = &value.source else {
59                rebuild.copy(old)?;
60                continue;
61            };
62            if !matches!(op, Op::Transpose { .. } | Op::Reshape { .. }) {
63                rebuild.copy(old)?;
64                continue;
65            }
66            let input = rebuild.mapped(inputs[0]);
67            // Look through one earlier layout node of the same kind.
68            let (source, source_shape, op) = match (links.get(&input), op) {
69                (
70                    Some(Link {
71                        op: Op::Transpose { perm: inner },
72                        source,
73                        source_shape,
74                    }),
75                    Op::Transpose { perm: outer },
76                ) => {
77                    let composed = compose(inner, outer);
78                    (
79                        *source,
80                        source_shape.clone(),
81                        Op::Transpose { perm: composed },
82                    )
83                }
84                (
85                    Some(Link {
86                        op: Op::Reshape { .. },
87                        source,
88                        source_shape,
89                    }),
90                    Op::Reshape { .. },
91                ) => (*source, source_shape.clone(), op.clone()),
92                _ => (input, graph.value(inputs[0]).ty.shape.clone(), op.clone()),
93            };
94            let identity = match &op {
95                Op::Transpose { perm } => perm.iter().enumerate().all(|(i, &p)| i == p),
96                Op::Reshape { shape } => *shape == source_shape,
97                _ => unreachable!("only layout ops reach here"),
98            };
99            if identity {
100                rebuild.alias(old, source);
101                changed += 1;
102                continue;
103            }
104            let new = if source == input {
105                rebuild.copy(old)?
106            } else {
107                changed += 1;
108                rebuild.replace_with_node(old, op.clone(), &[source])?
109            };
110            links.insert(
111                new,
112                Link {
113                    op,
114                    source,
115                    source_shape,
116                },
117            );
118        }
119        Ok(PassOutput {
120            graph: rebuild.finish()?,
121            changed,
122        })
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::GraphBuilder;
130
131    fn run(graph: &Graph) -> PassOutput {
132        LayoutElision::new(Limits::default()).run(graph).unwrap()
133    }
134
135    fn op_of(graph: &Graph, id: ValueId) -> &Op {
136        match &graph.value(id).source {
137            Source::Node { op, .. } => op,
138            other => panic!("{other:?}"),
139        }
140    }
141
142    fn input_of(graph: &Graph, id: ValueId) -> ValueId {
143        match &graph.value(id).source {
144            Source::Node { inputs, .. } => inputs[0],
145            other => panic!("{other:?}"),
146        }
147    }
148
149    #[test]
150    fn two_transposes_that_cancel_become_their_input() {
151        let mut b = GraphBuilder::new(Limits::default());
152        let x = b.input("x", vec![2, 3]).unwrap();
153        let t = b
154            .node("t", Op::Transpose { perm: vec![1, 0] }, &[x])
155            .unwrap();
156        let u = b
157            .node("u", Op::Transpose { perm: vec![1, 0] }, &[t])
158            .unwrap();
159        let g = b.finish(&[u]).unwrap();
160        let out = run(&g);
161        assert_eq!(out.changed, 1);
162        let output = out.graph.outputs()[0];
163        assert!(matches!(out.graph.value(output).source, Source::Input));
164        assert_eq!(out.graph.output_names(), ["u"]);
165    }
166
167    #[test]
168    fn two_transposes_compose_in_the_right_order() {
169        // inner [1, 2, 0], outer [1, 0, 2]: output dimension i is input
170        // dimension inner[outer[i]], so the composition is [2, 1, 0]. Swapping
171        // the two permutations would give [1, 0, 2], and a shape of [3, 2, 4].
172        let mut b = GraphBuilder::new(Limits::default());
173        let x = b.input("x", vec![2, 3, 4]).unwrap();
174        let t = b
175            .node(
176                "t",
177                Op::Transpose {
178                    perm: vec![1, 2, 0],
179                },
180                &[x],
181            )
182            .unwrap();
183        let u = b
184            .node(
185                "u",
186                Op::Transpose {
187                    perm: vec![1, 0, 2],
188                },
189                &[t],
190            )
191            .unwrap();
192        let g = b.finish(&[u]).unwrap();
193        let out = run(&g);
194        let output = out.graph.outputs()[0];
195        assert_eq!(
196            op_of(&out.graph, output),
197            &Op::Transpose {
198                perm: vec![2, 1, 0]
199            }
200        );
201        assert_eq!(out.graph.value(output).ty.shape, vec![4, 3, 2]);
202        assert!(matches!(
203            out.graph.value(input_of(&out.graph, output)).source,
204            Source::Input
205        ));
206    }
207
208    #[test]
209    fn inverse_permutations_cancel() {
210        let mut b = GraphBuilder::new(Limits::default());
211        let x = b.input("x", vec![2, 3, 4]).unwrap();
212        let t = b
213            .node(
214                "t",
215                Op::Transpose {
216                    perm: vec![1, 2, 0],
217                },
218                &[x],
219            )
220            .unwrap();
221        let u = b
222            .node(
223                "u",
224                Op::Transpose {
225                    perm: vec![2, 0, 1],
226                },
227                &[t],
228            )
229            .unwrap();
230        let g = b.finish(&[u]).unwrap();
231        let out = run(&g);
232        assert!(matches!(
233            out.graph.value(out.graph.outputs()[0]).source,
234            Source::Input
235        ));
236    }
237
238    #[test]
239    fn a_chain_of_three_collapses_to_one() {
240        let mut b = GraphBuilder::new(Limits::default());
241        let x = b.input("x", vec![2, 3, 4]).unwrap();
242        let t = b
243            .node(
244                "t",
245                Op::Transpose {
246                    perm: vec![1, 0, 2],
247                },
248                &[x],
249            )
250            .unwrap();
251        let u = b
252            .node(
253                "u",
254                Op::Transpose {
255                    perm: vec![0, 2, 1],
256                },
257                &[t],
258            )
259            .unwrap();
260        let v = b
261            .node(
262                "v",
263                Op::Transpose {
264                    perm: vec![2, 1, 0],
265                },
266                &[u],
267            )
268            .unwrap();
269        let g = b.finish(&[v]).unwrap();
270        let out = run(&g);
271        let output = out.graph.outputs()[0];
272        assert!(matches!(op_of(&out.graph, output), Op::Transpose { .. }));
273        assert!(matches!(
274            out.graph.value(input_of(&out.graph, output)).source,
275            Source::Input
276        ));
277    }
278
279    #[test]
280    fn an_identity_transpose_is_removed() {
281        let mut b = GraphBuilder::new(Limits::default());
282        let x = b.input("x", vec![2, 3]).unwrap();
283        let t = b
284            .node("t", Op::Transpose { perm: vec![0, 1] }, &[x])
285            .unwrap();
286        let g = b.finish(&[t]).unwrap();
287        let out = run(&g);
288        assert_eq!(out.changed, 1);
289        assert!(matches!(
290            out.graph.value(out.graph.outputs()[0]).source,
291            Source::Input
292        ));
293    }
294
295    #[test]
296    fn two_reshapes_become_one() {
297        let mut b = GraphBuilder::new(Limits::default());
298        let x = b.input("x", vec![6]).unwrap();
299        let r = b
300            .node("r", Op::Reshape { shape: vec![2, 3] }, &[x])
301            .unwrap();
302        let s = b
303            .node("s", Op::Reshape { shape: vec![3, 2] }, &[r])
304            .unwrap();
305        let g = b.finish(&[s]).unwrap();
306        let out = run(&g);
307        assert_eq!(out.changed, 1);
308        let output = out.graph.outputs()[0];
309        assert_eq!(
310            op_of(&out.graph, output),
311            &Op::Reshape { shape: vec![3, 2] }
312        );
313        assert!(matches!(
314            out.graph.value(input_of(&out.graph, output)).source,
315            Source::Input
316        ));
317    }
318
319    #[test]
320    fn a_reshape_back_to_the_original_shape_is_removed() {
321        let mut b = GraphBuilder::new(Limits::default());
322        let x = b.input("x", vec![2, 3]).unwrap();
323        let r = b.node("r", Op::Reshape { shape: vec![6] }, &[x]).unwrap();
324        let s = b
325            .node("s", Op::Reshape { shape: vec![2, 3] }, &[r])
326            .unwrap();
327        let g = b.finish(&[s]).unwrap();
328        let out = run(&g);
329        assert!(matches!(
330            out.graph.value(out.graph.outputs()[0]).source,
331            Source::Input
332        ));
333    }
334
335    #[test]
336    fn a_reshape_to_its_own_shape_is_removed() {
337        let mut b = GraphBuilder::new(Limits::default());
338        let x = b.input("x", vec![2, 3]).unwrap();
339        let r = b
340            .node("r", Op::Reshape { shape: vec![2, 3] }, &[x])
341            .unwrap();
342        let g = b.finish(&[r]).unwrap();
343        let out = run(&g);
344        assert_eq!(out.changed, 1);
345    }
346
347    #[test]
348    fn a_transpose_and_a_reshape_are_not_combined() {
349        let mut b = GraphBuilder::new(Limits::default());
350        let x = b.input("x", vec![2, 3]).unwrap();
351        let t = b
352            .node("t", Op::Transpose { perm: vec![1, 0] }, &[x])
353            .unwrap();
354        let r = b.node("r", Op::Reshape { shape: vec![6] }, &[t]).unwrap();
355        let g = b.finish(&[r]).unwrap();
356        let out = run(&g);
357        assert_eq!(out.changed, 0);
358        assert_eq!(out.graph.values().len(), 3);
359    }
360
361    #[test]
362    fn an_intermediate_with_another_reader_stays() {
363        let mut b = GraphBuilder::new(Limits::default());
364        let x = b.input("x", vec![2, 3]).unwrap();
365        let t = b
366            .node("t", Op::Transpose { perm: vec![1, 0] }, &[x])
367            .unwrap();
368        let u = b
369            .node("u", Op::Transpose { perm: vec![1, 0] }, &[t])
370            .unwrap();
371        let g = b.finish(&[t, u]).unwrap();
372        let out = run(&g);
373        assert_eq!(out.graph.output_names(), ["t", "u"]);
374        assert!(matches!(
375            op_of(&out.graph, out.graph.outputs()[0]),
376            Op::Transpose { .. }
377        ));
378        assert!(matches!(
379            out.graph.value(out.graph.outputs()[1]).source,
380            Source::Input
381        ));
382    }
383
384    #[test]
385    fn other_ops_are_left_alone() {
386        let mut b = GraphBuilder::new(Limits::default());
387        let x = b.input("x", vec![2]).unwrap();
388        let y = b.node("y", Op::Relu, &[x]).unwrap();
389        let g = b.finish(&[y]).unwrap();
390        let out = run(&g);
391        assert_eq!(out.changed, 0);
392        assert_eq!(out.graph, g);
393    }
394}
395
396/// Bounded model checking with Kani (`just kani`).
397#[cfg(kani)]
398mod proofs {
399    use super::compose_into;
400
401    const N: usize = 4;
402
403    fn is_permutation(p: &[usize]) -> bool {
404        (0..p.len()).all(|v| p.contains(&v))
405    }
406
407    /// For every pair of permutations of rank 4 and every coordinate, applying
408    /// the two transposes in turn lands on the same input coordinate as the
409    /// composed permutation, and the composition is itself a permutation.
410    /// Transposing by `perm` maps an output coordinate `z` to the input
411    /// coordinate `x` with `x[perm[k]] = z[k]`.
412    #[kani::proof]
413    #[kani::unwind(6)]
414    fn composing_two_transposes_equals_applying_them_in_turn() {
415        let inner: [usize; N] = kani::any();
416        let outer: [usize; N] = kani::any();
417        kani::assume(is_permutation(&inner) && is_permutation(&outer));
418        let mut composed = [0usize; N];
419        compose_into(&inner, &outer, &mut composed);
420        assert!(is_permutation(&composed));
421        let z: [u8; N] = kani::any();
422        let mut y = [0u8; N];
423        let mut x = [0u8; N];
424        let mut x_direct = [0u8; N];
425        for k in 0..N {
426            y[outer[k]] = z[k];
427        }
428        for m in 0..N {
429            x[inner[m]] = y[m];
430        }
431        for k in 0..N {
432            x_direct[composed[k]] = z[k];
433        }
434        assert_eq!(x, x_direct);
435    }
436}