Skip to main content

burn_flex/
tensor.rs

1#[cfg(target_has_atomic = "ptr")]
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4use core::fmt;
5#[cfg(not(target_has_atomic = "ptr"))]
6use portable_atomic_util::Arc;
7
8use burn_backend::{DType, Element, TensorData, TensorMetadata};
9use burn_std::{Bytes, Shape, bf16, f16};
10
11use crate::{FlexDevice, layout::Layout};
12
13/// CPU tensor primitive for the Flex backend.
14///
15/// Uses type-erased byte storage with runtime dtype and Arc-based sharing.
16/// Clone is O(1) (refcount increment). Copy-on-write for mutations.
17#[derive(Clone)]
18pub struct FlexTensor {
19    /// Shared byte storage. Clone increments refcount.
20    data: Arc<Bytes>,
21    /// Layout describing shape, strides, and offset.
22    layout: Layout,
23    /// Runtime data type.
24    dtype: DType,
25}
26
27impl fmt::Debug for FlexTensor {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        f.debug_struct("FlexTensor")
30            .field("shape", self.layout.shape())
31            .field("dtype", &self.dtype)
32            .field("contiguous", &self.layout.is_contiguous())
33            .field("unique", &self.is_unique())
34            .finish()
35    }
36}
37
38impl FlexTensor {
39    /// Create a new tensor from bytes, layout, and dtype.
40    pub fn new(data: Bytes, layout: Layout, dtype: DType) -> Self {
41        Self {
42            data: Arc::new(data),
43            layout,
44            dtype,
45        }
46    }
47
48    /// Create a tensor from TensorData.
49    pub fn from_data(data: TensorData) -> Self {
50        let shape = data.shape.clone();
51        let layout = Layout::contiguous(shape);
52        let dtype = data.dtype;
53        Self {
54            data: Arc::new(data.bytes),
55            layout,
56            dtype,
57        }
58    }
59
60    /// Convert tensor to TensorData.
61    ///
62    /// If non-contiguous or shared, this will copy data.
63    pub fn into_data(self) -> TensorData {
64        if self.layout.is_contiguous() && self.layout.start_offset() == 0 {
65            let expected_bytes = self.layout.num_elements() * dtype_size(self.dtype);
66            assert!(
67                expected_bytes <= self.data.len(),
68                "into_data: buffer ({} bytes) too small for {} elements of {:?}",
69                self.data.len(),
70                self.layout.num_elements(),
71                self.dtype
72            );
73            if self.data.len() == expected_bytes {
74                // Buffer exactly matches logical size; try zero-copy unwrap
75                match Arc::try_unwrap(self.data) {
76                    Ok(bytes) => TensorData {
77                        bytes,
78                        shape: self.layout.shape().clone(),
79                        dtype: self.dtype,
80                    },
81                    Err(arc) => {
82                        let bytes = Bytes::from_bytes_vec((*arc)[..expected_bytes].to_vec());
83                        TensorData {
84                            bytes,
85                            shape: self.layout.shape().clone(),
86                            dtype: self.dtype,
87                        }
88                    }
89                }
90            } else {
91                // Contiguous at offset 0 but buffer is oversized (e.g., narrowed view).
92                // Truncate to exact logical size.
93                let bytes = Bytes::from_bytes_vec(self.data[..expected_bytes].to_vec());
94                TensorData {
95                    bytes,
96                    shape: self.layout.shape().clone(),
97                    dtype: self.dtype,
98                }
99            }
100        } else {
101            // Non-contiguous or non-zero offset: copy to contiguous layout
102            self.to_contiguous().into_data()
103        }
104    }
105
106    /// Check if this tensor has exclusive ownership of its data.
107    ///
108    /// When true, in-place mutations are safe without copying.
109    #[inline]
110    pub fn is_unique(&self) -> bool {
111        Arc::strong_count(&self.data) == 1
112    }
113
114    /// Get the layout.
115    pub fn layout(&self) -> &Layout {
116        &self.layout
117    }
118
119    /// Create a new tensor with a different layout but sharing the same data.
120    ///
121    /// This is a zero-copy operation used for operations like flip, transpose, etc.
122    pub fn with_layout(self, layout: Layout) -> Self {
123        Self {
124            data: self.data,
125            layout,
126            dtype: self.dtype,
127        }
128    }
129
130    /// Get the dtype.
131    pub fn dtype(&self) -> DType {
132        self.dtype
133    }
134
135    /// Check if tensor is contiguous.
136    pub fn is_contiguous(&self) -> bool {
137        self.layout.is_contiguous()
138    }
139
140    /// Get the raw bytes (read-only).
141    pub fn bytes(&self) -> &[u8] {
142        &self.data
143    }
144
145    /// Get a clone of the Arc for sharing data with a new layout.
146    ///
147    /// Use this for zero-copy view operations (reshape, transpose, slice).
148    pub fn data_arc(&self) -> Arc<Bytes> {
149        Arc::clone(&self.data)
150    }
151
152    /// Create a tensor from shared data, layout, and dtype.
153    ///
154    /// Use this for zero-copy view operations.
155    pub fn from_arc(data: Arc<Bytes>, layout: Layout, dtype: DType) -> Self {
156        Self {
157            data,
158            layout,
159            dtype,
160        }
161    }
162
163    /// Zero-copy typed view of the full storage buffer.
164    ///
165    /// Use with `StridedIter` for non-contiguous access, or with
166    /// `layout().contiguous_offsets()` for the contiguous fast path.
167    ///
168    /// # Panics
169    /// Panics if `E::dtype()` doesn't match the tensor's dtype.
170    /// Note: Bool tensors are stored as u8, so both Bool(Native) and Bool(U8)
171    /// dtypes accept u8 access.
172    pub fn storage<E: Element + bytemuck::Pod>(&self) -> &[E] {
173        assert!(
174            E::dtype() == self.dtype
175                || (matches!(
176                    self.dtype,
177                    DType::Bool(burn_std::BoolStore::Native | burn_std::BoolStore::U8)
178                ) && E::dtype() == DType::U8),
179            "storage: dtype mismatch (expected {:?}, got {:?})",
180            self.dtype,
181            E::dtype()
182        );
183        bytemuck::cast_slice(&self.data)
184    }
185
186    /// Mutable typed view with copy-on-write semantics.
187    ///
188    /// If the tensor is shared (refcount > 1), this will copy the data first.
189    /// For in-place operations, prefer `try_storage_mut()` which returns None
190    /// if shared, allowing you to choose an alternative strategy.
191    ///
192    /// # Panics
193    /// Panics if `E::dtype()` doesn't match the tensor's dtype.
194    /// Note: Bool tensors are stored as u8, so both Bool(Native) and Bool(U8)
195    /// dtypes accept u8 access.
196    pub fn storage_mut<E: Element + bytemuck::Pod>(&mut self) -> &mut [E] {
197        assert!(
198            E::dtype() == self.dtype
199                || (matches!(
200                    self.dtype,
201                    DType::Bool(burn_std::BoolStore::Native | burn_std::BoolStore::U8)
202                ) && E::dtype() == DType::U8),
203            "storage_mut: dtype mismatch (expected {:?}, got {:?})",
204            self.dtype,
205            E::dtype()
206        );
207        // COW: clone data if shared
208        let bytes = Arc::make_mut(&mut self.data);
209        bytemuck::cast_slice_mut(bytes)
210    }
211
212    /// Try to get mutable storage without copying.
213    ///
214    /// Returns `Some` if tensor is uniquely owned, `None` if shared.
215    /// Use this when you want to avoid the implicit copy in `storage_mut()`.
216    /// Note: Bool tensors are stored as u8, so both Bool(Native) and Bool(U8)
217    /// dtypes accept u8 access.
218    pub fn try_storage_mut<E: Element + bytemuck::Pod>(&mut self) -> Option<&mut [E]> {
219        assert!(
220            E::dtype() == self.dtype
221                || (matches!(
222                    self.dtype,
223                    DType::Bool(burn_std::BoolStore::Native | burn_std::BoolStore::U8)
224                ) && E::dtype() == DType::U8),
225            "try_storage_mut: dtype mismatch (expected {:?}, got {:?})",
226            self.dtype,
227            E::dtype()
228        );
229        if self.is_unique() {
230            // Safe: we're the only owner
231            let bytes = Arc::get_mut(&mut self.data)?;
232            Some(bytemuck::cast_slice_mut(bytes))
233        } else {
234            None
235        }
236    }
237
238    /// Get typed slice view (zero-cost if contiguous and offset is 0).
239    ///
240    /// Returns None if dtype doesn't match E or tensor is non-contiguous.
241    pub fn as_slice<E: Element + bytemuck::Pod>(&self) -> Option<&[E]> {
242        if E::dtype() != self.dtype {
243            return None;
244        }
245        let storage: &[E] = self.storage();
246        self.layout
247            .contiguous_offsets()
248            .map(|(start, end)| &storage[start..end])
249    }
250
251    /// Create an empty tensor with given shape and dtype.
252    pub fn empty(shape: Shape, dtype: DType) -> Self {
253        let num_elements = shape.num_elements();
254        let elem_size = dtype_size(dtype);
255        let bytes = Bytes::from_bytes_vec(alloc::vec![0u8; num_elements * elem_size]);
256        let layout = Layout::contiguous(shape);
257        Self {
258            data: Arc::new(bytes),
259            layout,
260            dtype,
261        }
262    }
263
264    /// Create a tensor filled with zeros.
265    pub fn zeros(shape: Shape, dtype: DType) -> Self {
266        Self::empty(shape, dtype)
267    }
268
269    /// Create a tensor filled with `n` copies of a typed value.
270    pub fn filled_typed<E: bytemuck::Pod + Send + Sync>(
271        shape: Shape,
272        dtype: DType,
273        value: E,
274    ) -> Self {
275        assert_eq!(
276            dtype_size(dtype),
277            core::mem::size_of::<E>(),
278            "filled_typed: dtype size mismatch"
279        );
280        let n = shape.num_elements();
281        let data = alloc::vec![value; n];
282        let bytes = Bytes::from_elems(data);
283        Self {
284            data: Arc::new(bytes),
285            layout: Layout::contiguous(shape),
286            dtype,
287        }
288    }
289
290    /// Copy to contiguous layout if needed.
291    pub fn to_contiguous(&self) -> Self {
292        // Fast path requires the logical tensor to cover the whole buffer.
293        // A contiguous prefix view (e.g. [8, 5] sliced to [5, 5]) has
294        // canonical strides and offset 0 but an oversized buffer, and would
295        // otherwise mislead callers that read `storage()` / `bytes()` by
296        // length (e.g. the SIMD `mask_fill_*` kernels).
297        if self.is_contiguous()
298            && self.layout.start_offset() == 0
299            && self.data.len() == self.layout.num_elements() * dtype_size(self.dtype)
300        {
301            return self.clone();
302        }
303
304        // Copy data to new contiguous buffer
305        match self.dtype {
306            DType::F64 => self.copy_contiguous::<f64>(),
307            DType::F32 => self.copy_contiguous::<f32>(),
308            DType::F16 => self.copy_contiguous::<f16>(),
309            DType::BF16 => self.copy_contiguous::<bf16>(),
310            DType::I64 => self.copy_contiguous::<i64>(),
311            DType::I32 => self.copy_contiguous::<i32>(),
312            DType::I16 => self.copy_contiguous::<i16>(),
313            DType::I8 => self.copy_contiguous::<i8>(),
314            DType::U64 => self.copy_contiguous::<u64>(),
315            DType::U32 => self.copy_contiguous::<u32>(),
316            DType::U16 => self.copy_contiguous::<u16>(),
317            DType::U8 => self.copy_contiguous::<u8>(),
318            DType::Bool(burn_std::BoolStore::Native | burn_std::BoolStore::U8) => {
319                self.copy_contiguous::<u8>()
320            }
321            DType::Bool(burn_std::BoolStore::U32) => {
322                panic!("burn-flex: Bool(U32) storage is not yet supported")
323            }
324            _ => panic!("Unsupported dtype for contiguous copy: {:?}", self.dtype),
325        }
326    }
327
328    fn copy_contiguous<E: Element + bytemuck::Pod>(&self) -> Self {
329        let src: &[E] = bytemuck::cast_slice(&self.data);
330        let n = self.layout.num_elements();
331        let mut dst = Vec::with_capacity(n);
332
333        // Squeeze size-1 dims and merge adjacent stride-contiguous
334        // runs so e.g. a permuted `[N, H, W, C]` ConvNeXt layer-norm
335        // input becomes a plain 2D `[H*W, C]` transpose that the
336        // tiled copy below handles at near-memcpy speed. Without the
337        // collapse, the 4D ND fallback scalar-walks the tensor.
338        let collapsed = collapse_for_copy(self.layout.shape(), self.layout.strides());
339        let (shape, strides) = collapsed.as_slices();
340        let offset = self.layout.start_offset() as isize;
341        let all_positive = strides.iter().all(|&s| s >= 0);
342
343        if shape.len() <= 1 && all_positive {
344            // 0-D scalar or 1-D run with a uniform stride. Empty
345            // collapsed shape means rank 0 (numel 1); otherwise
346            // numel is the single dim's size (which may be 0 for
347            // zero-sized 1D tensors, so don't clamp via `.max(1)`).
348            let collapsed_numel = if shape.is_empty() { 1 } else { shape[0] };
349            debug_assert_eq!(n, collapsed_numel);
350            // SAFETY: capacity is n; we fill every position below.
351            unsafe { dst.set_len(n) };
352            if shape.is_empty() {
353                if n > 0 {
354                    dst[0] = src[offset as usize];
355                }
356            } else {
357                let len = shape[0];
358                let stride = strides[0];
359                if stride == 1 {
360                    dst[..len].copy_from_slice(&src[offset as usize..offset as usize + len]);
361                } else {
362                    for (i, slot) in dst.iter_mut().take(len).enumerate() {
363                        let idx = (offset + i as isize * stride) as usize;
364                        *slot = src[idx];
365                    }
366                }
367            }
368        } else if shape.len() == 2 && all_positive {
369            // 2D positive-stride (transpose-like): tile both dims so
370            // reads stay in cache. The loop-nesting chooser inside
371            // `copy_2d_tiled` picks whichever ordering puts the
372            // smaller source stride on the innermost loop.
373            debug_assert_eq!(shape[0] * shape[1], n, "2D strides must cover all elements");
374            // SAFETY: capacity is n; `copy_2d_tiled` writes every
375            // `(row, col)` position exactly once.
376            unsafe { dst.set_len(n) };
377            copy_2d_tiled(
378                &mut dst, src, offset, shape[0], shape[1], strides[0], strides[1],
379            );
380        } else if all_positive && strides.last().copied() == Some(1) {
381            // Since the inner stride is 1, we can bulk-copy
382            // the contiguous inner run for each outer index.
383            unsafe { dst.set_len(n) };
384            copy_inner_contiguous_run(&mut dst, src, offset, shape, strides);
385        } else {
386            // General fallback: covers negative strides (flipped
387            // tensors) and ND layouts that can't collapse to ≤2D.
388            for idx in crate::strided_index::StridedIter::new(&self.layout) {
389                dst.push(src[idx]);
390            }
391        }
392
393        let bytes = Bytes::from_elems(dst);
394        let layout = Layout::contiguous(self.layout.shape().clone());
395        Self {
396            data: Arc::new(bytes),
397            layout,
398            dtype: self.dtype,
399        }
400    }
401
402    /// Reshape tensor. Zero-copy if contiguous.
403    pub fn reshape(&self, new_shape: Shape) -> Self {
404        assert_eq!(
405            self.layout.num_elements(),
406            new_shape.num_elements(),
407            "reshape must preserve total elements"
408        );
409
410        if let Some(new_layout) = self.layout.reshape(new_shape.clone()) {
411            Self {
412                data: Arc::clone(&self.data),
413                layout: new_layout,
414                dtype: self.dtype,
415            }
416        } else {
417            // Non-contiguous: copy first
418            self.to_contiguous().reshape(new_shape)
419        }
420    }
421
422    /// Transpose two dimensions. Zero-copy (metadata only).
423    pub fn transpose(&self, dim1: usize, dim2: usize) -> Self {
424        Self {
425            data: Arc::clone(&self.data),
426            layout: self.layout.transpose(dim1, dim2),
427            dtype: self.dtype,
428        }
429    }
430
431    /// Narrow/slice along a dimension. Zero-copy (metadata only).
432    pub fn narrow(&self, dim: usize, start: usize, len: usize) -> Self {
433        Self {
434            data: Arc::clone(&self.data),
435            layout: self.layout.narrow(dim, start, len),
436            dtype: self.dtype,
437        }
438    }
439
440    /// Permute dimensions according to axes. Zero-copy (metadata only).
441    pub fn permute(&self, axes: &[usize]) -> Self {
442        Self {
443            data: Arc::clone(&self.data),
444            layout: self.layout.permute(axes),
445            dtype: self.dtype,
446        }
447    }
448}
449
450impl TensorMetadata for FlexTensor {
451    type Device = FlexDevice;
452
453    fn dtype(&self) -> DType {
454        self.dtype
455    }
456
457    fn shape(&self) -> Shape {
458        self.layout.shape().clone()
459    }
460
461    fn rank(&self) -> usize {
462        self.layout.num_dims()
463    }
464
465    fn device(&self) -> Self::Device {
466        FlexDevice
467    }
468
469    fn can_mut(&self) -> bool {
470        self.is_unique()
471    }
472}
473
474/// Max rank we're willing to handle without falling back to the
475/// strided iterator. Burn tensors are capped at 8 dims in practice.
476const COLLAPSE_MAX_RANK: usize = 8;
477
478/// Collapsed layout result of [`collapse_for_copy`], stored in stack
479/// arrays so `to_contiguous()` doesn't have to hit the allocator on
480/// its hot path.
481#[derive(Debug, Clone, Copy)]
482struct CollapsedLayout {
483    ndim: usize,
484    shape: [usize; COLLAPSE_MAX_RANK],
485    strides: [isize; COLLAPSE_MAX_RANK],
486}
487
488impl CollapsedLayout {
489    #[inline]
490    fn as_slices(&self) -> (&[usize], &[isize]) {
491        (&self.shape[..self.ndim], &self.strides[..self.ndim])
492    }
493}
494
495/// Collapse a shape/stride pair into the minimum-rank equivalent
496/// layout for a contiguous copy:
497///
498/// 1. Squeeze size-1 dims (their stride never gets stepped past 0).
499/// 2. Merge adjacent dims `(i, i+1)` when
500///    `stride[i] == stride[i+1] * shape[i+1]`, which means the two
501///    dims form a single logical run through memory.
502///
503/// Canonical example: a 4D ConvNeXt input `[1, 244, 224, 48]` with
504/// strides `[2_623_488, 224, 1, 54656]` (from
505/// `[N, C, H, W].permute([0, 2, 3, 1])`) collapses to 2D
506/// `[54656, 48]` with strides `[1, 54656]`.
507///
508/// If the input rank exceeds [`COLLAPSE_MAX_RANK`] the result is
509/// left at rank > 2 so the caller falls through to its generic
510/// strided path. If the input is rank > `COLLAPSE_MAX_RANK`, we
511/// return the original (un-collapsed) layout truncated, which the
512/// caller will reject via its `shape.len() == 2` gate.
513///
514/// PRECONDITION: the caller must gate on all-positive strides before
515/// using the collapsed layout. The merge rule assumes positive
516/// strides and will produce iteration-order-incorrect results for
517/// flipped tensors.
518fn collapse_for_copy(shape: &[usize], strides: &[isize]) -> CollapsedLayout {
519    let mut out = CollapsedLayout {
520        ndim: 0,
521        shape: [0; COLLAPSE_MAX_RANK],
522        strides: [0; COLLAPSE_MAX_RANK],
523    };
524
525    // Bail out to the caller's fallback if the rank is too large to
526    // fit our stack buffer. In practice this never triggers (burn
527    // tensors are ≤8 dims), but leaving the `ndim` high signals the
528    // caller to take the generic strided path.
529    if shape.len() > COLLAPSE_MAX_RANK {
530        out.ndim = shape.len().min(COLLAPSE_MAX_RANK);
531        return out;
532    }
533
534    // Single forward sweep: squeeze size-1 dims and merge whenever
535    // the current dim's `stride * size` equals the previous output
536    // dim's stride (i.e. the two form a contiguous run).
537    //
538    // Use `checked_mul` so a pathological layout whose stride math
539    // would overflow `isize` simply fails to merge rather than
540    // wrapping into an incorrect merge decision. Real tensors can't
541    // hit this (total numel is bounded by `isize::MAX`), but
542    // hand-built layouts passed through the test paths could.
543    for (&s, &st) in shape.iter().zip(strides.iter()) {
544        if s == 1 {
545            continue;
546        }
547        let merge = out.ndim > 0
548            && (s as isize)
549                .checked_mul(st)
550                .is_some_and(|run| out.strides[out.ndim - 1] == run);
551        if merge {
552            out.shape[out.ndim - 1] *= s;
553            out.strides[out.ndim - 1] = st;
554        } else {
555            out.shape[out.ndim] = s;
556            out.strides[out.ndim] = st;
557            out.ndim += 1;
558        }
559    }
560
561    out
562}
563
564/// Copy a strided source into a contiguous destination by treating the
565/// layout as a set of strided outer dims wrapping a contiguous inner run.
566///
567/// Precondition: strides are all positive and the innermost stride is 1
568#[inline]
569fn copy_inner_contiguous_run<E: Copy>(
570    dst: &mut [E],
571    src: &[E],
572    offset: isize,
573    shape: &[usize],
574    strides: &[isize],
575) {
576    debug_assert!(!shape.is_empty());
577    debug_assert_eq!(*strides.last().unwrap(), 1, "innermost stride must be 1");
578
579    // We want the maximal trailing run that is contiguous in src.
580    // Walk inward-out, a dim dim_idx extends the contiguous run if AND ONLY IF ("iff")
581    // strides[dim_idx] == product of inner shapes seen so far.
582    let mut expected_stride: isize = 1;
583    let mut split = shape.len(); // first OUTER index (dims [split..] are inner)
584    while split > 0 {
585        let dim_idx = split - 1;
586        if strides[dim_idx] == expected_stride {
587            expected_stride = expected_stride
588                .checked_mul(shape[dim_idx] as isize)
589                .expect("contiguous run length overflows isize");
590            split -= 1;
591        } else {
592            break;
593        }
594    }
595
596    let outer_shape = &shape[..split];
597    let outer_strides = &strides[..split];
598    let inner_run_length: usize = shape[split..].iter().product();
599
600    // Whole tensor is one contiguous run! We can bulk-copy the whole thing.
601    if outer_shape.is_empty() {
602        let src_index = offset as usize;
603        dst[..inner_run_length].copy_from_slice(&src[src_index..src_index + inner_run_length]);
604        return;
605    }
606
607    if inner_run_length == 0 || outer_shape.contains(&0) {
608        return;
609    }
610
611    // Walk the outer index space in row-major order via an odometer:
612    // each iteration ticks the rightmost counter, and so on overflow we
613    // reset it and carry +1 into the dim to its left.
614    // Row-major is what we want because dst is contiguous (consecutive outer-index
615    // tuples land in consecutive inner_run_length slots, so we just bump
616    // dst_position each step instead of recomputing it)
617    // We use this instead of N nested for loops because the rank isn't
618    // known at compile time.
619    let mut counter = [0usize; COLLAPSE_MAX_RANK];
620    let outer_dim_count = outer_shape.len();
621    let mut dst_position = 0usize;
622
623    loop {
624        let mut src_start = offset;
625        for dim_idx in 0..outer_dim_count {
626            src_start += counter[dim_idx] as isize * outer_strides[dim_idx];
627        }
628        let src_index = src_start as usize;
629        dst[dst_position..dst_position + inner_run_length]
630            .copy_from_slice(&src[src_index..src_index + inner_run_length]);
631        dst_position += inner_run_length;
632
633        let mut dim_idx = outer_dim_count;
634        loop {
635            if dim_idx == 0 {
636                return;
637            }
638            dim_idx -= 1;
639            counter[dim_idx] += 1;
640            if counter[dim_idx] < outer_shape[dim_idx] {
641                break;
642            }
643            counter[dim_idx] = 0;
644        }
645    }
646}
647
648/// Tiled 2D copy from a strided source into a contiguous destination.
649/// The loop nesting is chosen so the innermost read walks whichever
650/// source stride is smaller, which keeps the hot loop in cache even
651/// for transpose-like layouts.
652#[inline]
653fn copy_2d_tiled<E: Copy>(
654    dst: &mut [E],
655    src: &[E],
656    offset: isize,
657    rows: usize,
658    cols: usize,
659    row_stride: isize,
660    col_stride: isize,
661) {
662    const TILE: usize = 16;
663
664    if row_stride <= col_stride {
665        // row-inside-col: the inner loop walks `row_stride` (smaller).
666        for col_tile in (0..cols).step_by(TILE) {
667            let col_end = (col_tile + TILE).min(cols);
668            for row_tile in (0..rows).step_by(TILE) {
669                let row_end = (row_tile + TILE).min(rows);
670                for col in col_tile..col_end {
671                    let col_base = offset + col as isize * col_stride;
672                    for row in row_tile..row_end {
673                        let idx = (col_base + row as isize * row_stride) as usize;
674                        // SAFETY: caller set `dst.len() == rows * cols`
675                        // and each `(row, col)` is visited once.
676                        unsafe {
677                            *dst.get_unchecked_mut(row * cols + col) = src[idx];
678                        }
679                    }
680                }
681            }
682        }
683    } else {
684        // col-inside-row: the inner loop walks `col_stride` (smaller).
685        for row_tile in (0..rows).step_by(TILE) {
686            let row_end = (row_tile + TILE).min(rows);
687            for col_tile in (0..cols).step_by(TILE) {
688                let col_end = (col_tile + TILE).min(cols);
689                for row in row_tile..row_end {
690                    let row_base =
691                        offset + row as isize * row_stride + col_tile as isize * col_stride;
692                    let dst_base = row * cols + col_tile;
693                    for c in 0..(col_end - col_tile) {
694                        let idx = (row_base + c as isize * col_stride) as usize;
695                        // SAFETY: same as above.
696                        unsafe {
697                            *dst.get_unchecked_mut(dst_base + c) = src[idx];
698                        }
699                    }
700                }
701            }
702        }
703    }
704}
705
706/// Get the size in bytes for a dtype element.
707///
708/// Matches `burn_std::DType::size()` semantics: Bool(Native) and Bool(U8) are
709/// 1 byte, Bool(U32) is 4 bytes. This makes buffer-size validation correct
710/// regardless of which BoolStore variant the dtype carries.
711///
712/// # Panics
713///
714/// Panics if the dtype has a zero-byte element size. `burn_std::DType::size()`
715/// returns 0 for sub-byte quantized dtypes (Q4F, Q4S, Q2F, Q2S, and most
716/// `QuantStore::PackedNative` variants). burn-flex does not yet support these
717/// packed quantization formats; passing them here would silently produce
718/// empty allocations in `FlexTensor::empty`, truncated buffers in `into_data`,
719/// and zero-byte memcpys in `repeat_dim`. The panic turns all three into a
720/// loud, actionable failure at the dispatch boundary.
721pub(crate) fn dtype_size(dtype: DType) -> usize {
722    // Delegate to burn-std's canonical size to stay in sync.
723    let size = dtype.size();
724    assert!(
725        size > 0,
726        "burn-flex: dtype {:?} has zero-byte element size (sub-byte packed \
727         quantization is not yet supported)",
728        dtype
729    );
730    size
731}
732
733#[cfg(test)]
734mod tests {
735    use super::*;
736    use alloc::vec;
737
738    #[test]
739    fn test_from_data_roundtrip() {
740        let data = TensorData::from([1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
741        let tensor = FlexTensor::from_data(data.clone());
742        let result = tensor.into_data();
743        assert_eq!(data.shape, result.shape);
744        assert_eq!(data.dtype, result.dtype);
745    }
746
747    #[test]
748    fn test_collapse_for_copy_squeezes_size1_and_merges_contig() {
749        // Permuted ConvNeXt input: [1, 48, 244, 224].permute([0,2,3,1]).
750        let shape = vec![1, 244, 224, 48];
751        let strides = vec![2_623_488_isize, 224, 1, 54656];
752        let collapsed = collapse_for_copy(&shape, &strides);
753        let (s, st) = collapsed.as_slices();
754        assert_eq!(s, &[54656, 48]);
755        assert_eq!(st, &[1, 54656]);
756    }
757
758    #[test]
759    fn test_collapse_for_copy_already_contiguous_3d() {
760        let collapsed = collapse_for_copy(&[2, 3, 4], &[12, 4, 1]);
761        let (s, st) = collapsed.as_slices();
762        assert_eq!(s, &[24]);
763        assert_eq!(st, &[1]);
764    }
765
766    #[test]
767    fn test_collapse_for_copy_transpose_2d() {
768        let collapsed = collapse_for_copy(&[5, 3], &[1, 5]);
769        let (s, st) = collapsed.as_slices();
770        assert_eq!(s, &[5, 3]);
771        assert_eq!(st, &[1, 5]);
772    }
773
774    #[test]
775    fn test_collapse_for_copy_all_size1() {
776        let collapsed = collapse_for_copy(&[1, 1, 1], &[0, 0, 0]);
777        let (s, st) = collapsed.as_slices();
778        assert!(s.is_empty());
779        assert!(st.is_empty());
780    }
781
782    /// Regression: an empty 1D view produced by `narrow` at a
783    /// non-zero offset forces `copy_contiguous` to run (it can't
784    /// early-return via the contiguous-at-offset-0 shortcut). The
785    /// old `debug_assert_eq!(n, shape.product().max(1))` tripped
786    /// for this shape because `.max(1)` produced 1 while the true
787    /// numel is 0.
788    #[test]
789    fn test_to_contiguous_zero_sized_narrowed() {
790        let t = FlexTensor::from_data(TensorData::new(
791            (0..6).map(|i| i as f32).collect::<Vec<_>>(),
792            vec![6],
793        ));
794        // narrow(dim, start=3, len=0): shape [0], start_offset 3.
795        let empty_view = t.narrow(0, 3, 0);
796        assert_eq!(empty_view.shape().to_vec(), vec![0]);
797        assert_ne!(empty_view.layout().start_offset(), 0);
798
799        let contig = empty_view.to_contiguous();
800        assert_eq!(contig.shape().to_vec(), vec![0]);
801        assert_eq!(contig.layout().start_offset(), 0);
802        assert_eq!(contig.into_data().bytes.len(), 0);
803    }
804
805    /// Regression for #4855: a prefix view (e.g. `narrow(dim, 0, n)`) has
806    /// canonical contiguous strides and start_offset 0, but its underlying
807    /// buffer is still the larger original. `to_contiguous` must materialize
808    /// a right-sized copy so callers keying off `storage().len()` (like the
809    /// SIMD `mask_fill_*` kernels reached from `triu`/`tril` in LU on tall
810    /// matrices) don't walk past the logical shape.
811    #[test]
812    fn test_to_contiguous_prefix_view_shrinks_buffer() {
813        let data: Vec<f32> = (0..40).map(|i| i as f32).collect();
814        let t = FlexTensor::from_data(TensorData::new(data, vec![8, 5]));
815
816        let prefix = t.narrow(0, 0, 5);
817        assert_eq!(prefix.shape().to_vec(), vec![5, 5]);
818        assert_eq!(prefix.layout().strides(), &[5, 1]);
819        assert_eq!(prefix.layout().start_offset(), 0);
820        assert!(prefix.is_contiguous());
821        assert_eq!(prefix.storage::<f32>().len(), 40);
822
823        let contig = prefix.to_contiguous();
824        assert_eq!(contig.storage::<f32>().len(), 25);
825        assert_eq!(contig.layout().num_elements(), 25);
826        assert_eq!(
827            contig.storage::<f32>(),
828            &(0..5)
829                .flat_map(|r| (0..5).map(move |c| (r * 5 + c) as f32))
830                .collect::<Vec<_>>()[..]
831        );
832    }
833
834    /// 4D permuted layout round-trips through the collapse + tiled
835    /// copy path. Mirrors the ConvNeXt channels-last permute.
836    #[test]
837    fn test_to_contiguous_4d_permuted_matches_naive() {
838        let dims = [1, 48, 4, 5];
839        let n: usize = dims.iter().product();
840        let data: Vec<f32> = (0..n).map(|i| i as f32).collect();
841        let t = FlexTensor::from_data(TensorData::new(data.clone(), dims.to_vec()));
842        let permuted = t.permute(&[0, 2, 3, 1]);
843        assert!(!permuted.is_contiguous());
844
845        let contig = permuted.to_contiguous();
846        assert!(contig.is_contiguous());
847        assert_eq!(contig.shape().to_vec(), vec![1, 4, 5, 48]);
848
849        // Expected via manual strided walk of the source.
850        let mut expected = Vec::with_capacity(n);
851        for h in 0..4 {
852            for w in 0..5 {
853                for c in 0..48 {
854                    let idx = c * 20 + h * 5 + w;
855                    expected.push(data[idx]);
856                }
857            }
858        }
859
860        let result_data = contig.into_data();
861        let values = result_data.as_slice::<f32>().unwrap();
862        assert_eq!(values, expected.as_slice());
863    }
864
865    /// Regression: the ShuffleNet channel shuffle (reshape -> permute([0,2,1,3,4]) -> reshape)
866    /// collapses to a 3D layout with stride-1 innermost.
867    /// Before this fix that went into the StridedIter scalar walk in the final else in copy_contiguous
868    /// Now, we use a new inner-contiguous branch that bulk-copies the trailing run
869    #[test]
870    fn test_to_contiguous_3d_inner_stride1_matches_naive() {
871        let dims = [1, 2, 4, 3, 3]; // [N, G, C, H, W]
872        let n: usize = dims.iter().product();
873        let data: Vec<f32> = (0..n).map(|i| i as f32).collect();
874        let t = FlexTensor::from_data(TensorData::new(data.clone(), dims.to_vec()));
875        let permuted = t.permute(&[0, 2, 1, 3, 4]); // swap G and C from the original dims in the permute
876        assert!(!permuted.is_contiguous());
877
878        let contiguous_data = permuted.to_contiguous();
879        assert!(contiguous_data.is_contiguous());
880        assert_eq!(contiguous_data.shape().to_vec(), vec![1, 4, 2, 3, 3]);
881
882        // Now we manually do a strided walk to be able to check that against a naive solution.
883        // output[0, c, g, h, w] = input[0, g, c, h, w].
884        let mut expected = Vec::with_capacity(n);
885        for c in 0..4 {
886            for g in 0..2 {
887                for h in 0..3 {
888                    for w in 0..3 {
889                        let idx = g * 36 + c * 9 + h * 3 + w;
890                        expected.push(data[idx]);
891                    }
892                }
893            }
894        }
895
896        // Verify the naive solution matches (the manual strided walk).
897        let result_data = contiguous_data.into_data();
898        let values = result_data.as_slice::<f32>().unwrap();
899        assert_eq!(values, expected.as_slice());
900    }
901
902    /// Exercise the `row_stride > col_stride` branch of the 2D tiled
903    /// copy (the ConvNeXt case hits the other branch).
904    #[test]
905    fn test_to_contiguous_2d_row_stride_gt_col_stride() {
906        // `slice(s![..;2, ..])` on a [6, 3] contiguous tensor gives a
907        // [3, 3] view with strides [6, 1] that doesn't collapse, so
908        // the 2D branch runs with row_stride > col_stride.
909        let data: Vec<f32> = (0..18).map(|i| i as f32).collect();
910        let t = FlexTensor::from_data(TensorData::new(data, vec![6, 3]));
911        let stepped = crate::ops::slice::slice(
912            t,
913            &[
914                burn_std::Slice::new(0, Some(6), 2),
915                burn_std::Slice::new(0, None, 1),
916            ],
917        );
918        // Verify the layout matches what the branch requires.
919        assert_eq!(stepped.layout().shape().to_vec(), vec![3, 3]);
920        assert_eq!(stepped.layout().strides(), &[6, 1]);
921        assert!(!stepped.layout().is_contiguous());
922
923        let contig = stepped.to_contiguous();
924        assert!(contig.is_contiguous());
925        assert_eq!(contig.shape().to_vec(), vec![3, 3]);
926
927        let result_data = contig.into_data();
928        let values = result_data.as_slice::<f32>().unwrap();
929        // Expected: rows 0, 2, 4 of the original 6x3 tensor.
930        let expected = vec![
931            0.0f32, 1.0, 2.0, // row 0
932            6.0, 7.0, 8.0, // row 2
933            12.0, 13.0, 14.0, // row 4
934        ];
935        assert_eq!(values, expected.as_slice());
936    }
937
938    #[test]
939    fn test_reshape() {
940        let data = TensorData::new(vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], vec![2, 3]);
941        let tensor = FlexTensor::from_data(data);
942        let reshaped = tensor.reshape(Shape::from(vec![3, 2]));
943        assert_eq!(reshaped.shape().to_vec(), vec![3, 2]);
944    }
945
946    #[test]
947    fn test_transpose() {
948        let data = TensorData::new(vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], vec![2, 3]);
949        let tensor = FlexTensor::from_data(data);
950        let transposed = tensor.transpose(0, 1);
951        assert_eq!(transposed.shape().to_vec(), vec![3, 2]);
952        assert!(!transposed.is_contiguous());
953    }
954
955    #[test]
956    fn test_clone_is_cheap() {
957        let data = TensorData::from([1.0f32, 2.0, 3.0, 4.0]);
958        let tensor = FlexTensor::from_data(data);
959
960        // Before clone, tensor is unique
961        assert!(tensor.is_unique());
962
963        // Clone shares data
964        let cloned = tensor.clone();
965        assert!(!tensor.is_unique());
966        assert!(!cloned.is_unique());
967
968        // Both point to same data
969        assert!(core::ptr::eq(
970            tensor.bytes().as_ptr(),
971            cloned.bytes().as_ptr(),
972        ));
973    }
974
975    #[test]
976    fn test_cow_on_mutation() {
977        let data = TensorData::from([1.0f32, 2.0, 3.0, 4.0]);
978        let tensor = FlexTensor::from_data(data);
979        let mut cloned = tensor.clone();
980
981        // Both share data
982        assert!(!tensor.is_unique());
983        assert!(!cloned.is_unique());
984
985        // Mutate cloned - triggers COW
986        let storage: &mut [f32] = cloned.storage_mut();
987        storage[0] = 99.0;
988
989        // Now cloned has its own copy, tensor is unique again
990        assert!(tensor.is_unique());
991        assert!(cloned.is_unique());
992
993        // Data is different
994        assert_ne!(tensor.bytes().as_ptr(), cloned.bytes().as_ptr());
995        assert_eq!(tensor.storage::<f32>()[0], 1.0);
996        assert_eq!(cloned.storage::<f32>()[0], 99.0);
997    }
998
999    #[test]
1000    fn test_into_data_narrowed_at_offset_zero() {
1001        // [1, 2, 3, 4, 5, 6] shape [2, 3]
1002        let data = TensorData::new(vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], vec![2, 3]);
1003        let tensor = FlexTensor::from_data(data);
1004        // narrow to first row: shape [1, 3], offset 0, contiguous
1005        let narrowed = tensor.narrow(0, 0, 1);
1006        assert!(narrowed.is_contiguous());
1007        assert_eq!(narrowed.layout().start_offset(), 0);
1008
1009        let result = narrowed.into_data();
1010        assert_eq!(result.shape.to_vec(), vec![1, 3]);
1011        // Must have exactly 3 f32s = 12 bytes, not 24
1012        assert_eq!(result.bytes.len(), 3 * core::mem::size_of::<f32>());
1013        let values: Vec<f32> = result.to_vec().unwrap();
1014        assert_eq!(values, vec![1.0, 2.0, 3.0]);
1015    }
1016}