Skip to main content

cubecl_tile/space/partition/
level.rs

1//! One [`Level`]: which axes a loop steps, in tiles of what size, how many, and who takes them.
2
3use super::{ComputeScope, Count, CubeAxis, CubeOrder, GridCount, PlaneOrder, UnitCube};
4use crate::{Axis, AxisMap, Extent, Space};
5
6/// One decomposition level of a space: who takes its tiles and how it cuts each axis it names.
7/// Unnamed axes are handed down whole.
8#[derive(Clone, PartialEq, Eq, Hash, Debug)]
9pub struct Level {
10    coverage: Coverage,
11    cuts: AxisMap<Cut>,
12    /// Axes a cube level hands one tile of, riding the grid's `Z` ([`batching`](Level::batching)).
13    batches: Vec<Axis>,
14    shared_by: Option<usize>,
15    fillers: usize,
16    order: CubeOrder,
17    /// Who across the whole launch takes this level's tiles, where it spans the launch rather than
18    /// one cube.
19    launch_span: Option<LaunchSpan>,
20    /// Axes whose tiles go out last first ([`Levels::reversed`](crate::Levels::reversed)).
21    reversed: Vec<Axis>,
22}
23
24/// Who takes the tiles of a level spanning the launch.
25#[derive(Clone, PartialEq, Eq, Hash, Debug)]
26pub(crate) enum LaunchSpan {
27    /// The launch's planes, in this order ([`spanning_launch`](Level::spanning_launch)).
28    Planes(PlaneOrder),
29    /// The launch's units, grouped in these cubes
30    /// ([`units_spanning_launch`](Level::units_spanning_launch)).
31    Units(UnitCube),
32}
33
34/// One axis of a level: the tile it steps in, how many, and how its instances spread them.
35#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
36pub(crate) struct Cut {
37    pub(crate) tile: usize,
38    pub(crate) count: Count,
39    pub(crate) spread: Spread,
40}
41
42/// How a level covers its tiles.
43#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
44pub enum Coverage {
45    /// One instance steps the tiles in sequence.
46    Walk,
47    /// The tiles are distributed over the instances of a scope.
48    Distribute(ComputeScope),
49}
50
51impl Coverage {
52    /// The scope the tiles are distributed over, or `None` where one instance walks them.
53    pub fn scope(self) -> Option<ComputeScope> {
54        match self {
55            Coverage::Walk => None,
56            Coverage::Distribute(scope) => Some(scope),
57        }
58    }
59}
60
61/// How a distributed axis's tiles are handed to a scope's instances.
62#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
63pub enum Spread {
64    /// Instance `i` owns a contiguous run (cube 0 → `{0,1}`, cube 1 → `{2,3}`).
65    #[default]
66    Contiguous,
67    /// Instances take turns (cube 0 → `{0,2}`, cube 1 → `{1,3}`).
68    Interleaved,
69}
70
71impl Level {
72    /// The walk level over a region in tiles of `tile`, taking every one of them.
73    pub fn every(tile: &[(Axis, usize)]) -> Level {
74        let cuts: Vec<_> = tile
75            .iter()
76            .map(|&(axis, tile)| (axis, tile, Count::All, Spread::Contiguous))
77            .collect();
78        Level::new(Coverage::Walk, &cuts)
79    }
80
81    /// A level covered as `coverage` says over `cuts`, each `(axis, tile, count, spread)`.
82    pub(crate) fn new(coverage: Coverage, cuts: &[(Axis, usize, Count, Spread)]) -> Level {
83        for (i, &(axis, ..)) in cuts.iter().enumerate() {
84            assert!(
85                !cuts[..i].iter().any(|&(a, ..)| a == axis),
86                "Level: {axis:?} is named twice; a level states each of its axes once"
87            );
88        }
89        for &(axis, tile, count, _) in cuts {
90            assert!(tile > 0, "Level: {axis:?} has a tile of nothing");
91            match (coverage, count) {
92                (
93                    Coverage::Distribute(ComputeScope::Cube)
94                    | Coverage::Distribute(ComputeScope::Plane),
95                    Count::AllAcross(_),
96                ) => {}
97                (_, Count::AllAcross(_)) => {
98                    panic!(
99                        "Level: {axis:?} is distributed across workers in runs, which only cubes \
100                         and planes take"
101                    )
102                }
103                (
104                    Coverage::Distribute(ComputeScope::Unit)
105                    | Coverage::Distribute(ComputeScope::Plane)
106                    | Coverage::Distribute(ComputeScope::PlaneGroup { .. }),
107                    Count::All,
108                ) => panic!(
109                    "Level: {axis:?} takes every tile on a plane's workers, whose count is the \
110                     device's; state how many"
111                ),
112                (Coverage::Distribute(ComputeScope::Unit), Count::Distributed(_)) => assert!(
113                    matches!(
114                        cuts.iter().find(|&&(a, ..)| a == axis).map(|&(.., s)| s),
115                        Some(Spread::Interleaved)
116                    ),
117                    "Level: {axis:?} is distributed in turns to however many units the launch runs, so \
118                     the units take it interleaved"
119                ),
120                (_, Count::Distributed(_)) => panic!(
121                    "Level: {axis:?} is distributed in turns to however many units the launch runs, \
122                     which only a plane's units can be"
123                ),
124                _ => {}
125            }
126        }
127        let distributed = cuts
128            .iter()
129            .any(|&(_, _, count, _)| matches!(count, Count::Distributed(_)));
130        assert!(
131            !distributed
132                || (coverage == Coverage::Distribute(ComputeScope::Unit) && cuts.len() == 1),
133            "Level: an axis distributed to however many units the launch runs takes all of them, so \
134             the level distributes no other axis to the units; it distributes {}",
135            cuts.len()
136        );
137        let cuts: Vec<_> = cuts
138            .iter()
139            .map(|&(axis, tile, count, spread)| {
140                (
141                    axis,
142                    Cut {
143                        tile,
144                        count,
145                        spread,
146                    },
147                )
148            })
149            .collect();
150        Level {
151            coverage,
152            cuts: AxisMap::new(&cuts),
153            batches: Vec::new(),
154            shared_by: None,
155            fillers: 0,
156            order: CubeOrder::RowMajor,
157            launch_span: None,
158            reversed: Vec::new(),
159        }
160    }
161
162    /// The plane level over every tile of `tile`'s axes, each `(axis, tile)`, dealt to the planes
163    /// of the whole launch in `order` rather than to the planes of one cube: the outermost level
164    /// of a partitioning with no cube level, whose cubes are only how the launch groups its planes.
165    pub(crate) fn spanning_launch(tile: &[(Axis, usize)], order: PlaneOrder) -> Level {
166        assert!(
167            !tile.is_empty(),
168            "Level::spanning_launch: the launch's planes take the tiles of at least one axis"
169        );
170        for axis in order.block_axes() {
171            assert!(
172                tile.iter().any(|&(a, _)| a == axis),
173                "Level::spanning_launch: the order's block names {axis:?}, which the level does not"
174            );
175        }
176        assert!(
177            tile.len() > 1 || !order.block_order().swizzles(),
178            "Level::spanning_launch: the blocks of one axis have one order; a swizzle takes two"
179        );
180        let cuts: Vec<_> = tile
181            .iter()
182            .map(|&(axis, tile)| {
183                assert!(tile > 0, "Level: {axis:?} has a tile of nothing");
184                (
185                    axis,
186                    Cut {
187                        tile,
188                        count: Count::All,
189                        spread: Spread::Contiguous,
190                    },
191                )
192            })
193            .collect();
194        Level {
195            coverage: Coverage::Distribute(ComputeScope::Plane),
196            cuts: AxisMap::new(&cuts),
197            batches: Vec::new(),
198            shared_by: None,
199            fillers: 0,
200            order: CubeOrder::RowMajor,
201            launch_span: Some(LaunchSpan::Planes(order)),
202            reversed: Vec::new(),
203        }
204    }
205
206    /// The unit level over every tile of `tile`'s axes, each `(axis, tile)`, one tile to each
207    /// unit of the whole launch, the units grouped in cubes of `cube`: the outermost level of a
208    /// partitioning with no cube or plane level.
209    pub(crate) fn units_spanning_launch(tile: &[(Axis, usize)], cube: UnitCube) -> Level {
210        assert!(
211            cube.units() > 0,
212            "Level::units_spanning_launch: a cube holds at least one unit"
213        );
214        let order = cube.order();
215        Level {
216            coverage: Coverage::Distribute(ComputeScope::Unit),
217            launch_span: Some(LaunchSpan::Units(cube)),
218            ..Level::spanning_launch(tile, order)
219        }
220    }
221
222    /// The order the launch's planes take this level's tiles in, where it is a plane level
223    /// spanning the launch.
224    pub fn plane_order(&self) -> Option<&PlaneOrder> {
225        match &self.launch_span {
226            Some(LaunchSpan::Planes(order)) => Some(order),
227            _ => None,
228        }
229    }
230
231    /// The cubes the launch's units are grouped in, where it is a unit level spanning the launch.
232    pub fn unit_cube(&self) -> Option<UnitCube> {
233        match self.launch_span {
234            Some(LaunchSpan::Units(cube)) => Some(cube),
235            _ => None,
236        }
237    }
238
239    /// The order the launch's planes or units take this level's tiles in, where it spans the
240    /// launch: one tile a unit, in row order, for units.
241    pub(crate) fn launch_order(&self) -> Option<PlaneOrder> {
242        match &self.launch_span {
243            Some(LaunchSpan::Planes(order)) => Some(order.clone()),
244            Some(LaunchSpan::Units(cube)) => Some(cube.order()),
245            None => None,
246        }
247    }
248
249    /// Whether this level hands its tiles to the planes or units of the whole launch.
250    pub fn spans_launch(&self) -> bool {
251        self.launch_span.is_some()
252    }
253
254    /// One tile of each of `axes` per cube, on the grid's `Z` dimension.
255    pub(crate) fn batching(mut self, axes: &[Axis]) -> Level {
256        assert!(
257            self.coverage == Coverage::Distribute(ComputeScope::Cube),
258            "Level::batching: batches ride cubes; this level's tiles are taken by {:?}",
259            self.coverage
260        );
261        for &axis in axes {
262            self.push(
263                axis,
264                Cut {
265                    tile: 1,
266                    count: Count::All,
267                    spread: Spread::Contiguous,
268                },
269            );
270            self.batches.push(axis);
271        }
272        self
273    }
274
275    /// Every cut's tiles read as one index, of which each of `n` workers takes a run.
276    pub(crate) fn sharing(mut self, n: usize) -> Level {
277        match self.coverage {
278            Coverage::Distribute(ComputeScope::Cube)
279            | Coverage::Distribute(ComputeScope::Plane) => {}
280            Coverage::Distribute(ComputeScope::PlaneGroup { .. }) => panic!(
281                "Level::sharing: a plane group's partials of one box are not merged yet; deal \
282                 each group a box of its own"
283            ),
284            Coverage::Distribute(ComputeScope::Unit) => panic!(
285                "Level::sharing: the plane's units combine in registers, which needs them in \
286                 lockstep, and units holding different shares never are"
287            ),
288            Coverage::Walk => panic!("Level::sharing: a walk has no workers to share its tiles"),
289        }
290        assert!(
291            self.shared_by.is_none(),
292            "Level::sharing: this level already cuts its tiles as one"
293        );
294        for axis in self.axes() {
295            let cut = self.cuts.get(axis);
296            assert!(
297                cut.count == Count::All && cut.spread == Spread::Contiguous,
298                "Level::sharing: {axis:?} states a count or a spread of its own, which a share \
299                 of the whole has no use for"
300            );
301        }
302        self.shared_by = Some(n);
303        self
304    }
305
306    /// The stages of this walk are filled by `n` extra planes at the end of the cube.
307    pub(crate) fn filling(mut self, n: usize) -> Level {
308        assert!(
309            self.coverage == Coverage::Walk,
310            "Level::filling: only a walk's regions are staged, and this level's tiles are taken \
311             by {:?}",
312            self.coverage
313        );
314        self.fillers = n;
315        self
316    }
317
318    /// The order this cube level distributes its boxes to the grid.
319    pub(crate) fn distributed_in(mut self, order: CubeOrder) -> Level {
320        assert!(
321            self.coverage == Coverage::Distribute(ComputeScope::Cube),
322            "Level::distributed_in: only a cube level is distributed to a grid, and this level's tiles are \
323             taken by {:?}",
324            self.coverage
325        );
326        self.order = order.canonicalize();
327        self
328    }
329
330    /// This level handing out the tiles of each of `axes` from the last to the first, on top of
331    /// whatever order it deals them in: the first workers launched take the last tiles.
332    pub(crate) fn reversed(mut self, axes: &[Axis]) -> Level {
333        assert!(
334            self.coverage == Coverage::Distribute(ComputeScope::Cube) || self.spans_launch(),
335            "Level::reversed: only a cube level or a level spanning the launch hands its tiles out \
336             in a launch order, and this level's tiles are taken by {:?}",
337            self.coverage
338        );
339        assert!(
340            self.shared_by.is_none(),
341            "Level::reversed: this level cuts its tiles as one index, which has no axis to reverse"
342        );
343        for &axis in axes {
344            assert!(
345                self.cuts.contains(axis),
346                "Level::reversed: {axis:?} is not an axis of this level"
347            );
348            assert!(
349                !self.reversed.contains(&axis),
350                "Level::reversed: {axis:?} is reversed twice"
351            );
352            self.reversed.push(axis);
353        }
354        self
355    }
356
357    fn push(&mut self, axis: Axis, cut: Cut) {
358        assert!(
359            !self.cuts.contains(axis),
360            "Level: {axis:?} is named twice; a level states each of its axes once"
361        );
362        let mut cuts: Vec<_> = self
363            .axes()
364            .into_iter()
365            .map(|a| (a, self.cuts.get(a)))
366            .collect();
367        cuts.push((axis, cut));
368        self.cuts = AxisMap::new(&cuts);
369    }
370
371    /// Who takes this level's tiles.
372    pub fn coverage(&self) -> Coverage {
373        self.coverage
374    }
375
376    /// The axes this level names, in the order it named them.
377    pub fn axes(&self) -> Vec<Axis> {
378        (0..self.cuts.len()).map(|i| self.cuts.axis_at(i)).collect()
379    }
380
381    /// The tile this level steps `axis` in, where it names it.
382    pub fn tile(&self, axis: Axis) -> Option<usize> {
383        self.cuts.contains(axis).then(|| self.cuts.get(axis).tile)
384    }
385
386    /// How many tiles this level takes along `axis`, where it names it.
387    pub fn count(&self, axis: Axis) -> Option<Count> {
388        self.cuts.contains(axis).then(|| self.cuts.get(axis).count)
389    }
390
391    /// How the instances spread `axis`'s tiles, where the level names it.
392    pub fn spread(&self, axis: Axis) -> Option<Spread> {
393        self.cuts.contains(axis).then(|| self.cuts.get(axis).spread)
394    }
395
396    pub(crate) fn cut(&self, axis: Axis) -> Cut {
397        self.cuts.get(axis)
398    }
399
400    /// Whether this level distributes `axis`'s tiles over its scope one by one.
401    pub fn distributes(&self, axis: Axis) -> bool {
402        self.coverage != Coverage::Walk && self.shared_by.is_none() && self.cuts.contains(axis)
403    }
404
405    /// The grid dimension `axis` rides on a cube level: batches on `Z`, the others `X`, `Y`, `Z`
406    /// in the order named.
407    pub(crate) fn cube_axis(&self, axis: Axis) -> Option<CubeAxis> {
408        if self.coverage != Coverage::Distribute(ComputeScope::Cube) || !self.cuts.contains(axis) {
409            return None;
410        }
411        if self.batches.contains(&axis) {
412            return Some(CubeAxis::Z);
413        }
414        let dims = [CubeAxis::X, CubeAxis::Y, CubeAxis::Z];
415        let position = self
416            .axes()
417            .into_iter()
418            .filter(|a| !self.batches.contains(a))
419            .position(|a| a == axis)
420            .expect("the axis is named and not a batch");
421        Some(dims[position])
422    }
423
424    /// The workers taking this level's grid as one flat index.
425    pub fn shared_by(&self) -> Option<usize> {
426        self.shared_by
427    }
428
429    /// Planes that fill this level's stages.
430    pub fn fillers(&self) -> usize {
431        self.fillers
432    }
433
434    /// The order this level distributes its boxes to the grid.
435    pub fn order(&self) -> CubeOrder {
436        self.order
437    }
438
439    /// Whether this level hands out `axis`'s tiles from the last to the first.
440    pub fn is_reversed(&self, axis: Axis) -> bool {
441        self.reversed.contains(&axis)
442    }
443
444    /// The extent one region of this level covers along `axis` of `space`.
445    pub(crate) fn extent_in(&self, space: &Space, axis: Axis) -> Extent {
446        match self.tile(axis) {
447            Some(tile) => Extent::Static(tile),
448            None => space.extent_raw(axis),
449        }
450    }
451
452    /// The space one region of this level covers.
453    pub fn child(&self, space: &Space) -> Space {
454        Space::from_extents(
455            &space
456                .axes()
457                .map(|axis| (axis, self.extent_in(space, axis)))
458                .collect::<Vec<_>>(),
459        )
460    }
461
462    /// Whether this level's tile on `axis` fails to divide the extent `space` hands it.
463    /// Static extents only.
464    pub(crate) fn overhangs(&self, space: &Space, axis: Axis) -> bool {
465        let Some(tile) = self.tile(axis) else {
466            return false;
467        };
468        let cut = !space.extent(axis).is_multiple_of(tile);
469        match self.launch_order() {
470            None => cut,
471            Some(order) => cut || order.overruns(self, space, axis),
472        }
473    }
474
475    /// Tiles along `axis` of `space` at this level, a partial tile counted; static extents only.
476    pub(crate) fn tiles(&self, space: &Space, axis: Axis) -> usize {
477        match self.count(axis) {
478            None => 1,
479            Some(count) => count.tiles(space.extent(axis), self.cut(axis).tile),
480        }
481    }
482
483    /// Tiles along `axis` where the count is comptime.
484    pub(crate) fn tiles_const(&self, space: &Space, axis: Axis) -> Option<usize> {
485        match self.count(axis) {
486            None => Some(1),
487            Some(count) => count.tiles_const(space.extent_raw(axis), self.cut(axis).tile),
488        }
489    }
490
491    /// What a walk counts along `axis`.
492    pub(crate) fn grid(&self, axis: Axis) -> GridCount {
493        match self.count(axis) {
494            None => GridCount::Const(1),
495            Some(count) => count.grid(self.cut(axis).tile),
496        }
497    }
498
499    /// How many workers `axis` is distributed out to at this level, where comptime.
500    pub(crate) fn instances_along(&self, space: &Space, axis: Axis) -> Option<usize> {
501        match self.count(axis) {
502            None => Some(1),
503            Some(Count::Distributed(_)) => None,
504            Some(count) => match count.stated() {
505                Some(n) => Some(n),
506                None if !space.contains(axis) => None,
507                None => self.tiles_const(space, axis),
508            },
509        }
510    }
511}