Skip to main content

cubecl_tile/launch/
vectorization.rs

1//! The width each operand is bound at: the device's ceiling, the layout's [`ElemAlignment`], and
2//! the [`VectorRequirement`]s a kernel states, settled by [`Vectorization`].
3
4use crate::layout::ElemAlignment;
5
6/// Why an operand's width must be narrower than the device reads it at.
7///
8/// A requirement is about the tiles of an operand a component touches, never about the memory
9/// they live in: a microkernel is handed tiles and does not know whether its walker staged them.
10/// That holds because an operand keeps one width down the whole chain — the global buffer is
11/// bound at it, a stage the ring fills keeps it, a fragment loads the stage at it — so every
12/// requirement on an operand, from every component, narrows the same width.
13///
14/// A requirement's elements lie along the operand's contiguous axis, and a stretch starts at a
15/// multiple of its own length, since every tile is a regular cut of its parent: a width dividing
16/// the length never has a line cross the end of a stretch.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum VectorRequirement<O> {
19    /// A component reads or writes `operand`'s tiles in stretches of `elements` consecutive
20    /// elements, so the width divides them.
21    Access { operand: O, elements: usize },
22    /// A component reads or writes `operand` one element at a time, as out of shared-memory
23    /// atomics, which hand back a scalar: the width is one.
24    Elementwise { operand: O },
25    /// The launch binds `operand` masked one element at a time, as it does where a tile reaches
26    /// past the operand's edge along its contiguous axis: a line in the last, short tile would
27    /// reach past it, so the width is one.
28    Masked { operand: O },
29    /// A component lines `operand` together with `with`, one line of each per step, as a register
30    /// block lines the rhs along `n` with its accumulator: the two share one width, the narrowest
31    /// of what every operand lined with either admits alone.
32    Together { operand: O, with: O },
33    /// The launch binds `operand`'s buffer, whose layout is aligned to `alignment` elements along
34    /// its innermost dim: a line must read its elements and no others.
35    Aligned {
36        operand: O,
37        alignment: ElemAlignment,
38    },
39}
40
41impl<O: Copy + Eq> VectorRequirement<O> {
42    /// The operand this requirement narrows.
43    pub fn operand(&self) -> O {
44        match self {
45            VectorRequirement::Access { operand, .. }
46            | VectorRequirement::Elementwise { operand }
47            | VectorRequirement::Masked { operand }
48            | VectorRequirement::Together { operand, .. }
49            | VectorRequirement::Aligned { operand, .. } => *operand,
50        }
51    }
52
53    /// This requirement on the operands `operand` maps its own onto, where it maps every one: what
54    /// a family running a shared microkernel on its own operands requires of them. `None` for an
55    /// operand the family binds no buffer for, as a product it keeps in registers.
56    pub fn restated<P>(self, operand: impl Fn(O) -> Option<P>) -> Option<VectorRequirement<P>> {
57        Some(match self {
58            VectorRequirement::Access {
59                operand: at,
60                elements,
61            } => VectorRequirement::Access {
62                operand: operand(at)?,
63                elements,
64            },
65            VectorRequirement::Elementwise { operand: at } => VectorRequirement::Elementwise {
66                operand: operand(at)?,
67            },
68            VectorRequirement::Masked { operand: at } => VectorRequirement::Masked {
69                operand: operand(at)?,
70            },
71            VectorRequirement::Together { operand: at, with } => VectorRequirement::Together {
72                operand: operand(at)?,
73                with: operand(with)?,
74            },
75            VectorRequirement::Aligned {
76                operand: at,
77                alignment,
78            } => VectorRequirement::Aligned {
79                operand: operand(at)?,
80                alignment,
81            },
82        })
83    }
84
85    /// Whether a line of `width` meets this requirement.
86    pub fn admits(&self, width: usize) -> bool {
87        match self {
88            VectorRequirement::Access { elements, .. } => elements.is_multiple_of(width),
89            VectorRequirement::Elementwise { .. } | VectorRequirement::Masked { .. } => width == 1,
90            VectorRequirement::Aligned { alignment, .. } => alignment.admits(width),
91            // Settled across the operands it lines together by `width`, not by either alone.
92            VectorRequirement::Together { .. } => true,
93        }
94    }
95}
96
97/// The requirements every component of a launch enqueued, and the widths they settle.
98///
99/// A routine starts the pass with its own accesses and hands it to its microkernel, which adds
100/// its own and hands it to the ones it is built from: the pass walks down as the kernel does.
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct Vectorization<O> {
103    requirements: Vec<VectorRequirement<O>>,
104}
105
106impl<O: Copy + Eq> Vectorization<O> {
107    pub fn new() -> Self {
108        Self {
109            requirements: Vec::new(),
110        }
111    }
112
113    pub fn require(&mut self, requirement: VectorRequirement<O>) {
114        self.requirements.push(requirement);
115    }
116
117    /// Every requirement `shared` holds, restated on this pass's operands through `operand`
118    /// ([`VectorRequirement::restated`]): how a family hands a shared microkernel its own pass,
119    /// one multiplication's operands at a time.
120    pub fn restate<S: Copy + Eq>(
121        &mut self,
122        shared: Vectorization<S>,
123        operand: impl Fn(S) -> Option<O>,
124    ) {
125        let restated = shared
126            .requirements
127            .into_iter()
128            .filter_map(|requirement| requirement.restated(&operand));
129        self.requirements.extend(restated);
130    }
131
132    /// The width `operand` binds at: the narrowest, over `operand` and every operand lined together
133    /// with it through [`Together`](VectorRequirement::Together) pairs, of the widest power of two
134    /// at most `widest(member)` every requirement on that member admits.
135    pub fn width(&self, operand: O, widest: impl Fn(O) -> usize) -> usize {
136        let mut lined = vec![operand];
137        let mut next = 0;
138        while let Some(&at) = lined.get(next) {
139            for requirement in &self.requirements {
140                let VectorRequirement::Together { operand, with } = *requirement else {
141                    continue;
142                };
143                let other = match (operand == at, with == at) {
144                    (true, _) => with,
145                    (_, true) => operand,
146                    (false, false) => continue,
147                };
148                if !lined.contains(&other) {
149                    lined.push(other);
150                }
151            }
152            next += 1;
153        }
154        lined
155            .into_iter()
156            .map(|member| self.alone(member, widest(member)))
157            .min()
158            .unwrap_or(1)
159    }
160
161    /// The widest power of two, at most `widest`, every requirement on `operand` admits.
162    fn alone(&self, operand: O, widest: usize) -> usize {
163        let admitted = |width: usize| {
164            self.requirements
165                .iter()
166                .filter(|requirement| requirement.operand() == operand)
167                .all(|requirement| requirement.admits(width))
168        };
169        let mut width = widest.max(1).next_power_of_two();
170        while width > 1 && (width > widest || !admitted(width)) {
171            width /= 2;
172        }
173        width
174    }
175}
176
177impl<O: Copy + Eq> Default for Vectorization<O> {
178    fn default() -> Self {
179        Self::new()
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    /// Operands lined together share one width, the narrowest any of them admits alone — through a
188    /// chain of pairs too; an operand lined with none keeps its own.
189    #[test]
190    fn operands_lined_together_share_the_narrowest_width() {
191        let mut vectorization = Vectorization::new();
192        vectorization.require(VectorRequirement::Access {
193            operand: 'c',
194            elements: 2,
195        });
196        vectorization.require(VectorRequirement::Together {
197            operand: 'a',
198            with: 'b',
199        });
200        vectorization.require(VectorRequirement::Together {
201            operand: 'b',
202            with: 'c',
203        });
204        for operand in ['a', 'b', 'c'] {
205            assert_eq!(vectorization.width(operand, |_| 8), 2, "{operand}");
206        }
207        assert_eq!(vectorization.width('d', |_| 8), 8);
208    }
209
210    /// A buffer aligned to fewer elements than the device reads narrows the width to what it
211    /// admits.
212    #[test]
213    fn the_width_reads_the_buffer_whole_lines() {
214        let mut vectorization = Vectorization::new();
215        vectorization.require(VectorRequirement::Aligned {
216            operand: 'a',
217            alignment: ElemAlignment::new(&[4, 12], &[12, 1]),
218        });
219        assert_eq!(vectorization.width('a', |_| 8), 4);
220        vectorization.require(VectorRequirement::Aligned {
221            operand: 'b',
222            alignment: ElemAlignment::ONE,
223        });
224        assert_eq!(vectorization.width('b', |_| 8), 1);
225    }
226
227    /// A shared microkernel's requirements land on the operands the family binds; one on an
228    /// operand it keeps in registers, and a pair lined with one, land nowhere.
229    #[test]
230    fn a_restated_requirement_lands_on_the_bound_operand() {
231        let mut shared = Vectorization::new();
232        shared.require(VectorRequirement::Access {
233            operand: 'l',
234            elements: 4,
235        });
236        shared.require(VectorRequirement::Access {
237            operand: 'o',
238            elements: 1,
239        });
240        shared.require(VectorRequirement::Together {
241            operand: 'r',
242            with: 'o',
243        });
244        let mut family = Vectorization::new();
245        family.restate(shared, |operand| match operand {
246            'l' => Some('a'),
247            'r' => Some('b'),
248            _ => None,
249        });
250        assert_eq!(family.width('a', |_| 8), 4);
251        assert_eq!(family.width('b', |_| 8), 8);
252    }
253
254    #[test]
255    fn the_width_divides_every_access() {
256        let mut vectorization = Vectorization::new();
257        vectorization.require(VectorRequirement::Access {
258            operand: 'a',
259            elements: 24,
260        });
261        assert_eq!(vectorization.width('a', |_| 16), 8);
262        vectorization.require(VectorRequirement::Access {
263            operand: 'a',
264            elements: 12,
265        });
266        assert_eq!(vectorization.width('a', |_| 16), 4);
267        assert_eq!(vectorization.width('b', |_| 16), 16);
268    }
269
270    #[test]
271    fn an_elementwise_access_is_scalar() {
272        let mut vectorization = Vectorization::new();
273        vectorization.require(VectorRequirement::Access {
274            operand: 'a',
275            elements: 8,
276        });
277        vectorization.require(VectorRequirement::Elementwise { operand: 'a' });
278        assert_eq!(vectorization.width('a', |_| 8), 1);
279    }
280
281    #[test]
282    fn the_width_never_exceeds_the_widest_it_is_given() {
283        let vectorization = Vectorization::<char>::new();
284        assert_eq!(vectorization.width('a', |_| 2), 2);
285        assert_eq!(vectorization.width('a', |_| 3), 2);
286        assert_eq!(vectorization.width('a', |_| 1), 1);
287    }
288}