1mod 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#[derive(Debug, Clone)]
65pub struct Tensor {
66 pub(super) storage: Arc<Storage>,
67 pub(super) shape: Shape,
68 pub(super) strides: Vec<usize>,
70 pub(super) offset: usize,
72 pub(super) device: Device,
73}
74
75impl Tensor {
76 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 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 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 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 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 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 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 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 pub fn strides(&self) -> &[usize] {
186 &self.strides
187 }
188
189 pub fn is_contiguous(&self) -> bool {
195 self.strides == self.shape.strides()
196 }
197
198 pub(super) fn logical_offsets(&self) -> impl Iterator<Item = usize> + '_ {
208 let dims = self.shape.dims();
209 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 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 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 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 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 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 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 fn reorder(&self, decoded: &[f32]) -> Vec<f32> {
383 self.logical_offsets().map(|i| decoded[i]).collect()
384 }
385
386 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 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 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 }
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 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
482fn 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 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]; 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 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}