Skip to main content

cubecl_tile/tile/
slices.rs

1//! A tile cut into slices along one axis, where its holder keeps them ([`AxisSlices`]).
2
3use cubecl::prelude::*;
4
5use crate::*;
6
7/// Who keeps a tile's slices, and so how a slice is read whole.
8#[allow(clippy::large_enum_variant)]
9#[expect(
10    dead_code,
11    reason = "built through the expand type's generated constructors"
12)]
13#[derive(CubeType, Clone)]
14#[expand(derive(Clone))]
15pub(crate) enum AxisSlicesKind {
16    /// A register block a unit holds whole: a slice runs along its columns, in its registers.
17    Registers(RegisterData),
18    /// A window of shared memory a plane holds: its units take a slice's cells in turns and meet
19    /// on the slice's partials.
20    Window(Memory),
21    /// A plane's grid of fragments. A manual-mma fragment's cells are read in its registers, each
22    /// unit keeping the rows it holds cells of; a cmma fragment's lie where the instruction keeps
23    /// them, and are only scaled, through the plane's scratch.
24    Fragments(PlanePartition),
25    /// One fragment of a plane's, reached the same way.
26    Fragment(PlaneTile),
27}
28
29/// A tile cut into slices along `axis`, one for every index of its other axes, each read and
30/// written whole where the tile's holder keeps it: the verbs an op reducing or broadcasting along
31/// one axis runs. The slices write through to the tile.
32///
33/// `axis` is the tile's innermost, the one its cells are contiguous along; any other is refused
34/// by name.
35#[derive(CubeType)]
36pub struct AxisSlices {
37    kind: AxisSlicesKind,
38    #[cube(comptime)]
39    space: Space,
40    #[cube(comptime)]
41    axis: Axis,
42}
43
44#[cube]
45impl Tile {
46    /// This tile cut into slices along `axis`, where its holder keeps them: a unit's register
47    /// block, a plane's window of shared memory, or a plane's fragments. A window of memory any
48    /// wider holder shares has no one to read a slice whole, and is refused.
49    pub fn along(&self, #[comptime] axis: Axis) -> AxisSlices {
50        let holder = comptime!(self.place.holder());
51        let space = comptime!(self.place.space.clone());
52        comptime!(assert!(
53            space.axis_at(space.rank() - 1) == axis,
54            "Tile::along: a slice runs along the tile's innermost axis, and {axis:?} is not the \
55             innermost of {space:?}"
56        ));
57        let kind = match &self.kind {
58            TileKind::PlanePartition(partition) => partition.slices(),
59            TileKind::PlaneTile(tile) => tile.slices(),
60            TileKind::Memory(window) => {
61                comptime!(assert!(
62                    holder == ComputeScope::Plane,
63                    "Tile::along: a window of shared memory is read slice by slice by the plane \
64                     that holds it; this one is held by {holder:?}"
65                ));
66                AxisSlicesKind::new_Window(window.clone())
67            }
68            TileKind::TmaGmem(_) | TileKind::Procedural(_) | TileKind::Lines(_) => panic!(
69                "Tile::along: a register block, a plane's fragments, or a plane's window holds \
70                 slices"
71            ),
72        };
73        AxisSlices { kind, space, axis }
74    }
75}
76
77#[cube]
78impl Tile {
79    /// Slices along `axis` one unit keeps a state for: the rows a manual-mma grid's unit holds
80    /// cells of, which a reduction reads in its registers; every slice of any other holder.
81    pub fn slices_per_unit(&self, #[comptime] axis: Axis) -> comptime_type!(usize) {
82        let define!(Served) = self.element;
83        let every = comptime!(self.place.space.slices_along(axis));
84        match &self.kind {
85            TileKind::PlanePartition(partition) => match partition.at(0usize, 0usize) {
86                PlaneTile::Mma(fragment) => {
87                    let rows = fragment.rows_per_unit::<Served>();
88                    comptime!(partition.m_tiles * rows)
89                }
90                PlaneTile::Cmma(_) | PlaneTile::Registers(_) => comptime!(every),
91                PlaneTile::Wgmma(_) => panic!(
92                    "Tile::slices_per_unit: a warpgroup accumulator's slices are not read in \
93                     place; drain it first"
94                ),
95            },
96            TileKind::PlaneTile(tile) => match tile {
97                PlaneTile::Mma(fragment) => fragment.rows_per_unit::<Served>(),
98                PlaneTile::Cmma(_) | PlaneTile::Registers(_) => comptime!(every),
99                PlaneTile::Wgmma(_) => panic!(
100                    "Tile::slices_per_unit: a warpgroup accumulator's slices are not read in \
101                     place; drain it first"
102                ),
103            },
104            TileKind::Memory(_)
105            | TileKind::TmaGmem(_)
106            | TileKind::Procedural(_)
107            | TileKind::Lines(_) => comptime!(every),
108        }
109    }
110}
111
112#[cube]
113impl AxisSlices {
114    /// `self[i, s] = self[i, s] · scale + bias[i, s]`, the procedural `bias` read at each cell's
115    /// coordinates, windowed to the same region as the tile.
116    pub(crate) fn scale_add<E: Float>(&mut self, scale: E, bias: &Tile) {
117        let recipe = bias.recipe();
118        match &mut self.kind {
119            AxisSlicesKind::Registers(block) => {
120                block.scale_add_along::<E>(scale, recipe, self.space.clone())
121            }
122            AxisSlicesKind::Window(window) => {
123                window.scale_add_along::<E>(scale, recipe, self.space.clone(), self.axis)
124            }
125            AxisSlicesKind::Fragments(partition) => {
126                partition.scale_add_along::<E>(scale, recipe, self.space.clone())
127            }
128            AxisSlicesKind::Fragment(tile) => {
129                let mut fragment = tile.manual_fragment();
130                fragment.scale_add_along::<E>(scale, recipe, self.space.clone(), (0usize, 0usize))
131            }
132        }
133    }
134
135    /// Each slice's max, starting from `seed`'s.
136    pub(crate) fn maxima<E: Float>(&self, seed: &Array<E>) -> Array<E> {
137        match &self.kind {
138            AxisSlicesKind::Registers(block) => block.maxima_along::<E>(seed),
139            AxisSlicesKind::Window(window) => {
140                window.maxima_along::<E>(seed, self.space.clone(), self.axis)
141            }
142            AxisSlicesKind::Fragments(partition) => partition.maxima_along::<E>(seed),
143            AxisSlicesKind::Fragment(tile) => tile.manual_fragment().maxima_along(seed),
144        }
145    }
146
147    /// `self[i, s] = exp(self[i, s] − slices[i])` ([`exp_minus_cell`](Self::exp_minus_cell)).
148    pub(crate) fn exp_minus<E: Float>(&mut self, slices: &Array<E>) {
149        match &mut self.kind {
150            AxisSlicesKind::Registers(block) => block.exp_minus_along::<E>(slices),
151            AxisSlicesKind::Window(window) => {
152                window.exp_minus_along::<E>(slices, self.space.clone(), self.axis)
153            }
154            AxisSlicesKind::Fragments(partition) => partition.exp_minus_along::<E>(slices),
155            AxisSlicesKind::Fragment(tile) => {
156                let mut fragment = tile.manual_fragment();
157                fragment.exp_minus_along(slices, 0usize)
158            }
159        }
160    }
161
162    /// Each slice's sum.
163    pub(crate) fn sums<E: Float>(&self) -> Array<E> {
164        match &self.kind {
165            AxisSlicesKind::Registers(block) => block.sums_along::<E>(),
166            AxisSlicesKind::Window(window) => window.sums_along::<E>(self.space.clone(), self.axis),
167            AxisSlicesKind::Fragments(partition) => partition.sums_along::<E>(),
168            AxisSlicesKind::Fragment(tile) => tile.manual_fragment().sums_along(),
169        }
170    }
171
172    /// `self[i, s] *= factors[i]`. A plane's fragments bounce through its scratch, met on
173    /// `sync_plane`: every unit of the plane calls it.
174    pub fn mul<E: Float>(&mut self, factors: &Array<E>) {
175        match &mut self.kind {
176            AxisSlicesKind::Registers(block) => block.mul_along::<E>(factors, 0usize),
177            AxisSlicesKind::Window(window) => {
178                window.mul_along::<E>(factors, self.space.clone(), self.axis)
179            }
180            AxisSlicesKind::Fragments(partition) => partition.mul_along::<E>(factors),
181            AxisSlicesKind::Fragment(tile) => tile.mul_along(factors, 0usize),
182        }
183    }
184
185    /// `self[c] = exp(self[c] · scale + bias[c] − logsumexp[c])`: the probabilities of a softmax
186    /// whose logsumexp is known, read back from it instead of walking the score again.
187    /// `logsumexp` is a window of memory that omits the axis the softmax ran along, which it
188    /// broadcasts over: along the slices where that axis is the innermost, along each slice where
189    /// it is not, as a walk owned by the keys holds its scores keys first. Its other axes are this
190    /// tile's, or one wide. A cell whose logsumexp is the minimum value, every key of its softmax
191    /// masked, is left at zero.
192    pub fn probabilities<E: Float>(&mut self, scale: E, bias: &Tile, logsumexp: &Tile) {
193        let recipe = bias.recipe();
194        let broadcast = comptime!(Broadcast::new(&self.space, &logsumexp.place.space));
195        let values = logsumexp.cells::<E>(comptime!(logsumexp.place.space.cells()));
196        match &mut self.kind {
197            AxisSlicesKind::Registers(block) => block.probabilities_along::<E>(
198                scale,
199                recipe,
200                &values,
201                self.space.clone(),
202                broadcast,
203            ),
204            AxisSlicesKind::Window(window) => window.probabilities_along::<E>(
205                scale,
206                recipe,
207                &values,
208                self.space.clone(),
209                broadcast,
210            ),
211            AxisSlicesKind::Fragments(_) | AxisSlicesKind::Fragment(_) => {
212                AxisSlices::refuse_fragments("AxisSlices::probabilities")
213            }
214        }
215    }
216
217    /// `self[c] = probabilities[c] · (self[c] − delta[c])`: a softmax's gradient against its
218    /// scores, where `self` holds the gradient against its probabilities and `delta` is, for each
219    /// softmax, `Σ probabilities · self` along the axis it ran along. `delta` broadcasts as
220    /// [`probabilities`](Self::probabilities)'s logsumexp does; the probabilities are cut as
221    /// `self` is, held by the same holder.
222    pub fn softmax_gradient<E: Float>(&mut self, probabilities: &AxisSlices, delta: &Tile) {
223        comptime!(assert!(
224            self.space == probabilities.space && self.axis == probabilities.axis,
225            "AxisSlices::softmax_gradient: the probabilities are cut as the gradient is, along \
226             {:?} of {:?}, not along {:?} of {:?}",
227            self.axis,
228            self.space,
229            probabilities.axis,
230            probabilities.space
231        ));
232        let broadcast = comptime!(Broadcast::new(&self.space, &delta.place.space));
233        let values = delta.cells::<E>(comptime!(delta.place.space.cells()));
234        match (&mut self.kind, &probabilities.kind) {
235            (AxisSlicesKind::Registers(block), AxisSlicesKind::Registers(probabilities)) => {
236                block.softmax_gradient_along::<E>(probabilities, &values, broadcast)
237            }
238            (AxisSlicesKind::Window(window), AxisSlicesKind::Window(probabilities)) => window
239                .softmax_gradient_along::<E>(probabilities, &values, self.space.clone(), broadcast),
240            _ => AxisSlices::refuse_fragments("AxisSlices::softmax_gradient"),
241        }
242    }
243
244    /// `values`, the slots a unit of a manual-mma grid keeps for the rows it holds cells of
245    /// ([`Tile::slices_per_unit`]), written into `window`, a plane's window of one cell a slice in
246    /// slice order: each slice written once, by a unit holding it. The caller meets the plane
247    /// before the window is read.
248    pub(crate) fn store_held<E: Float>(&self, values: &Array<E>, window: &mut Tile) {
249        comptime!(assert!(
250            window.place.holder() == ComputeScope::Plane
251                && window.place.space.cells() == self.space.slices_along(self.axis),
252            "AxisSlices::store_held: a plane's window of one cell for each of the {} slices of \
253             {:?}, not {:?} held by {:?}",
254            self.space.slices_along(self.axis),
255            self.space,
256            window.place.space,
257            window.place.holder()
258        ));
259        match &mut window.kind {
260            TileKind::Memory(memory) => self.store_held_in::<E>(values, memory),
261            TileKind::PlanePartition(_)
262            | TileKind::PlaneTile(_)
263            | TileKind::TmaGmem(_)
264            | TileKind::Procedural(_)
265            | TileKind::Lines(_) => panic!(
266                "AxisSlices::store_held: the slices' state lands in a window of shared memory"
267            ),
268        }
269    }
270
271    /// [`store_held`](Self::store_held) into the window's memory.
272    fn store_held_in<E: Float>(&self, values: &Array<E>, memory: &mut Memory) {
273        match &self.kind {
274            AxisSlicesKind::Fragments(partition) => partition.store_along::<E>(values, memory),
275            AxisSlicesKind::Fragment(tile) => tile
276                .manual_fragment()
277                .store_along::<E>(values, memory, 0usize, 0usize),
278            AxisSlicesKind::Registers(_) | AxisSlicesKind::Window(_) => panic!(
279                "AxisSlices::store_held: a unit's block and a plane's window keep every slice's \
280                 slot alike; only a manual-mma grid's units keep their own rows"
281            ),
282        }
283    }
284
285    /// `exp(value − slice)`, zero where `slice` is itself masked: a slice with nothing live yet,
286    /// whose max is still the minimum value.
287    pub(crate) fn exp_minus_cell<E: Float>(value: E, slice: E) -> E {
288        let live = slice > E::min_value();
289        select(live, (value - slice).exp(), E::from_int(0))
290    }
291}
292
293#[cube]
294impl AxisSlices {
295    /// What an op that reads a value beside every cell declines on a plane's fragments, whose
296    /// cells lie where the instruction keeps them.
297    fn refuse_fragments(#[comptime] who: &str) -> ! {
298        panic!("{who}: a unit's register blocks or a plane's windows hold the cells, not fragments")
299    }
300}
301
302/// Where each cell of a tile cut into slices along its innermost axis reads a tile that omits
303/// some of its axes, which it broadcasts over: the read cell is an offset for each slice plus one
304/// step for each position along the slice.
305#[derive(Clone, Debug, PartialEq, Eq, Hash)]
306pub(crate) struct Broadcast {
307    /// The cell of the broadcast tile each slice's first position reads.
308    pub(crate) slice: Vec<usize>,
309    /// Cells of the broadcast tile one position along a slice moves: zero where the slices run
310    /// along an omitted axis.
311    pub(crate) position: usize,
312}
313
314impl Broadcast {
315    /// How a tile over `space` reads one over `broadcast`, whose every axis is one of `space`'s
316    /// at its extent, or one wide.
317    ///
318    /// # Panics
319    ///
320    /// Where an axis of `broadcast` is neither.
321    pub(crate) fn new(space: &Space, broadcast: &Space) -> Self {
322        assert!(
323            broadcast.axes().all(|axis| match space.contains(axis) {
324                true => space.extent(axis) == broadcast.extent(axis),
325                false => broadcast.extent(axis) == 1,
326            }),
327            "Broadcast::new: every axis of {broadcast:?} is one of {space:?}'s at its extent, or \
328             one wide"
329        );
330        let stride = |axis: Axis| {
331            (broadcast.position(axis) + 1..broadcast.rank())
332                .map(|p| broadcast.extent_at(p))
333                .product::<usize>()
334        };
335        let innermost = space.axis_at(space.rank() - 1);
336        let outer: Vec<Axis> = (0..space.rank() - 1).map(|p| space.axis_at(p)).collect();
337        let slices = outer.iter().map(|&a| space.extent(a)).product::<usize>();
338        let slice = (0..slices)
339            .map(|index| {
340                let mut rest = index;
341                let mut offset = 0;
342                for &axis in outer.iter().rev() {
343                    let extent = space.extent(axis);
344                    if broadcast.contains(axis) {
345                        offset += (rest % extent) * stride(axis);
346                    }
347                    rest /= extent;
348                }
349                offset
350            })
351            .collect();
352        let position = match broadcast.contains(innermost) {
353            true => stride(innermost),
354            false => 0,
355        };
356        Self { slice, position }
357    }
358
359    /// The broadcast tile's cell that position `position` of slice `slice` reads.
360    pub(crate) fn at(&self, slice: usize, position: usize) -> usize {
361        self.slice[slice] + position * self.position
362    }
363}