Skip to main content

burn_cubecl_fusion/optim/reduce/
optimization.rs

1use super::args::{
2    FusedReduceInput, FusedReduceInputLaunch, FusedReduceOutput, FusedReduceOutputLaunch,
3};
4#[cfg(feature = "autotune")]
5use super::tune::fused_reduce_autotune;
6use crate::{
7    CubeFusionHandle, FallbackOperation,
8    engine::{
9        codegen::ir::{
10            FuseArg, FuseBlockConfig, FuseType, GlobalArgsLaunch, RefLayout,
11            multi_block_variables_init,
12        },
13        launch::{
14            FuseTraceLauncher,
15            runner::{TraceRunner, Vectorization},
16        },
17        trace::{FuseTrace, TraceError, TuneOutput},
18    },
19    optim::{FusedOperation, elemwise::ElemwiseRunner, reduce::args::FusedReduceArgs},
20};
21use burn_backend::cubecl::{dtype_to_storage_type, elem_type_to_dtype};
22use burn_fusion::stream::Context;
23use burn_ir::ReduceDimOpIr;
24use burn_std::DType;
25use cubecl::{client::Client, prelude::*};
26use cubek::reduce::{
27    ReduceDtypes, ReduceError, VectorizationMode,
28    components::instructions::ReduceOperationConfig,
29    init_tensors,
30    launch::{RoutineStrategy, reduce_kernel_virtual},
31    output_vectorization_axis,
32    routines::{
33        ReduceBlueprint, ReduceLaunchSettings, ReduceProblem, ReduceVectorSettings, Routine,
34        cube::CubeRoutine, plane::PlaneRoutine, unit::UnitRoutine,
35    },
36};
37use serde::{Deserialize, Serialize};
38use std::sync::Arc;
39
40#[cfg(not(feature = "autotune"))]
41use cubek::reduce::routines::{BlueprintStrategy, unit::UnitStrategy};
42
43pub struct ReduceOptimization {
44    pub(crate) info: Arc<ReduceOptimizationInfo>,
45}
46
47pub(crate) struct ReduceOptimizationInfo {
48    pub(crate) trace: FuseTrace,
49    trace_read_fallback: FuseTrace,
50    trace_write_fallback: FuseTrace,
51    pub(crate) client: Client,
52    pub(crate) device: cubecl::Device,
53    pub(crate) len: usize,
54    pub(crate) len_read: usize,
55    pub(crate) reduce: FusedReduce,
56    settings: ReduceSettings,
57}
58
59impl ReduceOptimizationInfo {
60    pub fn from_state(device: &cubecl::Device, state: ReduceOptimizationState) -> Self {
61        let client = device.client();
62
63        Self {
64            trace: state.trace,
65            trace_read_fallback: state.trace_read_fallback,
66            trace_write_fallback: state.trace_write_fallback,
67            client,
68            device: device.clone(),
69            len: state.len,
70            len_read: state.len_read,
71            reduce: state.reduce,
72            settings: state.settings,
73        }
74    }
75    pub fn to_state(&self) -> ReduceOptimizationState {
76        ReduceOptimizationState {
77            trace: self.trace.clone(),
78            trace_read_fallback: self.trace_read_fallback.clone(),
79            trace_write_fallback: self.trace_write_fallback.clone(),
80            len: self.len,
81            len_read: self.len_read,
82            reduce: self.reduce.clone(),
83            settings: self.settings,
84        }
85    }
86}
87
88#[derive(Serialize, Deserialize, Copy, Clone)]
89pub enum ReduceSettings {
90    Always,
91    /// We only activate fuse-on-write when the reduction isn't on the last dimension, otherwise
92    /// vectorization is impossible. Only [VectorizationMode::Perpendicular] supports vectorization.
93    ///
94    /// We could still fuse some output operations, but it would probably lead to worse performance.
95    OnlyParallel,
96    Never,
97}
98
99pub(crate) struct ReduceOptimizationTuneArg {
100    pub(crate) info: Arc<ReduceOptimizationInfo>,
101    pub(crate) fallback: Arc<Box<dyn FallbackOperation>>,
102}
103
104impl Clone for ReduceOptimizationTuneArg {
105    fn clone(&self) -> Self {
106        Self {
107            info: self.info.clone(),
108            fallback: self.fallback.clone(),
109        }
110    }
111}
112
113#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
114pub enum ReduceInstruction {
115    ArgMax,
116    ArgMin,
117    Mean,
118    Prod,
119    Sum,
120    Max,
121    Min,
122    MaxAbs,
123    Any,
124    All,
125}
126
127pub trait ReduceFallbackFn: Send + Sync {
128    fn run(&self, context: &mut Context<CubeFusionHandle>);
129}
130
131#[derive(Serialize, Deserialize)]
132pub struct ReduceOptimizationState {
133    pub(crate) trace: FuseTrace,
134    pub(crate) trace_read_fallback: FuseTrace,
135    pub(crate) trace_write_fallback: FuseTrace,
136    pub(crate) reduce: FusedReduce,
137    pub(crate) len: usize,
138    pub(crate) len_read: usize,
139    pub(crate) settings: ReduceSettings,
140}
141
142impl core::fmt::Debug for ReduceOptimizationState {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        f.write_fmt(format_args!(
145            "{{ len_read: {}, len_total: {} }}",
146            self.len_read, self.len
147        ))
148    }
149}
150
151#[derive(Clone, Debug, Serialize, Deserialize)]
152pub struct FusedReduce {
153    pub(crate) input: FuseArg,
154    pub(crate) output: FuseArg,
155    pub(crate) acc: FuseType,
156    pub(crate) axis: usize,
157    pub(crate) op: ReduceDimOpIr,
158    pub(crate) use_planes: bool,
159    pub(crate) shared: bool,
160    pub(crate) inst: ReduceInstruction,
161}
162
163#[derive(new)]
164pub struct FusedReduceLaunch<'a> {
165    reduce: &'a FusedReduce,
166    strategy: RoutineStrategy,
167}
168
169#[derive(Debug)]
170pub enum FusedReduceError {
171    Reduce(ReduceError),
172    InvalidSelection(Box<&'static str>),
173    InvalidInput,
174}
175
176impl From<ReduceError> for FusedReduceError {
177    fn from(value: ReduceError) -> Self {
178        Self::Reduce(value)
179    }
180}
181
182impl ReduceOptimizationTuneArg {
183    pub fn execute_fused(
184        &self,
185        context: &mut Context<CubeFusionHandle>,
186        strategy: RoutineStrategy,
187    ) -> Result<TuneOutput, TraceError<FusedReduceError>> {
188        let launch = FusedReduceLaunch::new(&self.info.reduce, strategy);
189        let launcher = FuseTraceLauncher::new(&self.info.trace, &launch);
190        launcher.launch(&self.info.client, &self.info.device, context)
191    }
192
193    pub fn execute_fallback(&self, context: &mut Context<CubeFusionHandle>) -> TuneOutput {
194        let launcher = FuseTraceLauncher::new(&self.info.trace_read_fallback, &ElemwiseRunner);
195
196        #[allow(unused_mut)] // It is used when `autotune-checks` is activated.
197        let mut output_read = launcher
198            .launch(&self.info.client, &self.info.device, context)
199            .unwrap();
200
201        self.fallback.run(context);
202
203        #[cfg(feature = "autotune-checks")]
204        if let TuneOutput::Checked { handles } = &mut output_read {
205            let out_desc = context.tensors.get(&self.info.reduce.op.out.id).unwrap();
206            let handle_out = context
207                .handles
208                .get_handle(&out_desc.id, &burn_ir::TensorStatus::ReadOnly);
209
210            handles.insert(
211                self.info.reduce.op.out.id,
212                (out_desc.shape.clone(), handle_out.clone()),
213            );
214        }
215
216        let launcher = FuseTraceLauncher::new(&self.info.trace_write_fallback, &ElemwiseRunner);
217
218        let output_write = launcher
219            .launch(&self.info.client, &self.info.device, context)
220            .unwrap();
221
222        output_read.merge(output_write)
223    }
224}
225
226#[allow(clippy::too_many_arguments)]
227impl ReduceOptimization {
228    pub fn new(
229        trace: FuseTrace,
230        trace_read_fallback: FuseTrace,
231        trace_write_fallback: FuseTrace,
232        client: Client,
233        device: cubecl::Device,
234        len: usize,
235        len_read: usize,
236        reduce: FusedReduce,
237        settings: ReduceSettings,
238    ) -> Self {
239        let info = ReduceOptimizationInfo {
240            trace,
241            trace_read_fallback,
242            trace_write_fallback,
243            client,
244            device,
245            len,
246            len_read,
247            reduce,
248            settings,
249        };
250
251        Self {
252            info: Arc::new(info),
253        }
254    }
255    /// Execute the optimization.
256    pub fn execute(
257        &mut self,
258        context: &mut Context<CubeFusionHandle>,
259        fallback: impl FnOnce(usize) -> Box<dyn FallbackOperation>,
260    ) {
261        // The index of the fallback reduce is the number of ops fused as read.
262        let fallback = fallback(self.info.len_read);
263        let arg = ReduceOptimizationTuneArg {
264            info: self.info.clone(),
265            fallback: Arc::new(fallback),
266        };
267
268        // Same as `ReduceBroadcastedOptimization::execute`: an empty input leaves the fused
269        // kernels either an axis they reject or nothing to launch over.
270        let input = context.tensors.get(&self.info.reduce.op.input.id).unwrap();
271        if input.shape.num_elements() == 0 {
272            arg.execute_fallback(context);
273            return;
274        }
275
276        #[cfg(feature = "autotune")]
277        fused_reduce_autotune(arg, context);
278
279        #[cfg(not(feature = "autotune"))]
280        if arg
281            .execute_fused(
282                context,
283                RoutineStrategy::Unit(BlueprintStrategy::Inferred(UnitStrategy)),
284            )
285            .is_err()
286        {
287            arg.execute_fallback(context);
288        }
289    }
290
291    pub fn num_output_buffers(&self) -> usize {
292        self.info.trace_read_fallback.resources.outputs.len()
293    }
294
295    pub fn to_state(&self) -> ReduceOptimizationState {
296        ReduceOptimizationState {
297            trace: self.info.trace.clone(),
298            trace_read_fallback: self.info.trace_read_fallback.clone(),
299            trace_write_fallback: self.info.trace_write_fallback.clone(),
300            reduce: self.info.reduce.clone(),
301            len: self.info.len,
302            len_read: self.info.len_read,
303            settings: self.info.settings,
304        }
305    }
306
307    pub fn from_state(device: &cubecl::Device, state: ReduceOptimizationState) -> Self {
308        let client = device.client();
309
310        let info = ReduceOptimizationInfo {
311            trace: state.trace,
312            trace_read_fallback: state.trace_read_fallback,
313            trace_write_fallback: state.trace_write_fallback,
314            reduce: state.reduce,
315            len: state.len,
316            len_read: state.len_read,
317            client,
318            device: device.clone(),
319            settings: state.settings,
320        };
321
322        Self {
323            info: Arc::new(info),
324        }
325    }
326
327    /// Returns the number of output buffers added by fusion.
328    pub fn num_ops_fused(&self) -> usize {
329        self.info.len
330    }
331}
332
333// TODO: Implement better vectorization here.
334impl Vectorization for FusedReduceLaunch<'_> {}
335
336impl TraceRunner for FusedReduceLaunch<'_> {
337    type Error = FusedReduceError;
338
339    fn run<'a>(
340        &'a self,
341        client: &'a Client,
342        inputs: GlobalArgsLaunch,
343        outputs: GlobalArgsLaunch,
344        configs: &'a [FuseBlockConfig],
345    ) -> Result<(), FusedReduceError> {
346        let [config_read, config_write] = [&configs[0], &configs[1]];
347        // An output-concrete reference indexes the output arguments; shape and
348        // strides must both be resolved against that list.
349        let ref_args = match &config_read.ref_layout {
350            RefLayout::Concrete(FuseArg::Output(..)) => &outputs,
351            _ => &inputs,
352        };
353        let shape = ref_args.shape_ref(&config_read.ref_layout, config_read.rank);
354        let ref_strides = ref_args.strides_ref(&config_read.ref_layout, config_read.rank);
355        let reduce_count: usize = shape
356            .iter()
357            .enumerate()
358            .map(|(i, s)| if i == self.reduce.axis { 1 } else { *s })
359            .product();
360
361        let vectorization_mode = match self.reduce.axis == config_read.rank - 1 {
362            true => VectorizationMode::Parallel,
363            false => VectorizationMode::Perpendicular,
364        };
365        let address_type = inputs
366            .required_address_type()
367            .max(outputs.required_address_type());
368
369        let settings = ReduceVectorSettings {
370            vectorization_mode,
371            vector_size_input: config_read.width,
372            vector_size_output: config_write.width,
373            // Fused-reduce selection is cached per anchored key, so the
374            // unchecked comptime fast paths are never stable here.
375            unchecked_fast_paths: false,
376            // The fused read runs the trace, which may write materialized
377            // intermediates to global outputs.
378            fuse_on_read: true,
379        };
380        let problem = ReduceProblem {
381            reduce_len: shape[self.reduce.axis],
382            reduce_count,
383            axis: self.reduce.axis,
384            dtypes: ReduceDtypes {
385                input: dtype_to_storage_type(self.reduce.op.input.dtype),
386                output: dtype_to_storage_type(self.reduce.op.out.dtype),
387                accumulation: self.reduce.acc.into_elem(),
388            },
389            address_type,
390            instruction: reduce_instruction2config(&self.reduce.inst),
391        };
392
393        let (blueprint, settings) = match self.strategy.clone() {
394            RoutineStrategy::Unit(strategy) => {
395                let routine = UnitRoutine;
396                routine.prepare(client, problem, settings, strategy)?
397            }
398            RoutineStrategy::Plane(strategy) => {
399                let routine = PlaneRoutine;
400                routine.prepare(client, problem, settings, strategy)?
401            }
402            RoutineStrategy::Cube(strategy) => {
403                let routine = CubeRoutine;
404                routine.prepare(client, problem, settings, strategy)?
405            }
406        };
407
408        let out_vec_axis =
409            output_vectorization_axis(&ref_strides, self.reduce.axis, vectorization_mode);
410
411        let kwargs = ReduceKwArgs {
412            client,
413            inputs,
414            outputs,
415            reduce_axis: self.reduce.axis,
416            out_vec_axis,
417            config_fuse_read: config_read.clone(),
418            config_fuse_write: config_write.clone(),
419            input: self.reduce.input.clone(),
420            output: self.reduce.output.clone(),
421            blueprint,
422            settings,
423        };
424        let result = launch_reduce_mixed_precision(
425            kwargs,
426            self.reduce.inst,
427            self.reduce.op.input.dtype,
428            self.reduce.op.out.dtype,
429            elem_type_to_dtype(self.reduce.acc.into_elem()),
430        );
431
432        match result {
433            Ok(_) => Ok(()),
434            Err(err) => Err(FusedReduceError::Reduce(ReduceError::Launch(err))),
435        }
436    }
437}
438
439struct ReduceKwArgs<'b> {
440    client: &'b Client,
441    inputs: GlobalArgsLaunch,
442    outputs: GlobalArgsLaunch,
443    reduce_axis: usize,
444    out_vec_axis: usize,
445    blueprint: ReduceBlueprint,
446    settings: ReduceLaunchSettings,
447    config_fuse_read: FuseBlockConfig,
448    config_fuse_write: FuseBlockConfig,
449    input: FuseArg,
450    output: FuseArg,
451}
452
453fn launch_reduce_mixed_precision(
454    kwargs: ReduceKwArgs<'_>,
455    instruction: ReduceInstruction,
456    dtype_input: DType,
457    dtype_output: DType,
458    dtype_acc: DType,
459) -> Result<(), LaunchError> {
460    let config = reduce_instruction2config(&instruction);
461    launch_reduce(kwargs, config, dtype_input, dtype_output, dtype_acc)
462}
463
464pub(crate) fn reduce_instruction2config(instruction: &ReduceInstruction) -> ReduceOperationConfig {
465    match instruction {
466        ReduceInstruction::ArgMax => ReduceOperationConfig::ArgMax,
467        ReduceInstruction::ArgMin => ReduceOperationConfig::ArgMin,
468        ReduceInstruction::Prod => ReduceOperationConfig::Prod,
469        ReduceInstruction::Mean => ReduceOperationConfig::Mean,
470        ReduceInstruction::Sum => ReduceOperationConfig::Sum,
471        ReduceInstruction::Max => ReduceOperationConfig::Max,
472        ReduceInstruction::Min => ReduceOperationConfig::Min,
473        ReduceInstruction::MaxAbs => ReduceOperationConfig::MaxAbs,
474        ReduceInstruction::Any => ReduceOperationConfig::Any,
475        ReduceInstruction::All => ReduceOperationConfig::All,
476    }
477}
478
479fn launch_reduce(
480    kwargs: ReduceKwArgs<'_>,
481    inst: ReduceOperationConfig,
482    dtype_input: DType,
483    dtype_output: DType,
484    dtype_acc: DType,
485) -> Result<(), LaunchError> {
486    unsafe {
487        reduce_kernel_fused::launch_unchecked(
488            kwargs.client,
489            kwargs.settings.cube_count,
490            kwargs.settings.cube_dim,
491            kwargs.settings.address_type,
492            kwargs.config_fuse_read.width,
493            kwargs.config_fuse_write.width,
494            FusedReduceInputLaunch::new(kwargs.inputs, kwargs.config_fuse_read, kwargs.input),
495            FusedReduceOutputLaunch::new(kwargs.outputs, kwargs.config_fuse_write, kwargs.output),
496            kwargs.reduce_axis,
497            kwargs.out_vec_axis,
498            kwargs.blueprint,
499            inst,
500            dtype_to_storage_type(dtype_input),
501            dtype_to_storage_type(dtype_output),
502            dtype_to_storage_type(dtype_acc),
503        )
504    };
505
506    Ok(())
507}
508
509#[cube(launch_unchecked, address_type = "dynamic")]
510pub fn reduce_kernel_fused<In: Numeric, SizeIn: Size, Out: Numeric, SizeOut: Size, Acc: Numeric>(
511    input: &FusedReduceInput,
512    output: &mut FusedReduceOutput,
513    reduce_axis: usize,
514    out_vec_axis: usize,
515    #[comptime] blueprint: ReduceBlueprint,
516    #[comptime] config: ReduceOperationConfig,
517    #[define(In)] _input_dtype: ElemType,
518    #[define(Out)] _output_dtype: ElemType,
519    #[define(Acc)] _acc_dtype: ElemType,
520) {
521    multi_block_variables_init(&input.config, &mut output.global.variables);
522    multi_block_variables_init(&output.config, &mut output.global.variables);
523
524    let (input, mut output) =
525        init_tensors::<FusedReduceArgs, In, SizeIn, Out, SizeOut>(input, output);
526
527    reduce_kernel_virtual::<In, SizeIn, Out, SizeOut, Acc>(
528        &input,
529        &mut output,
530        reduce_axis,
531        out_vec_axis,
532        blueprint,
533        config,
534    );
535}
536
537/// Name of the reduce fusion optimization.
538pub const NAME: &str = "Reduce";
539
540impl ReduceOptimizationInfo {
541    /// The highest relative shape id across this reduce's trace and both of its fallbacks.
542    pub(crate) fn max_relative_shape_id(&self) -> Option<usize> {
543        [
544            self.trace.max_relative_shape_id(),
545            self.trace_read_fallback.max_relative_shape_id(),
546            self.trace_write_fallback.max_relative_shape_id(),
547        ]
548        .into_iter()
549        .flatten()
550        .max()
551    }
552}
553
554impl FusedOperation for ReduceOptimization {
555    fn max_relative_shape_id(&self) -> Option<usize> {
556        self.info.max_relative_shape_id()
557    }
558
559    const NAME: &'static str = self::NAME;
560    type State = ReduceOptimizationState;
561
562    fn num_ops_fused(&self) -> usize {
563        Self::num_ops_fused(self)
564    }
565
566    fn run(
567        &mut self,
568        context: &mut Context<CubeFusionHandle>,
569        fallback: &dyn Fn(usize) -> Box<dyn FallbackOperation>,
570    ) {
571        Self::execute(self, context, |index| fallback(index))
572    }
573
574    fn to_state(&self) -> Self::State {
575        Self::to_state(self)
576    }
577
578    fn from_state(device: &cubecl::Device, state: Self::State) -> Self {
579        Self::from_state(device, state)
580    }
581}