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