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/// Computes the strides for a contiguous tensor with the given shape.
45///
46/// In a contiguous row-major tensor, the stride for each dimension
47/// equals the product of all dimension sizes to its right.
48pub fn contiguous_strides(shape: &[usize]) -> Strides {
49    let mut strides = strides![0; shape.len()];
50    let mut current = 1;
51
52    for (i, &dim) in shape.iter().enumerate().rev() {
53        strides[i] = current;
54        current *= dim;
55    }
56
57    strides
58}
59
60/// The action to take for a reshape operation.
61#[derive(Debug)]
62pub enum ReshapeAction {
63    /// Updating the strides is sufficient to handle the reshape.
64    UpdateStrides {
65        /// The new strides.
66        strides: Strides,
67    },
68    /// The strides are not compatible, we should recompute the buffer.
69    Recompute,
70    /// The strides are already correct.
71    NoChange,
72}
73
74/// The reshape kind.
75#[derive(Debug, PartialEq)]
76pub enum ReshapeAnalysis {
77    /// Original tensor is contiguous, can update the strides.
78    IsContiguous,
79    /// Original tensor is highly permuted, can't update the strides.
80    HighlyPermuted,
81    /// Only batch dimensions are added, can update the strides.
82    Broadcasted,
83    /// Dimensions are only split, can update the strides.
84    Split,
85    /// Original tensor is bigger than output shape.
86    SmallerRank,
87    /// New shape is the same.
88    NoChange,
89}
90
91impl ReshapeAnalysis {
92    /// Returns the proper action to take for the current analysis.
93    pub fn action(&self, shape: &[usize], strides: &[usize], shape_new: &[usize]) -> ReshapeAction {
94        match self {
95            ReshapeAnalysis::IsContiguous => ReshapeAction::UpdateStrides {
96                strides: contiguous_strides(shape_new),
97            },
98            ReshapeAnalysis::NoChange => ReshapeAction::NoChange,
99            ReshapeAnalysis::HighlyPermuted | ReshapeAnalysis::SmallerRank => {
100                ReshapeAction::Recompute
101            }
102            ReshapeAnalysis::Broadcasted => {
103                let shape_rank = shape.len();
104                let shape_new_rank = shape_new.len();
105                let n_new_batch = shape_new_rank - shape_rank;
106                let num_elems = shape.iter().product::<usize>();
107                let strides_new = broadcast_strides(n_new_batch, shape_rank, num_elems, strides);
108
109                ReshapeAction::UpdateStrides {
110                    strides: strides_new,
111                }
112            }
113            ReshapeAnalysis::Split => {
114                let strides_new = split_strides(shape, strides, shape_new);
115
116                ReshapeAction::UpdateStrides {
117                    strides: strides_new,
118                }
119            }
120        }
121    }
122}
123
124/// Returns the proper action to take when reshaping a tensor.
125pub fn reshape_action(shape: &Shape, strides: &Strides, shape_new: &Shape) -> ReshapeAction {
126    reshape_analysis(shape, Some(strides), shape_new).action(shape, strides, shape_new)
127}
128
129/// Calculate the new strides given added batch dimensions.
130pub fn broadcast_strides(
131    n_new_batch: usize,
132    rank_prev: usize,
133    num_elems: usize,
134    strides: &[usize],
135) -> Strides {
136    let mut strides_new = strides![num_elems; rank_prev + n_new_batch];
137
138    for (i, s) in strides.iter().enumerate() {
139        strides_new[i + n_new_batch] = *s;
140    }
141
142    strides_new
143}
144
145/// Calculate the new strides given added split dimensions.
146pub fn split_strides(shape: &[usize], strides: &[usize], shape_new: &[usize]) -> Strides {
147    let mut strides_new = strides![1; shape_new.len()];
148
149    // Unit dims in the old shape never anchor a group of new dims, and their
150    // stride can be arbitrary (0 for broadcast views, pitched values, ...).
151    // Skip them so a real new dim never inherits a unit dim's stride —
152    // e.g. reshaping [26, 1] with strides [1, 0] (a `repeat_dim` broadcast
153    // view) to [26, 1, 1] must keep stride 1 on dim 0; propagating the 0
154    // would make every index along dim 0 alias the first element.
155    let skip_unit_dims = |mut idx: usize| {
156        while idx > 0 && shape[idx] == 1 {
157            idx -= 1;
158        }
159        idx
160    };
161
162    let mut old_idx = skip_unit_dims(shape.len() - 1);
163    let mut current_stride = strides[old_idx];
164    let mut dim_prod = 1;
165
166    for (i, dim) in shape_new.iter().enumerate().rev() {
167        dim_prod *= *dim;
168        strides_new[i] = current_stride;
169        if *dim == 1 {
170            continue;
171        } else if dim_prod == shape[old_idx] {
172            old_idx = skip_unit_dims(old_idx.saturating_sub(1));
173            current_stride = strides[old_idx];
174            dim_prod = 1;
175        } else {
176            current_stride *= *dim;
177        }
178    }
179
180    strides_new
181}
182
183/// Returns the analysis of a reshape operation.
184pub fn reshape_analysis(
185    shape: &Shape,
186    strides: Option<&Strides>,
187    shape_new: &Shape,
188) -> ReshapeAnalysis {
189    let shape_rank = shape.len();
190    let shape_new_rank = shape_new.len();
191
192    let is_contiguous = match strides {
193        Some(strides) => is_contiguous(shape, strides),
194        None => false,
195    };
196
197    if is_contiguous {
198        return ReshapeAnalysis::IsContiguous;
199    }
200
201    if shape_new_rank < shape_rank {
202        return ReshapeAnalysis::SmallerRank;
203    }
204
205    let n_new_batch = shape_new_rank - shape_rank;
206
207    match n_new_batch > 0 {
208        true => {
209            if shape.as_ref() == &shape_new[n_new_batch..shape_new_rank]
210                && shape_new[0..n_new_batch].iter().all(|it| *it == 1)
211            {
212                return ReshapeAnalysis::Broadcasted;
213            } else {
214                let mut dim_prod = 1;
215                let mut old_idx = 0;
216                for dim in shape_new.iter() {
217                    dim_prod *= *dim;
218
219                    // We need to ignore unit dims because they don't affect analysis and break
220                    // things because they match the default `dim_prod`. If we don't do this,
221                    // reshapes like [2, 3] to [2, 3, 1] will panic from out of bounds access.
222                    if *dim == 1 {
223                        continue;
224                    } else if dim_prod == shape[old_idx] {
225                        dim_prod = 1;
226                        old_idx += 1;
227                    } else if dim_prod > shape[old_idx] {
228                        return ReshapeAnalysis::HighlyPermuted;
229                    }
230                }
231                return ReshapeAnalysis::Split;
232            }
233        }
234
235        false => {
236            if shape == shape_new {
237                return ReshapeAnalysis::NoChange;
238            }
239        }
240    };
241
242    ReshapeAnalysis::HighlyPermuted
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn test_reshape_analysis_is_contiguous() {
251        let analysis = reshape_analysis(
252            &[32, 1, 1, 1].into(),
253            Some(&[1, 1, 1, 1].into()),
254            &[1, 1, 32, 1, 1, 1].into(),
255        );
256
257        assert_eq!(analysis, ReshapeAnalysis::IsContiguous)
258    }
259
260    #[test]
261    fn test_reshape_analysis_is_contiguous_2() {
262        let analysis = reshape_analysis(
263            &[32, 1, 1, 8].into(),
264            Some(&[8, 8, 8, 1].into()),
265            &[1, 1, 32, 1, 1, 8].into(),
266        );
267
268        assert_eq!(analysis, ReshapeAnalysis::IsContiguous)
269    }
270
271    #[test]
272    fn test_reshape_analysis_broadcasted_batch() {
273        let analysis = reshape_analysis(
274            &[32, 1, 1, 1].into(),
275            Some(&[1, 32, 32, 32].into()),
276            &[1, 1, 32, 1, 1, 1].into(),
277        );
278
279        assert_eq!(analysis, ReshapeAnalysis::Broadcasted)
280    }
281
282    #[test]
283    fn test_reshape_analysis_unsqueeze_split() {
284        // Unsqueeze
285        let analysis = reshape_analysis(
286            &[32, 1, 1, 1].into(),
287            Some(&[1, 32, 32, 32].into()),
288            &[32, 1, 1, 1, 1].into(),
289        );
290
291        assert_eq!(analysis, ReshapeAnalysis::Split)
292    }
293
294    #[test]
295    fn test_reshape_analysis_split() {
296        let analysis = reshape_analysis(
297            &[32, 1, 1, 1].into(),
298            Some(&[1, 32, 32, 32].into()),
299            &[4, 8, 1, 1, 1].into(),
300        );
301
302        assert_eq!(analysis, ReshapeAnalysis::Split)
303    }
304
305    #[test]
306    fn test_split_strides_trailing_unit_dim_broadcast_view() {
307        // A `repeat_dim` broadcast view: [26, 1] with strides [1, 0],
308        // unsqueezed to [26, 1, 1]. Dim 0 must keep stride 1 — propagating
309        // the unit dim's stride 0 makes every row alias row 0 (this breaks
310        // e.g. `scatter` index tensors built via unsqueeze + repeat).
311        let strides = split_strides(&[26, 1], &[1, 0], &[26, 1, 1]);
312        assert_eq!(strides.as_ref(), &[1, 1, 1]);
313    }
314
315    #[test]
316    fn test_split_strides_trailing_unit_dims_arbitrary_strides() {
317        // Unit dims can carry arbitrary strides (broadcast 0, pitched
318        // values, ...). They must not anchor the stride walk.
319        let strides = split_strides(&[32, 1, 1, 1], &[1, 32, 32, 32], &[32, 1, 1, 1, 1]);
320        assert_eq!(strides.as_ref(), &[1, 1, 1, 1, 1]);
321    }
322
323    #[test]
324    fn test_split_strides_split_of_broadcast_dim_keeps_zero() {
325        // Splitting a real broadcast (stride 0) dim keeps 0 on the split
326        // parts; the leading real dim keeps its stride.
327        let strides = split_strides(&[26, 16], &[1, 0], &[26, 4, 4]);
328        assert_eq!(strides.as_ref(), &[1, 0, 0]);
329    }
330
331    #[test]
332    fn test_split_strides_plain_unsqueeze() {
333        let strides = split_strides(&[26, 16], &[16, 1], &[26, 16, 1]);
334        assert_eq!(strides.as_ref(), &[16, 1, 1]);
335    }
336}