use alloc::vec;
use alloc::vec::Vec;
use core::{default::Default, ops::Deref};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct QuantScheme {
pub value: QuantValue,
pub param: QuantParam,
pub store: QuantStore,
pub level: QuantLevel,
pub mode: QuantMode,
}
impl Default for QuantScheme {
fn default() -> Self {
Self {
value: QuantValue::Q8F,
param: QuantParam::F32,
store: QuantStore::PackedU32(0),
level: QuantLevel::Tensor,
mode: QuantMode::Symmetric,
}
}
}
impl QuantScheme {
pub fn with_level(mut self, level: QuantLevel) -> Self {
self.level = level;
self
}
pub fn with_mode(mut self, mode: QuantMode) -> Self {
self.mode = mode;
self
}
pub fn with_value(mut self, value: QuantValue) -> Self {
self.value = value;
self
}
pub fn with_store(mut self, store: QuantStore) -> Self {
self.store = store;
self
}
pub fn with_param(mut self, param: QuantParam) -> Self {
self.param = param;
self
}
pub fn size_bits_stored(&self) -> usize {
self.store.size_bits(&self.value)
}
pub fn size_bits_value(&self) -> usize {
self.value.size_bits()
}
pub fn num_quants(&self) -> usize {
self.size_bits_stored() / self.value.size_bits()
}
pub fn native_packing(&self) -> usize {
self.value.native_packing()
}
pub fn packing_dim(&self) -> Option<usize> {
self.store.packing_dim()
}
pub fn swap_packing_dim(&mut self, dim0: usize, dim1: usize) {
if let QuantStore::PackedU32(packed_dim) | QuantStore::PackedNative(packed_dim) =
&mut self.store
{
if *packed_dim == dim0 {
*packed_dim = dim1;
} else if *packed_dim == dim1 {
*packed_dim = dim0;
}
}
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum QuantLevel {
Tensor,
Block(BlockSize),
BlockTensor {
block: BlockSize,
global: QuantParam,
},
}
impl QuantLevel {
pub fn block(values: impl AsRef<[u8]>) -> Self {
QuantLevel::Block(BlockSize::new(values))
}
pub fn block_tensor(values: impl AsRef<[u8]>, global: QuantParam) -> Self {
QuantLevel::BlockTensor {
block: BlockSize::new(values),
global,
}
}
pub fn block_size(&self) -> Option<BlockSize> {
match self {
QuantLevel::Tensor => None,
QuantLevel::Block(block) | QuantLevel::BlockTensor { block, .. } => Some(*block),
}
}
pub fn global_param(&self) -> Option<QuantParam> {
match self {
QuantLevel::Tensor | QuantLevel::Block(_) => None,
QuantLevel::BlockTensor { global, .. } => Some(*global),
}
}
}
impl QuantParam {
pub fn max_representable(&self) -> f32 {
match self {
QuantParam::F32 => f32::MAX,
QuantParam::F16 => half::f16::MAX.to_f32(),
QuantParam::BF16 => half::bf16::MAX.to_f32(),
QuantParam::UE8M0 => f32::from_bits(0x7F00_0000), QuantParam::UE4M3 => 448.0,
}
}
pub fn round_up(&self, scale: f32) -> Option<f32> {
match self {
QuantParam::F32 => {
return Some(scale);
}
QuantParam::UE8M0 => {
return None;
}
_ => {}
}
if scale.is_nan() {
return Some(scale);
}
debug_assert!(scale >= 0.0, "a quantization scale is never negative");
let max = self.max_representable();
if scale >= max {
return Some(max);
}
let grid = self.f32_grid();
if let Some(subnormals) = grid.subnormals
&& scale < subnormals.min_normal
{
return Some(num_traits::Float::ceil(scale / subnormals.spacing) * subnormals.spacing);
}
Some(f32::from_bits(
(scale.to_bits() + grid.round_up_bias()) & grid.truncate_mask(),
))
}
pub fn f32_grid(&self) -> F32Grid {
const fn bit_step(mantissa_digits: u32) -> u32 {
1 << (f32::MANTISSA_DIGITS - mantissa_digits)
}
match self {
QuantParam::F16 => F32Grid {
bit_step: bit_step(half::f16::MANTISSA_DIGITS),
subnormals: Some(SubnormalRange {
min_normal: half::f16::MIN_POSITIVE.to_f32(),
spacing: half::f16::MIN_POSITIVE_SUBNORMAL.to_f32(),
}),
},
QuantParam::BF16 => F32Grid {
bit_step: bit_step(half::bf16::MANTISSA_DIGITS),
subnormals: None,
},
QuantParam::UE4M3 => F32Grid {
bit_step: bit_step(4),
subnormals: Some(SubnormalRange {
min_normal: 0.015625, spacing: 0.001953125, }),
},
QuantParam::F32 => {
unimplemented!("F32 is the grid, it has no narrower one to round onto")
}
QuantParam::UE8M0 => unimplemented!("UE8M0 scales are not yet supported"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct F32Grid {
pub bit_step: u32,
pub subnormals: Option<SubnormalRange>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SubnormalRange {
pub min_normal: f32,
pub spacing: f32,
}
impl F32Grid {
pub fn truncate_mask(&self) -> u32 {
!(self.bit_step - 1)
}
pub fn round_up_bias(&self) -> u32 {
self.bit_step - 1
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum QuantValue {
Q8F,
E5M2,
E4M3,
Q4F,
E2M1,
Q2F,
Q8S,
Q4S,
Q2S,
}
impl QuantValue {
pub fn size_bits(&self) -> usize {
match self {
QuantValue::Q8F | QuantValue::Q8S | QuantValue::E4M3 | QuantValue::E5M2 => 8,
QuantValue::Q4F | QuantValue::Q4S | QuantValue::E2M1 => 4,
QuantValue::Q2F | QuantValue::Q2S => 2,
}
}
pub fn native_packing(&self) -> usize {
match self {
QuantValue::E2M1 => 2,
_ => 1,
}
}
pub fn range(&self) -> (f32, f32) {
match self {
QuantValue::Q8F => (i8::MIN as f32, i8::MAX as f32),
QuantValue::Q4F => (-8.0, 7.0),
QuantValue::Q2F => (-2.0, 1.0),
QuantValue::Q8S => (-i8::MAX as f32, i8::MAX as f32),
QuantValue::Q4S => (-7.0, 7.0),
QuantValue::Q2S => (-1.0, 1.0),
QuantValue::E4M3 => (-448.0, 448.0),
QuantValue::E5M2 => (-57344.0, 57344.0),
QuantValue::E2M1 => (-6.0, 6.0), }
}
pub fn is_symmetric(&self) -> bool {
match self {
Self::Q8F | Self::Q4F | Self::Q2F | Self::E4M3 | Self::E5M2 | Self::E2M1 => false,
Self::Q8S | Self::Q4S | Self::Q2S => true,
}
}
}
impl QuantStore {
pub fn size_bits(&self, value: &QuantValue) -> usize {
match self {
QuantStore::Native => value.size_bits(),
QuantStore::PackedNative(_) => value.size_bits() * value.native_packing(),
QuantStore::PackedU32(_) => 32,
}
}
fn packing_dim(&self) -> Option<usize> {
match self {
QuantStore::Native => None,
QuantStore::PackedNative(packing_dim) | QuantStore::PackedU32(packing_dim) => {
Some(*packing_dim)
}
}
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum QuantStore {
Native,
PackedNative(usize),
PackedU32(usize),
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum QuantMode {
Symmetric,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum QuantParam {
F32,
F16,
BF16,
UE8M0,
UE4M3,
}
const MAX_DIMS: usize = 5;
#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct BlockSize {
storage: [u8; MAX_DIMS],
len: u8,
}
impl core::fmt::Debug for BlockSize {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "BlockSize({:?})", self.as_slice())
}
}
impl BlockSize {
pub const MAX_DIMS: usize = MAX_DIMS;
pub fn new(values: impl AsRef<[u8]>) -> Self {
let values = values.as_ref();
debug_assert!(
values.len() <= MAX_DIMS,
"Tried creating a block size larger than the cap"
);
let len = values.len().min(MAX_DIMS);
let mut storage = [1; MAX_DIMS];
storage[..len].copy_from_slice(&values[..len]);
Self {
storage,
len: len as u8,
}
}
pub fn new_trim(values: impl AsRef<[u8]>) -> Self {
let values = values.as_ref();
let first_value = values.iter().position(|s| *s != 1).unwrap_or(0);
Self::new(&values[first_value..])
}
pub fn as_slice(&self) -> &[u8] {
&self.storage[..self.len as usize]
}
pub fn to_vec(&self) -> Vec<u8> {
self.storage[..self.len as usize].to_vec()
}
pub fn as_dim<const N: usize>(&self) -> [u8; N] {
let data_len = N.min(self.len as usize);
let data_start = N - data_len;
let mut out = [1; N];
out[data_start..].copy_from_slice(&self.storage[..data_len]);
out
}
pub fn to_dim_vec(&self, len: usize) -> Vec<u8> {
let data_len = len.min(self.len as usize);
let data_start = len - data_len;
let mut out = vec![1; len];
out[data_start..].copy_from_slice(&self.storage[..data_len]);
out
}
pub fn iter(&self) -> impl Iterator<Item = &u8> {
self.as_slice().iter()
}
pub fn num_elements(&self) -> usize {
self.iter().map(|it| *it as usize).product()
}
}
impl Deref for BlockSize {
type Target = [u8];
fn deref(&self) -> &Self::Target {
self.as_slice()
}
}
impl<T: AsRef<[u8]>> From<T> for BlockSize {
fn from(value: T) -> Self {
BlockSize::new(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_up_never_lands_below_the_scale() {
for param in [QuantParam::F16, QuantParam::BF16, QuantParam::UE4M3] {
for exp in -12..8 {
for step in 1..17 {
let scale = (step as f32 / 16.0) * 2f32.powi(exp);
let up = param.round_up(scale).unwrap();
assert!(
up >= scale,
"{param:?}: {up} is below {scale}, which clips the block maximum"
);
}
}
}
}
#[test]
fn round_up_saturates_rather_than_stepping_off_the_top() {
for param in [QuantParam::F16, QuantParam::BF16, QuantParam::UE4M3] {
let max = param.max_representable();
assert_eq!(param.round_up(max).unwrap(), max);
assert!(param.round_up(max * 2.0).unwrap().is_finite());
}
}
#[test]
fn round_up_answers_for_every_param() {
for param in [
QuantParam::F32,
QuantParam::F16,
QuantParam::BF16,
QuantParam::UE8M0,
QuantParam::UE4M3,
] {
assert_eq!(
param.round_up(0.3).is_some(),
param != QuantParam::UE8M0,
"{param:?}"
);
}
}
#[test]
fn round_up_is_the_identity_for_f32() {
for scale in [1.0e-30, 0.1, 1.0, 12345.678, f32::MAX] {
assert_eq!(QuantParam::F32.round_up(scale).unwrap(), scale);
}
}
#[cfg(feature = "fp8")]
mod storage_types {
use super::*;
#[test]
fn round_up_is_the_nearest_representable_value_not_below() {
for param in [QuantParam::F16, QuantParam::BF16, QuantParam::UE4M3] {
for exp in -8..6 {
let scale = 1.7 * 2f32.powi(exp);
let up = param.round_up(scale).unwrap();
assert_eq!(
up,
param.round_up(up).unwrap(),
"{param:?}: not idempotent at {scale}"
);
assert!(
step(param, up, -1) < scale,
"{param:?}: {up} overshoots {scale} by at least a step"
);
}
}
}
#[test]
fn f32_grid_matches_the_storage_types() {
for param in [QuantParam::F16, QuantParam::BF16, QuantParam::UE4M3] {
let grid = param.f32_grid();
if let Some(subnormals) = grid.subnormals {
assert_eq!(
subnormals.min_normal,
min_normal(param),
"{param:?}: minimum normal"
);
assert_eq!(
subnormals.spacing,
step(param, 0.0, 1),
"{param:?}: subnormal spacing"
);
}
let mut value = min_normal(param);
let max = param.max_representable();
while value < max {
let stepped = f32::from_bits(value.to_bits() + grid.bit_step);
assert_eq!(
stepped,
step(param, value, 1),
"{param:?}: step above {value}"
);
value = stepped;
}
assert_eq!(
value, max,
"{param:?}: the grid has to land exactly on the maximum"
);
}
}
#[test]
fn max_representable_matches_the_e4m3_type() {
assert_eq!(
QuantParam::UE4M3.max_representable(),
crate::e4m3::MAX.to_f32()
);
}
#[test]
fn max_representable_matches_the_e8m0_type() {
assert_eq!(
QuantParam::UE8M0.max_representable(),
crate::ue8m0::MAX as f32
);
}
fn step(param: QuantParam, value: f32, offset: i32) -> f32 {
match param {
QuantParam::F16 => half::f16::from_bits(
(half::f16::from_f32(value).to_bits() as i32 + offset) as u16,
)
.to_f32(),
QuantParam::BF16 => half::bf16::from_bits(
(half::bf16::from_f32(value).to_bits() as i32 + offset) as u16,
)
.to_f32(),
QuantParam::UE4M3 => crate::e4m3::from_bits(
(crate::e4m3::from_f32(value).to_bits() as i32 + offset) as u8,
)
.to_f32(),
QuantParam::F32 | QuantParam::UE8M0 => unreachable!(),
}
}
fn min_normal(param: QuantParam) -> f32 {
match param {
QuantParam::F16 => half::f16::MIN_POSITIVE.to_f32(),
QuantParam::BF16 => half::bf16::MIN_POSITIVE.to_f32(),
QuantParam::UE4M3 => crate::e4m3::MIN_POSITIVE.to_f32(),
QuantParam::F32 | QuantParam::UE8M0 => unreachable!(),
}
}
}
}