pub use cubecl_common::quant::scheme::{
BlockScale, BlockSize, QuantMode, QuantScheme, QuantStore, QuantValue, ScaleDtype,
};
pub const QPARAM_ALIGN: usize = core::mem::align_of::<f32>();
use alloc::vec::Vec;
use core::any::TypeId;
use cubecl_common::{e4m3, ue8m0};
use serde::{Deserialize, Serialize};
use super::packing::{PackedOrder, ValueLayout};
use crate::{DType, Metadata, Shape, bytes::Bytes};
#[derive(new, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct QuantConfig {
pub scheme: QuantScheme,
pub propagation: QuantPropagation,
}
#[derive(
Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default,
)]
pub enum QuantAcc {
#[default]
F32,
F16,
BF16,
}
pub enum Calibration {
MinMax,
AbsMean,
}
#[derive(
Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default,
)]
pub enum QuantPropagation {
Propagate,
#[default]
Inhibit,
}
#[derive(Clone, Debug)]
pub struct QParams<S> {
pub scales: S,
pub global: Option<S>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct DecodedScales {
pub block: Vec<f32>,
pub global: Option<f32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QParamTensor {
pub offset_start: usize,
pub offset_end: usize,
pub metadata: Metadata,
pub dtype: DType,
}
pub fn quantizable(scheme: &QuantScheme) -> bool {
match (scheme.block_scale(), global_scale_dtype(scheme)) {
(Some(block), Some(global)) => {
block.dtype.max_representable() <= crate::f16::MAX.to_f32() && global == ScaleDtype::F32
}
_ => true,
}
}
pub fn global_scale_dtype(scheme: &QuantScheme) -> Option<ScaleDtype> {
scheme.block_scale().and(scheme.tensor_scale())
}
pub fn params_shape(data_shape: &Shape, scheme: &QuantScheme) -> Shape {
match scheme.block_size() {
None => Shape::new([1]),
Some(block_size) => Shape::from(block_size.num_blocks(data_shape.as_slice())),
}
}
#[derive(Debug, Clone)]
pub struct BlockLayout {
shape: Shape,
block: Vec<u8>,
blocks: Shape,
}
impl BlockLayout {
pub fn new(shape: &Shape, block: &BlockSize) -> Self {
Self {
shape: shape.clone(),
block: block.to_dim_vec(shape.num_dims()),
blocks: Shape::from(block.num_blocks(shape.as_slice())),
}
}
pub fn num_blocks(&self) -> usize {
self.blocks.num_elements()
}
pub fn divides(&self) -> bool {
self.shape
.iter()
.zip(&self.block)
.all(|(&dim, &extent)| dim.is_multiple_of(extent as usize))
}
pub fn block_of(&self, mut index: usize) -> usize {
let mut block = 0;
let mut stride = 1;
for dim in (0..self.shape.num_dims()).rev() {
let coordinate = index % self.shape[dim];
index /= self.shape[dim];
block += coordinate / self.block[dim] as usize * stride;
stride *= self.blocks[dim];
}
block
}
}
pub struct QuantizedBytes {
pub bytes: Bytes,
pub scheme: QuantScheme,
pub shape: Shape,
}
impl QuantizedBytes {
pub fn new<E: bytemuck::CheckedBitPattern + bytemuck::NoUninit>(
value: Vec<E>,
shape: impl Into<Shape>,
scheme: QuantScheme,
scales: &[f32],
global: Option<f32>,
) -> Self {
let shape = shape.into();
assert_eq!(
value.len(),
shape.num_elements(),
"{} quantized values do not fill a tensor of shape {shape:?}",
value.len()
);
if TypeId::of::<E>() != TypeId::of::<i8>() {
panic!("Invalid quantized type");
}
let i8s: Vec<i8> = bytemuck::allocation::cast_vec(value);
let layout = ValueLayout::new(&scheme);
let mut bytes = match layout {
ValueLayout::Bytes => Bytes::from_elems(i8s),
ValueLayout::Packed { packed_dim, word } => {
Bytes::from_elems(PackedOrder::new(shape.as_slice(), packed_dim).pack(&i8s, &word))
}
};
let scales = match (
scheme.block_size(),
layout.block_scale_order(&shape, &scheme),
) {
(None, _) => scales[..1].to_vec(),
(Some(_), Some(order)) => order.to_stored(scales),
(Some(_), None) => scales.to_vec(),
};
let scale_bytes = encode_scales(&scales, scheme.scale_dtype());
bytes.extend_from_byte_slice_aligned(scale_bytes.as_slice(), QPARAM_ALIGN);
match (global_scale_dtype(&scheme), global) {
(Some(dtype), Some(global)) => {
assert_eq!(
dtype,
ScaleDtype::F32,
"a two-level scheme stores its per-tensor scale as f32, got {scheme:?}"
);
let global_bytes = encode_scales(&[global], dtype);
bytes.extend_from_byte_slice_aligned(global_bytes.as_slice(), QPARAM_ALIGN);
}
(Some(_), None) => panic!("{scheme:?} requires a per-tensor scale"),
(None, Some(_)) => panic!("{scheme:?} does not take a per-tensor scale"),
(None, None) => {}
}
Self {
bytes,
scheme,
shape,
}
}
pub fn num_elements(&self) -> usize {
self.shape.num_elements()
}
pub fn into_vec_i8(self) -> (Vec<i8>, DecodedScales) {
let scheme = self.scheme;
let scale_order = ValueLayout::new(&scheme).block_scale_order(&self.shape, &scheme);
let (values, (qparams, num_params)) = self.split_values_off();
let global_bytes = global_scale_size(&scheme);
let block_end = qparams
.len()
.checked_sub(global_bytes)
.expect("quantized parameter buffer is shorter than the scheme's global scale");
let block_start = block_end
.checked_sub(scale_size(scheme.scale_dtype()) * num_params)
.expect("quantized parameter buffer is shorter than the scheme's block scales");
let block = decode_scales(&qparams[block_start..block_end], scheme.scale_dtype());
let block = match scale_order {
Some(order) => order.to_row_major(&block),
None => block,
};
let global =
global_scale_dtype(&scheme).map(|dtype| decode_scales(&qparams[block_end..], dtype)[0]);
(values, DecodedScales { block, global })
}
fn split_values_off(self) -> (Vec<i8>, (Vec<u8>, usize)) {
let num_params = params_shape(&self.shape, &self.scheme).num_elements();
let scale_bytes =
scale_size(self.scheme.scale_dtype()) * num_params + global_scale_size(&self.scheme);
let (shape, scheme) = (self.shape, self.scheme);
let mut stored = read_bytes_to_i8(self.bytes);
let split_at = stored
.len()
.checked_sub(scale_bytes)
.expect("quantized tensor data is shorter than its scheme's parameters");
let qparams = bytemuck::cast_vec(stored.split_off(split_at));
let layout = ValueLayout::new(&scheme);
let expected = value_bytes(&scheme, &shape);
assert_eq!(
stored.len(),
expected,
"the values of a {shape:?} tensor under {scheme:?} take {expected} bytes, not {}",
stored.len()
);
let values = match layout {
ValueLayout::Bytes => stored,
ValueLayout::Packed { packed_dim, word } => {
PackedOrder::new(shape.as_slice(), packed_dim)
.unpack(bytemuck::cast_slice(&stored), &word)
}
};
(values, (qparams, num_params))
}
}
pub fn scale_to_dtype(scale: f32, dtype: ScaleDtype) -> f32 {
dtype.round_up(scale)
}
pub fn global_scale_size(scheme: &QuantScheme) -> usize {
global_scale_dtype(scheme).map_or(0, scale_size)
}
fn storage_elements(scheme: &QuantScheme, shape: &Shape) -> Option<usize> {
let num_quants = scheme.num_quants();
if num_quants == 0 {
return None;
}
match scheme.store {
QuantStore::PackedU32(packed_dim) | QuantStore::PackedNative(packed_dim) => {
if shape.is_empty() {
return (packed_dim == 0).then_some(1);
}
let packed_dim = shape.num_dims().checked_sub(packed_dim.checked_add(1)?)?;
let mut storage = shape.clone();
if num_quants > 1 {
storage[packed_dim] = storage[packed_dim].div_ceil(num_quants);
}
storage
.iter()
.try_fold(1usize, |elements, &dim| elements.checked_mul(dim))
}
QuantStore::Native => shape
.iter()
.try_fold(1usize, |elements, &dim| elements.checked_mul(dim)),
}
}
pub fn quantized_data_len(scheme: &QuantScheme, shape: &Shape) -> usize {
try_quantized_data_len(scheme, shape)
.expect("quantized tensor layout is invalid or overflows usize")
}
pub(crate) fn try_quantized_data_len(scheme: &QuantScheme, shape: &Shape) -> Option<usize> {
let value_bytes_per_element = match scheme.store {
QuantStore::Native => core::mem::size_of::<i8>(),
_ => scheme.size_bits_stored().div_ceil(8),
};
let value_bytes = storage_elements(scheme, shape)?.checked_mul(value_bytes_per_element)?;
let num_params = if let Some(block) = scheme.block_size() {
if block.as_slice().len() > shape.num_dims() {
return None;
}
let block_shape = block.to_dim_vec(shape.num_dims());
let mut num_params = 1usize;
for (&dim, block) in shape.iter().zip(block_shape) {
let block = block as usize;
if block == 0 {
return None;
}
num_params = num_params.checked_mul(dim.div_ceil(block))?;
}
num_params
} else {
1
};
let scale_bytes = num_params.checked_mul(scale_size(scheme.scale_dtype()))?;
value_bytes
.checked_add(scale_bytes)?
.checked_add(global_scale_size(scheme))
}
fn value_bytes(scheme: &QuantScheme, shape: &Shape) -> usize {
storage_elements(scheme, shape).expect("quantized tensor layout is invalid or overflows usize")
* scheme.size_bits_stored().div_ceil(8)
}
pub fn scale_size(dtype: ScaleDtype) -> usize {
match dtype {
ScaleDtype::F32 => 4,
ScaleDtype::F16 | ScaleDtype::BF16 => 2,
ScaleDtype::UE8M0 | ScaleDtype::UE4M3 => 1,
}
}
fn decode_scales(bytes: &[u8], dtype: ScaleDtype) -> Vec<f32> {
match dtype {
ScaleDtype::F32 => bytes
.as_chunks::<4>()
.0
.iter()
.map(|c| f32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
.collect(),
ScaleDtype::F16 => bytes
.as_chunks::<2>()
.0
.iter()
.map(|c| crate::f16::from_ne_bytes([c[0], c[1]]).to_f32())
.collect(),
ScaleDtype::BF16 => bytes
.as_chunks::<2>()
.0
.iter()
.map(|c| crate::bf16::from_ne_bytes([c[0], c[1]]).to_f32())
.collect(),
ScaleDtype::UE4M3 => bytes.iter().map(|b| e4m3::from_bits(*b).to_f32()).collect(),
ScaleDtype::UE8M0 => bytes
.iter()
.map(|b| ue8m0::from_bits(*b).to_f32())
.collect(),
}
}
fn encode_scales(scales: &[f32], dtype: ScaleDtype) -> Vec<u8> {
match dtype {
ScaleDtype::F32 => scales.iter().flat_map(|s| s.to_ne_bytes()).collect(),
ScaleDtype::F16 => scales
.iter()
.flat_map(|s| crate::f16::from_f32(*s).to_ne_bytes())
.collect(),
ScaleDtype::BF16 => scales
.iter()
.flat_map(|s| crate::bf16::from_f32(*s).to_ne_bytes())
.collect(),
ScaleDtype::UE4M3 => scales
.iter()
.map(|s| e4m3::from_f32(*s).to_bits())
.collect(),
ScaleDtype::UE8M0 => scales
.iter()
.map(|s| ue8m0::from_f32(*s).to_bits())
.collect(),
}
}
fn read_bytes_to_i8(bytes: Bytes) -> Vec<i8> {
match bytes.try_into_vec::<i8>() {
Ok(val) => val,
Err(bytes) => unsafe { core::mem::transmute::<Vec<u8>, Vec<i8>>(bytes.to_vec()) },
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn should_pack_unpack_quantization_parameters_per_tensor_symmetric() {
let scale = 0.03937008;
let values = vec![0i8, 25, 51, 76, 102, 127];
let q_bytes = QuantizedBytes::new(
values.clone(),
[2, 3],
QuantScheme::default()
.with_value(QuantValue::Q8S)
.with_store(QuantStore::Native),
&[scale],
None,
);
let (q_values, qparams) = q_bytes.into_vec_i8();
assert_eq!(qparams.block, vec![scale]);
assert_eq!(q_values, values);
}
#[test]
fn scale_to_dtype_survives_the_codec() {
let scales = [0.5f32, 0.3, 1.0 / 3.0, 500.0, 1e-3, 7.7e-4];
for dtype in [
ScaleDtype::F32,
ScaleDtype::F16,
ScaleDtype::BF16,
ScaleDtype::UE4M3,
ScaleDtype::UE8M0,
] {
let rounded: Vec<f32> = scales.iter().map(|s| scale_to_dtype(*s, dtype)).collect();
let via_codec = decode_scales(&encode_scales(&rounded, dtype), dtype);
assert_eq!(
rounded, via_codec,
"the codec moves a scale {dtype:?} can already represent"
);
for (scale, rounded) in scales.iter().zip(&rounded).filter(|(s, _)| **s < 500.0) {
assert!(
rounded >= scale,
"{scale} rounded down to {rounded} for {dtype:?}"
);
}
}
}
#[test]
fn should_pack_unpack_two_level_scales() {
let block_scales = [0.5f32, 0.125];
let global = 3.0f32;
let values = vec![0i8, 25, 51, 76, 102, 127, -128, -1];
let scheme = QuantScheme::default()
.with_value(QuantValue::Q8S)
.with_store(QuantStore::Native)
.per_block([4], ScaleDtype::UE4M3)
.per_tensor(ScaleDtype::F32);
let q_bytes = QuantizedBytes::new(values.clone(), [8], scheme, &block_scales, Some(global));
assert_eq!(q_bytes.bytes.len(), 8 + 2 + 4);
let (q_values, scales) = q_bytes.into_vec_i8();
assert_eq!(q_values, values);
assert_eq!(scales.block, block_scales);
assert_eq!(scales.global, Some(global));
}
#[test]
fn should_pack_unpack_ue8m0_block_scales() {
let block_scales = [0.25f32, 8.0];
let values = vec![0i8, 25, 51, 76, 102, 127, -128, -1];
let scheme = QuantScheme::default()
.with_value(QuantValue::Q8S)
.with_store(QuantStore::Native)
.per_block([4], ScaleDtype::UE8M0);
let q_bytes = QuantizedBytes::new(values.clone(), [8], scheme, &block_scales, None);
assert_eq!(q_bytes.bytes.len(), 8 + 2);
let (q_values, scales) = q_bytes.into_vec_i8();
assert_eq!(q_values, values);
assert_eq!(scales.block, block_scales);
assert_eq!(scales.global, None);
}
#[test]
#[should_panic(expected = "requires a per-tensor scale")]
fn two_level_scheme_without_a_global_scale_is_rejected() {
let scheme = QuantScheme::default()
.with_value(QuantValue::Q8S)
.with_store(QuantStore::Native)
.per_block([4], ScaleDtype::F32)
.per_tensor(ScaleDtype::F32);
QuantizedBytes::new(vec![0i8; 8], [8], scheme, &[0.5, 0.125], None);
}
#[test]
#[should_panic(expected = "stores its per-tensor scale as f32")]
fn a_narrower_per_tensor_scale_is_rejected() {
let scheme = QuantScheme::default()
.with_value(QuantValue::Q8S)
.with_store(QuantStore::Native)
.per_block([4], ScaleDtype::UE4M3)
.per_tensor(ScaleDtype::F16);
QuantizedBytes::new(vec![0i8; 8], [8], scheme, &[0.5, 0.125], Some(3.0));
}
#[test]
fn quantizable_declines_what_no_backend_can_store() {
assert!(quantizable(&QuantScheme::default()));
assert!(quantizable(
&QuantScheme::default().per_block([4], ScaleDtype::F16)
));
assert!(quantizable(
&QuantScheme::default()
.per_block([4], ScaleDtype::UE4M3)
.per_tensor(ScaleDtype::F32)
));
assert!(quantizable(
&QuantScheme::default().per_block([4], ScaleDtype::UE8M0)
));
assert!(quantizable(
&QuantScheme::default().per_tensor(ScaleDtype::UE8M0)
));
assert!(quantizable(
&QuantScheme::default()
.with_value(QuantValue::E2M1)
.per_block([32], ScaleDtype::UE8M0)
));
assert!(!quantizable(
&QuantScheme::default()
.per_block([4], ScaleDtype::F32)
.per_tensor(ScaleDtype::F32)
));
assert!(!quantizable(
&QuantScheme::default()
.per_block([4], ScaleDtype::UE4M3)
.per_tensor(ScaleDtype::BF16)
));
}
#[test]
fn encoded_scale_width_matches_scale_size() {
let scales = [0.5f32, 0.25, 0.125];
for dtype in [
ScaleDtype::F32,
ScaleDtype::F16,
ScaleDtype::BF16,
ScaleDtype::UE4M3,
ScaleDtype::UE8M0,
] {
assert_eq!(
encode_scales(&scales, dtype).len(),
scale_size(dtype) * scales.len(),
"encoded width disagrees with scale_size for {dtype:?}"
);
}
}
#[test]
fn should_pack_unpack_ue4m3_block_scales() {
let scales = [0.5f32, 0.125];
let values = vec![0i8, 25, 51, 76, 102, 127, -128, -1];
let q_bytes = QuantizedBytes::new(
values.clone(),
[8],
QuantScheme::default()
.with_value(QuantValue::Q8S)
.with_store(QuantStore::Native)
.per_block([4], ScaleDtype::UE4M3),
&scales,
None,
);
let (q_values, qparams) = q_bytes.into_vec_i8();
assert_eq!(qparams.block, scales);
assert_eq!(q_values, values);
}
}