use alloc::vec;
use cubecl_ir::{
NamedRewrite, Scope, dialect::general::CastOp, interfaces::TypedExt, prelude::*,
types::scalar::BFloat16Type,
};
use pliron::r#type::TypeHandle;
use crate::{self as cubecl, prelude::*};
define_size!(N);
const F32_SIGNIFICAND_BITS: u32 = f32::MANTISSA_DIGITS;
const F32_MANTISSA_BITS: u32 = f32::MANTISSA_DIGITS - 1;
const F32_EXPONENT_BIAS: u32 = (f32::MAX_EXP - 1) as u32;
pub type LowerWideBf16CastPass = MatchRewritePass<LowerWideBf16Cast>;
#[derive(new, Clone, Copy, Debug, Default, NamedRewrite)]
pub struct LowerWideBf16Cast;
impl MatchRewrite for LowerWideBf16Cast {
fn r#match(&mut self, ctx: &Context, op: Ptr<Operation>) -> bool {
op.is_op::<CastOp>(ctx)
&& is_bf16(ctx, op.result(ctx))
&& needs_rounding_to_odd(ctx, op.operand(ctx, 0).scalar_ty(ctx))
}
fn rewrite(
&mut self,
ctx: &mut Context,
rewriter: &mut MatchRewriter,
op: Ptr<Operation>,
) -> Result<()> {
let scope = Scope::from_context_and_inserter(ctx, rewriter);
let input = op.operand(ctx, 0);
let result_ty = op.result(ctx).get_type(ctx);
let lanes = input.vector_size(ctx);
debug_assert_eq!(
lanes,
result_ty.vector_size(ctx),
"A cast keeps its vectorization, so one `N` describes both sides"
);
scope.register_size::<N>(lanes);
let value = cast_value(&scope, to_f32_rounding_once(&scope, input), result_ty);
rewriter.replace_operation_with_values(ctx, op, vec![value]);
Ok(())
}
}
#[cube]
fn f64_to_f32_round_to_odd<N: Size>(value: Vector<f64, N>) -> Vector<f32, N> {
let nearest = Vector::<f32, N>::cast_from(value);
let widened = Vector::<f64, N>::cast_from(nearest);
let bits = Vector::<u32, N>::reinterpret(nearest);
let rounded_away = widened.abs().greater_than(&value.abs());
let toward_zero = select_many(rounded_away, bits - Vector::new(1u32), bits);
let odd = select_many(widened.equal(&value), bits, toward_zero | Vector::new(1u32));
Vector::<f32, N>::reinterpret(odd)
}
#[cube]
fn unsigned_to_f32_round_to_odd<U: Int, N: Size>(magnitude: Vector<U, N>) -> Vector<f32, N> {
let bits = Vector::new(U::size_bits().comptime() as u32);
let significant = bits - Vector::leading_zeros(magnitude);
let excess = select_many(
significant.greater_than(&Vector::new(F32_SIGNIFICAND_BITS)),
significant - Vector::new(F32_SIGNIFICAND_BITS),
Vector::new(0u32),
);
let shift = Vector::<U, N>::cast_from(excess);
let kept = magnitude >> shift;
let dropped = (kept << shift).not_equal(&magnitude);
let odd = kept | Vector::<U, N>::cast_from(dropped);
let scale = Vector::<f32, N>::reinterpret(
(excess + Vector::new(F32_EXPONENT_BIAS)) << Vector::new(F32_MANTISSA_BITS),
);
Vector::<f32, N>::cast_from(odd) * scale
}
#[cube]
fn signed_to_f32_round_to_odd<S: Int, U: Int, N: Size>(value: Vector<S, N>) -> Vector<f32, N> {
let negative = value.less_than(&Vector::zero());
let magnitude =
Vector::<U, N>::reinterpret(select_many(negative, Vector::zero() - value, value));
let rounded = unsigned_to_f32_round_to_odd::<U, N>(magnitude);
select_many(negative, Vector::new(0.0f32) - rounded, rounded)
}
fn is_bf16(ctx: &Context, value: impl Typed) -> bool {
value
.try_get_scalar_ty(ctx)
.is_some_and(|scalar| scalar.deref(ctx).is::<BFloat16Type>())
}
fn to_f32_rounding_once(scope: &Scope, value: Value) -> Value {
let scalar = value.scalar_ty(scope.ctx());
if !needs_rounding_to_odd(scope.ctx(), scalar) {
return cast_value(scope, value, Vector::<f32, N>::__expand_as_type(scope));
}
let signed = scalar.is_signed_int(scope.ctx());
if scalar.is_float64(scope.ctx()) {
let value = cast_value(scope, value, Vector::<f64, N>::__expand_as_type(scope));
f64_to_f32_round_to_odd::expand::<N>(scope, value.into()).read_value(scope)
} else if scalar.size_bits(scope.ctx()) <= u32::BITS as usize {
int_to_f32_round_to_odd::<i32, u32>(scope, value, signed)
} else {
int_to_f32_round_to_odd::<i64, u64>(scope, value, signed)
}
}
fn int_to_f32_round_to_odd<S: Int, U: Int>(scope: &Scope, value: Value, signed: bool) -> Value {
match signed {
true => {
let value = cast_value(scope, value, Vector::<S, N>::__expand_as_type(scope));
signed_to_f32_round_to_odd::expand::<S, U, N>(scope, value.into()).read_value(scope)
}
false => {
let value = cast_value(scope, value, Vector::<U, N>::__expand_as_type(scope));
unsigned_to_f32_round_to_odd::expand::<U, N>(scope, value.into()).read_value(scope)
}
}
}
fn needs_rounding_to_odd(ctx: &Context, scalar: TypeHandle) -> bool {
let integer = scalar.is_int(ctx) || scalar.is_index(ctx);
scalar.is_float64(ctx) || integer && scalar.size_bits(ctx) > F32_SIGNIFICAND_BITS as usize
}