Skip to main content

burn_cubecl/kernel/reduce/
tune.rs

1#![allow(missing_docs)]
2
3use super::{SumAutotuneKey, accumulator_len};
4use crate::{
5    CubeAutotuneKey, CubeTuneId,
6    kernel::reduce::bounds::{
7        with_reduce_bounds, with_reduce_with_indices_bounds, with_sum_bounds,
8    },
9    tensor::CubeTensor,
10};
11use burn_backend::cubecl::dtype_to_elem_type;
12use cubecl::{
13    AutotuneKey,
14    client::Client,
15    tune::{LocalTuner, Tunable, TunableSet, TuneGroup, local_tuner},
16};
17use cubek::reduce::{
18    ReduceDtypes, ReduceStrategy, ReduceWithIndicesDtypes,
19    components::instructions::ReduceOperationConfig,
20    launch::{RoutineStrategy, VectorizationStrategy, tune_key::ReduceAutotuneKey},
21    routines::{BlueprintStrategy, cube::CubeStrategy, plane::PlaneStrategy, unit::UnitStrategy},
22};
23use serde::{Deserialize, Serialize};
24
25/// Autotune key for both dim-reduce tuners, values-only and fused values+indices.
26///
27/// [`ReduceAutotuneKey`] alone would put max/min and every top-k on the same entry: it
28/// carries the input shape and dtypes, but not the accumulator length. Since shared memory
29/// scales with that length, a routine benchmarked at one length can be infeasible at
30/// another, and the tuner's cached fast path panics rather than falling back.
31#[derive(Hash, Eq, PartialEq, Debug, Clone, Serialize, Deserialize, AutotuneKey)]
32pub struct ReduceDimAutotuneKey {
33    /// The accumulator slots per reduction (`k` for top-k, `1` otherwise).
34    accumulator_len: usize,
35    /// The shape and dtypes of the reduction itself.
36    reduce: ReduceAutotuneKey,
37}
38
39/// The configured autotune level, read once — [`ReduceStrategy`] carries it
40/// so the kernel blueprints know whether raw shapes are their own keys.
41fn autotune_level() -> cubecl::config::autotune::AutotuneLevel {
42    use cubecl::config::{CubeClRuntimeConfig, RuntimeConfig};
43    static LEVEL: std::sync::OnceLock<cubecl::config::autotune::AutotuneLevel> =
44        std::sync::OnceLock::new();
45    LEVEL
46        .get_or_init(|| CubeClRuntimeConfig::get().autotune.level.clone())
47        .clone()
48}
49
50/// Registers the routine-selection tunables shared by [`autotune_reduce`] and
51/// [`autotune_reduce_with_indices`]: one tunable per (routine, vectorization) pair,
52/// in the same groups with the same priorities. Only the launched kernel differs,
53/// injected through `launch`; keeping the grid in one place keeps the two tuners
54/// from drifting apart when routines or priorities are adjusted.
55fn with_routine_tunables<In, Launch>(
56    mut set: TunableSet<ReduceDimAutotuneKey, In, ()>,
57    group_suffix: &str,
58    launch: Launch,
59) -> TunableSet<ReduceDimAutotuneKey, In, ()>
60where
61    In: Clone + Send + Sync + 'static,
62    Launch: Fn(ReduceStrategy, In) -> Result<(), String> + Clone + Send + Sync + 'static,
63{
64    const PRIORITY_MAX: i8 = 2;
65    const PRIORITY_MIN: i8 = 1;
66    const PRIORITY_SKIP: i8 = -1;
67
68    let default_group =
69        TuneGroup::<ReduceDimAutotuneKey>::new(&format!("default_{group_suffix}"), |_key| {
70            PRIORITY_MAX
71        });
72    let vectorized_parallel_group = TuneGroup::<ReduceDimAutotuneKey>::new(
73        &format!("vectorized_parallel_{group_suffix}"),
74        |key| {
75            if key.reduce.axis_is_contiguous {
76                PRIORITY_MAX
77            } else {
78                // We disable the tunable with the setting [vector_size.parallel_output_vectorization]
79                // when the reduce isn't parallel, since it would duplicate tunables.
80                PRIORITY_SKIP
81            }
82        },
83    );
84
85    enum ReduceProps {
86        GreatWithLowReduceCount,
87        GreatWithHighReduceCount,
88        Balanced,
89    }
90
91    for (vectorization, vector_size_ident) in [
92        (
93            VectorizationStrategy {
94                parallel_output_vectorization: true,
95            },
96            "_vectorized_parallel_reduce",
97        ),
98        (
99            VectorizationStrategy {
100                parallel_output_vectorization: false,
101            },
102            "",
103        ),
104    ] {
105        for (name, routine, props) in [
106            (
107                "unit",
108                RoutineStrategy::Unit(BlueprintStrategy::Inferred(UnitStrategy)),
109                ReduceProps::GreatWithHighReduceCount,
110            ),
111            (
112                "plane",
113                RoutineStrategy::Plane(BlueprintStrategy::Inferred(PlaneStrategy {
114                    independent: true,
115                })),
116                ReduceProps::Balanced,
117            ),
118            (
119                "cube",
120                RoutineStrategy::Cube(BlueprintStrategy::Inferred(CubeStrategy {
121                    use_planes: true,
122                })),
123                ReduceProps::GreatWithLowReduceCount,
124            ),
125        ] {
126            let name = format!("{name}{vector_size_ident}");
127            let launch = launch.clone();
128            let mut tunable = Tunable::new(&name, move |inputs| {
129                let strategy = ReduceStrategy {
130                    routine: routine.clone(),
131                    vectorization,
132                    // Routes the configured level into the blueprint:
133                    // unchecked comptime fast paths are only stable
134                    // (and only taken) when every raw shape is its
135                    // own key.
136                    autotune_level: autotune_level(),
137                };
138                launch(strategy, inputs)
139            });
140            if vectorization.parallel_output_vectorization {
141                tunable = tunable.group(&vectorized_parallel_group, |_| PRIORITY_MAX);
142            }
143
144            tunable = tunable.group(&default_group, move |key| match props {
145                ReduceProps::GreatWithLowReduceCount => {
146                    if key.reduce.vector_count < 128 {
147                        PRIORITY_MAX
148                    } else {
149                        // When you have a high level of vector to reduce, it is normally
150                        // better to use another routine.
151                        PRIORITY_MIN
152                    }
153                }
154                ReduceProps::GreatWithHighReduceCount => {
155                    if key.reduce.vector_count > 64 {
156                        PRIORITY_MAX
157                    } else {
158                        // Bellow 64 it is normally better to use another routine
159                        PRIORITY_MIN
160                    }
161                }
162                ReduceProps::Balanced => PRIORITY_MAX,
163            });
164            set = set.with(tunable);
165        }
166    }
167
168    set
169}
170
171/// Executes autotune on reduce operations.
172pub fn autotune_reduce(
173    client: &Client,
174    input: CubeTensor,
175    output: CubeTensor,
176    axis: usize,
177    config: ReduceOperationConfig,
178    dtypes: ReduceDtypes,
179) {
180    use reduce_ops::*;
181
182    static TUNER: LocalTuner<ReduceDimAutotuneKey, CubeTuneId> = local_tuner!("reduce-dim");
183
184    let tune_id = CubeTuneId::new(&input.client, &input.device);
185    let tunables = TUNER.init(&tune_id, || {
186        with_routine_tunables(
187            with_reduce_bounds(TunableSet::new(create_key, reduce_input_gen)),
188            "reduce",
189            |strategy,
190             (input, output, axis, config, dtypes): (
191                CubeTensor,
192                CubeTensor,
193                usize,
194                ReduceOperationConfig,
195                ReduceDtypes,
196            )| {
197                cubek::reduce::reduce(
198                    &output.client,
199                    input.binding(),
200                    output.clone().binding(),
201                    axis,
202                    strategy,
203                    config,
204                    dtypes,
205                )
206                .map_err(|e| format!("{e}"))
207            },
208        )
209    });
210
211    TUNER.execute(
212        &tune_id,
213        client,
214        tunables,
215        (input, output, axis, config, dtypes),
216    );
217}
218
219pub(crate) fn create_key(
220    (input, output, axis, config, dtypes): &(
221        CubeTensor,
222        CubeTensor,
223        usize,
224        ReduceOperationConfig,
225        ReduceDtypes,
226    ),
227) -> ReduceDimAutotuneKey {
228    let elem_input = dtype_to_elem_type(input.dtype);
229    let elem_output = dtype_to_elem_type(output.dtype);
230    let elem_acc = dtypes.accumulation;
231
232    ReduceDimAutotuneKey::new(
233        accumulator_len(*config),
234        ReduceAutotuneKey::generate(
235            elem_input,
236            elem_output,
237            elem_acc,
238            input.meta.shape(),
239            input.meta.strides()[*axis] == 1,
240            *axis,
241        ),
242    )
243}
244
245/// Executes autotune on the fused top-k that writes values and indices at once.
246///
247/// This needs its own tuner rather than reusing [`autotune_reduce`]: the tunables launch a
248/// different kernel with an extra output, so their timings are not interchangeable with the
249/// single-output ones. Without it the fused path would have to hardcode a routine, and
250/// picking wrong costs more than fusing saves: the unit routine is several times slower
251/// than plane/cube on a large reduce, which would make one fused launch lose to the two
252/// autotuned launches it replaces.
253#[allow(clippy::too_many_arguments)]
254pub fn autotune_reduce_with_indices(
255    client: &Client,
256    input: CubeTensor,
257    values: CubeTensor,
258    indices: CubeTensor,
259    axis: usize,
260    config: ReduceOperationConfig,
261    dtypes: ReduceWithIndicesDtypes,
262) {
263    use reduce_with_indices_ops::*;
264
265    static TUNER: LocalTuner<ReduceDimAutotuneKey, CubeTuneId> =
266        local_tuner!("reduce-dim-with-indices");
267
268    let tune_id = CubeTuneId::new(&input.client, &input.device);
269    let tunables = TUNER.init(&tune_id, || {
270        with_routine_tunables(
271            with_reduce_with_indices_bounds(TunableSet::new(
272                create_key_with_indices,
273                reduce_with_indices_input_gen,
274            )),
275            "reduce_with_indices",
276            |strategy,
277             (input, values, indices, axis, config, dtypes): (
278                CubeTensor,
279                CubeTensor,
280                CubeTensor,
281                usize,
282                ReduceOperationConfig,
283                ReduceWithIndicesDtypes,
284            )| {
285                cubek::reduce::reduce_with_indices(
286                    &values.client,
287                    input.binding(),
288                    values.clone().binding(),
289                    indices.clone().binding(),
290                    axis,
291                    strategy,
292                    config,
293                    dtypes,
294                )
295                .map_err(|e| format!("{e}"))
296            },
297        )
298    });
299
300    TUNER.execute(
301        &tune_id,
302        client,
303        tunables,
304        (input, values, indices, axis, config, dtypes),
305    );
306}
307
308pub(crate) fn create_key_with_indices(
309    (input, values, _indices, axis, config, dtypes): &(
310        CubeTensor,
311        CubeTensor,
312        CubeTensor,
313        usize,
314        ReduceOperationConfig,
315        ReduceWithIndicesDtypes,
316    ),
317) -> ReduceDimAutotuneKey {
318    let elem_input = dtype_to_elem_type(input.dtype);
319    let elem_output = dtype_to_elem_type(values.dtype);
320    let elem_acc = dtypes.accumulation;
321
322    ReduceDimAutotuneKey::new(
323        accumulator_len(*config),
324        ReduceAutotuneKey::generate(
325            elem_input,
326            elem_output,
327            elem_acc,
328            input.meta.shape(),
329            input.meta.strides()[*axis] == 1,
330            *axis,
331        ),
332    )
333}
334
335mod reduce_with_indices_ops {
336    #![allow(missing_docs)]
337
338    use super::*;
339
340    pub(crate) fn reduce_with_indices_input_gen(
341        _key: &ReduceDimAutotuneKey,
342        (input, values, indices, dim, config, dtypes): &(
343            CubeTensor,
344            CubeTensor,
345            CubeTensor,
346            usize,
347            ReduceOperationConfig,
348            ReduceWithIndicesDtypes,
349        ),
350    ) -> (
351        CubeTensor,
352        CubeTensor,
353        CubeTensor,
354        usize,
355        ReduceOperationConfig,
356        ReduceWithIndicesDtypes,
357    ) {
358        (
359            input.clone(),
360            values.copy(),
361            indices.copy(),
362            *dim,
363            *config,
364            *dtypes,
365        )
366    }
367}
368
369mod reduce_ops {
370    #![allow(missing_docs)]
371
372    use cubek::reduce::ReduceDtypes;
373
374    use super::*;
375
376    pub(crate) fn reduce_input_gen(
377        _key: &ReduceDimAutotuneKey,
378        (input, output, dim, config, dtypes): &(
379            CubeTensor,
380            CubeTensor,
381            usize,
382            ReduceOperationConfig,
383            ReduceDtypes,
384        ),
385    ) -> (
386        CubeTensor,
387        CubeTensor,
388        usize,
389        ReduceOperationConfig,
390        ReduceDtypes,
391    ) {
392        (input.clone(), output.copy(), *dim, *config, *dtypes)
393    }
394}
395
396/// Executes autotune on reduce operations.
397#[cfg(feature = "autotune")]
398pub fn autotune_sum(client: &Client, input: CubeTensor) -> CubeTensor {
399    use sum_ops::*;
400
401    static TUNER: LocalTuner<CubeAutotuneKey, CubeTuneId> = local_tuner!("autotune-sum");
402
403    let tune_id = CubeTuneId::new(&input.client, &input.device);
404    let tunables = TUNER.init(&tune_id, || {
405        with_sum_bounds(TunableSet::new(create_key_sum, sum_input_gen))
406            .with(Tunable::new("sum_chained", sum_chained))
407            .with(Tunable::new("sum_one_shot", sum_one_shot::<1>))
408            .with(Tunable::new("sum_one_shot", sum_one_shot::<2>))
409            .with(Tunable::new("sum_one_shot", sum_one_shot::<4>))
410            .with(Tunable::new("sum_one_shot", sum_one_shot::<8>))
411            .with(Tunable::new("sum_one_shot", sum_one_shot::<16>))
412            .with(Tunable::new("sum_one_shot", sum_one_shot::<32>))
413            .with(Tunable::new("sum_one_shot", sum_one_shot::<64>))
414    });
415
416    TUNER.execute(&tune_id, client, tunables, input)
417}
418
419pub(crate) fn create_key_sum(input: &CubeTensor) -> CubeAutotuneKey {
420    CubeAutotuneKey::Sum(SumAutotuneKey::generate(input))
421}
422
423impl SumAutotuneKey {
424    #[allow(unused)]
425    pub(crate) fn generate(input: &CubeTensor) -> Self {
426        let dtype = input.dtype;
427        let length = input.meta.num_elements();
428        Self::new(dtype, length)
429    }
430}
431mod sum_ops {
432    #![allow(missing_docs)]
433    use crate::ops::numeric::zeros_client;
434
435    use super::*;
436
437    pub(crate) fn sum_input_gen(_key: &CubeAutotuneKey, input: &CubeTensor) -> CubeTensor {
438        input.clone()
439    }
440
441    pub(crate) fn sum_one_shot<const C: u32>(input: CubeTensor) -> Result<CubeTensor, String> {
442        let client = input.client.clone();
443        let device = input.device.clone();
444        let output = zeros_client(client.clone(), device, [1].into(), input.dtype);
445        let dtype = input.dtype;
446
447        cubek::reduce::shared_sum(
448            &output.client,
449            input.binding(),
450            output.clone().binding(),
451            C,
452            dtype_to_elem_type(dtype),
453        )
454        .map_err(|e| e.to_string())
455        .map(|_| output)
456    }
457
458    #[cfg(feature = "autotune")]
459    pub(crate) fn sum_chained(input: CubeTensor) -> Result<CubeTensor, String> {
460        crate::kernel::reduce::reduce(
461            input,
462            None,
463            crate::kernel::reduce::KernelReduceStrategy::Autotune,
464            cubek::reduce::components::instructions::ReduceOperationConfig::Sum,
465        )
466        .map_err(|e| e.to_string())
467    }
468}