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