Skip to main content

cubecl_tile/space/partition/
base.rs

1//! A [`Space`] and the [`Level`]s that cut it, held as one value.
2
3use cubecl::{prelude::*, unexpanded};
4
5use crate::{
6    Axis, ComputeScope, Count, Coverage, CubeAxis, Level, LevelTable, Region, RegionExpand, Space,
7    Walk,
8};
9
10use super::UnitCube;
11
12/// A space with the levels that partition it, outermost first: what a kernel's loops iterate.
13#[derive(CubeType, CubeLaunch, Debug, Clone, PartialEq, Eq, Hash)]
14pub struct Partitioning {
15    pub(crate) space: Space,
16    #[cube(comptime)]
17    pub(crate) levels: Vec<Level>,
18}
19
20/// The comptime partitioning of a runtime one.
21impl PartitioningExpand {
22    #[allow(clippy::should_implement_trait)]
23    pub fn clone(&self) -> Partitioning {
24        Partitioning::new(self.space.clone(), self.levels.clone())
25    }
26
27    pub fn space(&self) -> Space {
28        self.space.clone()
29    }
30}
31
32/// The comptime reads of a partitioning a kernel is handed.
33impl PartitioningExpand {
34    /// The levels, outermost first.
35    pub fn levels(&self) -> &[Level] {
36        &self.levels
37    }
38}
39
40impl Partitioning {
41    /// The levels, outermost first.
42    pub fn levels(&self) -> &[Level] {
43        &self.levels
44    }
45
46    /// Level `i`, outermost first.
47    pub fn level(&self, i: usize) -> Level {
48        self.levels[i].clone()
49    }
50}
51
52#[cube]
53impl Partitioning {
54    /// The regions of the first level: what `for cube in space` iterates.
55    pub fn walk(&self) -> Walk {
56        Region::root(self).walk()
57    }
58
59    /// The regions of `level` over this space, a level of the kernel's own.
60    pub fn over(&self, #[comptime] level: &Level) -> Walk {
61        Walk::of(&self.space, comptime!(level.clone()), Region::root(self))
62    }
63}
64
65/// Host-side stand-in for `for plane in space`; never runs.
66impl IntoIterator for Partitioning {
67    type Item = Region;
68    type IntoIter = std::vec::IntoIter<Region>;
69
70    fn into_iter(self) -> Self::IntoIter {
71        unexpanded!()
72    }
73}
74
75impl IntoIterator for &Partitioning {
76    type Item = Region;
77    type IntoIter = std::vec::IntoIter<Region>;
78
79    fn into_iter(self) -> Self::IntoIter {
80        unexpanded!()
81    }
82}
83
84/// `for region in partitioning` iterates its first level.
85impl Iterable for PartitioningExpand {
86    type Item = RegionExpand;
87
88    fn expand(self, scope: &Scope, body: &mut dyn FnMut(&Scope, RegionExpand)) {
89        RegionExpand::__expand_root(scope, &self).expand(scope, body)
90    }
91
92    fn expand_unroll(self, scope: &Scope, body: &mut dyn FnMut(&Scope, RegionExpand)) {
93        RegionExpand::__expand_root(scope, &self).expand_unroll(scope, body)
94    }
95}
96
97/// `for cube in &space`: the same, leaving `space` to the body.
98impl Iterable for &PartitioningExpand {
99    type Item = RegionExpand;
100
101    fn expand(self, scope: &Scope, body: &mut dyn FnMut(&Scope, RegionExpand)) {
102        RegionExpand::__expand_root(scope, self).expand(scope, body)
103    }
104
105    fn expand_unroll(self, scope: &Scope, body: &mut dyn FnMut(&Scope, RegionExpand)) {
106        RegionExpand::__expand_root(scope, self).expand_unroll(scope, body)
107    }
108}
109
110impl Partitioning {
111    /// `space` cut by `levels`, outermost first. Panics where two levels distribute over one
112    /// scope: a scope's instances take one level's tiles.
113    pub fn new(space: Space, levels: Vec<Level>) -> Self {
114        for (i, level) in levels.iter().enumerate() {
115            if let Some(scope) = level.coverage().scope() {
116                assert!(
117                    levels[i + 1..]
118                        .iter()
119                        .all(|other| other.coverage() != level.coverage()),
120                    "Partitioning::new: two levels distribute over {scope:?}"
121                );
122            }
123            if level.spans_launch() {
124                assert!(
125                    i == 0,
126                    "Partitioning::new: a level spanning the launch is the outermost level, and \
127                     this one sits {i} levels down"
128                );
129                assert!(
130                    levels.iter().all(|other| other.coverage()
131                        != Coverage::Distribute(ComputeScope::Cube)
132                        && other.fillers() == 0),
133                    "Partitioning::new: the launch's planes or units take the outermost level, so \
134                     no cube level groups them and no plane only fills a cube's stages"
135                );
136            }
137            if level.unit_cube().is_some() {
138                assert!(
139                    levels.iter().all(|other| !matches!(
140                        other.coverage(),
141                        Coverage::Distribute(ComputeScope::Plane | ComputeScope::PlaneGroup { .. })
142                    )),
143                    "Partitioning::new: the launch's units take the outermost level, so no plane \
144                     level holds them"
145                );
146            }
147        }
148        Self { space, levels }
149    }
150
151    /// The space these levels cut.
152    pub fn space(&self) -> &Space {
153        &self.space
154    }
155
156    /// This partitioning with every extent `Dynamic`.
157    pub fn all_dynamic(self) -> Self {
158        Partitioning {
159            space: self.space.all_dynamic(),
160            levels: self.levels,
161        }
162    }
163
164    /// This partitioning's table with a name for each axis; unnamed axes print their index.
165    pub fn table<'a>(&'a self, labels: &'a [(Axis, &'a str)]) -> LevelTable<'a> {
166        LevelTable::new(self, labels)
167    }
168
169    /// The leaf the levels reach.
170    pub fn leaf(&self) -> Space {
171        self.space.leaf(&self.levels)
172    }
173
174    /// Whether some level's edge fails to divide the extent of `axis` handed to it.
175    pub fn overhangs(&self, axis: Axis) -> bool {
176        assert!(
177            !self.space.is_dynamic(axis),
178            "Partitioning::overhangs: axis {axis:?} is Dynamic; ask the concrete space"
179        );
180        let mut space = self.space.clone();
181        for level in &self.levels {
182            if level.overhangs(&space, axis) {
183                return true;
184            }
185            space = level.child(&space);
186        }
187        false
188    }
189
190    /// Every axis some tile reaches past the end of, whose accesses are masked, in the space's
191    /// order.
192    ///
193    /// One walk down the levels, every axis asked at each: [`overhangs`](Self::overhangs) asked
194    /// axis by axis would cut the space down through every level once per axis, and a launch asks
195    /// this of every operand it binds, on every call.
196    pub(crate) fn overhanging(&self) -> Vec<Axis> {
197        let axes: Vec<Axis> = self.space.axes().collect();
198        for &axis in &axes {
199            assert!(
200                !self.space.is_dynamic(axis),
201                "Partitioning::overhanging: axis {axis:?} is Dynamic; ask the concrete space"
202            );
203        }
204        let mut overhanging = vec![false; axes.len()];
205        let mut space = self.space.clone();
206        for level in &self.levels {
207            for (hangs, &axis) in overhanging.iter_mut().zip(&axes) {
208                *hangs |= level.overhangs(&space, axis);
209            }
210            space = level.child(&space);
211        }
212        axes.into_iter()
213            .zip(overhanging)
214            .filter_map(|(axis, hangs)| hangs.then_some(axis))
215            .collect()
216    }
217
218    /// The one level distributed over `scope`'s workers, if any.
219    pub fn level_distributed(&self, scope: ComputeScope) -> Option<&Level> {
220        let coverage = Coverage::Distribute(scope);
221        self.levels
222            .iter()
223            .find(|level| level.coverage() == coverage)
224    }
225
226    /// The box one of `scope`'s instances covers: the space cut down through the level distributed
227    /// over it. Panics where no level distributes over `scope`.
228    pub fn box_of(&self, scope: ComputeScope) -> Space {
229        let coverage = Coverage::Distribute(scope);
230        let mut space = self.space.clone();
231        for level in &self.levels {
232            space = level.child(&space);
233            if level.coverage() == coverage {
234                return space;
235            }
236        }
237        panic!("Partitioning::box_of: no level distributes over {scope:?}")
238    }
239
240    /// The levels one instance walks, outermost first.
241    pub fn walks(&self) -> impl Iterator<Item = &Level> + '_ {
242        self.levels
243            .iter()
244            .filter(|level| level.coverage() == Coverage::Walk)
245    }
246
247    /// Instances `coverage` distributes the space to.
248    pub(crate) fn instances(&self, coverage: Coverage) -> u32 {
249        match coverage {
250            Coverage::Distribute(ComputeScope::Cube) => [CubeAxis::X, CubeAxis::Y, CubeAxis::Z]
251                .into_iter()
252                .map(|dim| self.cube_instances(dim))
253                .product(),
254            Coverage::Distribute(ComputeScope::Plane)
255            | Coverage::Distribute(ComputeScope::PlaneGroup { .. })
256            | Coverage::Distribute(ComputeScope::Unit) => {
257                self.count_instances(|level| level.coverage() == coverage, |_, _| true)
258            }
259            Coverage::Walk => 1,
260        }
261    }
262
263    /// Cubes on grid dimension `dim`.
264    fn cube_instances(&self, dim: CubeAxis) -> u32 {
265        self.count_instances(
266            |level| {
267                level.coverage() == Coverage::Distribute(ComputeScope::Cube)
268                    && (level.shared_by().is_none() || dim == CubeAxis::X)
269            },
270            |level, axis| level.cube_axis(axis) == Some(dim),
271        )
272    }
273
274    /// The product over the selected levels of their instance counts; partial tiles count.
275    fn count_instances(
276        &self,
277        levels: impl Fn(&Level) -> bool,
278        axes: impl Fn(&Level, Axis) -> bool,
279    ) -> u32 {
280        let mut total = 1u32;
281        let mut space = self.space.clone();
282        for level in &self.levels {
283            // A unit of a level spanning the launch takes one tile, and asks for no units of its own.
284            if levels(level) && level.unit_cube().is_none() {
285                match level.shared_by() {
286                    Some(workers) => total *= workers as u32,
287                    None => {
288                        for axis in space.axes() {
289                            if level.distributes(axis) && axes(level, axis) {
290                                total *= match level.count(axis) {
291                                    Some(Count::AllAcross(workers)) => workers,
292                                    // Launch-sized units ask for none of their own.
293                                    Some(Count::Distributed(_)) => 1,
294                                    _ => level.tiles(&space, axis),
295                                } as u32;
296                            }
297                        }
298                    }
299                }
300            }
301            space = level.child(&space);
302        }
303        total
304    }
305
306    /// The cube grid these levels distribute to.
307    pub fn cube_count(&self) -> CubeCount {
308        if let Some(order) = self.levels.first().and_then(Level::launch_order) {
309            let (x, y) = order.grid(&self.levels[0], &self.space);
310            return CubeCount::Static(x as u32, y as u32, 1);
311        }
312        CubeCount::Static(
313            self.cube_instances(CubeAxis::X),
314            self.cube_instances(CubeAxis::Y),
315            self.cube_instances(CubeAxis::Z),
316        )
317    }
318
319    /// Planes one cube holds: the levels' plane cuts, each plane group's planes, plus any that
320    /// only fill; where the planes or units span the launch, the cubes their order groups them in.
321    pub fn planes_per_cube(&self) -> u32 {
322        if let Some(order) = self.levels.first().and_then(Level::plane_order) {
323            return order.planes_per_cube() as u32;
324        }
325        if let Some(cube) = self.unit_cube() {
326            return cube.planes as u32;
327        }
328        // `Levels` deals plane groups by one level, beside no plane level.
329        let grouped = self.levels.iter().find_map(|level| {
330            let scope = level.coverage().scope()?;
331            let planes = scope.planes().filter(|_| scope != ComputeScope::Plane)?;
332            Some(self.instances(level.coverage()) * planes as u32)
333        });
334        grouped.unwrap_or_else(|| self.instances(Coverage::Distribute(ComputeScope::Plane)))
335            + self.fillers()
336    }
337
338    /// Planes the cube holds that fill a walk's stages and take no tile
339    /// ([`Levels::filled_by`](crate::Levels::filled_by)).
340    pub fn fillers(&self) -> u32 {
341        self.levels.iter().map(|level| level.fillers() as u32).sum()
342    }
343
344    /// Units one instance holds; `1` where no level cuts to units or they take tiles in turns.
345    pub fn units(&self) -> u32 {
346        self.instances(Coverage::Distribute(ComputeScope::Unit))
347    }
348
349    /// The cube this partitioning asks for at `plane_size`; where the units span the launch, the
350    /// cube they are grouped in, whatever `plane_size`.
351    pub fn cube_dim(&self, plane_size: u32) -> CubeDim {
352        match self.unit_cube() {
353            Some(cube) => CubeDim::new_2d(cube.plane_width as u32, cube.planes as u32),
354            None => CubeDim::new_2d(plane_size, self.planes_per_cube()),
355        }
356    }
357
358    /// The cubes the launch's units are grouped in, where a unit level spans the launch.
359    pub fn unit_cube(&self) -> Option<UnitCube> {
360        self.levels.first().and_then(Level::unit_cube)
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367    use crate::Levels;
368
369    const M: Axis = Axis(0);
370    const N: Axis = Axis(1);
371    const K: Axis = Axis(2);
372
373    /// A cube grid over `M`/`N`, a `K` walk, then one partition per plane.
374    fn staged(fillers: usize) -> Partitioning {
375        Partitioning::new(
376            Space::new(&[(M, 256), (N, 256), (K, 512)]),
377            Levels::leaf(&[(M, 64), (N, 64), (K, 64)])
378                .planes(&[(M, 2), (N, 2)])
379                .walk_every(&[K])
380                .filled_by(fillers)
381                .cubes(&[M, N])
382                .build(),
383        )
384    }
385
386    #[test]
387    fn a_walk_with_no_fillers_is_the_partitioning_it_always_was() {
388        let plain = staged(0);
389        assert_eq!(plain.fillers(), 0);
390        assert_eq!(plain.planes_per_cube(), 4);
391        assert_eq!(plain.cube_dim(32), CubeDim::new_2d(32, 4));
392    }
393
394    /// Filling planes widen the cube and change nothing else.
395    #[test]
396    fn a_filling_plane_widens_the_cube_and_nothing_else() {
397        let plain = staged(0);
398        let filled = staged(1);
399
400        assert_eq!(filled.planes_per_cube(), plain.planes_per_cube() + 1);
401        assert_eq!(filled.cube_dim(32), CubeDim::new_2d(32, 5));
402
403        assert_eq!(
404            filled.instances(Coverage::Distribute(ComputeScope::Plane)),
405            plain.instances(Coverage::Distribute(ComputeScope::Plane))
406        );
407        assert_eq!(filled.units(), plain.units());
408        for dim in [CubeAxis::X, CubeAxis::Y, CubeAxis::Z] {
409            assert_eq!(filled.cube_instances(dim), plain.cube_instances(dim));
410        }
411    }
412
413    #[test]
414    fn the_fillers_of_every_level_are_counted() {
415        assert_eq!(staged(3).fillers(), 3);
416    }
417
418    /// Only a walk can be filled.
419    #[test]
420    #[should_panic(expected = "only a walk's regions are staged")]
421    fn a_level_that_distributes_its_tiles_cannot_be_filled_by_anyone() {
422        Levels::leaf(&[(M, 64)])
423            .planes(&[(M, 2)])
424            .filled_by(1)
425            .build();
426    }
427
428    /// A scope's instances take one level's tiles, so a second level over the same scope is
429    /// refused.
430    #[test]
431    #[should_panic(expected = "two levels distribute over Plane")]
432    fn two_levels_over_one_scope_are_refused() {
433        Partitioning::new(
434            Space::new(&[(M, 64), (N, 64)]),
435            Levels::leaf(&[(M, 8), (N, 8)])
436                .planes(&[(M, 2)])
437                .planes(&[(N, 2)])
438                .build(),
439        );
440    }
441
442    /// The overhanging axes are the ones some level's tile fails to divide, asked axis by axis,
443    /// in the space's order: an edge at the cube level, one only a plane's box meets, and none.
444    #[test]
445    fn the_overhanging_axes_are_the_ones_some_level_overhangs() {
446        let partitioning = Partitioning::new(
447            Space::new(&[(M, 200), (N, 256), (K, 96)]),
448            Levels::leaf(&[(M, 64), (N, 48), (K, 32)])
449                .planes(&[(M, 2), (N, 2)])
450                .walk_every(&[K])
451                .cubes(&[M, N])
452                .build(),
453        );
454        let asked: Vec<Axis> = [M, N, K]
455            .into_iter()
456            .filter(|&axis| partitioning.overhangs(axis))
457            .collect();
458        assert_eq!(partitioning.overhanging(), asked);
459        assert_eq!(asked, vec![M, N]);
460        assert!(staged(0).overhanging().is_empty());
461    }
462
463    /// A scope's box is the space cut down through its level, whatever walks inside it.
464    #[test]
465    fn a_scope_covers_the_box_its_level_cuts() {
466        let partitioning = staged(0);
467        let cube = Space::new(&[(M, 128), (N, 128), (K, 512)]);
468        assert_eq!(partitioning.box_of(ComputeScope::Cube), cube);
469        let plane = Space::new(&[(M, 64), (N, 64), (K, 64)]);
470        assert_eq!(partitioning.box_of(ComputeScope::Plane), plane);
471    }
472}