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