Skip to main content

cubecl_tile/layout/storage/
partitioning.rs

1//! [`StoragePartitioning`]: how a tensor's values are laid down in memory, as nested tiles.
2
3use core::fmt::{self, Display, Formatter};
4
5use cubecl::zspace::{MetadataError, SmallVec, Tiling};
6
7use crate::{Axis, Geometry, Space};
8
9/// The most pieces a statement keeps on the stack: a few stated tiles and the rest of each axis.
10const PIECES: usize = 12;
11
12/// A list of pieces, or of what each piece gives, without a heap allocation.
13type PieceList<T> = SmallVec<[T; PIECES]>;
14
15/// How a tensor's values are laid down: stated tiles, finest first, then the rest's order.
16///
17/// ```ignore
18/// // 32 x 32 tiles of a [k, n] weight, stored for a four-wide read, the tiles along N
19/// let storage = StorageLevels::new(&[(N, 4)])
20///     .tile(&[(N, 8), (K, 32)])
21///     .grid(&[N, K]);
22/// ```
23#[derive(Clone, PartialEq, Eq, Hash, Debug)]
24pub struct StoragePartitioning {
25    /// Stated tiles, finest first: each `(axis, count)` holds `count` of the piece below it.
26    tiles: PieceList<(Axis, usize)>,
27    /// The rest of each axis, finest first; its counts are what the extents leave.
28    order: SmallVec<[Axis; Space::MAX_RANK]>,
29}
30
31impl StoragePartitioning {
32    /// The statement a bound buffer was written from, read off its dims by stride; `labels` name
33    /// the trailing dims. `None` where the buffer's order is no partitioning.
34    pub fn new(geometry: &Geometry, labels: &[Axis]) -> Option<Self> {
35        let rank = geometry.rank();
36        let unlabelled = rank.saturating_sub(labels.len());
37        let tiling = geometry.tiling();
38        // Dims past the logical rank are stated pieces.
39        let first_stated = match tiling.is_tiled() {
40            true => tiling.logical_rank(rank).unwrap_or(rank),
41            false => rank,
42        };
43        let mut dims: PieceList<(usize, usize, usize)> = geometry
44            .dims()
45            .enumerate()
46            .filter(|&(dim, (extent, stride))| {
47                let piece_of_one = extent == 1 && dim >= first_stated;
48                dim >= unlabelled && (piece_of_one || extent > 1 && stride > 0)
49            })
50            .map(|(dim, (extent, stride))| (dim, extent, stride))
51            .collect();
52        dims.sort_by_key(|&(dim, extent, stride)| (stride, extent > 1, core::cmp::Reverse(dim)));
53        let (stated, rest): (PieceList<_>, PieceList<_>) = dims
54            .iter()
55            .copied()
56            .partition(|&(dim, _, _)| dim >= first_stated);
57        let finest_rest = rest.first().map_or(usize::MAX, |&(_, _, stride)| stride);
58        if stated
59            .iter()
60            .any(|&(_, extent, stride)| extent > 1 && stride > finest_rest)
61        {
62            return None;
63        }
64        Some(Self {
65            tiles: stated
66                .iter()
67                .map(|&(dim, extent, _)| (labels[dim - unlabelled], extent))
68                .collect(),
69            order: rest
70                .iter()
71                .map(|&(dim, _, _)| labels[dim - unlabelled])
72                .collect(),
73        })
74    }
75
76    /// The stated tiles, finest first.
77    pub fn tiles(&self) -> &[(Axis, usize)] {
78        &self.tiles
79    }
80
81    /// The order the rest of each axis follows, finest first.
82    pub fn order(&self) -> &[Axis] {
83        &self.order
84    }
85
86    /// The buffer's dims, coarsest first, each named by the axis it is a piece of.
87    pub fn labels(&self, axes: &[Axis]) -> Vec<Axis> {
88        StoragePartitioning::level_major(axes, &self.fragments(axes))
89    }
90
91    /// The dims of a buffer storing `axes` as `fragments` pieces each, in cubecl's [`Tiling`]
92    /// order: `[k, n]` stored `[k/32, n/32, 32, 32]` is `[K, N, K, N]`.
93    pub(crate) fn level_major(axes: &[Axis], fragments: &[usize]) -> Vec<Axis> {
94        assert_eq!(
95            axes.len(),
96            fragments.len(),
97            "StoragePartitioning::level_major: {} axes but {} piece counts",
98            axes.len(),
99            fragments.len()
100        );
101        let depth = fragments.iter().copied().max().unwrap_or(0);
102        (0..depth)
103            .flat_map(|level| {
104                axes.iter()
105                    .zip(fragments)
106                    .filter(move |&(_, &pieces)| level < pieces)
107                    .map(|(&axis, _)| axis)
108            })
109            .collect()
110    }
111
112    /// Whether `tile`, finest first, is held by this partitioning over a tensor of `extents`.
113    /// Stated tiles are taken whole; the rest of an axis may be divided.
114    pub fn holds(
115        &self,
116        tile: &[(Axis, usize)],
117        extents: &[(Axis, usize)],
118    ) -> Result<(), TileMisfit> {
119        let mut pieces = self.pieces(extents)?;
120        pieces.reverse();
121        for &(axis, count) in tile {
122            let mut run = 1;
123            while run < count {
124                let Some(mut piece) = pieces.pop() else {
125                    return Err(TileMisfit::RunsOut { wanted: axis });
126                };
127                if piece.count == 1 {
128                    continue;
129                }
130                if piece.axis != axis {
131                    return Err(TileMisfit::Interleaved {
132                        wanted: axis,
133                        found: piece.axis,
134                    });
135                }
136                let missing = count / run;
137                if missing.is_multiple_of(piece.count) {
138                    run *= piece.count;
139                    continue;
140                }
141                if piece.stated || !piece.count.is_multiple_of(missing) {
142                    return Err(TileMisfit::Overshoots {
143                        axis,
144                        wanted: count,
145                        piece: piece.count,
146                    });
147                }
148                piece.count /= missing;
149                pieces.push(piece);
150                run = count;
151            }
152        }
153        Ok(())
154    }
155
156    /// The tiles, finest first, a window can be addressed inside with one stride per axis.
157    pub(crate) fn contiguous_tiles(
158        &self,
159        extents: &[(Axis, usize)],
160    ) -> PieceList<SmallVec<[(Axis, usize); Space::MAX_RANK]>> {
161        let Ok(pieces) = self.pieces(extents) else {
162            return PieceList::new();
163        };
164        let mut tiles = PieceList::new();
165        let mut tile: SmallVec<[(Axis, usize); Space::MAX_RANK]> = SmallVec::new();
166        for piece in pieces.into_iter().filter(|piece| piece.count > 1) {
167            let continues = tile.last().is_some_and(|&(axis, _)| axis == piece.axis);
168            match tile.iter_mut().find(|(axis, _)| *axis == piece.axis) {
169                // An axis met again after another one sat between: no longer one run per axis.
170                Some(_) if !continues => break,
171                Some((_, extent)) => *extent *= piece.count,
172                None => tile.push((piece.axis, piece.count)),
173            }
174            tiles.push(tile.clone());
175        }
176        tiles
177    }
178
179    /// The buffer storing a tensor of `extents`: its dims in [`labels`](Self::labels)' order and
180    /// its [`Tiling`].
181    pub fn physical(&self, extents: &[(Axis, usize)]) -> Result<Geometry, StorageMisfit> {
182        let axes: SmallVec<[Axis; Space::MAX_RANK]> =
183            extents.iter().map(|&(axis, _)| axis).collect();
184        let named = |axis: &Axis| self.order.iter().filter(|&a| a == axis).count() == 1;
185        if self.order.len() != axes.len() || !axes.iter().all(named) {
186            return Err(StorageMisfit::Order {
187                order: self.order.to_vec(),
188                axes: axes.to_vec(),
189            });
190        }
191        let pieces = self.pieces(extents).map_err(|misfit| match misfit {
192            TileMisfit::Unwhole { axis, extent, tile } => {
193                StorageMisfit::PartialTile { axis, extent, tile }
194            }
195            _ => unreachable!("only the rest of an axis is computed against its extent"),
196        })?;
197        let tiling = Tiling::new(&self.fragments(&axes)).map_err(StorageMisfit::Unrecordable)?;
198        let strides: PieceList<usize> = pieces
199            .iter()
200            .scan(1, |run, piece| {
201                let stride = *run;
202                *run *= piece.count;
203                Some(stride)
204            })
205            .collect();
206        // `labels` names an axis's pieces coarsest first; `pieces` lists them finest first.
207        let mut taken: PieceList<bool> = SmallVec::from_elem(false, pieces.len());
208        let dims: PieceList<(usize, usize)> = self
209            .labels(&axes)
210            .iter()
211            .map(|&axis| {
212                let p = (0..pieces.len())
213                    .rev()
214                    .find(|&p| !taken[p] && pieces[p].axis == axis)
215                    .expect("an axis has as many pieces as it has dims");
216                taken[p] = true;
217                (pieces[p].count, strides[p])
218            })
219            .collect();
220        Ok(Geometry::new(&dims).with_tiling(tiling))
221    }
222
223    /// How many buffer dims each of `axes` is stored as: one per stated tile along it, and one for
224    /// the rest of it.
225    fn fragments(&self, axes: &[Axis]) -> SmallVec<[usize; Space::MAX_RANK]> {
226        axes.iter()
227            .map(|&axis| 1 + self.tiles.iter().filter(|&&(a, _)| a == axis).count())
228            .collect()
229    }
230
231    /// Every piece over a tensor of `extents`, finest first: stated tiles, then each rest.
232    fn pieces(&self, extents: &[(Axis, usize)]) -> Result<PieceList<Piece>, TileMisfit> {
233        let mut pieces: PieceList<Piece> = self
234            .tiles
235            .iter()
236            .map(|&(axis, count)| Piece {
237                axis,
238                count,
239                stated: true,
240            })
241            .collect();
242        for &axis in &self.order {
243            let extent = extents
244                .iter()
245                .find(|&&(a, _)| a == axis)
246                .map_or(1, |&(_, extent)| extent);
247            let tile: usize = self
248                .tiles
249                .iter()
250                .filter(|&&(a, _)| a == axis)
251                .map(|&(_, count)| count)
252                .product();
253            if !extent.is_multiple_of(tile) {
254                return Err(TileMisfit::Unwhole { axis, extent, tile });
255            }
256            pieces.push(Piece {
257                axis,
258                count: extent / tile,
259                stated: false,
260            });
261        }
262        Ok(pieces)
263    }
264}
265
266/// A [`StoragePartitioning`] being stated leaf-up, until [`grid`](Self::grid).
267#[derive(Clone, Debug)]
268pub struct StorageLevels {
269    tiles: PieceList<(Axis, usize)>,
270}
271
272impl StorageLevels {
273    /// The finest tile, `(axis, count)` finest first, in values.
274    pub fn new(tile: &[(Axis, usize)]) -> Self {
275        Self {
276            tiles: SmallVec::from_slice(tile),
277        }
278    }
279
280    /// A coarser tile, each count how many of the tile below it holds.
281    pub fn tile(mut self, level: &[(Axis, usize)]) -> Self {
282        self.tiles.extend_from_slice(level);
283        self
284    }
285
286    /// The partitioning, the rest of each axis following `order`, finest first.
287    pub fn grid(self, order: &[Axis]) -> StoragePartitioning {
288        StoragePartitioning {
289            tiles: self.tiles,
290            order: SmallVec::from_slice(order),
291        }
292    }
293}
294
295/// Where a tile stops being held by a [`StoragePartitioning`] ([`StoragePartitioning::holds`]).
296#[derive(Clone, Copy, Debug, PartialEq, Eq)]
297pub enum TileMisfit {
298    /// A piece of another axis sits inside a wanted piece: the wanted tile is not one run.
299    Interleaved { wanted: Axis, found: Axis },
300    /// A piece reaches past the wanted count.
301    Overshoots {
302        axis: Axis,
303        wanted: usize,
304        piece: usize,
305    },
306    /// The partitioning ends before the wanted piece closes.
307    RunsOut { wanted: Axis },
308    /// The stated tiles do not close the axis's extent.
309    Unwhole {
310        axis: Axis,
311        extent: usize,
312        tile: usize,
313    },
314}
315
316impl Display for TileMisfit {
317    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
318        match self {
319            Self::Interleaved { wanted, found } => write!(
320                f,
321                "a piece along {found:?} sits inside the one wanted along {wanted:?}"
322            ),
323            Self::Overshoots {
324                axis,
325                wanted,
326                piece,
327            } => write!(
328                f,
329                "a piece of {piece} along {axis:?} reaches past the {wanted} wanted, and is not \
330                 the rest of the axis, which it would divide"
331            ),
332            Self::RunsOut { wanted } => {
333                write!(f, "nothing is left along {wanted:?} for the piece wanted")
334            }
335            Self::Unwhole { axis, extent, tile } => write!(
336                f,
337                "axis {axis:?} runs {extent}, which is not a whole number of {tile}-wide tiles"
338            ),
339        }
340    }
341}
342
343/// Why a [`StoragePartitioning`] cannot store a tensor ([`StoragePartitioning::physical`]).
344#[derive(Clone, Debug, PartialEq, Eq)]
345pub enum StorageMisfit {
346    /// The tiles along `axis` do not close its extent.
347    PartialTile {
348        axis: Axis,
349        extent: usize,
350        tile: usize,
351    },
352    /// The order does not name each of the tensor's axes once.
353    Order { order: Vec<Axis>, axes: Vec<Axis> },
354    /// More pieces than the buffer's metadata records ([`Tiling`]).
355    Unrecordable(MetadataError),
356}
357
358impl Display for StorageMisfit {
359    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
360        match self {
361            Self::PartialTile { axis, extent, tile } => write!(
362                f,
363                "axis {axis:?} runs {extent}, which is not a whole number of {tile}-wide tiles"
364            ),
365            Self::Order { order, axes } => write!(
366                f,
367                "the tiles follow {order:?}, but the tensor's axes are {axes:?}: the order names \
368                 each once"
369            ),
370            Self::Unrecordable(why) => {
371                write!(f, "the buffer's metadata cannot record its pieces: {why:?}")
372            }
373        }
374    }
375}
376
377/// One piece of a partitioning over a tensor: `count` of the piece below it along `axis`.
378#[derive(Clone, Copy, Debug)]
379struct Piece {
380    axis: Axis,
381    count: usize,
382    /// Whether the count was stated, so a reader must take the piece whole.
383    stated: bool,
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389    use crate::*;
390
391    const M: Axis = Axis(0);
392    const N: Axis = Axis(1);
393    const K: Axis = Axis(2);
394
395    /// A buffer reads back as the statement it was written from, whatever order its tiling lists.
396    #[test]
397    fn a_written_buffer_reads_back_as_its_statement() {
398        let statements = [
399            StorageLevels::new(&[(N, 32), (K, 16)]).grid(&[N, K]),
400            StorageLevels::new(&[(N, 16), (K, 32)]).grid(&[K, N]),
401            StorageLevels::new(&[(N, 4), (N, 8)])
402                .tile(&[(K, 2), (K, 16)])
403                .grid(&[K, N]),
404            StorageLevels::new(&[(N, 4)])
405                .tile(&[(N, 1), (K, 2)])
406                .grid(&[N, K]),
407        ];
408        for storage in statements {
409            let geometry = storage.physical(&[(K, 64), (N, 64)]).unwrap();
410            let read = StoragePartitioning::new(&geometry, &geometry.labels(&[K, N])).unwrap();
411            assert_eq!(read, storage);
412        }
413    }
414
415    /// An untiled buffer states no tiles: its order is its dims by stride.
416    #[test]
417    fn an_untiled_buffer_is_its_order_alone() {
418        let rows = Geometry::new(&[(64, 4096), (4096, 1)]);
419        let read = StoragePartitioning::new(&rows, &[K, N]).unwrap();
420        assert!(read.tiles().is_empty());
421        assert_eq!(read.order(), &[N, K]);
422
423        let transposed = Geometry::new(&[(64, 1), (4096, 64)]);
424        let read = StoragePartitioning::new(&transposed, &[K, N]).unwrap();
425        assert_eq!(read.order(), &[K, N]);
426
427        let broadcast = Geometry::new(&[(3, 0), (1, 5), (4096, 1)]);
428        let read = StoragePartitioning::new(&broadcast, &[M, K, N]).unwrap();
429        assert_eq!(read.order(), &[N]);
430    }
431
432    #[test]
433    fn a_stored_read_and_its_whole_tile_are_both_held() {
434        let with_read = StorageLevels::new(&[(N, 4)])
435            .tile(&[(N, 8), (K, 32)])
436            .grid(&[N, K]);
437        let extents = [(K, 64), (N, 64)];
438        assert_eq!(with_read.holds(&[(N, 4)], &extents), Ok(()));
439        assert_eq!(with_read.holds(&[(N, 32), (K, 32)], &extents), Ok(()));
440    }
441
442    #[test]
443    fn a_stated_tile_is_never_split() {
444        let extents = [(K, 64), (N, 64)];
445        let tiles = StorageLevels::new(&[(N, 32), (K, 32)]).grid(&[N, K]);
446        assert_eq!(
447            tiles.holds(&[(N, 4)], &extents),
448            Err(TileMisfit::Overshoots {
449                axis: N,
450                wanted: 4,
451                piece: 32
452            })
453        );
454        let column_first = StorageLevels::new(&[(K, 32), (N, 32)]).grid(&[N, K]);
455        assert_eq!(
456            column_first.holds(&[(N, 32), (K, 32)], &extents),
457            Err(TileMisfit::Interleaved {
458                wanted: N,
459                found: K
460            })
461        );
462    }
463
464    #[test]
465    fn a_read_cuts_itself_out_of_the_rest_of_an_axis() {
466        let rows = StorageLevels::new(&[]).grid(&[N, K]);
467        let extents = [(K, 64), (N, 4096)];
468        assert_eq!(rows.holds(&[(N, 8)], &extents), Ok(()));
469        assert!(rows.holds(&[(N, 3)], &extents).is_err());
470    }
471
472    /// NVFP4: a word of eight along `K` and a two-by-two-word read are whole stated tiles.
473    #[test]
474    fn a_word_and_a_two_dimensional_read_are_whole_tiles() {
475        let values = StorageLevels::new(&[(K, 8)])
476            .tile(&[(K, 2), (N, 2)])
477            .tile(&[(K, 8), (N, 8)])
478            .grid(&[K, N]);
479        let extents = [(K, 256), (N, 64)];
480        assert_eq!(values.holds(&[(K, 8)], &extents), Ok(()));
481        assert_eq!(values.holds(&[(K, 16), (N, 2)], &extents), Ok(()));
482        assert!(values.holds(&[(K, 32)], &extents).is_err());
483    }
484
485    #[test]
486    fn a_k_first_grid_moves_the_strides_not_the_dims() {
487        let storage = StorageLevels::new(&[(N, 16), (K, 32)]).grid(&[K, N]);
488        let geometry = storage.physical(&[(K, 128), (N, 64)]).unwrap();
489        // [k/32, n/16, 32, 16]: a tile is 512 values, the next along K 512 on, along N 4 tiles on.
490        assert_eq!(
491            geometry,
492            Geometry::new(&[(4, 512), (4, 2048), (32, 16), (16, 1)])
493                .with_tiling(cubecl::zspace::Tiling::new(&[2, 2]).unwrap())
494        );
495    }
496
497    #[test]
498    fn a_statement_that_does_not_fit_the_tensor_is_refused_when_written() {
499        let eight_rows = StorageLevels::new(&[(K, 8)]).grid(&[K, N]);
500        assert_eq!(
501            eight_rows.physical(&[(K, 12), (N, 4)]),
502            Err(StorageMisfit::PartialTile {
503                axis: K,
504                extent: 12,
505                tile: 8
506            })
507        );
508        let one_axis = StorageLevels::new(&[(K, 4)]).grid(&[K]);
509        assert!(matches!(
510            one_axis.physical(&[(K, 12), (N, 4)]),
511            Err(StorageMisfit::Order { .. })
512        ));
513    }
514
515    /// A tile's piece of one is kept as a dim of the buffer, and a reader is held the same either way.
516    #[test]
517    fn a_piece_of_one_is_a_dim_kept_as_stated() {
518        let with_one = StorageLevels::new(&[(N, 4)])
519            .tile(&[(N, 1), (K, 2)])
520            .grid(&[N, K]);
521        let without = StorageLevels::new(&[(N, 4), (K, 2)]).grid(&[N, K]);
522        assert_ne!(with_one, without);
523        assert_eq!(with_one.labels(&[K, N]), vec![K, N, K, N, N]);
524        assert_eq!(without.labels(&[K, N]), vec![K, N, K, N]);
525        let (read, extents) = ([(N, 4), (K, 2)], [(K, 8), (N, 8)]);
526        assert_eq!(with_one.holds(&read, &extents), Ok(()));
527        assert_eq!(without.holds(&read, &extents), Ok(()));
528    }
529
530    /// The level-major order cubecl's tiling lists dims in: every axis's coarsest piece in the
531    /// tensor's order, then the next of every axis still split, so an untiled axis drops out after
532    /// the first level and a deeper axis appears alone at the finest.
533    #[test]
534    fn labels_name_the_dims_level_major_coarsest_first() {
535        let untiled = StorageLevels::new(&[]).grid(&[N, K]);
536        assert_eq!(untiled.labels(&[K, N]), vec![K, N]);
537        let one_level = StorageLevels::new(&[(N, 32), (K, 32)]).grid(&[N, K]);
538        assert_eq!(one_level.labels(&[K, N]), vec![K, N, K, N]);
539        let deeper_n = StorageLevels::new(&[(N, 4), (N, 8), (K, 32)]).grid(&[N, K]);
540        assert_eq!(deeper_n.labels(&[K, N]), vec![K, N, K, N, N]);
541        // A batch axis ahead of the tiled block is its own single dim.
542        let batched = StorageLevels::new(&[(N, 32), (K, 32)]).grid(&[N, K, M]);
543        assert_eq!(batched.labels(&[M, K, N]), vec![M, K, N, K, N]);
544    }
545
546    /// The windows `storage` addresses with one stride per axis over `extents`, as lists.
547    fn windows(
548        storage: &StoragePartitioning,
549        extents: &[(Axis, usize)],
550    ) -> Vec<Vec<(Axis, usize)>> {
551        storage
552            .contiguous_tiles(extents)
553            .iter()
554            .map(|tile| tile.to_vec())
555            .collect()
556    }
557
558    #[test]
559    fn a_window_is_one_run_inside_the_tiles_whose_axes_do_not_interleave() {
560        let with_read = StorageLevels::new(&[(N, 4)])
561            .tile(&[(N, 8), (K, 32)])
562            .grid(&[N, K]);
563        assert_eq!(
564            windows(&with_read, &[(K, 64), (N, 64)]),
565            vec![vec![(N, 4)], vec![(N, 32)], vec![(N, 32), (K, 32)]]
566        );
567
568        let down_k = StorageLevels::new(&[(N, 16), (K, 32)]).grid(&[K, N]);
569        assert_eq!(
570            windows(&down_k, &[(K, 128), (N, 64)]),
571            vec![
572                vec![(N, 16)],
573                vec![(N, 16), (K, 32)],
574                vec![(N, 16), (K, 128)]
575            ]
576        );
577
578        let interleaved = StorageLevels::new(&[(N, 16), (K, 2)])
579            .tile(&[(N, 2), (K, 16)])
580            .grid(&[N, K]);
581        assert_eq!(
582            windows(&interleaved, &[(K, 64), (N, 64)]),
583            vec![vec![(N, 16)], vec![(N, 16), (K, 2)]]
584        );
585    }
586}