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
18pub 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 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 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 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 let mut next = self.clone();
151 next.fuse_no_check(operation);
152
153 if next.properties().ready && !next.is_consistent() {
155 self.state = ReduceBroadcastedStatus::Closed;
157 } else {
158 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 let (_tensor3_out, tensor3) = tensor(2, &[1, 0], TensorStatus::ReadWrite);
264 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 let (_tensor5_out, tensor5) = tensor(4, &[1, 2], TensorStatus::ReadWrite);
282 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 let (_tensor2_out, mut tensor2) = tensor(2, &[1, 2], TensorStatus::ReadOnly);
322 let (tensor3_out, tensor3) = tensor(3, &[1, 2], TensorStatus::ReadWrite);
323
324 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 let (tensor5_out, _tensor5) = tensor(5, &[1, 2], TensorStatus::ReadWrite);
357 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}