Skip to main content

cubecl_core/frontend/
polyfills.rs

1use alloc::vec;
2use core::f32::consts::PI;
3
4use cubecl_ir::{Type, cube_op, interfaces::TypedExt, prelude::*};
5use num_traits::One;
6
7use crate::prelude::*;
8use crate::{self as cubecl, unexpanded};
9
10define_scalar!(ElemA);
11define_size!(SizeA);
12
13/// Change the meaning of the given cube primitive type during compilation.
14///
15/// # Warning
16///
17/// To be used for very custom kernels, it would likely lead to a JIT compiler error otherwise.
18pub fn set_polyfill<E: Scalar, N: Size>(_elem: Type) {
19    unexpanded!()
20}
21
22/// Expand module of [`set_polyfill()`].
23pub mod set_polyfill {
24    use super::*;
25
26    /// Expand function of [`set_polyfill()`].
27    pub fn expand<E: Scalar, N: Size>(scope: &Scope, ty: Type) {
28        scope.register_type::<E>(ty.elem_type());
29        scope.register_size::<N>(ty.vector_size());
30    }
31}
32
33/// Floor modulo using integer arithmetic, preserving precision for supported inputs.
34/// Division by zero and signed `MIN % -1` have no portable result or error guarantee.
35pub fn expand_signed_mod_floor(scope: &Scope, lhs: Value, rhs: Value) -> Value {
36    define_scalar!(I);
37    define_size!(N);
38    scope.register_value_type::<I, N>(lhs);
39    signed_mod_floor::expand::<I, N>(scope, lhs.into(), rhs.into()).read_value(scope)
40}
41
42#[cube]
43fn signed_mod_floor<I: Int, N: Size>(lhs: Vector<I, N>, rhs: Vector<I, N>) -> Vector<I, N> {
44    let remainder = lhs % rhs;
45    let zero = Vector::new(I::from_int(0));
46    let different_signs = (remainder ^ rhs).less_than(&zero);
47    let adjust = remainder.not_equal(&zero).vec_and(different_signs);
48    remainder + select_many(adjust, rhs, zero)
49}
50
51/// Lower NaN-preserving clamp to the corresponding binary operations.
52pub fn expand_clamp_nan(scope: &Scope, input: Value, min: Value, max: Value) -> Value {
53    use cubecl_ir::dialect::cmp::{FMaxNanOp, FMinNanOp};
54    let upper = FMinNanOp::new(scope.ctx_mut(), input, max);
55    let upper = scope.register_with_result(&upper);
56    let lower = FMaxNanOp::new(scope.ctx_mut(), upper, min);
57    scope.register_with_result(&lower)
58}
59
60/// Portable lowering for NaN-preserving minimum.
61pub fn expand_min_nan(scope: &Scope, lhs: Value, rhs: Value) -> Value {
62    define_scalar!(F);
63    define_size!(N);
64    scope.register_value_type::<F, N>(lhs);
65    min_nan_fallback::expand::<F, N>(scope, lhs.into(), rhs.into()).read_value(scope)
66}
67
68#[cube]
69fn min_nan_fallback<F: Float, N: Size>(lhs: Vector<F, N>, rhs: Vector<F, N>) -> Vector<F, N> {
70    let value = lhs.min(rhs);
71    let value = select_many(rhs.is_nan(), rhs, value);
72    select_many(lhs.is_nan(), lhs, value)
73}
74
75/// Portable lowering for NaN-preserving maximum.
76pub fn expand_max_nan(scope: &Scope, lhs: Value, rhs: Value) -> Value {
77    define_scalar!(F);
78    define_size!(N);
79    scope.register_value_type::<F, N>(lhs);
80    max_nan_fallback::expand::<F, N>(scope, lhs.into(), rhs.into()).read_value(scope)
81}
82
83#[cube]
84fn max_nan_fallback<F: Float, N: Size>(lhs: Vector<F, N>, rhs: Vector<F, N>) -> Vector<F, N> {
85    let value = lhs.max(rhs);
86    let value = select_many(rhs.is_nan(), rhs, value);
87    select_many(lhs.is_nan(), lhs, value)
88}
89
90#[cube]
91pub fn erf<F: Float, N: Size>(x: Vector<F, N>) -> Vector<F, N> {
92    let erf = erf_positive(x.abs());
93    select_many(x.less_than(&Vector::new(F::new(0f32))), -erf, erf)
94}
95
96/// An approximation of the error function: <https://en.wikipedia.org/wiki/Error_function#Numerical_approximations>
97///
98/// > (maximum error: 1.5×10−7)
99/// > All of these approximations are valid for x ≥ 0. To use these approximations for negative x, use the fact that erf x is an odd function, so erf x = −erf(−x).
100#[cube]
101fn erf_positive<F: Float, N: Size>(x: Vector<F, N>) -> Vector<F, N> {
102    let p = Vector::new(F::new(0.3275911_f32));
103    let a1 = Vector::new(F::new(0.2548296_f32));
104    let a2 = Vector::new(F::new(-0.28449674_f32));
105    let a3 = Vector::new(F::new(1.4214137_f32));
106    let a4 = Vector::new(F::new(-1.453152_f32));
107    let a5 = Vector::new(F::new(1.0614054_f32));
108    let one = Vector::new(F::new(1.0_f32));
109
110    let t = one / (one + p * x);
111    let tmp = ((((a5 * t + a4) * t) + a3) * t + a2) * t + a1;
112
113    one - (tmp * t * (-x * x).exp())
114}
115
116#[cube]
117fn himul_i64<I: Int, N: Size>(lhs: Vector<I, N>, rhs: Vector<I, N>) -> Vector<I, N> {
118    let shift = Vector::new(32);
119    let mul = (Vector::<i64, N>::cast_from(lhs) * Vector::<i64, N>::cast_from(rhs)) >> shift;
120    Vector::cast_from(mul)
121}
122
123#[cube]
124fn himul_u64<I: Int, N: Size>(lhs: Vector<I, N>, rhs: Vector<I, N>) -> Vector<I, N> {
125    let shift = Vector::new(32);
126    let mul = (Vector::<u64, N>::cast_from(lhs) * Vector::<u64, N>::cast_from(rhs)) >> shift;
127    Vector::cast_from(mul)
128}
129
130#[allow(missing_docs)]
131pub fn expand_s_himul_64(scope: &Scope, lhs: Value, rhs: Value) -> Value {
132    scope.register_value_type::<ElemA, SizeA>(lhs);
133    himul_i64::expand::<ElemA, SizeA>(scope, lhs.into(), rhs.into()).value(scope)
134}
135
136#[allow(missing_docs)]
137pub fn expand_u_himul_64(scope: &Scope, lhs: Value, rhs: Value) -> Value {
138    scope.register_value_type::<ElemA, SizeA>(lhs);
139    himul_u64::expand::<ElemA, SizeA>(scope, lhs.into(), rhs.into()).value(scope)
140}
141
142#[cube]
143fn himul_sim<T: Int, N: Size>(lhs: Vector<T, N>, rhs: Vector<T, N>) -> Vector<T, N> {
144    let half_bits = T::size_bits().comptime() / 2;
145    let low_mask = Vector::new(T::new(comptime!((1i64 << half_bits) - 1)));
146    let shift = Vector::new(T::new(half_bits as i64));
147
148    let lhs_low = lhs & low_mask;
149    let lhs_hi = (lhs >> shift) & low_mask;
150    let rhs_low = rhs & low_mask;
151    let rhs_hi = (rhs >> shift) & low_mask;
152
153    let low_low = lhs_low * rhs_low;
154    let high_low = lhs_hi * rhs_low;
155    let low_high = lhs_low * rhs_hi;
156    let high_high = lhs_hi * rhs_hi;
157
158    let mid = ((low_low >> shift) & low_mask) + (high_low & low_mask) + (low_high & low_mask);
159    high_high
160        + ((high_low >> shift) & low_mask)
161        + ((low_high >> shift) & low_mask)
162        + ((mid >> shift) & low_mask)
163}
164
165#[allow(missing_docs)]
166pub fn expand_himul_sim(scope: &Scope, lhs: Value, rhs: Value) -> Value {
167    scope.register_value_type::<ElemA, SizeA>(lhs);
168    himul_sim::expand::<ElemA, SizeA>(scope, lhs.into(), rhs.into()).value(scope)
169}
170
171/// Portable implementation of a four-byte signed dot product with accumulation.
172#[cube]
173pub fn dp4a_polyfill<N: Size>(
174    a: Vector<i32, N>,
175    b: Vector<i32, N>,
176    c: Vector<i32, N>,
177) -> Vector<i32, N> {
178    let a = Vector::<u32, N>::reinterpret(a);
179    let b = Vector::<u32, N>::reinterpret(b);
180    let shift_8 = Vector::new(8);
181    let shift_16 = Vector::new(16);
182    let shift_24 = Vector::new(24);
183    let byte_mask = Vector::new(0xff);
184    let sign_mask = Vector::new(0x80);
185    let sign_offset = Vector::<i32, N>::new(0x80);
186
187    let a0 = Vector::<i32, N>::cast_from((a & byte_mask) ^ sign_mask) - sign_offset;
188    let a1 = Vector::<i32, N>::cast_from(((a >> shift_8) & byte_mask) ^ sign_mask) - sign_offset;
189    let a2 = Vector::<i32, N>::cast_from(((a >> shift_16) & byte_mask) ^ sign_mask) - sign_offset;
190    let a3 = Vector::<i32, N>::cast_from((a >> shift_24) ^ sign_mask) - sign_offset;
191
192    let b0 = Vector::<i32, N>::cast_from((b & byte_mask) ^ sign_mask) - sign_offset;
193    let b1 = Vector::<i32, N>::cast_from(((b >> shift_8) & byte_mask) ^ sign_mask) - sign_offset;
194    let b2 = Vector::<i32, N>::cast_from(((b >> shift_16) & byte_mask) ^ sign_mask) - sign_offset;
195    let b3 = Vector::<i32, N>::cast_from((b >> shift_24) ^ sign_mask) - sign_offset;
196
197    c + a0 * b0 + a1 * b1 + a2 * b2 + a3 * b3
198}
199
200#[allow(missing_docs)]
201pub fn expand_dp4a_polyfill(scope: &Scope, a: Value, b: Value, c: Value) -> Value {
202    scope.register_size::<SizeA>(a.vector_size(scope.ctx()));
203    dp4a_polyfill::expand::<SizeA>(scope, a.into(), b.into(), c.into()).value(scope)
204}
205
206#[cube]
207pub fn log1p<T: Float, N: Size>(input: Vector<T, N>) -> Vector<T, N> {
208    (input + Vector::one()).ln()
209}
210
211#[cube]
212pub fn expm1<T: Float, N: Size>(x: Vector<T, N>) -> Vector<T, N> {
213    let sq = x * x;
214    let a = sq * Vector::new(T::new(0.5_f32));
215    let b = sq * x * Vector::new(T::new(1.0_f32 / 6.0_f32));
216    let taylor = x + a + b;
217    let is_small = x.abs().less_than(&Vector::new(T::new(1e-5_f32)));
218    select_many(is_small, taylor, x.exp() - Vector::one())
219}
220
221/// `powf` without any edge case handling. Useful as a common mapping for the backend version that
222/// doesn't handle edge cases normally.
223#[cube_op(name = "polyfill.simple_pow")]
224#[result_ty(same_as = base)]
225pub struct SimplePowOp {
226    pub base: Value,
227    pub exp: Value,
228}
229
230/// use the simple version because otherwise we'd get an infinite lowering loop
231#[cube]
232fn simple_pow<T: Float, N: Size>(base: Vector<T, N>, exp: Vector<T, N>) -> Vector<T, N> {
233    intrinsic!(|scope| {
234        let base = base.read_value(scope);
235        let exp = exp.read_value(scope);
236        let powf = SimplePowOp::new(scope.ctx_mut(), base, exp);
237        scope.register_with_result(&powf).into()
238    })
239}
240
241#[cube]
242pub fn powf<T: Float, N: Size>(base: Vector<T, N>, exp: Vector<T, N>) -> Vector<T, N> {
243    let modulo = exp.mod_floor(Vector::new(T::new(2.0_f32)));
244    let is_even = modulo.equal(&Vector::zero());
245    let is_odd = modulo.equal(&Vector::one());
246    let is_neg_base = base.less_than(&Vector::zero());
247
248    let even_res = simple_pow(base.abs(), exp);
249    let odd_neg_res = -(simple_pow(-base, exp));
250    let default = simple_pow(base, exp);
251
252    let sel1 = select_many(is_odd.vec_and(is_neg_base), odd_neg_res, default);
253    select_many(is_even, even_res, sel1)
254}
255
256#[cube]
257pub fn powi<T: Float, N: Size>(base: Vector<T, N>, exp: Vector<i32, N>) -> Vector<T, N> {
258    let is_even = exp.is_multiple_of(2);
259    let is_neg_base = base.less_than(&Vector::zero());
260    let exp = Vector::cast_from(exp);
261
262    let even_res = simple_pow(base.abs(), exp);
263    let odd_neg_res = -(simple_pow(-base, exp));
264    let default = simple_pow(base, exp);
265
266    let sel1 = select_many((!is_even).vec_and(is_neg_base), odd_neg_res, default);
267    select_many(is_even, even_res, sel1)
268}
269
270/// Wrapping integer power, interpreting the exponent as `u32`.
271#[cube]
272pub fn powi_int<T: Int, N: Size>(base: Vector<T, N>, exp: Vector<i32, N>) -> Vector<T, N> {
273    // Use unsigned arithmetic so intermediate squares wrap on C++ backends too.
274    // Narrow integers can use u32: truncating the final result preserves their low bits
275    // and avoids C++ integer promotion turning multiplication into signed arithmetic.
276    if T::size_bits().comptime() <= 32 {
277        Vector::cast_from(powi_int_unsigned::<u32, N>(Vector::cast_from(base), exp))
278    } else {
279        Vector::cast_from(powi_int_unsigned::<u64, N>(Vector::cast_from(base), exp))
280    }
281}
282
283// Only instantiate with u32 or u64 to keep multiplication unsigned on every backend.
284#[cube]
285fn powi_int_unsigned<T: Int, N: Size>(base: Vector<T, N>, exp: Vector<i32, N>) -> Vector<T, N> {
286    let one_u = Vector::<u32, N>::new(1);
287    let one_t = Vector::<T, N>::new(T::from_int(1));
288
289    // Integer powers use an unsigned exponent, matching primitive integer `pow` semantics.
290    let mut exp = Vector::<u32, N>::cast_from(exp);
291    let mut result = one_t;
292    let mut factor = base;
293
294    #[unroll]
295    for _ in 0..32 {
296        // TODO: implement peephole optimization for masked multiplication
297        result *= select_many((exp & one_u).equal(&one_u), factor, one_t);
298        factor *= factor;
299        exp >>= one_u;
300    }
301
302    result
303}
304
305#[cube]
306pub fn recip<T: Float, N: Size>(input: Vector<T, N>) -> Vector<T, N> {
307    Vector::one() / input
308}
309
310#[cube]
311pub fn to_degrees<T: Float, N: Size>(input: Vector<T, N>) -> Vector<T, N> {
312    input * Vector::new(T::new(comptime!(180.0_f32 / PI)))
313}
314
315#[cube]
316pub fn to_radians<T: Float, N: Size>(input: Vector<T, N>) -> Vector<T, N> {
317    input * Vector::new(T::new(comptime!(PI / 180.0_f32)))
318}
319
320pub mod bitwise {
321    use super::*;
322
323    #[cube]
324    pub fn u64_leading_zeros<I: Int, N: Size>(x: Vector<I, N>) -> Vector<u32, N> {
325        let shift = Vector::new(I::new(32));
326
327        let low = Vector::<u32, N>::cast_from(x);
328        let high = Vector::<u32, N>::cast_from(x >> shift);
329        let low_zeros = Vector::leading_zeros(low);
330        let high_zeros = Vector::leading_zeros(high);
331
332        select_many(
333            high_zeros.equal(&Vector::new(32)),
334            low_zeros + high_zeros,
335            high_zeros,
336        )
337    }
338
339    #[cube]
340    pub fn u64_trailing_zeros<I: Int, N: Size>(x: Vector<I, N>) -> Vector<u32, N> {
341        let shift = Vector::new(I::new(32));
342
343        let low = Vector::<u32, N>::cast_from(x);
344        let high = Vector::<u32, N>::cast_from(x >> shift);
345        let low_tz = Vector::trailing_zeros(low);
346        let high_tz = Vector::trailing_zeros(high);
347
348        let high_tz = select_many(
349            high_tz.equal(&Vector::new(32)),
350            Vector::new(64),
351            high_tz + Vector::new(32),
352        );
353        select_many(low_tz.equal(&Vector::new(32)), high_tz, low_tz)
354    }
355
356    #[cube]
357    pub fn u64_ffs<I: Int, N: Size>(x: Vector<I, N>) -> Vector<u32, N> {
358        let shift = Vector::new(I::new(32));
359
360        let low = Vector::<u32, N>::cast_from(x);
361        let high = Vector::<u32, N>::cast_from(x >> shift);
362        let low_ffs = Vector::find_first_set(low);
363        let high_ffs = Vector::find_first_set(high);
364
365        let high_ffs = select_many(
366            high_ffs.equal(&Vector::new(0)),
367            high_ffs,
368            high_ffs + Vector::new(32),
369        );
370        select_many(low_ffs.equal(&Vector::new(0)), high_ffs, low_ffs)
371    }
372}
373
374/// The plane reductions and scans, as folds over the shuffles.
375///
376/// A backend that has cross-lane shuffles but no reduction of its own gets them from here; the
377/// C++ backends and the LLVM one share these.
378pub mod plane {
379    use super::*;
380    use crate::prelude::{
381        CUBE_DIM, CubeAdd, CubeMul, CubePartialOrd, PLANE_DIM, UNIT_POS_PLANE, max, min,
382        plane_shuffle_up, plane_shuffle_xor, select,
383    };
384
385    #[cube]
386    pub trait PlaneOp<T: Scalar, N: Size> {
387        fn apply(lhs: Vector<T, N>, rhs: Vector<T, N>) -> Vector<T, N>;
388    }
389
390    pub struct OpAdd;
391    pub struct OpMul;
392    pub struct OpMin;
393    pub struct OpMax;
394
395    #[cube]
396    impl<T: Scalar + CubeAdd, N: Size> PlaneOp<T, N> for OpAdd {
397        fn apply(lhs: Vector<T, N>, rhs: Vector<T, N>) -> Vector<T, N> {
398            lhs + rhs
399        }
400    }
401    #[cube]
402    impl<T: Scalar + CubeMul, N: Size> PlaneOp<T, N> for OpMul {
403        fn apply(lhs: Vector<T, N>, rhs: Vector<T, N>) -> Vector<T, N> {
404            lhs * rhs
405        }
406    }
407    #[cube]
408    impl<T: Scalar + CubePartialOrd, N: Size> PlaneOp<T, N> for OpMin {
409        fn apply(lhs: Vector<T, N>, rhs: Vector<T, N>) -> Vector<T, N> {
410            min(lhs, rhs)
411        }
412    }
413    #[cube]
414    impl<T: Scalar + CubePartialOrd, N: Size> PlaneOp<T, N> for OpMax {
415        fn apply(lhs: Vector<T, N>, rhs: Vector<T, N>) -> Vector<T, N> {
416            max(lhs, rhs)
417        }
418    }
419
420    #[cube]
421    fn plane_dim_checked() -> u32 {
422        min(PLANE_DIM, CUBE_DIM)
423    }
424
425    #[cube]
426    pub fn plane_reduce<T: Scalar, N: Size, Op: PlaneOp<T, N>>(val: Vector<T, N>) -> Vector<T, N> {
427        let plane_dim = plane_dim_checked();
428        let mut acc = val;
429        let mut offset = 1;
430        while offset < plane_dim {
431            acc = Op::apply(acc, plane_shuffle_xor(acc, offset));
432            offset *= 2;
433        }
434        acc
435    }
436
437    #[cube]
438    pub fn plane_reduce_inclusive<T: Scalar, N: Size, Op: PlaneOp<T, N>>(
439        val: Vector<T, N>,
440    ) -> Vector<T, N> {
441        let plane_dim = plane_dim_checked();
442        let mut acc = val;
443        let mut offset = 1;
444        while offset < plane_dim {
445            let tmp = Op::apply(acc, plane_shuffle_up(acc, offset));
446            if UNIT_POS_PLANE >= offset {
447                acc = tmp;
448            }
449            offset *= 2;
450        }
451        acc
452    }
453
454    #[cube]
455    pub fn plane_reduce_exclusive<T: Numeric, N: Size, Op: PlaneOp<T, N>>(
456        val: Vector<T, N>,
457        #[comptime] default: i64,
458    ) -> Vector<T, N> {
459        let inclusive = plane_reduce_inclusive::<T, N, Op>(val);
460        let shfl = plane_shuffle_up(inclusive, 1);
461        select(UNIT_POS_PLANE == 0, Vector::new(T::from_int(default)), shfl)
462    }
463}
464
465/// Where a lane's registers sit in the tile, for the manual `mma.sync` matrix API.
466///
467/// Unlike the cooperative API, whose fragment layout is the hardware's business, the manual one
468/// hands a kernel the registers and expects it to know which element of the tile each holds.
469/// NVIDIA documents that mapping, and every backend generating `mma.sync` has to agree with it
470/// exactly -- a kernel that indexes its own fragment differently from the way the instruction
471/// reads it computes a wrong answer rather than failing -- so it is written once here and the
472/// C++ and LLVM backends both expand it.
473///
474/// Derived from the PTX shape documentation:
475/// <https://docs.nvidia.com/cuda/parallel-thread-execution/#warp-level-matrix-instructions-for-mma>
476pub mod mma {
477    use super::*;
478    use crate::ir::types::MatrixIdent;
479
480    /// The row of the tile that `lane_id`'s `i`th element holds.
481    #[cube]
482    pub fn row_index(
483        lane_id: u32,
484        i: u32,
485        #[comptime] elems_per_reg: usize,
486        #[comptime] ident: MatrixIdent,
487    ) -> u32 {
488        let elems_per_reg = elems_per_reg as u32;
489        match ident {
490            MatrixIdent::A => {
491                let group_id = lane_id / 4;
492                let odd_register = (i / elems_per_reg) & 1;
493                group_id + odd_register * 8
494            }
495            MatrixIdent::B => {
496                let thread_id_in_group = lane_id % 4;
497                let offset = thread_id_in_group * elems_per_reg + (i % elems_per_reg);
498                let reg = i / elems_per_reg;
499                offset + elems_per_reg * 4 * reg
500            }
501            MatrixIdent::Accumulator => {
502                let group_id = lane_id / 4;
503                let offset = (i << 2) & 8;
504                group_id + offset
505            }
506        }
507    }
508
509    /// The column of the tile that `lane_id`'s `i`th element holds.
510    #[cube]
511    pub fn col_index(
512        lane_id: u32,
513        i: u32,
514        #[comptime] elems_per_reg: usize,
515        #[comptime] ident: MatrixIdent,
516    ) -> u32 {
517        let elems_per_reg = elems_per_reg as u32;
518        match ident {
519            MatrixIdent::A => {
520                let thread_id_in_group = lane_id % 4;
521                let offset = thread_id_in_group * elems_per_reg + (i % elems_per_reg);
522                let group_2 = (i / (2 * elems_per_reg)) & 1;
523                offset + 4 * elems_per_reg * group_2
524            }
525            MatrixIdent::B => lane_id >> 2,
526            MatrixIdent::Accumulator => {
527                let thread_id_in_group = lane_id % 4;
528                (thread_id_in_group * 2) + (i % 2)
529            }
530        }
531    }
532}