Skip to main content

cubecl_zspace/
metadata.rs

1use serde::{Deserialize, Serialize};
2use smallvec::SmallVec;
3
4use crate::{
5    INLINE_DIMS, MetadataError,
6    shape::Shape,
7    strides::Strides,
8    tiling::{MAX_FRAGMENTS, Tiling},
9};
10
11#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)]
12pub struct Metadata {
13    pub shape: Shape,
14    pub strides: Strides,
15    /// How many fragments each logical dim is stored as; untiled by default.
16    /// The shape and strides stay physical. See [`Tiling`].
17    pub tiling: Tiling,
18}
19
20impl Metadata {
21    pub fn new(shape: impl Into<Shape>, strides: impl Into<Strides>) -> Self {
22        let shape = shape.into();
23        let strides = strides.into();
24        debug_assert_eq!(
25            shape.rank(),
26            strides.rank(),
27            "Rank of shape and strides must be the same"
28        );
29
30        Self {
31            shape,
32            strides,
33            tiling: Tiling::UNTILED,
34        }
35    }
36
37    /// This metadata with `tiling` labelling its physical dims.
38    ///
39    /// # Errors
40    ///
41    /// When `tiling` does not describe this rank: see [`Tiling::new`].
42    pub fn with_tiling(mut self, tiling: Tiling) -> Result<Self, MetadataError> {
43        tiling.logical_rank(self.rank())?;
44        self.tiling = tiling;
45        Ok(self)
46    }
47
48    /// Whether any logical dim is stored as more than one fragment.
49    pub fn is_tiled(&self) -> bool {
50        self.tiling.is_tiled()
51    }
52
53    /// How many dims this buffer stands for: its rank, less the extra fragments
54    /// the tiling splits dims into.
55    ///
56    /// # Errors
57    ///
58    /// When the tiling does not fit this rank: see [`Tiling::logical_rank`].
59    pub fn logical_rank(&self) -> Result<usize, MetadataError> {
60        self.tiling.logical_rank(self.rank())
61    }
62
63    /// The extents this buffer stands for: each logical dim's fragments
64    /// multiplied back together, in logical order. An untiled metadata gives its
65    /// shape unchanged.
66    ///
67    /// # Errors
68    ///
69    /// When the tiling does not fit this rank: see [`Tiling::logical_rank`].
70    pub fn logical_shape(&self) -> Result<Shape, MetadataError> {
71        let fragments = self.tiling.fragments(self.logical_rank()?);
72        let mut extents = SmallVec::<[usize; INLINE_DIMS]>::from_elem(1, fragments.len());
73        // A dim's fragments are spread through the buffer rather than adjacent,
74        // so walk the levels in the order the storage was laid out, coarsest
75        // first, and each dim collects its own.
76        let dims = self.shape.as_slice();
77        let mut physical = 0;
78        for level in 0..MAX_FRAGMENTS {
79            for (dim, &count) in fragments.iter().enumerate() {
80                if level < count {
81                    extents[dim] *= dims[physical];
82                    physical += 1;
83                }
84            }
85        }
86        Ok(Shape::new_raw(extents))
87    }
88
89    /// The dim-changing ops do not carry a tiling yet: they refuse rather than
90    /// return counts over dims that moved.
91    fn assert_untiled(&self, op: &str) {
92        assert!(
93            !self.is_tiled(),
94            "Metadata::{op} on a storage-tiled tensor is not supported: {:?}",
95            self.tiling
96        );
97    }
98
99    pub fn shape(&self) -> &Shape {
100        &self.shape
101    }
102
103    /// The shape, to rewrite in place. A storage-tiled tensor's dims are its tiling's fragments,
104    /// which a rewrite would leave stale, so it refuses like the dim-changing ops do.
105    pub fn shape_mut(&mut self) -> &mut Shape {
106        self.assert_untiled("shape_mut");
107        &mut self.shape
108    }
109
110    pub fn strides(&self) -> &Strides {
111        &self.strides
112    }
113
114    /// The strides, to rewrite in place. Refuses on a storage-tiled tensor, as
115    /// [`shape_mut`](Self::shape_mut) does: its strides step its tiles, and a rewrite would
116    /// keep the tiling over strides that no longer do.
117    pub fn strides_mut(&mut self) -> &mut Strides {
118        self.assert_untiled("strides_mut");
119        &mut self.strides
120    }
121
122    pub fn rank(&self) -> usize {
123        self.num_dims()
124    }
125
126    pub fn num_dims(&self) -> usize {
127        self.shape.num_dims()
128    }
129
130    /// Returns the total number of elements of a tensor having this shape
131    pub fn num_elements(&self) -> usize {
132        self.shape.num_elements()
133    }
134
135    pub fn swapped(mut self, dim0: usize, dim1: usize) -> Self {
136        self.swap(dim0, dim1);
137        self
138    }
139
140    pub fn swap(&mut self, dim0: usize, dim1: usize) {
141        self.assert_untiled("swap");
142        debug_assert!(dim0 < self.rank(), "dim0 is out of bounds");
143        debug_assert!(dim1 < self.rank(), "dim1 is out of bounds");
144        self.shape.swap(dim0, dim1);
145        self.strides.swap(dim0, dim1);
146    }
147
148    /// Reorder the shape dimensions according to the permutation of `axes`.
149    pub fn permute(&mut self, axes: &[usize]) -> Result<(), MetadataError> {
150        self.assert_untiled("permute");
151        self.shape.permute(axes)?;
152        self.strides.permute(axes)?;
153
154        Ok(())
155    }
156
157    pub fn permuted(mut self, axes: &[usize]) -> Result<Self, MetadataError> {
158        self.permute(axes)?;
159        Ok(self)
160    }
161
162    /// Insert a dimension of `shape` with `stride` at position `index`.
163    pub fn insert(&mut self, index: usize, shape: usize, stride: usize) {
164        self.assert_untiled("insert");
165        self.shape.insert(index, shape);
166        self.strides.insert(index, stride);
167    }
168
169    /// Remove and return the dimension at position `index` from the metadata.
170    pub fn remove(&mut self, index: usize) -> (usize, usize) {
171        self.assert_untiled("remove");
172        let shape = self.shape.remove(index);
173        let stride = self.strides.remove(index);
174        (shape, stride)
175    }
176
177    /// Appends a dimension of `shape` with `stride` to the back of the metadata.
178    pub fn push(&mut self, shape: usize, stride: usize) {
179        self.assert_untiled("push");
180        self.shape.push(shape);
181        self.strides.push(stride);
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    /// A `[b, m, k]` operand stored `[Bs, Mx, Ky, Mi, Kj]`: the two tiled dims
190    /// collect a fragment from each level, the untiled one drops out after the
191    /// first.
192    #[test]
193    fn logical_shape_multiplies_a_dim_s_fragments_back_together() {
194        let meta = Metadata::new(
195            [2, 128, 344, 32, 32],
196            [128 * 344 * 1024, 344 * 1024, 1024, 32, 1],
197        )
198        .with_tiling(Tiling::new(&[1, 2, 2]).unwrap())
199        .unwrap();
200
201        assert_eq!(meta.logical_rank(), Ok(3));
202        assert_eq!(meta.logical_shape(), Ok(Shape::new([2, 4096, 11008])));
203    }
204
205    /// The untiled case is the identity, so a caller reasoning in logical dims
206    /// need not ask whether the tensor is tiled first.
207    #[test]
208    fn an_untiled_metadata_stands_for_its_own_shape() {
209        let meta = Metadata::new([2, 4096, 11008], [4096 * 11008, 11008, 1]);
210
211        assert_eq!(meta.logical_rank(), Ok(3));
212        assert_eq!(meta.logical_shape(), Ok(meta.shape.clone()));
213    }
214
215    /// Three levels deep on one dim, one on another: the fragment counts need
216    /// not match, and the physical order stays level-major.
217    #[test]
218    fn dims_tiled_to_different_depths_each_collect_their_own() {
219        let meta = Metadata::new([4, 8, 2, 4, 2], [1; 5])
220            .with_tiling(Tiling::new(&[3, 2]).unwrap())
221            .unwrap();
222
223        assert_eq!(meta.logical_shape(), Ok(Shape::new([4 * 2 * 2, 8 * 4])));
224    }
225
226    /// A buffer too short to hold the fragments is the caller pairing the wrong
227    /// tensor with the wrong description, and says so rather than guessing.
228    #[test]
229    fn a_buffer_too_short_for_the_tiling_is_refused() {
230        let mut meta = Metadata::new([2, 128, 344, 32, 32], [1; 5])
231            .with_tiling(Tiling::new(&[1, 2, 2]).unwrap())
232            .unwrap();
233        meta.shape.remove(4);
234        meta.strides.remove(4);
235        meta.shape.remove(3);
236        meta.strides.remove(3);
237
238        assert!(meta.logical_rank().is_err());
239        assert!(meta.logical_shape().is_err());
240    }
241}