1mod 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#[derive(Debug, Clone)]
61pub struct Tensor {
62 pub(super) storage: Arc<Storage>,
63 pub(super) shape: Shape,
64 pub(super) strides: Vec<usize>,
66 pub(super) offset: usize,
68 pub(super) device: Device,
69}
70
71impl Tensor {
72 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 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 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 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 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 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 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 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 pub fn strides(&self) -> &[usize] {
182 &self.strides
183 }
184
185 pub fn is_contiguous(&self) -> bool {
191 self.strides == self.shape.strides()
192 }
193
194 pub(super) fn logical_offsets(&self) -> impl Iterator<Item = usize> + '_ {
204 let dims = self.shape.dims();
205 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 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 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 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 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 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 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 fn reorder(&self, decoded: &[f32]) -> Vec<f32> {
379 self.logical_offsets().map(|i| decoded[i]).collect()
380 }
381
382 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 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 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 }
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 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
478fn 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 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]; 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 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}