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)]
23pub struct SumAutotuneKey {
25 dtype: burn_backend::DType,
27 #[autotune(anchor)]
29 length: usize,
30}
31
32fn 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 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
54fn 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
82fn 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
90pub fn sum_fallback(
92 tensor: CubeTensor,
93 mut strategy: SumStrategy,
94) -> Result<CubeTensor, ReduceError> {
95 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
104pub fn sum(tensor: CubeTensor, strategy: SumStrategy) -> Result<CubeTensor, ReduceError> {
111 let client = tensor.client.clone();
112 let device = tensor.device.clone();
113
114 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
142pub enum SumStrategy {
144 OneShot(u32),
147 Chained(KernelReduceStrategy),
149 #[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
164pub fn reduce(
171 mut tensor: CubeTensor,
172 output_dtype: Option<DType>,
173 strategy: KernelReduceStrategy,
174 config: ReduceOperationConfig,
175) -> Result<CubeTensor, cubek::reduce::ReduceError> {
176 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 *tensor.meta = Metadata::new([1], [1]);
184 Ok(tensor)
185}
186
187pub 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 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 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
271fn 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 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
311fn 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
329pub 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; out
363}
364
365pub(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
383pub 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 let axis_length = input.meta.shape[dim];
426 if axis_length == 0 {
427 return reduce_empty_axis(output, axis_length, config);
428 }
429 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
469pub 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 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 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 if input.meta.shape[dim] == 0 {
534 return Err(ReduceError::ReduceAxisTooSmall {
535 axis_length: 0,
536 k: out_len,
537 });
538 }
539 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
591pub 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
607pub 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#[derive(Clone, Debug)]
624pub enum KernelReduceStrategy {
625 Unspecified,
628 Specific(cubek::reduce::launch::ReduceStrategy),
630 #[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}