Skip to main content

cubecl_tile/layout/storage/
geometry.rs

1//! An operand's extents and strides: [`Geometry`] on the host, [`RuntimeGeometry`] in the kernel.
2
3use cubecl::prelude::*;
4
5use core::fmt::{self, Display, Formatter};
6
7use cubecl::zspace::{SmallVec, Tiling};
8
9use crate::{Axis, Space, StoragePartitioning, TileMisfit};
10
11use crate::Coords;
12
13/// One operand's physical `(extent, stride)` per dim, in scalars, and its storage tiling.
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub struct Geometry {
16    shape: Vec<usize>,
17    strides: Vec<usize>,
18    tiling: Tiling,
19}
20
21impl Geometry {
22    /// One `(extent, stride)` per physical dim, coarsest first, both in scalars.
23    pub fn new(dims: &[(usize, usize)]) -> Self {
24        Self {
25            shape: dims.iter().map(|&(extent, _)| extent).collect(),
26            strides: dims.iter().map(|&(_, stride)| stride).collect(),
27            tiling: Tiling::UNTILED,
28        }
29    }
30
31    /// A dense row-major buffer of `shape`, coarsest first: what an output with no tensor behind it
32    /// is counted in.
33    pub fn row_major(shape: &[usize]) -> Self {
34        let mut strides = vec![1; shape.len()];
35        for dim in (0..shape.len().saturating_sub(1)).rev() {
36            strides[dim] = strides[dim + 1] * shape[dim + 1];
37        }
38        Self {
39            shape: shape.to_vec(),
40            strides,
41            tiling: Tiling::UNTILED,
42        }
43    }
44
45    /// This geometry with its dims stated as pieces of logical dims, as `tiling` says.
46    pub(crate) fn with_tiling(mut self, tiling: Tiling) -> Self {
47        self.tiling = tiling;
48        self
49    }
50
51    /// Which dims are pieces of one logical dim: untiled unless the binding said otherwise.
52    pub fn tiling(&self) -> Tiling {
53        self.tiling
54    }
55
56    /// Its trailing dims, coarsest first, each named by the axis it is a piece of: `axes` name the
57    /// trailing logical dims in the tensor's order, each once per piece its tiling stores it as, in
58    /// [`StoragePartitioning::labels`]' order. The dims ahead of them are batch dims, one each.
59    ///
60    /// # Panics
61    ///
62    /// When the tiling does not fit the rank, when the buffer stands for fewer logical dims than
63    /// `axes` name, or when it tiles a batch dim, which is stored one dim.
64    pub fn labels(&self, axes: &[Axis]) -> Vec<Axis> {
65        if !self.tiling.is_tiled() {
66            return axes.to_vec();
67        }
68        let logical_rank = self
69            .tiling
70            .logical_rank(self.rank())
71            .unwrap_or_else(|e| panic!("Geometry::labels: {e:?}"));
72        assert!(
73            logical_rank >= axes.len(),
74            "Geometry::labels: the buffer stands for {logical_rank} dims but {} axes name them",
75            axes.len()
76        );
77        let fragments = self.tiling.fragments(logical_rank);
78        let (batches, block) = fragments.split_at(logical_rank - axes.len());
79        assert!(
80            batches.iter().all(|&n| n == 1),
81            "Geometry::labels: batch dims are stored one dim each, so {batches:?} pieces over \
82             them cannot be named"
83        );
84        StoragePartitioning::level_major(axes, block)
85    }
86
87    /// The extents, coarsest first.
88    pub fn shape(&self) -> &[usize] {
89        &self.shape
90    }
91
92    /// The strides that step them, in scalars.
93    pub fn strides(&self) -> &[usize] {
94        &self.strides
95    }
96
97    /// How many physical dims the operand has.
98    pub fn rank(&self) -> usize {
99        self.shape.len()
100    }
101
102    /// This geometry with its trailing `labels.len()` dims and labels reordered by stride, coarsest
103    /// first; leading (batch) dims stay put.
104    pub fn in_stride_order(&self, labels: &[Axis]) -> (Geometry, Vec<Axis>) {
105        let batch_dims = self.rank() - labels.len();
106        let mut trailing: Vec<(usize, (usize, usize))> =
107            self.dims().enumerate().skip(batch_dims).collect();
108        trailing.sort_by_key(|&(_, dim)| stride_order(dim));
109        let dims: Vec<(usize, usize)> = self
110            .dims()
111            .take(batch_dims)
112            .chain(trailing.iter().map(|&(_, dim)| dim))
113            .collect();
114        let ordered = trailing
115            .iter()
116            .map(|&(dim, _)| labels[dim - batch_dims])
117            .collect();
118        (Geometry::new(&dims), ordered)
119    }
120
121    pub fn dims(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
122        self.shape.iter().copied().zip(self.strides.iter().copied())
123    }
124
125    /// Whether this buffer can be read a `tile` at a time, `tile` finest first over the axes
126    /// `labels` name, right-aligned to the dims.
127    pub fn serves(&self, tile: &[(Axis, usize)], labels: &[Axis]) -> Result<(), LineMisfit> {
128        let values: usize = tile.iter().map(|&(_, count)| count).product();
129        if values == 1 {
130            return Ok(());
131        }
132        if self.rank() == 0 {
133            return Err(LineMisfit::NoDims);
134        }
135        let storage = StoragePartitioning::new(self, labels).ok_or(LineMisfit::NotPartitioned)?;
136        storage
137            .holds(tile, &self.extents(labels))
138            .map_err(LineMisfit::Tile)?;
139        // The run, from the finest dim up: each dim it takes steps by what it has taken so far.
140        let mut by_stride: SmallVec<[(usize, (usize, usize)); 8]> = self
141            .dims()
142            .enumerate()
143            .filter(|&(_, (extent, stride))| extent > 1 && stride > 0)
144            .collect();
145        by_stride.sort_by_key(|&(dim, (_, stride))| (stride, core::cmp::Reverse(dim)));
146        let mut inside: SmallVec<[usize; 8]> = SmallVec::new();
147        let mut run = 1;
148        for &(dim, (extent, stride)) in &by_stride {
149            if run >= values {
150                break;
151            }
152            if stride != run {
153                return Err(match run {
154                    1 => LineMisfit::InnermostStrided(stride),
155                    _ => LineMisfit::Gap(stride),
156                });
157            }
158            inside.push(dim);
159            run *= extent;
160        }
161        // A dim of one element steps nowhere, so its stride lands no value inside a line.
162        match self.dims().enumerate().find(|&(dim, (extent, stride))| {
163            extent > 1 && !inside.contains(&dim) && !stride.is_multiple_of(values)
164        }) {
165            Some((_, (_, stride))) => Err(LineMisfit::StrideInsideLine(stride)),
166            None => Ok(()),
167        }
168    }
169
170    /// Each labelled axis's extent: the product of the dims it labels.
171    pub(crate) fn extents(&self, labels: &[Axis]) -> Vec<(Axis, usize)> {
172        let unlabelled = self.rank().saturating_sub(labels.len());
173        let mut extents: Vec<(Axis, usize)> = Vec::new();
174        for (dim, extent) in self.shape.iter().copied().enumerate().skip(unlabelled) {
175            let axis = labels[dim - unlabelled];
176            match extents.iter_mut().find(|(a, _)| *a == axis) {
177                Some((_, product)) => *product *= extent,
178                None => extents.push((axis, extent)),
179            }
180        }
181        extents
182    }
183
184    /// The dims with extent above one and non-zero stride, as `(index, (extent, stride))`,
185    /// coarsest stride first.
186    pub(crate) fn addressing_dims_by_stride(
187        &self,
188    ) -> SmallVec<[(usize, (usize, usize)); Space::MAX_RANK]> {
189        let mut dims: SmallVec<[(usize, (usize, usize)); Space::MAX_RANK]> = self
190            .dims()
191            .enumerate()
192            .filter(|&(_, (extent, stride))| extent > 1 && stride > 0)
193            .collect();
194        dims.sort_by_key(|&(_, (_, stride))| core::cmp::Reverse(stride));
195        dims
196    }
197}
198
199/// Why a [`Geometry`] cannot be read a tile at a time ([`Geometry::serves`]).
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum LineMisfit {
202    /// The finest dim's own stride, when it is not 1: consecutive values are not one run.
203    InnermostStrided(usize),
204    /// A dim inside the tile that does not step by what is finer than it: a gap in the run.
205    Gap(usize),
206    /// The storage partitioning does not hold the tile: see [`TileMisfit`].
207    Tile(TileMisfit),
208    /// A stride outside the tile that is not a whole number of tiles.
209    StrideInsideLine(usize),
210    /// The buffer's order is no partitioning: a stated piece coarser than the rest of an axis.
211    NotPartitioned,
212    /// No dims at all, so only a single value fits.
213    NoDims,
214}
215
216impl Display for LineMisfit {
217    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
218        match self {
219            Self::InnermostStrided(stride) => write!(
220                f,
221                "its finest dim steps by {stride} rather than 1, so consecutive values are not \
222                 one run"
223            ),
224            Self::Gap(stride) => write!(
225                f,
226                "a dim inside the tile steps by {stride}, leaving a gap in its run"
227            ),
228            Self::Tile(why) => write!(f, "its storage does not hold the tile: {why}"),
229            Self::StrideInsideLine(stride) => write!(
230                f,
231                "its stride {stride} is not a whole number of tiles, so a coarser step lands \
232                 inside one"
233            ),
234            Self::NotPartitioned => write!(
235                f,
236                "a stated piece sits coarser than the rest of an axis, so its order is no \
237                 partitioning"
238            ),
239            Self::NoDims => write!(f, "it has no dims, so only a single value fits"),
240        }
241    }
242}
243
244impl From<&TensorBinding> for Geometry {
245    fn from(binding: &TensorBinding) -> Self {
246        Self {
247            shape: binding.shape.to_vec(),
248            strides: binding.strides.to_vec(),
249            tiling: binding.tiling,
250        }
251    }
252}
253
254/// [`Geometry`]'s kernel-side twin.
255#[derive(CubeType, Clone)]
256#[expand(derive(Clone))]
257pub struct RuntimeGeometry {
258    pub(crate) shape: Coords<u32>,
259    pub(crate) strides: Coords<u32>,
260}
261
262#[cube]
263impl RuntimeGeometry {
264    /// An empty geometry, grown a dim at a time by `push`.
265    // A cube constructor: `Default` has no kernel expansion.
266    #[allow(clippy::new_without_default)]
267    pub fn new() -> RuntimeGeometry {
268        RuntimeGeometry {
269            shape: Coords::<u32>::new(),
270            strides: Coords::<u32>::new(),
271        }
272    }
273
274    /// The geometry a launched tensor carries, over its first `rank` dims.
275    pub fn of_tensor<E: CubePrimitive>(
276        tensor: &Tensor<E>,
277        #[comptime] rank: usize,
278    ) -> RuntimeGeometry {
279        let mut geometry = RuntimeGeometry::new();
280        #[unroll]
281        for i in 0..rank {
282            geometry.push(tensor.shape(i) as u32, tensor.stride(i) as u32);
283        }
284        geometry
285    }
286
287    /// One physical dim, coarsest first: how far it runs, and the scalar stride that steps it.
288    pub fn push(&mut self, extent: u32, stride: u32) {
289        self.shape.push(extent);
290        self.strides.push(stride);
291    }
292}
293
294/// Where a dim of `(extent, stride)` sorts among a buffer's dims in stride order: by stride,
295/// coarsest first, and on a tie a dim of one element before the others. It steps nowhere, so its
296/// stride places it nowhere: a column `[k, 1]` at strides `[1, 1]` runs along `k`, and its lines
297/// run along `k` too, not along the dim one element long.
298pub(crate) fn stride_order((extent, stride): (usize, usize)) -> (core::cmp::Reverse<usize>, bool) {
299    (core::cmp::Reverse(stride), extent != 1)
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use crate::StorageLevels;
306
307    const M: Axis = Axis(0);
308    const N: Axis = Axis(1);
309    const K: Axis = Axis(2);
310
311    #[test]
312    fn stride_order_reorders_the_dims_and_their_labels() {
313        // `[n, k]` behind a `[k, n]` view: `k` strides by one.
314        let (dims, labels) = Geometry::new(&[(4096, 1), (6144, 4096)]).in_stride_order(&[K, N]);
315        assert_eq!(labels, vec![N, K]);
316        assert_eq!(dims, Geometry::new(&[(6144, 4096), (4096, 1)]));
317
318        // Already in order: nothing moves.
319        let (dims, labels) = Geometry::new(&[(4096, 6144), (6144, 1)]).in_stride_order(&[K, N]);
320        assert_eq!(labels, vec![K, N]);
321        assert_eq!(dims, Geometry::new(&[(4096, 6144), (6144, 1)]));
322
323        // A broadcast batch dim strides by zero and stays first all the same.
324        let (dims, labels) =
325            Geometry::new(&[(8, 0), (4096, 1), (6144, 4096)]).in_stride_order(&[K, N]);
326        assert_eq!(labels, vec![N, K]);
327        assert_eq!(dims, Geometry::new(&[(8, 0), (6144, 4096), (4096, 1)]));
328    }
329
330    /// On untiled buffers, `serves` agrees with the one-axis line rule ([`line_rule_serves`]).
331    #[test]
332    fn a_line_is_served_where_the_line_rule_served_it() {
333        let geometries: Vec<(Geometry, Vec<Axis>)> = vec![
334            (Geometry::new(&[(64, 128), (128, 1)]), vec![M, N]),
335            (Geometry::new(&[(64, 136), (128, 1)]), vec![M, N]),
336            (Geometry::new(&[(64, 130), (130, 1)]), vec![M, N]),
337            (Geometry::new(&[(64, 1), (128, 64)]), vec![M, N]),
338            (Geometry::new(&[(3, 0), (64, 128), (128, 1)]), vec![M, N]),
339            (Geometry::new(&[(1, 5), (64, 128), (128, 1)]), vec![M, N]),
340            (Geometry::new(&[(2, 4096), (32, 1)]), vec![K, N]),
341            (
342                Geometry::new(&[(4, 2048), (4, 512), (16, 32), (32, 1)]),
343                vec![K, N, K, N],
344            ),
345            (Geometry::new(&[(6, 1)]), vec![N]),
346            // A row viewed out of a column: its single row keeps the column's stride.
347            (Geometry::new(&[(1, 1), (8, 1)]), vec![M, N]),
348        ];
349        for (geometry, labels) in &geometries {
350            let line = *labels.last().unwrap();
351            for width in [1, 2, 3, 4, 8, 16, 32] {
352                assert_eq!(
353                    geometry.serves(&[(line, width)], labels).is_ok(),
354                    line_rule_serves(geometry, width),
355                    "{geometry:?} at {width}"
356                );
357            }
358        }
359    }
360
361    #[test]
362    fn a_tiled_buffer_serves_the_reads_it_was_stored_for() {
363        let storage = StorageLevels::new(&[(N, 4)])
364            .tile(&[(N, 8), (K, 32)])
365            .grid(&[N, K]);
366        let geometry = storage.physical(&[(K, 64), (N, 64)]).unwrap();
367        let labels = geometry.labels(&[K, N]);
368        assert_eq!(geometry.serves(&[(N, 4)], &labels), Ok(()));
369        assert_eq!(geometry.serves(&[(N, 32), (K, 32)], &labels), Ok(()));
370        assert!(geometry.serves(&[(N, 2)], &labels).is_err());
371        assert!(geometry.serves(&[(N, 8)], &labels).is_err());
372    }
373
374    /// The one-axis line rule: a line of `width` is served where the innermost dim is contiguous
375    /// and its extent and every outer stride are multiples of `width`; an outer dim of one
376    /// element steps nowhere, so its stride counts for nothing.
377    fn line_rule_serves(geometry: &Geometry, width: usize) -> bool {
378        if width == 1 {
379            return true;
380        }
381        let Some(last) = geometry.rank().checked_sub(1) else {
382            return false;
383        };
384        geometry.strides()[last] == 1
385            && geometry.shape()[last].is_multiple_of(width)
386            && geometry.strides()[..last]
387                .iter()
388                .zip(geometry.shape())
389                .all(|(s, &extent)| extent == 1 || s.is_multiple_of(width))
390    }
391}