1use 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
23struct Link {
26 op: Op,
27 source: ValueId,
28 source_shape: Vec<usize>,
29}
30
31fn 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
39fn 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 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 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#[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 #[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}