Skip to main content

cubecl_tile/space/partition/
plane_order.rs

1//! The order a plane level spanning the launch hands its tiles to the launch's planes
2//! ([`PlaneOrder`]), and the cubes a unit level spanning the launch groups its units in
3//! ([`UnitCube`]).
4
5use cubecl::prelude::*;
6
7use super::{ComputeScope, CubeOrder, Level, cube_positions};
8use crate::{Axis, Coords, CubeAxis, Integer, IntegerExpand, Space};
9
10/// The most cubes one grid dimension takes on every device a launch may run on.
11const CUBES_PER_GRID_DIMENSION: usize = 65_535;
12
13/// The order the planes of a launch take a level's tiles in, where a plane level spans the launch
14/// rather than one cube ([`Levels::launch_planes`](crate::Levels::launch_planes)): in blocks, each
15/// a box of tiles held by consecutive planes, the blocks taken in a [`CubeOrder`] over their grid.
16///
17/// A plane of the launch is numbered by its cube and its place in it, `cube ยท planes_per_cube +
18/// UNIT_POS_Y`, the cube read off the grid's `x` and `y`. Where a block holds a cube's planes,
19/// as it does unless [`cubes_of`](PlaneOrder::cubes_of) says otherwise, each cube takes one block,
20/// and the launch deals its tiles as a cube level with a plane level inside it would. Any order
21/// is right: the planes share nothing, so the order only says which tiles the planes of one cube
22/// take together.
23///
24/// Every plane the launch spawns walks one tile, as every plane of a cube level's cube does: a
25/// plane past the last tile, of a block the edge cuts short or of a cube past the last block,
26/// walks one past the grid, which the binding masks (`PlaneOrder::overruns`). So the
27/// planes of a cube all reach any barrier one of them meets.
28#[derive(Clone, PartialEq, Eq, Hash, Debug)]
29pub struct PlaneOrder {
30    /// Tiles one block holds along each axis it names; along an axis it does not name, one.
31    block: Vec<(Axis, usize)>,
32    block_order: CubeOrder,
33    /// The launch's `y`.
34    planes_per_cube: usize,
35}
36
37impl PlaneOrder {
38    /// Blocks of `block` tiles, one tile a plane, taken in `block_order`, one block a cube: what a
39    /// cube level holding a plane level of `block` hands out.
40    pub fn new(block: &[(Axis, usize)], block_order: CubeOrder) -> Self {
41        for &(axis, tiles) in block {
42            assert!(
43                tiles > 0,
44                "PlaneOrder: a block holds no tile along {axis:?}"
45            );
46        }
47        let block = block.to_vec();
48        let planes_per_cube = block.iter().map(|&(_, tiles)| tiles).product();
49        Self {
50            block,
51            block_order: block_order.canonicalize(),
52            planes_per_cube,
53        }
54    }
55
56    /// This order with the launch's cubes holding `planes` planes each, which need not be a
57    /// block's: a block then spans several cubes, or a cube several blocks.
58    pub fn cubes_of(self, planes: usize) -> Self {
59        assert!(
60            planes > 0,
61            "PlaneOrder::cubes_of: a cube holds at least one plane"
62        );
63        Self {
64            planes_per_cube: planes,
65            ..self
66        }
67    }
68
69    pub fn planes_per_cube(&self) -> usize {
70        self.planes_per_cube
71    }
72
73    /// Tiles one block holds along `axis`.
74    pub fn block_along(&self, axis: Axis) -> usize {
75        self.block
76            .iter()
77            .find(|&&(a, _)| a == axis)
78            .map_or(1, |&(_, tiles)| tiles)
79    }
80
81    pub fn block_order(&self) -> CubeOrder {
82        self.block_order
83    }
84
85    pub(crate) fn block_axes(&self) -> impl Iterator<Item = Axis> + '_ {
86        self.block.iter().map(|&(axis, _)| axis)
87    }
88
89    /// Planes one block holds, a tile each.
90    pub(crate) fn block_planes(&self) -> usize {
91        self.block.iter().map(|&(_, tiles)| tiles).product()
92    }
93
94    /// Whether each cube takes one block.
95    pub(crate) fn one_block_a_cube(&self) -> bool {
96        self.block_planes() == self.planes_per_cube
97    }
98
99    /// The grid's `x` and `y` for `level`'s blocks over `space`: every block's planes in cubes of
100    /// [`planes_per_cube`](Self::planes_per_cube), laid along `x` and spilling into `y` past what
101    /// one dimension takes. At least one cube, so an empty problem divides by no empty dimension.
102    /// Static extents only.
103    ///
104    /// A spill lays the cubes out exactly where it can: `x` is the widest count one dimension
105    /// takes that divides the cubes, so no cube runs past the last block. Only a count with no
106    /// such divisor rounds `y` up, and leaves the spare cubes [`overruns`](Self::overruns) masks.
107    pub(crate) fn grid(&self, level: &Level, space: &Space) -> (usize, usize) {
108        let planes = self.blocks(level, space) * self.block_planes();
109        let cubes = planes.div_ceil(self.planes_per_cube).max(1);
110        if cubes <= CUBES_PER_GRID_DIMENSION {
111            return (cubes, 1);
112        }
113        let rows = cubes.div_ceil(CUBES_PER_GRID_DIMENSION);
114        let x = (rows..=CUBES_PER_GRID_DIMENSION)
115            .find(|&rows| cubes.is_multiple_of(rows) && cubes / rows <= CUBES_PER_GRID_DIMENSION)
116            .map_or(CUBES_PER_GRID_DIMENSION, |rows| cubes / rows);
117        (x, cubes.div_ceil(x))
118    }
119
120    /// Whether some plane the launch spawns for `level` over `space` walks a tile past the last
121    /// along `axis`, so the binding masks it there: where the edge cuts a block short along
122    /// `axis`, or where the grid holds more planes than the blocks do and the blocks past the last
123    /// land past `axis` ([`block_coords`]). `axis` is static; where an extent the grid counts is
124    /// only the launch's, the planes may run past it, and it is masked.
125    pub(crate) fn overruns(&self, level: &Level, space: &Space, axis: Axis) -> bool {
126        let ragged = !level
127            .tiles(space, axis)
128            .is_multiple_of(self.block_along(axis));
129        if ragged {
130            return true;
131        }
132        if Self::spare_blocks_land_past(level, self.block_order) != axis {
133            return false;
134        }
135        if level.axes().into_iter().any(|axis| space.is_dynamic(axis)) {
136            return true;
137        }
138        let (x, y) = self.grid(level, space);
139        x * y * self.planes_per_cube > self.blocks(level, space) * self.block_planes()
140    }
141
142    /// Blocks `level` holds over `space`, the edge's counted.
143    fn blocks(&self, level: &Level, space: &Space) -> usize {
144        level
145            .axes()
146            .into_iter()
147            .map(|axis| level.tiles(space, axis).div_ceil(self.block_along(axis)))
148            .product()
149    }
150
151    /// The axis a block index past the last lands past ([`block_coords`]): the one axis; of two,
152    /// the one `order` runs out along; of more, the outermost, which is never wrapped.
153    fn spare_blocks_land_past(level: &Level, order: CubeOrder) -> Axis {
154        let axes = level.axes();
155        match axes.len() {
156            1 => axes[0],
157            2 => match order.canonicalize() {
158                CubeOrder::SwizzleRow(_) => axes[0],
159                CubeOrder::RowMajor | CubeOrder::SwizzleCol(_) => axes[1],
160            },
161            _ => axes[axes.len() - 1],
162        }
163    }
164}
165
166/// The cubes a unit level spanning the launch groups its units in
167/// ([`Levels::launch_units`](crate::Levels::launch_units)): `planes` planes of `plane_width` units
168/// each. The units share nothing, so the cube is only how the launch groups them.
169#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
170pub struct UnitCube {
171    pub plane_width: usize,
172    pub planes: usize,
173}
174
175impl UnitCube {
176    /// Units one cube holds.
177    pub fn units(&self) -> usize {
178        self.plane_width * self.planes
179    }
180
181    /// The order the launch's units take a level's tiles in: one tile a unit, the tiles in row
182    /// order, [`units`](Self::units) a cube.
183    pub(crate) fn order(&self) -> PlaneOrder {
184        PlaneOrder::new(&[], CubeOrder::RowMajor).cubes_of(self.units())
185    }
186}
187
188/// This plane's or unit's tile of `level`, a level spanning the launch, along each axis of
189/// `space`: `(positions, counts)`: along each of the level's axes the tile's index and `1`, an
190/// index past the last for an instance past the last tile; along each other axis `0` and every
191/// tile. `grid` is the tiles along each axis.
192#[cube]
193pub(crate) fn launch_tiles(
194    #[comptime] space: Space,
195    #[comptime] level: Level,
196    grid: &Coords<usize>,
197) -> (Coords<usize>, Coords<usize>) {
198    let order = comptime!(
199        level
200            .launch_order()
201            .expect("a level spanning the launch states the order its instances take")
202    );
203    // An instance's place in its cube: a plane's row, or a unit's place among all of its units.
204    let in_cube = match comptime!(level.coverage().scope()) {
205        Some(ComputeScope::Unit) => UNIT_POS as usize,
206        _ => UNIT_POS_Y as usize,
207    };
208    let rank = comptime!(space.rank());
209    // The level's axes in the order it names them: the first rides fastest across the blocks, as
210    // a cube level's `X` does.
211    let named = comptime!(
212        level
213            .axes()
214            .into_iter()
215            .map(|axis| space.position(axis))
216            .collect::<Vec<_>>()
217    );
218    // The grid's `x` and `y`, as `Partitioning::cube_count` lays the cubes out:
219    // its `z` is always one.
220    let cube = CubeAxis::position(CubeAxis::X)
221        .plus(CubeAxis::position(CubeAxis::Y).times(CUBE_COUNT_X as usize));
222    let (block, local) = if comptime!(order.one_block_a_cube()) {
223        (cube, in_cube)
224    } else {
225        let flat = cube
226            .times(comptime!(order.planes_per_cube()).runtime())
227            .plus(in_cube);
228        let planes = comptime!(order.block_planes()).runtime();
229        (flat.divided_by(planes), flat.remainder(planes))
230    };
231
232    // Blocks along each named axis, the last one ragged.
233    let mut blocks = Coords::<usize>::new();
234    #[unroll]
235    for p in comptime!(named.clone()) {
236        let along = comptime!(order.block_along(space.axis_at(p)));
237        blocks.push(
238            grid.at(p)
239                .plus(comptime!(along - 1).runtime())
240                .divided_by(along.runtime()),
241        );
242    }
243    let at = block_coords(
244        block,
245        &blocks,
246        comptime!(named.len()),
247        comptime!(order.block_order()),
248    );
249
250    let mut positions = Coords::<usize>::new();
251    let mut counts = Coords::<usize>::new();
252    #[unroll]
253    for p in 0..rank {
254        match comptime!(named.iter().position(|&q| q == p)) {
255            None => {
256                positions.push(0usize);
257                counts.push(grid.at(p));
258            }
259            Some(i) => {
260                let along = comptime!(order.block_along(space.axis_at(p)));
261                // The last named axis rides fastest within a block, as a plane level's does.
262                let inner = comptime!(
263                    named[i + 1..]
264                        .iter()
265                        .map(|&q| order.block_along(space.axis_at(q)))
266                        .product::<usize>()
267                );
268                let within = local.divided_by(inner.runtime()).remainder(along.runtime());
269                let tile = at.at(i).times(along.runtime()).plus(within);
270                positions.push(tile);
271                counts.push(1usize);
272            }
273        }
274    }
275    (positions, counts)
276}
277
278/// Block `block`'s coordinates over `blocks`, `named` axes of them: the first two in `order`, the
279/// others outermost, first fastest, the last never wrapped, so a block past the grid lands off it.
280#[cube]
281fn block_coords(
282    block: usize,
283    blocks: &Coords<usize>,
284    #[comptime] named: usize,
285    #[comptime] order: CubeOrder,
286) -> Coords<usize> {
287    let mut out = Coords::<usize>::new();
288    if comptime!(named == 1) {
289        out.push(block);
290    } else {
291        let plane = blocks.at(0).times(blocks.at(1));
292        let first_two = if comptime!(named == 2) {
293            block
294        } else {
295            block.remainder(plane)
296        };
297        let (x, y) = cube_positions(first_two, (blocks.at(0), blocks.at(1)), order);
298        out.push(x);
299        out.push(y);
300        let mut rest = block.divided_by(plane);
301        #[unroll]
302        for i in 2..named {
303            if comptime!(i + 1 == named) {
304                out.push(rest);
305            } else {
306                out.push(rest.remainder(blocks.at(i)));
307                rest = rest.divided_by(blocks.at(i));
308            }
309        }
310    }
311    out
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use crate::{Levels, Partitioning};
318
319    const N: Axis = Axis(0);
320
321    /// The grid's `x`, `y` and `z`.
322    fn counts(partitioning: &Partitioning) -> (u32, u32, u32) {
323        let CubeCount::Static(x, y, z) = partitioning.cube_count() else {
324            unreachable!("a static space lays a static grid");
325        };
326        (x, y, z)
327    }
328
329    /// One plane a tile of two along `N`, one plane a cube, over `columns`.
330    fn one_plane_a_cube(columns: usize) -> Partitioning {
331        Partitioning::new(
332            Space::new(&[(N, columns)]),
333            Levels::leaf(&[(N, 2)])
334                .launch_planes(&[N], PlaneOrder::new(&[(N, 1)], CubeOrder::RowMajor))
335                .build(),
336        )
337    }
338
339    /// More cubes than one dimension takes are laid out exactly where a count one dimension takes
340    /// divides them: a vocabulary of columns, two a plane, spills into a second row of the same
341    /// width, and no plane runs past the last tile. A count with no such divisor, a prime past
342    /// one dimension, still spills into a row it does not fill, and overruns it.
343    #[test]
344    fn a_spill_divides_the_cubes_where_it_can() {
345        let vocabulary = one_plane_a_cube(151_936);
346        assert_eq!(counts(&vocabulary), (37_984, 2, 1));
347        assert!(!vocabulary.overhangs(N));
348
349        let prime = one_plane_a_cube(2 * 65_537);
350        assert_eq!(counts(&prime), (CUBES_PER_GRID_DIMENSION as u32, 2, 1));
351        assert!(prime.overhangs(N));
352    }
353
354    /// A grid one dimension holds stays one row.
355    #[test]
356    fn a_grid_one_dimension_holds_is_one_row() {
357        assert_eq!(counts(&one_plane_a_cube(2 * 1000)), (1000, 1, 1));
358    }
359}