Skip to main content

candle_einops/
backend.rs

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
72/// Tensor operations used by [`crate::einops!`].
73///
74/// Transformations return Candle [`Result`] values so backend failures retain
75/// their original error and context.
76pub trait Backend {
77    type Output;
78    fn shape(self) -> Vec<usize>;
79    /// Reshapes a tensor while preserving its storage and layout when the
80    /// requested dimensions are already exact.
81    ///
82    /// Call [`Tensor::contiguous`] explicitly when contiguous storage is
83    /// required; an identity reshape no longer provides accidental
84    /// materialization for non-contiguous inputs.
85    fn reshape(self, shape: &[usize]) -> Result<Self::Output>;
86    fn transpose(self, axes: &[usize]) -> Result<Self::Output>;
87    /// Composes adjacent logical axis groups after any preceding permutation.
88    ///
89    /// The default retains the historical reshape sequence. Tensor backends
90    /// may recover a storage-sharing layout before falling back to that copy.
91    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    /// Applies a permutation followed immediately by axis composition.
98    ///
99    /// Backends may specialize this boundary. The default preserves the
100    /// historical operation sequence for third-party implementations.
101    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    /// Inserts new axes as broadcast views.
116    ///
117    /// The returned tensor can be non-contiguous and can alias the input.
118    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    // A recovered rank-two transpose view makes a later CPU materialization
392    // substantially slower than Candle's direct permute-and-reshape copy. GPU
393    // providers keep the view so they can avoid an enqueue when it is consumed
394    // by a layout-aware kernel.
395    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}