Skip to main content

cubecl_common/float/fp8/
fp8_e8m0.rs

1use core::fmt::Display;
2
3use bytemuck::{Pod, Zeroable};
4
5/// An 8-bit unsigned floating point type with 8 exponent bits and no mantissa bits.
6/// Used for scaling factors.
7///
8/// [`Minifloat`]: https://en.wikipedia.org/wiki/Minifloat
9#[allow(non_camel_case_types)]
10#[repr(transparent)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12#[derive(Clone, Copy, Default, Zeroable, Pod, PartialEq, PartialOrd, Debug)]
13pub struct ue8m0(u8);
14
15impl ue8m0 {
16    /// Maximum representable value
17    pub const MAX: ue8m0 = ue8m0(0xfe);
18    /// Minimum representable value
19    pub const MIN: ue8m0 = ue8m0(0);
20
21    /// Constructs a [`ue8m0`] value from the raw bits.
22    #[inline]
23    #[must_use]
24    pub const fn from_bits(bits: u8) -> ue8m0 {
25        ue8m0(bits)
26    }
27
28    /// Constructs a [`ue8m0`] value from a 32-bit floating point value.
29    ///
30    /// Rounds up to a power of two, as a scale is stored: values past the range saturate to its
31    /// ends, a value at or below zero becomes the minimum, and NaN stays NaN.
32    #[inline]
33    #[must_use]
34    pub const fn from_f32(value: f32) -> ue8m0 {
35        Self::from_f64(value as f64)
36    }
37
38    /// Constructs a [`ue8m0`] value from a 64-bit floating point value.
39    ///
40    /// Rounds up to a power of two, as a scale is stored: values past the range saturate to its
41    /// ends, a value at or below zero becomes the minimum, and NaN stays NaN.
42    #[inline]
43    #[must_use]
44    pub const fn from_f64(value: f64) -> ue8m0 {
45        if value.is_nan() {
46            return ue8m0(0xff);
47        }
48        if value <= 0.0 {
49            return Self::MIN;
50        }
51        let bits = value.to_bits();
52        let exponent = ((bits >> 52) & 0x7ff) as i32 - 1023;
53        // The base-2 log rounded up: the exponent, plus one unless the value is a power of two.
54        let log2_up = exponent + (bits & ((1 << 52) - 1) != 0) as i32;
55        match log2_up + 127 {
56            biased if biased < 0 => Self::MIN,
57            biased if biased > 0xfe => Self::MAX,
58            biased => ue8m0(biased as u8),
59        }
60    }
61
62    /// Converts a [`ue8m0`] into the underlying bit representation.
63    #[inline]
64    #[must_use]
65    pub const fn to_bits(self) -> u8 {
66        self.0
67    }
68
69    /// Converts a [`ue8m0`] value into an [`f32`] value.
70    ///
71    /// This conversion is lossless as all values can be represented exactly in [`f32`].
72    #[inline]
73    #[must_use]
74    pub const fn to_f32(self) -> f32 {
75        self.to_f64() as f32
76    }
77
78    /// Converts a [`ue8m0`] value into an [`f64`] value.
79    ///
80    /// This conversion is lossless as all values can be represented exactly in [`f64`].
81    #[inline]
82    #[must_use]
83    pub const fn to_f64(self) -> f64 {
84        match self.0 {
85            0xff => f64::NAN,
86            // 2^(code - 127), a normal f64 whatever the code.
87            code => f64::from_bits((code as u64 + 1023 - 127) << 52),
88        }
89    }
90}
91
92impl Display for ue8m0 {
93    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
94        write!(f, "ue8m0({:x})", self.0)
95    }
96}
97
98mod numeric {
99    use num_traits::{NumCast, ToPrimitive};
100
101    use super::*;
102    use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
103
104    impl Neg for ue8m0 {
105        type Output = Self;
106
107        fn neg(self) -> Self::Output {
108            Self::from_f32(self.to_f32().neg())
109        }
110    }
111
112    impl Mul for ue8m0 {
113        type Output = Self;
114
115        fn mul(self, rhs: Self) -> Self::Output {
116            Self::from_f32(self.to_f32() * rhs.to_f32())
117        }
118    }
119
120    impl MulAssign for ue8m0 {
121        fn mul_assign(&mut self, rhs: Self) {
122            *self = *self * rhs;
123        }
124    }
125
126    impl Div for ue8m0 {
127        type Output = Self;
128
129        fn div(self, rhs: Self) -> Self::Output {
130            Self::from_f32(self.to_f32() / rhs.to_f32())
131        }
132    }
133
134    impl DivAssign for ue8m0 {
135        fn div_assign(&mut self, rhs: Self) {
136            *self = *self / rhs;
137        }
138    }
139
140    impl Add for ue8m0 {
141        type Output = Self;
142
143        fn add(self, rhs: Self) -> Self::Output {
144            Self::from_f32(self.to_f32() + rhs.to_f32())
145        }
146    }
147
148    impl AddAssign for ue8m0 {
149        fn add_assign(&mut self, rhs: Self) {
150            *self = *self + rhs;
151        }
152    }
153
154    impl Sub for ue8m0 {
155        type Output = Self;
156
157        fn sub(self, rhs: Self) -> Self::Output {
158            Self::from_f32(self.to_f32() - rhs.to_f32())
159        }
160    }
161
162    impl SubAssign for ue8m0 {
163        fn sub_assign(&mut self, rhs: Self) {
164            *self = *self - rhs;
165        }
166    }
167
168    impl ToPrimitive for ue8m0 {
169        fn to_i64(&self) -> Option<i64> {
170            Some(ue8m0::to_f32(*self) as i64)
171        }
172
173        fn to_u64(&self) -> Option<u64> {
174            Some(ue8m0::to_f64(*self) as u64)
175        }
176
177        fn to_f32(&self) -> Option<f32> {
178            Some(ue8m0::to_f32(*self))
179        }
180
181        fn to_f64(&self) -> Option<f64> {
182            Some(ue8m0::to_f64(*self))
183        }
184    }
185
186    impl NumCast for ue8m0 {
187        fn from<T: num_traits::ToPrimitive>(n: T) -> Option<Self> {
188            Some(Self::from_f32(n.to_f32()?))
189        }
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn every_code_converts_to_its_value_and_back() {
199        for code in 0..=0xfeu8 {
200            let value = ue8m0::from_bits(code).to_f32();
201            assert_eq!(
202                ue8m0::from_f32(value).to_bits(),
203                code,
204                "{code:#x} is {value:e}"
205            );
206        }
207        assert!(ue8m0::from_bits(0xff).to_f32().is_nan());
208    }
209
210    #[test]
211    fn a_value_between_powers_of_two_rounds_up() {
212        assert_eq!(ue8m0::from_f32(3.0).to_f32(), 4.0);
213        assert_eq!(ue8m0::from_f64(1.0 + f64::EPSILON).to_f64(), 2.0);
214        assert_eq!(ue8m0::from_f64(4.0 * (1.0 + f64::EPSILON)).to_f64(), 8.0);
215        let above_min = ue8m0::MIN.to_f64() * (1.0 + f64::EPSILON);
216        assert_eq!(ue8m0::from_f64(above_min).to_bits(), 1);
217        assert_eq!(ue8m0::from_f32(-1.0), ue8m0::MIN);
218    }
219
220    #[test]
221    fn nan_is_the_all_ones_code() {
222        assert_eq!(ue8m0::from_f32(f32::NAN).to_bits(), 0xff);
223    }
224}