1use std::sync::Arc;
16use torsh_core::{
17 device::DeviceType,
18 dtype::{FloatElement, TensorElement},
19 error::{Result, TorshError},
20};
21
22use crate::{
23 core_ops::{Tensor, UnaryKind},
24 storage::TensorStorage,
25};
26
27const PARALLEL_SUM_THRESHOLD: usize = 65_536;
29
30#[cfg(feature = "simd")]
32const SIMD_SUM_THRESHOLD: usize = 1_024;
33
34fn sum_slice<T>(data: &[T]) -> T
37where
38 T: TensorElement + Copy + std::ops::Add<Output = T> + num_traits::Zero,
39{
40 let zero = <T as num_traits::Zero>::zero();
41
42 #[cfg(feature = "simd")]
43 {
44 if data.len() >= SIMD_SUM_THRESHOLD
45 && std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>()
46 {
47 use scirs2_core::simd_ops::SimdUnifiedOps;
48 let as_f32: &[f32] =
50 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, data.len()) };
51 let total =
52 <f32 as SimdUnifiedOps>::simd_sum(&scirs2_core::ndarray::ArrayView1::from(as_f32));
53 if let Some(value) = <T as TensorElement>::from_f64(f64::from(total)) {
56 return value;
57 }
58 }
59 }
60
61 #[cfg(feature = "parallel")]
62 {
63 if data.len() >= PARALLEL_SUM_THRESHOLD {
64 use scirs2_core::parallel_ops::*;
65 return data
66 .par_chunks(16_384)
67 .map(|chunk| chunk.iter().fold(zero, |acc, &x| acc + x))
68 .reduce(|| zero, |a, b| a + b);
69 }
70 }
71
72 data.iter().fold(zero, |acc, &x| acc + x)
73}
74
75impl<T: FloatElement + Copy> Tensor<T> {
77 pub fn scalar(value: T) -> Result<Self> {
79 Self::from_data(vec![value], vec![], DeviceType::Cpu)
80 }
81
82 pub fn as_ndarray(&self) -> Result<scirs2_core::ndarray::ArrayD<T>> {
84 use scirs2_core::ndarray::ArrayD;
85 let data = self.data()?;
86 let shape_obj = self.shape().clone();
87 let shape = shape_obj.dims();
88 ArrayD::from_shape_vec(shape, data.to_vec())
89 .map_err(|e| TorshError::InvalidShape(format!("ndarray conversion failed: {}", e)))
90 }
91
92 pub fn from_ndarray(
94 array: scirs2_core::ndarray::ArrayD<T>,
95 device: DeviceType,
96 ) -> Result<Self> {
97 let shape = array.shape().to_vec();
98 let (data, _offset) = array.into_raw_vec_and_offset();
99 Self::from_data(data, shape, device)
100 }
101}
102
103impl<T: TensorElement + Copy> Tensor<T>
105where
106 T: PartialEq + num_traits::Zero,
107{
108 pub fn all(&self) -> Result<Tensor<bool>> {
110 let data = self.to_vec()?;
111 let zero = <T as num_traits::Zero>::zero();
112 let all_true = data.iter().all(|&x| x != zero);
113 Tensor::from_data(vec![all_true], vec![], self.device())
114 }
115
116 pub fn any(&self) -> Result<Tensor<bool>> {
118 let data = self.to_vec()?;
119 let zero = <T as num_traits::Zero>::zero();
120 let any_true = data.iter().any(|&x| x != zero);
121 Tensor::from_data(vec![any_true], vec![], self.device())
122 }
123
124 pub fn all_dim(&self, dim: i32, keepdim: bool) -> Result<Tensor<bool>> {
126 let shape_binding = self.shape();
127 let input_shape = shape_binding.dims();
128
129 let normalized_dim = if dim < 0 {
130 (input_shape.len() as i32 + dim) as usize
131 } else {
132 dim as usize
133 };
134
135 if normalized_dim >= input_shape.len() {
136 return Err(torsh_core::error::TorshError::InvalidDimension {
137 dim: normalized_dim,
138 ndim: input_shape.len(),
139 });
140 }
141
142 let data = self.data()?;
143 let zero = <T as num_traits::Zero>::zero();
144
145 let outer_size: usize = input_shape[..normalized_dim].iter().product();
146 let dim_size = input_shape[normalized_dim];
147 let inner_size: usize = input_shape[normalized_dim + 1..].iter().product();
148
149 let output_size = outer_size * inner_size;
150 let mut result_data = vec![true; output_size];
151
152 for outer in 0..outer_size {
153 for inner in 0..inner_size {
154 let all_nonzero = (0..dim_size).all(|d| {
155 let idx = outer * dim_size * inner_size + d * inner_size + inner;
156 data[idx] != zero
157 });
158 let out_idx = outer * inner_size + inner;
159 result_data[out_idx] = all_nonzero;
160 }
161 }
162
163 let mut output_shape = input_shape.to_vec();
164 if keepdim {
165 output_shape[normalized_dim] = 1;
166 } else {
167 output_shape.remove(normalized_dim);
168 }
169
170 Tensor::<bool>::from_data(result_data, output_shape, self.device())
171 }
172
173 pub fn any_dim(&self, dim: i32, keepdim: bool) -> Result<Tensor<bool>> {
175 let shape_binding = self.shape();
176 let input_shape = shape_binding.dims();
177
178 let normalized_dim = if dim < 0 {
179 (input_shape.len() as i32 + dim) as usize
180 } else {
181 dim as usize
182 };
183
184 if normalized_dim >= input_shape.len() {
185 return Err(torsh_core::error::TorshError::InvalidDimension {
186 dim: normalized_dim,
187 ndim: input_shape.len(),
188 });
189 }
190
191 let data = self.data()?;
192 let zero = <T as num_traits::Zero>::zero();
193
194 let outer_size: usize = input_shape[..normalized_dim].iter().product();
195 let dim_size = input_shape[normalized_dim];
196 let inner_size: usize = input_shape[normalized_dim + 1..].iter().product();
197
198 let output_size = outer_size * inner_size;
199 let mut result_data = vec![false; output_size];
200
201 for outer in 0..outer_size {
202 for inner in 0..inner_size {
203 let any_nonzero = (0..dim_size).any(|d| {
204 let idx = outer * dim_size * inner_size + d * inner_size + inner;
205 data[idx] != zero
206 });
207 let out_idx = outer * inner_size + inner;
208 result_data[out_idx] = any_nonzero;
209 }
210 }
211
212 let mut output_shape = input_shape.to_vec();
213 if keepdim {
214 output_shape[normalized_dim] = 1;
215 } else {
216 output_shape.remove(normalized_dim);
217 }
218
219 Tensor::<bool>::from_data(result_data, output_shape, self.device())
220 }
221}
222
223impl<T: TensorElement + Copy> Tensor<T> {
225 pub fn sum(&self) -> Result<Self>
230 where
231 T: std::ops::Add<Output = T> + num_traits::Zero,
232 {
233 let sum_value = self.with_contiguous_data(|data| Ok(sum_slice(data)))?;
234 let mut result = Tensor::from_data(vec![sum_value], vec![], self.device())?;
235
236 if crate::should_record_grad(self.requires_grad) {
238 result.requires_grad = true;
239 result.operation = crate::core_ops::Operation::Sum {
240 input: Arc::new(self.clone()),
241 };
242 }
243
244 Ok(result)
245 }
246
247 pub fn min(&self) -> Result<Self>
250 where
251 T: std::cmp::PartialOrd + Copy,
252 {
253 let min_val = self.with_contiguous_data(|data| {
254 let mut iter = data.iter().copied();
255 let first = iter.next().ok_or_else(|| {
256 TorshError::InvalidOperation("Cannot compute min of empty tensor".to_string())
257 })?;
258 Ok(iter.fold(first, |acc, x| if x < acc { x } else { acc }))
259 })?;
260 Self::from_data(vec![min_val], vec![], self.device)
261 }
262
263 pub fn t(&self) -> Result<Self>
284 where
285 T: Copy + num_traits::Zero,
286 {
287 let shape = self.shape();
288 let dims = shape.dims();
289
290 if dims.len() != 2 {
291 return Err(TorshError::InvalidOperation(
292 "Transpose operation only supported for 2D tensors".to_string(),
293 ));
294 }
295
296 self.transpose(0, 1)
297 }
298
299 pub fn shares_storage(&self, other: &Self) -> bool {
301 match (&self.storage, &other.storage) {
303 (TensorStorage::InMemory(a), TensorStorage::InMemory(b)) => Arc::ptr_eq(a, b),
304 (TensorStorage::MemoryMapped(a), TensorStorage::MemoryMapped(b)) => Arc::ptr_eq(a, b),
305 #[cfg(feature = "gpu")]
309 (TensorStorage::Device { buffer: a, .. }, TensorStorage::Device { buffer: b, .. }) => {
310 Arc::ptr_eq(a, b)
311 }
312 _ => false,
313 }
314 }
315
316 pub fn data(&self) -> Result<Vec<T>>
318 where
319 T: Copy,
320 {
321 self.to_vec()
322 }
323
324 pub(crate) fn with_contiguous_data<R, F>(&self, f: F) -> Result<R>
330 where
331 F: FnOnce(&[T]) -> Result<R>,
332 T: Copy,
333 {
334 let numel = self.numel();
335 if self.is_view() || self.strides.is_some() || self.storage_offset != 0 {
336 let data = self.to_vec()?;
337 return f(&data);
338 }
339
340 self.storage.with_slice(|slice| {
341 if slice.len() < numel {
342 return Err(TorshError::InvalidOperation(format!(
343 "storage holds {} elements but the tensor shape needs {}",
344 slice.len(),
345 numel
346 )));
347 }
348 f(&slice[..numel])
349 })
350 }
351
352 pub(crate) fn materialize_contiguous(&mut self) -> Result<()>
363 where
364 T: Copy,
365 {
366 let data_vec = self.to_vec()?;
367 self.storage = Self::mutable_storage(data_vec)?;
368 self.strides = None;
369 self.storage_offset = 0;
370 self.base_tensor = None;
371 Ok(())
372 }
373
374 fn mutable_storage(data: Vec<T>) -> Result<TensorStorage<T>> {
376 #[cfg(feature = "simd")]
377 {
378 const MEMORY_MAPPING_BYTES: usize = 1024 * 1024 * 1024;
380 if data.len() * std::mem::size_of::<T>() < MEMORY_MAPPING_BYTES {
381 return TensorStorage::aligned(data);
382 }
383 }
384 TensorStorage::create_optimal(data)
385 }
386
387 pub fn data_mut_apply<F>(&mut self, mut func: F) -> Result<()>
389 where
390 F: FnMut(&mut T),
391 T: Copy,
392 {
393 self.prepare_for_inplace()?;
394
395 match &self.storage {
396 TensorStorage::MemoryMapped(_) => {
397 let mut new_data = self.to_vec()?;
399 for item in new_data.iter_mut() {
400 func(item);
401 }
402 self.storage = TensorStorage::create_optimal(new_data)?;
403 Ok(())
404 }
405 _ => {
406 let numel = self.numel();
407 self.storage.with_slice_mut(|slice| {
408 for item in slice.iter_mut().take(numel) {
409 func(item);
410 }
411 Ok(())
412 })
413 }
414 }
415 }
416
417 pub fn clone_data(&self) -> Self
419 where
420 T: Copy,
421 {
422 let data = self
423 .to_vec()
424 .expect("tensor to vec conversion should succeed");
425 Self::from_data(data, self.shape().dims().to_vec(), self.device)
426 .expect("tensor creation should succeed")
427 }
428
429 pub fn make_unique(&mut self) -> Result<()> {
437 if self.is_view() || self.strides.is_some() || self.storage_offset != 0 {
438 return self.materialize_contiguous();
439 }
440
441 match &self.storage {
442 TensorStorage::InMemory(data) => {
443 if Arc::strong_count(data) > 1 {
444 let data_vec = self.to_vec()?;
445 self.storage = Self::mutable_storage(data_vec)?;
446 }
447 }
448 TensorStorage::MemoryMapped(storage) => {
449 if Arc::strong_count(storage) > 1 {
450 let data_vec = self.to_vec()?;
451 self.storage = TensorStorage::create_optimal(data_vec)?;
452 }
453 }
454 #[cfg(feature = "simd")]
455 TensorStorage::Aligned(data) => {
456 if Arc::strong_count(data) > 1 {
457 let data_vec = self.to_vec()?;
458 self.storage = Self::mutable_storage(data_vec)?;
459 }
460 }
461 #[cfg(feature = "simd")]
462 TensorStorage::SimdOptimized(storage) => {
463 let promoted = storage.with_slice(TensorStorage::aligned_from_slice)?;
474 self.storage = promoted;
475 }
476 #[cfg(feature = "gpu")]
477 TensorStorage::Device { .. } => {
478 let data_vec = self.to_vec()?;
483 self.storage = Self::mutable_storage(data_vec)?;
484 }
485 }
486 Ok(())
487 }
488
489 pub(crate) fn prepare_for_inplace(&mut self) -> Result<()>
492 where
493 T: Copy,
494 {
495 self.make_unique()
496 }
497
498 pub fn apply_<F>(&mut self, func: F) -> Result<()>
503 where
504 F: Fn(T) -> T,
505 T: Copy,
506 {
507 self.prepare_for_inplace()?;
508
509 if matches!(self.storage, TensorStorage::MemoryMapped(_)) {
510 let data = self.to_vec()?;
512 let new_data: Vec<T> = data.into_iter().map(func).collect();
513 self.storage = TensorStorage::create_optimal(new_data)?;
514 return Ok(());
515 }
516
517 let numel = self.numel();
518 self.storage.with_slice_mut(|slice| {
519 for value in slice.iter_mut().take(numel) {
520 *value = func(*value);
521 }
522 Ok(())
523 })
524 }
525
526 pub fn map<F>(&self, func: F) -> Result<Self>
542 where
543 F: Fn(T) -> T,
544 T: Copy,
545 {
546 let new_data = self.with_contiguous_data(|data| {
547 let mut out = Vec::with_capacity(data.len());
548 out.extend(data.iter().map(|&x| func(x)));
549 Ok(out)
550 })?;
551 Self::from_data(new_data, self.shape().dims().to_vec(), self.device)
552 }
553
554 pub fn item(&self) -> Result<T>
556 where
557 T: Copy,
558 {
559 let data = self.data()?;
560 if data.len() != 1 {
561 return Err(TorshError::InvalidArgument(format!(
562 "item() can only be called on single-element tensors, got {} elements",
563 data.len()
564 )));
565 }
566 Ok(data[0])
567 }
568
569 pub fn cat(tensors: &[&Self], dim: i32) -> Result<Self>
571 where
572 T: Copy,
573 {
574 if tensors.is_empty() {
575 return Err(TorshError::InvalidArgument(
576 "Cannot concatenate empty tensor list".to_string(),
577 ));
578 }
579
580 let first_shape_binding = tensors[0].shape();
581 let first_shape = first_shape_binding.dims();
582 let ndim = first_shape.len();
583
584 let actual_dim = if dim < 0 {
586 (ndim as i32 + dim) as usize
587 } else {
588 dim as usize
589 };
590
591 if actual_dim >= ndim {
592 return Err(TorshError::InvalidArgument(format!(
593 "Dimension {} out of range for {}-dimensional tensor",
594 dim, ndim
595 )));
596 }
597
598 for (i, tensor) in tensors.iter().enumerate().skip(1) {
600 let shape_binding = tensor.shape();
601 let shape = shape_binding.dims();
602 if shape.len() != ndim {
603 return Err(TorshError::InvalidArgument(format!(
604 "Tensor {} has {} dimensions but first tensor has {}",
605 i,
606 shape.len(),
607 ndim
608 )));
609 }
610 for (d, (&s1, &s2)) in first_shape.iter().zip(shape.iter()).enumerate() {
611 if d != actual_dim && s1 != s2 {
612 return Err(TorshError::ShapeMismatch {
613 expected: first_shape.to_vec(),
614 got: shape.to_vec(),
615 });
616 }
617 }
618 }
619
620 let cat_dim_total: usize = tensors.iter().map(|t| t.shape().dims()[actual_dim]).sum();
622 let mut result_shape = first_shape.to_vec();
623 result_shape[actual_dim] = cat_dim_total;
624
625 let outer_size: usize = first_shape[..actual_dim].iter().product();
629 let inner_size: usize = first_shape[actual_dim + 1..].iter().product();
630
631 let total_numel: usize = result_shape.iter().product();
632 let mut result_data = Vec::with_capacity(total_numel);
633
634 let sources: Vec<Vec<T>> = tensors
637 .iter()
638 .map(|tensor| tensor.to_vec())
639 .collect::<Result<Vec<_>>>()?;
640 let cat_sizes: Vec<usize> = tensors
641 .iter()
642 .map(|tensor| tensor.shape().dims()[actual_dim])
643 .collect();
644
645 for outer in 0..outer_size {
646 for (source, &cat_size) in sources.iter().zip(cat_sizes.iter()) {
647 let run = cat_size * inner_size;
649 let start = outer * run;
650 result_data.extend_from_slice(&source[start..start + run]);
651 }
652 }
653
654 let mut result = Self::from_data(result_data, result_shape, tensors[0].device)?;
655
656 let any_requires_grad = tensors.iter().any(|t| t.requires_grad);
659 if crate::should_record_grad(any_requires_grad) {
660 result.requires_grad = true;
661 result.operation = crate::core_ops::Operation::Concat {
662 inputs: tensors.iter().map(|t| Arc::new((*t).clone())).collect(),
663 dim: actual_dim,
664 };
665 }
666
667 Ok(result)
668 }
669}
670
671impl<T: TensorElement + Copy> Tensor<T>
673where
674 T: num_traits::Float,
675{
676 pub fn norm(&self) -> Result<Self> {
678 let data = self.data()?;
679 let sum_squares: T = data
680 .iter()
681 .map(|&x| x * x)
682 .fold(num_traits::Zero::zero(), |acc, x| acc + x);
683 let norm_value = sum_squares.sqrt();
684
685 Tensor::from_data(vec![norm_value], vec![], self.device())
687 }
688
689 pub fn norm_lp(&self, p: f64, dims: Option<&[usize]>, keepdim: bool) -> Result<Self>
705 where
706 T: num_traits::FromPrimitive,
707 {
708 let shape_binding = self.shape();
709 let input_shape = shape_binding.dims().to_vec();
710 let ndim = input_shape.len();
711
712 let reduce_dims: Vec<usize> = match dims {
713 Some(requested) => {
714 for &dim in requested {
715 if dim >= ndim {
716 return Err(TorshError::InvalidOperation(format!(
717 "Dimension {} out of range for {}-dimensional tensor",
718 dim, ndim
719 )));
720 }
721 }
722 let mut normalized = requested.to_vec();
723 normalized.sort_unstable();
724 normalized.dedup();
725 normalized
726 }
727 None => (0..ndim).collect(),
728 };
729
730 let convert = |value: f64| -> Result<T> {
731 <T as num_traits::FromPrimitive>::from_f64(value).ok_or_else(|| {
732 TorshError::InvalidOperation(format!(
733 "norm: p={} cannot be represented in this tensor's element type",
734 p
735 ))
736 })
737 };
738
739 let zero = <T as num_traits::Zero>::zero();
740 let one = <T as num_traits::One>::one();
741
742 #[derive(Clone, Copy, PartialEq)]
743 enum NormKind {
744 L0,
745 L1,
746 L2,
747 MaxAbs,
748 MinAbs,
749 General,
750 }
751
752 let kind = if p == 1.0 {
753 NormKind::L1
754 } else if p == 2.0 {
755 NormKind::L2
756 } else if p == 0.0 {
757 NormKind::L0
758 } else if p == f64::INFINITY {
759 NormKind::MaxAbs
760 } else if p == f64::NEG_INFINITY {
761 NormKind::MinAbs
762 } else {
763 NormKind::General
764 };
765
766 let (p_t, inv_p_t) = if kind == NormKind::General {
767 (convert(p)?, convert(1.0 / p)?)
768 } else {
769 (zero, zero)
770 };
771
772 let init: T = if kind == NormKind::MinAbs {
774 <T as num_traits::Float>::infinity()
775 } else {
776 zero
777 };
778
779 let elem = |x: T| -> T {
780 match kind {
781 NormKind::L1 | NormKind::MaxAbs | NormKind::MinAbs => x.abs(),
782 NormKind::L2 => x * x,
783 NormKind::L0 => {
784 if x == zero {
785 zero
786 } else {
787 one
788 }
789 }
790 NormKind::General => x.abs().powf(p_t),
791 }
792 };
793
794 let combine = |a: T, b: T| -> T {
795 match kind {
796 NormKind::MaxAbs => a.max(b),
797 NormKind::MinAbs => a.min(b),
798 _ => a + b,
799 }
800 };
801
802 let finalize = |s: T| -> T {
803 match kind {
804 NormKind::L2 => s.sqrt(),
805 NormKind::General => s.powf(inv_p_t),
806 _ => s,
807 }
808 };
809
810 let data = self.data()?;
811
812 if ndim == 0 || reduce_dims.len() == ndim {
814 let acc = data.iter().fold(init, |acc, &x| combine(acc, elem(x)));
815 let value = finalize(acc);
816 let out_shape = if keepdim { vec![1; ndim] } else { vec![] };
817 return Self::from_data(vec![value], out_shape, self.device());
818 }
819
820 let mut is_reduced = vec![false; ndim];
824 for &d in &reduce_dims {
825 is_reduced[d] = true;
826 }
827
828 let mut input_strides = vec![1usize; ndim];
829 for i in (0..ndim - 1).rev() {
830 input_strides[i] = input_strides[i + 1] * input_shape[i + 1];
831 }
832
833 let mut output_shape_keepdim = input_shape.clone();
834 for &d in &reduce_dims {
835 output_shape_keepdim[d] = 1;
836 }
837 let mut output_strides = vec![1usize; ndim];
838 for i in (0..ndim - 1).rev() {
839 output_strides[i] = output_strides[i + 1] * output_shape_keepdim[i + 1];
840 }
841 let output_size: usize = output_shape_keepdim.iter().product();
842
843 let mut acc = vec![init; output_size];
844 for (flat_idx, &x) in data.iter().enumerate() {
845 let mut remaining = flat_idx;
846 let mut out_flat = 0usize;
847 for (dim, &reduced) in is_reduced.iter().enumerate() {
848 let coord = remaining / input_strides[dim];
849 remaining %= input_strides[dim];
850 if !reduced {
851 out_flat += coord * output_strides[dim];
852 }
853 }
854 acc[out_flat] = combine(acc[out_flat], elem(x));
855 }
856
857 let result_data: Vec<T> = acc.into_iter().map(finalize).collect();
858
859 let final_shape = if keepdim {
860 output_shape_keepdim
861 } else {
862 input_shape
863 .into_iter()
864 .zip(is_reduced.iter())
865 .filter(|(_, &reduced)| !reduced)
866 .map(|(size, _)| size)
867 .collect::<Vec<_>>()
868 };
869
870 Self::from_data(result_data, final_shape, self.device())
871 }
872}
873
874impl<T: TensorElement + Copy> Tensor<T> {
876 pub fn matmul_scirs2(&self, other: &Self) -> Result<Self>
878 where
879 T: num_traits::Float + num_traits::Zero + num_traits::One + std::iter::Sum,
880 {
881 self.basic_matmul(other)
884 }
885
886 pub fn sum_scirs2(&self) -> Result<Self>
888 where
889 T: std::ops::Add<Output = T> + num_traits::Zero,
890 {
891 let data = self.data()?;
893 let sum_value = data
894 .iter()
895 .fold(<T as num_traits::Zero>::zero(), |acc, &x| acc + x);
896 Tensor::from_data(vec![sum_value], vec![], self.device())
897 }
898
899 pub fn mean_scirs2(&self) -> Result<Self>
901 where
902 T: std::ops::Add<Output = T>
903 + std::ops::Div<Output = T>
904 + num_traits::Zero
905 + From<usize>
906 + num_traits::FromPrimitive,
907 {
908 let data = self.data()?;
910 if data.is_empty() {
911 return Err(TorshError::InvalidArgument(
912 "Cannot compute mean of empty tensor".to_string(),
913 ));
914 }
915 let sum_value = data
916 .iter()
917 .fold(<T as num_traits::Zero>::zero(), |acc, &x| acc + x);
918 let mean_value = sum_value / T::from(data.len());
919 Tensor::from_data(vec![mean_value], vec![], self.device())
920 }
921
922 pub fn relu_scirs2(&self) -> Result<Self>
929 where
930 T: PartialOrd + num_traits::Zero,
931 {
932 let zero = <T as num_traits::Zero>::zero();
934 let result = self.map(|x| if x > zero { x } else { zero })?;
935 Ok(self.record_unary(result, UnaryKind::Relu))
936 }
937
938 pub fn sigmoid_scirs2(&self) -> Result<Self>
944 where
945 T: num_traits::Float,
946 {
947 let result = self.map(|x| {
949 let one = <T as num_traits::One>::one();
950 one / (one + (-x).exp())
951 })?;
952 Ok(self.record_unary(result, UnaryKind::Sigmoid))
953 }
954
955 pub fn tanh_scirs2(&self) -> Result<Self>
959 where
960 T: num_traits::Float,
961 {
962 let result = self.map(|x| x.tanh())?;
964 Ok(self.record_unary(result, UnaryKind::Tanh))
965 }
966
967 pub fn softmax(&self, dim: i32) -> Result<Self>
970 where
971 T: torsh_core::dtype::FloatElement
972 + Copy
973 + std::ops::Sub<Output = T>
974 + std::ops::Div<Output = T>,
975 {
976 let data = self.data()?;
977 let shape_binding = self.shape();
978 let shape = shape_binding.dims();
979
980 if data.is_empty() || shape.is_empty() {
982 return Err(TorshError::InvalidOperation(
983 "Cannot compute softmax on empty tensor".to_string(),
984 ));
985 }
986
987 let actual_dim = if dim < 0 {
989 (shape.len() as i32 + dim) as usize
990 } else {
991 dim as usize
992 };
993
994 if actual_dim >= shape.len() {
995 return Err(TorshError::InvalidOperation(format!(
996 "Dimension {} out of range for {}-dimensional tensor",
997 actual_dim,
998 shape.len()
999 )));
1000 }
1001
1002 let max_tensor = self.max(Some(actual_dim), true)?;
1004
1005 let expanded_max = max_tensor.expand(shape)?;
1007 let shifted = self.sub(&expanded_max)?;
1008 let exp_tensor = shifted.exp()?;
1009 let sum_tensor = exp_tensor.sum_dim(&[actual_dim as i32], true)?;
1010
1011 let expanded_sum = sum_tensor.expand(shape)?;
1013 exp_tensor.div(&expanded_sum)
1014 }
1015
1016 pub fn log_softmax(&self, dim: i32) -> Result<Self>
1027 where
1028 T: torsh_core::dtype::FloatElement + Copy + std::ops::Sub<Output = T>,
1029 {
1030 let shape_binding = self.shape();
1031 let shape = shape_binding.dims().to_vec();
1032
1033 if shape.is_empty() {
1034 return Err(TorshError::InvalidOperation(
1035 "Cannot compute log_softmax on empty tensor".to_string(),
1036 ));
1037 }
1038
1039 let actual_dim = if dim < 0 {
1040 (shape.len() as i32 + dim) as usize
1041 } else {
1042 dim as usize
1043 };
1044
1045 if actual_dim >= shape.len() {
1046 return Err(TorshError::InvalidOperation(format!(
1047 "Dimension {} out of range for {}-dimensional tensor",
1048 actual_dim,
1049 shape.len()
1050 )));
1051 }
1052
1053 let max_tensor = self.max(Some(actual_dim), true)?;
1055 let expanded_max = max_tensor.expand(&shape)?;
1056 let shifted = self.sub(&expanded_max)?;
1057
1058 let sum_exp = shifted.exp()?.sum_dim(&[actual_dim as i32], true)?;
1060 let log_sum_exp = sum_exp.log()?;
1061 let expanded_log_sum = log_sum_exp.expand(&shape)?;
1062
1063 let mut result = shifted.sub(&expanded_log_sum)?;
1064
1065 if crate::should_record_grad(self.requires_grad) {
1070 result.requires_grad = true;
1071 result.operation = crate::core_ops::Operation::LogSoftmax {
1072 input: Arc::new(self.clone()),
1073 dim: actual_dim,
1074 };
1075 }
1076
1077 Ok(result)
1078 }
1079
1080 pub fn topk(
1082 &self,
1083 k: usize,
1084 dim: Option<i32>,
1085 largest: bool,
1086 sorted: bool,
1087 ) -> Result<(Self, Tensor<i64>)>
1088 where
1089 T: std::cmp::PartialOrd + Copy + num_traits::Zero,
1090 {
1091 let data = self.data()?;
1092 let shape_binding = self.shape();
1093 let shape = shape_binding.dims();
1094
1095 if shape.is_empty() {
1096 return Err(TorshError::InvalidOperation(
1097 "Cannot compute topk on empty tensor".to_string(),
1098 ));
1099 }
1100
1101 if k == 0 {
1102 return Err(TorshError::InvalidArgument(
1103 "k must be greater than 0".to_string(),
1104 ));
1105 }
1106
1107 let actual_dim = match dim {
1109 Some(d) => {
1110 let norm = if d < 0 {
1111 (shape.len() as i32 + d) as usize
1112 } else {
1113 d as usize
1114 };
1115 if norm >= shape.len() {
1116 return Err(TorshError::InvalidArgument(format!(
1117 "Dimension {} out of range for {}-dimensional tensor",
1118 d,
1119 shape.len()
1120 )));
1121 }
1122 norm
1123 }
1124 None => shape.len() - 1,
1125 };
1126
1127 let dim_size = shape[actual_dim];
1128 let effective_k = k.min(dim_size);
1129
1130 let outer_size: usize = shape[..actual_dim].iter().product();
1131 let inner_size: usize = shape[actual_dim + 1..].iter().product();
1132
1133 let mut result_shape = shape.to_vec();
1135 result_shape[actual_dim] = effective_k;
1136
1137 let mut values_data = Vec::with_capacity(outer_size * effective_k * inner_size);
1138 let mut indices_data = Vec::with_capacity(outer_size * effective_k * inner_size);
1139
1140 for outer in 0..outer_size {
1141 for inner in 0..inner_size {
1142 let mut slice: Vec<(usize, T)> = (0..dim_size)
1144 .map(|d| {
1145 let src = outer * dim_size * inner_size + d * inner_size + inner;
1146 (d, data[src])
1147 })
1148 .collect();
1149
1150 if largest {
1152 slice
1153 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1154 } else {
1155 slice
1156 .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1157 }
1158
1159 let mut top_k: Vec<(usize, T)> = slice.into_iter().take(effective_k).collect();
1160
1161 if !sorted {
1163 top_k.sort_by_key(|(idx, _)| *idx);
1164 }
1165
1166 for (local_idx, val) in &top_k {
1167 values_data.push(*val);
1168 indices_data.push(*local_idx as i64);
1169 }
1170 }
1171 }
1172
1173 let transposed_len = outer_size * effective_k * inner_size;
1177 let mut values_transposed = Vec::with_capacity(transposed_len);
1178 let mut indices_transposed = Vec::with_capacity(transposed_len);
1179
1180 for outer in 0..outer_size {
1181 for k_idx in 0..effective_k {
1182 for inner in 0..inner_size {
1183 let src = outer * inner_size * effective_k + inner * effective_k + k_idx;
1184 values_transposed.push(values_data[src]);
1185 indices_transposed.push(indices_data[src]);
1186 }
1187 }
1188 }
1189
1190 let values_tensor = Self::from_data(values_transposed, result_shape.clone(), self.device)?;
1191 let indices_tensor =
1192 Tensor::<i64>::from_data(indices_transposed, result_shape, self.device)?;
1193
1194 Ok((values_tensor, indices_tensor))
1195 }
1196}
1197
1198#[cfg(test)]
1199mod tests {
1200 use super::*;
1201 use torsh_core::device::DeviceType;
1202
1203 #[cfg(feature = "simd")]
1210 #[test]
1211 fn make_unique_isolates_shared_simd_storage() {
1212 let mut tensor = Tensor::from_data(vec![1.0f32; 4096], vec![4096], DeviceType::Cpu)
1214 .expect("tensor creation should succeed");
1215 assert!(
1216 matches!(tensor.storage, TensorStorage::SimdOptimized(_)),
1217 "precondition: a 16 KiB tensor uses lock-free SIMD storage"
1218 );
1219 let sibling = tensor.clone();
1220
1221 tensor.make_unique().expect("make_unique should succeed");
1222 assert!(
1223 !matches!(tensor.storage, TensorStorage::SimdOptimized(_)),
1224 "SimdOptimized storage must be promoted to single-buffer mutable storage"
1225 );
1226
1227 tensor.apply_(|value| value + 1.0).expect("apply_");
1228 assert_eq!(
1229 tensor.to_vec().expect("to_vec")[0],
1230 2.0,
1231 "the writer must see its own update"
1232 );
1233 assert_eq!(
1234 sibling.to_vec().expect("to_vec")[0],
1235 1.0,
1236 "the sibling must not observe the write"
1237 );
1238 }
1239
1240 #[cfg(feature = "simd")]
1243 #[test]
1244 fn make_unique_preserves_mutated_simd_contents() {
1245 let source = Tensor::from_data(
1246 (0..4096).map(|i| i as f32).collect::<Vec<_>>(),
1247 vec![4096],
1248 DeviceType::Cpu,
1249 )
1250 .expect("tensor creation should succeed");
1251
1252 source
1254 .storage
1255 .with_slice_mut(|slice| {
1256 slice[0] = -1.0;
1257 Ok(())
1258 })
1259 .expect("copy-on-write write should succeed");
1260
1261 let mut promoted = source.clone();
1262 promoted.make_unique().expect("make_unique should succeed");
1263 let data = promoted.to_vec().expect("to_vec");
1264 assert_eq!(data[0], -1.0, "the promotion must copy the CoW buffer");
1265 assert_eq!(data[4095], 4095.0);
1266 }
1267
1268 #[test]
1269 fn test_scalar_creation() {
1270 let scalar = Tensor::<f32>::scalar(42.0).expect("operation should succeed");
1271 assert_eq!(scalar.shape().dims(), &[] as &[usize]);
1272 assert_eq!(scalar.item().expect("item extraction should succeed"), 42.0);
1273 }
1274
1275 #[test]
1276 fn test_max_reduction() {
1277 let data = vec![1.0f32, 5.0, 3.0, 2.0];
1278 let tensor =
1279 Tensor::from_data(data, vec![4], DeviceType::Cpu).expect("operation should succeed");
1280 let max_val = tensor.max(None, false).expect("operation should succeed");
1281 assert_eq!(max_val.item().expect("item extraction should succeed"), 5.0);
1282 }
1283
1284 #[test]
1285 fn test_norm_computation() {
1286 let data = vec![3.0f32, 4.0]; let tensor =
1288 Tensor::from_data(data, vec![2], DeviceType::Cpu).expect("operation should succeed");
1289 let norm = tensor.norm().expect("norm computation should succeed");
1290 assert!((norm.item().expect("item extraction should succeed") - 5.0).abs() < 1e-6);
1291 }
1292
1293 #[test]
1294 fn test_apply_operations() {
1295 let data = vec![1.0f32, 2.0, 3.0, 4.0];
1296 let mut tensor =
1297 Tensor::from_data(data, vec![4], DeviceType::Cpu).expect("operation should succeed");
1298
1299 tensor
1301 .apply_(|x| x * 2.0)
1302 .expect("operation should succeed");
1303 assert_eq!(
1304 tensor.data().expect("data retrieval should succeed"),
1305 vec![2.0, 4.0, 6.0, 8.0]
1306 );
1307
1308 let original = Tensor::from_data(vec![1.0f32, 2.0, 3.0], vec![3], DeviceType::Cpu)
1310 .expect("operation should succeed");
1311 let mapped = original.map(|x| x + 1.0).expect("operation should succeed");
1312 assert_eq!(
1313 mapped.data().expect("data retrieval should succeed"),
1314 vec![2.0, 3.0, 4.0]
1315 );
1316 assert_eq!(
1317 original.data().expect("data retrieval should succeed"),
1318 vec![1.0, 2.0, 3.0]
1319 ); }
1321
1322 #[test]
1323 fn test_activation_functions() {
1324 let data = vec![-1.0f32, 0.0, 1.0, 2.0];
1325 let tensor =
1326 Tensor::from_data(data, vec![4], DeviceType::Cpu).expect("operation should succeed");
1327
1328 let relu_result = tensor.relu().expect("relu should succeed");
1330 assert_eq!(
1331 relu_result.data().expect("data retrieval should succeed"),
1332 vec![0.0, 0.0, 1.0, 2.0]
1333 );
1334
1335 let abs_result = tensor.abs().expect("abs computation should succeed");
1337 assert_eq!(
1338 abs_result.data().expect("data retrieval should succeed"),
1339 vec![1.0, 0.0, 1.0, 2.0]
1340 );
1341
1342 let clamped = tensor.clamp(-0.5, 1.5).expect("operation should succeed");
1344 assert_eq!(
1345 clamped.data().expect("data retrieval should succeed"),
1346 vec![-0.5, 0.0, 1.0, 1.5]
1347 );
1348 }
1349
1350 #[test]
1351 fn test_storage_sharing() {
1352 let tensor1 =
1353 Tensor::<f32>::zeros(&[2, 2], DeviceType::Cpu).expect("operation should succeed");
1354 let tensor2 = tensor1.clone();
1355 let tensor3 = tensor1.clone_data();
1356
1357 assert!(tensor1.shares_storage(&tensor2));
1358 assert!(!tensor1.shares_storage(&tensor3));
1359 }
1360
1361 #[test]
1362 fn test_basic_matmul() {
1363 let a = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
1364 .expect("operation should succeed");
1365 let b = Tensor::from_data(vec![5.0f32, 6.0, 7.0, 8.0], vec![2, 2], DeviceType::Cpu)
1366 .expect("operation should succeed");
1367
1368 let result = a.basic_matmul(&b).expect("operation should succeed");
1369 assert_eq!(result.shape().dims(), &[2, 2]);
1370
1371 let expected = vec![19.0, 22.0, 43.0, 50.0];
1374 assert_eq!(
1375 result.data().expect("data retrieval should succeed"),
1376 expected
1377 );
1378 }
1379
1380 #[test]
1381 fn test_reductions() {
1382 let data = vec![1.0f32, 2.0, 3.0, 4.0];
1383 let tensor =
1384 Tensor::from_data(data, vec![4], DeviceType::Cpu).expect("operation should succeed");
1385
1386 let sum = tensor.sum().expect("sum should succeed");
1387 assert_eq!(sum.item().expect("item extraction should succeed"), 10.0);
1388
1389 let mean = tensor.mean(None, false).expect("operation should succeed");
1390 assert_eq!(mean.item().expect("item extraction should succeed"), 2.5);
1391 }
1392
1393 #[test]
1394 fn test_copy_on_write() {
1395 let mut tensor1 =
1396 Tensor::<f32>::ones(&[2], DeviceType::Cpu).expect("operation should succeed");
1397 let tensor2 = tensor1.clone();
1398
1399 assert!(tensor1.shares_storage(&tensor2));
1401
1402 tensor1.make_unique().expect("make_unique should succeed");
1404 assert!(!tensor1.shares_storage(&tensor2));
1405 }
1406
1407 #[test]
1413 fn test_make_unique_does_not_realloc_unshared_storage() {
1414 let mut tensor = Tensor::<f32>::from_data(vec![1.0; 4096], vec![4096], DeviceType::Cpu)
1415 .expect("tensor creation should succeed");
1416 assert_eq!(
1417 tensor.storage_type(),
1418 "simd_optimized",
1419 "precondition: large f32 tensors start out immutable"
1420 );
1421
1422 tensor.make_unique().expect("make_unique should succeed");
1423 assert_eq!(tensor.storage_type(), "aligned_simd");
1424 let ptr_after_first = tensor
1425 .storage
1426 .with_slice(|s| Ok(s.as_ptr() as usize))
1427 .expect("with_slice should succeed");
1428
1429 tensor.make_unique().expect("make_unique should succeed");
1430 let ptr_after_second = tensor
1431 .storage
1432 .with_slice(|s| Ok(s.as_ptr() as usize))
1433 .expect("with_slice should succeed");
1434
1435 assert_eq!(
1436 ptr_after_first, ptr_after_second,
1437 "make_unique must not copy storage that is already unique and mutable"
1438 );
1439 }
1440
1441 #[test]
1446 fn test_make_unique_cow_copy_stays_mutable() {
1447 let mut tensor = Tensor::<f32>::from_data(vec![1.0; 4096], vec![4096], DeviceType::Cpu)
1448 .expect("tensor creation should succeed");
1449 tensor.make_unique().expect("make_unique should succeed");
1450 assert_eq!(tensor.storage_type(), "aligned_simd");
1451
1452 let shared = tensor.clone();
1453 tensor.make_unique().expect("make_unique should succeed");
1454 assert_eq!(
1455 tensor.storage_type(),
1456 "aligned_simd",
1457 "a copy-on-write split must not fall back to immutable storage"
1458 );
1459 tensor.apply_(|x| x + 1.0).expect("apply_ should succeed");
1460 assert_eq!(tensor.to_vec().expect("to_vec")[0], 2.0);
1461 assert_eq!(
1462 shared.to_vec().expect("to_vec")[0],
1463 1.0,
1464 "the shared tensor must be unaffected"
1465 );
1466 }
1467
1468 #[test]
1470 fn test_apply_mutates_storage_in_place() {
1471 let mut tensor = Tensor::<f32>::from_data(vec![2.0; 4096], vec![4096], DeviceType::Cpu)
1472 .expect("tensor creation should succeed");
1473 tensor.make_unique().expect("make_unique should succeed");
1474 let ptr_before = tensor
1475 .storage
1476 .with_slice(|s| Ok(s.as_ptr() as usize))
1477 .expect("with_slice should succeed");
1478
1479 tensor.apply_(|x| x * 3.0).expect("apply_ should succeed");
1480
1481 let ptr_after = tensor
1482 .storage
1483 .with_slice(|s| Ok(s.as_ptr() as usize))
1484 .expect("with_slice should succeed");
1485 assert_eq!(ptr_before, ptr_after, "apply_ must not reallocate storage");
1486 assert_eq!(tensor.to_vec().expect("to_vec")[0], 6.0);
1487 }
1488
1489 #[test]
1490 fn test_item_extraction() {
1491 let scalar = Tensor::from_data(vec![42.0f32], vec![], DeviceType::Cpu)
1492 .expect("operation should succeed");
1493 assert_eq!(scalar.item().expect("item extraction should succeed"), 42.0);
1494
1495 let vector = Tensor::from_data(vec![1.0f32, 2.0], vec![2], DeviceType::Cpu)
1496 .expect("operation should succeed");
1497 assert!(vector.item().is_err()); }
1499
1500 #[test]
1501 fn test_all_dim() {
1502 let data = vec![1i32, 0, 1, 1, 1, 1];
1504 let tensor = Tensor::from_data(data, vec![2, 3], DeviceType::Cpu)
1505 .expect("tensor creation should succeed");
1506
1507 let result = tensor.all_dim(0, false).expect("all_dim should succeed");
1510 assert_eq!(result.shape().dims(), &[3]);
1511 assert_eq!(
1512 result.to_vec().expect("to_vec should succeed"),
1513 vec![true, false, true]
1514 );
1515
1516 let result_row = tensor.all_dim(1, false).expect("all_dim should succeed");
1519 assert_eq!(result_row.shape().dims(), &[2]);
1520 assert_eq!(
1521 result_row.to_vec().expect("to_vec should succeed"),
1522 vec![false, true]
1523 );
1524
1525 let result_kd = tensor.all_dim(1, true).expect("all_dim should succeed");
1527 assert_eq!(result_kd.shape().dims(), &[2, 1]);
1528 }
1529
1530 #[test]
1531 fn test_any_dim() {
1532 let data = vec![0i32, 0, 0, 0, 1, 0];
1534 let tensor = Tensor::from_data(data, vec![2, 3], DeviceType::Cpu)
1535 .expect("tensor creation should succeed");
1536
1537 let result = tensor.any_dim(0, false).expect("any_dim should succeed");
1539 assert_eq!(result.shape().dims(), &[3]);
1540 assert_eq!(
1541 result.to_vec().expect("to_vec should succeed"),
1542 vec![false, true, false]
1543 );
1544
1545 let result_row = tensor.any_dim(1, false).expect("any_dim should succeed");
1547 assert_eq!(result_row.shape().dims(), &[2]);
1548 assert_eq!(
1549 result_row.to_vec().expect("to_vec should succeed"),
1550 vec![false, true]
1551 );
1552 }
1553
1554 #[test]
1555 fn test_cat_multidim() {
1556 let a = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
1558 .expect("tensor creation should succeed");
1559 let b = Tensor::from_data(vec![5.0f32, 6.0], vec![1, 2], DeviceType::Cpu)
1560 .expect("tensor creation should succeed");
1561
1562 let cat0 = Tensor::<f32>::cat(&[&a, &b], 0).expect("cat should succeed");
1563 assert_eq!(cat0.shape().dims(), &[3, 2]);
1564 assert_eq!(
1565 cat0.to_vec().expect("to_vec should succeed"),
1566 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
1567 );
1568
1569 let c = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
1571 .expect("tensor creation should succeed");
1572 let d = Tensor::from_data(vec![5.0f32, 6.0, 7.0, 8.0], vec![2, 2], DeviceType::Cpu)
1573 .expect("tensor creation should succeed");
1574
1575 let cat1 = Tensor::<f32>::cat(&[&c, &d], 1).expect("cat should succeed");
1576 assert_eq!(cat1.shape().dims(), &[2, 4]);
1577 assert_eq!(
1578 cat1.to_vec().expect("to_vec should succeed"),
1579 vec![1.0, 2.0, 5.0, 6.0, 3.0, 4.0, 7.0, 8.0]
1580 );
1581 }
1582
1583 #[test]
1584 fn test_topk_along_dim() {
1585 let data = vec![3.0f32, 1.0, 4.0, 2.0, 5.0, 9.0, 2.0, 6.0];
1587 let tensor = Tensor::from_data(data, vec![2, 4], DeviceType::Cpu)
1588 .expect("tensor creation should succeed");
1589
1590 let (vals, idxs) = tensor
1591 .topk(2, Some(1), true, true)
1592 .expect("topk should succeed");
1593 assert_eq!(vals.shape().dims(), &[2, 2]);
1594 assert_eq!(idxs.shape().dims(), &[2, 2]);
1595
1596 let vals_data = vals.to_vec().expect("to_vec should succeed");
1599 let idxs_data = idxs.to_vec().expect("to_vec should succeed");
1600 assert_eq!(vals_data[0], 4.0);
1601 assert_eq!(vals_data[1], 3.0);
1602 assert_eq!(vals_data[2], 9.0);
1603 assert_eq!(vals_data[3], 6.0);
1604 assert_eq!(idxs_data[0], 2);
1605 assert_eq!(idxs_data[1], 0);
1606 assert_eq!(idxs_data[2], 1);
1607 assert_eq!(idxs_data[3], 3);
1608 }
1609
1610 #[test]
1613 fn test_issue_43_mean_propagates_requires_grad() {
1614 let input = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![4], DeviceType::Cpu)
1616 .expect("tensor creation failed")
1617 .requires_grad_(true);
1618
1619 let result = input.mean(None, false).expect("mean should succeed");
1620 assert!(
1621 result.requires_grad(),
1622 "mean result must have requires_grad=true when input does"
1623 );
1624 }
1625
1626 #[test]
1627 fn test_issue_43_mean_no_requires_grad_when_input_has_none() {
1628 let input = Tensor::from_data(vec![1.0f32, 2.0, 3.0], vec![3], DeviceType::Cpu)
1630 .expect("tensor creation failed");
1631
1632 let result = input.mean(None, false).expect("mean should succeed");
1633 assert!(
1634 !result.requires_grad(),
1635 "mean result must not require grad when input does not"
1636 );
1637 }
1638
1639 #[test]
1640 fn test_issue_43_mean_backward() {
1641 let n = 4usize;
1644 let input = Tensor::from_data(vec![2.0f32, 4.0, 6.0, 8.0], vec![n], DeviceType::Cpu)
1645 .expect("tensor creation failed")
1646 .requires_grad_(true);
1647
1648 let result = input.mean(None, false).expect("mean should succeed");
1649 assert!(result.requires_grad(), "mean result must track gradients");
1650 result.backward().expect("backward should succeed");
1652
1653 let grad = input
1654 .grad()
1655 .expect("input must have gradient after backward");
1656 let grad_data = grad.data().expect("gradient data");
1657
1658 let expected = 1.0f32 / n as f32;
1660 for &g in &grad_data {
1661 assert!(
1662 (g - expected).abs() < 1e-6,
1663 "each element grad should be 1/n={expected}, got {g}"
1664 );
1665 }
1666 }
1667
1668 #[test]
1669 fn test_sum_backward() {
1670 let x = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![4], DeviceType::Cpu)
1672 .expect("tensor creation failed")
1673 .requires_grad_(true);
1674 let loss = x.sum().expect("sum should succeed");
1675 assert!(loss.requires_grad(), "sum result must track gradients");
1676 loss.backward().expect("backward should succeed");
1677 let grad = x.grad().expect("x must have a gradient after backward");
1678 let grad_data = grad.data().expect("gradient data");
1679 assert_eq!(
1680 grad_data,
1681 vec![1.0f32, 1.0, 1.0, 1.0],
1682 "d(sum)/dx must be all ones"
1683 );
1684 }
1685
1686 #[test]
1687 fn test_matmul_backward() {
1688 let a = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
1693 .expect("tensor creation failed")
1694 .requires_grad_(true);
1695 let b = Tensor::from_data(vec![5.0f32, 6.0, 7.0, 8.0], vec![2, 2], DeviceType::Cpu)
1696 .expect("tensor creation failed")
1697 .requires_grad_(true);
1698
1699 let c = a.matmul(&b).expect("matmul should succeed");
1700 assert!(c.requires_grad(), "matmul result must track gradients");
1701 let loss = c.sum().expect("sum should succeed");
1702 loss.backward().expect("backward should succeed");
1703
1704 let grad_a = a
1705 .grad()
1706 .expect("A must have a gradient")
1707 .data()
1708 .expect("grad data");
1709 let grad_b = b
1710 .grad()
1711 .expect("B must have a gradient")
1712 .data()
1713 .expect("grad data");
1714 assert_eq!(
1715 grad_a,
1716 vec![11.0f32, 15.0, 11.0, 15.0],
1717 "grad_A = ones @ Bᵀ"
1718 );
1719 assert_eq!(grad_b, vec![4.0f32, 4.0, 6.0, 6.0], "grad_B = Aᵀ @ ones");
1720 }
1721}