use furiosa_opt_macro::primitive;
use num_traits::{Num, One, Zero};
use rand::distr::StandardUniform;
use std::fmt::Debug;
use std::ops::{Add, Div, Mul, Rem, Sub};
pub trait Scalar: ndarray::LinalgScalar + Debug + Clone + Copy + PartialEq + Num + Send + Sync {
const BITS: usize;
fn size_in_bytes_from_length(length: usize) -> usize {
crate::constraints::size_in_bytes(Self::BITS, length)
}
fn load(bytes: &[u8], i: usize) -> Self {
let n = std::mem::size_of::<Self>();
unsafe { std::ptr::read_unaligned(bytes[i * n..(i + 1) * n].as_ptr() as *const Self) }
}
fn store(bytes: &mut [u8], i: usize, v: Self) {
let n = std::mem::size_of::<Self>();
let raw = unsafe { std::slice::from_raw_parts(&v as *const Self as *const u8, n) };
bytes[i * n..(i + 1) * n].copy_from_slice(raw);
}
fn length_from_bytes(bytes: usize) -> usize {
assert_eq!(
(bytes * 8) % Self::BITS,
0,
"bytes must correspond to a whole number of elements"
);
(bytes * 8) / Self::BITS
}
fn to_buf(vals: &[Self]) -> Vec<u8> {
let bytes = unsafe { std::slice::from_raw_parts(vals.as_ptr() as *const u8, std::mem::size_of_val(vals)) };
bytes.to_vec()
}
}
pub trait ScalarBytes: Scalar {
fn from_le_bytes(bytes: &[u8]) -> Self;
}
pub trait MaterializableScalar: Scalar {}
macro_rules! impl_scalar {
($($t:ty);* $(;)?) => {
$(
impl Scalar for $t {
const BITS: usize = ::std::mem::size_of::<$t>() * 8;
}
impl ScalarBytes for $t {
fn from_le_bytes(bytes: &[u8]) -> Self {
let arr = bytes.try_into().unwrap_or_else(|_| {
panic!(
"{} expects {} bytes, got {}",
stringify!($t),
<$t as Scalar>::BITS / 8,
bytes.len()
)
});
<$t>::from_le_bytes(arr)
}
}
impl MaterializableScalar for $t {}
)*
};
}
impl_scalar! {
i8;
u8;
i16;
i32;
f8e4m3;
f8e5m2;
bf16;
f32;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Opt<D> {
Init(D),
Uninit,
}
impl<D> Opt<D> {
#[inline]
pub fn map<D2>(self, f: impl FnOnce(D) -> D2) -> Opt<D2> {
match self {
Opt::Init(val) => Opt::Init(f(val)),
Opt::Uninit => Opt::Uninit,
}
}
#[inline]
pub fn zip_map<D2, R>(self, other: Opt<D2>, f: impl FnOnce(D, D2) -> R) -> Opt<R> {
match (self, other) {
(Opt::Init(a), Opt::Init(b)) => Opt::Init(f(a, b)),
_ => Opt::Uninit,
}
}
pub fn unwrap(self) -> D {
let Opt::Init(val) = self else {
panic!("Called unwrap on an uninitialized Opt value.");
};
val
}
}
impl<D: Add<Output = D>> Add for Opt<D> {
type Output = Self;
fn add(self, rhs: Self) -> Opt<D> {
self.zip_map(rhs, |l, r| l + r)
}
}
impl<D: Mul<Output = D>> Mul for Opt<D> {
type Output = Self;
fn mul(self, rhs: Self) -> Opt<D> {
self.zip_map(rhs, |l, r| l * r)
}
}
impl<D: Zero> Zero for Opt<D> {
fn zero() -> Self {
Opt::Init(D::zero())
}
fn is_zero(&self) -> bool {
let Opt::Init(val) = self else {
return false;
};
val.is_zero()
}
}
impl<D: One> One for Opt<D> {
fn one() -> Self {
Opt::Init(D::one())
}
}
#[primitive(bf16)]
#[expect(non_camel_case_types)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct bf16(half::bf16);
impl Zero for bf16 {
fn zero() -> Self {
bf16(half::bf16::from_f32(0.0))
}
fn is_zero(&self) -> bool {
self.0.is_zero()
}
}
impl One for bf16 {
fn one() -> Self {
bf16(half::bf16::from_f32(1.0))
}
}
impl Add<Self> for bf16 {
type Output = Self;
fn add(self, rhs: Self) -> Self {
bf16(self.0 + rhs.0)
}
}
impl Sub<Self> for bf16 {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
bf16(self.0 - rhs.0)
}
}
impl Mul<Self> for bf16 {
type Output = Self;
fn mul(self, rhs: Self) -> Self {
bf16(self.0 * rhs.0)
}
}
impl Div<Self> for bf16 {
type Output = Self;
fn div(self, rhs: Self) -> Self {
bf16(self.0 / rhs.0)
}
}
impl Rem<Self> for bf16 {
type Output = Self;
fn rem(self, rhs: Self) -> Self {
bf16(self.0 % rhs.0)
}
}
impl Num for bf16 {
type FromStrRadixErr = <half::bf16 as Num>::FromStrRadixErr;
fn from_str_radix(str: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
Ok(bf16(half::bf16::from_str_radix(str, radix)?))
}
}
impl rand::distr::Distribution<bf16> for StandardUniform {
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> bf16 {
let val: f32 = rng.random_range(-1.0..1.0);
bf16(half::bf16::from_f32(val))
}
}
impl bf16 {
pub(crate) fn from_le_bytes(bytes: [u8; 2]) -> Self {
bf16(half::bf16::from_bits(u16::from_le_bytes(bytes)))
}
pub fn from_f32(val: f32) -> Self {
bf16(half::bf16::from_f32(val))
}
pub fn to_f32(self) -> f32 {
self.0.to_f32()
}
}
impl From<bf16> for f32 {
fn from(val: bf16) -> Self {
val.to_f32()
}
}
#[primitive(f8e4m3)]
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct f8e4m3(u8);
impl Zero for f8e4m3 {
fn zero() -> Self {
f8e4m3(crate::float::F8E4_ZERO)
}
fn is_zero(&self) -> bool {
crate::float::f8_e4_is_zero(self.0)
}
}
impl One for f8e4m3 {
fn one() -> Self {
f8e4m3(crate::float::F8E4_ONE)
}
}
impl Add<Self> for f8e4m3 {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self::from_f32(self.to_f32() + rhs.to_f32())
}
}
impl Sub<Self> for f8e4m3 {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self::from_f32(self.to_f32() - rhs.to_f32())
}
}
impl Mul<Self> for f8e4m3 {
type Output = Self;
fn mul(self, rhs: Self) -> Self {
Self::from_f32(self.to_f32() * rhs.to_f32())
}
}
impl Div<Self> for f8e4m3 {
type Output = Self;
fn div(self, rhs: Self) -> Self {
Self::from_f32(self.to_f32() / rhs.to_f32())
}
}
impl Rem<Self> for f8e4m3 {
type Output = Self;
fn rem(self, rhs: Self) -> Self {
Self::from_f32(self.to_f32() % rhs.to_f32())
}
}
impl Num for f8e4m3 {
type FromStrRadixErr = <f32 as Num>::FromStrRadixErr;
fn from_str_radix(str: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
Ok(Self::from_f32(f32::from_str_radix(str, radix)?))
}
}
impl rand::distr::Distribution<f8e4m3> for StandardUniform {
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> f8e4m3 {
let val: f32 = rng.random_range(-1.0..1.0);
f8e4m3::from_f32(val)
}
}
impl f8e4m3 {
pub(crate) fn from_le_bytes(bytes: [u8; 1]) -> Self {
f8e4m3(bytes[0])
}
pub fn from_f32(val: f32) -> Self {
f8e4m3(crate::float::f8_e4_from_f32(val))
}
pub fn to_f32(self) -> f32 {
crate::float::f8_e4_to_f32(self.0)
}
}
impl From<f8e4m3> for f32 {
fn from(val: f8e4m3) -> Self {
val.to_f32()
}
}
#[primitive(f8e5m2)]
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct f8e5m2(u8);
impl Zero for f8e5m2 {
fn zero() -> Self {
f8e5m2(crate::float::F8E5_ZERO)
}
fn is_zero(&self) -> bool {
crate::float::f8_e5_is_zero(self.0)
}
}
impl One for f8e5m2 {
fn one() -> Self {
f8e5m2(crate::float::F8E5_ONE)
}
}
impl Add<Self> for f8e5m2 {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self::from_f32(self.to_f32() + rhs.to_f32())
}
}
impl Sub<Self> for f8e5m2 {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self::from_f32(self.to_f32() - rhs.to_f32())
}
}
impl Mul<Self> for f8e5m2 {
type Output = Self;
fn mul(self, rhs: Self) -> Self {
Self::from_f32(self.to_f32() * rhs.to_f32())
}
}
impl Div<Self> for f8e5m2 {
type Output = Self;
fn div(self, rhs: Self) -> Self {
Self::from_f32(self.to_f32() / rhs.to_f32())
}
}
impl Rem<Self> for f8e5m2 {
type Output = Self;
fn rem(self, rhs: Self) -> Self {
Self::from_f32(self.to_f32() % rhs.to_f32())
}
}
impl Num for f8e5m2 {
type FromStrRadixErr = <f32 as Num>::FromStrRadixErr;
fn from_str_radix(str: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
Ok(Self::from_f32(f32::from_str_radix(str, radix)?))
}
}
impl rand::distr::Distribution<f8e5m2> for StandardUniform {
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> f8e5m2 {
let val: f32 = rng.random_range(-1.0..1.0);
f8e5m2::from_f32(val)
}
}
impl f8e5m2 {
pub(crate) fn from_le_bytes(bytes: [u8; 1]) -> Self {
f8e5m2(bytes[0])
}
pub fn from_f32(val: f32) -> Self {
f8e5m2(crate::float::f8_e5_from_f32(val))
}
pub fn to_f32(self) -> f32 {
crate::float::f8_e5_to_f32(self.0)
}
}
impl From<f8e5m2> for f32 {
fn from(val: f8e5m2) -> Self {
val.to_f32()
}
}
#[primitive(i4)]
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct i4(i8);
impl Zero for i4 {
fn zero() -> Self {
i4(0)
}
fn is_zero(&self) -> bool {
self.0 == 0
}
}
impl One for i4 {
fn one() -> Self {
i4(1)
}
}
impl Add<Self> for i4 {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self::from_lsb(self.0 + rhs.0)
}
}
impl Sub<Self> for i4 {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self::from_lsb(self.0 - rhs.0)
}
}
impl Mul<Self> for i4 {
type Output = Self;
fn mul(self, rhs: Self) -> Self {
Self::from_lsb(self.0 * rhs.0)
}
}
impl Div<Self> for i4 {
type Output = Self;
fn div(self, rhs: Self) -> Self {
Self::from_lsb(self.0 / rhs.0)
}
}
impl Rem<Self> for i4 {
type Output = Self;
fn rem(self, rhs: Self) -> Self {
Self::from_lsb(self.0 % rhs.0)
}
}
impl Num for i4 {
type FromStrRadixErr = <i8 as Num>::FromStrRadixErr;
fn from_str_radix(str: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
Ok(Self::from_lsb(i8::from_str_radix(str, radix)?))
}
}
impl Scalar for i4 {
const BITS: usize = 4;
fn load(bytes: &[u8], i: usize) -> Self {
i4::from_i32(get_nibble(bytes, i) as i32)
}
fn store(bytes: &mut [u8], i: usize, v: i4) {
set_nibble(bytes, i, v.to_i32() as u8);
}
fn to_buf(vals: &[Self]) -> Vec<u8> {
pack_nibbles(vals)
}
}
impl i4 {
fn from_lsb(n: i8) -> Self {
i4((n << 4) >> 4)
}
pub fn from_i32(val: i32) -> Self {
Self::from_lsb(val as i8)
}
pub fn to_i32(self) -> i32 {
i32::from(self.0)
}
}
impl From<i4> for i32 {
fn from(val: i4) -> Self {
val.to_i32()
}
}
#[primitive(i5)]
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct i5(i8);
impl Zero for i5 {
fn zero() -> Self {
i5(0)
}
fn is_zero(&self) -> bool {
self.0 == 0
}
}
impl One for i5 {
fn one() -> Self {
i5(1)
}
}
impl Add<Self> for i5 {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self::from_i32(self.to_i32() + rhs.to_i32())
}
}
impl Sub<Self> for i5 {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self::from_i32(self.to_i32() - rhs.to_i32())
}
}
impl Mul<Self> for i5 {
type Output = Self;
fn mul(self, rhs: Self) -> Self {
Self::from_i32(self.to_i32() * rhs.to_i32())
}
}
impl Div<Self> for i5 {
type Output = Self;
fn div(self, rhs: Self) -> Self {
Self::from_i32(self.to_i32() / rhs.to_i32())
}
}
impl Rem<Self> for i5 {
type Output = Self;
fn rem(self, rhs: Self) -> Self {
Self::from_i32(self.to_i32() % rhs.to_i32())
}
}
impl Num for i5 {
type FromStrRadixErr = <i8 as Num>::FromStrRadixErr;
fn from_str_radix(str: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
Ok(Self::from_lsb(i8::from_str_radix(str, radix)?))
}
}
impl Scalar for i5 {
const BITS: usize = 4;
}
impl i5 {
fn from_lsb(n: i8) -> Self {
i5((n << 3) >> 3)
}
pub fn from_i32(val: i32) -> Self {
Self::from_lsb(val as i8)
}
pub fn to_i32(self) -> i32 {
i32::from(self.0)
}
}
impl From<i5> for i32 {
fn from(val: i5) -> Self {
val.to_i32()
}
}
#[primitive(i9)]
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct i9(i16);
impl Zero for i9 {
fn zero() -> Self {
i9(0)
}
fn is_zero(&self) -> bool {
self.0 == 0
}
}
impl One for i9 {
fn one() -> Self {
i9(1)
}
}
impl Add<Self> for i9 {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self::from_i32(self.to_i32() + rhs.to_i32())
}
}
impl Sub<Self> for i9 {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self::from_i32(self.to_i32() - rhs.to_i32())
}
}
impl Mul<Self> for i9 {
type Output = Self;
fn mul(self, rhs: Self) -> Self {
Self::from_i32(self.to_i32() * rhs.to_i32())
}
}
impl Div<Self> for i9 {
type Output = Self;
fn div(self, rhs: Self) -> Self {
Self::from_i32(self.to_i32() / rhs.to_i32())
}
}
impl Rem<Self> for i9 {
type Output = Self;
fn rem(self, rhs: Self) -> Self {
Self::from_i32(self.to_i32() % rhs.to_i32())
}
}
impl Num for i9 {
type FromStrRadixErr = <i16 as Num>::FromStrRadixErr;
fn from_str_radix(str: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
Ok(Self::from_lsb(i16::from_str_radix(str, radix)?))
}
}
impl Scalar for i9 {
const BITS: usize = 8;
}
impl i9 {
fn from_lsb(n: i16) -> Self {
i9((n << 7) >> 7)
}
pub fn from_i32(val: i32) -> Self {
Self::from_lsb(val as i16)
}
pub fn to_i32(self) -> i32 {
i32::from(self.0)
}
}
impl From<i9> for i32 {
fn from(val: i9) -> Self {
val.to_i32()
}
}
impl MaterializableScalar for i4 {}
#[primitive(f4e2m1)]
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Debug)]
pub struct f4e2m1(u8);
const F4E2M1_TO_F8E4: [u8; 16] = [
0x00, 0x30, 0x38, 0x3c, 0x40, 0x44, 0x48, 0x4c, 0x80, 0xb0, 0xb8, 0xbc, 0xc0, 0xc4, 0xc8, 0xcc,
];
impl f4e2m1 {
#[inline]
pub const fn from_bits(bits: u8) -> Self {
f4e2m1(bits & 0xf)
}
pub const fn to_bits(self) -> u8 {
self.0
}
pub fn to_f8e4m3(self) -> f8e4m3 {
f8e4m3(F4E2M1_TO_F8E4[self.0 as usize])
}
pub fn to_f32(self) -> f32 {
self.to_f8e4m3().to_f32()
}
pub fn from_f32(v: f32) -> Self {
let mut best = 0u8;
let mut best_err = f32::INFINITY;
for code in 0..16u8 {
let err = (Self::from_bits(code).to_f32() - v).abs();
if err < best_err {
best_err = err;
best = code;
}
}
Self::from_bits(best)
}
}
impl From<f4e2m1> for f32 {
fn from(val: f4e2m1) -> Self {
val.to_f32()
}
}
impl PartialEq for f4e2m1 {
fn eq(&self, other: &Self) -> bool {
self.to_f32() == other.to_f32()
}
}
impl Zero for f4e2m1 {
fn zero() -> Self {
Self::from_bits(0x0)
}
fn is_zero(&self) -> bool {
self.0 == 0x0 || self.0 == 0x8
}
}
impl One for f4e2m1 {
fn one() -> Self {
Self::from_bits(0x2)
}
}
impl Add<Self> for f4e2m1 {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self::from_f32(self.to_f32() + rhs.to_f32())
}
}
impl Sub<Self> for f4e2m1 {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self::from_f32(self.to_f32() - rhs.to_f32())
}
}
impl Mul<Self> for f4e2m1 {
type Output = Self;
fn mul(self, rhs: Self) -> Self {
Self::from_f32(self.to_f32() * rhs.to_f32())
}
}
impl Div<Self> for f4e2m1 {
type Output = Self;
fn div(self, rhs: Self) -> Self {
Self::from_f32(self.to_f32() / rhs.to_f32())
}
}
impl Rem<Self> for f4e2m1 {
type Output = Self;
fn rem(self, rhs: Self) -> Self {
Self::from_f32(self.to_f32() % rhs.to_f32())
}
}
impl Num for f4e2m1 {
type FromStrRadixErr = <f32 as Num>::FromStrRadixErr;
fn from_str_radix(str: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
Ok(Self::from_f32(f32::from_str_radix(str, radix)?))
}
}
impl rand::distr::Distribution<f4e2m1> for StandardUniform {
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> f4e2m1 {
f4e2m1::from_bits(rng.random_range(0..16u8))
}
}
impl Scalar for f4e2m1 {
const BITS: usize = 4;
fn load(bytes: &[u8], i: usize) -> Self {
f4e2m1::from_bits(get_nibble(bytes, i))
}
fn store(bytes: &mut [u8], i: usize, v: f4e2m1) {
set_nibble(bytes, i, v.to_bits());
}
fn to_buf(vals: &[Self]) -> Vec<u8> {
pack_nibbles(vals)
}
}
impl MaterializableScalar for f4e2m1 {}
#[inline]
fn get_nibble(bytes: &[u8], i: usize) -> u8 {
if i & 1 == 0 {
bytes[i / 2] & 0x0f
} else {
bytes[i / 2] >> 4
}
}
#[inline]
fn set_nibble(bytes: &mut [u8], i: usize, nib: u8) {
let b = &mut bytes[i / 2];
let nib = nib & 0x0f;
*b = if i & 1 == 0 {
(*b & 0xf0) | nib
} else {
(*b & 0x0f) | (nib << 4)
};
}
fn pack_nibbles<D: Scalar>(vals: &[D]) -> Vec<u8> {
assert!(
vals.len().is_multiple_of(2),
"a 4-bit stream needs an even (byte-aligned) element count, got {}",
vals.len(),
);
let mut out = vec![0u8; vals.len() / 2];
for (i, &v) in vals.iter().enumerate() {
D::store(&mut out, i, v);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn to_buf_byte_aligned_is_passthrough() {
assert_eq!(i8::to_buf(&[1, -1, 127]), vec![1, 0xff, 127]);
assert_eq!(
f32::to_buf(&[1.0f32, 2.0]),
[1.0f32.to_le_bytes(), 2.0f32.to_le_bytes()].concat()
);
assert_eq!(f32::to_buf(&[0.0; 5]).len(), f32::size_in_bytes_from_length(5));
}
#[test]
fn to_buf_4bit_is_dense() {
let vals: Vec<f4e2m1> = [0x1, 0x2, 0x3, 0x4].iter().map(|&b| f4e2m1::from_bits(b)).collect();
assert_eq!(f4e2m1::to_buf(&vals), vec![0x21, 0x43]);
assert_eq!(f4e2m1::to_buf(&vals).len(), f4e2m1::size_in_bytes_from_length(4));
let i4s: Vec<i4> = [1, 2, -1].iter().map(|&n| i4::from_i32(n)).collect();
assert_eq!(i4::to_buf(&i4s[..2]), vec![0x21]);
}
}