use std::ops::RangeInclusive;
use crate::scalar::{bf16, f8e4m3, f8e5m2, i4, i5, i9};
use super::scalar::Scalar;
pub trait FetchCast<D: Scalar>: Into<D> + Cast<D> {}
impl<D> FetchCast<D> for D where D: Scalar {}
impl FetchCast<i32> for i8 {}
impl FetchCast<f32> for bf16 {}
impl FetchCast<f32> for f8e4m3 {}
impl FetchCast<f32> for f8e5m2 {}
impl FetchCast<i32> for i4 {}
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)
}
}
pub trait Cast<D: Scalar> {
fn cast(self) -> D;
}
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 {
self.to_f32()
}
}
impl Cast<bf16> for f32 {
#[inline]
fn cast(self) -> bf16 {
bf16::from_f32(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 {
fn cast(self) -> f32 {
self.to_f32()
}
}
impl Cast<f8e5m2> for f32 {
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 u8 {
#[inline]
fn cast(self) -> i32 {
i32::from(self)
}
}
impl Cast<u8> for i32 {
#[inline]
fn cast(self) -> u8 {
self as u8
}
}
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 + Cast<<Self as ContractionCast>::Output> {
type Output: Scalar + Cast<Self>;
}
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 i16 {
type Output = i32;
}
impl ContractionCast for u8 {
type Output = i32;
}
impl ContractionCast for i5 {
type Output = i32;
}
impl ContractionCast for i9 {
type Output = i32;
}
impl ContractionCast for i32 {
type Output = i32;
}
impl ContractionCast for f32 {
type Output = f32;
}
#[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([i16::MIN, -1, 0, 1, i16::MAX]);
assert_roundtrip([i32::MIN, -1, 0, 1, i32::MAX]);
assert_roundtrip([0u8, 1, u8::MAX]);
assert_roundtrip([f32::MIN, -1.5, 0.0, 1.5, f32::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));
}
#[test]
fn i32_accumulator_stays_in_i32() {
let (l, r) = (1i32 << 16, 1i32 << 16);
let prod = narrow::<i32>(widen(l).wrapping_mul(widen(r)));
assert_eq!(prod, l.wrapping_mul(r));
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);
let acc16: i32 = std::iter::repeat_n((widen(1000i16), widen(1000i16)), 256)
.map(|(l, r)| l * r)
.sum();
assert_eq!(acc16, 256_000_000);
assert_eq!(narrow::<i16>(acc16), 256_000_000i32 as i16);
let accu: i32 = std::iter::repeat_n((widen(200u8), widen(200u8)), 8)
.map(|(l, r)| l * r)
.sum();
assert_eq!(accu, 8 * 200 * 200);
assert_eq!(narrow::<u8>(accu), (8 * 200 * 200i32) as u8);
assert_eq!(narrow::<u8>(-1i32), 255u8);
let _: <i8 as ContractionCast>::Output = acc;
let _: <i4 as ContractionCast>::Output = 0i32;
let _: <i16 as ContractionCast>::Output = acc16;
let _: <u8 as ContractionCast>::Output = accu;
}
#[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);
}
}