Skip to main content

burn_flex/
layout.rs

1use alloc::vec;
2use alloc::vec::Vec;
3use burn_std::{Shape, Slice};
4
5/// Layout describes how to interpret a linear buffer as an N-dimensional tensor.
6///
7/// Stores shape, strides (in elements, can be negative for flipped dimensions),
8/// and a start offset for views/slices.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Layout {
11    shape: Shape,
12    /// Strides in elements. Negative strides enable zero-copy flip.
13    strides: Vec<isize>,
14    start_offset: usize,
15}
16
17/// Compute row-major contiguous strides for a shape (as `usize`).
18pub(crate) fn contiguous_strides_usize(shape: &Shape) -> Vec<usize> {
19    let ndims = shape.num_dims();
20    let mut strides = vec![1usize; ndims];
21    for i in (0..ndims.saturating_sub(1)).rev() {
22        strides[i] = strides[i + 1] * shape[i + 1];
23    }
24    strides
25}
26
27/// Compute the flat offset for the `slice_idx`-th 1D fiber along `dim`.
28///
29/// Enumerates all index combinations for dimensions other than `dim`,
30/// mapping the flat `slice_idx` (0..product of non-dim sizes) to the
31/// corresponding starting offset in a contiguous buffer.
32pub(crate) fn slice_base_offset(
33    slice_idx: usize,
34    shape: &Shape,
35    strides: &[usize],
36    dim: usize,
37) -> usize {
38    let ndims = shape.num_dims();
39    let mut offset = 0;
40    let mut remaining = slice_idx;
41    for d in (0..ndims).rev() {
42        if d == dim {
43            continue;
44        }
45        let s = shape[d];
46        offset += (remaining % s) * strides[d];
47        remaining /= s;
48    }
49    offset
50}
51
52impl Layout {
53    /// Create a new contiguous layout (row-major/C-order).
54    pub fn contiguous(shape: Shape) -> Self {
55        let strides: Vec<isize> = contiguous_strides_usize(&shape)
56            .into_iter()
57            .map(|s| s as isize)
58            .collect();
59
60        Self {
61            shape,
62            strides,
63            start_offset: 0,
64        }
65    }
66
67    /// Create a layout with explicit strides.
68    pub fn new(shape: Shape, strides: Vec<isize>, start_offset: usize) -> Self {
69        debug_assert_eq!(shape.num_dims(), strides.len());
70        Self {
71            shape,
72            strides,
73            start_offset,
74        }
75    }
76
77    /// The shape of the tensor.
78    pub fn shape(&self) -> &Shape {
79        &self.shape
80    }
81
82    /// The strides in elements (can be negative for flipped dimensions).
83    pub fn strides(&self) -> &[isize] {
84        &self.strides
85    }
86
87    /// The start offset for views/slices.
88    pub fn start_offset(&self) -> usize {
89        self.start_offset
90    }
91
92    /// Number of dimensions.
93    pub fn num_dims(&self) -> usize {
94        self.shape.num_dims()
95    }
96
97    /// Total number of elements.
98    pub fn num_elements(&self) -> usize {
99        self.shape.num_elements()
100    }
101
102    /// Check if this layout is contiguous (row-major, positive strides).
103    pub fn is_contiguous(&self) -> bool {
104        if self.shape.num_dims() == 0 || self.num_elements() == 0 {
105            return true;
106        }
107
108        if self.strides.iter().any(|&s| s <= 0) {
109            return false;
110        }
111
112        let mut expected_stride = 1isize;
113        for i in (0..self.shape.num_dims()).rev() {
114            if self.shape[i] == 1 {
115                continue;
116            }
117            if self.strides[i] != expected_stride {
118                return false;
119            }
120            match expected_stride.checked_mul(self.shape[i] as isize) {
121                Some(next) => expected_stride = next,
122                None => return false,
123            }
124        }
125        true
126    }
127
128    /// Returns true if this layout represents contiguous, non-broadcast storage.
129    pub fn is_dense_unique_storage(&self) -> bool {
130        self.is_contiguous()
131            && !self
132                .strides
133                .iter()
134                .zip(self.shape.iter())
135                .any(|(&stride, &dim)| dim > 1 && stride == 0)
136    }
137
138    /// If contiguous, return (start, end) offsets for direct slice access.
139    pub fn contiguous_offsets(&self) -> Option<(usize, usize)> {
140        if self.is_contiguous() {
141            Some((self.start_offset, self.start_offset + self.num_elements()))
142        } else {
143            None
144        }
145    }
146
147    /// Transpose: swap two dimensions (zero-copy, metadata only).
148    pub fn transpose(&self, dim1: usize, dim2: usize) -> Self {
149        let mut dims = self.shape.to_vec();
150        let mut strides = self.strides.clone();
151        dims.swap(dim1, dim2);
152        strides.swap(dim1, dim2);
153        Self {
154            shape: Shape::from(dims),
155            strides,
156            start_offset: self.start_offset,
157        }
158    }
159
160    /// Permute: reorder dimensions according to axes (zero-copy, metadata only).
161    ///
162    /// `axes` must be a permutation of 0..ndim.
163    pub fn permute(&self, axes: &[usize]) -> Self {
164        debug_assert_eq!(
165            axes.len(),
166            self.num_dims(),
167            "permute: axes length must match number of dimensions"
168        );
169
170        let new_dims: Vec<usize> = axes.iter().map(|&i| self.shape[i]).collect();
171        let new_strides: Vec<isize> = axes.iter().map(|&i| self.strides[i]).collect();
172
173        Self {
174            shape: Shape::from(new_dims),
175            strides: new_strides,
176            start_offset: self.start_offset,
177        }
178    }
179
180    /// Flip: reverse elements along specified axes (zero-copy, metadata only).
181    ///
182    /// For each flipped axis, negates the stride and adjusts start_offset
183    /// to point to the last element along that dimension.
184    pub fn flip(&self, axes: &[usize]) -> Self {
185        let mut new_strides = self.strides.clone();
186        let mut offset_adjustment: isize = 0;
187
188        for &axis in axes {
189            debug_assert!(
190                axis < self.num_dims(),
191                "flip: axis {} out of bounds for {} dimensions",
192                axis,
193                self.num_dims()
194            );
195
196            let dim_size = self.shape[axis];
197            if dim_size > 1 {
198                // Move start to last element along this axis
199                offset_adjustment += (dim_size as isize - 1) * self.strides[axis];
200                // Negate stride to iterate backwards
201                new_strides[axis] = -new_strides[axis];
202            }
203        }
204
205        let new_start_isize = self.start_offset as isize + offset_adjustment;
206        debug_assert!(new_start_isize >= 0, "flip: negative offset");
207        let new_start = new_start_isize as usize;
208
209        Self {
210            shape: self.shape.clone(),
211            strides: new_strides,
212            start_offset: new_start,
213        }
214    }
215
216    /// Narrow/slice along a dimension (zero-copy, metadata only).
217    pub fn narrow(&self, dim: usize, start: usize, len: usize) -> Self {
218        debug_assert!(
219            start + len <= self.shape[dim],
220            "narrow: start ({}) + len ({}) exceeds dimension size ({})",
221            start,
222            len,
223            self.shape[dim]
224        );
225        let mut dims = self.shape.to_vec();
226        dims[dim] = len;
227
228        let new_offset_isize = self.start_offset as isize + self.strides[dim] * start as isize;
229        debug_assert!(new_offset_isize >= 0, "narrow: negative offset");
230        let new_offset = new_offset_isize as usize;
231
232        Self {
233            shape: Shape::from(dims),
234            strides: self.strides.clone(),
235            start_offset: new_offset,
236        }
237    }
238
239    /// Apply slices to create a new layout.
240    ///
241    /// Returns `(new_layout, needs_copy)`:
242    /// - `needs_copy = false`: Can use zero-copy view with new layout
243    /// - `needs_copy = true`: Has negative steps requiring data reordering
244    pub fn slice(&self, slices: &[Slice]) -> (Self, bool) {
245        let ndims = self.num_dims();
246        let mut new_dims = self.shape.to_vec();
247        let mut new_strides = self.strides.clone();
248        let mut new_offset = self.start_offset as isize;
249
250        for (dim, slice) in slices.iter().enumerate() {
251            if dim >= ndims {
252                break;
253            }
254
255            let dim_size = self.shape[dim] as isize;
256            let stride = self.strides[dim];
257
258            // Normalize start index (handle negative)
259            let start = if slice.start < 0 {
260                (dim_size + slice.start).max(0) as usize
261            } else {
262                (slice.start as usize).min(dim_size as usize)
263            };
264
265            // Normalize end index (handle negative and None)
266            // Note: Range [start, end) determines WHICH elements to select,
267            // step determines iteration ORDER
268            let end = match slice.end {
269                Some(e) if e < 0 => (dim_size + e).max(0) as usize,
270                Some(e) => (e as usize).min(dim_size as usize),
271                None => dim_size as usize, // Always full range when end is None
272            };
273
274            let step = slice.step;
275            let abs_step = step.unsigned_abs();
276
277            let len = if end > start {
278                (end - start).div_ceil(abs_step)
279            } else {
280                0
281            };
282            new_dims[dim] = len;
283            new_strides[dim] = stride * step;
284
285            if step > 0 {
286                // Positive step: forward iteration
287                if len > 0 {
288                    new_offset += stride * start as isize;
289                }
290            } else {
291                // Negative step: select range then iterate in reverse via negative stride
292                if len > 0 {
293                    new_offset += (end as isize - 1) * stride;
294                }
295            }
296        }
297
298        debug_assert!(new_offset >= 0, "slice: negative offset");
299
300        (
301            Self {
302                shape: Shape::from(new_dims),
303                strides: new_strides,
304                start_offset: new_offset as usize,
305            },
306            false,
307        )
308    }
309
310    /// Reshape to a new shape. Only works if contiguous.
311    ///
312    /// Preserves start_offset for zero-copy view of contiguous slices.
313    /// Returns None if not contiguous (would require data copy).
314    pub fn reshape(&self, new_shape: Shape) -> Option<Self> {
315        if !self.is_contiguous() {
316            return None;
317        }
318        debug_assert_eq!(
319            self.num_elements(),
320            new_shape.num_elements(),
321            "reshape must preserve total elements"
322        );
323        let mut layout = Self::contiguous(new_shape);
324        layout.start_offset = self.start_offset;
325        Some(layout)
326    }
327
328    /// Compute linear index from multi-dimensional indices.
329    pub fn index(&self, indices: &[usize]) -> usize {
330        debug_assert_eq!(indices.len(), self.num_dims());
331        let mut offset = self.start_offset as isize;
332        for (i, &idx) in indices.iter().enumerate() {
333            offset += idx as isize * self.strides[i];
334        }
335        debug_assert!(offset >= 0, "index: negative offset");
336        offset as usize
337    }
338
339    /// Get stride of the innermost (last) dimension.
340    /// Returns 1 for contiguous tensors, larger values for transposed.
341    /// Returns absolute value (ignores flip).
342    pub fn inner_stride(&self) -> usize {
343        self.strides.last().map(|s| s.unsigned_abs()).unwrap_or(1)
344    }
345
346    /// Check if innermost dimension is contiguous (|stride| == 1).
347    /// This enables efficient vectorized inner loops.
348    pub fn has_contiguous_inner(&self) -> bool {
349        self.inner_stride() == 1
350    }
351
352    /// For 2D layouts, get (outer_size, inner_size, outer_stride, inner_stride).
353    /// Returns None if not 2D.
354    pub fn as_2d_strides(&self) -> Option<(usize, usize, isize, isize)> {
355        if self.num_dims() != 2 {
356            return None;
357        }
358        Some((
359            self.shape[0],
360            self.shape[1],
361            self.strides[0],
362            self.strides[1],
363        ))
364    }
365
366    /// Check if all strides are non-negative.
367    pub fn has_positive_strides(&self) -> bool {
368        self.strides.iter().all(|&s| s >= 0)
369    }
370
371    /// Compute strided blocks for efficient iteration.
372    ///
373    /// Returns (block_len, num_blocks, block_stride) where:
374    /// - block_len: number of contiguous elements in each block
375    /// - num_blocks: total number of blocks
376    /// - block_stride: stride between consecutive blocks (0 if single block)
377    ///
378    /// For contiguous tensors: single block covering all elements.
379    /// For transposed/strided: multiple blocks of contiguous data.
380    pub fn strided_blocks(&self) -> StridedBlocks<'_> {
381        let n = self.num_elements();
382        if n == 0 {
383            return StridedBlocks::Single { start: 0, len: 0 };
384        }
385
386        // Fast path: fully contiguous
387        if self.is_contiguous() {
388            return StridedBlocks::Single {
389                start: self.start_offset,
390                len: n,
391            };
392        }
393
394        // Find contiguous inner dimensions (only positive strides)
395        // Start from innermost and work outward while strides match contiguous pattern
396        let ndims = self.num_dims();
397        let mut block_len = 1usize;
398        let mut expected_stride = 1isize;
399
400        for i in (0..ndims).rev() {
401            if self.strides[i] == expected_stride {
402                block_len *= self.shape[i];
403                expected_stride *= self.shape[i] as isize;
404            } else {
405                break;
406            }
407        }
408
409        if block_len == n {
410            // All dimensions contiguous (just offset)
411            return StridedBlocks::Single {
412                start: self.start_offset,
413                len: n,
414            };
415        }
416
417        let num_blocks = n / block_len;
418        StridedBlocks::Multiple {
419            layout: self,
420            block_len,
421            num_blocks,
422        }
423    }
424}
425
426/// Result of strided block analysis.
427#[derive(Debug, Clone)]
428pub enum StridedBlocks<'a> {
429    /// Single contiguous block - direct slice access.
430    Single { start: usize, len: usize },
431    /// Multiple blocks requiring iteration.
432    Multiple {
433        layout: &'a Layout,
434        block_len: usize,
435        num_blocks: usize,
436    },
437}
438
439impl<'a> StridedBlocks<'a> {
440    /// Get the block length (elements per block).
441    pub fn block_len(&self) -> usize {
442        match self {
443            Self::Single { len, .. } => *len,
444            Self::Multiple { block_len, .. } => *block_len,
445        }
446    }
447
448    /// Iterator over block start indices.
449    pub fn block_starts(&self) -> BlockStartIter<'_> {
450        match self {
451            Self::Single { start, .. } => BlockStartIter::Single {
452                start: *start,
453                done: false,
454            },
455            Self::Multiple {
456                layout,
457                block_len,
458                num_blocks,
459            } => {
460                // Calculate dimensions for outer iteration (non-contiguous part)
461                let ndims = layout.num_dims();
462                let mut outer_dims = 0;
463                let mut expected_stride = 1isize;
464
465                for i in (0..ndims).rev() {
466                    if layout.strides[i] == expected_stride {
467                        expected_stride *= layout.shape[i] as isize;
468                    } else {
469                        outer_dims = i + 1;
470                        break;
471                    }
472                }
473
474                BlockStartIter::Multiple {
475                    layout,
476                    multi_index: vec![0; outer_dims],
477                    remaining: *num_blocks,
478                    block_len: *block_len,
479                }
480            }
481        }
482    }
483}
484
485/// Iterator over block start indices.
486pub enum BlockStartIter<'a> {
487    Single {
488        start: usize,
489        done: bool,
490    },
491    Multiple {
492        layout: &'a Layout,
493        multi_index: Vec<usize>,
494        remaining: usize,
495        block_len: usize,
496    },
497}
498
499impl Iterator for BlockStartIter<'_> {
500    type Item = usize;
501
502    fn next(&mut self) -> Option<usize> {
503        match self {
504            Self::Single { start, done } => {
505                if *done {
506                    None
507                } else {
508                    *done = true;
509                    Some(*start)
510                }
511            }
512            Self::Multiple {
513                layout,
514                multi_index,
515                remaining,
516                block_len: _,
517            } => {
518                if *remaining == 0 {
519                    return None;
520                }
521
522                // Compute current block start
523                let outer_dims = multi_index.len();
524                let mut offset = layout.start_offset as isize;
525                for (i, &idx) in multi_index.iter().enumerate() {
526                    offset += idx as isize * layout.strides[i];
527                }
528
529                *remaining -= 1;
530
531                // Advance multi-index for next iteration
532                let shape = &layout.shape;
533                for d in (0..outer_dims).rev() {
534                    multi_index[d] += 1;
535                    if multi_index[d] < shape[d] {
536                        break;
537                    }
538                    multi_index[d] = 0;
539                }
540
541                Some(offset as usize)
542            }
543        }
544    }
545
546    fn size_hint(&self) -> (usize, Option<usize>) {
547        let len = match self {
548            Self::Single { done, .. } => {
549                if *done {
550                    0
551                } else {
552                    1
553                }
554            }
555            Self::Multiple { remaining, .. } => *remaining,
556        };
557        (len, Some(len))
558    }
559}
560
561impl ExactSizeIterator for BlockStartIter<'_> {}
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566
567    #[test]
568    fn test_contiguous_layout() {
569        let layout = Layout::contiguous(Shape::from(vec![2, 3, 4]));
570        assert_eq!(layout.strides(), &[12, 4, 1]);
571        assert!(layout.is_contiguous());
572    }
573
574    #[test]
575    fn test_transpose() {
576        let layout = Layout::contiguous(Shape::from(vec![2, 3]));
577        let transposed = layout.transpose(0, 1);
578        assert_eq!(transposed.shape().to_vec(), vec![3, 2]);
579        assert_eq!(transposed.strides(), &[1, 3]);
580        assert!(!transposed.is_contiguous());
581    }
582
583    #[test]
584    fn test_narrow() {
585        let layout = Layout::contiguous(Shape::from(vec![4, 4]));
586        let narrowed = layout.narrow(0, 1, 2);
587        assert_eq!(narrowed.shape().to_vec(), vec![2, 4]);
588        assert_eq!(narrowed.start_offset(), 4);
589    }
590
591    #[test]
592    fn test_contiguous_offsets() {
593        let layout = Layout::contiguous(Shape::from(vec![2, 3]));
594        assert_eq!(layout.contiguous_offsets(), Some((0, 6)));
595    }
596
597    #[test]
598    fn test_index() {
599        let layout = Layout::contiguous(Shape::from(vec![2, 3]));
600        assert_eq!(layout.index(&[0, 0]), 0);
601        assert_eq!(layout.index(&[0, 2]), 2);
602        assert_eq!(layout.index(&[1, 0]), 3);
603        assert_eq!(layout.index(&[1, 2]), 5);
604    }
605
606    #[test]
607    fn test_flip_1d() {
608        // Original: [0, 1, 2, 3] with strides [1]
609        // Flipped: strides [-1], start_offset = 3
610        let layout = Layout::contiguous(Shape::from(vec![4]));
611        let flipped = layout.flip(&[0]);
612
613        assert_eq!(flipped.shape().to_vec(), vec![4]);
614        assert_eq!(flipped.strides(), &[-1]);
615        assert_eq!(flipped.start_offset(), 3);
616
617        // Verify indices: logical [0] -> physical [3], logical [1] -> physical [2], etc.
618        assert_eq!(flipped.index(&[0]), 3);
619        assert_eq!(flipped.index(&[1]), 2);
620        assert_eq!(flipped.index(&[2]), 1);
621        assert_eq!(flipped.index(&[3]), 0);
622    }
623
624    #[test]
625    fn test_flip_2d_axis0() {
626        // [[0, 1, 2], [3, 4, 5]] with strides [3, 1]
627        // Flip axis 0: strides [-3, 1], start_offset = 3
628        let layout = Layout::contiguous(Shape::from(vec![2, 3]));
629        let flipped = layout.flip(&[0]);
630
631        assert_eq!(flipped.strides(), &[-3, 1]);
632        assert_eq!(flipped.start_offset(), 3);
633
634        // Row 0 of flipped = Row 1 of original
635        assert_eq!(flipped.index(&[0, 0]), 3);
636        assert_eq!(flipped.index(&[0, 1]), 4);
637        assert_eq!(flipped.index(&[0, 2]), 5);
638        // Row 1 of flipped = Row 0 of original
639        assert_eq!(flipped.index(&[1, 0]), 0);
640        assert_eq!(flipped.index(&[1, 1]), 1);
641        assert_eq!(flipped.index(&[1, 2]), 2);
642    }
643
644    #[test]
645    fn test_flip_2d_axis1() {
646        // [[0, 1, 2], [3, 4, 5]] with strides [3, 1]
647        // Flip axis 1: strides [3, -1], start_offset = 2
648        let layout = Layout::contiguous(Shape::from(vec![2, 3]));
649        let flipped = layout.flip(&[1]);
650
651        assert_eq!(flipped.strides(), &[3, -1]);
652        assert_eq!(flipped.start_offset(), 2);
653
654        // Col 0 of flipped = Col 2 of original
655        assert_eq!(flipped.index(&[0, 0]), 2);
656        assert_eq!(flipped.index(&[0, 1]), 1);
657        assert_eq!(flipped.index(&[0, 2]), 0);
658        assert_eq!(flipped.index(&[1, 0]), 5);
659        assert_eq!(flipped.index(&[1, 1]), 4);
660        assert_eq!(flipped.index(&[1, 2]), 3);
661    }
662
663    #[test]
664    fn test_flip_both_axes() {
665        // [[0, 1, 2], [3, 4, 5]] -> [[5, 4, 3], [2, 1, 0]]
666        let layout = Layout::contiguous(Shape::from(vec![2, 3]));
667        let flipped = layout.flip(&[0, 1]);
668
669        assert_eq!(flipped.strides(), &[-3, -1]);
670        assert_eq!(flipped.start_offset(), 5); // 3 + 2 = 5
671
672        assert_eq!(flipped.index(&[0, 0]), 5);
673        assert_eq!(flipped.index(&[0, 1]), 4);
674        assert_eq!(flipped.index(&[0, 2]), 3);
675        assert_eq!(flipped.index(&[1, 0]), 2);
676        assert_eq!(flipped.index(&[1, 1]), 1);
677        assert_eq!(flipped.index(&[1, 2]), 0);
678    }
679}