Skip to main content

burn_flex/
tensor.rs

1use alloc::vec::Vec;
2use core::fmt;
3
4use burn_backend::{DType, Element, TensorData, TensorMetadata};
5use burn_std::sync::Arc;
6use burn_std::{Bytes, Shape, bf16, f16};
7
8use crate::{FlexDevice, layout::Layout};
9
10/// CPU tensor primitive for the Flex backend.
11///
12/// Uses type-erased byte storage with runtime dtype and Arc-based sharing.
13/// Clone is O(1) (refcount increment). Copy-on-write for mutations.
14#[derive(Clone)]
15pub struct FlexTensor {
16    /// Shared byte storage. Clone increments refcount.
17    data: Arc<Bytes>,
18    /// Layout describing shape, strides, and offset.
19    layout: Layout,
20    /// Runtime data type.
21    dtype: DType,
22}
23
24impl fmt::Debug for FlexTensor {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        f.debug_struct("FlexTensor")
27            .field("shape", self.layout.shape())
28            .field("dtype", &self.dtype)
29            .field("contiguous", &self.layout.is_contiguous())
30            .field("unique", &self.is_unique())
31            .finish()
32    }
33}
34
35impl FlexTensor {
36    /// Create a new tensor from bytes, layout, and dtype.
37    pub fn new(data: Bytes, layout: Layout, dtype: DType) -> Self {
38        Self {
39            data: Arc::new(data),
40            layout,
41            dtype,
42        }
43    }
44
45    /// Create a tensor from TensorData.
46    pub fn from_data(data: TensorData) -> Self {
47        let (bytes, shape, dtype) = data.into_parts();
48        let layout = Layout::contiguous(shape);
49        Self {
50            data: Arc::new(bytes),
51            layout,
52            dtype,
53        }
54    }
55
56    /// Convert tensor to TensorData.
57    ///
58    /// If non-contiguous or shared, this will copy data.
59    pub fn into_data(self) -> TensorData {
60        if self.layout.is_contiguous() && self.layout.start_offset() == 0 {
61            let expected_bytes = self.layout.num_elements() * dtype_size(self.dtype);
62            assert!(
63                expected_bytes <= self.data.len(),
64                "into_data: buffer ({} bytes) too small for {} elements of {:?}",
65                self.data.len(),
66                self.layout.num_elements(),
67                self.dtype
68            );
69            let bytes = if self.data.len() == expected_bytes {
70                // Buffer exactly matches logical size; try zero-copy unwrap
71                match Arc::try_unwrap(self.data) {
72                    Ok(bytes) => bytes,
73                    Err(arc) => Bytes::from_bytes_vec((*arc)[..expected_bytes].to_vec()),
74                }
75            } else {
76                // Contiguous at offset 0 but buffer is oversized (e.g., narrowed view).
77                // Truncate to exact logical size.
78                Bytes::from_bytes_vec(self.data[..expected_bytes].to_vec())
79            };
80            TensorData::from_bytes(bytes, self.layout.shape().clone(), self.dtype)
81        } else {
82            // Non-contiguous or non-zero offset: copy to contiguous layout
83            self.to_contiguous().into_data()
84        }
85    }
86
87    /// Check if this tensor has exclusive ownership of its data.
88    ///
89    /// When true, in-place mutations are safe without copying.
90    #[inline]
91    pub fn is_unique(&self) -> bool {
92        Arc::strong_count(&self.data) == 1
93    }
94
95    /// Get the layout.
96    pub fn layout(&self) -> &Layout {
97        &self.layout
98    }
99
100    /// Create a new tensor with a different layout but sharing the same data.
101    ///
102    /// This is a zero-copy operation used for operations like flip, transpose, etc.
103    pub fn with_layout(self, layout: Layout) -> Self {
104        Self {
105            data: self.data,
106            layout,
107            dtype: self.dtype,
108        }
109    }
110
111    /// Get the dtype.
112    pub fn dtype(&self) -> DType {
113        self.dtype
114    }
115
116    /// Check if tensor is contiguous.
117    pub fn is_contiguous(&self) -> bool {
118        self.layout.is_contiguous()
119    }
120
121    /// Get the raw bytes (read-only).
122    pub fn bytes(&self) -> &[u8] {
123        &self.data
124    }
125
126    /// Get a clone of the Arc for sharing data with a new layout.
127    ///
128    /// Use this for zero-copy view operations (reshape, transpose, slice).
129    pub fn data_arc(&self) -> Arc<Bytes> {
130        Arc::clone(&self.data)
131    }
132
133    /// Create a tensor from shared data, layout, and dtype.
134    ///
135    /// Use this for zero-copy view operations.
136    pub fn from_arc(data: Arc<Bytes>, layout: Layout, dtype: DType) -> Self {
137        Self {
138            data,
139            layout,
140            dtype,
141        }
142    }
143
144    /// Zero-copy typed view of the full storage buffer.
145    ///
146    /// Use with `StridedIter` for non-contiguous access, or with
147    /// `layout().contiguous_offsets()` for the contiguous fast path.
148    ///
149    /// # Panics
150    /// Panics if `E::dtype()` doesn't match the tensor's dtype.
151    /// Note: Bool tensors are stored as u8, so both Bool(Native) and Bool(U8)
152    /// dtypes accept u8 access.
153    pub fn storage<E: Element + bytemuck::Pod>(&self) -> &[E] {
154        assert!(
155            E::dtype() == self.dtype
156                || (matches!(
157                    self.dtype,
158                    DType::Bool(burn_std::BoolStore::Native | burn_std::BoolStore::U8)
159                ) && E::dtype() == DType::U8),
160            "storage: dtype mismatch (expected {:?}, got {:?})",
161            self.dtype,
162            E::dtype()
163        );
164        bytemuck::cast_slice(&self.data)
165    }
166
167    /// Mutable typed view with copy-on-write semantics.
168    ///
169    /// If the tensor is shared (refcount > 1), this will copy the data first.
170    /// For in-place operations, prefer `try_storage_mut()` which returns None
171    /// if shared, allowing you to choose an alternative strategy.
172    ///
173    /// # Panics
174    /// Panics if `E::dtype()` doesn't match the tensor's dtype.
175    /// Note: Bool tensors are stored as u8, so both Bool(Native) and Bool(U8)
176    /// dtypes accept u8 access.
177    pub fn storage_mut<E: Element + bytemuck::Pod>(&mut self) -> &mut [E] {
178        assert!(
179            E::dtype() == self.dtype
180                || (matches!(
181                    self.dtype,
182                    DType::Bool(burn_std::BoolStore::Native | burn_std::BoolStore::U8)
183                ) && E::dtype() == DType::U8),
184            "storage_mut: dtype mismatch (expected {:?}, got {:?})",
185            self.dtype,
186            E::dtype()
187        );
188        // COW: clone data if shared
189        let bytes = Arc::make_mut(&mut self.data);
190        bytemuck::cast_slice_mut(bytes)
191    }
192
193    /// Try to get mutable storage without copying.
194    ///
195    /// Returns `Some` if tensor is uniquely owned, `None` if shared.
196    /// Use this when you want to avoid the implicit copy in `storage_mut()`.
197    /// Note: Bool tensors are stored as u8, so both Bool(Native) and Bool(U8)
198    /// dtypes accept u8 access.
199    pub fn try_storage_mut<E: Element + bytemuck::Pod>(&mut self) -> Option<&mut [E]> {
200        assert!(
201            E::dtype() == self.dtype
202                || (matches!(
203                    self.dtype,
204                    DType::Bool(burn_std::BoolStore::Native | burn_std::BoolStore::U8)
205                ) && E::dtype() == DType::U8),
206            "try_storage_mut: dtype mismatch (expected {:?}, got {:?})",
207            self.dtype,
208            E::dtype()
209        );
210        if self.is_unique() {
211            // Safe: we're the only owner
212            let bytes = Arc::get_mut(&mut self.data)?;
213            Some(bytemuck::cast_slice_mut(bytes))
214        } else {
215            None
216        }
217    }
218
219    /// Get typed slice view (zero-cost if contiguous and offset is 0).
220    ///
221    /// Returns None if dtype doesn't match E or tensor is non-contiguous.
222    pub fn as_slice<E: Element + bytemuck::Pod>(&self) -> Option<&[E]> {
223        if E::dtype() != self.dtype {
224            return None;
225        }
226        let storage: &[E] = self.storage();
227        self.layout
228            .contiguous_offsets()
229            .map(|(start, end)| &storage[start..end])
230    }
231
232    /// Create an empty tensor with given shape and dtype.
233    pub fn empty(shape: Shape, dtype: DType) -> Self {
234        let num_elements = shape.num_elements();
235        let elem_size = dtype_size(dtype);
236        let bytes = Bytes::from_bytes_vec(alloc::vec![0u8; num_elements * elem_size]);
237        let layout = Layout::contiguous(shape);
238        Self {
239            data: Arc::new(bytes),
240            layout,
241            dtype,
242        }
243    }
244
245    /// Create a tensor filled with zeros.
246    pub fn zeros(shape: Shape, dtype: DType) -> Self {
247        Self::empty(shape, dtype)
248    }
249
250    /// Create a tensor filled with `n` copies of a typed value.
251    pub fn filled_typed<E: bytemuck::Pod + Send + Sync>(
252        shape: Shape,
253        dtype: DType,
254        value: E,
255    ) -> Self {
256        assert_eq!(
257            dtype_size(dtype),
258            core::mem::size_of::<E>(),
259            "filled_typed: dtype size mismatch"
260        );
261        let n = shape.num_elements();
262        let data = alloc::vec![value; n];
263        let bytes = Bytes::from_elems(data);
264        Self {
265            data: Arc::new(bytes),
266            layout: Layout::contiguous(shape),
267            dtype,
268        }
269    }
270
271    /// Copy to contiguous layout if needed, consuming `self`.
272    ///
273    /// Prefer this over [`Self::to_contiguous`] when you own the tensor and
274    /// will mutate it through [`Self::storage_mut`]: the borrowing form has
275    /// to hand back a second `Arc` handle, which makes the later
276    /// copy-on-write check see a shared buffer.
277    pub(crate) fn into_contiguous(self) -> Self {
278        // Fast path requires the logical tensor to cover the whole buffer.
279        // A contiguous prefix view (e.g. [8, 5] sliced to [5, 5]) has
280        // canonical strides and offset 0 but an oversized buffer, and would
281        // otherwise mislead callers that read `storage()` / `bytes()` by
282        // length (e.g. the SIMD `mask_fill_*` kernels).
283        if self.is_contiguous()
284            && self.layout.start_offset() == 0
285            && self.data.len() == self.layout.num_elements() * dtype_size(self.dtype)
286        {
287            return self;
288        }
289
290        // Copy data to new contiguous buffer
291        match self.dtype {
292            DType::F64 => self.copy_contiguous::<f64>(),
293            DType::F32 => self.copy_contiguous::<f32>(),
294            DType::F16 => self.copy_contiguous::<f16>(),
295            DType::BF16 => self.copy_contiguous::<bf16>(),
296            DType::I64 => self.copy_contiguous::<i64>(),
297            DType::I32 => self.copy_contiguous::<i32>(),
298            DType::I16 => self.copy_contiguous::<i16>(),
299            DType::I8 => self.copy_contiguous::<i8>(),
300            DType::U64 => self.copy_contiguous::<u64>(),
301            DType::U32 => self.copy_contiguous::<u32>(),
302            DType::U16 => self.copy_contiguous::<u16>(),
303            DType::U8 => self.copy_contiguous::<u8>(),
304            DType::Bool(burn_std::BoolStore::Native | burn_std::BoolStore::U8) => {
305                self.copy_contiguous::<u8>()
306            }
307            DType::Bool(burn_std::BoolStore::U32) => {
308                panic!("burn-flex: Bool(U32) storage is not yet supported")
309            }
310            _ => panic!("Unsupported dtype for contiguous copy: {:?}", self.dtype),
311        }
312    }
313
314    /// Copy to contiguous layout if needed.
315    pub fn to_contiguous(&self) -> Self {
316        self.clone().into_contiguous()
317    }
318
319    fn copy_contiguous<E: Element + bytemuck::Pod>(&self) -> Self {
320        let src: &[E] = bytemuck::cast_slice(&self.data);
321        let n = self.layout.num_elements();
322        let mut dst = Vec::with_capacity(n);
323
324        // Squeeze size-1 dims and merge adjacent stride-contiguous
325        // runs so e.g. a permuted `[N, H, W, C]` ConvNeXt layer-norm
326        // input becomes a plain 2D `[H*W, C]` transpose that the
327        // tiled copy below handles at near-memcpy speed. Without the
328        // collapse, the 4D ND fallback scalar-walks the tensor.
329        let collapsed = collapse_for_copy(self.layout.shape(), self.layout.strides());
330        let (shape, strides) = collapsed.as_slices();
331        let offset = self.layout.start_offset() as isize;
332        let all_positive = strides.iter().all(|&s| s >= 0);
333
334        if n == 0 {
335            return Self::empty(self.layout.shape().clone(), self.dtype);
336        }
337
338        if shape.len() <= 1 {
339            // 0-D scalar or 1-D run with a uniform stride (positive or negative).
340            let collapsed_numel = if shape.is_empty() { 1 } else { shape[0] };
341            debug_assert_eq!(n, collapsed_numel);
342            // SAFETY: capacity is n; we fill every position below.
343            unsafe { dst.set_len(n) };
344            if shape.is_empty() {
345                if n > 0 {
346                    dst[0] = src[offset as usize];
347                }
348            } else {
349                let len = shape[0];
350                let stride = strides[0];
351                if stride == 1 {
352                    dst[..len].copy_from_slice(&src[offset as usize..offset as usize + len]);
353                } else if stride == -1 {
354                    let start = (offset - (len as isize - 1)) as usize;
355                    let src_slice = &src[start..start + len];
356                    for (slot, &val) in dst[..len].iter_mut().zip(src_slice.iter().rev()) {
357                        *slot = val;
358                    }
359                } else {
360                    for (i, slot) in dst.iter_mut().take(len).enumerate() {
361                        let idx = (offset + i as isize * stride) as usize;
362                        *slot = src[idx];
363                    }
364                }
365            }
366        } else if shape.len() == 2 && all_positive {
367            // 2D positive-stride (transpose-like): tile both dims so
368            // reads stay in cache. The loop-nesting chooser inside
369            // `copy_2d_tiled` picks whichever ordering puts the
370            // smaller source stride on the innermost loop.
371            debug_assert_eq!(shape[0] * shape[1], n, "2D strides must cover all elements");
372            // SAFETY: capacity is n; `copy_2d_tiled` writes every
373            // `(row, col)` position exactly once.
374            unsafe { dst.set_len(n) };
375            copy_2d_tiled(
376                &mut dst, src, offset, shape[0], shape[1], strides[0], strides[1],
377            );
378        } else if shape.len() == 2 && !all_positive {
379            // 2D negative-stride (flipped): fast row-based copies.
380            debug_assert_eq!(shape[0] * shape[1], n);
381            unsafe { dst.set_len(n) };
382            copy_2d_negative(
383                &mut dst, src, offset, shape[0], shape[1], strides[0], strides[1],
384            );
385        } else if shape.len() >= 3 && all_positive && is_transposed_2d(shape, strides) {
386            // Batched 2D transpose: iterate over leading batch dims and execute copy_2d_tiled per slice.
387            unsafe { dst.set_len(n) };
388            copy_batched_2d_tiled(&mut dst, src, offset, shape, strides);
389        } else if all_positive && strides.last().copied() == Some(1) {
390            // Since the inner stride is 1, we can bulk-copy
391            // the contiguous inner run for each outer index.
392            unsafe { dst.set_len(n) };
393            copy_inner_contiguous_run(&mut dst, src, offset, shape, strides);
394        } else {
395            // General fallback: covers negative strides (flipped
396            // tensors) and ND layouts that can't collapse to ≤2D.
397            for idx in crate::strided_index::StridedIter::new(&self.layout) {
398                dst.push(src[idx]);
399            }
400        }
401
402        let bytes = Bytes::from_elems(dst);
403        let layout = Layout::contiguous(self.layout.shape().clone());
404        Self {
405            data: Arc::new(bytes),
406            layout,
407            dtype: self.dtype,
408        }
409    }
410
411    /// Reshape tensor. Zero-copy if contiguous.
412    pub fn reshape(&self, new_shape: Shape) -> Self {
413        assert_eq!(
414            self.layout.num_elements(),
415            new_shape.num_elements(),
416            "reshape must preserve total elements"
417        );
418
419        if let Some(new_layout) = self.layout.reshape(new_shape.clone()) {
420            Self {
421                data: Arc::clone(&self.data),
422                layout: new_layout,
423                dtype: self.dtype,
424            }
425        } else {
426            // Non-contiguous: copy first
427            self.to_contiguous().reshape(new_shape)
428        }
429    }
430
431    /// Transpose two dimensions. Zero-copy (metadata only).
432    pub fn transpose(&self, dim1: usize, dim2: usize) -> Self {
433        Self {
434            data: Arc::clone(&self.data),
435            layout: self.layout.transpose(dim1, dim2),
436            dtype: self.dtype,
437        }
438    }
439
440    /// Narrow/slice along a dimension. Zero-copy (metadata only).
441    pub fn narrow(&self, dim: usize, start: usize, len: usize) -> Self {
442        Self {
443            data: Arc::clone(&self.data),
444            layout: self.layout.narrow(dim, start, len),
445            dtype: self.dtype,
446        }
447    }
448
449    /// Permute dimensions according to axes. Zero-copy (metadata only).
450    pub fn permute(&self, axes: &[usize]) -> Self {
451        Self {
452            data: Arc::clone(&self.data),
453            layout: self.layout.permute(axes),
454            dtype: self.dtype,
455        }
456    }
457}
458
459impl TensorMetadata for FlexTensor {
460    type Device = FlexDevice;
461
462    fn dtype(&self) -> DType {
463        self.dtype
464    }
465
466    fn shape(&self) -> Shape {
467        self.layout.shape().clone()
468    }
469
470    fn rank(&self) -> usize {
471        self.layout.num_dims()
472    }
473
474    fn device(&self) -> Self::Device {
475        FlexDevice
476    }
477
478    fn can_mut(&self) -> bool {
479        self.is_unique()
480    }
481}
482
483/// Max rank we're willing to handle without falling back to the
484/// strided iterator. Burn tensors are capped at 8 dims in practice.
485const COLLAPSE_MAX_RANK: usize = 8;
486
487/// Collapsed layout result of [`collapse_for_copy`], stored in stack
488/// arrays so `to_contiguous()` doesn't have to hit the allocator on
489/// its hot path.
490#[derive(Debug, Clone, Copy)]
491struct CollapsedLayout {
492    ndim: usize,
493    shape: [usize; COLLAPSE_MAX_RANK],
494    strides: [isize; COLLAPSE_MAX_RANK],
495}
496
497impl CollapsedLayout {
498    #[inline]
499    fn as_slices(&self) -> (&[usize], &[isize]) {
500        (&self.shape[..self.ndim], &self.strides[..self.ndim])
501    }
502}
503
504/// Collapse a shape/stride pair into the minimum-rank equivalent
505/// layout for a contiguous copy:
506///
507/// 1. Squeeze size-1 dims (their stride never gets stepped past 0).
508/// 2. Merge adjacent dims `(i, i+1)` when
509///    `stride[i] == stride[i+1] * shape[i+1]`, which means the two
510///    dims form a single logical run through memory.
511///
512/// Canonical example: a 4D ConvNeXt input `[1, 244, 224, 48]` with
513/// strides `[2_623_488, 224, 1, 54656]` (from
514/// `[N, C, H, W].permute([0, 2, 3, 1])`) collapses to 2D
515/// `[54656, 48]` with strides `[1, 54656]`.
516///
517/// If the input rank exceeds [`COLLAPSE_MAX_RANK`] the result is
518/// left at rank > 2 so the caller falls through to its generic
519/// strided path. If the input is rank > `COLLAPSE_MAX_RANK`, we
520/// return the original (un-collapsed) layout truncated, which the
521/// caller will reject via its `shape.len() == 2` gate.
522///
523/// PRECONDITION: the caller must gate on all-positive strides before
524/// using the collapsed layout. The merge rule assumes positive
525/// strides and will produce iteration-order-incorrect results for
526/// flipped tensors.
527fn collapse_for_copy(shape: &[usize], strides: &[isize]) -> CollapsedLayout {
528    let mut out = CollapsedLayout {
529        ndim: 0,
530        shape: [0; COLLAPSE_MAX_RANK],
531        strides: [0; COLLAPSE_MAX_RANK],
532    };
533
534    // Bail out to the caller's fallback if the rank is too large to
535    // fit our stack buffer. In practice this never triggers (burn
536    // tensors are ≤8 dims), but leaving the `ndim` high signals the
537    // caller to take the generic strided path.
538    if shape.len() > COLLAPSE_MAX_RANK {
539        out.ndim = shape.len().min(COLLAPSE_MAX_RANK);
540        return out;
541    }
542
543    // Single forward sweep: squeeze size-1 dims and merge whenever
544    // the current dim's `stride * size` equals the previous output
545    // dim's stride (i.e. the two form a contiguous run).
546    //
547    // Use `checked_mul` so a pathological layout whose stride math
548    // would overflow `isize` simply fails to merge rather than
549    // wrapping into an incorrect merge decision. Real tensors can't
550    // hit this (total numel is bounded by `isize::MAX`), but
551    // hand-built layouts passed through the test paths could.
552    for (&s, &st) in shape.iter().zip(strides.iter()) {
553        if s == 1 {
554            continue;
555        }
556        let merge = out.ndim > 0
557            && st > 0
558            && out.strides[out.ndim - 1] > 0
559            && (s as isize)
560                .checked_mul(st)
561                .is_some_and(|run| out.strides[out.ndim - 1] == run);
562        if merge {
563            out.shape[out.ndim - 1] *= s;
564            out.strides[out.ndim - 1] = st;
565        } else {
566            out.shape[out.ndim] = s;
567            out.strides[out.ndim] = st;
568            out.ndim += 1;
569        }
570    }
571
572    out
573}
574
575#[inline]
576fn is_transposed_2d(shape: &[usize], strides: &[isize]) -> bool {
577    let rank = shape.len();
578    if rank < 3 {
579        return false;
580    }
581    let r_st = strides[rank - 2];
582    let c_st = strides[rank - 1];
583    (r_st == 1 && c_st > 1) || (c_st == 1 && r_st > 1 && r_st != shape[rank - 1] as isize)
584}
585
586fn copy_batched_2d_tiled<E: Copy + Send + Sync>(
587    dst: &mut [E],
588    src: &[E],
589    offset: isize,
590    shape: &[usize],
591    strides: &[isize],
592) {
593    let rank = shape.len();
594    let rows = shape[rank - 2];
595    let cols = shape[rank - 1];
596    let r_st = strides[rank - 2];
597    let c_st = strides[rank - 1];
598    let slice_len = rows * cols;
599
600    let batch_shape = &shape[..rank - 2];
601    let batch_strides = &strides[..rank - 2];
602    #[cfg(feature = "rayon")]
603    {
604        let batch_count: usize = batch_shape.iter().product();
605        if batch_count * slice_len >= crate::ops::PARALLEL_THRESHOLD && batch_count > 1 {
606            use rayon::prelude::*;
607            dst.par_chunks_mut(slice_len)
608                .enumerate()
609                .for_each(|(b, slice_dst)| {
610                    let mut remaining = b;
611                    let mut batch_offset = offset;
612                    for d in (0..batch_shape.len()).rev() {
613                        let coord = remaining % batch_shape[d];
614                        remaining /= batch_shape[d];
615                        batch_offset += coord as isize * batch_strides[d];
616                    }
617                    copy_2d_tiled(slice_dst, src, batch_offset, rows, cols, r_st, c_st);
618                });
619            return;
620        }
621    }
622
623    for (b, slice_dst) in dst.chunks_mut(slice_len).enumerate() {
624        let mut remaining = b;
625        let mut batch_offset = offset;
626        for d in (0..batch_shape.len()).rev() {
627            let coord = remaining % batch_shape[d];
628            remaining /= batch_shape[d];
629            batch_offset += coord as isize * batch_strides[d];
630        }
631        copy_2d_tiled(slice_dst, src, batch_offset, rows, cols, r_st, c_st);
632    }
633}
634
635fn copy_2d_negative<E: Copy + Send + Sync>(
636    dst: &mut [E],
637    src: &[E],
638    offset: isize,
639    rows: usize,
640    cols: usize,
641    row_stride: isize,
642    col_stride: isize,
643) {
644    #[cfg(feature = "rayon")]
645    let n = rows * cols;
646    if row_stride < 0 && col_stride == 1 {
647        #[cfg(feature = "rayon")]
648        if n >= crate::ops::PARALLEL_THRESHOLD {
649            use rayon::prelude::*;
650            dst.par_chunks_mut(cols)
651                .enumerate()
652                .for_each(|(r, dst_row)| {
653                    let row_src = (offset + r as isize * row_stride) as usize;
654                    dst_row.copy_from_slice(&src[row_src..row_src + cols]);
655                });
656            return;
657        }
658        for r in 0..rows {
659            let row_src = (offset + r as isize * row_stride) as usize;
660            let dst_start = r * cols;
661            dst[dst_start..dst_start + cols].copy_from_slice(&src[row_src..row_src + cols]);
662        }
663    } else if col_stride == -1 {
664        #[cfg(feature = "rayon")]
665        if n >= crate::ops::PARALLEL_THRESHOLD {
666            use rayon::prelude::*;
667            dst.par_chunks_mut(cols)
668                .enumerate()
669                .for_each(|(r, dst_row)| {
670                    let row_src_end = (offset + r as isize * row_stride) as usize;
671                    let row_src_start = row_src_end + 1 - cols;
672                    let src_slice = &src[row_src_start..=row_src_end];
673                    for (slot, &val) in dst_row.iter_mut().zip(src_slice.iter().rev()) {
674                        *slot = val;
675                    }
676                });
677            return;
678        }
679        for r in 0..rows {
680            let row_src_end = (offset + r as isize * row_stride) as usize;
681            let row_src_start = row_src_end + 1 - cols;
682            let src_slice = &src[row_src_start..=row_src_end];
683            let dst_row = &mut dst[r * cols..(r + 1) * cols];
684            for (slot, &val) in dst_row.iter_mut().zip(src_slice.iter().rev()) {
685                *slot = val;
686            }
687        }
688    } else {
689        for r in 0..rows {
690            let row_base = offset + r as isize * row_stride;
691            let dst_row = &mut dst[r * cols..(r + 1) * cols];
692            for (c, slot) in dst_row.iter_mut().enumerate() {
693                let idx = (row_base + c as isize * col_stride) as usize;
694                *slot = src[idx];
695            }
696        }
697    }
698}
699
700/// Copy a strided source into a contiguous destination by treating the
701/// layout as a set of strided outer dims wrapping a contiguous inner run.
702///
703/// Precondition: strides are all positive and the innermost stride is 1
704#[inline]
705fn copy_inner_contiguous_run<E: Copy>(
706    dst: &mut [E],
707    src: &[E],
708    offset: isize,
709    shape: &[usize],
710    strides: &[isize],
711) {
712    debug_assert!(!shape.is_empty());
713    debug_assert_eq!(*strides.last().unwrap(), 1, "innermost stride must be 1");
714
715    // We want the maximal trailing run that is contiguous in src.
716    // Walk inward-out, a dim dim_idx extends the contiguous run if AND ONLY IF ("iff")
717    // strides[dim_idx] == product of inner shapes seen so far.
718    let mut expected_stride: isize = 1;
719    let mut split = shape.len(); // first OUTER index (dims [split..] are inner)
720    while split > 0 {
721        let dim_idx = split - 1;
722        if strides[dim_idx] == expected_stride {
723            expected_stride = expected_stride
724                .checked_mul(shape[dim_idx] as isize)
725                .expect("contiguous run length overflows isize");
726            split -= 1;
727        } else {
728            break;
729        }
730    }
731
732    let outer_shape = &shape[..split];
733    let outer_strides = &strides[..split];
734    let inner_run_length: usize = shape[split..].iter().product();
735
736    // Whole tensor is one contiguous run! We can bulk-copy the whole thing.
737    if outer_shape.is_empty() {
738        let src_index = offset as usize;
739        dst[..inner_run_length].copy_from_slice(&src[src_index..src_index + inner_run_length]);
740        return;
741    }
742
743    if inner_run_length == 0 || outer_shape.contains(&0) {
744        return;
745    }
746
747    // Walk the outer index space in row-major order via an odometer:
748    // each iteration ticks the rightmost counter, and so on overflow we
749    // reset it and carry +1 into the dim to its left.
750    // Row-major is what we want because dst is contiguous (consecutive outer-index
751    // tuples land in consecutive inner_run_length slots, so we just bump
752    // dst_position each step instead of recomputing it)
753    // We use this instead of N nested for loops because the rank isn't
754    // known at compile time.
755    let mut counter = [0usize; COLLAPSE_MAX_RANK];
756    let outer_dim_count = outer_shape.len();
757    let mut dst_position = 0usize;
758
759    loop {
760        let mut src_start = offset;
761        for dim_idx in 0..outer_dim_count {
762            src_start += counter[dim_idx] as isize * outer_strides[dim_idx];
763        }
764        let src_index = src_start as usize;
765        dst[dst_position..dst_position + inner_run_length]
766            .copy_from_slice(&src[src_index..src_index + inner_run_length]);
767        dst_position += inner_run_length;
768
769        let mut dim_idx = outer_dim_count;
770        loop {
771            if dim_idx == 0 {
772                return;
773            }
774            dim_idx -= 1;
775            counter[dim_idx] += 1;
776            if counter[dim_idx] < outer_shape[dim_idx] {
777                break;
778            }
779            counter[dim_idx] = 0;
780        }
781    }
782}
783
784/// Minimum number of elements required to trigger parallel 2D tiled copy.
785/// Avoids thread fork/steal overhead on ~1MB cache-resident tensors.
786#[cfg(any(feature = "rayon", test))]
787pub(crate) const COPY_2D_PARALLEL_THRESHOLD: usize = 1_048_576;
788
789/// Tiled 2D copy from a strided source into a contiguous destination.
790/// The loop nesting is chosen so the innermost read walks whichever
791/// source stride is smaller, which keeps the hot loop in cache even
792/// for transpose-like layouts.
793#[inline]
794fn copy_2d_tiled<E: Copy + Send + Sync>(
795    dst: &mut [E],
796    src: &[E],
797    offset: isize,
798    rows: usize,
799    cols: usize,
800    row_stride: isize,
801    col_stride: isize,
802) {
803    const TILE: usize = 16;
804
805    #[cfg(feature = "rayon")]
806    if rows * cols >= COPY_2D_PARALLEL_THRESHOLD {
807        use rayon::prelude::*;
808        let total_tile_rows = rows.div_ceil(TILE);
809        let num_threads = rayon::current_num_threads();
810        let tile_rows_per_task = total_tile_rows.div_ceil(num_threads).max(1);
811        let chunk_len = tile_rows_per_task * TILE * cols;
812
813        dst.par_chunks_mut(chunk_len)
814            .enumerate()
815            .for_each(|(task_idx, dst_chunk)| {
816                let row_start = task_idx * tile_rows_per_task * TILE;
817                let chunk_rows = dst_chunk.len() / cols;
818                if row_stride <= col_stride {
819                    // row-inside-col: the inner loop walks `row_stride` (smaller).
820                    for col_tile in (0..cols).step_by(TILE) {
821                        let col_end = (col_tile + TILE).min(cols);
822                        for r_tile in (0..chunk_rows).step_by(TILE) {
823                            let r_end = (r_tile + TILE).min(chunk_rows);
824                            for col in col_tile..col_end {
825                                let col_base = offset + col as isize * col_stride;
826                                for r in r_tile..r_end {
827                                    let row = row_start + r;
828                                    let idx = (col_base + row as isize * row_stride) as usize;
829                                    // SAFETY: caller set `dst.len() == rows * cols`
830                                    // and each `(r, col)` is visited once in this chunk.
831                                    unsafe {
832                                        *dst_chunk.get_unchecked_mut(r * cols + col) = src[idx];
833                                    }
834                                }
835                            }
836                        }
837                    }
838                } else {
839                    // col-inside-row: the inner loop walks `col_stride` (smaller).
840                    for r_tile in (0..chunk_rows).step_by(TILE) {
841                        let r_end = (r_tile + TILE).min(chunk_rows);
842                        for col_tile in (0..cols).step_by(TILE) {
843                            let col_end = (col_tile + TILE).min(cols);
844                            for r in r_tile..r_end {
845                                let row = row_start + r;
846                                let row_base = offset
847                                    + row as isize * row_stride
848                                    + col_tile as isize * col_stride;
849                                let dst_base = r * cols + col_tile;
850                                for c in 0..(col_end - col_tile) {
851                                    let idx = (row_base + c as isize * col_stride) as usize;
852                                    // SAFETY: caller set `dst.len() == rows * cols`
853                                    // and each `(r, col)` is visited once in this chunk.
854                                    unsafe {
855                                        *dst_chunk.get_unchecked_mut(dst_base + c) = src[idx];
856                                    }
857                                }
858                            }
859                        }
860                    }
861                }
862            });
863        return;
864    }
865
866    if row_stride <= col_stride {
867        // row-inside-col: the inner loop walks `row_stride` (smaller).
868        for col_tile in (0..cols).step_by(TILE) {
869            let col_end = (col_tile + TILE).min(cols);
870            for row_tile in (0..rows).step_by(TILE) {
871                let row_end = (row_tile + TILE).min(rows);
872                for col in col_tile..col_end {
873                    let col_base = offset + col as isize * col_stride;
874                    for row in row_tile..row_end {
875                        let idx = (col_base + row as isize * row_stride) as usize;
876                        // SAFETY: caller set `dst.len() == rows * cols`
877                        // and each `(row, col)` is visited once.
878                        unsafe {
879                            *dst.get_unchecked_mut(row * cols + col) = src[idx];
880                        }
881                    }
882                }
883            }
884        }
885    } else {
886        // col-inside-row: the inner loop walks `col_stride` (smaller).
887        for row_tile in (0..rows).step_by(TILE) {
888            let row_end = (row_tile + TILE).min(rows);
889            for col_tile in (0..cols).step_by(TILE) {
890                let col_end = (col_tile + TILE).min(cols);
891                for row in row_tile..row_end {
892                    let row_base =
893                        offset + row as isize * row_stride + col_tile as isize * col_stride;
894                    let dst_base = row * cols + col_tile;
895                    for c in 0..(col_end - col_tile) {
896                        let idx = (row_base + c as isize * col_stride) as usize;
897                        // SAFETY: caller set `dst.len() == rows * cols`
898                        // and each `(row, col)` is visited once.
899                        unsafe {
900                            *dst.get_unchecked_mut(dst_base + c) = src[idx];
901                        }
902                    }
903                }
904            }
905        }
906    }
907}
908
909/// Get the size in bytes for a dtype element.
910///
911/// Matches `burn_std::DType::size()` semantics: Bool(Native) and Bool(U8) are
912/// 1 byte, Bool(U32) is 4 bytes. This makes buffer-size validation correct
913/// regardless of which BoolStore variant the dtype carries.
914///
915/// # Panics
916///
917/// Panics if the dtype has a zero-byte element size. `burn_std::DType::size()`
918/// returns 0 for sub-byte quantized dtypes (Q4F, Q4S, Q2F, Q2S, and most
919/// `QuantStore::PackedNative` variants). burn-flex does not yet support these
920/// packed quantization formats; passing them here would silently produce
921/// empty allocations in `FlexTensor::empty`, truncated buffers in `into_data`,
922/// and zero-byte memcpys in `repeat_dim`. The panic turns all three into a
923/// loud, actionable failure at the dispatch boundary.
924pub(crate) fn dtype_size(dtype: DType) -> usize {
925    // Delegate to burn-std's canonical size to stay in sync.
926    let size = dtype.size();
927    assert!(
928        size > 0,
929        "burn-flex: dtype {:?} has zero-byte element size (sub-byte packed \
930         quantization is not yet supported)",
931        dtype
932    );
933    size
934}
935
936#[cfg(test)]
937mod tests {
938    use super::*;
939    use alloc::vec;
940
941    #[test]
942    fn test_from_data_roundtrip() {
943        let data = TensorData::from([1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
944        let tensor = FlexTensor::from_data(data.clone());
945        let result = tensor.into_data();
946        assert_eq!(data.shape(), result.shape());
947        assert_eq!(data.dtype(), result.dtype());
948    }
949
950    #[test]
951    fn test_collapse_for_copy_squeezes_size1_and_merges_contig() {
952        // Permuted ConvNeXt input: [1, 48, 244, 224].permute([0,2,3,1]).
953        let shape = vec![1, 244, 224, 48];
954        let strides = vec![2_623_488_isize, 224, 1, 54656];
955        let collapsed = collapse_for_copy(&shape, &strides);
956        let (s, st) = collapsed.as_slices();
957        assert_eq!(s, &[54656, 48]);
958        assert_eq!(st, &[1, 54656]);
959    }
960
961    #[test]
962    fn test_collapse_for_copy_already_contiguous_3d() {
963        let collapsed = collapse_for_copy(&[2, 3, 4], &[12, 4, 1]);
964        let (s, st) = collapsed.as_slices();
965        assert_eq!(s, &[24]);
966        assert_eq!(st, &[1]);
967    }
968
969    #[test]
970    fn test_collapse_for_copy_transpose_2d() {
971        let collapsed = collapse_for_copy(&[5, 3], &[1, 5]);
972        let (s, st) = collapsed.as_slices();
973        assert_eq!(s, &[5, 3]);
974        assert_eq!(st, &[1, 5]);
975    }
976
977    #[test]
978    fn test_collapse_for_copy_all_size1() {
979        let collapsed = collapse_for_copy(&[1, 1, 1], &[0, 0, 0]);
980        let (s, st) = collapsed.as_slices();
981        assert!(s.is_empty());
982        assert!(st.is_empty());
983    }
984
985    /// Regression: an empty 1D view produced by `narrow` at a
986    /// non-zero offset forces `copy_contiguous` to run (it can't
987    /// early-return via the contiguous-at-offset-0 shortcut). The
988    /// old `debug_assert_eq!(n, shape.product().max(1))` tripped
989    /// for this shape because `.max(1)` produced 1 while the true
990    /// numel is 0.
991    #[test]
992    fn test_to_contiguous_zero_sized_narrowed() {
993        let t = FlexTensor::from_data(TensorData::new(
994            (0..6).map(|i| i as f32).collect::<Vec<_>>(),
995            vec![6],
996        ));
997        // narrow(dim, start=3, len=0): shape [0], start_offset 3.
998        let empty_view = t.narrow(0, 3, 0);
999        assert_eq!(empty_view.shape().to_vec(), vec![0]);
1000        assert_ne!(empty_view.layout().start_offset(), 0);
1001
1002        let contig = empty_view.to_contiguous();
1003        assert_eq!(contig.shape().to_vec(), vec![0]);
1004        assert_eq!(contig.layout().start_offset(), 0);
1005        assert_eq!(contig.into_data().bytes().len(), 0);
1006    }
1007
1008    #[test]
1009    fn test_to_contiguous_zero_sized_negative_stride() {
1010        let t = FlexTensor::from_data(TensorData::new(
1011            (0..6).map(|i| i as f32).collect::<Vec<_>>(),
1012            vec![6],
1013        ));
1014        let empty_neg = crate::ops::slice::slice(t, &[burn_std::Slice::new(3, Some(3), -1)]);
1015        assert_eq!(empty_neg.shape().to_vec(), vec![0]);
1016        let contig = empty_neg.to_contiguous();
1017        assert_eq!(contig.shape().to_vec(), vec![0]);
1018        assert_eq!(contig.into_data().bytes().len(), 0);
1019    }
1020
1021    /// Regression for #4855: a prefix view (e.g. `narrow(dim, 0, n)`) has
1022    /// canonical contiguous strides and start_offset 0, but its underlying
1023    /// buffer is still the larger original. `to_contiguous` must materialize
1024    /// a right-sized copy so callers keying off `storage().len()` (like the
1025    /// SIMD `mask_fill_*` kernels reached from `triu`/`tril` in LU on tall
1026    /// matrices) don't walk past the logical shape.
1027    #[test]
1028    fn test_to_contiguous_prefix_view_shrinks_buffer() {
1029        let data: Vec<f32> = (0..40).map(|i| i as f32).collect();
1030        let t = FlexTensor::from_data(TensorData::new(data, vec![8, 5]));
1031
1032        let prefix = t.narrow(0, 0, 5);
1033        assert_eq!(prefix.shape().to_vec(), vec![5, 5]);
1034        assert_eq!(prefix.layout().strides(), &[5, 1]);
1035        assert_eq!(prefix.layout().start_offset(), 0);
1036        assert!(prefix.is_contiguous());
1037        assert_eq!(prefix.storage::<f32>().len(), 40);
1038
1039        let contig = prefix.to_contiguous();
1040        assert_eq!(contig.storage::<f32>().len(), 25);
1041        assert_eq!(contig.layout().num_elements(), 25);
1042        assert_eq!(
1043            contig.storage::<f32>(),
1044            &(0..5)
1045                .flat_map(|r| (0..5).map(move |c| (r * 5 + c) as f32))
1046                .collect::<Vec<_>>()[..]
1047        );
1048    }
1049
1050    /// 4D permuted layout round-trips through the collapse + tiled
1051    /// copy path. Mirrors the ConvNeXt channels-last permute.
1052    #[test]
1053    fn test_to_contiguous_4d_permuted_matches_naive() {
1054        let dims = [1, 48, 4, 5];
1055        let n: usize = dims.iter().product();
1056        let data: Vec<f32> = (0..n).map(|i| i as f32).collect();
1057        let t = FlexTensor::from_data(TensorData::new(data.clone(), dims.to_vec()));
1058        let permuted = t.permute(&[0, 2, 3, 1]);
1059        assert!(!permuted.is_contiguous());
1060
1061        let contig = permuted.to_contiguous();
1062        assert!(contig.is_contiguous());
1063        assert_eq!(contig.shape().to_vec(), vec![1, 4, 5, 48]);
1064
1065        // Expected via manual strided walk of the source.
1066        let mut expected = Vec::with_capacity(n);
1067        for h in 0..4 {
1068            for w in 0..5 {
1069                for c in 0..48 {
1070                    let idx = c * 20 + h * 5 + w;
1071                    expected.push(data[idx]);
1072                }
1073            }
1074        }
1075
1076        let result_data = contig.into_data();
1077        let values = result_data.as_slice::<f32>().unwrap();
1078        assert_eq!(values, expected.as_slice());
1079    }
1080
1081    /// Regression: the ShuffleNet channel shuffle (reshape -> permute([0,2,1,3,4]) -> reshape)
1082    /// collapses to a 3D layout with stride-1 innermost.
1083    /// Before this fix that went into the StridedIter scalar walk in the final else in copy_contiguous
1084    /// Now, we use a new inner-contiguous branch that bulk-copies the trailing run
1085    #[test]
1086    fn test_to_contiguous_3d_inner_stride1_matches_naive() {
1087        let dims = [1, 2, 4, 3, 3]; // [N, G, C, H, W]
1088        let n: usize = dims.iter().product();
1089        let data: Vec<f32> = (0..n).map(|i| i as f32).collect();
1090        let t = FlexTensor::from_data(TensorData::new(data.clone(), dims.to_vec()));
1091        let permuted = t.permute(&[0, 2, 1, 3, 4]); // swap G and C from the original dims in the permute
1092        assert!(!permuted.is_contiguous());
1093
1094        let contiguous_data = permuted.to_contiguous();
1095        assert!(contiguous_data.is_contiguous());
1096        assert_eq!(contiguous_data.shape().to_vec(), vec![1, 4, 2, 3, 3]);
1097
1098        // Now we manually do a strided walk to be able to check that against a naive solution.
1099        // output[0, c, g, h, w] = input[0, g, c, h, w].
1100        let mut expected = Vec::with_capacity(n);
1101        for c in 0..4 {
1102            for g in 0..2 {
1103                for h in 0..3 {
1104                    for w in 0..3 {
1105                        let idx = g * 36 + c * 9 + h * 3 + w;
1106                        expected.push(data[idx]);
1107                    }
1108                }
1109            }
1110        }
1111
1112        // Verify the naive solution matches (the manual strided walk).
1113        let result_data = contiguous_data.into_data();
1114        let values = result_data.as_slice::<f32>().unwrap();
1115        assert_eq!(values, expected.as_slice());
1116    }
1117
1118    /// Exercise the `row_stride > col_stride` branch of the 2D tiled
1119    /// copy (the ConvNeXt case hits the other branch).
1120    #[test]
1121    fn test_to_contiguous_2d_row_stride_gt_col_stride() {
1122        // `slice(s![..;2, ..])` on a [6, 3] contiguous tensor gives a
1123        // [3, 3] view with strides [6, 1] that doesn't collapse, so
1124        // the 2D branch runs with row_stride > col_stride.
1125        let data: Vec<f32> = (0..18).map(|i| i as f32).collect();
1126        let t = FlexTensor::from_data(TensorData::new(data, vec![6, 3]));
1127        let stepped = crate::ops::slice::slice(
1128            t,
1129            &[
1130                burn_std::Slice::new(0, Some(6), 2),
1131                burn_std::Slice::new(0, None, 1),
1132            ],
1133        );
1134        // Verify the layout matches what the branch requires.
1135        assert_eq!(stepped.layout().shape().to_vec(), vec![3, 3]);
1136        assert_eq!(stepped.layout().strides(), &[6, 1]);
1137        assert!(!stepped.layout().is_contiguous());
1138
1139        let contig = stepped.to_contiguous();
1140        assert!(contig.is_contiguous());
1141        assert_eq!(contig.shape().to_vec(), vec![3, 3]);
1142
1143        let result_data = contig.into_data();
1144        let values = result_data.as_slice::<f32>().unwrap();
1145        // Expected: rows 0, 2, 4 of the original 6x3 tensor.
1146        let expected = vec![
1147            0.0f32, 1.0, 2.0, // row 0
1148            6.0, 7.0, 8.0, // row 2
1149            12.0, 13.0, 14.0, // row 4
1150        ];
1151        assert_eq!(values, expected.as_slice());
1152    }
1153
1154    /// Exercise the `row_stride < col_stride` branch of the 2D tiled
1155    /// copy (e.g. transposed 2D layout).
1156    #[test]
1157    fn test_to_contiguous_2d_row_stride_lt_col_stride() {
1158        let data: Vec<f32> = (0..18).map(|i| i as f32).collect();
1159        let t = FlexTensor::from_data(TensorData::new(data, vec![3, 6]));
1160        let transposed = t.transpose(0, 1);
1161        assert_eq!(transposed.layout().strides(), &[1, 6]);
1162        assert!(!transposed.layout().is_contiguous());
1163
1164        let contig = transposed.to_contiguous();
1165        assert!(contig.is_contiguous());
1166        assert_eq!(contig.shape().to_vec(), vec![6, 3]);
1167
1168        let result_data = contig.into_data();
1169        let values = result_data.as_slice::<f32>().unwrap();
1170        let mut expected = Vec::with_capacity(18);
1171        for c in 0..6 {
1172            for r in 0..3 {
1173                expected.push((r * 6 + c) as f32);
1174            }
1175        }
1176        assert_eq!(values, expected.as_slice());
1177    }
1178
1179    /// Parallel 2D tiled copy when `row_stride <= col_stride` (>= 1M elements).
1180    #[test]
1181    fn test_to_contiguous_2d_parallel_row_stride_le_col_stride() {
1182        let rows = 1024;
1183        let cols = 1024;
1184        let n = rows * cols;
1185        assert!(n >= COPY_2D_PARALLEL_THRESHOLD);
1186
1187        let data: Vec<f32> = (0..n).map(|i| (i % 997) as f32).collect();
1188        let t = FlexTensor::from_data(TensorData::new(data.clone(), vec![rows, cols]));
1189        let transposed = t.transpose(0, 1);
1190        assert_eq!(transposed.layout().strides(), &[1, rows as isize]);
1191        assert!(!transposed.layout().is_contiguous());
1192
1193        let contig = transposed.to_contiguous();
1194        assert!(contig.is_contiguous());
1195        assert_eq!(contig.shape().to_vec(), vec![cols, rows]);
1196
1197        let result_data = contig.into_data();
1198        let values = result_data.as_slice::<f32>().unwrap();
1199        for r in 0..cols {
1200            for c in 0..rows {
1201                let expected = data[c * cols + r];
1202                assert_eq!(values[r * rows + c], expected);
1203            }
1204        }
1205    }
1206
1207    /// Parallel 2D tiled copy when `row_stride > col_stride` (>= 1M elements).
1208    #[test]
1209    fn test_to_contiguous_2d_parallel_row_stride_gt_col_stride() {
1210        let rows = 1024;
1211        let cols = 1024;
1212        let n = rows * cols;
1213        assert!(n >= COPY_2D_PARALLEL_THRESHOLD);
1214
1215        // [1024, 2048] tensor sliced to [1024, 1024] => row_stride = 2048, col_stride = 1
1216        let full_cols = 2048;
1217        let data: Vec<f32> = (0..rows * full_cols).map(|i| (i % 997) as f32).collect();
1218        let t = FlexTensor::from_data(TensorData::new(data.clone(), vec![rows, full_cols]));
1219        let sliced = crate::ops::slice::slice(
1220            t,
1221            &[
1222                burn_std::Slice::new(0, Some(rows as isize), 1),
1223                burn_std::Slice::new(0, Some(cols as isize), 1),
1224            ],
1225        );
1226        assert_eq!(sliced.layout().strides(), &[full_cols as isize, 1]);
1227        assert!(!sliced.layout().is_contiguous());
1228
1229        let contig = sliced.to_contiguous();
1230        assert!(contig.is_contiguous());
1231        assert_eq!(contig.shape().to_vec(), vec![rows, cols]);
1232
1233        let result_data = contig.into_data();
1234        let values = result_data.as_slice::<f32>().unwrap();
1235        for r in 0..rows {
1236            for c in 0..cols {
1237                let expected = data[r * full_cols + c];
1238                assert_eq!(values[r * cols + c], expected);
1239            }
1240        }
1241    }
1242
1243    #[test]
1244    fn test_reshape() {
1245        let data = TensorData::new(vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], vec![2, 3]);
1246        let tensor = FlexTensor::from_data(data);
1247        let reshaped = tensor.reshape(Shape::from(vec![3, 2]));
1248        assert_eq!(reshaped.shape().to_vec(), vec![3, 2]);
1249    }
1250
1251    #[test]
1252    fn test_transpose() {
1253        let data = TensorData::new(vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], vec![2, 3]);
1254        let tensor = FlexTensor::from_data(data);
1255        let transposed = tensor.transpose(0, 1);
1256        assert_eq!(transposed.shape().to_vec(), vec![3, 2]);
1257        assert!(!transposed.is_contiguous());
1258    }
1259
1260    #[test]
1261    fn test_clone_is_cheap() {
1262        let data = TensorData::from([1.0f32, 2.0, 3.0, 4.0]);
1263        let tensor = FlexTensor::from_data(data);
1264
1265        // Before clone, tensor is unique
1266        assert!(tensor.is_unique());
1267
1268        // Clone shares data
1269        let cloned = tensor.clone();
1270        assert!(!tensor.is_unique());
1271        assert!(!cloned.is_unique());
1272
1273        // Both point to same data
1274        assert!(core::ptr::eq(
1275            tensor.bytes().as_ptr(),
1276            cloned.bytes().as_ptr(),
1277        ));
1278    }
1279
1280    #[test]
1281    fn test_cow_on_mutation() {
1282        let data = TensorData::from([1.0f32, 2.0, 3.0, 4.0]);
1283        let tensor = FlexTensor::from_data(data);
1284        let mut cloned = tensor.clone();
1285
1286        // Both share data
1287        assert!(!tensor.is_unique());
1288        assert!(!cloned.is_unique());
1289
1290        // Mutate cloned - triggers COW
1291        let storage: &mut [f32] = cloned.storage_mut();
1292        storage[0] = 99.0;
1293
1294        // Now cloned has its own copy, tensor is unique again
1295        assert!(tensor.is_unique());
1296        assert!(cloned.is_unique());
1297
1298        // Data is different
1299        assert_ne!(tensor.bytes().as_ptr(), cloned.bytes().as_ptr());
1300        assert_eq!(tensor.storage::<f32>()[0], 1.0);
1301        assert_eq!(cloned.storage::<f32>()[0], 99.0);
1302    }
1303
1304    #[test]
1305    fn test_into_data_narrowed_at_offset_zero() {
1306        // [1, 2, 3, 4, 5, 6] shape [2, 3]
1307        let data = TensorData::new(vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], vec![2, 3]);
1308        let tensor = FlexTensor::from_data(data);
1309        // narrow to first row: shape [1, 3], offset 0, contiguous
1310        let narrowed = tensor.narrow(0, 0, 1);
1311        assert!(narrowed.is_contiguous());
1312        assert_eq!(narrowed.layout().start_offset(), 0);
1313
1314        let result = narrowed.into_data();
1315        assert_eq!(result.shape().to_vec(), vec![1, 3]);
1316        // Must have exactly 3 f32s = 12 bytes, not 24
1317        assert_eq!(result.bytes().len(), 3 * core::mem::size_of::<f32>());
1318        let values: Vec<f32> = result.try_into_vec().unwrap();
1319        assert_eq!(values, vec![1.0, 2.0, 3.0]);
1320    }
1321
1322    #[test]
1323    fn test_reshape_preserves_offset_zero_copy() {
1324        let data: Vec<f32> = (0..16).map(|i| i as f32).collect();
1325        let tensor = FlexTensor::from_data(TensorData::new(data, vec![4, 4]));
1326        // Slice rows 1..3: shape [2, 4], contiguous, start_offset = 4
1327        let sliced = tensor.narrow(0, 1, 2);
1328        assert!(sliced.is_contiguous());
1329        assert_eq!(sliced.layout().start_offset(), 4);
1330
1331        // Reshape [2, 4] -> [8] preserving non-zero offset
1332        let reshaped = sliced.reshape(Shape::from(vec![8]));
1333        assert!(reshaped.is_contiguous());
1334        assert_eq!(reshaped.layout().start_offset(), 4);
1335        assert_eq!(reshaped.shape().to_vec(), vec![8]);
1336
1337        // Guarantees zero-copy pointer identity
1338        assert!(core::ptr::eq(
1339            sliced.bytes().as_ptr(),
1340            reshaped.bytes().as_ptr()
1341        ));
1342
1343        let values: Vec<f32> = reshaped.into_data().try_into_vec().unwrap();
1344        assert_eq!(values, vec![4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0]);
1345    }
1346
1347    #[test]
1348    fn test_transpose_unit_dim_zero_copy() {
1349        // [6, 1] tensor transposed to [1, 6] has stride [1, 6].
1350        // Squeezing the size-1 dimension makes it contiguous!
1351        let data: Vec<f32> = (0..6).map(|i| i as f32).collect();
1352        let tensor = FlexTensor::from_data(TensorData::new(data, vec![6, 1]));
1353        let transposed = tensor.transpose(0, 1);
1354        assert_eq!(transposed.shape().to_vec(), vec![1, 6]);
1355        assert_eq!(transposed.layout().strides(), &[1, 1]);
1356        assert!(transposed.is_contiguous());
1357
1358        // to_contiguous should be a no-op that reuses the existing storage
1359        let contig = transposed.to_contiguous();
1360        assert!(core::ptr::eq(
1361            transposed.bytes().as_ptr(),
1362            contig.bytes().as_ptr()
1363        ));
1364    }
1365
1366    #[test]
1367    fn test_batched_transpose_parity() {
1368        // [2, 3, 4] transposed on trailing dims (1, 2) -> [2, 4, 3]
1369        let data: Vec<f32> = (0..24).map(|i| i as f32).collect();
1370        let tensor = FlexTensor::from_data(TensorData::new(data, vec![2, 3, 4]));
1371        let transposed = tensor.transpose(1, 2);
1372        assert_eq!(transposed.shape().to_vec(), vec![2, 4, 3]);
1373        assert!(!transposed.is_contiguous());
1374
1375        let contig = transposed.to_contiguous();
1376        assert!(contig.is_contiguous());
1377
1378        let values: Vec<f32> = contig.into_data().try_into_vec().unwrap();
1379        // Compute manual reference
1380        let mut expected = Vec::with_capacity(24);
1381        for b in 0..2 {
1382            for j in 0..4 {
1383                for i in 0..3 {
1384                    expected.push((b * 12 + i * 4 + j) as f32);
1385                }
1386            }
1387        }
1388        assert_eq!(values, expected);
1389    }
1390
1391    #[test]
1392    fn test_negative_stride_flip_acceleration() {
1393        let data: Vec<f32> = (0..12).map(|i| i as f32).collect();
1394        let tensor = FlexTensor::from_data(TensorData::new(data, vec![3, 4]));
1395        // Negative step on axis 0: reverse rows
1396        let flipped = crate::ops::slice::slice(
1397            tensor,
1398            &[
1399                burn_std::Slice::new(0, None, -1),
1400                burn_std::Slice::new(0, None, 1),
1401            ],
1402        );
1403        assert_eq!(flipped.shape().to_vec(), vec![3, 4]);
1404        assert_eq!(flipped.layout().strides(), &[-4, 1]);
1405        assert!(!flipped.is_contiguous());
1406
1407        let contig = flipped.to_contiguous();
1408        assert!(contig.is_contiguous());
1409
1410        let values: Vec<f32> = contig.into_data().try_into_vec().unwrap();
1411        let expected = vec![
1412            8.0, 9.0, 10.0, 11.0, // row 2
1413            4.0, 5.0, 6.0, 7.0, // row 1
1414            0.0, 1.0, 2.0, 3.0, // row 0
1415        ];
1416        assert_eq!(values, expected);
1417    }
1418}