Skip to main content

cubecl_cpp/shared/
unary.rs

1use cubecl_core::{
2    self as cubecl,
3    frontend::polyfills::{erf, log1p, recip, to_degrees, to_radians},
4    ir::{
5        dialect::{
6            atomic::AtomicLoadOp,
7            bitwise::{
8                BitwiseNotOp, CountOnesOp, FindFirstSetOp, LeadingZerosBitsOp, ReverseBitsOp,
9                TrailingZerosBitsOp,
10            },
11            general::{BoolNotOp, CastOp, FreeOp, ReinterpretCastOp},
12            math::*,
13            memory::{LoadOp, StoreOp},
14            plane::{AtomicUniformLoadOp, UniformLoadOp},
15            synchronization::{SyncOp, SyncScope, SyncScopeAttr},
16        },
17        interfaces::TypedExt,
18        prelude::*,
19    },
20    prelude::*,
21};
22use half::bf16;
23use num_traits::{One, Zero};
24
25use crate::{
26    cuda::packed_ops::{PackableOp, packable},
27    shared::{
28        CppValue, OpToCPP,
29        convert::{no_half, promotes_int},
30        lowering::LowerOp,
31        shared_op, shared_op_with_out,
32        ty::{TypeExtCPP, TypedExtCPP},
33        unroll::unrolling,
34    },
35    target::{CtxTarget, Shared, Target},
36};
37
38pub trait FunctionFmt {
39    fn base_function_name() -> &'static str;
40    fn function_name(ctx: &Context, ty: impl Typed) -> String {
41        let prefix = ctx.target().ty_prefix(ctx, ty);
42        format!("{prefix}{}", Self::base_function_name())
43    }
44    fn format_unary(ctx: &Context, input: Value) -> String {
45        format!("{}({})", Self::function_name(ctx, input), input.name(ctx))
46    }
47}
48
49macro_rules! function {
50    ($name:ident, $func:expr, $($flags: ident),*) => {
51        impl FunctionFmt for $name {
52            fn base_function_name() -> &'static str {
53                $func
54            }
55        }
56
57        #[op_interface_impl]
58        impl OpToCPP<Shared> for $name {
59            fn to_cpp(&self, ctx: &Context) -> String {
60                format!(
61                    "{} = {};",
62                    self.get_result(ctx).fmt_left(ctx),
63                    Self::format_unary(ctx, self.input(ctx))
64                )
65            }
66        }
67        unrolling!($name);
68        $($flags!($name);)*
69    };
70}
71
72function!(LogOp, "log", packable);
73// function!(FastLog, "__logf", no_half);
74function!(SinOp, "sin", packable);
75function!(CosOp, "cos", packable);
76function!(TanOp, "tan", no_half);
77function!(TanhOp, "tanh", packable);
78function!(SinhOp, "sinh", no_half);
79function!(CoshOp, "cosh", no_half);
80function!(ArcCosOp, "acos", no_half);
81function!(ArcSinOp, "asin", no_half);
82function!(ArcTanOp, "atan", no_half);
83function!(ArcSinhOp, "asinh", no_half);
84function!(ArcCoshOp, "acosh", no_half);
85function!(ArcTanhOp, "atanh", no_half);
86// function!(FastSinOp, "__sinf", false);
87// function!(FastCosOp, "__cosf", false);
88function!(SqrtOp, "sqrt", packable);
89function!(RsqrtOp, "rsqrt", packable);
90// function!(FastSqrt, "__fsqrt_rn", false);
91// function!(FastInverseSqrt, "__frsqrt_rn", false);
92function!(ExpOp, "exp", packable);
93// function!(FastExp, "__expf", false);
94function!(Expm1Op, "expm1", no_half);
95function!(CeilOp, "ceil", packable);
96function!(TruncOp, "trunc", packable);
97function!(FloorOp, "floor", packable);
98function!(RoundOp, "rint", packable);
99// function!(FastRecip, "__frcp_rn", false);
100// function!(FastTanhOp, "__tanhf", false);
101
102function!(ErfOp, "erf", no_half);
103
104shared_op_with_out!(SAbsOp, |op, ctx| {
105    format!("abs({})", op.input(ctx).name(ctx))
106});
107unrolling!(SAbsOp);
108promotes_int!(SAbsOp);
109
110shared_op!(FreeOp, |_, _| String::new());
111
112shared_op_with_out!(CAbsOp, |op, ctx| {
113    format!("abs({})", op.input(ctx).name(ctx))
114});
115shared_op_with_out!(CConjOp, |op, ctx| {
116    let input = op.input(ctx);
117    let function = if input.size(ctx) == 8 {
118        "cuConjf"
119    } else {
120        "cuConj"
121    };
122    format!("{function}({})", input.name(ctx))
123});
124shared_op_with_out!(CRealOp, |op, ctx| {
125    let input = op.input(ctx);
126    let function = if input.size(ctx) == 8 {
127        "cuCrealf"
128    } else {
129        "cuCreal"
130    };
131    format!("{function}({})", input.name(ctx))
132});
133shared_op_with_out!(CImagOp, |op, ctx| {
134    let input = op.input(ctx);
135    let function = if input.size(ctx) == 8 {
136        "cuCimagf"
137    } else {
138        "cuCimag"
139    };
140    format!("{function}({})", input.name(ctx))
141});
142
143shared_op_with_out!(FAbsOp, |op, ctx| {
144    let input = op.input(ctx);
145    if input.is_half(ctx) {
146        format!("__habs({})", input.name(ctx))
147    } else if input.is_half2(ctx) {
148        format!("__habs2({})", input.name(ctx))
149    } else {
150        format!("fabs({})", input.name(ctx))
151    }
152});
153unrolling!(FAbsOp);
154packable!(FAbsOp);
155
156shared_op_with_out!(SNegOp, |op, ctx| format!("-{}", op.input(ctx).name(ctx)));
157unrolling!(SNegOp);
158promotes_int!(SNegOp);
159
160shared_op_with_out!(FNegOp, |op, ctx| format!("-{}", op.input(ctx).name(ctx)));
161unrolling!(FNegOp);
162packable!(FNegOp);
163
164shared_op_with_out!(BoolNotOp, |op, ctx| format!("!{}", op.input(ctx).name(ctx)));
165unrolling!(BoolNotOp);
166
167shared_op_with_out!(BitwiseNotOp, |op, ctx| format!(
168    "~{}",
169    op.input(ctx).name(ctx)
170));
171unrolling!(BitwiseNotOp);
172promotes_int!(BitwiseNotOp);
173
174// Handle bitcount stuff
175
176shared_op_with_out!(CountOnesOp, |op, ctx| {
177    let input = op.input(ctx);
178    match input.size(ctx) {
179        4 => format!("__popc({})", input.name(ctx)),
180        8 => format!("__popcll({})", input.name(ctx)),
181        _ => unreachable!("Unsupported size"),
182    }
183});
184unrolling!(CountOnesOp);
185
186shared_op_with_out!(ReverseBitsOp, |op, ctx| {
187    let input = op.input(ctx);
188    match input.size(ctx) {
189        4 => format!("__brev({})", input.name(ctx)),
190        8 => format!("__brevll({})", input.name(ctx)),
191        _ => unreachable!("Unsupported size"),
192    }
193});
194unrolling!(ReverseBitsOp);
195
196shared_op_with_out!(LeadingZerosBitsOp, |op, ctx| {
197    let input = op.input(ctx);
198    match input.size(ctx) {
199        4 => format!("__clz({})", input.name(ctx)),
200        8 => format!("__clzll({})", input.name(ctx)),
201        _ => unreachable!("Unsupported size"),
202    }
203});
204unrolling!(LeadingZerosBitsOp);
205
206shared_op_with_out!(FindFirstSetOp, |op, ctx| {
207    let input = op.input(ctx);
208    match input.size(ctx) {
209        4 => format!("__ffs({})", input.name(ctx)),
210        8 => format!("__ffsll({})", input.name(ctx)),
211        _ => unreachable!("Unsupported size"),
212    }
213});
214unrolling!(FindFirstSetOp);
215
216shared_op_with_out!(CastOp, |op, ctx| {
217    let input = op.input(ctx);
218    let ty = op.get_result(ctx).get_type(ctx);
219    format!("{}({})", ty.to_cpp(ctx), input.name(ctx))
220});
221unrolling!(CastOp);
222
223// In and out packability might differ for cast. Also need to exempt bf16<->f16 because CUDA for some
224// reason omits this cast in packed form, even though it allows packed casts to and from minifloats.
225#[op_interface_impl]
226impl PackableOp for CastOp {
227    fn should_pack(&self, ctx: &Context) -> bool {
228        let is_bf16_to_half = is_bf16(ctx, self.input(ctx)) && is_f16(ctx, self.get_result(ctx));
229        let is_half_to_bf16 = is_f16(ctx, self.input(ctx)) && is_bf16(ctx, self.get_result(ctx));
230        let can_pack_both = self.input(ctx).can_pack(ctx) && self.get_result(ctx).can_pack(ctx);
231        !is_bf16_to_half && !is_half_to_bf16 && can_pack_both
232    }
233}
234
235fn is_f16(ctx: &Context, val: Value) -> bool {
236    val.try_get_scalar_ty(ctx)
237        .is_some_and(|scalar| scalar.is_float16(ctx))
238}
239
240fn is_bf16(ctx: &Context, val: Value) -> bool {
241    val.try_get_scalar_ty(ctx)
242        .is_some_and(|scalar| scalar.is_bfloat16(ctx))
243}
244
245shared_op_with_out!(ReinterpretCastOp, |op, ctx| {
246    let input = op.input(ctx);
247    let ty = op.get_result(ctx).get_type(ctx).to_cpp(ctx);
248    if input.is_ptr(ctx) {
249        format!("reinterpret_cast<{ty}>({})", input.name(ctx))
250    } else {
251        format!("reinterpret_cast<const {ty}&>({})", input.name(ctx))
252    }
253});
254
255shared_op_with_out!(LoadOp, |op, ctx| format!("*{}", op.ptr(ctx).name(ctx)));
256shared_op!(StoreOp, |op, ctx| {
257    let value = op.value(ctx).name(ctx);
258    format!("*{} = {value};\n", op.ptr(ctx).name(ctx))
259});
260
261macro_rules! lower_unop {
262    ($ty: ty, $name: ident, $pred: expr) => {
263        $crate::shared::unary::lower_target_unop!($ty, $name, $crate::target::Shared, $pred);
264    };
265    ($ty: ty, $name: ident) => {
266        $crate::shared::unary::lower_unop!($ty, $name, |_, _| true);
267    };
268}
269pub(crate) use lower_unop;
270
271macro_rules! lower_target_unop {
272    ($ty: ty, $name: ident, $target: ty, $pred: expr) => {
273        #[::pliron::derive::op_interface_impl]
274        impl $crate::shared::lowering::LowerOp<$target> for $ty {
275            fn should_lower(&self, ctx: &pliron::context::Context) -> bool {
276                $crate::shared::closure_inference_hack::<$ty, bool>(self, ctx, $pred)
277            }
278
279            fn lower(&self, scope: &cubecl_core::ir::Scope) -> Vec<pliron::value::Value> {
280                use cubecl_core::ir::prelude::*;
281                use cubecl_core::prelude::*;
282                define_scalar!(T);
283                define_size!(S);
284                let input = self.get_operand(scope.ctx());
285                scope.register_value_type::<T, S>(input);
286                vec![$name::expand::<T, S>(scope, input.into()).read_value(scope)]
287            }
288        }
289    };
290    ($ty: ty, $name: ident, $target: ty) => {
291        lower_target_unop!($ty, $name, $target, |_, _| true);
292    };
293}
294pub(crate) use lower_target_unop;
295
296#[cube]
297fn find_first_set<T: Int, N: Size>(input: Vector<T, N>) -> Vector<u32, N> {
298    let bits = Vector::new(T::size_bits().comptime() as u32);
299    let out = bits - (input & (!input + Vector::one())).leading_zeros();
300    select_many(input.equal(&Vector::zero()), Vector::zero(), out)
301}
302
303#[cube]
304fn trailing_zeros<T: Int, N: Size>(input: Vector<T, N>) -> Vector<u32, N> {
305    let bits = Vector::new(T::size_bits().comptime() as u32);
306    let out = input.find_first_set() - Vector::one();
307    select_many(input.equal(&Vector::zero()), bits, out)
308}
309
310#[cube]
311fn cast_f16_bf16<T: Scalar, N: Size>(input: Vector<T, N>) -> Vector<bf16, N> {
312    Vector::<bf16, N>::cast_from(Vector::<f32, N>::cast_from(input))
313}
314
315#[cube]
316fn count_ones<T: Scalar, N: Size>(input: Vector<T, N>) -> Vector<u32, N> {
317    Vector::<u32, N>::cast_from(Vector::<u32, N>::cast_from(input).count_ones())
318}
319
320lower_unop!(RecipOp, recip);
321lower_unop!(Log1pOp, log1p);
322lower_unop!(DegreesOp, to_degrees);
323lower_unop!(RadiansOp, to_radians);
324lower_unop!(FindFirstSetOp, find_first_set, |_, ctx| {
325    ctx.target() == Target::Metal
326});
327lower_unop!(TrailingZerosBitsOp, trailing_zeros, |_, ctx| {
328    matches!(ctx.target(), Target::Cuda | Target::Hip)
329});
330lower_unop!(ErfOp, erf, |_, ctx| ctx.target() == Target::Metal);
331lower_unop!(CastOp, cast_f16_bf16, |op, ctx| {
332    op.input(ctx).is_float16(ctx)
333        && op.get_result(ctx).is_bfloat16(ctx)
334        && matches!(ctx.target(), Target::Cuda | Target::Hip)
335});
336
337// `isnan` / `isinf` are defined for cuda/hip/metal with same prefixes for half/bf16 on cuda/hip
338
339fn elem_function_name(ctx: &Context, base_name: &'static str, ty: impl Typed) -> String {
340    // Math functions prefix (no leading underscores)
341    let prefix = ctx.target().ty_prefix(ctx, ty);
342    if prefix.is_empty() {
343        base_name.to_string()
344    } else if prefix == "h" || prefix == "h2" {
345        format!("__{prefix}{base_name}")
346    } else {
347        panic!("Unknown prefix '{prefix}'");
348    }
349}
350
351shared_op_with_out!(IsNanOp, |op, ctx| {
352    let input = op.input(ctx);
353    let func = elem_function_name(ctx, "isnan", input);
354    format!("{func}({})", input.name(ctx))
355});
356unrolling!(IsNanOp);
357
358shared_op_with_out!(IsInfOp, |op, ctx| {
359    let input = op.input(ctx);
360    let func = elem_function_name(ctx, "isinf", input);
361    format!("{func}({})", input.name(ctx))
362});
363unrolling!(IsInfOp);
364
365#[op_interface_impl]
366impl LowerOp for UniformLoadOp {
367    fn lower(&self, scope: &Scope) -> Vec<Value> {
368        scope.register(&SyncOp::new(
369            scope.ctx_mut(),
370            SyncScopeAttr::new(SyncScope::Cube),
371        ));
372        let ptr = self.ptr(scope.ctx());
373        vec![scope.register_with_result(&LoadOp::new(scope.ctx_mut(), ptr))]
374    }
375}
376
377#[op_interface_impl]
378impl LowerOp for AtomicUniformLoadOp {
379    fn lower(&self, scope: &Scope) -> Vec<Value> {
380        scope.register(&SyncOp::new(
381            scope.ctx_mut(),
382            SyncScopeAttr::new(SyncScope::Cube),
383        ));
384        let ptr = self.ptr(scope.ctx());
385        vec![scope.register_with_result(&AtomicLoadOp::new(scope.ctx_mut(), ptr))]
386    }
387}