use std::ops::RangeInclusive;
use furiosa_opt_lower::CastKind;
use crate::scalar::{bf16, f4e2m1, f8e4m3, f8e5m2, i4, i5, i9};
use super::scalar::{MaterializableScalar, Scalar};
pub trait Cast<D: Scalar> {
fn cast(self) -> D;
}
pub trait FetchCast<D: Scalar>: Cast<D> {}
impl<D> FetchCast<D> for D where D: Scalar {}
impl FetchCast<i32> for i4 {}
impl FetchCast<i32> for i8 {}
impl FetchCast<i32> for i16 {}
impl FetchCast<f32> for f8e4m3 {}
impl FetchCast<f32> for f8e5m2 {}
impl FetchCast<f32> for bf16 {}
impl FetchCast<bf16> for f32 {}
pub trait TableLookup<D: Scalar> {
fn lookup(self) -> D;
}
impl TableLookup<f8e4m3> for f4e2m1 {
fn lookup(self) -> f8e4m3 {
self.to_f8e4m3()
}
}
impl TableLookup<f8e5m2> for f4e2m1 {
fn lookup(self) -> f8e5m2 {
f8e5m2::from_f32(self.to_f32())
}
}
impl TableLookup<bf16> for f8e5m2 {
fn lookup(self) -> bf16 {
bf16::from_f32(self.to_f32())
}
}
impl TableLookup<bf16> for f8e4m3 {
fn lookup(self) -> bf16 {
bf16::from_f32(self.to_f32())
}
}
pub trait FetchZeroPointSub<Out: Scalar>: Scalar {
const ZERO_POINT_RANGE: RangeInclusive<i32>;
fn zero_point_sub(self, zero_point: i32) -> Out;
}
impl FetchZeroPointSub<i5> for i4 {
const ZERO_POINT_RANGE: RangeInclusive<i32> = -8..=7;
fn zero_point_sub(self, zero_point: i32) -> i5 {
i5::from_i32(i32::from(self) - zero_point)
}
}
impl FetchZeroPointSub<i9> for i8 {
const ZERO_POINT_RANGE: RangeInclusive<i32> = -128..=127;
fn zero_point_sub(self, zero_point: i32) -> i9 {
i9::from_i32(i32::from(self) - zero_point)
}
}
#[diagnostic::on_unimplemented(
message = "the Cast Engine cannot cast `{Self}` to `{D}`",
label = "not a cast-compaction conversion",
note = "widenings belong to the Fetch Adapter: `.fetch_cast::<{D}>()`"
)]
pub trait CastEngineCast<D: Scalar>: Cast<D> {
const KIND: CastKind;
}
impl CastEngineCast<i4> for i32 {
const KIND: CastKind = CastKind::I32ToI4;
}
impl CastEngineCast<i8> for i32 {
const KIND: CastKind = CastKind::I32ToI8;
}
impl CastEngineCast<i16> for i32 {
const KIND: CastKind = CastKind::I32ToI16;
}
impl CastEngineCast<f8e4m3> for f32 {
const KIND: CastKind = CastKind::F32ToF8E4M3;
}
impl CastEngineCast<f8e5m2> for f32 {
const KIND: CastKind = CastKind::F32ToF8E5M2;
}
impl CastEngineCast<bf16> for f32 {
const KIND: CastKind = CastKind::F32ToBf16;
}
#[diagnostic::on_unimplemented(
message = "the Commit Adapter cannot cast `{Self}` to `{D}`",
label = "commit_cast converts only `f32` to `bf16`",
note = "narrow to anything else in the Cast Engine: `.cast::<{D}, OutPacket>()`"
)]
pub trait CommitCast<D: Scalar>: Cast<D> {
fn cast_relu(self) -> D;
}
impl CommitCast<bf16> for f32 {
fn cast_relu(self) -> bf16 {
if !self.is_nan() && self.is_sign_negative() {
return Cast::cast(0.0);
}
Cast::cast(self)
}
}
impl<D: Scalar> Cast<D> for D {
#[inline]
fn cast(self) -> D {
self
}
}
impl Cast<i32> for i8 {
#[inline]
fn cast(self) -> i32 {
self as i32
}
}
impl Cast<i8> for i32 {
#[inline]
fn cast(self) -> i8 {
self as i8
}
}
impl Cast<f32> for bf16 {
#[inline]
fn cast(self) -> f32 {
crate::float::bf16_to_f32(self.to_half())
}
}
impl Cast<bf16> for f32 {
#[inline]
fn cast(self) -> bf16 {
bf16::from_half(crate::float::f32_to_bf16(self))
}
}
impl Cast<f32> for f8e4m3 {
#[inline]
fn cast(self) -> f32 {
self.to_f32()
}
}
impl Cast<f8e4m3> for f32 {
#[inline]
fn cast(self) -> f8e4m3 {
f8e4m3::from_f32(self)
}
}
impl Cast<f32> for f8e5m2 {
#[inline]
fn cast(self) -> f32 {
self.to_f32()
}
}
impl Cast<f8e5m2> for f32 {
#[inline]
fn cast(self) -> f8e5m2 {
f8e5m2::from_f32(self)
}
}
impl Cast<i32> for i4 {
#[inline]
fn cast(self) -> i32 {
self.to_i32()
}
}
impl Cast<i4> for i32 {
#[inline]
fn cast(self) -> i4 {
i4::from_i32(self)
}
}
impl Cast<i32> for i16 {
#[inline]
fn cast(self) -> i32 {
i32::from(self)
}
}
impl Cast<i16> for i32 {
#[inline]
fn cast(self) -> i16 {
self as i16
}
}
impl Cast<i32> for i5 {
#[inline]
fn cast(self) -> i32 {
self.to_i32()
}
}
impl Cast<i5> for i32 {
#[inline]
fn cast(self) -> i5 {
i5::from_i32(self)
}
}
impl Cast<i32> for i9 {
#[inline]
fn cast(self) -> i32 {
self.to_i32()
}
}
impl Cast<i9> for i32 {
#[inline]
fn cast(self) -> i9 {
i9::from_i32(self)
}
}
pub trait ContractionCast: Scalar + ContractionWeight<Self> + Cast<<Self as ContractionCast>::Output> {
type Output: ContractionAccumulator + Cast<Self>;
}
#[diagnostic::on_unimplemented(
message = "a contraction cannot accumulate in `{Self}`",
label = "not a contraction accumulator width",
note = "a contraction accumulates in `i32` (integer operands) or `f32` (float operands)"
)]
pub trait ContractionAccumulator: MaterializableScalar {}
impl ContractionAccumulator for i32 {}
impl ContractionAccumulator for f32 {}
pub trait ContractionWeight<Stream: Scalar>: Scalar {}
impl ContractionWeight<i4> for i4 {}
impl ContractionWeight<i5> for i4 {}
impl ContractionWeight<i4> for i5 {}
impl ContractionWeight<i5> for i5 {}
impl ContractionWeight<i8> for i8 {}
impl ContractionWeight<i9> for i8 {}
impl ContractionWeight<i8> for i9 {}
impl ContractionWeight<i9> for i9 {}
impl ContractionWeight<bf16> for bf16 {}
impl ContractionWeight<f8e4m3> for f8e4m3 {}
impl ContractionWeight<f8e5m2> for f8e5m2 {}
impl ContractionCast for i8 {
type Output = i32;
}
impl ContractionCast for bf16 {
type Output = f32;
}
impl ContractionCast for f8e4m3 {
type Output = f32;
}
impl ContractionCast for f8e5m2 {
type Output = f32;
}
impl ContractionCast for i4 {
type Output = i32;
}
impl ContractionCast for i5 {
type Output = i32;
}
impl ContractionCast for i9 {
type Output = i32;
}
#[cfg(test)]
mod tests {
use super::*;
fn widen<S: ContractionCast>(x: S) -> <S as ContractionCast>::Output {
Cast::cast(x)
}
fn narrow<S: ContractionCast>(acc: <S as ContractionCast>::Output) -> S {
Cast::cast(acc)
}
fn assert_roundtrip<S: ContractionCast + std::fmt::Debug>(samples: impl IntoIterator<Item = S>) {
for x in samples {
assert_eq!(narrow::<S>(widen(x)), x, "narrow ∘ widen must round-trip {x:?}");
}
}
#[test]
fn narrow_widen_round_trips() {
assert_roundtrip([i8::MIN, -1, 0, 1, i8::MAX]);
assert_roundtrip([-8, -1, 0, 1, 7].map(i4::from_i32));
assert_roundtrip([-2.0, -1.0, 0.0, 0.5, 1.0, 2.0].map(bf16::from_f32));
assert_roundtrip([-2.0, -1.0, 0.0, 0.5, 1.0, 2.0].map(f8e4m3::from_f32));
assert_roundtrip([-2.0, -1.0, 0.0, 0.5, 1.0, 2.0].map(f8e5m2::from_f32));
}
#[test]
fn contraction_operands_are_the_engine_input_types() {
fn assert_operand<D: ContractionCast>() {}
assert_operand::<i4>();
assert_operand::<i5>();
assert_operand::<i8>();
assert_operand::<i9>();
assert_operand::<f8e4m3>();
assert_operand::<f8e5m2>();
assert_operand::<bf16>();
}
#[test]
fn integer_accumulator_stays_in_i32() {
type Acc = <i8 as ContractionCast>::Output;
let (l, r): (Acc, Acc) = (1 << 16, 1 << 16);
let prod = l.wrapping_mul(r);
assert_eq!(prod, 0, "2^32 wraps to 0 in i32");
assert_ne!(i64::from(prod), i64::from(l) * i64::from(r));
}
#[test]
fn narrow_integer_accumulates_in_i32() {
let (l, r) = (i4::from_i32(-8), i4::from_i32(-8));
assert_eq!(widen(l) * widen(r), 64);
let acc: i32 = std::iter::repeat_n((widen(100i8), widen(100i8)), 256)
.map(|(l, r)| l * r)
.sum();
assert_eq!(acc, 256 * 100 * 100);
assert_eq!(narrow::<i8>(acc), 2_560_000i32 as i8);
let _: <i8 as ContractionCast>::Output = acc;
let _: <i4 as ContractionCast>::Output = 0i32;
let _: <i5 as ContractionCast>::Output = 0i32;
let _: <i9 as ContractionCast>::Output = 0i32;
}
#[test]
fn bf16_narrow_is_round_to_nearest_even() {
let one_ulp = f32::exp2(-7.0); assert_eq!(narrow::<bf16>(1.0 + one_ulp / 2.0).to_f32(), 1.0);
assert_eq!(narrow::<bf16>(1.0 + one_ulp * 1.5).to_f32(), 1.0 + 2.0 * one_ulp);
assert_eq!(narrow::<bf16>(1.0 + one_ulp * 0.75).to_f32(), 1.0 + one_ulp);
}
}