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