Skip to main content

burn_cubecl/kernel/reduce/
base.rs

1#[cfg(feature = "autotune")]
2use super::{autotune_reduce, autotune_reduce_with_indices, autotune_sum};
3use crate::ops::permute;
4use crate::{
5    ops::numeric::{empty_device_contiguous_dtype, fill_device_dtype, zeros_client},
6    tensor::CubeTensor,
7};
8use burn_backend::cubecl::{dtype_to_elem_type, dtype_to_storage_type, elem_type_to_dtype};
9use burn_backend::{DType, TensorMetadata};
10use burn_std::{BoolDType, Metadata};
11use burn_std::{Shape, Strides};
12use cubecl::{AutotuneKey, client::Client, features::AtomicUsage, ir::Type, prelude::InputScalar};
13use cubek::reduce::{
14    ReduceDtypes, ReduceError, ReduceStrategy, ReduceWithIndicesDtypes,
15    components::instructions::ReduceOperationConfig,
16    launch::{RoutineStrategy, VectorizationStrategy},
17    routines::{BlueprintStrategy, unit::UnitStrategy},
18    shared_sum,
19};
20use serde::{Deserialize, Serialize};
21
22#[derive(Hash, Eq, PartialEq, Debug, Clone, Serialize, Deserialize, AutotuneKey)]
23/// Autotune key representative of sum versions
24pub struct SumAutotuneKey {
25    /// The type of the tensor
26    dtype: burn_backend::DType,
27    /// The anchored length of the tensor
28    #[autotune(anchor)]
29    length: usize,
30}
31
32/// The value a reduction over zero elements must produce, or `None` if there is none.
33///
34/// Reducing zero elements yields the folded operation's identity. The extrema have no identity in
35/// a bounded numeric type (there is no integer below `i32::MIN`) and an `Arg*` would have to name
36/// an element that does not exist, so they return `None`: numpy raises `ValueError` and torch
37/// raises `IndexError` for all of them, as do burn's CPU backends for an empty `max`/`min`.
38fn empty_reduce_identity(config: ReduceOperationConfig, dtype: DType) -> Option<f64> {
39    match config {
40        ReduceOperationConfig::Sum | ReduceOperationConfig::Any => Some(0.0),
41        ReduceOperationConfig::Prod | ReduceOperationConfig::All => Some(1.0),
42        // Only floats can carry `NaN`; an integer mean of nothing has no representable value.
43        ReduceOperationConfig::Mean => dtype.is_float().then_some(f64::NAN),
44        ReduceOperationConfig::Max
45        | ReduceOperationConfig::Min
46        | ReduceOperationConfig::MaxAbs
47        | ReduceOperationConfig::TopK(_)
48        | ReduceOperationConfig::ArgMax
49        | ReduceOperationConfig::ArgMin
50        | ReduceOperationConfig::ArgTopK(_) => None,
51    }
52}
53
54/// Fill `output` with the identity of `config`, or report that `config` has none.
55///
56/// Filled directly rather than by a reduce kernel because cubek's `validate_shapes` rejects a
57/// zero-length axis with [`ReduceError::ReduceAxisTooSmall`], so no reduction can run.
58///
59/// An operation with no identity is rejected even when `output` is itself empty: emptiness of the
60/// output depends on the *other* axes, so allowing it would make `max` succeed for shape `[0, 0]`
61/// and fail for `[3, 0]`.
62fn reduce_empty_axis(
63    output: CubeTensor,
64    axis_length: usize,
65    config: ReduceOperationConfig,
66) -> Result<CubeTensor, ReduceError> {
67    let identity =
68        empty_reduce_identity(config, output.dtype).ok_or(ReduceError::ReduceAxisTooSmall {
69            axis_length,
70            k: accumulator_len(config),
71        })?;
72
73    if output.meta.num_elements() == 0 {
74        return Ok(output);
75    }
76
77    let identity = InputScalar::new(identity, dtype_to_storage_type(output.dtype));
78
79    Ok(fill_device_dtype(output, identity))
80}
81
82/// Check if the client supports atomic add for the given element type.
83fn supports_atomic_add(client: &Client, dtype: DType) -> bool {
84    client
85        .properties()
86        .atomic_type_usage(Type::atomic(dtype_to_elem_type(dtype)))
87        .contains(AtomicUsage::Add)
88}
89
90/// [Sum](sum) with fallback when `client` doesn't support atomic add for the type `E`.
91pub fn sum_fallback(
92    tensor: CubeTensor,
93    mut strategy: SumStrategy,
94) -> Result<CubeTensor, ReduceError> {
95    // Early check before creating output and fallback
96    if matches!(strategy, SumStrategy::OneShot(_))
97        && !supports_atomic_add(&tensor.client, tensor.dtype)
98    {
99        strategy = SumStrategy::Chained(Default::default());
100    }
101    sum(tensor, strategy)
102}
103
104/// Specialize reduce function to compute the sum of all elements of the `input` tensor and return
105/// the value into a single-element tensor of shape `1 x 1 x 1 x ...` with the same rank as `input`.
106///
107/// This is expected to be faster for larger tensors than calling [reduce] with the `Sum` instruction.
108///
109/// Return an error if the `client` doesn't support atomic add for the type `E`.
110pub fn sum(tensor: CubeTensor, strategy: SumStrategy) -> Result<CubeTensor, ReduceError> {
111    let client = tensor.client.clone();
112    let device = tensor.device.clone();
113
114    // No strategy can launch a kernel over an empty input, so write the additive identity.
115    if tensor.meta.num_elements() == 0 {
116        return Ok(zeros_client(client, device, [1].into(), tensor.dtype));
117    }
118
119    match strategy {
120        SumStrategy::OneShot(cube_count) => {
121            let output = zeros_client(client.clone(), device, [1].into(), tensor.dtype);
122            let dtype = tensor.dtype;
123
124            shared_sum(
125                &client,
126                tensor.binding(),
127                output.clone().binding(),
128                cube_count,
129                dtype_to_elem_type(dtype),
130            )?;
131
132            Ok(output)
133        }
134        SumStrategy::Chained(strategy) => {
135            reduce(tensor, None, strategy, ReduceOperationConfig::Sum)
136        }
137        #[cfg(feature = "autotune")]
138        SumStrategy::Autotune => Ok(autotune_sum(&client, tensor)),
139    }
140}
141
142/// Select a strategy to perform a sum.
143pub enum SumStrategy {
144    /// Run a single kernel with many cubes working in parallel to sum all elements.
145    /// The provided value is the number of elements summed per unit (up-to-rounding )
146    OneShot(u32),
147    /// Use multiple kernels
148    Chained(KernelReduceStrategy),
149    /// Use autotune to find the best cube count given the hardware and the input.
150    #[cfg(feature = "autotune")]
151    Autotune,
152}
153
154impl Default for SumStrategy {
155    fn default() -> Self {
156        #[cfg(feature = "autotune")]
157        return Self::Autotune;
158
159        #[cfg(not(feature = "autotune"))]
160        return Self::OneShot(4);
161    }
162}
163
164/// Reduce all elements of the `input` tensor using the instruction `Rd` and the given [Strategy](ReduceStrategy).
165///
166/// Return an error if `strategy` is `Specific(strategy)` and the specified strategy is not supported by the `client`.
167///
168/// If there is no error, the output is a tensor with decreasing strides
169/// where the shape of reduced dim is set to 1 but all shape are similar to the input.
170pub fn reduce(
171    mut tensor: CubeTensor,
172    output_dtype: Option<DType>,
173    strategy: KernelReduceStrategy,
174    config: ReduceOperationConfig,
175) -> Result<CubeTensor, cubek::reduce::ReduceError> {
176    // In practice, it looks like starting by the axis with the smallest shape
177    // and going in increasing order lead to the fastest calculation.
178    let sorted_axis = argsort(tensor.meta.shape());
179    for axis in sorted_axis {
180        tensor = reduce_dim(tensor, output_dtype, axis, strategy.clone(), config)?;
181    }
182    // reshape to scalar tensor
183    *tensor.meta = Metadata::new([1], [1]);
184    Ok(tensor)
185}
186
187/// Reduce several `dims` of the `input` tensor with the instruction `config`,
188/// keeping each of them with length one.
189///
190/// Reducing one dimension at a time writes and reads back an intermediate per
191/// dimension, the first of which is nearly the size of the input. Dimensions
192/// that sit next to each other in memory can instead be folded into one axis by
193/// a stride change alone, and reduced in a single launch — for a channels-last
194/// tensor that is every non-channel dimension at once.
195///
196/// Only a run that memory already holds together folds, so each pass takes the
197/// largest such run and reduces that, largest first because it is the pass that
198/// leaves the least behind. The folded axis is presented *first* and the
199/// remaining dimensions after it in logical order, which puts the reduction's
200/// output back in logical order and contiguous, so the next pass starts from a
201/// tensor whose memory order is its logical one. A layout holding every reduced
202/// dimension together therefore takes a single launch, and one that scatters them
203/// takes no more launches than reducing them one at a time would have.
204pub fn reduce_dims(
205    input: CubeTensor,
206    output_dtype: Option<DType>,
207    dims: &[usize],
208    strategy: KernelReduceStrategy,
209    config: ReduceOperationConfig,
210) -> Result<CubeTensor, ReduceError> {
211    let rank = input.meta.num_dims();
212    let mut shape = input.meta.shape().clone();
213    let reduced: Vec<usize> = (0..rank).filter(|dim| dims.contains(dim)).collect();
214
215    let empty: Vec<usize> = reduced
216        .iter()
217        .copied()
218        .filter(|dim| shape[*dim] == 0)
219        .collect();
220
221    // A dimension already of length one is reduced by being left alone, and its
222    // stride is arbitrary, so keeping it among the dimensions to fold would let
223    // it break a run of dimensions that do fold.
224    let mut left: Vec<usize> = reduced
225        .iter()
226        .copied()
227        .filter(|dim| shape[*dim] > 1)
228        .collect();
229
230    if empty.is_empty() {
231        match (reduced.first(), left.len()) {
232            (None, _) => return Ok(input),
233            (Some(&dim), 0) => return reduce_dim(input, output_dtype, dim, strategy, config),
234            (_, 1) => return reduce_dim(input, output_dtype, left[0], strategy, config),
235            _ => {}
236        }
237    }
238
239    let mut tensor = input;
240
241    for dim in empty {
242        tensor = reduce_dim(tensor, output_dtype, dim, strategy.clone(), config)?;
243        shape[dim] = 1;
244    }
245
246    while !left.is_empty() {
247        let run =
248            largest_run_memory_holds_together(tensor.meta.shape(), tensor.meta.strides(), &left);
249        let rest = (0..rank).filter(|dim| !run.contains(dim));
250        let presented_dims: Vec<usize> = run.iter().copied().chain(rest).collect();
251
252        let mut presented = permute(tensor, &presented_dims);
253        fold_leading_dims(&mut presented, run.len());
254
255        tensor = reduce_dim(presented, output_dtype, 0, strategy.clone(), config)?;
256
257        // The reduction writes contiguously, the folded run first at length one
258        // and every other dimension after it in logical order — which is the
259        // logical shape again with the run's dimensions at length one.
260        for dim in &run {
261            shape[*dim] = 1;
262        }
263        *tensor.meta = Metadata::new(shape.clone(), burn_std::tensor::contiguous_strides(&shape));
264
265        left.retain(|dim| !run.contains(dim));
266    }
267
268    Ok(tensor)
269}
270
271/// The dimensions among `left` that memory already holds together — consecutive in
272/// memory order and nesting densely, so folding them into one axis is a stride
273/// change — taking the run of most elements where there is more than one.
274fn largest_run_memory_holds_together(
275    shape: &Shape,
276    strides: &Strides,
277    left: &[usize],
278) -> Vec<usize> {
279    let mut memory_order: Vec<usize> = (0..shape.num_dims()).collect();
280    memory_order.sort_by(|a, b| strides[*b].cmp(&strides[*a]).then(a.cmp(b)));
281
282    let elements = |run: &[usize]| run.iter().map(|dim| shape[*dim]).product::<usize>();
283    let mut largest: Vec<usize> = Vec::new();
284    let mut run: Vec<usize> = Vec::new();
285
286    for dim in memory_order {
287        if shape[dim] == 1 {
288            continue;
289        }
290        if !left.contains(&dim) {
291            run.clear();
292            continue;
293        }
294        // A gap under the dimension outside this one leaves the two spanning more
295        // than their extents, and no stride change folds them into one axis.
296        if let Some(&outside) = run.last()
297            && strides[outside] != strides[dim] * shape[dim]
298        {
299            run.clear();
300        }
301        run.push(dim);
302
303        if elements(&run) > elements(&largest) {
304            largest.clone_from(&run);
305        }
306    }
307
308    largest
309}
310
311/// Fold the leading `count` dimensions of `tensor` into one.
312///
313/// Sound only for dimensions memory holds together, which is what
314/// [largest_run_memory_holds_together] returns: they span exactly their extents,
315/// so the axis replacing them runs at the stride of the innermost of them.
316fn fold_leading_dims(tensor: &mut CubeTensor, count: usize) {
317    let shape = tensor.meta.shape();
318    let strides = tensor.meta.strides();
319
320    let mut folded_shape: Vec<usize> = vec![shape[..count].iter().product()];
321    folded_shape.extend_from_slice(&shape[count..]);
322
323    let mut folded_strides: Vec<usize> = vec![strides[count - 1]];
324    folded_strides.extend_from_slice(&strides[count..]);
325
326    *tensor.meta = Metadata::new(Shape::from(folded_shape), Strides::new(&folded_strides));
327}
328
329/// Reduce with a logical instruction ([`Any`](ReduceOperationConfig::Any) /
330/// [`All`](ReduceOperationConfig::All)) and return the result as a boolean tensor.
331///
332/// `Any` / `All` require the output dtype (like `Arg*` index outputs): the
333/// kernel writes the `0/1` flags directly into the numeric backing of the
334/// boolean storage (cubek has no bool elem), so the only step left here is the
335/// kernel-free relabel to `Bool`.
336///
337/// `dim == None` reduces the whole tensor to a scalar; `Some(dim)` reduces a
338/// single axis, keeping it with length 1.
339pub fn reduce_logical(
340    tensor: CubeTensor,
341    dim: Option<usize>,
342    config: ReduceOperationConfig,
343    out_dtype: BoolDType,
344) -> CubeTensor {
345    debug_assert!(
346        matches!(
347            config,
348            ReduceOperationConfig::Any | ReduceOperationConfig::All
349        ),
350        "reduce_logical only supports Any / All, got {config:?}"
351    );
352    let out_bool = DType::Bool(out_dtype);
353    let backing = elem_type_to_dtype(dtype_to_elem_type(out_bool));
354
355    let mut out = match dim {
356        Some(d) => reduce_dim(tensor, Some(backing), d, Default::default(), config),
357        None => reduce(tensor, Some(backing), Default::default(), config),
358    }
359    .expect("Any/All reduce on a valid axis cannot fail");
360
361    out.dtype = out_bool; // same storage, relabel as Bool (no kernel)
362    out
363}
364
365/// Accumulator slots one reduction needs: `k` for top-k, `1` for every other operation.
366///
367/// Shared memory scales with it, so a routine that fits at one length can overrun the
368/// device limit at another. That makes it part of the fused autotune key as well as the
369/// output length along the reduced axis.
370pub(crate) fn accumulator_len(config: ReduceOperationConfig) -> usize {
371    match config {
372        ReduceOperationConfig::TopK(k) | ReduceOperationConfig::ArgTopK(k) => k,
373        _ => 1,
374    }
375}
376
377fn argsort(shape: &[usize]) -> Vec<usize> {
378    let mut indices = (0..shape.len()).collect::<Vec<_>>();
379    indices.sort_by_key(|&i| &shape[i]);
380    indices
381}
382
383/// Reduce the given `axis` of the `input` tensor using the instruction `Rd` and the given [Strategy](ReduceStrategy).
384///
385/// Return an error if `strategy` is `Specific(strategy)` and the specified strategy is not supported by the `client`.
386/// Also returns an error if the `axis` is larger than the `input` rank or if the shape of `output` is invalid.
387///
388/// If there is no error, the output is a tensor with decreasing strides
389/// where the shape of reduced dim is set to 1 but all shape are similar to the input.
390pub fn reduce_dim(
391    input: CubeTensor,
392    output_dtype: Option<DType>,
393    dim: usize,
394    strategy: KernelReduceStrategy,
395    config: ReduceOperationConfig,
396) -> Result<CubeTensor, cubek::reduce::ReduceError> {
397    let input = crate::kernel::untile(input);
398    debug_assert!(
399        !matches!(
400            config,
401            ReduceOperationConfig::ArgMax
402                | ReduceOperationConfig::ArgMin
403                | ReduceOperationConfig::ArgTopK(_)
404                | ReduceOperationConfig::Any
405                | ReduceOperationConfig::All
406        ) || output_dtype.is_some(),
407        "The `output_dtype` has to be `Some` when the `config` is `ArgMax`, `ArgMin`, `ArgTopK`, `Any` or `All`.
408        "
409    );
410
411    let accumulator_len = accumulator_len(config);
412    let dtypes = config.precision(
413        dtype_to_elem_type(input.dtype),
414        output_dtype.map(dtype_to_elem_type),
415    );
416    let client = input.client.clone();
417    let output = init_reduce_output(&input, dim, &dtypes, accumulator_len).ok_or(
418        cubek::reduce::ReduceError::InvalidAxis {
419            axis: dim,
420            rank: input.meta.num_dims(),
421        },
422    )?;
423
424    // `output` already carries the right shape here, with `dim` set to `accumulator_len`.
425    let axis_length = input.meta.shape[dim];
426    if axis_length == 0 {
427        return reduce_empty_axis(output, axis_length, config);
428    }
429    // A zero-length axis other than `dim` leaves no output to compute, and cubek cannot size a
430    // launch over zero output units. A `k` too large for the axis still goes to cubek to be rejected.
431    if output.meta.num_elements() == 0 && axis_length >= accumulator_len {
432        return Ok(output);
433    }
434
435    let result = match strategy {
436        KernelReduceStrategy::Unspecified => cubek::reduce::reduce(
437            &client,
438            input.binding(),
439            output.clone().binding(),
440            dim,
441            ReduceStrategy {
442                routine: RoutineStrategy::Unit(BlueprintStrategy::Inferred(UnitStrategy)),
443                vectorization: VectorizationStrategy {
444                    parallel_output_vectorization: false,
445                },
446                autotune_level: Default::default(),
447            },
448            config,
449            dtypes,
450        ),
451        KernelReduceStrategy::Specific(strategy) => cubek::reduce::reduce(
452            &client,
453            input.binding(),
454            output.clone().binding(),
455            dim,
456            strategy,
457            config,
458            dtypes,
459        ),
460        #[cfg(feature = "autotune")]
461        KernelReduceStrategy::Autotune => {
462            autotune_reduce(&client, input, output.clone(), dim, config, dtypes);
463            Ok(())
464        }
465    };
466    result.map(|_| output)
467}
468
469/// Reduce the given `axis` of `input`, returning the values **and** their indices from a
470/// single kernel launch.
471///
472/// Running the value reduction and its `Arg*` counterpart separately walks the input twice
473/// and discards half of each result, even though one reduction already computes both. The
474/// reduce kernels are memory bound, so folding the two launches into one roughly halves
475/// the work.
476///
477/// `config` must be an operation with a meaningful index (top-k, max, min); each `Arg*`
478/// config is an alias of its value counterpart here, since both halves are written either
479/// way. Any other operation returns [`ReduceError::IndicesUnsupported`]. Both outputs are
480/// contiguous with the reduced `dim` set to `k` for top-k and `1` otherwise.
481pub fn reduce_dim_with_indices(
482    input: CubeTensor,
483    indices_dtype: DType,
484    dim: usize,
485    strategy: KernelReduceStrategy,
486    config: ReduceOperationConfig,
487) -> Result<(CubeTensor, CubeTensor), ReduceError> {
488    let unsupported = |operation| ReduceError::IndicesUnsupported { operation };
489
490    // Fold each `Arg*` onto its value counterpart: `precision` would otherwise demand an
491    // output dtype, which here only ever applies to the indices.
492    let config = match config {
493        ReduceOperationConfig::ArgMax => ReduceOperationConfig::Max,
494        ReduceOperationConfig::ArgMin => ReduceOperationConfig::Min,
495        ReduceOperationConfig::ArgTopK(k) => ReduceOperationConfig::TopK(k),
496        ReduceOperationConfig::Max
497        | ReduceOperationConfig::Min
498        | ReduceOperationConfig::TopK(_) => config,
499        ReduceOperationConfig::Sum => return Err(unsupported("Sum")),
500        ReduceOperationConfig::Prod => return Err(unsupported("Prod")),
501        ReduceOperationConfig::Mean => return Err(unsupported("Mean")),
502        ReduceOperationConfig::MaxAbs => return Err(unsupported("MaxAbs")),
503        ReduceOperationConfig::Any => return Err(unsupported("Any")),
504        ReduceOperationConfig::All => return Err(unsupported("All")),
505    };
506
507    let out_len = accumulator_len(config);
508
509    // `precision` for these operations keeps input/values/accumulation at the input
510    // dtype; the index dtype is the caller's and is converted for free in the final
511    // output write.
512    let value_dtypes = config.precision(dtype_to_elem_type(input.dtype), None);
513    let dtypes = ReduceWithIndicesDtypes {
514        input: value_dtypes.input,
515        values: value_dtypes.output,
516        indices: dtype_to_elem_type(indices_dtype),
517        accumulation: value_dtypes.accumulation,
518    };
519
520    let invalid_axis = || ReduceError::InvalidAxis {
521        axis: dim,
522        rank: input.meta.num_dims(),
523    };
524
525    let values = init_reduce_output_dtype(&input, dim, elem_type_to_dtype(dtypes.values), out_len)
526        .ok_or_else(invalid_axis)?;
527    let indices =
528        init_reduce_output_dtype(&input, dim, indices_dtype, out_len).ok_or_else(invalid_axis)?;
529
530    // Every `config` reaching this point is an extremum, so an empty axis leaves it with no value
531    // to report and no index to name. Always rejected, including when the outputs are themselves
532    // empty - see `reduce_empty_axis`.
533    if input.meta.shape[dim] == 0 {
534        return Err(ReduceError::ReduceAxisTooSmall {
535            axis_length: 0,
536            k: out_len,
537        });
538    }
539    // Same as in `reduce_dim`: an empty output has nothing to compute.
540    if values.meta.num_elements() == 0 && input.meta.shape[dim] >= out_len {
541        return Ok((values, indices));
542    }
543
544    let client = input.client.clone();
545
546    let result = match strategy {
547        KernelReduceStrategy::Unspecified => cubek::reduce::reduce_with_indices(
548            &client,
549            input.binding(),
550            values.clone().binding(),
551            indices.clone().binding(),
552            dim,
553            ReduceStrategy {
554                routine: RoutineStrategy::Unit(BlueprintStrategy::Inferred(UnitStrategy)),
555                vectorization: VectorizationStrategy {
556                    parallel_output_vectorization: false,
557                },
558                autotune_level: Default::default(),
559            },
560            config,
561            dtypes,
562        ),
563        KernelReduceStrategy::Specific(strategy) => cubek::reduce::reduce_with_indices(
564            &client,
565            input.binding(),
566            values.clone().binding(),
567            indices.clone().binding(),
568            dim,
569            strategy,
570            config,
571            dtypes,
572        ),
573        #[cfg(feature = "autotune")]
574        KernelReduceStrategy::Autotune => {
575            autotune_reduce_with_indices(
576                &client,
577                input,
578                values.clone(),
579                indices.clone(),
580                dim,
581                config,
582                dtypes,
583            );
584            Ok(())
585        }
586    };
587
588    result.map(|_| (values, indices))
589}
590
591/// Creates an empty output tensor with the proper shape and decreasing strides to reduce the given `axis` of `input`
592/// or return `None` if `axis` is out-of-bound.
593pub fn init_reduce_output(
594    input: &CubeTensor,
595    dim: usize,
596    dtypes: &ReduceDtypes,
597    accumulator_len: usize,
598) -> Option<CubeTensor> {
599    init_reduce_output_dtype(
600        input,
601        dim,
602        elem_type_to_dtype(dtypes.output),
603        accumulator_len,
604    )
605}
606
607/// Like [`init_reduce_output`], but with the output dtype given directly rather than taken
608/// from a [`ReduceDtypes`]. Needed when one reduce writes two outputs of different dtypes.
609pub fn init_reduce_output_dtype(
610    input: &CubeTensor,
611    dim: usize,
612    dtype: DType,
613    accumulator_len: usize,
614) -> Option<CubeTensor> {
615    (dim < input.meta.num_dims()).then(|| {
616        let mut shape_out = input.shape();
617        shape_out[dim] = accumulator_len;
618        empty_device_contiguous_dtype(input.client.clone(), input.device.clone(), shape_out, dtype)
619    })
620}
621
622/// Select a strategy to perform a reduction.
623#[derive(Clone, Debug)]
624pub enum KernelReduceStrategy {
625    /// Use a best-effort strategy based on the hardware capacity.
626    /// This differs from Autotune as it doesn't try and compare many strategies to select the best.
627    Unspecified,
628    /// Fix the exact strategy for the reduction.
629    Specific(cubek::reduce::launch::ReduceStrategy),
630    /// Use autotune to find the best strategy given the hardware and the inputs.
631    #[cfg(feature = "autotune")]
632    Autotune,
633}
634
635impl Default for KernelReduceStrategy {
636    fn default() -> Self {
637        #[cfg(feature = "autotune")]
638        return Self::Autotune;
639
640        #[cfg(not(feature = "autotune"))]
641        return Self::Unspecified;
642    }
643}