Skip to main content

burn_cubecl/ops/
numeric.rs

1use crate::{
2    CubeDevice,
3    kernel::utils::{address_type, shape_divmod},
4};
5use crate::{element::CubeElement, tensor::CubeTensor};
6use crate::{
7    kernel::{
8        AddOp, BitwiseAndOp, BitwiseOrOp, BitwiseXorOp, DivOp, MulOp, PowOp, RemainderOp, SubOp,
9        launch_binop, launch_binop_int, launch_scalar_binop, launch_scalar_binop_int,
10    },
11    ops::max_vector_size,
12};
13use burn_backend::cubecl::dtype_to_storage_type;
14use burn_backend::{DType, Shape, TensorMetadata};
15use burn_std::Metadata;
16use cubecl::{
17    calculate_cube_count_elemwise,
18    ir::{ElemType, dialect::math::IsNanOp},
19    prelude::*,
20    std::tensor::layout::linear::LinearViewMut,
21};
22use cubecl::{client::Client, server::MemoryLayout};
23use cubecl::{server::MemoryLayoutDescriptor, std::FastDivmod};
24
25/// Creates a tensor filled with `value`
26pub fn full<E: CubeElement>(shape: Shape, device: &CubeDevice, value: E) -> CubeTensor {
27    let client = device.client();
28
29    full_client::<E>(client, shape, device.clone(), value)
30}
31
32/// Creates a tensor filled with `value`
33pub fn full_client<E: CubeElement>(
34    client: Client,
35    shape: Shape,
36    device: CubeDevice,
37    value: E,
38) -> CubeTensor {
39    let dtype = E::dtype();
40    full_device_dtype(
41        client,
42        shape,
43        device,
44        InputScalar::new(value, dtype_to_storage_type(dtype)),
45        dtype,
46    )
47}
48
49/// Creates a tensor filled with `value`
50pub fn full_device_dtype(
51    client: Client,
52    shape: Shape,
53    device: CubeDevice,
54    value: InputScalar,
55    dtype: DType,
56) -> CubeTensor {
57    let empty = empty_device_dtype(client, device, shape, dtype);
58
59    fill_device_dtype(empty, value)
60}
61
62/// Fills an existing tensor with `value`
63pub(crate) fn fill_device_dtype(tensor: CubeTensor, value: InputScalar) -> CubeTensor {
64    #[cube(launch_unchecked, address_type = "dynamic")]
65    pub fn full_kernel<C: Numeric, N: Size>(
66        mut tensor: LinearViewMut<'_, Vector<C, N>>,
67        value: InputScalar,
68        #[define(C)] _dtype: ElemType,
69    ) {
70        if !tensor.is_in_bounds(ABSOLUTE_POS) {
71            terminate!();
72        }
73
74        tensor.write(ABSOLUTE_POS, Vector::new(value.get::<C>()));
75    }
76
77    let num_elems = tensor.meta.num_elements();
78    let vector_size = max_vector_size(&tensor);
79
80    let working_units = num_elems / vector_size as usize;
81    let cube_dim = CubeDim::new(&tensor.client, working_units);
82    let cube_count = calculate_cube_count_elemwise(&tensor.client, working_units, cube_dim);
83
84    unsafe {
85        full_kernel::launch_unchecked(
86            &tensor.client,
87            cube_count,
88            cube_dim,
89            address_type!(tensor),
90            vector_size,
91            tensor.clone().into_linear_view(),
92            value,
93            dtype_to_storage_type(tensor.dtype),
94        );
95    }
96
97    tensor
98}
99
100/// Creates a tensor filled with zeros
101pub fn zeros(device: CubeDevice, shape: Shape, dtype: DType) -> CubeTensor {
102    let client = device.client();
103    full_device_dtype(
104        client,
105        shape,
106        device,
107        InputScalar::new(0u32, dtype_to_storage_type(dtype)),
108        dtype,
109    )
110}
111
112/// Creates a tensor filled with ones
113pub fn ones(device: CubeDevice, shape: Shape, dtype: DType) -> CubeTensor {
114    let client = device.client();
115    full_device_dtype(
116        client,
117        shape,
118        device,
119        InputScalar::new(1u32, dtype_to_storage_type(dtype)),
120        dtype,
121    )
122}
123
124/// Creates a tensor filled with zeros
125pub fn zeros_client(client: Client, device: CubeDevice, shape: Shape, dtype: DType) -> CubeTensor {
126    full_device_dtype(
127        client,
128        shape,
129        device,
130        InputScalar::new(0u32, dtype_to_storage_type(dtype)),
131        dtype,
132    )
133}
134
135/// Creates a tensor filled with ones
136pub fn ones_client(client: Client, device: CubeDevice, shape: Shape, dtype: DType) -> CubeTensor {
137    full_device_dtype(
138        client,
139        shape,
140        device,
141        InputScalar::new(1u32, dtype_to_storage_type(dtype)),
142        dtype,
143    )
144}
145
146/// Create a tensor with uninitialized memory
147pub fn empty_device<E: CubeElement>(
148    client: Client,
149    device: CubeDevice,
150    shape: Shape,
151) -> CubeTensor {
152    let MemoryLayout { memory, strides } = client.empty_tensor(shape.clone(), size_of::<E>());
153
154    CubeTensor::new(
155        client,
156        memory,
157        Metadata::new(shape, strides),
158        device,
159        E::dtype(),
160    )
161}
162
163/// Create a tensor with uninitialized memory
164pub fn empty_device_dtype(
165    client: Client,
166    device: CubeDevice,
167    shape: Shape,
168    dtype: DType,
169) -> CubeTensor {
170    let MemoryLayout { memory, strides } = client.empty_tensor(shape.clone(), dtype.size());
171
172    CubeTensor::new(client, memory, Metadata::new(shape, strides), device, dtype)
173}
174
175/// Create a contiguous tensor with uninitialized memory
176pub fn empty_device_contiguous_dtype(
177    client: Client,
178    device: CubeDevice,
179    shape: Shape,
180    dtype: DType,
181) -> CubeTensor {
182    let descriptor = MemoryLayoutDescriptor::contiguous(shape.clone(), dtype.size());
183    let MemoryLayout { memory, strides } = client.empty_tensors(vec![descriptor]).remove(0);
184
185    CubeTensor::new(client, memory, Metadata::new(shape, strides), device, dtype)
186}
187
188/// Add two tensors
189pub fn add(lhs: CubeTensor, rhs: CubeTensor) -> CubeTensor {
190    launch_binop::<AddOp>(lhs, rhs)
191}
192
193/// Add a tensor and a scalar
194pub fn add_scalar(lhs: CubeTensor, rhs: InputScalar) -> CubeTensor {
195    launch_scalar_binop::<AddOp>(lhs, rhs)
196}
197
198/// Subtract two tensors
199pub fn sub(lhs: CubeTensor, rhs: CubeTensor) -> CubeTensor {
200    launch_binop::<SubOp>(lhs, rhs)
201}
202
203/// Subtract a tensor and a scalar
204pub fn sub_scalar(lhs: CubeTensor, rhs: InputScalar) -> CubeTensor {
205    launch_scalar_binop::<SubOp>(lhs, rhs)
206}
207
208/// Multiply two tensors
209pub fn mul(lhs: CubeTensor, rhs: CubeTensor) -> CubeTensor {
210    launch_binop::<MulOp>(lhs, rhs)
211}
212
213/// Multiply a tensor and a scalar
214pub fn mul_scalar(lhs: CubeTensor, rhs: InputScalar) -> CubeTensor {
215    launch_scalar_binop::<MulOp>(lhs, rhs)
216}
217
218/// Divide two tensors
219pub fn div(lhs: CubeTensor, rhs: CubeTensor) -> CubeTensor {
220    launch_binop::<DivOp>(lhs, rhs)
221}
222
223/// Divide a tensor by a scalar
224pub fn div_scalar(lhs: CubeTensor, rhs: InputScalar) -> CubeTensor {
225    launch_scalar_binop::<DivOp>(lhs, rhs)
226}
227
228/// Calculate remainder of two tensors
229pub fn remainder(lhs: CubeTensor, rhs: CubeTensor) -> CubeTensor {
230    launch_binop::<RemainderOp>(lhs, rhs)
231}
232
233/// Calculate the remainder of a tensor with a scalar
234pub fn remainder_scalar(lhs: CubeTensor, rhs: InputScalar) -> CubeTensor {
235    launch_scalar_binop::<RemainderOp>(lhs, rhs)
236}
237
238/// Calculate the power of two tensors
239pub fn pow(lhs: CubeTensor, rhs: CubeTensor) -> CubeTensor {
240    launch_binop::<PowOp>(lhs, rhs)
241}
242
243/// Bitwise and two tensors
244pub fn bitwise_and(lhs: CubeTensor, rhs: CubeTensor) -> CubeTensor {
245    launch_binop_int::<BitwiseAndOp>(lhs, rhs)
246}
247
248/// Bitwise and with a scalar
249pub fn bitwise_and_scalar(lhs: CubeTensor, rhs: InputScalar) -> CubeTensor {
250    launch_scalar_binop_int::<BitwiseAndOp>(lhs, rhs)
251}
252
253/// Bitwise or two tensors
254pub fn bitwise_or(lhs: CubeTensor, rhs: CubeTensor) -> CubeTensor {
255    launch_binop_int::<BitwiseOrOp>(lhs, rhs)
256}
257
258/// Bitwise or with a scalar
259pub fn bitwise_or_scalar(lhs: CubeTensor, rhs: InputScalar) -> CubeTensor {
260    launch_scalar_binop_int::<BitwiseOrOp>(lhs, rhs)
261}
262
263/// Bitwise xor two tensors
264pub fn bitwise_xor(lhs: CubeTensor, rhs: CubeTensor) -> CubeTensor {
265    launch_binop_int::<BitwiseXorOp>(lhs, rhs)
266}
267
268/// Bitwise xor with a scalar
269pub fn bitwise_xor_scalar(lhs: CubeTensor, rhs: InputScalar) -> CubeTensor {
270    launch_scalar_binop_int::<BitwiseXorOp>(lhs, rhs)
271}
272
273/// Operation family trait for cumulative operations
274pub(crate) trait CumulativeOpFamily: Send + Sync + 'static {
275    type CumulativeOp<C: Numeric>: CumulativeOp<C>;
276}
277
278/// Trait for cumulative operations
279#[cube]
280pub(crate) trait CumulativeOp<C: Numeric>: 'static + Send + Sync {
281    /// Execute a cumulative operation
282    fn execute(lhs: C, rhs: C) -> C;
283
284    /// Get the initial value for the accumulator
285    fn init_value(first_element: C) -> C;
286}
287
288// Operation types
289struct SumOp;
290struct ProdOp;
291struct MaxOp;
292struct MinOp;
293
294// `N: Numeric` can't call the float-only `IsNan` trait after a comptime type check, so emit
295// the same Cube IR operation directly. Callers keep this inside float-only comptime branches.
296#[cube]
297fn numeric_is_nan<N: Numeric>(value: N) -> bool {
298    intrinsic!(|scope| {
299        let is_nan = IsNanOp::new(scope.ctx_mut(), value.read_value(scope));
300        scope.register_with_result(&is_nan).into()
301    })
302}
303
304#[cube]
305fn cumulative_max<N: Numeric>(lhs: N, rhs: N) -> N {
306    let elem_type = elem_type_of::<N>();
307    if comptime!(elem_type.is_float()) {
308        if numeric_is_nan::<N>(lhs) {
309            lhs
310        } else if numeric_is_nan::<N>(rhs) {
311            rhs
312        } else {
313            max(lhs, rhs)
314        }
315    } else {
316        max(lhs, rhs)
317    }
318}
319
320#[cube]
321fn cumulative_min<N: Numeric>(lhs: N, rhs: N) -> N {
322    let elem_type = elem_type_of::<N>();
323    if comptime!(elem_type.is_float()) {
324        if numeric_is_nan::<N>(lhs) {
325            lhs
326        } else if numeric_is_nan::<N>(rhs) {
327            rhs
328        } else {
329            min(lhs, rhs)
330        }
331    } else {
332        min(lhs, rhs)
333    }
334}
335
336// Implement CumulativeOpFamily for each operation
337impl CumulativeOpFamily for SumOp {
338    type CumulativeOp<C: Numeric> = Self;
339}
340
341impl CumulativeOpFamily for ProdOp {
342    type CumulativeOp<C: Numeric> = Self;
343}
344
345impl CumulativeOpFamily for MaxOp {
346    type CumulativeOp<C: Numeric> = Self;
347}
348
349impl CumulativeOpFamily for MinOp {
350    type CumulativeOp<C: Numeric> = Self;
351}
352
353// Implement CumulativeOp for each operation type
354#[cube]
355impl<N: Numeric> CumulativeOp<N> for SumOp {
356    fn execute(lhs: N, rhs: N) -> N {
357        lhs + rhs
358    }
359
360    fn init_value(_first_element: N) -> N {
361        N::zero()
362    }
363}
364
365#[cube]
366impl<N: Numeric> CumulativeOp<N> for ProdOp {
367    fn execute(lhs: N, rhs: N) -> N {
368        lhs * rhs
369    }
370
371    fn init_value(_first_element: N) -> N {
372        N::from_int(1)
373    }
374}
375
376#[cube]
377impl<N: Numeric> CumulativeOp<N> for MaxOp {
378    fn execute(lhs: N, rhs: N) -> N {
379        cumulative_max::<N>(lhs, rhs)
380    }
381
382    fn init_value(first_element: N) -> N {
383        first_element
384    }
385}
386
387#[cube]
388impl<N: Numeric> CumulativeOp<N> for MinOp {
389    fn execute(lhs: N, rhs: N) -> N {
390        cumulative_min::<N>(lhs, rhs)
391    }
392
393    fn init_value(first_element: N) -> N {
394        first_element
395    }
396}
397
398/// Generic cumulative operation kernel
399///
400/// # Limitations
401///
402/// This is a **naive sequential implementation** along the cumulative dimension:
403/// - Each output element sequentially reads all previous elements along the dimension
404/// - Computational complexity: O(n^2) memory reads where n is the size of the cumulative dimension
405/// - **Performance:** Suitable for small tensors or small dimensions. For large tensors,
406///   performance will degrade significantly compared to an optimized parallel scan algorithm.
407///
408/// # TODO
409///
410/// Implement an efficient GPU-optimized parallel scan algorithm.
411#[cube(launch_unchecked, address_type = "dynamic")]
412fn cumulative_kernel<C: Numeric, O: CumulativeOpFamily>(
413    input: &Tensor<C>,
414    mut output: LinearViewMut<'_, C>,
415    shape: Sequence<FastDivmod<usize>>,
416    #[comptime] dim: usize,
417    #[define(C)] _dtype: ElemType,
418) {
419    if !output.is_in_bounds(ABSOLUTE_POS) {
420        terminate!();
421    }
422
423    let rank = comptime![shape.len()];
424    let dim_stride = input.stride(dim);
425
426    let mut remainder = ABSOLUTE_POS;
427    let mut offset = 0;
428    let mut dim_idx = 0;
429
430    #[unroll]
431    for i in 0..shape.len() {
432        let i = comptime![rank - i - 1];
433        let (rem, local_idx) = shape.index(i).div_mod(remainder);
434        remainder = rem;
435        if i == dim {
436            dim_idx = local_idx;
437        } else {
438            offset += local_idx * input.stride(i);
439        }
440    }
441
442    // Read first element
443    let first_read_idx = offset + dim_idx * dim_stride;
444    let first_elem = input[first_read_idx];
445
446    // Initialize accumulator
447    let mut result = O::CumulativeOp::<C>::init_value(first_elem);
448
449    // Accumulate values
450    for i in 0..=dim_idx {
451        let read_idx = offset + i * dim_stride;
452        result = O::CumulativeOp::<C>::execute(result, input[read_idx]);
453    }
454    output.write(ABSOLUTE_POS, result);
455}
456
457/// Compute the cumulative sum along a dimension
458pub fn cumsum(input: CubeTensor, dim: usize) -> CubeTensor {
459    cumulative_op::<SumOp>(input, dim)
460}
461
462/// Compute the cumulative product along a dimension
463pub fn cumprod(input: CubeTensor, dim: usize) -> CubeTensor {
464    cumulative_op::<ProdOp>(input, dim)
465}
466
467/// Compute the cumulative minimum along a dimension
468pub fn cummin(input: CubeTensor, dim: usize) -> CubeTensor {
469    cumulative_op::<MinOp>(input, dim)
470}
471
472/// Compute the cumulative maximum along a dimension
473pub fn cummax(input: CubeTensor, dim: usize) -> CubeTensor {
474    cumulative_op::<MaxOp>(input, dim)
475}
476
477/// Generic cumulative operation function
478fn cumulative_op<O: CumulativeOpFamily>(input: CubeTensor, dim: usize) -> CubeTensor {
479    let client = input.client.clone();
480    let device = input.device.clone();
481
482    let output = empty_device_dtype(client.clone(), device, input.shape(), input.dtype);
483
484    let num_elems = output.meta.num_elements();
485    let working_units = num_elems;
486    let cube_dim = CubeDim::new(&client, working_units);
487    let cube_count = calculate_cube_count_elemwise(&client, working_units, cube_dim);
488    let shape = shape_divmod(&input);
489
490    unsafe {
491        cumulative_kernel::launch_unchecked::<O>(
492            &client,
493            cube_count,
494            cube_dim,
495            address_type!(input, output),
496            input.into_tensor_arg(),
497            output.clone().into_linear_view(),
498            shape,
499            dim,
500            dtype_to_storage_type(output.dtype),
501        );
502    }
503
504    output
505}