Skip to main content

cubecl_tile/layout/storage/
vector_tile.rs

1//! [`VectorTile`]: the values one vector load brings, as a tile.
2//!
3//! An operand says how it is stored, and the kernel picks how wide it loads; the tile a load
4//! covers follows from both. A plain buffer is loaded in a run along its innermost axis. A buffer
5//! stored in tiles is loaded a stored tile at a time: its finest tiles, fused per axis, until they
6//! hold the load's values, so an NVFP4 buffer whose word runs along `K` and whose next pieces are
7//! two words along `K` and two columns loads 32 values as 16 along `K` by 2 along `N`.
8
9use cubecl::prelude::*;
10
11use cubecl_std::tensor::layout::CoordsDyn;
12
13use crate::{
14    Axis, Coords, Extent, Space, TileMisfit,
15    algebra::{Integer, IntegerExpand},
16    comptime_only,
17};
18
19/// The values one vector load brings, as a tile: its extents, finest first, one per axis.
20///
21/// ```ignore
22/// // a plain row-major buffer, four wide
23/// VectorTile::new(&[], N, 4)                                  // [(N, 4)]
24/// // NVFP4 values: a word of 8 along K, 2 words along K, 2 columns; 32 values a load
25/// VectorTile::new(&[(K, 8), (K, 2), (N, 2)], N, 32)           // [(K, 16), (N, 2)]
26/// ```
27#[derive(Clone, PartialEq, Eq, Hash, Debug)]
28pub struct VectorTile {
29    extents: Vec<(Axis, usize)>,
30}
31
32impl VectorTile {
33    /// The tile a load of `values` values covers over a buffer stored in `stored` tiles, finest
34    /// first ([`TileSpec::stored_tiles`](crate::TileSpec::stored_tiles)): its finest tiles, fused
35    /// where one axis continues, until they hold `values`. Nothing stated, a run of `values`
36    /// along `innermost`.
37    ///
38    /// # Errors
39    ///
40    /// A load that would cut a stored tile, one wider than every stored tile together, or one
41    /// whose axis comes back after another, which is no longer one extent per axis.
42    pub fn new(
43        stored: &[(Axis, usize)],
44        innermost: Axis,
45        values: usize,
46    ) -> Result<Self, TileMisfit> {
47        if stored.is_empty() || values == 1 {
48            return Ok(Self::run(innermost, values));
49        }
50        let mut extents: Vec<(Axis, usize)> = Vec::new();
51        let mut held = 1;
52        for &(axis, count) in stored.iter().filter(|&&(_, count)| count > 1) {
53            if held == values {
54                break;
55            }
56            if !(values / held).is_multiple_of(count) {
57                return Err(TileMisfit::Overshoots {
58                    axis,
59                    wanted: values / held,
60                    piece: count,
61                });
62            }
63            held *= count;
64            match extents.last().copied() {
65                Some((last, extent)) if last == axis => {
66                    *extents.last_mut().expect("just read") = (axis, extent * count);
67                }
68                Some((last, _)) if extents.iter().any(|&(a, _)| a == axis) => {
69                    return Err(TileMisfit::Interleaved {
70                        wanted: axis,
71                        found: last,
72                    });
73                }
74                _ => extents.push((axis, count)),
75            }
76        }
77        if held != values {
78            return Err(TileMisfit::RunsOut {
79                wanted: extents.last().map_or(innermost, |&(axis, _)| axis),
80            });
81        }
82        Ok(Self { extents })
83    }
84
85    /// A run of `values` along `axis`: how a plain buffer is loaded.
86    pub fn run(axis: Axis, values: usize) -> Self {
87        Self {
88            extents: vec![(axis, values)],
89        }
90    }
91
92    /// The extents, finest first.
93    pub fn extents(&self) -> &[(Axis, usize)] {
94        &self.extents
95    }
96
97    /// Values one load holds along its finest axis: one line of it, the whole load where it spans
98    /// one axis. A reader walking lines along that axis takes `values() / run()` of them a load.
99    pub(crate) fn run_length(&self) -> usize {
100        self.extents.first().map_or(1, |&(_, run)| run)
101    }
102
103    /// The box one load covers, an axis to each extent.
104    pub fn space(&self) -> Space {
105        Space::new(&self.extents)
106    }
107
108    /// How many values the load brings.
109    pub fn values(&self) -> usize {
110        self.extents.iter().map(|&(_, extent)| extent).product()
111    }
112
113    /// The axis a one-axis load runs along and its width, for a `reader` that places a load along
114    /// one axis.
115    ///
116    /// # Panics
117    ///
118    /// When the load spans several axes, naming `reader`.
119    pub(crate) fn along_one_axis(&self, reader: &str) -> (Axis, usize) {
120        match self.extents.as_slice() {
121            &[one] => one,
122            extents => panic!(
123                "{reader} places a load along one axis, and this operand loads {extents:?}: \
124                 read it with a narrower vector, or through a path that places a tile"
125            ),
126        }
127    }
128
129    /// How far one load reaches along `axis`: its extent there, `1` off the axes it spans.
130    pub(crate) fn extent_along(&self, axis: Axis) -> usize {
131        self.extents
132            .iter()
133            .find(|&&(a, _)| a == axis)
134            .map_or(1, |&(_, extent)| extent)
135    }
136
137    /// How many values of one load lie between two neighbours along `axis`: the product of the
138    /// extents finer than `axis`'s. `0` off the axes the load spans.
139    pub(crate) fn step_along(&self, axis: Axis) -> usize {
140        let mut step = 1;
141        for &(a, extent) in &self.extents {
142            if a == axis {
143                return step;
144            }
145            step *= extent;
146        }
147        0
148    }
149
150    /// How far along `axis` the `line`-th of these loads of a window spanning `space` sits from
151    /// the window's first value, the loads counted with the last axis fastest: the place
152    /// [`start`](VectorTile::start) gives, known at expansion.
153    pub(crate) fn offset_in(&self, line: usize, space: &Space, axis: Axis) -> usize {
154        let mut rest = line;
155        for at in space.axes().collect::<Vec<_>>().into_iter().rev() {
156            let width = self.extent_along(at);
157            let count = space.extent(at).div_ceil(width);
158            if at == axis {
159                return (rest % count) * width;
160            }
161            rest /= count;
162        }
163        0
164    }
165
166    /// How far along `axis` the value at `position` of one load sits from the load's first: the
167    /// position unravelled over the load's extents, finest first. `20` of a 16 by 2 load along `K`
168    /// then `N` is 4 along `K` and 1 along `N`.
169    pub(crate) fn offset_along(&self, position: usize, axis: Axis) -> usize {
170        let mut rest = position;
171        for &(a, extent) in &self.extents {
172            if a == axis {
173                return rest % extent;
174            }
175            rest /= extent;
176        }
177        0
178    }
179
180    /// How many loads an edge of `edge` values along `axis` holds, rounded up: an edge cuts whole
181    /// loads, or is the axis's whole `extent`, since a padded stage's innermost extent need not
182    /// fill its last load.
183    ///
184    /// # Panics
185    ///
186    /// An edge that is neither, so the next region would start mid-load. A `Dynamic` axis has no
187    /// extent to be cut whole, so it owes the divisibility.
188    pub(crate) fn loads_in(&self, axis: Axis, edge: usize, extent: Extent) -> usize {
189        let w = self.extent_along(axis);
190        assert!(
191            edge.is_multiple_of(w) || matches!(extent, Extent::Static(x) if x == edge),
192            "Memory::at: the edge {edge} along {axis:?} is neither a whole number of loads {w} \
193             wide there nor the axis's whole extent ({extent:?}), so a step would start mid-load"
194        );
195        edge.div_ceil(w)
196    }
197
198    /// How many loads fit along each axis of `space`, rounded up: a padded stage's innermost extent
199    /// need not fill whole loads, and a checked read's box must include the partial last one.
200    pub(crate) fn counts(&self, space: &Space) -> Vec<usize> {
201        space
202            .axes()
203            .map(|axis| space.extent(axis).div_ceil(self.extent_along(axis)))
204            .collect()
205    }
206
207    /// How much of each of a buffer's `rank` dims one load covers, over a buffer stored in
208    /// `stored` tiles, finest first, whose trailing dims `labels` name: a stated tile the load
209    /// takes whole is covered entirely, and a plain buffer's innermost dim by the load's width.
210    /// `1` elsewhere.
211    ///
212    /// A buffer read in loads is the same buffer with each dim's extent divided by what one load
213    /// covers of it and every stride counted in loads: `extent / part` and `stride * part /
214    /// values`, which for a plain buffer is its innermost dim in lines and the rest's strides
215    /// divided by the width.
216    pub(crate) fn parts(
217        &self,
218        stored: &[(Axis, usize)],
219        labels: &[Axis],
220        rank: usize,
221    ) -> Vec<usize> {
222        let mut parts = vec![1; rank];
223        if stored.is_empty() || self.values() == 1 {
224            if let Some(last) = parts.last_mut() {
225                *last = self.values();
226            }
227            return parts;
228        }
229        let mut taken = 1;
230        for (&(_, count), dim) in stored.iter().zip(Self::stated_dims(stored, labels, rank)) {
231            if count == 1 {
232                continue;
233            }
234            if taken == self.values() {
235                break;
236            }
237            parts[dim] = count;
238            taken *= count;
239        }
240        parts
241    }
242
243    /// The extent of each of a buffer's `rank` dims that is a stated tile, over a buffer stored
244    /// in `stored` tiles whose trailing dims `labels` name; `None` for a dim the buffer's own
245    /// shape sets. A stated tile's extent is fixed where the kernel is written, so a read over it
246    /// divides by a constant rather than by a dim read off the buffer.
247    pub(crate) fn stated_extents(
248        stored: &[(Axis, usize)],
249        labels: &[Axis],
250        rank: usize,
251    ) -> Vec<Option<usize>> {
252        let mut extents = vec![None; rank];
253        for (&(_, count), dim) in stored.iter().zip(Self::stated_dims(stored, labels, rank)) {
254            extents[dim] = Some(count);
255        }
256        extents
257    }
258
259    /// The dim of a buffer of `rank` dims, whose trailing dims `labels` name, each of `stored`'s
260    /// tiles is: the `j`-th finest stated tile of an axis is its `j`-th dim from the end.
261    fn stated_dims(stored: &[(Axis, usize)], labels: &[Axis], rank: usize) -> Vec<usize> {
262        let unlabelled = rank - labels.len();
263        let mut from_end: Vec<(Axis, usize)> = Vec::new();
264        stored
265            .iter()
266            .map(|&(axis, _)| {
267                let nth = match from_end.iter_mut().find(|(a, _)| *a == axis) {
268                    Some((_, seen)) => {
269                        *seen += 1;
270                        *seen
271                    }
272                    None => {
273                        from_end.push((axis, 1));
274                        1
275                    }
276                };
277                let dim = (0..labels.len())
278                    .rev()
279                    .filter(|&d| labels[d] == axis)
280                    .nth(nth - 1)
281                    .expect("a stated tile is a dim of the buffer");
282                unlabelled + dim
283            })
284            .collect()
285    }
286}
287
288#[cube]
289impl VectorTile {
290    fn count_of(#[comptime] space: &Space, #[comptime] tile: &VectorTile) -> u32 {
291        comptime!(tile.counts(space).iter().product::<usize>() as u32).runtime()
292    }
293
294    fn start_of(
295        line: u32,
296        #[comptime] space: &Space,
297        #[comptime] tile: &VectorTile,
298    ) -> Coords<u32> {
299        let rank = comptime!(space.rank());
300        let place = Coords::constant(comptime!(tile.counts(space))).unravel(line);
301        let mut coords = Coords::<u32>::new();
302        #[unroll]
303        for p in 0..rank {
304            let extent = comptime!(tile.extent_along(space.axis_at(p)) as u32);
305            coords.push(place.at(p).times(extent));
306        }
307        coords
308    }
309
310    fn index_of(
311        coords: &Coords<u32>,
312        #[comptime] space: &Space,
313        #[comptime] tile: &VectorTile,
314    ) -> CoordsDyn {
315        let rank = comptime!(space.rank());
316        let mut index = CoordsDyn::new();
317        #[unroll]
318        for p in 0..rank {
319            let extent = comptime!(tile.extent_along(space.axis_at(p)) as u32);
320            index.push(coords.at(p).divided_by(extent));
321        }
322        index
323    }
324}
325
326/// `load.count(&space)`, `load.start(line, &space)`, `load.index(&coords, &space)`: written out by
327/// hand, since a compile-time value has no kernel-side twin for `#[cube]` to hang a method on.
328impl VectorTile {
329    /// How many loads a window spanning `space` holds.
330    pub fn count(&self, space: &Space) -> u32 {
331        VectorTile::count_of(space, self)
332    }
333
334    /// The first value of the `line`-th load of a window spanning `space`, one coordinate per
335    /// axis, the loads counted with the last axis fastest.
336    pub fn start(&self, line: u32, space: &Space) -> Coords<u32> {
337        VectorTile::start_of(line, space, self)
338    }
339
340    /// Where the load starting at `coords` sits in a view of a window spanning `space` read in
341    /// these loads: each coordinate divided by the load's extent along its axis.
342    pub fn index(&self, coords: &Coords<u32>, space: &Space) -> CoordsDyn {
343        VectorTile::index_of(coords, space, self)
344    }
345
346    pub fn __expand_count_method(&self, scope: &Scope, space: &Space) -> NativeExpand<u32> {
347        VectorTile::__expand_count_of(scope, space, self)
348    }
349
350    pub fn __expand_start_method(
351        &self,
352        scope: &Scope,
353        line: NativeExpand<u32>,
354        space: &Space,
355    ) -> <Coords<u32> as CubeType>::ExpandType {
356        VectorTile::__expand_start_of(scope, line, space, self)
357    }
358
359    pub fn __expand_index_method(
360        &self,
361        scope: &Scope,
362        coords: &<Coords<u32> as CubeType>::ExpandType,
363        space: &Space,
364    ) -> <CoordsDyn as CubeType>::ExpandType {
365        VectorTile::__expand_index_of(scope, coords, space, self)
366    }
367}
368
369comptime_only!(VectorTile);
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    const K: Axis = Axis(0);
376    const N: Axis = Axis(1);
377
378    /// A plain buffer loads a run along its innermost axis, whatever the width.
379    #[test]
380    fn a_plain_buffer_loads_along_its_innermost_axis() {
381        assert_eq!(VectorTile::new(&[], N, 4).unwrap().extents(), &[(N, 4)]);
382        assert_eq!(VectorTile::new(&[], N, 1).unwrap().extents(), &[(N, 1)]);
383    }
384
385    /// An NVFP4 load of four words, two along `K` and two columns, is a 16 by 2 rectangle; two
386    /// words are one column's 16 values, and one word its 8.
387    #[test]
388    fn a_stored_buffer_loads_its_finest_tiles() {
389        let stored = [(K, 8), (K, 2), (N, 2), (K, 32), (N, 8)];
390        assert_eq!(
391            VectorTile::new(&stored, N, 32).unwrap().extents(),
392            &[(K, 16), (N, 2)]
393        );
394        assert_eq!(
395            VectorTile::new(&stored, N, 16).unwrap().extents(),
396            &[(K, 16)]
397        );
398        assert_eq!(VectorTile::new(&stored, N, 8).unwrap().extents(), &[(K, 8)]);
399        assert_eq!(VectorTile::new(&stored, N, 32).unwrap().values(), 32);
400    }
401
402    /// A load never cuts a stored tile: four values of an eight-value word, or 24 values that would
403    /// take one and a half of the next tile, are refused. Neither may it outgrow what is stated.
404    #[test]
405    fn a_load_that_cuts_a_stored_tile_is_refused() {
406        let stored = [(K, 8), (N, 2)];
407        assert_eq!(
408            VectorTile::new(&stored, N, 4),
409            Err(TileMisfit::Overshoots {
410                axis: K,
411                wanted: 4,
412                piece: 8
413            })
414        );
415        assert!(VectorTile::new(&stored, N, 24).is_err());
416        assert!(VectorTile::new(&stored, N, 32).is_err());
417    }
418
419    /// A load is one extent per axis: stored tiles that go along `K`, then `N`, then `K` again
420    /// make no load that spans all three, since its values along `K` would not be one run.
421    #[test]
422    fn a_load_that_returns_to_an_axis_is_refused() {
423        assert_eq!(
424            VectorTile::new(&[(K, 8), (N, 2), (K, 2)], N, 32),
425            Err(TileMisfit::Interleaved {
426                wanted: K,
427                found: N
428            })
429        );
430    }
431
432    /// A value's place in a load reads off its position unravelled over the load's extents, finest
433    /// first: the 21st value of a 16 by 2 load is the second column's fifth, a run's is itself.
434    #[test]
435    fn a_position_in_a_load_is_an_offset_along_each_axis() {
436        let load = VectorTile::new(&[(K, 8), (K, 2), (N, 2)], N, 32).unwrap();
437        assert_eq!(load.offset_along(20, K), 4);
438        assert_eq!(load.offset_along(20, N), 1);
439        assert_eq!(VectorTile::run(N, 4).offset_along(3, N), 3);
440        assert_eq!(VectorTile::run(N, 4).offset_along(3, K), 0);
441    }
442
443    /// A stated tile of one is a dim of the buffer, not a piece of a load.
444    #[test]
445    fn a_stored_tile_of_one_is_skipped() {
446        let stored = [(N, 4), (N, 1), (K, 2)];
447        assert_eq!(
448            VectorTile::new(&stored, N, 8).unwrap().extents(),
449            &[(N, 4), (K, 2)]
450        );
451    }
452
453    /// A reader that places a load along one axis refuses a rectangle, naming itself, rather
454    /// than landing half its values on the wrong column.
455    #[test]
456    #[should_panic(expected = "the fragment load places a load along one axis")]
457    fn a_rectangle_is_refused_by_a_one_axis_reader() {
458        let tile = VectorTile::new(&[(K, 8), (N, 2)], N, 16).unwrap();
459        tile.along_one_axis("the fragment load");
460    }
461
462    /// Read in loads, a plain buffer's innermost dim counts lines and nothing else moves; an
463    /// NVFP4 buffer's word, its second word along `K` and its second column are each one load's
464    /// whole, wherever the level-major order puts them, and a batch dim ahead is untouched.
465    #[test]
466    fn a_load_covers_the_innermost_dim_or_the_stored_tiles_it_takes() {
467        let run = VectorTile::run(N, 4);
468        assert_eq!(run.parts(&[], &[K, N], 2), vec![1, 4]);
469
470        // `[m, k]` stored as a word of 8 along `K`, 2 words along `K` by 2 rows, then 4 by 4:
471        // `[M, K, M, K, M, K, K]` level-major, the word the last dim.
472        const M: Axis = Axis(2);
473        let stored = [(K, 8), (K, 2), (M, 2), (K, 4), (M, 4)];
474        let labels = [M, K, M, K, M, K, K];
475        let load = VectorTile::new(&stored, K, 32).unwrap();
476        assert_eq!(load.extents(), &[(K, 16), (M, 2)]);
477        assert_eq!(load.parts(&stored, &labels, 7), vec![1, 1, 1, 1, 2, 2, 8]);
478        assert_eq!(
479            load.parts(&stored, &labels, 8),
480            vec![1, 1, 1, 1, 1, 2, 2, 8]
481        );
482    }
483
484    /// Every stated tile's dim reads at the tile's count, and only the grid dims are left to the
485    /// buffer's shape: a read over the tiles then divides by constants. A batch dim ahead is
486    /// the buffer's too.
487    #[test]
488    fn the_stated_tiles_have_their_stated_extents() {
489        const M: Axis = Axis(2);
490        let stored = [(K, 8), (K, 2), (M, 2), (K, 4), (M, 4)];
491        let labels = [M, K, M, K, M, K, K];
492        let tiled = [Some(4), Some(4), Some(2), Some(2), Some(8)];
493        let expected: Vec<_> = [None, None].into_iter().chain(tiled).collect();
494        assert_eq!(VectorTile::stated_extents(&stored, &labels, 7), expected);
495        let batched: Vec<_> = [None].into_iter().chain(expected).collect();
496        assert_eq!(VectorTile::stated_extents(&stored, &labels, 8), batched);
497    }
498}