Skip to main content

burn_cubecl_fusion/optim/reduce_broadcasted/fuser/
base.rs

1use crate::optim::{
2    CubeOptimization,
3    reduce::{ReduceFuser, ReduceFuserInfo, ReduceSettings},
4    reduce_broadcasted::{
5        ReduceBlockOptimInfo, ReduceBroadcastedOptimization, ReduceBroadcastedOptimizationInfo,
6        fuser::{
7            block::{ReduceBlockFuser, ReduceBlockFusionAnalysis, ReduceBroadcastedStatus},
8            full::ReduceBroadcastedFullFuser,
9            full_analyzer::FullFuserAnalyzer,
10        },
11    },
12};
13use burn_fusion::{FuserProperties, FuserStatus, OperationFuser};
14use burn_ir::OperationIr;
15use cubecl::Runtime;
16use std::sync::Arc;
17
18/// Fuses element wise operations around a reduce operation.
19pub struct ReduceBroadcastedFuser<R: Runtime> {
20    blocks: Vec<ReduceBlockFuser<R>>,
21    fuser_default: ReduceFuser<R>,
22    num_ops: usize,
23    state: ReduceBroadcastedStatus,
24    max_bindings: u32,
25}
26
27impl<R: Runtime> Clone for ReduceBroadcastedFuser<R> {
28    fn clone(&self) -> Self {
29        Self {
30            blocks: self.blocks.clone(),
31            fuser_default: self.fuser_default.clone(),
32            num_ops: self.num_ops,
33            state: self.state.clone(),
34            max_bindings: self.max_bindings,
35        }
36    }
37}
38
39impl<R: Runtime> ReduceBroadcastedFuser<R> {
40    pub fn new(device: R::Device) -> Self {
41        let fuser = ReduceFuser::new(device, ReduceSettings::Always);
42        let max_bindings = fuser.fuser.max_bindings;
43        let block = ReduceBlockFuser::new(fuser.clone());
44
45        Self {
46            blocks: vec![block],
47            fuser_default: fuser,
48            num_ops: 0,
49            state: ReduceBroadcastedStatus::Starting,
50            max_bindings,
51        }
52    }
53
54    /// Checks whether the full fuser and all fallback blocks together account for every
55    /// operation that has been registered across all blocks.
56    ///
57    /// This is a dry-run consistency check: it simulates finishing all blocks without
58    /// mutating any state, then verifies two invariants:
59    ///
60    /// 1. **Full fuser coverage** — the number of operations absorbed by the
61    ///    [`ReduceBroadcastedFullFuser`] matches the total operation count.
62    /// 2. **Fallback coverage** — the sum of operations across all fallback
63    ///    [`ReduceBlockOptimInfo`] entries also matches the total operation count.
64    ///
65    /// If either invariant fails, the fuser's state should be marked as
66    /// [`ReduceBroadcastedStatus::Abort`] because the optimization would produce
67    /// incorrect results.
68    fn is_consistent(&self) -> bool {
69        let analyzer = FullFuserAnalyzer::new(&self.blocks);
70        let mut full = ReduceBroadcastedFullFuser::new(self.max_bindings, analyzer);
71        let mut num_ops = 0;
72        let fallbacks = self
73            .blocks
74            .clone()
75            .iter_mut()
76            .map(|block| block.finish(&mut num_ops, &mut full))
77            .collect::<Vec<_>>();
78
79        let mut num_ops_fallback = 0;
80
81        for f in fallbacks.iter() {
82            num_ops_fallback += match f {
83                ReduceBlockOptimInfo::Reduce(info) => info.len,
84                ReduceBlockOptimInfo::Elemwise(info) => info.num_ops_fused(),
85            };
86        }
87
88        let full_fuser_covers_all = full.num_ops_fused() == num_ops;
89        let fallbacks_cover_all = num_ops_fallback == num_ops;
90
91        full_fuser_covers_all && fallbacks_cover_all
92    }
93
94    /// Fuses without checking consistency and the current state.
95    fn fuse_no_check(&mut self, operation: &OperationIr) {
96        let block = self.blocks.last_mut().unwrap();
97        let analyze = block.analyze(operation, &self.state, &self.fuser_default);
98
99        let info = match analyze {
100            ReduceBlockFusionAnalysis::Accept => {
101                block.fuse(operation);
102                self.num_ops += 1;
103                block.fuser.reduce_info()
104            }
105            ReduceBlockFusionAnalysis::Refuse => {
106                self.state = ReduceBroadcastedStatus::Closed;
107                return;
108            }
109            ReduceBlockFusionAnalysis::NewBlockRequired => {
110                let info = block.fuser.reduce_info();
111                let mut block = ReduceBlockFuser::new(self.fuser_default.clone());
112                block.fuse(operation);
113                self.num_ops += 1;
114                self.blocks.push(block);
115                info
116            }
117        };
118
119        match info {
120            ReduceFuserInfo::FusedReduce {
121                shape_input_id,
122                axis,
123            } => {
124                // Only support last axis for now.
125                if axis != shape_input_id.len() - 1 {
126                    self.state = ReduceBroadcastedStatus::Abort;
127                } else {
128                    self.state = ReduceBroadcastedStatus::Init {
129                        shape_id: shape_input_id,
130                        axis,
131                    };
132                }
133            }
134            ReduceFuserInfo::FusedElemwise { .. } => {}
135        }
136    }
137}
138
139impl<R: Runtime> OperationFuser<CubeOptimization<R>> for ReduceBroadcastedFuser<R> {
140    fn fuse(&mut self, operation: &OperationIr) {
141        if matches!(
142            &self.state,
143            ReduceBroadcastedStatus::Closed | ReduceBroadcastedStatus::Abort
144        ) {
145            return;
146        }
147
148        // We first need to simulate the fusion to check the consistency, then we perform the
149        // fusion.
150        let mut next = self.clone();
151        next.fuse_no_check(operation);
152
153        // We can only check for consistency if the optimization is ready.
154        if next.properties().ready && !next.is_consistent() {
155            // Fusions that lead to inconsistent trace are closed.
156            self.state = ReduceBroadcastedStatus::Closed;
157        } else {
158            // Normal path.
159            self.fuse_no_check(operation);
160        }
161    }
162
163    fn finish(&mut self) -> CubeOptimization<R> {
164        let analyzer = FullFuserAnalyzer::new(&self.blocks);
165        let mut full = ReduceBroadcastedFullFuser::new(self.max_bindings, analyzer);
166        let mut num_ops = 0;
167        let fallbacks = self
168            .blocks
169            .iter_mut()
170            .map(|block| block.finish(&mut num_ops, &mut full))
171            .collect::<Vec<_>>();
172
173        let broadcasted = Arc::new(full.finish());
174        let info = Arc::new(ReduceBroadcastedOptimizationInfo {
175            fallbacks,
176            broadcasted,
177        });
178        CubeOptimization::ReduceBroadcasted(ReduceBroadcastedOptimization { info, num_ops })
179    }
180
181    fn reset(&mut self) {
182        let block = ReduceBlockFuser::new(self.fuser_default.clone());
183        self.blocks = vec![block];
184        self.num_ops = 0;
185        self.state = ReduceBroadcastedStatus::Starting;
186    }
187
188    fn status(&self) -> FuserStatus {
189        match self.state {
190            ReduceBroadcastedStatus::Closed | ReduceBroadcastedStatus::Abort => {
191                return FuserStatus::Closed;
192            }
193            _ => {}
194        };
195
196        let fuser = self.blocks.last().unwrap();
197        fuser.fuser.status()
198    }
199
200    fn properties(&self) -> FuserProperties {
201        let ready = match self.state {
202            ReduceBroadcastedStatus::Starting | ReduceBroadcastedStatus::Abort => false,
203            ReduceBroadcastedStatus::Closed if self.blocks.len() == 1 => {
204                !self.blocks[0].is_elemwise()
205            }
206            _ => true,
207        };
208        let mut props = FuserProperties { score: 0, ready };
209        for block in self.blocks.iter() {
210            let p = block.properties();
211            props.score += p.score;
212            props.ready = p.ready && props.ready;
213        }
214        props
215    }
216
217    fn len(&self) -> usize {
218        self.num_ops
219    }
220
221    fn clone_dyn(&self) -> Box<dyn OperationFuser<CubeOptimization<R>>> {
222        Box::new(self.clone())
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use burn_ir::{
229        BaseOperationIr, BinaryOpIr, CreationOpIr, ReduceDimOpIr, TensorId, TensorIr, TensorStatus,
230    };
231    use burn_std::{DType, Shape};
232
233    use super::*;
234
235    type Run = cubecl::TestRuntime;
236
237    #[test]
238    fn reduce_broadcast_workflow_1() {
239        let device: <Run as Runtime>::Device = Default::default();
240        let mut fuser = ReduceBroadcastedFuser::<Run>::new(device);
241        let (tensor1_out, tensor1) = tensor(0, &[1, 2], TensorStatus::ReadWrite);
242        let (tensor2_out, tensor2) = tensor(1, &[1, 0], TensorStatus::ReadWrite);
243
244        fuser.fuse(&OperationIr::BaseFloat(BaseOperationIr::Ones(
245            CreationOpIr { out: tensor1_out },
246        )));
247        fuser.fuse(&OperationIr::NumericFloat(
248            DType::F32,
249            burn_ir::NumericOperationIr::SumDim(ReduceDimOpIr {
250                input: tensor1,
251                out: tensor2_out,
252                axis: 1,
253                accumulator_len: 1,
254            }),
255        ));
256
257        let status = fuser.status();
258        assert_eq!(2, fuser.len());
259        assert_eq!(status, FuserStatus::Open);
260        assert!(fuser.properties().ready,);
261
262        // An existing tensor
263        let (_tensor3_out, tensor3) = tensor(2, &[1, 0], TensorStatus::ReadWrite);
264        // A new tensor
265        let (tensor4_out, tensor4) = tensor(3, &[1, 0], TensorStatus::ReadWrite);
266        fuser.fuse(&OperationIr::NumericFloat(
267            DType::F32,
268            burn_ir::NumericOperationIr::Add(BinaryOpIr {
269                lhs: tensor2,
270                rhs: tensor3,
271                out: tensor4_out,
272            }),
273        ));
274
275        let status = fuser.status();
276        assert_eq!(3, fuser.len());
277        assert_eq!(status, FuserStatus::Open);
278        assert!(fuser.properties().ready,);
279
280        // An existing tensor
281        let (_tensor5_out, tensor5) = tensor(4, &[1, 2], TensorStatus::ReadWrite);
282        // A new tensor
283        let (tensor6_out, tensor6) = tensor(5, &[1, 2], TensorStatus::ReadWrite);
284        fuser.fuse(&OperationIr::NumericFloat(
285            DType::F32,
286            burn_ir::NumericOperationIr::Add(BinaryOpIr {
287                lhs: tensor4,
288                rhs: tensor5,
289                out: tensor6_out,
290            }),
291        ));
292
293        let status = fuser.status();
294        assert_eq!(4, fuser.len());
295        assert_eq!(status, FuserStatus::Open);
296        assert!(fuser.properties().ready,);
297
298        let (tensor7_out, _tensor7) = tensor(6, &[1, 0], TensorStatus::ReadWrite);
299        fuser.fuse(&OperationIr::NumericFloat(
300            DType::F32,
301            burn_ir::NumericOperationIr::SumDim(ReduceDimOpIr {
302                input: tensor6,
303                out: tensor7_out,
304                axis: 1,
305                accumulator_len: 1,
306            }),
307        ));
308        assert_eq!(5, fuser.len());
309        assert_eq!(status, FuserStatus::Open);
310        assert!(fuser.properties().ready,);
311
312        let _optimization = fuser.finish();
313    }
314
315    #[test]
316    fn reduce_broadcast_workflow_2() {
317        let device: <Run as Runtime>::Device = Default::default();
318        let mut fuser = ReduceBroadcastedFuser::<Run>::new(device);
319        let (tensor1_out, tensor1) = tensor(0, &[1, 2], TensorStatus::ReadWrite);
320        // An existing tensor
321        let (_tensor2_out, mut tensor2) = tensor(2, &[1, 2], TensorStatus::ReadOnly);
322        let (tensor3_out, tensor3) = tensor(3, &[1, 2], TensorStatus::ReadWrite);
323
324        // First reduce output
325        let (tensor4_out, tensor4) = tensor(1, &[1, 0], TensorStatus::ReadWrite);
326
327        fuser.fuse(&OperationIr::BaseFloat(BaseOperationIr::Ones(
328            CreationOpIr { out: tensor1_out },
329        )));
330
331        fuser.fuse(&OperationIr::NumericFloat(
332            DType::F32,
333            burn_ir::NumericOperationIr::Add(BinaryOpIr {
334                lhs: tensor1,
335                rhs: tensor2.clone(),
336                out: tensor3_out,
337            }),
338        ));
339
340        fuser.fuse(&OperationIr::NumericFloat(
341            DType::F32,
342            burn_ir::NumericOperationIr::SumDim(ReduceDimOpIr {
343                input: tensor3,
344                out: tensor4_out,
345                axis: 1,
346                accumulator_len: 1,
347            }),
348        ));
349
350        let status = fuser.status();
351        assert_eq!(3, fuser.len());
352        assert_eq!(status, FuserStatus::Open);
353        assert!(fuser.properties().ready,);
354
355        // A new tensor
356        let (tensor5_out, _tensor5) = tensor(5, &[1, 2], TensorStatus::ReadWrite);
357        // Last time we use tensor2.
358        tensor2.status = TensorStatus::ReadWrite;
359        fuser.fuse(&OperationIr::NumericFloat(
360            DType::F32,
361            burn_ir::NumericOperationIr::Add(BinaryOpIr {
362                lhs: tensor4,
363                rhs: tensor2,
364                out: tensor5_out,
365            }),
366        ));
367
368        let status = fuser.status();
369        assert_eq!(4, fuser.len());
370        assert_eq!(status, FuserStatus::Open);
371        assert!(fuser.properties().ready,);
372
373        let _optimization = fuser.finish();
374    }
375
376    fn tensor(id: u64, shape: &[usize], status: TensorStatus) -> (TensorIr, TensorIr) {
377        let tensor = TensorIr {
378            id: TensorId::new(id),
379            shape: Shape::from(shape),
380            status: TensorStatus::NotInit,
381            dtype: DType::F32,
382        };
383        let mut tensor_init = tensor.clone();
384        tensor_init.status = status;
385
386        (tensor, tensor_init)
387    }
388}