#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum Specials {
None,
SaturateOnOverflow,
InfinityOnOverflow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum SignEncoding {
Absent,
Magnitude,
TwosComplement,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct FloatLayout {
pub exponent_bits: u8,
pub mantissa_bits: u8,
pub specials: Specials,
pub bias_override: Option<i16>,
pub sign: SignEncoding,
}
const fn pow2(exponent: i32) -> f32 {
match exponent {
-126..=127 => f32::from_bits(((exponent + 127) as u32) << 23),
-149..=-127 => f32::from_bits(1 << (exponent + 149)),
..=-150 => 0.0,
_ => f32::INFINITY,
}
}
impl FloatLayout {
pub const E5M2: Self = Self::ieee_like(5, 2, Specials::InfinityOnOverflow);
pub const E5M2_SATURATING: Self = Self::ieee_like(5, 2, Specials::SaturateOnOverflow);
pub const F16: Self = Self::ieee_like(5, 10, Specials::InfinityOnOverflow);
pub const E2M1: Self = Self::ieee_like(2, 1, Specials::None);
pub const E2M2: Self = Self::ieee_like(2, 2, Specials::None);
pub const E3M2: Self = Self::ieee_like(3, 2, Specials::None);
pub const E2M3: Self = Self::ieee_like(2, 3, Specials::None);
pub const BF32: Self = Self::ieee_like(8, 23, Specials::InfinityOnOverflow);
pub const BF16: Self = Self::ieee_like(8, 7, Specials::InfinityOnOverflow);
pub const BF8: Self = Self::E5M2;
pub const BF4: Self = Self::E2M1;
pub const INT8: Self = Self::integer(8, SignEncoding::TwosComplement);
pub const UINT8: Self = Self::integer(8, SignEncoding::Absent);
pub const INT16: Self = Self::integer(16, SignEncoding::TwosComplement);
pub const UINT16: Self = Self::integer(16, SignEncoding::Absent);
pub const INT32: Self = Self::integer(32, SignEncoding::TwosComplement);
pub const UINT32: Self = Self::integer(32, SignEncoding::Absent);
pub const UINT1: Self = Self::integer(1, SignEncoding::Absent);
pub const UINT2: Self = Self::integer(2, SignEncoding::Absent);
pub const UINT3: Self = Self::integer(3, SignEncoding::Absent);
pub const UINT4: Self = Self::integer(4, SignEncoding::Absent);
pub const UINT5: Self = Self::integer(5, SignEncoding::Absent);
pub const UINT6: Self = Self::integer(6, SignEncoding::Absent);
pub const SIGN_MAGNITUDE_INT8: Self = Self::integer(8, SignEncoding::Magnitude);
const fn ieee_like(exponent_bits: u8, mantissa_bits: u8, specials: Specials) -> Self {
Self {
exponent_bits,
mantissa_bits,
specials,
bias_override: None,
sign: SignEncoding::Magnitude,
}
}
const fn integer(width: u8, sign: SignEncoding) -> Self {
let mantissa_bits = match sign {
SignEncoding::Absent => width,
SignEncoding::Magnitude | SignEncoding::TwosComplement => width - 1,
};
Self {
exponent_bits: 0,
mantissa_bits,
specials: Specials::None,
bias_override: Some(1 - mantissa_bits as i16),
sign,
}
}
#[must_use]
pub const fn bias(&self) -> i16 {
match self.bias_override {
Some(bias) => bias,
None => (1i16 << self.exponent_bits.saturating_sub(1)) - 1,
}
}
#[must_use]
pub const fn width(&self) -> u8 {
let sign_bits = match self.sign {
SignEncoding::Absent => 0,
SignEncoding::Magnitude | SignEncoding::TwosComplement => 1,
};
sign_bits + self.exponent_bits + self.mantissa_bits
}
#[must_use]
pub fn same_format(&self, other: &Self) -> bool {
self.exponent_bits == other.exponent_bits
&& self.mantissa_bits == other.mantissa_bits
&& self.specials == other.specials
&& self.bias() == other.bias()
&& self.sign == other.sign
}
#[must_use]
pub const fn packed_bytes(
&self,
elements: u64,
scale_block: Option<(FloatLayout, u64)>,
) -> u64 {
let element_bits = elements * self.width() as u64;
let scale_bits = match scale_block {
Some((scale, block)) if block > 0 => elements.div_ceil(block) * scale.width() as u64,
_ => 0,
};
(element_bits + scale_bits).div_ceil(8)
}
#[must_use]
pub const fn max_normal(&self) -> f32 {
let exponent_codes = (1i32 << self.exponent_bits) - 1;
let top_code = match self.specials {
Specials::None => exponent_codes,
Specials::SaturateOnOverflow | Specials::InfinityOnOverflow => exponent_codes - 1,
};
let significand = 2.0 - pow2(-(self.mantissa_bits as i32));
significand * pow2(top_code - self.bias() as i32)
}
#[must_use]
pub const fn min_normal(&self) -> f32 {
pow2(1 - self.bias() as i32)
}
#[must_use]
pub const fn min_subnormal(&self) -> f32 {
pow2(1 - self.bias() as i32 - self.mantissa_bits as i32)
}
}
#[cfg(test)]
mod tests {
use super::*;
const CASES: [(FloatLayout, i16, u8, f32, f32, f32); 6] = [
(
FloatLayout::E5M2,
15,
8,
57344.0,
6.103_515_6e-5,
1.525_878_9e-5,
),
(FloatLayout::E2M1, 1, 4, 6.0, 1.0, 0.5),
(FloatLayout::E3M2, 3, 6, 28.0, 0.25, 0.0625),
(FloatLayout::E2M3, 1, 6, 7.5, 1.0, 0.125),
(
FloatLayout::BF16,
127,
16,
3.389_531_4e38,
f32::MIN_POSITIVE,
f32::from_bits(1 << 16),
),
(
FloatLayout::BF32,
127,
32,
f32::MAX,
f32::MIN_POSITIVE,
1.0e-45,
),
];
#[test]
fn float_layout_derived_bias_per_layout() {
let biases = CASES.map(|case| case.0.bias());
assert_eq!(biases, CASES.map(|case| case.1));
}
#[test]
fn float_layout_derived_width_per_layout() {
let widths = CASES.map(|case| case.0.width());
assert_eq!(widths, CASES.map(|case| case.2));
}
#[test]
fn float_layout_derived_max_normal_per_layout() {
let maxima = CASES.map(|case| case.0.max_normal());
assert_eq!(maxima, CASES.map(|case| case.3));
}
#[test]
fn float_layout_derived_min_normal_per_layout() {
let minima = CASES.map(|case| case.0.min_normal());
assert_eq!(minima, CASES.map(|case| case.4));
}
#[test]
fn float_layout_derived_min_subnormal_per_layout() {
let minima = CASES.map(|case| case.0.min_subnormal());
assert_eq!(minima, CASES.map(|case| case.5));
}
#[test]
fn float_layout_derived_bias_override_shifts_range() {
let shifted = FloatLayout {
bias_override: Some(4),
..FloatLayout::E3M2
};
assert_eq!(shifted.bias(), 4);
assert_eq!(shifted.max_normal(), 14.0);
}
#[test]
fn float_layout_constants_bf8_equals_e5m2() {
assert_eq!(FloatLayout::BF8, FloatLayout::E5M2);
}
#[test]
fn float_layout_constants_bf4_equals_e2m1() {
assert_eq!(FloatLayout::BF4, FloatLayout::E2M1);
}
#[test]
fn float_layout_constants_bf32_is_e8m23() {
let layout = FloatLayout::BF32;
assert_eq!((layout.exponent_bits, layout.mantissa_bits), (8, 23));
assert_eq!(layout.max_normal(), f32::MAX);
assert_eq!(layout.min_subnormal().to_bits(), 1);
}
#[test]
fn float_layout_constants_f16_is_e5m10() {
let layout = FloatLayout::F16;
assert_eq!((layout.exponent_bits, layout.mantissa_bits), (5, 10));
assert_eq!(layout.max_normal(), 65504.0);
assert_eq!(layout.min_subnormal(), 2.0f32.powi(-24));
}
#[test]
fn float_layout_constants_bf16_is_e8m7() {
let layout = FloatLayout::BF16;
assert_eq!((layout.exponent_bits, layout.mantissa_bits), (8, 7));
assert_eq!(layout.max_normal().to_bits(), 0x7f7f_0000);
}
#[test]
fn float_layout_constants_specials_and_widths_per_layout() {
let observed = [
FloatLayout::E5M2,
FloatLayout::BF8,
FloatLayout::E5M2_SATURATING,
FloatLayout::BF16,
FloatLayout::BF32,
FloatLayout::E3M2,
FloatLayout::E2M3,
FloatLayout::E2M1,
FloatLayout::BF4,
]
.map(|layout| (layout.specials, layout.width()));
let saturate = Specials::SaturateOnOverflow;
let infinity = Specials::InfinityOnOverflow;
let expected = [
(infinity, 8),
(infinity, 8),
(saturate, 8),
(infinity, 16),
(infinity, 32),
(Specials::None, 6),
(Specials::None, 6),
(Specials::None, 4),
(Specials::None, 4),
];
assert_eq!(observed, expected);
}
}