1use candle_core::{Result, Shape, Tensor};
2
3use crate::Operation;
4
5#[derive(Debug)]
6struct ReductionRun {
7 axes: Vec<usize>,
8 operation: Operation,
9}
10
11fn operations_are_fusible(left: Operation, right: Operation) -> bool {
12 matches!(
13 (left, right),
14 (Operation::Sum, Operation::Sum)
15 | (Operation::Mean, Operation::Mean)
16 | (Operation::Min, Operation::Min)
17 | (Operation::Max, Operation::Max)
18 )
19}
20
21fn plan_reduction_runs(axes_operations: &mut [(usize, Operation)]) -> Vec<ReductionRun> {
22 axes_operations.sort_by_key(|(axis, _)| *axis);
23 let mut runs: Vec<ReductionRun> = Vec::new();
24 for &(axis, operation) in axes_operations.iter().rev() {
25 if let Some(run) = runs.last_mut()
26 && operations_are_fusible(run.operation, operation)
27 && (matches!(operation, Operation::Sum | Operation::Mean)
28 || run
29 .axes
30 .last()
31 .is_some_and(|&previous| axis + 1 == previous))
32 {
33 run.axes.push(axis);
34 } else {
35 runs.push(ReductionRun {
36 axes: vec![axis],
37 operation,
38 });
39 }
40 }
41 for run in &mut runs {
42 if matches!(run.operation, Operation::Sum | Operation::Mean) {
43 run.axes.reverse();
44 }
45 }
46 runs
47}
48
49#[cfg(test)]
50std::thread_local! {
51 static BACKEND_REDUCTION_CALL_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
52}
53
54#[cfg(test)]
55fn record_backend_reduction_call() {
56 BACKEND_REDUCTION_CALL_COUNT.set(BACKEND_REDUCTION_CALL_COUNT.get() + 1);
57}
58
59#[cfg(not(test))]
60fn record_backend_reduction_call() {}
61
62#[cfg(test)]
63fn reset_backend_reduction_call_count() {
64 BACKEND_REDUCTION_CALL_COUNT.set(0);
65}
66
67#[cfg(test)]
68fn backend_reduction_call_count() -> usize {
69 BACKEND_REDUCTION_CALL_COUNT.get()
70}
71
72pub trait Backend {
77 type Output;
78 fn shape(self) -> Vec<usize>;
79 fn reshape(self, shape: &[usize]) -> Result<Self::Output>;
86 fn transpose(self, axes: &[usize]) -> Result<Self::Output>;
87 fn compose_axes(self, output_shape: &[usize], _group_lengths: &[usize]) -> Result<Self::Output>
92 where
93 Self: Sized,
94 {
95 self.reshape(output_shape)
96 }
97 fn permute_and_compose(
102 self,
103 permutation: &[usize],
104 output_shape: &[usize],
105 _group_lengths: &[usize],
106 ) -> Result<<Self::Output as Backend>::Output>
107 where
108 Self: Sized,
109 Self::Output: Backend,
110 {
111 let output = self.transpose(permutation)?;
112 Backend::reshape(output, output_shape)
113 }
114 fn reduce_axes(self, axes_operations: &mut [(usize, Operation)]) -> Result<Self::Output>;
115 fn add_axes(self, naxes: usize, pos2len: &[(usize, usize)]) -> Result<Self::Output>;
119}
120
121impl<T: AsRef<Tensor>> Backend for T {
122 type Output = Tensor;
123
124 fn shape(self) -> Vec<usize> {
125 self.as_ref().dims().to_vec()
126 }
127
128 fn reshape(self, shape: &[usize]) -> Result<Self::Output> {
129 let input = self.as_ref();
130 let shape = Shape::from_dims(shape);
131 if shape.elem_count() != input.elem_count() {
132 return input.reshape(shape);
133 }
134 if input.dims() == shape.dims() {
135 Ok(input.clone())
136 } else {
137 input.reshape(shape)
138 }
139 }
140
141 fn transpose(self, axes: &[usize]) -> Result<Self::Output> {
142 self.as_ref().permute(axes)
143 }
144
145 fn compose_axes(self, output_shape: &[usize], group_lengths: &[usize]) -> Result<Self::Output> {
146 execute_tensor_compose_axes(self.as_ref(), output_shape, group_lengths)
147 }
148
149 fn permute_and_compose(
150 self,
151 permutation: &[usize],
152 output_shape: &[usize],
153 group_lengths: &[usize],
154 ) -> Result<<Self::Output as Backend>::Output>
155 where
156 Self::Output: Backend,
157 {
158 execute_tensor_permute_and_compose(self.as_ref(), permutation, output_shape, group_lengths)
159 }
160
161 fn reduce_axes(self, axes_operations: &mut [(usize, Operation)]) -> Result<Self::Output> {
162 let mut output = self.as_ref().clone();
163 let mut occupied = vec![false; output.rank()];
164
165 for &(axis, _) in axes_operations.iter() {
166 if axis >= occupied.len() {
167 candle_core::bail!(
168 "reduce_axes: axis {axis} out of range for rank {}",
169 occupied.len()
170 )
171 }
172 if occupied[axis] {
173 candle_core::bail!("reduce_axes: duplicate axis {axis}")
174 }
175 occupied[axis] = true;
176 }
177
178 for run in plan_reduction_runs(axes_operations) {
179 output = match run.operation {
180 Operation::Min | Operation::Max if run.axes.len() > 1 => {
181 if let Some(collapsed) =
182 collapse_extrema_run(&output, &run.axes, run.operation)?
183 {
184 record_backend_reduction_call();
185 collapsed
186 } else {
187 let mut reduced = output;
188 for axis in run.axes {
189 record_backend_reduction_call();
190 reduced = match run.operation {
191 Operation::Min => reduced.min(axis)?,
192 Operation::Max => reduced.max(axis)?,
193 _ => unreachable!("extrema run operation"),
194 };
195 }
196 reduced
197 }
198 }
199 Operation::Min => {
200 record_backend_reduction_call();
201 output.min(run.axes[0])?
202 }
203 Operation::Max => {
204 record_backend_reduction_call();
205 output.max(run.axes[0])?
206 }
207 Operation::Sum => {
208 record_backend_reduction_call();
209 output.sum(run.axes.as_slice())?
210 }
211 Operation::Mean => {
212 record_backend_reduction_call();
213 output.mean(run.axes.as_slice())?
214 }
215 Operation::Prod => {
216 record_backend_reduction_call();
217 reduce_product_axis(&output, run.axes[0])?
218 }
219 };
220 }
221
222 Ok(output)
223 }
224
225 fn add_axes(self, naxes: usize, pos2len: &[(usize, usize)]) -> Result<Self::Output> {
226 let input = self.as_ref();
227
228 let expected_naxes = input.rank() + pos2len.len();
229 if naxes != expected_naxes {
230 candle_core::bail!("add_axes: expected final rank {expected_naxes}, got {naxes}")
231 }
232
233 let mut inserted_lengths = vec![1; naxes];
234 let mut occupied = vec![false; naxes];
235
236 for &(axis_pos, axis_len) in pos2len {
237 if axis_pos >= naxes {
238 candle_core::bail!(
239 "add_axes: axis position {axis_pos} out of range for final rank {naxes}"
240 )
241 }
242 if occupied[axis_pos] {
243 candle_core::bail!("add_axes: duplicate axis position {axis_pos}")
244 }
245 occupied[axis_pos] = true;
246 inserted_lengths[axis_pos] = axis_len;
247 }
248
249 let mut singleton_shape = Vec::with_capacity(naxes);
250 let mut final_shape = Vec::with_capacity(naxes);
251 let mut input_axis = 0;
252 for axis in 0..naxes {
253 if occupied[axis] {
254 singleton_shape.push(1);
255 final_shape.push(inserted_lengths[axis]);
256 } else {
257 let length = input.dims()[input_axis];
258 singleton_shape.push(length);
259 final_shape.push(length);
260 input_axis += 1;
261 }
262 }
263
264 let expanded = if input.is_contiguous() {
265 input.reshape(Shape::from_dims(&singleton_shape))?
266 } else {
267 let mut output = input.clone();
268 let mut positions = pos2len
269 .iter()
270 .map(|&(axis_pos, _)| axis_pos)
271 .collect::<Vec<_>>();
272 positions.sort_unstable();
273 for axis_pos in positions {
274 output = output.unsqueeze(axis_pos)?;
275 }
276 output
277 };
278
279 expanded.broadcast_as(Shape::from_dims(&final_shape))
280 }
281}
282
283fn reduce_product_axis(input: &Tensor, axis: usize) -> Result<Tensor> {
284 let axis_len = input.dim(axis)?;
285 if axis_len == 0 {
286 let mut shape = input.dims().to_vec();
287 shape.remove(axis);
288 return Tensor::ones(Shape::from_dims(&shape), input.dtype(), input.device());
289 }
290
291 let mut factors = (0..axis_len)
292 .map(|index| input.narrow(axis, index, 1)?.squeeze(axis))
293 .collect::<Result<Vec<_>>>()?;
294 while factors.len() > 1 {
295 let mut products = Vec::with_capacity(factors.len().div_ceil(2));
296 let mut factor_iter = factors.into_iter();
297 while let Some(left) = factor_iter.next() {
298 products.push(match factor_iter.next() {
299 Some(right) => left.mul(&right)?,
300 None => left,
301 });
302 }
303 factors = products;
304 }
305 Ok(factors.pop().expect("non-empty product factors"))
306}
307
308fn collapse_extrema_run(
309 input: &Tensor,
310 descending_axes: &[usize],
311 operation: Operation,
312) -> Result<Option<Tensor>> {
313 let start = *descending_axes
314 .last()
315 .expect("an extrema run contains at least one axis");
316 if input.device().is_cpu() {
317 return Ok(None);
318 }
319 let mut group_lengths = vec![1; start];
320 group_lengths.push(descending_axes.len());
321 group_lengths.extend(std::iter::repeat_n(
322 1,
323 input.rank() - start - descending_axes.len(),
324 ));
325 let identity = (0..input.rank()).collect::<Vec<_>>();
326 if plan_permute_compose_group_order(input.dims(), input.stride(), &identity, &group_lengths)?
327 .is_none()
328 {
329 return Ok(None);
330 }
331 let mut collapsed_shape = input.dims()[..start].to_vec();
332 let collapsed_extent = input.dims()[start..start + descending_axes.len()]
333 .iter()
334 .try_fold(1usize, |product, &extent| product.checked_mul(extent))
335 .ok_or_else(|| candle_core::Error::msg("extrema collapsed extent overflows usize"))?;
336 collapsed_shape.push(collapsed_extent);
337 collapsed_shape.extend_from_slice(&input.dims()[start + descending_axes.len()..]);
338 let collapsed = execute_tensor_compose_axes(input, &collapsed_shape, &group_lengths)?;
339 match operation {
340 Operation::Min => collapsed.min(start).map(Some),
341 Operation::Max => collapsed.max(start).map(Some),
342 _ => unreachable!("collapsed extrema operation"),
343 }
344}
345
346pub(crate) fn execute_tensor_permute_and_compose(
347 input: &Tensor,
348 permutation: &[usize],
349 output_shape: &[usize],
350 group_lengths: &[usize],
351) -> Result<Tensor> {
352 let rank = input.rank();
353 if permutation.len() != rank {
354 candle_core::bail!(
355 "permute_and_compose: permutation rank {} does not match input rank {rank}",
356 permutation.len()
357 )
358 }
359 let mut seen = vec![false; rank];
360 for &axis in permutation {
361 if axis >= rank || seen[axis] {
362 candle_core::bail!("permute_and_compose: invalid permutation")
363 }
364 seen[axis] = true;
365 }
366 if group_lengths.len() != output_shape.len()
367 || group_lengths.contains(&0)
368 || group_lengths
369 .iter()
370 .try_fold(0usize, |sum, &length| sum.checked_add(length))
371 != Some(rank)
372 {
373 candle_core::bail!("permute_and_compose: invalid group metadata")
374 }
375
376 let mut groups = Vec::with_capacity(group_lengths.len());
377 let mut start = 0;
378 for (&length, &expected_output) in group_lengths.iter().zip(output_shape) {
379 let end = start + length;
380 let group = permutation[start..end].to_vec();
381 let product = checked_axis_product(input.dims(), &group)?;
382 if product != expected_output {
383 candle_core::bail!(
384 "permute_and_compose: group product {product} does not match output extent {expected_output}"
385 )
386 }
387 groups.push(group);
388 start = end;
389 }
390
391 if input.device().is_cpu() && input.elem_count() != 0 && output_shape.len() == 2 {
396 return input.permute(permutation)?.reshape(output_shape);
397 }
398
399 if let Some(order) =
400 plan_permute_compose_group_order(input.dims(), input.stride(), permutation, group_lengths)?
401 {
402 let pre_permutation = order
403 .iter()
404 .flat_map(|&group| groups[group].iter().copied())
405 .collect::<Vec<_>>();
406 let reshape_dims = order
407 .iter()
408 .map(|&group| output_shape[group])
409 .collect::<Vec<_>>();
410 let post_permutation = (0..groups.len())
411 .map(|desired| {
412 order
413 .iter()
414 .position(|&group| group == desired)
415 .expect("group order is a permutation")
416 })
417 .collect::<Vec<_>>();
418 let permuted = input.permute(pre_permutation)?;
419 let reshaped = permuted.reshape(&reshape_dims)?;
420 return reshaped.permute(post_permutation);
421 }
422
423 input.permute(permutation)?.reshape(output_shape)
424}
425
426pub(crate) fn execute_tensor_compose_axes(
427 input: &Tensor,
428 output_shape: &[usize],
429 group_lengths: &[usize],
430) -> Result<Tensor> {
431 if output_shape.len() != group_lengths.len() {
432 candle_core::bail!("compose_axes: output and group ranks differ")
433 }
434 let mut nonempty_shape = Vec::with_capacity(output_shape.len());
435 let mut nonempty_lengths = Vec::with_capacity(group_lengths.len());
436 for (&extent, &length) in output_shape.iter().zip(group_lengths) {
437 if length == 0 {
438 if extent != 1 {
439 candle_core::bail!("compose_axes: an empty group must have extent one")
440 }
441 } else {
442 nonempty_shape.push(extent);
443 nonempty_lengths.push(length);
444 }
445 }
446 let mut output = if nonempty_lengths.is_empty() {
447 input.clone()
448 } else {
449 execute_tensor_permute_and_compose(
450 input,
451 &(0..input.rank()).collect::<Vec<_>>(),
452 &nonempty_shape,
453 &nonempty_lengths,
454 )?
455 };
456 for (axis, &length) in group_lengths.iter().enumerate() {
457 if length == 0 {
458 output = output.unsqueeze(axis)?;
459 }
460 }
461 Ok(output)
462}
463
464fn checked_axis_product(dims: &[usize], axes: &[usize]) -> Result<usize> {
465 axes.iter().try_fold(1usize, |product, &axis| {
466 product.checked_mul(dims[axis]).ok_or_else(|| {
467 candle_core::Error::msg("permute_and_compose: group product overflows usize")
468 })
469 })
470}
471
472pub(crate) fn plan_permute_compose_group_order(
473 dims: &[usize],
474 strides: &[usize],
475 permutation: &[usize],
476 group_lengths: &[usize],
477) -> Result<Option<Vec<usize>>> {
478 if dims.len() != strides.len()
479 || permutation.len() != dims.len()
480 || group_lengths.contains(&0)
481 || group_lengths
482 .iter()
483 .try_fold(0usize, |sum, &length| sum.checked_add(length))
484 != Some(dims.len())
485 {
486 candle_core::bail!("permute_and_compose: invalid layout metadata")
487 }
488 if group_lengths.len() > 8 {
489 return Ok(None);
490 }
491 if permutation_is_c_contiguous(dims, strides, permutation)? {
492 return Ok(Some((0..group_lengths.len()).collect()));
493 }
494
495 let mut starts = Vec::with_capacity(group_lengths.len());
496 let mut start = 0;
497 for &length in group_lengths {
498 starts.push(start);
499 start += length;
500 }
501 fn search(
502 dims: &[usize],
503 strides: &[usize],
504 permutation: &[usize],
505 starts: &[usize],
506 lengths: &[usize],
507 order: &mut Vec<usize>,
508 used: &mut [bool],
509 ) -> Result<Option<Vec<usize>>> {
510 if order.len() == lengths.len() {
511 let mut expected = 1usize;
512 for &group in order.iter().rev() {
513 let start = starts[group];
514 let end = start + lengths[group];
515 for &axis in permutation[start..end].iter().rev() {
516 let extent = dims[axis];
517 if extent > 1 && strides[axis] != expected {
518 return Ok(None);
519 }
520 expected = expected.checked_mul(extent).ok_or_else(|| {
521 candle_core::Error::msg(
522 "permute_and_compose: stride product overflows usize",
523 )
524 })?;
525 }
526 }
527 return Ok(Some(order.clone()));
528 }
529 for group in 0..lengths.len() {
530 if used[group] {
531 continue;
532 }
533 used[group] = true;
534 order.push(group);
535 if let Some(plan) = search(dims, strides, permutation, starts, lengths, order, used)? {
536 return Ok(Some(plan));
537 }
538 order.pop();
539 used[group] = false;
540 }
541 Ok(None)
542 }
543 search(
544 dims,
545 strides,
546 permutation,
547 &starts,
548 group_lengths,
549 &mut Vec::with_capacity(group_lengths.len()),
550 &mut vec![false; group_lengths.len()],
551 )
552}
553
554fn permutation_is_c_contiguous(
555 dims: &[usize],
556 strides: &[usize],
557 permutation: &[usize],
558) -> Result<bool> {
559 let mut expected = 1usize;
560 for &axis in permutation.iter().rev() {
561 let extent = dims[axis];
562 if extent > 1 && strides[axis] != expected {
563 return Ok(false);
564 }
565 expected = expected.checked_mul(extent).ok_or_else(|| {
566 candle_core::Error::msg("permute_and_compose: stride product overflows usize")
567 })?;
568 }
569 Ok(true)
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575 use candle_core::{Device, Result};
576
577 #[test]
578 fn plans_only_adjacent_homogeneous_sum_and_mean_runs() {
579 let mut sums = [
580 (0, Operation::Sum),
581 (1, Operation::Sum),
582 (2, Operation::Sum),
583 ];
584 let runs = plan_reduction_runs(&mut sums);
585 assert_eq!(runs.len(), 1);
586 assert_eq!(
587 runs[0].axes,
588 [0, 1, 2],
589 "fused dimensions must preserve physical stride order for GPU fast-reduce kernels"
590 );
591 assert!(matches!(runs[0].operation, Operation::Sum));
592
593 let mut mixed = [
594 (0, Operation::Sum),
595 (1, Operation::Max),
596 (2, Operation::Sum),
597 (3, Operation::Sum),
598 ];
599 let runs = plan_reduction_runs(&mut mixed);
600 assert_eq!(runs.len(), 3);
601 assert_eq!(runs[0].axes, [2, 3]);
602 assert!(matches!(runs[0].operation, Operation::Sum));
603 assert_eq!(runs[1].axes, [1]);
604 assert!(matches!(runs[1].operation, Operation::Max));
605 assert_eq!(runs[2].axes, [0]);
606 assert!(matches!(runs[2].operation, Operation::Sum));
607
608 let mut excluded = [
609 (0, Operation::Min),
610 (1, Operation::Min),
611 (2, Operation::Prod),
612 (3, Operation::Prod),
613 ];
614 let runs = plan_reduction_runs(&mut excluded);
615 assert_eq!(
616 runs.len(),
617 3,
618 "adjacent min may collapse; prod remains sequential"
619 );
620 assert_eq!(runs[2].axes, [1, 0]);
621 assert!(matches!(runs[2].operation, Operation::Min));
622 }
623
624 #[test]
625 fn homogeneous_runs_issue_one_backend_reduction_call() -> Result<()> {
626 let input = Tensor::arange(0f32, 2. * 3. * 4., &Device::Cpu)?.reshape(&[2, 3, 4])?;
627
628 reset_backend_reduction_call_count();
629 (&input).reduce_axes(&mut [
630 (0, Operation::Sum),
631 (1, Operation::Sum),
632 (2, Operation::Sum),
633 ])?;
634 assert_eq!(backend_reduction_call_count(), 1);
635
636 reset_backend_reduction_call_count();
637 (&input).reduce_axes(&mut [
638 (0, Operation::Mean),
639 (1, Operation::Mean),
640 (2, Operation::Mean),
641 ])?;
642 assert_eq!(backend_reduction_call_count(), 1);
643
644 reset_backend_reduction_call_count();
645 (&input).reduce_axes(&mut [
646 (0, Operation::Sum),
647 (1, Operation::Max),
648 (2, Operation::Sum),
649 ])?;
650 assert_eq!(backend_reduction_call_count(), 3);
651
652 reset_backend_reduction_call_count();
653 (&input).reduce_axes(&mut [(1, Operation::Min), (2, Operation::Min)])?;
654 assert_eq!(
655 backend_reduction_call_count(),
656 2,
657 "CPU extrema should retain Candle's faster sequential trailing-axis route"
658 );
659
660 reset_backend_reduction_call_count();
661 (&input).reduce_axes(&mut [(0, Operation::Max), (1, Operation::Max)])?;
662 assert_eq!(
663 backend_reduction_call_count(),
664 2,
665 "CPU extrema should retain Candle's faster sequential leading-axis route"
666 );
667
668 let strided = input.permute([0, 2, 1])?;
669 reset_backend_reduction_call_count();
670 let selected = (&strided).reduce_axes(&mut [(1, Operation::Max), (2, Operation::Max)])?;
671 assert_eq!(backend_reduction_call_count(), 2);
672 assert_eq!(
673 selected.to_vec1::<f32>()?,
674 strided.max(2)?.max(1)?.to_vec1::<f32>()?
675 );
676 Ok(())
677 }
678
679 #[test]
680 fn product_reduction_uses_balanced_association() -> Result<()> {
681 let values = (0..64)
682 .map(|index| 1. + ((index % 7) as f32 - 3.) * 0.0001)
683 .collect::<Vec<_>>();
684 let input = Tensor::from_vec(values, (1, 64), &Device::Cpu)?;
685 let selected = (&input).reduce_axes(&mut [(1, Operation::Prod)])?;
686
687 let mut factors = (0..64)
688 .map(|index| input.narrow(1, index, 1)?.squeeze(1))
689 .collect::<Result<Vec<_>>>()?;
690 while factors.len() > 1 {
691 let mut next = Vec::with_capacity(factors.len().div_ceil(2));
692 let mut factor_iter = factors.into_iter();
693 while let Some(left) = factor_iter.next() {
694 next.push(match factor_iter.next() {
695 Some(right) => left.mul(&right)?,
696 None => left,
697 });
698 }
699 factors = next;
700 }
701 let balanced = factors.pop().expect("non-empty factors");
702
703 assert_eq!(selected.to_vec1::<f32>()?, balanced.to_vec1::<f32>()?);
704 Ok(())
705 }
706
707 #[test]
708 fn reduce() -> Result<()> {
709 let tests = vec![
710 (
711 Tensor::new(
712 &[
713 0.66984287f32,
714 0.52894678,
715 0.85415958,
716 0.17721198,
717 0.81804799,
718 0.80991797,
719 0.64868822,
720 0.96697902,
721 0.08047191,
722 0.46024353,
723 0.21955009,
724 0.31731976,
725 0.05446258,
726 0.39454557,
727 0.40949016,
728 0.21366165,
729 0.2357463,
730 0.93699481,
731 0.64522596,
732 0.4383618,
733 0.54871827,
734 0.87823442,
735 0.01261184,
736 0.90636503,
737 ],
738 &Device::Cpu,
739 )?
740 .reshape(&[4, 2, 3])?,
741 [(0, Operation::Min)],
742 Tensor::new(
743 &[
744 [0.05446258f32, 0.39454557, 0.08047191],
745 [0.17721198, 0.01261184, 0.31731976],
746 ],
747 &Device::Cpu,
748 )?,
749 ),
750 (
751 Tensor::new(
752 &[
753 0.66984287f32,
754 0.52894678,
755 0.85415958,
756 0.17721198,
757 0.81804799,
758 0.80991797,
759 0.64868822,
760 0.96697902,
761 0.08047191,
762 0.46024353,
763 0.21955009,
764 0.31731976,
765 0.05446258,
766 0.39454557,
767 0.40949016,
768 0.21366165,
769 0.2357463,
770 0.93699481,
771 0.64522596,
772 0.4383618,
773 0.54871827,
774 0.87823442,
775 0.01261184,
776 0.90636503,
777 ],
778 &Device::Cpu,
779 )?
780 .reshape(&[4, 2, 3])?,
781 [(0, Operation::Max)],
782 Tensor::new(
783 &[
784 [0.6698429f32, 0.966979, 0.8541596],
785 [0.87823445, 0.818048, 0.9369948],
786 ],
787 &Device::Cpu,
788 )?,
789 ),
790 ];
791
792 for (tensor, mut axes_operations, expected) in tests {
793 assert_eq!(
794 tensor.reduce_axes(&mut axes_operations)?.to_vec2::<f32>()?,
795 expected.to_vec2::<f32>()?
796 );
797 }
798
799 Ok(())
800 }
801
802 #[test]
803 fn candle_transpose() -> Result<()> {
804 let tests = vec![(
805 Tensor::arange(0f32, (2 * 3 * 4) as f32, &Device::Cpu)?.reshape(&[2, 3, 4])?,
806 &[2, 0, 1],
807 Tensor::new(
808 &[
809 [[0.0f32, 4.0, 8.0], [12.0, 16.0, 20.0]],
810 [[1.0, 5.0, 9.0], [13.0, 17.0, 21.0]],
811 [[2.0, 6.0, 10.0], [14.0, 18.0, 22.0]],
812 [[3.0, 7.0, 11.0], [15.0, 19.0, 23.0]],
813 ],
814 &Device::Cpu,
815 )?,
816 )];
817
818 for (tensor, axes, expected) in tests {
819 assert_eq!(
820 Backend::transpose(&tensor, axes)?.to_vec3::<f32>()?,
821 expected.to_vec3::<f32>()?
822 );
823 }
824
825 Ok(())
826 }
827
828 #[test]
829 fn tch_add_axes() -> Result<()> {
830 let tests = vec![(
831 Tensor::arange(0u8, 1 * 2 * 3, &Device::Cpu)?.reshape(&[1, 2, 3])?,
832 5,
833 &[(0, 5), (3, 3)],
834 Tensor::new(
835 vec![
836 0u8, 1, 2, 0, 1, 2, 0, 1, 2, 3, 4, 5, 3, 4, 5, 3, 4, 5, 0, 1, 2, 0, 1, 2, 0, 1,
837 2, 3, 4, 5, 3, 4, 5, 3, 4, 5, 0, 1, 2, 0, 1, 2, 0, 1, 2, 3, 4, 5, 3, 4, 5, 3,
838 4, 5, 0, 1, 2, 0, 1, 2, 0, 1, 2, 3, 4, 5, 3, 4, 5, 3, 4, 5, 0, 1, 2, 0, 1, 2,
839 0, 1, 2, 3, 4, 5, 3, 4, 5, 3, 4, 5,
840 ],
841 &Device::Cpu,
842 )?
843 .reshape(&[5, 1, 2, 3, 3])?,
844 )];
845
846 for (tensor, naxes, pos2len, expected) in tests {
847 assert_eq!(
848 tensor
849 .add_axes(naxes, pos2len)?
850 .flatten_all()?
851 .to_vec1::<u8>()?,
852 expected.flatten_all()?.to_vec1::<u8>()?
853 );
854 }
855
856 Ok(())
857 }
858}