Skip to main content

cubecl_tile/space/walk/
base.rs

1//! The [`Walk`]: the regions one [`Level`] of a [`Space`] hands the instance running the code.
2
3use cubecl::prelude::*;
4use cubecl::unexpanded;
5
6use crate::dialect::LevelOp;
7use crate::{
8    Axis, ComputeScope, Coords, Integer, IntegerExpand, Level, Planes, Region, RegionExpand, Space,
9    StageOwner,
10};
11
12use super::distribution::{AxisDistribution, Distribution};
13use crate::Spread;
14use crate::space::partition::{GridCount, in_plane_axes, launch_tiles, swizzled_positions};
15
16/// The runtime odometer over a [`Space`]'s tiles under one [`Level`].
17#[derive(CubeType)]
18pub struct Walk {
19    /// The level's tiles along each axis, which a reversed axis counts back from.
20    tiles: Coords<usize>,
21    counts: Coords<usize>,
22    positions: Coords<usize>,
23    scales: Coords<usize>,
24    /// Per-axis routed coordinate ([`routed`](Walk::routed)), `0` elsewhere.
25    route: Coords<u32>,
26    /// Which axes [`route`](Self::route) speaks for.
27    #[cube(comptime)]
28    routed_at: Vec<usize>,
29    base: usize,
30    steps: usize,
31    pub(crate) parent: Region,
32    #[cube(comptime)]
33    pub(crate) space: Space,
34    #[cube(comptime)]
35    pub(crate) level: Level,
36    /// How the level distributes each axis of the space, in axis order.
37    #[cube(comptime)]
38    distributes: Vec<AxisDistribution>,
39    /// Whether iterating this walk unrolls.
40    #[cube(comptime)]
41    pub(crate) unroll: bool,
42    /// The axis this walk steps a run along ([`Region::walk_within`]), where it steps nothing
43    /// else and in order: every region but the last then lies inside the run.
44    #[cube(comptime)]
45    pub(crate) run: Option<Axis>,
46}
47
48#[cube]
49impl Walk {
50    // The loops are `#[unroll]`ed cube loops over a comptime `Vec`, not host iteration.
51    #[allow(clippy::needless_range_loop)]
52    pub(crate) fn of(space: &Space, #[comptime] level: Level, parent: Region) -> Walk {
53        let host = comptime!(space.clone());
54        let rank = comptime!(host.rank());
55        // A swizzled cube level decodes its two in-plane axes jointly from the flat dispatch index.
56        let in_plane = comptime!(
57            level
58                .order()
59                .swizzles()
60                .then(|| in_plane_axes(&host, &level))
61        );
62        let distributes = comptime!(
63            (0..rank)
64                .map(|p| AxisDistribution::new(&level, &host, p, in_plane))
65                .collect::<Vec<_>>()
66        );
67
68        let mut grid = Coords::<usize>::new();
69        #[unroll]
70        for p in 0..rank {
71            match comptime!(level.grid(space.axis_at(p))) {
72                GridCount::Const(n) => grid.push(n.runtime()),
73                GridCount::Extent(tile) => grid.push(space.count(p, tile)),
74            }
75        }
76
77        // A level distributed over a scope reaches the IR as one `tile.level` op.
78        let (counts, positions, scales) = match comptime!(level.coverage().scope()) {
79            Some(_) => intrinsic!(|scope| LevelOp::trace(scope, |scope| {
80                Walk::__expand_tiles_of(
81                    scope,
82                    host.clone(),
83                    level.clone(),
84                    distributes.clone(),
85                    &grid,
86                )
87            })),
88            None => Walk::tiles_of(
89                comptime!(host.clone()),
90                comptime!(level.clone()),
91                comptime!(distributes.clone()),
92                &grid,
93            ),
94        };
95
96        // Computed whole, not accumulated, so a static walk can unroll.
97        let steps = counts.product(comptime!((0..rank).collect::<Vec<_>>()));
98
99        Walk {
100            tiles: grid,
101            counts,
102            positions,
103            scales,
104            route: Coords::constant(comptime!(vec![0; rank])),
105            routed_at: comptime!(Vec::new()),
106            base: 0usize,
107            steps,
108            parent,
109            distributes,
110            space: host,
111            level,
112            unroll: comptime!(false),
113            run: comptime!(None),
114        }
115    }
116
117    /// `(counts, positions, scales)` of this instance at `level`.
118    fn tiles_of(
119        #[comptime] host: Space,
120        #[comptime] level: Level,
121        #[comptime] distributes: Vec<AxisDistribution>,
122        grid: &Coords<usize>,
123    ) -> (Coords<usize>, Coords<usize>, Coords<usize>) {
124        match comptime!(level.spans_launch()) {
125            true => Walk::spanning_launch(host, level, grid),
126            false => Walk::dealt(host, level, distributes, grid),
127        }
128    }
129
130    /// `(counts, positions, scales)` of a level spanning the launch: along each of its axes, the
131    /// one tile this plane's or unit's place in the launch names, past the last for one past the
132    /// last tile, which the binding masks; every tile of the others.
133    fn spanning_launch(
134        #[comptime] host: Space,
135        #[comptime] level: Level,
136        grid: &Coords<usize>,
137    ) -> (Coords<usize>, Coords<usize>, Coords<usize>) {
138        let (positions, counts) = launch_tiles(comptime!(host.clone()), level, grid);
139        let mut scales = Coords::<usize>::new();
140        #[unroll]
141        for _ in 0..comptime!(host.rank()) {
142            scales.push(1usize);
143        }
144        (counts, positions, scales)
145    }
146
147    /// `(counts, positions, scales)` of a level dealt to the instances of one cube, or of the
148    /// grid's cubes, each axis as `distributes` says.
149    #[allow(clippy::needless_range_loop)]
150    fn dealt(
151        #[comptime] host: Space,
152        #[comptime] level: Level,
153        #[comptime] distributes: Vec<AxisDistribution>,
154        grid: &Coords<usize>,
155    ) -> (Coords<usize>, Coords<usize>, Coords<usize>) {
156        let rank = comptime!(host.rank());
157        // Computed whole, so a constant grid's decode stays constant too.
158        let mut instances = Coords::<usize>::new();
159        #[unroll]
160        for p in 0..rank {
161            match comptime!(distributes[p].clone()) {
162                AxisDistribution::Distributed(Distribution {
163                    across: Some(workers),
164                    ..
165                }) => instances.push(workers.runtime()),
166                // The plane's width is the launch's, so it is read, not compiled in.
167                AxisDistribution::Distributed(Distribution { in_turns: true, .. }) => {
168                    instances.push(CUBE_DIM_X as usize)
169                }
170                AxisDistribution::Distributed(_) => instances.push(grid.at(p)),
171                AxisDistribution::Walked => instances.push(1usize),
172            }
173        }
174        let swizzled = swizzled_positions(
175            comptime!(host.clone()),
176            comptime!(level.clone()),
177            &instances,
178        );
179
180        let mut counts = Coords::<usize>::new();
181        let mut positions = Coords::<usize>::new();
182        let mut scales = Coords::<usize>::new();
183        #[unroll]
184        for p in 0..rank {
185            match comptime!(distributes[p].clone()) {
186                AxisDistribution::Walked => {
187                    counts.push(grid.at(p));
188                    positions.push(0usize);
189                    scales.push(1usize);
190                }
191                AxisDistribution::Distributed(Distribution {
192                    scope: compute_scope,
193                    dim,
194                    spread,
195                    across,
196                    in_turns: _,
197                    divides,
198                    inner,
199                    unspanned,
200                    swizzled: joint,
201                }) => {
202                    // Mixed-radix stride over the later same-dimension axes, spanned or not.
203                    let inner_weight = instances.product(inner) * comptime!(unspanned).runtime();
204                    let position = match comptime!(joint) {
205                        Some(i) => swizzled.at(i),
206                        None => AxisDistribution::hardware(compute_scope, dim)
207                            .divided_by(inner_weight)
208                            .remainder(instances.at(p)),
209                    };
210                    let run = match comptime!(across) {
211                        Some(workers) => grid
212                            .at(p)
213                            .plus(comptime!(workers - 1).runtime())
214                            .divided_by(workers.runtime()),
215                        None => 1usize.runtime(),
216                    };
217                    counts.push(AxisDistribution::tiles(
218                        grid.at(p),
219                        position,
220                        instances.at(p),
221                        run,
222                        spread,
223                        divides,
224                    ));
225                    positions.push(position);
226                    match comptime!(spread) {
227                        Spread::Contiguous => scales.push(run),
228                        Spread::Interleaved => scales.push(instances.at(p)),
229                    }
230                }
231            }
232        }
233
234        (counts, positions, scales)
235    }
236
237    /// This walk taking one step along `axis`, at the coordinate `coord`.
238    /// The caller must keep `coord` within the axis's extent; nothing checks it.
239    pub fn routed(self, #[comptime] axis: Axis, coord: usize) -> Walk {
240        let rank = comptime!(self.space.rank());
241        let at = comptime!(self.space.position(axis));
242        comptime!(assert!(
243            self.space.contains(axis),
244            "Walk::routed: {axis:?} is not an axis of this walk's space, so it has no \
245             coordinate to state"
246        ));
247
248        let mut counts = Coords::<usize>::new();
249        let mut route = Coords::<u32>::new();
250        #[unroll]
251        for p in 0..rank {
252            // Clamped: a data-derived coordinate may be out of range, and a panic here goes unseen.
253            if comptime!(p == at) {
254                // The axis's own tiles, not `counts`, which is this instance's share.
255                let last = comptime!(self.level.tiles(&self.space, axis) - 1).runtime();
256                counts.push(1usize);
257                route.push(coord.min_with(last).cast::<u32>());
258            } else {
259                counts.push(self.counts.at(p));
260                route.push(self.route.at(p));
261            }
262        }
263        let steps = counts.product(comptime!((0..rank).collect::<Vec<_>>()));
264
265        Walk {
266            tiles: self.tiles,
267            counts,
268            positions: self.positions,
269            scales: self.scales,
270            route,
271            routed_at: comptime!({
272                let mut routed_at = self.routed_at.clone();
273                routed_at.push(at);
274                routed_at
275            }),
276            base: self.base,
277            steps,
278            parent: self.parent,
279            distributes: comptime!(self.distributes.clone()),
280            space: comptime!(self.space.clone()),
281            level: comptime!(self.level.clone()),
282            unroll: comptime!(self.unroll),
283            run: comptime!(self.run),
284        }
285    }
286
287    /// Who the stages filled along this walk belong to ([`StageOwner`]): each plane, where the
288    /// walk sits under a level handing every plane a region of its own, or hands planes their own
289    /// regions itself; the cube otherwise.
290    pub(crate) fn stage_owner(&self) -> comptime_type!(StageOwner) {
291        comptime!(StageOwner::new(
292            self.parent
293                .path
294                .scope()
295                .min(self.level.coverage().scope().unwrap_or(ComputeScope::Cube)),
296            self.parent.path.planes_per_cube(),
297            self.level.fillers(),
298        ))
299    }
300
301    /// The region count.
302    pub fn total(&self) -> usize {
303        self.steps
304    }
305
306    /// This instance's position among those the level distributes axis `p` to, `0` for a walked
307    /// axis.
308    pub(crate) fn position_at(&self, #[comptime] p: usize) -> usize {
309        self.positions.at(p)
310    }
311
312    /// This instance's place among the workers the level deals `axis` to, `0` for a walked axis:
313    /// which plane of those taking `axis` in runs, where the tiles a worker walks no longer name
314    /// it.
315    pub fn position(&self, #[comptime] axis: Axis) -> usize {
316        self.positions.at(comptime!(self.space.position(axis)))
317    }
318
319    /// The `i`-th region of the walk.
320    pub fn region(&self, i: usize) -> Region {
321        let idx = self.base.plus(i);
322        self.parent
323            .below(self.resolve(idx), comptime!(self.level.clone()))
324    }
325
326    /// The planes below this walk's regions, each once, at its first step ([`Region::planes`]):
327    /// for a walk with no single region above it, such as a run of interleaved steps.
328    pub fn planes(&self) -> Planes {
329        self.region(0).planes()
330    }
331
332    /// Unravel a runtime step `idx` to its per-axis coordinates.
333    fn resolve(&self, idx: usize) -> Coords<u32> {
334        let mut coords = Coords::<u32>::new();
335
336        #[unroll]
337        for p in 0..comptime!(self.space.rank()) {
338            if comptime!(self.routed_at.contains(&p)) {
339                coords.push(self.route.at(p));
340            } else {
341                coords.push(self.tile_coord(self.step_coord(idx, p), p).cast::<u32>());
342            }
343        }
344        coords
345    }
346
347    /// The coordinate of step `idx` along axis `p` (last axis fastest).
348    fn step_coord(&self, idx: usize, #[comptime] p: usize) -> usize {
349        let rank = comptime!(self.space.rank());
350        let quot = idx.divided_by(
351            self.counts
352                .product(comptime!(((p + 1)..rank).collect::<Vec<_>>())),
353        );
354        // `% count` is skipped when `idx` has no more significant coordinate, which constant
355        // evaluation cannot know; except for a count of one, where `% 1` is `0`.
356        let count = self.counts.at(p);
357        let one = count.constant();
358        let earlier = self
359            .counts
360            .product(comptime!((0..p).collect::<Vec<_>>()))
361            .constant();
362        if comptime!(one != Some(1) && earlier == Some(1)) {
363            quot
364        } else {
365            quot.remainder(count)
366        }
367    }
368
369    /// The tile axis `p`'s step coordinate `coord` reaches, with the instance's position added per
370    /// the [`Spread`], counted from the
371    /// last tile where the level hands the axis out [`reversed`](crate::Levels::reversed).
372    fn tile_coord(&self, coord: usize, #[comptime] p: usize) -> usize {
373        let tile = match comptime!(self.distributes[p].clone()) {
374            AxisDistribution::Walked => coord,
375            AxisDistribution::Distributed(Distribution {
376                spread: Spread::Contiguous,
377                ..
378            }) => coord.plus(self.positions.at(p).times(self.scales.at(p))),
379            AxisDistribution::Distributed(Distribution {
380                spread: Spread::Interleaved,
381                ..
382            }) => coord.times(self.scales.at(p)).plus(self.positions.at(p)),
383        };
384        let tiles = self.tiles.at(p);
385        // A plane past the last tile stays past it, where the binding masks it.
386        if comptime!(self.level.is_reversed(self.space.axis_at(p))) && tile < tiles {
387            tiles.minus(1usize).minus(tile)
388        } else {
389            tile
390        }
391    }
392}
393
394/// `for region in walk` visits the regions in order.
395impl IntoIterator for Walk {
396    type Item = Region;
397    type IntoIter = std::vec::IntoIter<Region>;
398
399    fn into_iter(self) -> Self::IntoIter {
400        let mut regions = Vec::new();
401        for i in 0..self.total() {
402            regions.push(self.region(i));
403        }
404        regions.into_iter()
405    }
406}
407
408impl Iterable for WalkExpand {
409    type Item = RegionExpand;
410
411    fn expand(self, scope: &Scope, body: &mut dyn FnMut(&Scope, RegionExpand)) {
412        let start = 0usize.into_expand(scope);
413        let total = self.__expand_total_method(scope);
414        let range = RangeExpand::new(start, total);
415        if self.unroll {
416            range.expand_unroll(scope, &mut |scope, i| {
417                body(scope, self.__expand_region_method(scope, i));
418            });
419        } else {
420            range.expand(scope, &mut |scope, i| {
421                body(scope, self.__expand_region_method(scope, i));
422            });
423        }
424    }
425
426    fn expand_unroll(self, scope: &Scope, body: &mut dyn FnMut(&Scope, RegionExpand)) {
427        let start = 0usize.into_expand(scope);
428        let total = self.__expand_total_method(scope);
429        RangeExpand::new(start, total).expand_unroll(scope, &mut |scope, i| {
430            body(scope, self.__expand_region_method(scope, i));
431        });
432    }
433
434    /// The region count when it is a constant, so a single region needs no loop.
435    fn const_len(&self) -> Option<usize> {
436        crate::algebra::constant(&self.steps).map(|n| n as usize)
437    }
438}
439
440/// The settings a walk is told after it is built.
441impl Walk {
442    /// This walk, unrolled when iterated (static spaces only).
443    pub fn unrolled(self) -> Walk {
444        unexpanded!()
445    }
446
447    /// This walk over the `steps` regions starting at flat step `base`.
448    /// The caller must keep `base + steps` within [`total`](Walk::total); nothing checks it.
449    pub fn range(self, _base: usize, _steps: usize) -> Walk {
450        unexpanded!()
451    }
452
453    /// The depth of the regions this walk hands out.
454    pub(crate) fn depth(&self) -> usize {
455        self.parent.path.depth() + 1
456    }
457}
458
459impl WalkExpand {
460    pub fn __expand_unrolled_method(mut self, _scope: &Scope) -> Self {
461        self.unroll = true;
462        self
463    }
464
465    pub fn __expand_range_method(
466        mut self,
467        _scope: &Scope,
468        base: NativeExpand<usize>,
469        steps: NativeExpand<usize>,
470    ) -> Self {
471        self.base = base;
472        self.steps = steps;
473        self
474    }
475
476    pub(crate) fn depth(&self) -> usize {
477        self.parent.path.depth() + 1
478    }
479}