1use torsh_core::{
19 dtype::{FloatElement, TensorElement},
20 error::{Result, TorshError},
21};
22
23use crate::core_ops::Tensor;
24
25pub(crate) fn normalize_dim(dim: i32, ndim: usize) -> Result<usize> {
30 let resolved = if dim < 0 {
31 i64::from(dim) + ndim as i64
32 } else {
33 i64::from(dim)
34 };
35
36 if resolved < 0 || resolved as usize >= ndim {
37 return Err(TorshError::InvalidOperation(format!(
38 "Dimension {} out of range for {}-dimensional tensor",
39 dim, ndim
40 )));
41 }
42
43 Ok(resolved as usize)
44}
45
46pub(crate) fn split_extents(shape: &[usize], dim: usize) -> (usize, usize, usize) {
48 let outer: usize = shape[..dim].iter().product();
49 let dim_size = shape[dim];
50 let inner: usize = shape[dim + 1..].iter().product();
51 (outer, dim_size, inner)
52}
53
54pub(crate) fn reduced_shape(shape: &[usize], dim: usize, keepdim: bool) -> Vec<usize> {
56 let mut out = shape.to_vec();
57 if keepdim {
58 out[dim] = 1;
59 } else {
60 out.remove(dim);
61 }
62 out
63}
64
65fn extremum_values<T: Copy>(
68 data: &[T],
69 outer: usize,
70 dim_size: usize,
71 inner: usize,
72 prefer_new: impl Fn(T, T) -> bool,
73) -> Vec<T> {
74 let mut result = Vec::with_capacity(outer * inner);
75 for o in 0..outer {
76 for n in 0..inner {
77 let base = o * dim_size * inner + n;
78 let mut best = data[base];
79 for d in 1..dim_size {
80 let value = data[base + d * inner];
81 if prefer_new(value, best) {
82 best = value;
83 }
84 }
85 result.push(best);
86 }
87 }
88 result
89}
90
91fn extremum_indices<T: Copy>(
94 data: &[T],
95 outer: usize,
96 dim_size: usize,
97 inner: usize,
98 prefer_new: impl Fn(T, T) -> bool,
99) -> Vec<i64> {
100 let mut result = Vec::with_capacity(outer * inner);
101 for o in 0..outer {
102 for n in 0..inner {
103 let base = o * dim_size * inner + n;
104 let mut best = data[base];
105 let mut best_idx = 0i64;
106 for d in 1..dim_size {
107 let value = data[base + d * inner];
108 if prefer_new(value, best) {
109 best = value;
110 best_idx = d as i64;
111 }
112 }
113 result.push(best_idx);
114 }
115 }
116 result
117}
118
119impl<T: FloatElement + Copy> Tensor<T> {
124 pub fn max(&self, dim: Option<usize>, keepdim: bool) -> Result<Self> {
129 match dim {
130 None => {
131 let max_val =
132 self.with_contiguous_data(|data| global_extremum(data, |a, b| a > b, "max"))?;
133 if keepdim {
134 let shape = vec![1; self.shape().dims().len()];
135 Self::from_data(vec![max_val], shape, self.device())
136 } else {
137 Self::scalar(max_val)
138 }
139 }
140 Some(axis) => self.extremum_along_dim(axis, keepdim, true),
141 }
142 }
143
144 pub fn max_dim(&self, dim: i32, keepdim: bool) -> Result<Self> {
146 let axis = normalize_dim(dim, self.shape().dims().len())?;
147 self.extremum_along_dim(axis, keepdim, true)
148 }
149
150 pub fn min_dim(&self, dim: i32, keepdim: bool) -> Result<Self> {
152 let axis = normalize_dim(dim, self.shape().dims().len())?;
153 self.extremum_along_dim(axis, keepdim, false)
154 }
155
156 pub fn amin(&self, dim: Option<usize>, keepdim: bool) -> Result<Self> {
161 match dim {
162 None => {
163 let min_val =
164 self.with_contiguous_data(|data| global_extremum(data, |a, b| a < b, "min"))?;
165 if keepdim {
166 let shape = vec![1; self.shape().dims().len()];
167 Self::from_data(vec![min_val], shape, self.device())
168 } else {
169 Self::scalar(min_val)
170 }
171 }
172 Some(axis) => self.extremum_along_dim(axis, keepdim, false),
173 }
174 }
175
176 pub fn amax(&self, dim: Option<usize>, keepdim: bool) -> Result<Self> {
181 self.max(dim, keepdim)
182 }
183
184 fn extremum_along_dim(&self, axis: usize, keepdim: bool, want_max: bool) -> Result<Self> {
186 let shape_binding = self.shape();
187 let input_shape = shape_binding.dims();
188
189 if axis >= input_shape.len() {
190 return Err(TorshError::InvalidOperation(format!(
191 "Axis {} out of bounds for {}-dimensional tensor",
192 axis,
193 input_shape.len()
194 )));
195 }
196
197 let (outer, dim_size, inner) = split_extents(input_shape, axis);
198 if dim_size == 0 {
199 return Err(TorshError::InvalidOperation(
200 "Cannot reduce a dimension with zero size".to_string(),
201 ));
202 }
203
204 let output_shape = reduced_shape(input_shape, axis, keepdim);
205
206 let kind = if want_max {
209 ReduceKind::Max
210 } else {
211 ReduceKind::Min
212 };
213 if let Some(result) = self.try_device_reduce(kind, axis, &output_shape) {
214 return Ok(result);
215 }
216
217 let result_data = self.with_contiguous_data(|data| {
218 Ok(if want_max {
219 extremum_values(data, outer, dim_size, inner, |new, best| new > best)
220 } else {
221 extremum_values(data, outer, dim_size, inner, |new, best| new < best)
222 })
223 })?;
224
225 Self::from_data(result_data, output_shape, self.device())
226 }
227}
228
229fn global_extremum<T: Copy>(data: &[T], prefer_new: impl Fn(T, T) -> bool, op: &str) -> Result<T> {
231 let mut iter = data.iter().copied();
232 let first = iter.next().ok_or_else(|| {
233 TorshError::InvalidOperation(format!("Cannot compute {} of empty tensor", op))
234 })?;
235 Ok(iter.fold(first, |best, x| if prefer_new(x, best) { x } else { best }))
236}
237
238#[derive(Clone, Copy)]
248enum ReduceKind {
249 Sum,
250 Max,
251 Min,
252}
253
254impl<T: TensorElement + Copy> Tensor<T> {
255 fn try_device_reduce(
270 &self,
271 kind: ReduceKind,
272 axis: usize,
273 output_shape: &[usize],
274 ) -> Option<Self> {
275 #[cfg(feature = "gpu")]
276 {
277 let op = match kind {
278 ReduceKind::Sum => crate::gpu_dispatch::ReduceOp::Sum,
279 ReduceKind::Max => crate::gpu_dispatch::ReduceOp::Max,
280 ReduceKind::Min => crate::gpu_dispatch::ReduceOp::Min,
281 };
282 crate::gpu_dispatch::try_reduce_axis_f32(self, op, axis, output_shape)
283 }
284 #[cfg(not(feature = "gpu"))]
285 {
286 let _ = (kind, axis, output_shape);
287 None
288 }
289 }
290
291 fn record_sum_dim(&self, result: &mut Self, reduce_dims: &[usize], keepdim: bool) {
299 if crate::should_record_grad(self.requires_grad) {
300 result.requires_grad = true;
301 result.operation = crate::core_ops::Operation::SumDim {
302 input: std::sync::Arc::new(self.clone()),
303 dims: reduce_dims.to_vec(),
304 keepdim,
305 };
306 }
307 }
308
309 pub fn sum_dim(&self, dims: &[i32], keepdim: bool) -> Result<Self>
315 where
316 T: std::ops::Add<Output = T> + num_traits::Zero,
317 {
318 if dims.is_empty() {
319 return self.sum();
320 }
321
322 let shape_binding = self.shape();
323 let input_shape = shape_binding.dims().to_vec();
324 let ndim = input_shape.len();
325
326 let mut reduce_dims = Vec::with_capacity(dims.len());
327 for &dim in dims {
328 reduce_dims.push(normalize_dim(dim, ndim)?);
329 }
330 reduce_dims.sort_unstable();
331 reduce_dims.dedup();
332
333 if reduce_dims.len() == 1 {
335 let axis = reduce_dims[0];
336 let (outer, dim_size, inner) = split_extents(&input_shape, axis);
337 let output_shape = reduced_shape(&input_shape, axis, keepdim);
338
339 let device_result = self.try_device_reduce(ReduceKind::Sum, axis, &output_shape);
342
343 let mut result = match device_result {
344 Some(result) => result,
345 None => {
346 let result_data = self.with_contiguous_data(|data| {
347 let mut result = vec![<T as num_traits::Zero>::zero(); outer * inner];
348 for o in 0..outer {
349 for n in 0..inner {
350 let base = o * dim_size * inner + n;
351 let mut acc = <T as num_traits::Zero>::zero();
352 for d in 0..dim_size {
353 acc = acc + data[base + d * inner];
354 }
355 result[o * inner + n] = acc;
356 }
357 }
358 Ok(result)
359 })?;
360 Self::from_data(result_data, output_shape, self.device())?
361 }
362 };
363 self.record_sum_dim(&mut result, &reduce_dims, keepdim);
366 return Ok(result);
367 }
368
369 let mut is_reduced = vec![false; ndim];
372 for &d in &reduce_dims {
373 is_reduced[d] = true;
374 }
375
376 let input_strides = contiguous_strides(&input_shape);
377 let mut output_shape_keepdim = input_shape.clone();
378 for &d in &reduce_dims {
379 output_shape_keepdim[d] = 1;
380 }
381 let output_strides = contiguous_strides(&output_shape_keepdim);
382 let output_size: usize = output_shape_keepdim.iter().product();
383
384 let result_data = self.with_contiguous_data(|data| {
385 let mut acc = vec![<T as num_traits::Zero>::zero(); output_size];
386 for (flat_idx, &value) in data.iter().enumerate() {
387 let mut remaining = flat_idx;
388 let mut out_flat = 0usize;
389 for (dim, &reduced) in is_reduced.iter().enumerate() {
390 let coord = remaining / input_strides[dim];
391 remaining %= input_strides[dim];
392 if !reduced {
393 out_flat += coord * output_strides[dim];
394 }
395 }
396 acc[out_flat] = acc[out_flat] + value;
397 }
398 Ok(acc)
399 })?;
400
401 let final_shape = if keepdim {
402 output_shape_keepdim
403 } else {
404 input_shape
405 .into_iter()
406 .zip(is_reduced.iter())
407 .filter(|(_, &reduced)| !reduced)
408 .map(|(size, _)| size)
409 .collect::<Vec<_>>()
410 };
411
412 let mut result = Self::from_data(result_data, final_shape, self.device())?;
413 self.record_sum_dim(&mut result, &reduce_dims, keepdim);
414 Ok(result)
415 }
416
417 pub fn mean(&self, dims: Option<&[usize]>, keepdim: bool) -> Result<Self>
422 where
423 T: std::ops::Add<Output = T>
424 + std::ops::Div<Output = T>
425 + num_traits::Zero
426 + num_traits::One
427 + num_traits::FromPrimitive,
428 {
429 let ndim = self.shape().ndim();
430
431 let reduce_dims: Option<Vec<usize>> = match dims {
433 Some(requested) => {
434 for &dim in requested {
435 if dim >= ndim {
436 return Err(TorshError::InvalidOperation(format!(
437 "Dimension {} out of range for {}-dimensional tensor",
438 dim, ndim
439 )));
440 }
441 }
442 let mut normalized = requested.to_vec();
443 normalized.sort_unstable();
444 normalized.dedup();
445 Some(normalized)
446 }
447 None => None,
448 };
449
450 let sum = match &reduce_dims {
451 Some(reduce) => {
452 let as_i32: Vec<i32> = reduce.iter().map(|&d| d as i32).collect();
453 self.sum_dim(&as_i32, keepdim)?
454 }
455 None => {
456 let scalar_sum = self.sum()?;
457 if keepdim {
458 let keepdim_shape = vec![1; ndim];
459 scalar_sum.view(&keepdim_shape)?
460 } else {
461 scalar_sum
462 }
463 }
464 };
465
466 let count = match &reduce_dims {
467 Some(reduce) => {
468 let dims_binding = self.shape();
469 let shape = dims_binding.dims();
470 reduce.iter().map(|&d| shape[d]).product::<usize>() as f64
471 }
472 None => self.numel() as f64,
473 };
474
475 let result = sum.div_scalar(
476 <T as num_traits::FromPrimitive>::from_f64(count)
477 .unwrap_or_else(|| <T as num_traits::One>::one()),
478 )?;
479
480 Ok(result)
481 }
482
483 pub fn cumsum(&self, dim: i32) -> Result<Self>
489 where
490 T: std::ops::Add<Output = T> + num_traits::Zero + Copy,
491 {
492 let shape_binding = self.shape();
493 let shape = shape_binding.dims().to_vec();
494 let axis = normalize_dim(dim, shape.len())?;
495 let (outer, dim_size, inner) = split_extents(&shape, axis);
496
497 let mut result_data = self.data()?;
498 for o in 0..outer {
499 for n in 0..inner {
500 let base = o * dim_size * inner + n;
501 let mut acc = <T as num_traits::Zero>::zero();
502 for d in 0..dim_size {
503 let idx = base + d * inner;
504 acc = acc + result_data[idx];
505 result_data[idx] = acc;
506 }
507 }
508 }
509
510 Self::from_data(result_data, shape, self.device())
511 }
512
513 pub fn cumprod(&self, dim: i32) -> Result<Self>
515 where
516 T: std::ops::Mul<Output = T> + num_traits::One + Copy,
517 {
518 let shape_binding = self.shape();
519 let shape = shape_binding.dims().to_vec();
520 let axis = normalize_dim(dim, shape.len())?;
521 let (outer, dim_size, inner) = split_extents(&shape, axis);
522
523 let mut result_data = self.data()?;
525 for o in 0..outer {
526 for n in 0..inner {
527 let base = o * dim_size * inner + n;
528 let mut acc = <T as num_traits::One>::one();
529 for d in 0..dim_size {
530 let idx = base + d * inner;
531 acc = acc * result_data[idx];
532 result_data[idx] = acc;
533 }
534 }
535 }
536
537 Self::from_data(result_data, shape, self.device())
538 }
539
540 pub fn sort(&self, dim: Option<i32>, descending: bool) -> Result<(Self, Tensor<i64>)>
546 where
547 T: PartialOrd + Copy,
548 {
549 let shape_binding = self.shape();
550 let shape = shape_binding.dims().to_vec();
551
552 if shape.is_empty() {
553 let values = Self::from_data(self.data()?, shape.clone(), self.device())?;
555 let indices = Tensor::<i64>::from_data(vec![0], shape, self.device())?;
556 return Ok((values, indices));
557 }
558
559 let axis = match dim {
560 Some(d) => normalize_dim(d, shape.len())?,
561 None => shape.len() - 1,
562 };
563 let (outer, dim_size, inner) = split_extents(&shape, axis);
564
565 let data = self.data()?;
566 if data.is_empty() {
567 let values = Self::from_data(Vec::new(), shape.clone(), self.device())?;
568 let indices = Tensor::<i64>::from_data(Vec::new(), shape, self.device())?;
569 return Ok((values, indices));
570 }
571
572 let mut sorted_values = vec![data[0]; data.len()];
573 let mut sorted_indices = vec![0i64; data.len()];
574 let mut slice: Vec<(usize, T)> = Vec::with_capacity(dim_size);
575
576 for o in 0..outer {
577 for n in 0..inner {
578 let base = o * dim_size * inner + n;
579 slice.clear();
580 slice.extend((0..dim_size).map(|d| (d, data[base + d * inner])));
581
582 if descending {
583 slice
584 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
585 } else {
586 slice
587 .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
588 }
589
590 for (d, (src_idx, value)) in slice.iter().enumerate() {
591 let dst = base + d * inner;
592 sorted_values[dst] = *value;
593 sorted_indices[dst] = *src_idx as i64;
594 }
595 }
596 }
597
598 let values = Self::from_data(sorted_values, shape.clone(), self.device())?;
599 let indices = Tensor::<i64>::from_data(sorted_indices, shape, self.device())?;
600 Ok((values, indices))
601 }
602
603 pub fn argmin(&self, dim: Option<i32>) -> Result<Tensor<i64>>
609 where
610 T: PartialOrd + Copy,
611 {
612 match dim {
613 Some(d) => self.arg_extremum_dim(d, false, false),
614 None => self.arg_extremum_global(false),
615 }
616 }
617
618 pub fn argmax(&self, dim: Option<i32>) -> Result<Tensor<i64>>
620 where
621 T: PartialOrd + Copy,
622 {
623 match dim {
624 Some(d) => self.arg_extremum_dim(d, true, false),
625 None => self.arg_extremum_global(true),
626 }
627 }
628
629 pub fn argmin_dim(&self, dim: i32, keepdim: bool) -> Result<Tensor<i64>>
631 where
632 T: PartialOrd + Copy,
633 {
634 self.arg_extremum_dim(dim, false, keepdim)
635 }
636
637 pub fn argmax_dim(&self, dim: i32, keepdim: bool) -> Result<Tensor<i64>>
639 where
640 T: PartialOrd + Copy,
641 {
642 self.arg_extremum_dim(dim, true, keepdim)
643 }
644
645 fn arg_extremum_dim(&self, dim: i32, want_max: bool, keepdim: bool) -> Result<Tensor<i64>>
646 where
647 T: PartialOrd + Copy,
648 {
649 let shape_binding = self.shape();
650 let shape = shape_binding.dims().to_vec();
651 let axis = normalize_dim(dim, shape.len())?;
652 let (outer, dim_size, inner) = split_extents(&shape, axis);
653
654 if dim_size == 0 {
655 return Err(TorshError::InvalidOperation(
656 "Cannot compute arg-reduction over a zero-sized dimension".to_string(),
657 ));
658 }
659
660 let result_data = self.with_contiguous_data(|data| {
661 Ok(if want_max {
662 extremum_indices(data, outer, dim_size, inner, |new, best| new > best)
663 } else {
664 extremum_indices(data, outer, dim_size, inner, |new, best| new < best)
665 })
666 })?;
667
668 Tensor::<i64>::from_data(
669 result_data,
670 reduced_shape(&shape, axis, keepdim),
671 self.device(),
672 )
673 }
674
675 fn arg_extremum_global(&self, want_max: bool) -> Result<Tensor<i64>>
676 where
677 T: PartialOrd + Copy,
678 {
679 let index = self.with_contiguous_data(|data| {
680 let mut iter = data.iter().copied().enumerate();
681 let (mut best_idx, mut best) = iter.next().ok_or_else(|| {
682 TorshError::InvalidOperation(
683 "Cannot compute arg-reduction on empty tensor".to_string(),
684 )
685 })?;
686 for (idx, value) in iter {
687 let better = if want_max { value > best } else { value < best };
688 if better {
689 best = value;
690 best_idx = idx;
691 }
692 }
693 Ok(best_idx as i64)
694 })?;
695
696 Tensor::<i64>::from_data(vec![index], vec![], self.device())
697 }
698}
699
700pub(crate) fn contiguous_strides(shape: &[usize]) -> Vec<usize> {
702 let ndim = shape.len();
703 let mut strides = vec![1usize; ndim];
704 for i in (0..ndim.saturating_sub(1)).rev() {
705 strides[i] = strides[i + 1] * shape[i + 1];
706 }
707 strides
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713 use torsh_core::device::DeviceType;
714
715 #[test]
716 fn normalize_dim_handles_negative_and_range() {
717 assert_eq!(normalize_dim(-1, 3).expect("valid dim"), 2);
718 assert_eq!(normalize_dim(0, 3).expect("valid dim"), 0);
719 assert!(normalize_dim(3, 3).is_err());
720 assert!(normalize_dim(-4, 3).is_err());
721 assert!(normalize_dim(0, 0).is_err());
722 }
723
724 #[test]
725 fn amin_amax_are_symmetric() {
726 let t = Tensor::from_data(vec![1.0f32, 5.0, 3.0, 2.0], vec![2, 2], DeviceType::Cpu)
727 .expect("tensor creation should succeed");
728
729 let amax_dim1 = t.amax(Some(1), false).expect("amax should succeed");
730 let amin_dim1 = t.amin(Some(1), false).expect("amin should succeed");
731 assert_eq!(amax_dim1.to_vec().expect("to_vec"), vec![5.0, 3.0]);
732 assert_eq!(amin_dim1.to_vec().expect("to_vec"), vec![1.0, 2.0]);
733
734 let amax_all = t.amax(None, false).expect("amax should succeed");
735 let amin_all = t.amin(None, false).expect("amin should succeed");
736 assert_eq!(amax_all.item().expect("item"), 5.0);
737 assert_eq!(amin_all.item().expect("item"), 1.0);
738
739 let kept = t.amin(Some(0), true).expect("amin should succeed");
741 assert_eq!(kept.shape().dims(), &[1, 2]);
742 }
743
744 #[test]
745 fn min_dim_and_max_dim_agree_with_amin_amax() {
746 let t = Tensor::from_data(
747 vec![1.0f32, 5.0, 3.0, 2.0, 8.0, 0.0],
748 vec![2, 3],
749 DeviceType::Cpu,
750 )
751 .expect("tensor creation should succeed");
752
753 assert_eq!(
754 t.min_dim(-1, false).expect("min_dim").to_vec().expect("v"),
755 t.amin(Some(1), false).expect("amin").to_vec().expect("v")
756 );
757 assert_eq!(
758 t.max_dim(-1, false).expect("max_dim").to_vec().expect("v"),
759 t.amax(Some(1), false).expect("amax").to_vec().expect("v")
760 );
761 }
762
763 #[test]
764 fn argmax_dim_keepdim_shape() {
765 let t = Tensor::from_data(
766 vec![1.0f32, 5.0, 3.0, 2.0, 8.0, 0.0],
767 vec![2, 3],
768 DeviceType::Cpu,
769 )
770 .expect("tensor creation should succeed");
771 let out = t.argmax_dim(1, true).expect("argmax_dim should succeed");
772 assert_eq!(out.shape().dims(), &[2, 1]);
773 assert_eq!(out.to_vec().expect("to_vec"), vec![1i64, 1]);
774 }
775
776 #[test]
777 fn sort_indices_are_i64_positions_within_dim() {
778 let t = Tensor::from_data(
779 vec![3.0f32, 1.0, 2.0, 0.0, 5.0, 4.0],
780 vec![2, 3],
781 DeviceType::Cpu,
782 )
783 .expect("tensor creation should succeed");
784 let (values, indices) = t.sort(Some(1), false).expect("sort should succeed");
785 assert_eq!(
786 values.to_vec().expect("v"),
787 vec![1.0, 2.0, 3.0, 0.0, 4.0, 5.0]
788 );
789 assert_eq!(indices.to_vec().expect("v"), vec![1i64, 2, 0, 0, 2, 1]);
790 }
791
792 #[test]
793 fn sort_on_a_view_uses_view_order() {
794 let base = Tensor::from_data(
795 vec![3.0f32, 1.0, 2.0, 0.0, 5.0, 4.0],
796 vec![2, 3],
797 DeviceType::Cpu,
798 )
799 .expect("tensor creation should succeed");
800 let view = base.transpose_view(0, 1).expect("transpose_view");
801 let (values, indices) = view.sort(Some(1), false).expect("sort should succeed");
803 assert_eq!(
804 values.to_vec().expect("v"),
805 vec![0.0, 3.0, 1.0, 5.0, 2.0, 4.0]
806 );
807 assert_eq!(indices.to_vec().expect("v"), vec![1i64, 0, 0, 1, 0, 1]);
808 }
809}