Skip to main content

rten_simd/
float16.rs

1//! A 16-bit floating point ("half precision") type.
2//!
3//! Rust does not yet have a stable built-in `f16` type, so this module defines
4//! one as a wrapper around [`u16`].
5//!
6//! This can be replaced with f16 from the Rust standard library when that is
7//! stabilized. See <https://github.com/rust-lang/rust/issues/116909>.
8
9use crate::elem::{Elem, WrappingAdd};
10
11/// A 16-bit floating point number, stored in IEEE 754 half-precision format.
12#[allow(non_camel_case_types)]
13#[derive(Copy, Clone, Default, PartialEq)]
14#[repr(transparent)]
15pub struct f16(u16);
16
17impl f16 {
18    /// Create an `f16` from its raw bit pattern.
19    #[inline]
20    pub const fn from_bits(bits: u16) -> Self {
21        f16(bits)
22    }
23
24    /// Return the raw bit pattern of this value.
25    #[inline]
26    pub const fn to_bits(self) -> u16 {
27        self.0
28    }
29
30    /// Convert an `f32` to the nearest `f16`, rounding ties to even.
31    #[inline]
32    pub fn from_f32(x: f32) -> Self {
33        f16(f32_to_f16(x))
34    }
35
36    /// Convert this value to an `f32`.
37    ///
38    /// This conversion is always exact, since every `f16` value is
39    /// representable as an `f32`.
40    #[inline]
41    pub fn to_f32(self) -> f32 {
42        f16_to_f32(self.0)
43    }
44}
45
46impl std::fmt::Debug for f16 {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        write!(f, "{}", self.to_f32())
49    }
50}
51
52impl From<f32> for f16 {
53    #[inline]
54    fn from(x: f32) -> f16 {
55        f16::from_f32(x)
56    }
57}
58
59impl From<f16> for f32 {
60    #[inline]
61    fn from(x: f16) -> f32 {
62        x.to_f32()
63    }
64}
65
66// This is implemented only because the `Elem` trait requires it.
67impl WrappingAdd for f16 {
68    type Output = Self;
69
70    fn wrapping_add(self, x: Self) -> Self {
71        f16::from_f32(self.to_f32() + x.to_f32())
72    }
73}
74
75impl Elem for f16 {
76    fn one() -> Self {
77        // 1.0 in IEEE 754 half precision.
78        f16(0x3C00)
79    }
80}
81
82/// Convert an f16 value to f32.
83///
84/// The implementation is copied from the `half` crate -
85/// <https://github.com/VoidStarKat/half-rs>.
86pub fn f16_to_f32(i: u16) -> f32 {
87    // Check for signed zero
88    if i & 0x7FFFu16 == 0 {
89        return f32::from_bits((i as u32) << 16);
90    }
91
92    let half_sign = (i & 0x8000u16) as u32;
93    let half_exp = (i & 0x7C00u16) as u32;
94    let half_man = (i & 0x03FFu16) as u32;
95
96    // Check for an infinity or NaN when all exponent bits set
97    if half_exp == 0x7C00u32 {
98        // Check for signed infinity if mantissa is zero
99        if half_man == 0 {
100            return f32::from_bits((half_sign << 16) | 0x7F80_0000u32);
101        } else {
102            // NaN, keep current mantissa but also set most significiant mantissa bit
103            return f32::from_bits((half_sign << 16) | 0x7FC0_0000u32 | (half_man << 13));
104        }
105    }
106
107    // Calculate single-precision components with adjusted exponent
108    let sign = half_sign << 16;
109    // Unbias exponent
110    let unbiased_exp = ((half_exp as i32) >> 10) - 15;
111
112    // Check for subnormals, which will be normalized by adjusting exponent
113    if half_exp == 0 {
114        // Calculate how much to adjust the exponent by
115        let e = (half_man as u16).leading_zeros() - 6;
116
117        // Rebias and adjust exponent
118        let exp = (127 - 15 - e) << 23;
119        let man = (half_man << (14 + e)) & 0x7F_FF_FFu32;
120        return f32::from_bits(sign | exp | man);
121    }
122
123    // Rebias exponent for a normalized normal
124    let exp = ((unbiased_exp + 127) as u32) << 23;
125    let man = (half_man & 0x03FFu32) << 13;
126    f32::from_bits(sign | exp | man)
127}
128
129/// Convert an f16 value to f32.
130///
131/// The implementation is copied from the `half` crate -
132/// <https://github.com/VoidStarKat/half-rs>.
133pub fn f32_to_f16(value: f32) -> u16 {
134    let x: u32 = value.to_bits();
135
136    // Extract IEEE754 components
137    let sign = x & 0x8000_0000u32;
138    let exp = x & 0x7F80_0000u32;
139    let man = x & 0x007F_FFFFu32;
140
141    // Check for all exponent bits being set, which is Infinity or NaN
142    if exp == 0x7F80_0000u32 {
143        // Set mantissa MSB for NaN (and also keep shifted mantissa bits)
144        let nan_bit = if man == 0 { 0 } else { 0x0200u32 };
145        return ((sign >> 16) | 0x7C00u32 | nan_bit | (man >> 13)) as u16;
146    }
147
148    // The number is normalized, start assembling half precision version
149    let half_sign = sign >> 16;
150    // Unbias the exponent, then bias for half precision
151    let unbiased_exp = ((exp >> 23) as i32) - 127;
152    let half_exp = unbiased_exp + 15;
153
154    // Check for exponent overflow, return +infinity
155    if half_exp >= 0x1F {
156        return (half_sign | 0x7C00u32) as u16;
157    }
158
159    // Check for underflow
160    if half_exp <= 0 {
161        // Check mantissa for what we can do
162        if 14 - half_exp > 24 {
163            // No rounding possibility, so this is a full underflow, return signed zero
164            return half_sign as u16;
165        }
166        // Don't forget about hidden leading mantissa bit when assembling mantissa
167        let man = man | 0x0080_0000u32;
168        let mut half_man = man >> (14 - half_exp);
169        // Check for rounding (see comment above functions)
170        let round_bit = 1 << (13 - half_exp);
171        if (man & round_bit) != 0 && (man & (3 * round_bit - 1)) != 0 {
172            half_man += 1;
173        }
174        // No exponent for subnormals
175        return (half_sign | half_man) as u16;
176    }
177
178    // Rebias the exponent
179    let half_exp = (half_exp as u32) << 10;
180    let half_man = man >> 13;
181    // Check for rounding (see comment above functions)
182    let round_bit = 0x0000_1000u32;
183    if (man & round_bit) != 0 && (man & (3 * round_bit - 1)) != 0 {
184        // Round it
185        ((half_sign | half_exp | half_man) + 1) as u16
186    } else {
187        (half_sign | half_exp | half_man) as u16
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::{f16, f16_to_f32, f32_to_f16};
194
195    #[test]
196    fn test_known_values() {
197        // (f32 value, f16 bit pattern)
198        let cases = [
199            (0.0f32, 0x0000u16),
200            (-0.0, 0x8000),
201            (1.0, 0x3C00),
202            (-1.0, 0xBC00),
203            (2.0, 0x4000),
204            (0.5, 0x3800),
205            (-2.0, 0xC000),
206            (65504.0, 0x7BFF), // Largest normal f16.
207            (f32::INFINITY, 0x7C00),
208            (f32::NEG_INFINITY, 0xFC00),
209        ];
210
211        for (f, bits) in cases {
212            assert_eq!(f32_to_f16(f), bits, "f32_to_f16({f})");
213            assert_eq!(f16_to_f32(bits), f, "f16_to_f32({bits:#06x})");
214        }
215    }
216
217    #[test]
218    fn test_overflow_to_inf() {
219        assert_eq!(f32_to_f16(1e30), 0x7C00);
220        assert_eq!(f32_to_f16(-1e30), 0xFC00);
221    }
222
223    #[test]
224    fn test_nan() {
225        let nan = f32_to_f16(f32::NAN);
226        assert!(f16_to_f32(nan).is_nan());
227    }
228
229    #[test]
230    fn test_subnormal() {
231        // Smallest positive subnormal f16 is 2^-24.
232        let smallest = 2f32.powi(-24);
233        assert_eq!(f32_to_f16(smallest), 0x0001);
234        assert_eq!(f16_to_f32(0x0001), smallest);
235
236        // Values below half the smallest subnormal round to zero.
237        assert_eq!(f32_to_f16(2f32.powi(-26)), 0x0000);
238    }
239
240    #[test]
241    fn test_round_to_even() {
242        // 2^-25 is exactly halfway between 0 and the smallest subnormal
243        // (2^-24). Ties round to even, so it rounds down to zero.
244        assert_eq!(f32_to_f16(2f32.powi(-25)), 0x0000);
245        // Just above the halfway point rounds up.
246        assert_eq!(f32_to_f16(2f32.powi(-25) * 1.001), 0x0001);
247    }
248
249    #[test]
250    fn test_roundtrip_exact() {
251        // Every f16 -> f32 -> f16 round-trip is exact.
252        for bits in 0..=u16::MAX {
253            // Skip NaNs, whose bit pattern is not preserved exactly.
254            let exp = (bits >> 10) & 0x1F;
255            let mant = bits & 0x3FF;
256            if exp == 0x1F && mant != 0 {
257                continue;
258            }
259            let f = f16_to_f32(bits);
260            assert_eq!(f32_to_f16(f), bits, "roundtrip {bits:#06x}");
261        }
262    }
263
264    #[test]
265    fn test_f16_wrapper() {
266        assert_eq!(f16::from_f32(1.0).to_bits(), 0x3C00);
267        assert_eq!(f16::from_bits(0x4000).to_f32(), 2.0);
268        assert_eq!(f32::from(f16::from(3.5f32)), 3.5);
269        assert_eq!(format!("{:?}", f16::from_f32(1.5)), "1.5");
270    }
271}