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