Skip to main content

burn_ndarray/
tensor.rs

1use burn_backend::{
2    AllocationProperty, DType, Element, Shape, TensorData, TensorMetadata,
3    quantization::{QParams, QuantLevel, QuantMode, QuantScheme, QuantValue},
4};
5use burn_std::BoolStore;
6
7use crate::ops::quantization::{QuantizationStrategy, SymmetricQuantization};
8use crate::{NdArrayDevice, NdArrayStorage};
9use alloc::vec::Vec;
10use ndarray::{ArcArray, ArrayD, IxDyn};
11
12/// Concrete storage type for ndarray (owned with COW semantics via Arc)
13pub type SharedArray<E> = ArcArray<E, IxDyn>;
14
15/// Tensor primitive used by the [ndarray backend](crate::NdArray).
16///
17/// Supports both owned and borrowed (zero-copy) data via `NdArrayStorage`.
18/// When data is borrowed from external sources (like burnpack files),
19/// it remains zero-copy until a mutating operation is performed.
20#[derive(Debug, Clone)]
21#[allow(missing_docs)]
22pub enum NdArrayTensor {
23    F64(NdArrayStorage<f64>),
24    F32(NdArrayStorage<f32>),
25    I64(NdArrayStorage<i64>),
26    I32(NdArrayStorage<i32>),
27    I16(NdArrayStorage<i16>),
28    I8(NdArrayStorage<i8>),
29    U64(NdArrayStorage<u64>),
30    U32(NdArrayStorage<u32>),
31    U16(NdArrayStorage<u16>),
32    U8(NdArrayStorage<u8>),
33    Bool(NdArrayStorage<bool>),
34}
35
36impl NdArrayTensor {
37    /// Extract bool array, converting to owned if necessary.
38    pub(crate) fn bool(self) -> SharedArray<bool> {
39        match self {
40            NdArrayTensor::Bool(storage) => storage.into_shared(),
41            _ => unimplemented!("Expected bool tensor, got {:?}", self.dtype()),
42        }
43    }
44
45    /// Returns true if this tensor uses borrowed (zero-copy) storage.
46    #[inline]
47    pub fn is_borrowed(&self) -> bool {
48        macro_rules! check {
49            ($($variant:ident),*) => {
50                match self {
51                    $(NdArrayTensor::$variant(s) => s.is_borrowed(),)*
52                }
53            };
54        }
55        check!(F64, F32, I64, I32, I16, I8, U64, U32, U16, U8, Bool)
56    }
57}
58
59pub(crate) fn cast_to_dtype<E1: Element>(array: SharedArray<E1>, dtype: DType) -> NdArrayTensor
60where
61    NdArrayTensor: From<SharedArray<E1>>,
62{
63    fn cast<E1: Element, E2: Element>(array: SharedArray<E1>) -> SharedArray<E2> {
64        array.mapv(|a| a.elem()).into_shared()
65    }
66
67    if E1::dtype() == dtype {
68        return array.into();
69    }
70
71    match dtype {
72        DType::F64 => cast::<E1, f64>(array).into(),
73        DType::F32 => cast::<E1, f32>(array).into(),
74        DType::Flex32 => cast::<E1, f32>(array).into(),
75        DType::I64 => cast::<E1, i64>(array).into(),
76        DType::I32 => cast::<E1, i32>(array).into(),
77        DType::I16 => cast::<E1, i16>(array).into(),
78        DType::I8 => cast::<E1, i8>(array).into(),
79        DType::U64 => cast::<E1, u64>(array).into(),
80        DType::U32 => cast::<E1, u32>(array).into(),
81        DType::U16 => cast::<E1, u16>(array).into(),
82        DType::U8 => cast::<E1, u8>(array).into(),
83        DType::Bool(BoolStore::Native) => cast::<E1, bool>(array).into(),
84        dtype => panic!("Unsupported dtype: {dtype:?}"),
85    }
86}
87
88macro_rules! impl_from {
89    ($($ty: ty => $dtype: ident),*) => {
90        // From SharedArray (owned) -> NdArrayTensor
91        $(impl From<SharedArray<$ty>> for NdArrayTensor {
92           fn from(value: SharedArray<$ty>) -> NdArrayTensor {
93                NdArrayTensor::$dtype(NdArrayStorage::from_owned(value))
94           }
95        })*
96
97        // From NdArrayStorage -> NdArrayTensor
98        $(impl From<NdArrayStorage<$ty>> for NdArrayTensor {
99           fn from(value: NdArrayStorage<$ty>) -> NdArrayTensor {
100                NdArrayTensor::$dtype(value)
101           }
102        })*
103    };
104}
105
106impl_from!(
107    f64 => F64, f32 => F32,
108    i64 => I64, i32 => I32, i16 => I16, i8 => I8,
109    u64 => U64, u32 => U32, u16 => U16, u8 => U8,
110    bool => Bool
111);
112
113/// Macro to execute an operation on a given element type.
114///
115/// Extracts the storage from NdArrayTensor, converts to SharedArray, and passes to operation.
116///
117/// # Panics
118/// Since there is no automatic type cast at this time, binary operations for different
119/// floating point precision data types will panic with a data type mismatch.
120#[macro_export]
121macro_rules! execute_with_dtype {
122    (($lhs:expr, $rhs:expr),$element:ident,  $op:expr, [$($dtype: ident => $ty: ty),*]) => {{
123        let lhs_dtype = burn_backend::TensorMetadata::dtype(&$lhs);
124        let rhs_dtype = burn_backend::TensorMetadata::dtype(&$rhs);
125        match ($lhs, $rhs) {
126            $(
127                ($crate::NdArrayTensor::$dtype(lhs), $crate::NdArrayTensor::$dtype(rhs)) => {
128                    #[allow(unused)]
129                    type $element = $ty;
130                    // Convert storage to SharedArray for compatibility with existing operations
131                    $op(lhs.into_shared(), rhs.into_shared()).into()
132                }
133            )*
134            _ => panic!(
135                "Data type mismatch (lhs: {:?}, rhs: {:?})",
136                lhs_dtype, rhs_dtype
137            ),
138        }
139    }};
140    // Binary op: type automatically inferred by the compiler
141    (($lhs:expr, $rhs:expr), $op:expr) => {{
142        $crate::execute_with_dtype!(($lhs, $rhs), E, $op)
143    }};
144
145    // Binary op: generic type cannot be inferred for an operation
146    (($lhs:expr, $rhs:expr), $element:ident, $op:expr) => {{
147        $crate::execute_with_dtype!(($lhs, $rhs), $element, $op, [
148            F64 => f64, F32 => f32,
149            I64 => i64, I32 => i32, I16 => i16, I8 => i8,
150            U64 => u64, U32 => u32, U16 => u16, U8 => u8,
151            Bool => bool
152        ])
153    }};
154
155    ($tensor:expr, $element:ident, $op:expr, [$($dtype: ident => $ty: ty),*]) => {{
156        match $tensor {
157            $(
158                $crate::NdArrayTensor::$dtype(storage) => {
159                    #[allow(unused)]
160                    type $element = $ty;
161                    // Convert to SharedArray for compatibility with most operations
162                    $op(storage.into_shared()).into()
163                }
164            )*
165            #[allow(unreachable_patterns)]
166            other => unimplemented!("unsupported dtype: {:?}", other.dtype())
167        }
168    }};
169    // Unary op: type automatically inferred by the compiler
170    ($tensor:expr, $op:expr) => {{
171        $crate::execute_with_dtype!($tensor, E, $op)
172    }};
173
174    // Unary op: generic type cannot be inferred for an operation
175    ($tensor:expr, $element:ident, $op:expr) => {{
176        $crate::execute_with_dtype!($tensor, $element, $op, [
177            F64 => f64, F32 => f32,
178            I64 => i64, I32 => i32, I16 => i16, I8 => i8,
179            U64 => u64, U32 => u32, U16 => u16, U8 => u8,
180            Bool => bool
181        ])
182    }};
183}
184
185/// Macro to execute an operation a given element type.
186/// Only handles float types.
187///
188/// # Panics
189/// Since there is no automatic type cast at this time, binary operations for different
190/// floating point precision data types will panic with a data type mismatch.
191#[macro_export]
192macro_rules! execute_with_float_dtype {
193    // Binary op: type automatically inferred by the compiler
194    (($lhs:expr, $rhs:expr), $op:expr) => {{
195        $crate::execute_with_float_dtype!(($lhs, $rhs), E, $op)
196    }};
197
198    // Binary op: generic type cannot be inferred for an operation
199    (($lhs:expr, $rhs:expr), $element:ident, $op:expr) => {{
200        $crate::execute_with_dtype!(($lhs, $rhs), $element, $op, [
201            F64 => f64, F32 => f32
202        ])
203    }};
204
205    // Unary op: type automatically inferred by the compiler
206    ($tensor:expr, $op:expr) => {{
207        $crate::execute_with_float_dtype!($tensor, E, $op)
208    }};
209
210    // Unary op: generic type cannot be inferred for an operation
211    ($tensor:expr, $element:ident, $op:expr) => {{
212        $crate::execute_with_dtype!($tensor, $element, $op, [
213            F64 => f64, F32 => f32
214        ])
215    }};
216}
217
218/// Macro to execute an operation a given element type.
219/// Only handles int types.
220///
221/// # Panics
222/// Since there is no automatic type cast at this time, binary operations for different
223/// floating point precision data types will panic with a data type mismatch.
224#[macro_export]
225macro_rules! execute_with_int_dtype {
226    // Binary op: type automatically inferred by the compiler
227    (($lhs:expr, $rhs:expr), $op:expr) => {{
228        $crate::execute_with_int_dtype!(($lhs, $rhs), E, $op)
229    }};
230
231    // Binary op: generic type cannot be inferred for an operation
232    (($lhs:expr, $rhs:expr), $element:ident, $op:expr) => {{
233        $crate::execute_with_dtype!(($lhs, $rhs), $element, $op, [
234            I64 => i64, I32 => i32, I16 => i16, I8 => i8,
235            U64 => u64, U32 => u32, U16 => u16, U8 => u8
236        ])
237    }};
238
239    // Unary op: type automatically inferred by the compiler
240    ($tensor:expr, $op:expr) => {{
241        $crate::execute_with_int_dtype!($tensor, E, $op)
242    }};
243
244    // Unary op: generic type cannot be inferred for an operation
245    ($tensor:expr, $element:ident, $op:expr) => {{
246        $crate::execute_with_dtype!($tensor, $element, $op, [
247            I64 => i64, I32 => i32, I16 => i16, I8 => i8,
248            U64 => u64, U32 => u32, U16 => u16, U8 => u8
249        ])
250    }};
251}
252
253/// Macro to execute an operation a given element type.
254/// Only handles numeric types
255///
256/// # Panics
257/// Since there is no automatic type cast at this time, binary operations for different
258/// floating point precision data types will panic with a data type mismatch.
259#[macro_export]
260macro_rules! execute_with_numeric_dtype {
261    // Binary op: type automatically inferred by the compiler
262    (($lhs:expr, $rhs:expr), $op:expr) => {{
263        $crate::execute_with_numeric_dtype!(($lhs, $rhs), E, $op)
264    }};
265
266    // Binary op: generic type cannot be inferred for an operation
267    (($lhs:expr, $rhs:expr), $element:ident, $op:expr) => {{
268        $crate::execute_with_dtype!(($lhs, $rhs), $element, $op, [
269            F64 => f64, F32 => f32,
270            I64 => i64, I32 => i32, I16 => i16, I8 => i8,
271            U64 => u64, U32 => u32, U16 => u16, U8 => u8
272        ])
273    }};
274
275    // Unary op: type automatically inferred by the compiler
276    ($tensor:expr, $op:expr) => {{
277        $crate::execute_with_numeric_dtype!($tensor, E, $op)
278    }};
279
280    // Unary op: generic type cannot be inferred for an operation
281    ($tensor:expr, $element:ident, $op:expr) => {{
282        $crate::execute_with_dtype!($tensor, $element, $op, [
283            F64 => f64, F32 => f32,
284            I64 => i64, I32 => i32, I16 => i16, I8 => i8,
285            U64 => u64, U32 => u32, U16 => u16, U8 => u8
286        ])
287    }};
288}
289
290/// Macro to execute a cat operation on a given set of element types.
291///
292/// Uses zero-copy views from storage for concatenation.
293///
294/// # Panics
295/// Since there is no automatic type cast at this time, binary operations for different
296/// floating point precision data types will panic with a data type mismatch.
297#[macro_export]
298macro_rules! cat_with_dtype {
299    ($tensors: expr, $dim: expr, [$($dtype: ident),*]) => {
300        match &$tensors[0] {
301            $(NdArrayTensor::$dtype(_) => {
302                let tensors = $tensors
303                    .iter()
304                    .map(|t| {
305                        if let NdArrayTensor::$dtype(storage) = t {
306                            // Use storage.view() for zero-copy access
307                            storage.view()
308                        } else {
309                            panic!("Concatenate data type mismatch (expected {:?}, got {:?})", $tensors[0].dtype(), t.dtype())
310                        }
311                    })
312                    .collect::<Vec<_>>();
313                NdArrayOps::concatenate(&tensors, $dim).into()
314            })*
315            _ => panic!("Unsupported dtype: {:?}", $tensors[0].dtype())
316        }
317    };
318}
319
320/// Macro to execute an operation that returns a given element type.
321#[macro_export]
322macro_rules! execute_with_float_out_dtype {
323    ($out_dtype:expr, $element:ident, $op:expr, [$($dtype: ident => $ty: ty),*]) => {{
324        match $out_dtype {
325            $(
326                burn_std::FloatDType::$dtype => {
327                    #[allow(unused)]
328                    type $element = $ty;
329                    $op
330                }
331            )*
332            #[allow(unreachable_patterns)]
333            other => unimplemented!("unsupported dtype: {other:?}")
334        }
335    }};
336    // Unary op: type automatically inferred by the compiler
337    ($out_dtype:expr, $op:expr) => {{
338        $crate::execute_with_float_out_dtype!($out_dtype, E, $op)
339    }};
340
341    // Unary op: generic type cannot be inferred for an operation
342    ($out_dtype:expr, $element:ident, $op:expr) => {{
343        $crate::execute_with_float_out_dtype!($out_dtype, $element, $op, [
344            F64 => f64, F32 => f32
345        ])
346    }};
347}
348
349/// Macro to execute an operation that returns a given element type.
350#[macro_export]
351macro_rules! execute_with_int_out_dtype {
352    ($out_dtype:expr, $element:ident, $op:expr, [$($dtype: ident => $ty: ty),*]) => {{
353        match $out_dtype {
354            $(
355                burn_std::IntDType::$dtype => {
356                    #[allow(unused)]
357                    type $element = $ty;
358                    $op
359                }
360            )*
361            #[allow(unreachable_patterns)]
362            other => unimplemented!("unsupported dtype: {other:?}")
363        }
364    }};
365    // Unary op: type automatically inferred by the compiler
366    ($out_dtype:expr, $op:expr) => {{
367        $crate::execute_with_int_out_dtype!($out_dtype, E, $op)
368    }};
369
370    // Unary op: generic type cannot be inferred for an operation
371    ($out_dtype:expr, $element:ident, $op:expr) => {{
372        $crate::execute_with_int_out_dtype!($out_dtype, $element, $op, [
373            I64 => i64, I32 => i32, I16 => i16, I8 => i8,
374            U64 => u64, U32 => u32, U16 => u16, U8 => u8
375        ])
376    }};
377}
378
379impl TensorMetadata for NdArrayTensor {
380    type Device = NdArrayDevice;
381    fn dtype(&self) -> DType {
382        match self {
383            NdArrayTensor::F64(_) => DType::F64,
384            NdArrayTensor::F32(_) => DType::F32,
385            NdArrayTensor::I64(_) => DType::I64,
386            NdArrayTensor::I32(_) => DType::I32,
387            NdArrayTensor::I16(_) => DType::I16,
388            NdArrayTensor::I8(_) => DType::I8,
389            NdArrayTensor::U64(_) => DType::U64,
390            NdArrayTensor::U32(_) => DType::U32,
391            NdArrayTensor::U16(_) => DType::U16,
392            NdArrayTensor::U8(_) => DType::U8,
393            NdArrayTensor::Bool(_) => DType::Bool(BoolStore::Native),
394        }
395    }
396
397    fn shape(&self) -> Shape {
398        // Use storage's shape method (works for both borrowed and owned)
399        macro_rules! get_shape {
400            ($($variant:ident),*) => {
401                match self {
402                    $(NdArrayTensor::$variant(storage) => Shape::from(storage.shape().to_vec()),)*
403                }
404            };
405        }
406        get_shape!(F64, F32, I64, I32, I16, I8, U64, U32, U16, U8, Bool)
407    }
408
409    fn rank(&self) -> usize {
410        self.shape().num_dims()
411    }
412
413    fn device(&self) -> NdArrayDevice {
414        NdArrayDevice::Cpu
415    }
416
417    fn can_mut(&self) -> bool {
418        // NdArray storage is copy-on-write (`ArcArray`) without a public
419        // uniqueness check at this level; in-place ops resolve sharing
420        // themselves, so conservatively report the buffer as shared.
421        false
422    }
423}
424
425pub(crate) trait ShapeOps {
426    fn num_dims(self) -> usize;
427    fn num_elements(self) -> usize;
428    fn dims<const N: usize>(self) -> [usize; N];
429    fn into_shape(self) -> Shape;
430}
431
432impl ShapeOps for &[usize] {
433    fn num_dims(self) -> usize {
434        self.len()
435    }
436
437    fn num_elements(self) -> usize {
438        self.iter().product()
439    }
440
441    fn dims<const N: usize>(self) -> [usize; N] {
442        self.try_into().unwrap()
443    }
444
445    fn into_shape(self) -> Shape {
446        Shape::from(self)
447    }
448}
449
450mod utils {
451    use burn_std::tensor::is_contiguous;
452
453    use super::*;
454
455    impl NdArrayTensor {
456        pub(crate) fn into_data(self) -> TensorData {
457            let shape = self.shape();
458            let contiguous = self.is_contiguous();
459
460            fn inner<E: Element>(
461                shape: Shape,
462                is_contiguous: bool,
463                array: ArcArray<E, IxDyn>,
464            ) -> TensorData {
465                let vec = if is_contiguous {
466                    match array.try_into_owned_nocopy() {
467                        Ok(owned) => {
468                            let (mut vec, offset) = owned.into_raw_vec_and_offset();
469                            if let Some(offset) = offset {
470                                vec.drain(..offset);
471                            }
472                            if vec.len() > shape.num_elements() {
473                                vec.drain(shape.num_elements()..vec.len());
474                            }
475                            vec
476                        }
477                        Err(array) => array.into_iter().collect(),
478                    }
479                } else {
480                    array.into_iter().collect()
481                };
482
483                TensorData::new(vec, shape)
484            }
485
486            // Convert storage to owned array before extracting data
487            execute_with_dtype!(self, |arr| inner(shape, contiguous, arr))
488        }
489
490        pub(crate) fn is_contiguous(&self) -> bool {
491            // For borrowed data, we assume it's contiguous (it came from TensorData which is contiguous)
492            // For owned data, we check the strides
493            macro_rules! check_contiguous {
494                ($($variant:ident),*) => {
495                    match self {
496                        $(NdArrayTensor::$variant(storage) => {
497                            match storage {
498                                NdArrayStorage::Borrowed { .. } => {
499                                    // Borrowed storage requires contiguous row-major data
500                                    // (see NdArrayStorage::from_borrowed documentation)
501                                    true
502                                }
503                                NdArrayStorage::Owned(array) => {
504                                    let shape = array.shape();
505                                    let mut strides = Vec::with_capacity(array.strides().len());
506                                    for &stride in array.strides() {
507                                        if stride <= 0 {
508                                            return false;
509                                        }
510                                        strides.push(stride as usize);
511                                    }
512                                    is_contiguous(shape, &strides)
513                                }
514                            }
515                        })*
516                    }
517                };
518            }
519            check_contiguous!(F64, F32, I64, I32, I16, I8, U64, U32, U16, U8, Bool)
520        }
521    }
522}
523
524/// Converts a slice of usize to a typed dimension.
525#[macro_export(local_inner_macros)]
526macro_rules! to_typed_dims {
527    (
528        $n:expr,
529        $dims:expr,
530        justdim
531    ) => {{
532        let mut dims = [0; $n];
533        for i in 0..$n {
534            dims[i] = $dims[i];
535        }
536        let dim: Dim<[usize; $n]> = Dim(dims);
537        dim
538    }};
539}
540
541/// Reshapes an array into a tensor.
542#[macro_export(local_inner_macros)]
543macro_rules! reshape {
544    (
545        ty $ty:ty,
546        n $n:expr,
547        shape $shape:expr,
548        array $array:expr
549    ) => {{
550        let dim = $crate::to_typed_dims!($n, $shape, justdim);
551        let array = match $array.is_standard_layout() {
552            // Move the array into the new shape rather than going through
553            // `to_shape`: the latter returns a borrowed view here, which
554            // `into_shared` then clones, copying the buffer on every reshape.
555            // Moving rewrites the dimensions in place, and the buffer stays
556            // shared for copy-on-write like in any other operation.
557            true => {
558                match $array.into_shape_with_order(dim) {
559                    Ok(val) => val,
560                    Err(err) => {
561                        core::panic!("Shape should be compatible shape={dim:?}: {err:?}");
562                    }
563                }
564            },
565            false => $array.to_shape(dim).unwrap().as_standard_layout().into_shared(),
566        };
567        array.into_dyn()
568    }};
569    (
570        ty $ty:ty,
571        shape $shape:expr,
572        array $array:expr,
573        d $D:expr
574    ) => {{
575        match $D {
576            1 => reshape!(ty $ty, n 1, shape $shape, array $array),
577            2 => reshape!(ty $ty, n 2, shape $shape, array $array),
578            3 => reshape!(ty $ty, n 3, shape $shape, array $array),
579            4 => reshape!(ty $ty, n 4, shape $shape, array $array),
580            5 => reshape!(ty $ty, n 5, shape $shape, array $array),
581            6 => reshape!(ty $ty, n 6, shape $shape, array $array),
582            _ => core::panic!("NdArray supports arrays up to 6 dimensions, received: {}", $D),
583        }
584    }};
585}
586
587/// Slice a tensor
588#[macro_export]
589macro_rules! slice {
590    ($tensor:expr, $slices:expr) => {
591        slice!($tensor, $slices, F64, F32, I64, I32, I16, I8, U64, U32, U16, U8, Bool)
592    };
593    ($tensor:expr, $slices:expr, $($variant:ident),*) => {
594        match $tensor {
595            $(NdArrayTensor::$variant(s) => { NdArrayOps::slice(s.view(), $slices).into() })*
596        }
597    };
598}
599
600impl NdArrayTensor {
601    /// Create a new [ndarray tensor](NdArrayTensor) from [data](TensorData).
602    ///
603    /// This method attempts zero-copy loading when possible. If the data has properly
604    /// aligned bytes that can be borrowed, it creates a borrowed tensor. Otherwise,
605    /// it falls back to copying the data.
606    ///
607    /// Zero-copy loading works when:
608    /// - The data's bytes are properly aligned for the element type
609    /// - The bytes can be borrowed (e.g., from mmap'd file or static data)
610    pub fn from_data(data: TensorData) -> NdArrayTensor {
611        // Only use Borrowed storage for non-native allocations (e.g., burnpack mmap/file).
612        // For native Rust heap allocations (the common case), go directly to owned storage:
613        // `from_data_owned` reclaims the Vec zero-copy via `into_vec`, while
614        // Borrowed storage would trigger a full memcopy on every single operation.
615        if data.bytes.property() != AllocationProperty::Native {
616            match Self::try_from_data_borrowed(data) {
617                Ok(tensor) => return tensor,
618                Err(data) => return Self::from_data_owned(data),
619            }
620        }
621        Self::from_data_owned(data)
622    }
623
624    /// Try to create a tensor with borrowed storage (zero-copy).
625    ///
626    /// Takes ownership of TensorData and returns it back on failure.
627    /// No cloning occurs - bytes are moved into storage or returned on failure.
628    ///
629    /// Returns `Err(data)` if borrowing is not possible (e.g., misaligned data).
630    fn try_from_data_borrowed(data: TensorData) -> Result<NdArrayTensor, TensorData> {
631        let TensorData {
632            bytes,
633            shape,
634            dtype,
635        } = data;
636
637        macro_rules! try_borrow {
638            ($ty:ty, $variant:ident, $bytes:expr, $shape:expr) => {
639                match NdArrayStorage::<$ty>::from_borrowed($bytes, $shape) {
640                    Ok(storage) => return Ok(NdArrayTensor::$variant(storage)),
641                    Err((bytes, shape)) => (bytes, shape),
642                }
643            };
644        }
645
646        // Try to create borrowed storage; get bytes back on failure
647        let (bytes, shape) = match dtype {
648            DType::F64 => try_borrow!(f64, F64, bytes, shape),
649            DType::F32 => try_borrow!(f32, F32, bytes, shape),
650            DType::I64 => try_borrow!(i64, I64, bytes, shape),
651            DType::I32 => try_borrow!(i32, I32, bytes, shape),
652            DType::I16 => try_borrow!(i16, I16, bytes, shape),
653            DType::I8 => try_borrow!(i8, I8, bytes, shape),
654            DType::U64 => try_borrow!(u64, U64, bytes, shape),
655            DType::U32 => try_borrow!(u32, U32, bytes, shape),
656            DType::U16 => try_borrow!(u16, U16, bytes, shape),
657            DType::U8 => try_borrow!(u8, U8, bytes, shape),
658            DType::Bool(BoolStore::Native) => try_borrow!(bool, Bool, bytes, shape),
659            _ => (bytes, shape), // QFloat not supported for zero-copy
660        };
661
662        Err(TensorData {
663            bytes,
664            shape,
665            dtype,
666        })
667    }
668
669    /// Create a tensor with owned storage.
670    ///
671    /// This may or may not copy data depending on whether the underlying bytes
672    /// can be reclaimed (via `try_into_vec`). If bytes are uniquely owned,
673    /// no copy occurs; otherwise data is copied to a new allocation.
674    fn from_data_owned(data: TensorData) -> NdArrayTensor {
675        let shape = data.shape.to_vec(); // TODO: into_vec
676
677        macro_rules! execute {
678            ($data: expr, [$($dtype: pat => $ty: ty),*]) => {
679                match $data.dtype {
680                    $( $dtype => {
681                        match data.into_vec::<$ty>() {
682                            Ok(vec) => unsafe { ArrayD::from_shape_vec_unchecked(shape, vec) }.into_shared(),
683                            Err(err) => panic!("Data should have the same element type as the tensor {err:?}"),
684                        }.into()
685                    }, )*
686                    other => unimplemented!("Unsupported dtype {other:?}"),
687                }
688            };
689        }
690
691        execute!(data, [
692            DType::F64 => f64, DType::F32 => f32,
693            DType::I64 => i64, DType::I32 => i32, DType::I16 => i16, DType::I8 => i8,
694            DType::U64 => u64, DType::U32 => u32, DType::U16 => u16, DType::U8 => u8,
695            DType::Bool(BoolStore::Native) => bool
696        ])
697    }
698}
699
700/// A quantized tensor for the ndarray backend.
701#[derive(Clone, Debug)]
702pub struct NdArrayQTensor {
703    /// The quantized tensor.
704    pub qtensor: NdArrayTensor,
705    /// The quantization scheme.
706    pub scheme: QuantScheme,
707    /// The quantization parameters.
708    pub qparams: Vec<QParams<f32>>,
709}
710
711impl NdArrayQTensor {
712    /// Returns the quantization strategy, including quantization parameters, for the given tensor.
713    pub fn strategy(&self) -> QuantizationStrategy {
714        match self.scheme {
715            QuantScheme {
716                level: QuantLevel::Tensor,
717                mode: QuantMode::Symmetric,
718                value:
719                    QuantValue::Q8F
720                    | QuantValue::Q8S
721                    | QuantValue::E4M3
722                    | QuantValue::E5M2
723                    | QuantValue::Q4F
724                    | QuantValue::Q4S
725                    | QuantValue::E2M1
726                    | QuantValue::Q2F
727                    | QuantValue::Q2S,
728                ..
729            } => QuantizationStrategy::PerTensorSymmetric(SymmetricQuantization::init(
730                self.qparams[0].scales,
731                self.scheme.value,
732            )),
733            QuantScheme {
734                level: QuantLevel::Block(block_size),
735                mode: QuantMode::Symmetric,
736                value:
737                    QuantValue::Q8F
738                    | QuantValue::Q8S
739                    | QuantValue::E4M3
740                    | QuantValue::E5M2
741                    | QuantValue::Q4F
742                    | QuantValue::Q4S
743                    | QuantValue::E2M1
744                    | QuantValue::Q2F
745                    | QuantValue::Q2S,
746                ..
747            } => QuantizationStrategy::PerBlockSymmetric(
748                self.qparams
749                    .iter()
750                    .map(|q| SymmetricQuantization::init(q.scales, self.scheme.value))
751                    .collect(),
752                block_size,
753            ),
754        }
755    }
756}
757
758impl TensorMetadata for NdArrayQTensor {
759    type Device = NdArrayDevice;
760    fn dtype(&self) -> DType {
761        DType::QFloat(self.scheme)
762    }
763
764    fn shape(&self) -> Shape {
765        self.qtensor.shape()
766    }
767
768    fn rank(&self) -> usize {
769        self.shape().num_dims()
770    }
771
772    fn device(&self) -> Self::Device {
773        NdArrayDevice::Cpu
774    }
775
776    fn can_mut(&self) -> bool {
777        self.qtensor.can_mut()
778    }
779}
780
781#[cfg(test)]
782mod tests {
783    use crate::NdArray;
784    use alloc::vec;
785
786    use super::*;
787    use burn_backend::{
788        Distribution,
789        ops::{FloatTensorOps, QTensorOps},
790        quantization::{QuantStore, QuantizationParametersPrimitive},
791    };
792    use burn_std::rand::get_seeded_rng;
793
794    #[test]
795    fn should_support_into_and_from_data_1d() {
796        let data_expected = TensorData::random::<f32, _, _>(
797            Shape::new([3]),
798            Distribution::Default,
799            &mut get_seeded_rng(),
800        );
801        let tensor = NdArrayTensor::from_data(data_expected.clone());
802
803        let data_actual = tensor.into_data();
804
805        assert_eq!(data_expected, data_actual);
806    }
807
808    #[test]
809    fn should_support_into_and_from_data_2d() {
810        let data_expected = TensorData::random::<f32, _, _>(
811            Shape::new([2, 3]),
812            Distribution::Default,
813            &mut get_seeded_rng(),
814        );
815        let tensor = NdArrayTensor::from_data(data_expected.clone());
816
817        let data_actual = tensor.into_data();
818
819        assert_eq!(data_expected, data_actual);
820    }
821
822    #[test]
823    fn should_support_into_and_from_data_3d() {
824        let data_expected = TensorData::random::<f32, _, _>(
825            Shape::new([2, 3, 4]),
826            Distribution::Default,
827            &mut get_seeded_rng(),
828        );
829        let tensor = NdArrayTensor::from_data(data_expected.clone());
830
831        let data_actual = tensor.into_data();
832
833        assert_eq!(data_expected, data_actual);
834    }
835
836    #[test]
837    fn should_support_into_and_from_data_4d() {
838        let data_expected = TensorData::random::<f32, _, _>(
839            Shape::new([2, 3, 4, 2]),
840            Distribution::Default,
841            &mut get_seeded_rng(),
842        );
843        let tensor = NdArrayTensor::from_data(data_expected.clone());
844
845        let data_actual = tensor.into_data();
846
847        assert_eq!(data_expected, data_actual);
848    }
849
850    #[test]
851    fn should_support_qtensor_strategy() {
852        type B = NdArray;
853        let scale: f32 = 0.009_019_608;
854        let device = Default::default();
855
856        let tensor = B::float_from_data(TensorData::from([-1.8f32, -1.0, 0.0, 0.5]), &device);
857        let scheme = QuantScheme::default()
858            .with_value(QuantValue::Q8S)
859            .with_store(QuantStore::Native);
860        let qparams = QuantizationParametersPrimitive {
861            scales: B::float_from_data(TensorData::from([scale]), &device),
862        };
863        let qtensor: NdArrayQTensor = B::quantize(tensor, &scheme, qparams);
864
865        assert_eq!(qtensor.scheme(), scheme);
866        assert_eq!(
867            qtensor.strategy(),
868            QuantizationStrategy::PerTensorSymmetric(SymmetricQuantization::init(
869                scale,
870                QuantValue::Q8S
871            ))
872        );
873    }
874
875    // ==========================================================================
876    // Zero-copy integration tests
877    // These tests verify end-to-end zero-copy behavior through NdArrayTensor.
878    // ==========================================================================
879
880    #[test]
881    fn zero_copy_creates_borrowed_storage_for_non_native() {
882        // Verify that from_data creates borrowed storage for non-native allocations
883        // (e.g. burnpack mmap/file data tagged with AllocationProperty::Other or File).
884        // Native heap allocations intentionally use Owned storage for performance.
885        use burn_backend::AllocationProperty;
886        use burn_std::Bytes;
887
888        let data: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0];
889        let bytes = Bytes::from_elems(data);
890        // Tag as Other to simulate burnpack / mmap data (non-native backing storage)
891        let non_native_bytes = Bytes::from_shared(
892            bytes::Bytes::copy_from_slice(&bytes),
893            AllocationProperty::Other,
894        );
895        let tensor_data = TensorData::from_bytes(non_native_bytes, Shape::new([2, 2]), DType::F32);
896
897        let tensor = NdArrayTensor::from_data(tensor_data);
898
899        match &tensor {
900            NdArrayTensor::F32(storage) => {
901                assert!(
902                    storage.is_borrowed(),
903                    "ZERO-COPY REGRESSION: from_data should create borrowed storage \
904                     for non-native (e.g. burnpack) TensorData"
905                );
906                assert!(
907                    !storage.is_unique(),
908                    "ZERO-COPY REGRESSION: borrowed storage must report is_unique() == false"
909                );
910            }
911            _ => panic!("Expected F32 tensor"),
912        }
913    }
914
915    #[test]
916    fn native_alloc_creates_owned_storage() {
917        // Native heap allocations must use Owned storage to avoid the memcpy.
918        use burn_std::Bytes;
919
920        let data: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0];
921        let bytes = Bytes::from_elems(data); // AllocationProperty::Native
922        let tensor_data = TensorData::from_bytes(bytes, Shape::new([2, 2]), DType::F32);
923
924        let tensor = NdArrayTensor::from_data(tensor_data);
925
926        match &tensor {
927            NdArrayTensor::F32(storage) => {
928                assert!(
929                    !storage.is_borrowed(),
930                    "PERF REGRESSION: from_data must NOT create borrowed storage \
931                     for native TensorData"
932                );
933            }
934            _ => panic!("Expected F32 tensor"),
935        }
936    }
937
938    #[test]
939    fn zero_copy_data_integrity() {
940        // Verify data is correctly accessible through borrowed storage
941        use burn_std::Bytes;
942
943        let data: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0];
944        let bytes = Bytes::from_elems(data);
945        let tensor_data = TensorData::from_bytes(bytes, Shape::new([2, 2]), DType::F32);
946
947        let tensor = NdArrayTensor::from_data(tensor_data);
948
949        match &tensor {
950            NdArrayTensor::F32(storage) => {
951                let view = storage.view();
952                assert_eq!(view[[0, 0]], 1.0);
953                assert_eq!(view[[0, 1]], 2.0);
954                assert_eq!(view[[1, 0]], 3.0);
955                assert_eq!(view[[1, 1]], 4.0);
956            }
957            _ => panic!("Expected F32 tensor"),
958        }
959    }
960
961    #[test]
962    fn zero_copy_fallback_when_bytes_owned() {
963        // When TensorData owns bytes exclusively, it may use the copy path
964        // This is expected behavior - verify it still works correctly
965        let data = TensorData::from([1.0f32, 2.0, 3.0, 4.0]);
966        let tensor = NdArrayTensor::from_data(data.clone());
967        let result = tensor.into_data();
968
969        assert_eq!(data, result, "Data should round-trip correctly");
970    }
971}