Skip to main content

burn_std/tensor/
mod.rs

1/// Generic container for storing tensors keyed by an id.
2pub mod container;
3/// Tensor data type definitions.
4pub mod dtype;
5/// The order a tensor's dimensions occupy memory in.
6pub mod layout;
7/// Batched matmul transformation utilities.
8pub mod matmul;
9/// Quantization data representation.
10pub mod quantization;
11/// Tensor shape utilities.
12pub mod shape;
13/// Tensor slicing utilities.
14pub mod slice;
15
16pub use dtype::*;
17pub use layout::*;
18pub use matmul::*;
19pub use quantization::*;
20pub use shape::*;
21pub use slice::*;
22
23pub use cubecl_zspace::indexing::{self, *};
24pub use cubecl_zspace::{Strides, metadata::Metadata, strides};
25
26/// Check if the current tensor is contiguous.
27///
28/// A tensor is considered contiguous if its elements are stored in memory
29/// such that the stride at position `k` is equal to the product of the shapes
30/// of all dimensions greater than `k`, except for dimensions of size one.
31///
32/// This means that strides increase as you move from the rightmost to the leftmost dimension,
33/// ignoring dimensions of size one.
34///
35/// Dimensions of size one may have any stride, including zero, since their stride is never applied.
36pub fn is_contiguous(shape: &[usize], strides: &[usize]) -> bool {
37    if shape.is_empty() {
38        return true;
39    }
40
41    // A dimension of size one is visited once, so its stride is never applied and can hold
42    // anything: a permute leaves whatever the axis carried before, and broadcast views use 0.
43    // Comparing it would report a contiguous tensor as strided, as [2, 3, 1] with strides
44    // [3, 1, 3] coming out of `permute([0, 2, 1])`.
45    for ((&expected, &stride), &dim) in contiguous_strides(shape)
46        .iter()
47        .zip(strides)
48        .zip(shape.iter())
49    {
50        if dim != 1 && expected != stride {
51            return false;
52        }
53    }
54
55    true
56}
57
58/// Check if the current tensor fills its buffer without gaps.
59///
60/// Unlike [is_contiguous], this holds for any ordering of the dimensions: a permuted tensor is
61/// dense, a tensor whose rows were padded by a pitched allocator is not. Dimensions of size one
62/// carry no information about the layout and are ignored.
63pub fn is_dense(shape: &[usize], strides: &[usize]) -> bool {
64    if shape.len() != strides.len() {
65        return false;
66    }
67
68    let mut dims: SmallVec<[(usize, usize); 5]> = shape
69        .iter()
70        .zip(strides)
71        .filter(|&(&dim, _)| dim > 1)
72        .map(|(&dim, &stride)| (dim, stride))
73        .collect();
74
75    dims.sort_unstable_by_key(|&(_, stride)| stride);
76
77    let mut expected = 1;
78
79    for (dim, stride) in dims {
80        if stride != expected {
81            return false;
82        }
83
84        expected *= dim;
85    }
86
87    true
88}
89
90/// Computes the strides for a contiguous tensor with the given shape.
91///
92/// In a contiguous row-major tensor, the stride for each dimension
93/// equals the product of all dimension sizes to its right.
94pub fn contiguous_strides(shape: &[usize]) -> Strides {
95    let mut strides = strides![0; shape.len()];
96    let mut current = 1;
97
98    for (i, &dim) in shape.iter().enumerate().rev() {
99        strides[i] = current;
100        current *= dim;
101    }
102
103    strides
104}
105
106/// The action to take for a reshape operation.
107#[derive(Debug)]
108pub enum ReshapeAction {
109    /// Updating the strides is sufficient to handle the reshape.
110    UpdateStrides {
111        /// The new strides.
112        strides: Strides,
113    },
114    /// The strides are not compatible, we should recompute the buffer.
115    Recompute,
116    /// The strides are already correct.
117    NoChange,
118}
119
120/// The reshape kind.
121#[derive(Debug, PartialEq)]
122pub enum ReshapeAnalysis {
123    /// Original tensor is contiguous, can update the strides.
124    IsContiguous,
125    /// Original tensor is highly permuted, can't update the strides.
126    HighlyPermuted,
127    /// Only batch dimensions are added, can update the strides.
128    Broadcasted,
129    /// Dimensions are only split, can update the strides.
130    Split,
131    /// Original tensor is bigger than output shape.
132    SmallerRank,
133    /// New shape is the same.
134    NoChange,
135}
136
137impl ReshapeAnalysis {
138    /// Returns the proper action to take for the current analysis.
139    pub fn action(&self, shape: &[usize], strides: &[usize], shape_new: &[usize]) -> ReshapeAction {
140        match self {
141            ReshapeAnalysis::IsContiguous => ReshapeAction::UpdateStrides {
142                strides: contiguous_strides(shape_new),
143            },
144            ReshapeAnalysis::NoChange => ReshapeAction::NoChange,
145            ReshapeAnalysis::HighlyPermuted | ReshapeAnalysis::SmallerRank => {
146                ReshapeAction::Recompute
147            }
148            ReshapeAnalysis::Broadcasted => {
149                let shape_rank = shape.len();
150                let shape_new_rank = shape_new.len();
151                let n_new_batch = shape_new_rank - shape_rank;
152                let num_elems = shape.iter().product::<usize>();
153                let strides_new = broadcast_strides(n_new_batch, shape_rank, num_elems, strides);
154
155                ReshapeAction::UpdateStrides {
156                    strides: strides_new,
157                }
158            }
159            ReshapeAnalysis::Split => {
160                let strides_new = split_strides(shape, strides, shape_new);
161
162                ReshapeAction::UpdateStrides {
163                    strides: strides_new,
164                }
165            }
166        }
167    }
168}
169
170/// Returns the proper action to take when reshaping a tensor.
171pub fn reshape_action(shape: &Shape, strides: &Strides, shape_new: &Shape) -> ReshapeAction {
172    reshape_analysis(shape, Some(strides), shape_new).action(shape, strides, shape_new)
173}
174
175/// Calculate the new strides given added batch dimensions.
176pub fn broadcast_strides(
177    n_new_batch: usize,
178    rank_prev: usize,
179    num_elems: usize,
180    strides: &[usize],
181) -> Strides {
182    let mut strides_new = strides![num_elems; rank_prev + n_new_batch];
183
184    for (i, s) in strides.iter().enumerate() {
185        strides_new[i + n_new_batch] = *s;
186    }
187
188    strides_new
189}
190
191/// Calculate the new strides given added split dimensions.
192pub fn split_strides(shape: &[usize], strides: &[usize], shape_new: &[usize]) -> Strides {
193    let mut strides_new = strides![1; shape_new.len()];
194
195    // Unit dims in the old shape never anchor a group of new dims, and their
196    // stride can be arbitrary (0 for broadcast views, pitched values, ...).
197    // Skip them so a real new dim never inherits a unit dim's stride —
198    // e.g. reshaping [26, 1] with strides [1, 0] (a `repeat_dim` broadcast
199    // view) to [26, 1, 1] must keep stride 1 on dim 0; propagating the 0
200    // would make every index along dim 0 alias the first element.
201    let skip_unit_dims = |mut idx: usize| {
202        while idx > 0 && shape[idx] == 1 {
203            idx -= 1;
204        }
205        idx
206    };
207
208    let mut old_idx = skip_unit_dims(shape.len() - 1);
209    let mut current_stride = strides[old_idx];
210    let mut dim_prod = 1;
211
212    for (i, dim) in shape_new.iter().enumerate().rev() {
213        dim_prod *= *dim;
214        strides_new[i] = current_stride;
215        if *dim == 1 {
216            continue;
217        } else if dim_prod == shape[old_idx] {
218            old_idx = skip_unit_dims(old_idx.saturating_sub(1));
219            current_stride = strides[old_idx];
220            dim_prod = 1;
221        } else {
222            current_stride *= *dim;
223        }
224    }
225
226    strides_new
227}
228
229/// Returns the analysis of a reshape operation.
230pub fn reshape_analysis(
231    shape: &Shape,
232    strides: Option<&Strides>,
233    shape_new: &Shape,
234) -> ReshapeAnalysis {
235    let shape_rank = shape.len();
236    let shape_new_rank = shape_new.len();
237
238    let is_contiguous = match strides {
239        Some(strides) => is_contiguous(shape, strides),
240        None => false,
241    };
242
243    if is_contiguous {
244        return ReshapeAnalysis::IsContiguous;
245    }
246
247    if shape_new_rank < shape_rank {
248        return ReshapeAnalysis::SmallerRank;
249    }
250
251    let n_new_batch = shape_new_rank - shape_rank;
252
253    match n_new_batch > 0 {
254        true => {
255            if shape.as_ref() == &shape_new[n_new_batch..shape_new_rank]
256                && shape_new[0..n_new_batch].iter().all(|it| *it == 1)
257            {
258                return ReshapeAnalysis::Broadcasted;
259            } else {
260                let mut dim_prod = 1;
261                let mut old_idx = 0;
262                for dim in shape_new.iter() {
263                    dim_prod *= *dim;
264
265                    // We need to ignore unit dims because they don't affect analysis and break
266                    // things because they match the default `dim_prod`. If we don't do this,
267                    // reshapes like [2, 3] to [2, 3, 1] will panic from out of bounds access.
268                    if *dim == 1 {
269                        continue;
270                    } else if dim_prod == shape[old_idx] {
271                        dim_prod = 1;
272                        old_idx += 1;
273                    } else if dim_prod > shape[old_idx] {
274                        return ReshapeAnalysis::HighlyPermuted;
275                    }
276                }
277                return ReshapeAnalysis::Split;
278            }
279        }
280
281        false => {
282            if shape == shape_new {
283                return ReshapeAnalysis::NoChange;
284            }
285        }
286    };
287
288    ReshapeAnalysis::HighlyPermuted
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn test_reshape_analysis_is_contiguous() {
297        let analysis = reshape_analysis(
298            &[32, 1, 1, 1].into(),
299            Some(&[1, 1, 1, 1].into()),
300            &[1, 1, 32, 1, 1, 1].into(),
301        );
302
303        assert_eq!(analysis, ReshapeAnalysis::IsContiguous)
304    }
305
306    #[test]
307    fn test_reshape_analysis_is_contiguous_2() {
308        let analysis = reshape_analysis(
309            &[32, 1, 1, 8].into(),
310            Some(&[8, 8, 8, 1].into()),
311            &[1, 1, 32, 1, 1, 8].into(),
312        );
313
314        assert_eq!(analysis, ReshapeAnalysis::IsContiguous)
315    }
316
317    // [32, 1, 1, 1] with strides [1, 32, 32, 32] is contiguous: only the first axis is
318    // ever indexed, and it steps by one. The trailing strides belong to axes of size one
319    // and are never applied, so the analysis short-circuits before reaching the batch and
320    // split branches, which the three tests below cover with genuinely strided inputs.
321    #[test]
322    fn test_reshape_analysis_unit_axes_are_contiguous() {
323        for shape_new in [
324            [1, 1, 32, 1, 1, 1].as_slice(),
325            [32, 1, 1, 1, 1].as_slice(),
326            [4, 8, 1, 1, 1].as_slice(),
327        ] {
328            let analysis = reshape_analysis(
329                &[32, 1, 1, 1].into(),
330                Some(&[1, 32, 32, 32].into()),
331                &shape_new.into(),
332            );
333
334            assert_eq!(analysis, ReshapeAnalysis::IsContiguous, "{shape_new:?}")
335        }
336    }
337
338    #[test]
339    fn test_reshape_analysis_broadcasted_batch() {
340        let analysis =
341            reshape_analysis(&[32, 32].into(), Some(&[1, 32].into()), &[1, 32, 32].into());
342
343        assert_eq!(analysis, ReshapeAnalysis::Broadcasted)
344    }
345
346    #[test]
347    fn test_reshape_analysis_unsqueeze_split() {
348        // Unsqueeze
349        let analysis =
350            reshape_analysis(&[32, 32].into(), Some(&[1, 32].into()), &[32, 32, 1].into());
351
352        assert_eq!(analysis, ReshapeAnalysis::Split)
353    }
354
355    #[test]
356    fn test_reshape_analysis_split() {
357        let analysis =
358            reshape_analysis(&[32, 32].into(), Some(&[1, 32].into()), &[4, 8, 32].into());
359
360        assert_eq!(analysis, ReshapeAnalysis::Split)
361    }
362
363    #[test]
364    fn test_split_strides_trailing_unit_dim_broadcast_view() {
365        // A `repeat_dim` broadcast view: [26, 1] with strides [1, 0],
366        // unsqueezed to [26, 1, 1]. Dim 0 must keep stride 1 — propagating
367        // the unit dim's stride 0 makes every row alias row 0 (this breaks
368        // e.g. `scatter` index tensors built via unsqueeze + repeat).
369        let strides = split_strides(&[26, 1], &[1, 0], &[26, 1, 1]);
370        assert_eq!(strides.as_ref(), &[1, 1, 1]);
371    }
372
373    #[test]
374    fn test_split_strides_trailing_unit_dims_arbitrary_strides() {
375        // Unit dims can carry arbitrary strides (broadcast 0, pitched
376        // values, ...). They must not anchor the stride walk.
377        let strides = split_strides(&[32, 1, 1, 1], &[1, 32, 32, 32], &[32, 1, 1, 1, 1]);
378        assert_eq!(strides.as_ref(), &[1, 1, 1, 1, 1]);
379    }
380
381    #[test]
382    fn test_split_strides_split_of_broadcast_dim_keeps_zero() {
383        // Splitting a real broadcast (stride 0) dim keeps 0 on the split
384        // parts; the leading real dim keeps its stride.
385        let strides = split_strides(&[26, 16], &[1, 0], &[26, 4, 4]);
386        assert_eq!(strides.as_ref(), &[1, 0, 0]);
387    }
388
389    #[test]
390    fn test_is_dense_contiguous() {
391        assert!(is_dense(&[2, 2, 2, 2], &[8, 4, 2, 1]));
392    }
393
394    #[test]
395    fn test_is_dense_permuted() {
396        assert!(is_dense(&[2, 2, 2, 2], &[8, 1, 4, 2]));
397    }
398
399    #[test]
400    fn test_is_dense_pitched_row() {
401        assert!(!is_dense(&[2, 2, 2, 2], &[16, 8, 4, 1]));
402        assert!(!is_dense(&[1, 8, 6, 6], &[384, 48, 8, 1]));
403    }
404
405    #[test]
406    fn test_is_dense_unit_dims_carry_no_layout() {
407        // A unit dim can hold any stride, a broadcast 0 included, without leaving a gap.
408        assert!(is_dense(&[1, 4, 1], &[0, 1, 7]));
409    }
410
411    #[test]
412    fn test_is_dense_rank_mismatch() {
413        assert!(!is_dense(&[2, 3], &[1]));
414    }
415
416    #[test]
417    fn test_split_strides_plain_unsqueeze() {
418        let strides = split_strides(&[26, 16], &[16, 1], &[26, 16, 1]);
419        assert_eq!(strides.as_ref(), &[16, 1, 1]);
420    }
421}