Skip to main content

kopitiam_tensor/tensor/
mod.rs

1//! [`Tensor`]: the shared-storage, strided, CPU tensor at the center of the
2//! Kopitiam Runtime.
3//!
4//! This module owns the struct, its constructors, its accessors, and the
5//! data-movement operations that need direct access to `storage`/`strides`/
6//! `offset` (dtype conversion, reshape, transpose, narrow, broadcast,
7//! concat). The math operations (matmul, elementwise arithmetic, softmax,
8//! reductions, normalization, activations, embedding gather) live in
9//! sibling modules — `matmul.rs`, `elementwise.rs`, and so on — each adding
10//! another `impl Tensor` block. They are child modules of this one
11//! specifically so they can see `Tensor`'s private fields directly, the
12//! same way methods in a single large `impl` block would, without forcing
13//! every field to be `pub(crate)` (and therefore visible to the rest of the
14//! crate, not just the tensor family of modules).
15
16mod matmul;
17mod elementwise;
18mod softmax;
19mod reduce;
20mod norm;
21mod activation;
22mod gather;
23mod concat;
24
25use std::sync::Arc;
26
27use kopitiam_core::{DType, Device, Error, Result, Shape};
28
29use crate::half;
30use crate::quant;
31use crate::storage::Storage;
32
33/// A CPU tensor: a [`Shape`] plus a strided view into a shared, ref-counted
34/// [`Storage`] buffer.
35///
36/// # Why `Arc<Storage>` plus separate strides and an offset
37///
38/// Model weights are hundreds of megabytes; every op that produces a new
39/// logical view of existing data — `reshape`, `transpose`, `narrow`,
40/// `broadcast_to` — must not copy that data. Sharing `storage` via `Arc`
41/// makes `Tensor::clone()` and every pure view operation an O(1) pointer
42/// bump plus a small `Shape`/`Vec<usize>` copy, never an O(n) memcpy of
43/// element data. `strides` (in elements, one per dimension) and `offset`
44/// (in elements, into the shared buffer) are what let a view disagree with
45/// its storage about layout: `narrow` changes `offset`, `transpose`
46/// reorders `strides`, `broadcast_to` introduces zero strides.
47///
48/// # Why quantized tensors don't get real views
49///
50/// A [`DType::is_quantized`] tensor's `strides` are always its shape's
51/// canonical strides and its `offset` is always `0` — block-quantized data
52/// cannot be addressed at anything finer than a whole block, so "a strided
53/// view into the middle of a Q4_0 tensor" is not a representable concept.
54/// Every op that would need one ([`Tensor::narrow`], [`Tensor::transpose`],
55/// [`Tensor::broadcast_to`], the arithmetic and normalization ops) rejects
56/// quantized tensors with [`Error::QuantizedElementAccess`] via
57/// [`Tensor::require_elementwise`] and [`Tensor::require_dtype`]. Call
58/// [`Tensor::to_dtype`] with [`DType::F32`] first.
59#[derive(Debug, Clone)]
60pub struct Tensor {
61    pub(super) storage: Arc<Storage>,
62    pub(super) shape: Shape,
63    /// Element strides, one per dimension of `shape` (same rank).
64    pub(super) strides: Vec<usize>,
65    /// Element offset into `storage`'s flat buffer.
66    pub(super) offset: usize,
67    pub(super) device: Device,
68}
69
70impl Tensor {
71    // -- Constructors --------------------------------------------------
72
73    /// Builds a contiguous `f32` tensor from `data`, which must hold
74    /// exactly `shape.elem_count()` elements.
75    pub fn from_f32(data: Vec<f32>, shape: impl Into<Shape>) -> Result<Self> {
76        let shape = shape.into();
77        check_len(&shape, DType::F32, data.len())?;
78        Ok(Self::new_contiguous(Storage::F32(data), shape))
79    }
80
81    /// Builds a tensor from raw IEEE 754 binary16 bits. Values are decoded
82    /// lazily by ops via [`Tensor::to_dtype`]; nothing here interprets them.
83    pub fn from_f16(data: Vec<u16>, shape: impl Into<Shape>) -> Result<Self> {
84        let shape = shape.into();
85        check_len(&shape, DType::F16, data.len())?;
86        Ok(Self::new_contiguous(Storage::F16(data), shape))
87    }
88
89    /// Builds a tensor from raw bfloat16 bits.
90    pub fn from_bf16(data: Vec<u16>, shape: impl Into<Shape>) -> Result<Self> {
91        let shape = shape.into();
92        check_len(&shape, DType::BF16, data.len())?;
93        Ok(Self::new_contiguous(Storage::BF16(data), shape))
94    }
95
96    pub fn from_i8(data: Vec<i8>, shape: impl Into<Shape>) -> Result<Self> {
97        let shape = shape.into();
98        check_len(&shape, DType::I8, data.len())?;
99        Ok(Self::new_contiguous(Storage::I8(data), shape))
100    }
101
102    /// Builds an `i32` tensor — the dtype [`kopitiam_core::DType`] documents
103    /// as "mostly for token ids and indices", e.g. the input to
104    /// [`Tensor::gather_rows`].
105    pub fn from_i32(data: Vec<i32>, shape: impl Into<Shape>) -> Result<Self> {
106        let shape = shape.into();
107        check_len(&shape, DType::I32, data.len())?;
108        Ok(Self::new_contiguous(Storage::I32(data), shape))
109    }
110
111    /// Builds a block-quantized tensor from raw on-disk block bytes.
112    ///
113    /// `bytes` must be exactly the number of bytes `shape.elem_count()`
114    /// elements of `dtype` require; see [`Storage::new_quantized`] for the
115    /// precise failure modes ([`Error::PartialQuantizedBlock`],
116    /// [`Error::StorageTooSmall`]).
117    pub fn from_quantized(dtype: DType, bytes: Vec<u8>, shape: impl Into<Shape>) -> Result<Self> {
118        let shape = shape.into();
119        let storage = Storage::new_quantized(dtype, bytes, shape.elem_count()).map_err(|e| {
120            match e {
121                // Re-attach the tensor's real (possibly multi-dimensional)
122                // shape rather than the synthetic 1-D shape Storage used,
123                // since Storage doesn't know about Shape's dimensions.
124                Error::StorageTooSmall { dtype, expected, actual, .. } => Error::StorageTooSmall {
125                    shape: shape.clone(),
126                    dtype,
127                    expected,
128                    actual,
129                },
130                other => other,
131            }
132        })?;
133        Ok(Self::new_contiguous(storage, shape))
134    }
135
136    /// A tensor of `f32` zeros. Convenience for tests and for initializing
137    /// accumulators; not meant as a general "any dtype" factory, since
138    /// every op in this crate computes in `f32` (see the crate docs).
139    pub fn zeros(shape: impl Into<Shape>) -> Self {
140        let shape = shape.into();
141        let data = vec![0.0f32; shape.elem_count()];
142        Self::new_contiguous(Storage::F32(data), shape)
143    }
144
145    fn new_contiguous(storage: Storage, shape: Shape) -> Self {
146        let strides = shape.strides();
147        Self {
148            storage: Arc::new(storage),
149            shape,
150            strides,
151            offset: 0,
152            device: Device::Cpu,
153        }
154    }
155
156    // -- Accessors --------------------------------------------------------
157
158    pub fn shape(&self) -> &Shape {
159        &self.shape
160    }
161
162    pub fn dtype(&self) -> DType {
163        self.storage.dtype()
164    }
165
166    pub fn device(&self) -> Device {
167        self.device
168    }
169
170    pub fn rank(&self) -> usize {
171        self.shape.rank()
172    }
173
174    pub fn elem_count(&self) -> usize {
175        self.shape.elem_count()
176    }
177
178    /// This view's element strides. Not necessarily the canonical strides
179    /// of `shape()` — see [`Tensor::is_contiguous`].
180    pub fn strides(&self) -> &[usize] {
181        &self.strides
182    }
183
184    /// Whether this view's elements sit sequentially in storage in
185    /// row-major order with no gaps, i.e. whether `strides()` equals
186    /// `shape().strides()`. A fresh tensor from any `from_*` constructor is
187    /// always contiguous; `narrow`, `transpose`, and `broadcast_to` can all
188    /// produce non-contiguous views.
189    pub fn is_contiguous(&self) -> bool {
190        self.strides == self.shape.strides()
191    }
192
193    // -- Element access helpers (used throughout this module family) -----
194
195    /// Storage offsets, in this tensor's row-major logical element order.
196    ///
197    /// This is the one place broadcasting (zero strides), slicing (nonzero
198    /// offset), and transposition (permuted strides) all reduce to the same
199    /// formula, so every op that needs to walk a tensor's elements —
200    /// materializing to a `Vec`, elementwise arithmetic, reductions,
201    /// gather — shares this instead of hand-rolling nested loops per rank.
202    pub(super) fn logical_offsets(&self) -> impl Iterator<Item = usize> + '_ {
203        let dims = self.shape.dims();
204        // Canonical (contiguous) strides for this tensor's own shape are
205        // exactly the divisors needed to decompose a flat row-major index
206        // `flat` into per-dimension coordinates: `coord[d] = (flat /
207        // canonical[d]) % dims[d]`.
208        let canonical = self.shape.strides();
209        let total = self.shape.elem_count();
210        (0..total).map(move |flat| {
211            dims.iter().enumerate().fold(self.offset, |acc, (d, &dim)| {
212                if dim == 0 {
213                    return acc;
214                }
215                let coord = (flat / canonical[d]) % dim;
216                acc + coord * self.strides[d]
217            })
218        })
219    }
220
221    /// Errors unless this tensor's dtype is exactly `dtype`.
222    ///
223    /// Used by every arithmetic/normalization/activation op, all of which
224    /// are implemented for `f32` only (see the crate docs for why). Because
225    /// every quantized dtype differs from `DType::F32`, this doubles as the
226    /// "reject quantized input" check for those ops — callers see a single
227    /// clear [`Error::DTypeMismatch`] rather than two different error paths
228    /// depending on *which* wrong dtype they passed.
229    pub(super) fn require_dtype(&self, dtype: DType) -> Result<()> {
230        if self.dtype() != dtype {
231            return Err(Error::DTypeMismatch {
232                expected: dtype,
233                actual: self.dtype(),
234            });
235        }
236        Ok(())
237    }
238
239    /// Errors if this tensor's dtype is block-quantized.
240    ///
241    /// Used by the pure data-movement ops (`narrow`, `transpose`,
242    /// `broadcast_to`, `contiguous`) that work for *any* addressable dtype,
243    /// not just `f32` — unlike [`Tensor::require_dtype`], which pins to one
244    /// specific type.
245    pub(super) fn require_elementwise(&self) -> Result<()> {
246        if self.dtype().is_quantized() {
247            return Err(Error::QuantizedElementAccess {
248                dtype: self.dtype(),
249                block_size: self.dtype().block_size(),
250            });
251        }
252        Ok(())
253    }
254
255    // -- Materialization ----------------------------------------------------
256
257    /// Copies this view's elements out as a plain `Vec<f32>` in row-major
258    /// order. Requires `dtype() == DType::F32`; call [`Tensor::to_dtype`]
259    /// first for any other dtype, including the quantized ones.
260    pub fn to_vec_f32(&self) -> Result<Vec<f32>> {
261        self.require_dtype(DType::F32)?;
262        let Storage::F32(data) = self.storage.as_ref() else {
263            unreachable!("require_dtype(F32) guarantees Storage::F32")
264        };
265        Ok(self.logical_offsets().map(|i| data[i]).collect())
266    }
267
268    /// Copies this view's elements out as a plain `Vec<i32>` in row-major
269    /// order. The `i32` twin of [`Tensor::to_vec_f32`]: needs
270    /// `dtype() == DType::I32`, errors [`Error::DTypeMismatch`] otherwise.
271    ///
272    /// This is how you read the *output* of an index-producing op — namely
273    /// [`Tensor::argmax`], whose result is the token ids a greedy sampler
274    /// hands back to the runtime. Without it argmax would be a dead end: you
275    /// could compute the winning indices but never get them out as plain
276    /// numbers. (There's deliberately no `i32 <-> f32` cast anywhere in this
277    /// crate — see [`Tensor::to_dtype`] — so this accessor is the *only* way
278    /// I32 data leaves a tensor, same as `to_vec_f32` is for F32.)
279    pub fn to_vec_i32(&self) -> Result<Vec<i32>> {
280        self.require_dtype(DType::I32)?;
281        let Storage::I32(data) = self.storage.as_ref() else {
282            unreachable!("require_dtype(I32) guarantees Storage::I32")
283        };
284        Ok(self.logical_offsets().map(|i| data[i]).collect())
285    }
286
287    /// Returns a tensor equal to `self` but guaranteed contiguous, copying
288    /// only if `self` is not already contiguous (in which case it is a
289    /// cheap `Arc` clone).
290    ///
291    /// This is the shared implementation behind [`Tensor::reshape`] and
292    /// [`Tensor::concat`]: both need row-major-sequential data to work with
293    /// and neither cares whether that came for free or required a copy.
294    pub fn contiguous(&self) -> Result<Tensor> {
295        if self.is_contiguous() {
296            return Ok(self.clone());
297        }
298        self.require_elementwise()?;
299        let offsets: Vec<usize> = self.logical_offsets().collect();
300        let storage = match self.storage.as_ref() {
301            Storage::F32(d) => Storage::F32(offsets.iter().map(|&i| d[i]).collect()),
302            Storage::F16(d) => Storage::F16(offsets.iter().map(|&i| d[i]).collect()),
303            Storage::BF16(d) => Storage::BF16(offsets.iter().map(|&i| d[i]).collect()),
304            Storage::I8(d) => Storage::I8(offsets.iter().map(|&i| d[i]).collect()),
305            Storage::I32(d) => Storage::I32(offsets.iter().map(|&i| d[i]).collect()),
306            Storage::Quantized { .. } => unreachable!("require_elementwise excludes quantized"),
307        };
308        Ok(Self::new_contiguous(storage, self.shape.clone()))
309    }
310
311    // -- Dtype conversion -------------------------------------------------
312
313    /// Converts to `dtype`, decoding as needed.
314    ///
315    /// Converting to `dtype() == dtype` is a cheap `Arc` clone. Converting
316    /// *to* [`DType::F32`] is supported from every dtype this crate knows,
317    /// including every quantized format (this is the dequantization entry
318    /// point — see [`crate::quant`] for the block layouts) and `f16`/`bf16`
319    /// (see [`crate::half`]). Converting *from* `f32` to `f16` or `bf16` is
320    /// also supported, for storing activations compactly.
321    ///
322    /// Not supported: converting `f32` back to a quantized format.
323    /// Requantization is a model-export/training-time concern — it
324    /// requires choosing a quantization *scheme* (per-tensor vs per-block
325    /// scale, calibration, error-diffusion) — not a forward-pass inference
326    /// concern, which is this crate's whole scope. `i8`/`i32` casts to or
327    /// from `f32` are likewise not implemented: nothing in the ops this
328    /// crate provides needs them (token ids are consumed as `i32` directly
329    /// by [`Tensor::gather_rows`], never mixed arithmetically with `f32`).
330    pub fn to_dtype(&self, dtype: DType) -> Result<Tensor> {
331        if dtype == self.dtype() {
332            return Ok(self.clone());
333        }
334        match (self.storage.as_ref(), dtype) {
335            (Storage::Quantized { dtype: source, bytes }, DType::F32) => {
336                let data = quant::dequantize(*source, bytes)?;
337                Tensor::from_f32(data, self.shape.clone())
338            }
339            (Storage::F16(_), DType::F32) => {
340                let data: Vec<f32> = self.raw_f16().iter().copied().map(half::f16_to_f32).collect();
341                let data = self.reorder(&data);
342                Tensor::from_f32(data, self.shape.clone())
343            }
344            (Storage::BF16(_), DType::F32) => {
345                let data: Vec<f32> = self.raw_bf16().iter().copied().map(half::bf16_to_f32).collect();
346                let data = self.reorder(&data);
347                Tensor::from_f32(data, self.shape.clone())
348            }
349            (Storage::F32(d), DType::F16) => {
350                let data: Vec<u16> = self.logical_offsets().map(|i| half::f32_to_f16(d[i])).collect();
351                Tensor::from_f16(data, self.shape.clone())
352            }
353            (Storage::F32(d), DType::BF16) => {
354                let data: Vec<u16> = self.logical_offsets().map(|i| half::f32_to_bf16(d[i])).collect();
355                Tensor::from_bf16(data, self.shape.clone())
356            }
357            _ => Err(Error::UnsupportedDType { op: "to_dtype", dtype }),
358        }
359    }
360
361    fn raw_f16(&self) -> &[u16] {
362        match self.storage.as_ref() {
363            Storage::F16(d) => d,
364            _ => unreachable!(),
365        }
366    }
367
368    fn raw_bf16(&self) -> &[u16] {
369        match self.storage.as_ref() {
370            Storage::BF16(d) => d,
371            _ => unreachable!(),
372        }
373    }
374
375    /// Reorders an already-decoded, storage-order `Vec<f32>` (one entry per
376    /// raw storage element) into this view's logical row-major order.
377    fn reorder(&self, decoded: &[f32]) -> Vec<f32> {
378        self.logical_offsets().map(|i| decoded[i]).collect()
379    }
380
381    // -- Shape-only views (zero-copy) --------------------------------------
382
383    /// Returns a view with `dim0` and `dim1` swapped. Zero-copy: only the
384    /// shape and strides change.
385    pub fn transpose(&self, dim0: usize, dim1: usize) -> Result<Tensor> {
386        self.require_elementwise()?;
387        let rank = self.rank();
388        if dim0 >= rank {
389            return Err(Error::IndexOutOfBounds { dim: dim0, index: dim0, len: rank });
390        }
391        if dim1 >= rank {
392            return Err(Error::IndexOutOfBounds { dim: dim1, index: dim1, len: rank });
393        }
394        let mut dims = self.shape.dims().to_vec();
395        let mut strides = self.strides.clone();
396        dims.swap(dim0, dim1);
397        strides.swap(dim0, dim1);
398        Ok(Tensor {
399            storage: self.storage.clone(),
400            shape: Shape::new(dims),
401            strides,
402            offset: self.offset,
403            device: self.device,
404        })
405    }
406
407    /// Returns the sub-view `[start, start + len)` along `dim`. Zero-copy:
408    /// only the shape and offset change.
409    pub fn narrow(&self, dim: usize, start: usize, len: usize) -> Result<Tensor> {
410        self.require_elementwise()?;
411        let rank = self.rank();
412        if dim >= rank {
413            return Err(Error::IndexOutOfBounds { dim, index: dim, len: rank });
414        }
415        let dim_len = self.shape.dims()[dim];
416        if start + len > dim_len {
417            return Err(Error::IndexOutOfBounds { dim, index: start + len, len: dim_len });
418        }
419        let mut dims = self.shape.dims().to_vec();
420        dims[dim] = len;
421        Ok(Tensor {
422            storage: self.storage.clone(),
423            shape: Shape::new(dims),
424            strides: self.strides.clone(),
425            offset: self.offset + start * self.strides[dim],
426            device: self.device,
427        })
428    }
429
430    /// Returns a view broadcast to `shape`, following [`Shape::broadcast`]'s
431    /// NumPy right-aligned rule. Zero-copy: dimensions being stretched from
432    /// size 1 get stride 0, so every "broadcast" element reads the same
433    /// underlying value.
434    pub fn broadcast_to(&self, shape: impl Into<Shape>) -> Result<Tensor> {
435        self.require_elementwise()?;
436        let target = shape.into();
437        let result = self.shape.broadcast(&target)?;
438        if result != target {
439            return Err(Error::NotBroadcastable { left: self.shape.clone(), right: target });
440        }
441        let rank = target.rank();
442        let mut strides = vec![0usize; rank];
443        let offset_in_rank = rank - self.rank();
444        for i in 0..self.rank() {
445            if self.shape.dims()[i] != 1 {
446                strides[offset_in_rank + i] = self.strides[i];
447            }
448            // else: dim is being stretched from 1, so it keeps stride 0
449            // (already the vec's default).
450        }
451        Ok(Tensor {
452            storage: self.storage.clone(),
453            shape: target,
454            strides,
455            offset: self.offset,
456            device: self.device,
457        })
458    }
459
460    /// Reinterprets this tensor as `dims`, which must describe the same
461    /// number of elements. Zero-copy when `self` is already contiguous;
462    /// otherwise materializes a contiguous copy first (see
463    /// [`Tensor::contiguous`]).
464    pub fn reshape(&self, dims: impl Into<Vec<usize>>) -> Result<Tensor> {
465        let new_shape = self.shape.reshape(dims)?;
466        let base = self.contiguous()?;
467        Ok(Tensor {
468            storage: base.storage.clone(),
469            strides: new_shape.strides(),
470            shape: new_shape,
471            offset: base.offset,
472            device: base.device,
473        })
474    }
475}
476
477/// Checks that `actual_elems` matches what `shape` demands for `dtype`,
478/// producing an [`Error::StorageTooSmall`] (expressed in bytes, matching
479/// the error's own documented units) if not.
480fn check_len(shape: &Shape, dtype: DType, actual_elems: usize) -> Result<()> {
481    let expected_elems = shape.elem_count();
482    if actual_elems != expected_elems {
483        let expected = dtype.storage_bytes(expected_elems).unwrap_or(expected_elems * dtype.block_bytes());
484        let actual = dtype.storage_bytes(actual_elems).unwrap_or(actual_elems * dtype.block_bytes());
485        return Err(Error::StorageTooSmall {
486            shape: shape.clone(),
487            dtype,
488            expected,
489            actual,
490        });
491    }
492    Ok(())
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498
499    #[test]
500    fn from_f32_rejects_a_length_mismatch() {
501        let err = Tensor::from_f32(vec![1.0, 2.0], [3]).unwrap_err();
502        assert!(matches!(err, Error::StorageTooSmall { .. }));
503    }
504
505    #[test]
506    fn basic_accessors_report_shape_and_dtype() {
507        let t = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]).unwrap();
508        assert_eq!(t.shape().dims(), &[2, 3]);
509        assert_eq!(t.dtype(), DType::F32);
510        assert_eq!(t.rank(), 2);
511        assert_eq!(t.elem_count(), 6);
512        assert!(t.is_contiguous());
513    }
514
515    #[test]
516    fn clone_shares_storage_without_copying() {
517        let t = Tensor::from_f32(vec![1.0, 2.0, 3.0], [3]).unwrap();
518        let cloned = t.clone();
519        assert!(Arc::ptr_eq(&t.storage, &cloned.storage));
520    }
521
522    #[test]
523    fn transpose_is_zero_copy_and_produces_correct_logical_order() {
524        // [[1, 2, 3], [4, 5, 6]] -> transposed -> [[1, 4], [2, 5], [3, 6]]
525        let t = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]).unwrap();
526        let tt = t.transpose(0, 1).unwrap();
527        assert!(Arc::ptr_eq(&t.storage, &tt.storage), "transpose must not copy");
528        assert_eq!(tt.shape().dims(), &[3, 2]);
529        assert!(!tt.is_contiguous());
530        assert_eq!(tt.to_vec_f32().unwrap(), vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
531    }
532
533    #[test]
534    fn narrow_is_zero_copy_and_slices_the_requested_range() {
535        let t = Tensor::from_f32((0..12).map(|v| v as f32).collect(), [4, 3]).unwrap();
536        let n = t.narrow(0, 1, 2).unwrap();
537        assert!(Arc::ptr_eq(&t.storage, &n.storage), "narrow must not copy");
538        assert_eq!(n.shape().dims(), &[2, 3]);
539        assert_eq!(n.to_vec_f32().unwrap(), vec![3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
540    }
541
542    #[test]
543    fn narrow_out_of_range_is_rejected() {
544        let t = Tensor::from_f32(vec![1.0, 2.0, 3.0], [3]).unwrap();
545        assert!(matches!(t.narrow(0, 1, 5), Err(Error::IndexOutOfBounds { .. })));
546    }
547
548    #[test]
549    fn broadcast_to_is_zero_copy_and_repeats_the_stretched_dimension() {
550        let t = Tensor::from_f32(vec![1.0, 2.0, 3.0], [1, 3]).unwrap();
551        let b = t.broadcast_to([4, 3]).unwrap();
552        assert!(Arc::ptr_eq(&t.storage, &b.storage), "broadcast_to must not copy");
553        assert_eq!(b.strides()[0], 0);
554        assert_eq!(b.to_vec_f32().unwrap(), vec![
555            1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0,
556        ]);
557    }
558
559    #[test]
560    fn reshape_is_zero_copy_when_contiguous() {
561        let t = Tensor::from_f32((0..6).map(|v| v as f32).collect(), [2, 3]).unwrap();
562        let r = t.reshape([3, 2]).unwrap();
563        assert!(Arc::ptr_eq(&t.storage, &r.storage), "reshape of a contiguous tensor must not copy");
564        assert_eq!(r.to_vec_f32().unwrap(), vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]);
565    }
566
567    #[test]
568    fn reshape_after_transpose_materializes_a_copy_but_stays_correct() {
569        let t = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]).unwrap();
570        let r = t.transpose(0, 1).unwrap().reshape([6]).unwrap();
571        assert_eq!(r.to_vec_f32().unwrap(), vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
572    }
573
574    #[test]
575    fn reshape_rejects_an_element_count_mismatch() {
576        let t = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0], [2, 2]).unwrap();
577        assert!(matches!(t.reshape([3]), Err(Error::ShapeMismatch { .. })));
578    }
579
580    #[test]
581    fn quantized_tensors_reject_view_operations() {
582        let bytes = vec![0u8; 18]; // one Q4_0 block, all-zero
583        let t = Tensor::from_quantized(DType::Q4_0, bytes, [32]).unwrap();
584        assert!(matches!(t.narrow(0, 0, 1), Err(Error::QuantizedElementAccess { .. })));
585        assert!(matches!(t.transpose(0, 0), Err(Error::QuantizedElementAccess { .. })));
586        assert!(matches!(t.broadcast_to([2, 32]), Err(Error::QuantizedElementAccess { .. })));
587        assert!(matches!(t.to_vec_f32(), Err(Error::DTypeMismatch { .. })));
588    }
589
590    #[test]
591    fn to_dtype_identity_is_a_cheap_clone() {
592        let t = Tensor::from_f32(vec![1.0, 2.0], [2]).unwrap();
593        let same = t.to_dtype(DType::F32).unwrap();
594        assert!(Arc::ptr_eq(&t.storage, &same.storage));
595    }
596
597    #[test]
598    fn to_dtype_f16_round_trips_through_f32() {
599        let t = Tensor::from_f32(vec![1.0, 2.5, -3.0, 0.0], [4]).unwrap();
600        let as_f16 = t.to_dtype(DType::F16).unwrap();
601        assert_eq!(as_f16.dtype(), DType::F16);
602        let back = as_f16.to_dtype(DType::F32).unwrap();
603        assert_eq!(back.to_vec_f32().unwrap(), vec![1.0, 2.5, -3.0, 0.0]);
604    }
605
606    #[test]
607    fn to_dtype_bf16_round_trips_through_f32() {
608        let t = Tensor::from_f32(vec![1.0, 2.5, -3.0, 0.0], [4]).unwrap();
609        let as_bf16 = t.to_dtype(DType::BF16).unwrap();
610        let back = as_bf16.to_dtype(DType::F32).unwrap();
611        assert_eq!(back.to_vec_f32().unwrap(), vec![1.0, 2.5, -3.0, 0.0]);
612    }
613
614    #[test]
615    fn to_dtype_respects_a_transposed_views_logical_order() {
616        // Regression check for the f16/bf16 conversion path: it must read
617        // through `logical_offsets`, not raw storage order, or a converted
618        // transposed tensor would silently come out in the wrong order.
619        let t = Tensor::from_f16(
620            [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0].map(half::f32_to_f16).to_vec(),
621            [2, 3],
622        )
623        .unwrap();
624        let transposed = t.transpose(0, 1).unwrap();
625        let f32_transposed = transposed.to_dtype(DType::F32).unwrap();
626        assert_eq!(f32_transposed.to_vec_f32().unwrap(), vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
627    }
628
629    #[test]
630    fn to_dtype_unsupported_pair_is_rejected() {
631        let t = Tensor::from_i32(vec![1, 2, 3], [3]).unwrap();
632        assert!(matches!(t.to_dtype(DType::F32), Err(Error::UnsupportedDType { .. })));
633    }
634}