use cubecl::quant::scheme::QuantMode;
use cubecl_common::{
e4m3, e5m2,
quant::scheme::{QuantLevel, QuantScheme, QuantStore, QuantValue},
};
pub fn quantize(
values: &[f32],
shape: &[usize],
scales: &[f32],
block_dims: &[usize],
scheme: &QuantScheme,
) -> Vec<u8> {
let scales_shape = scales_shape(shape, block_dims);
let quantized = quantized_values(values, shape, scales, block_dims, &scales_shape, scheme);
match scheme.store {
QuantStore::Native => encode_native(&quantized, scheme.value),
QuantStore::PackedU32(_) => encode_packed_u32(&quantized, scheme),
other => panic!("quantize stub: unsupported store {other:?}"),
}
}
#[allow(dead_code)]
pub fn dequantize(
bytes: &[u8],
shape: &[usize],
scales: &[f32],
block_dims: &[usize],
scheme: &QuantScheme,
) -> Vec<f32> {
let scales_shape = scales_shape(shape, block_dims);
let quants = match scheme.store {
QuantStore::Native => decode_native(bytes, scheme.value),
QuantStore::PackedU32(_) => decode_packed_u32(bytes, scheme),
other => panic!("dequantize stub: unsupported store {other:?}"),
};
dequantized_values(&quants, scales, shape, block_dims, &scales_shape, scheme)
}
fn quantized_values(
values: &[f32],
shape: &[usize],
scales: &[f32],
block_dims: &[usize],
scales_shape: &[usize],
scheme: &QuantScheme,
) -> Vec<f32> {
let (range_min, range_max) = scheme.value.range();
match scheme.mode {
QuantMode::Symmetric => values
.iter()
.enumerate()
.map(|(i, &v)| {
let scale = scales[scale_index(i, shape, block_dims, scales_shape)];
(v / scale).round().clamp(range_min, range_max)
})
.collect(),
}
}
#[allow(dead_code)]
fn dequantized_values(
quantized: &[f32],
scales: &[f32],
shape: &[usize],
block_dims: &[usize],
scales_shape: &[usize],
scheme: &QuantScheme,
) -> Vec<f32> {
match scheme.mode {
QuantMode::Symmetric => quantized
.iter()
.enumerate()
.map(|(i, &q)| q * scales[scale_index(i, shape, block_dims, scales_shape)])
.collect(),
}
}
fn encode_native(quantized: &[f32], value: QuantValue) -> Vec<u8> {
match value {
QuantValue::Q8F | QuantValue::Q8S => quantized.iter().map(|&q| q as i8 as u8).collect(),
QuantValue::E4M3 => quantized
.iter()
.map(|&q| e4m3::from_f32(q).to_bits())
.collect(),
QuantValue::E5M2 => quantized
.iter()
.map(|&q| e5m2::from_f32(q).to_bits())
.collect(),
other => panic!("quantize stub: {other:?} is not supported for native storage"),
}
}
#[allow(dead_code)]
fn decode_native(bytes: &[u8], value: QuantValue) -> Vec<f32> {
match value {
QuantValue::Q8F | QuantValue::Q8S => bytes.iter().map(|&b| b as i8 as f32).collect(),
QuantValue::E4M3 => bytes.iter().map(|&b| e4m3::from_bits(b).to_f32()).collect(),
QuantValue::E5M2 => bytes.iter().map(|&b| e5m2::from_bits(b).to_f32()).collect(),
other => panic!("dequantize stub: {other:?} is not supported for native storage"),
}
}
pub fn pack_q_values(q: &[i32], scheme: &QuantScheme) -> Vec<u32> {
let size_quant = scheme.size_bits_value();
let num_quants = scheme.num_quants();
let mask = quant_mask(size_quant);
assert!(
q.len().is_multiple_of(num_quants),
"pack_q_values: {} values do not fill whole {num_quants}-value words",
q.len()
);
q.chunks(num_quants)
.map(|chunk| {
chunk.iter().enumerate().fold(0u32, |acc, (p, &v)| {
acc | ((v as u32 & mask) << (p * size_quant))
})
})
.collect()
}
fn encode_packed_u32(quantized: &[f32], scheme: &QuantScheme) -> Vec<u8> {
let size_quant = scheme.size_bits_value();
let num_quants = scheme.num_quants();
let mask = quant_mask(size_quant);
let packed: Vec<u32> = quantized
.chunks(num_quants)
.map(|chunk| {
chunk.iter().enumerate().fold(0u32, |acc, (p, &q)| {
let bits = (q as i32 as u32) & mask;
acc | (bits << (p * size_quant))
})
})
.collect();
bytemuck::cast_slice(&packed).to_vec()
}
#[allow(dead_code)]
fn decode_packed_u32(bytes: &[u8], scheme: &QuantScheme) -> Vec<f32> {
let size_quant = scheme.size_bits_value();
let num_quants = scheme.num_quants();
let mask = quant_mask(size_quant);
let sign_bit = 1u32 << (size_quant - 1);
let mut out = Vec::with_capacity((bytes.len() / 4) * num_quants);
for &word in bytemuck::cast_slice::<u8, u32>(bytes) {
for p in 0..num_quants {
let raw = (word >> (p * size_quant)) & mask;
let q = if raw & sign_bit != 0 {
(raw | !mask) as i32
} else {
raw as i32
};
out.push(q as f32);
}
}
out
}
fn quant_mask(size_quant: usize) -> u32 {
if size_quant >= 32 {
u32::MAX
} else {
(1u32 << size_quant) - 1
}
}
pub(crate) fn block_dims(scheme: &QuantScheme, shape: &[usize]) -> Vec<usize> {
match scheme.level {
QuantLevel::Tensor => shape.to_vec(),
QuantLevel::Block(bs) => bs
.to_dim_vec(shape.len())
.iter()
.map(|&b| b as usize)
.collect(),
QuantLevel::BlockTensor { .. } => unimplemented!(
"two-level quantization is not supported here, got {:?}",
scheme.level
),
}
}
pub(crate) fn scales_shape(shape: &[usize], block_dims: &[usize]) -> Vec<usize> {
assert_eq!(
shape.len(),
block_dims.len(),
"shape/block_dims rank mismatch"
);
shape
.iter()
.zip(block_dims)
.map(|(&d, &b)| {
assert!(
d.is_multiple_of(b),
"block dim {b} must divide dimension {d}"
);
d / b
})
.collect()
}
fn scale_index(
linear: usize,
shape: &[usize],
block_dims: &[usize],
scales_shape: &[usize],
) -> usize {
let rank = shape.len();
let mut rem = linear;
let mut coord = vec![0usize; rank];
for d in (0..rank).rev() {
coord[d] = rem % shape[d];
rem /= shape[d];
}
let mut block_linear = 0;
for d in 0..rank {
block_linear = block_linear * scales_shape[d] + coord[d] / block_dims[d];
}
block_linear
}
#[cfg(test)]
mod tests {
use super::*;
use cubecl_common::quant::scheme::{QuantLevel, QuantMode, QuantParam, QuantStore, QuantValue};
fn scheme(value: QuantValue, store: QuantStore, level: QuantLevel) -> QuantScheme {
QuantScheme::default()
.with_mode(QuantMode::Symmetric)
.with_level(level)
.with_value(value)
.with_store(store)
.with_param(QuantParam::F32)
}
fn assert_round_trips(value: QuantValue, store: QuantStore, scale: f32) {
let s = scheme(value, store, QuantLevel::Tensor);
let n = 64; let values: Vec<f32> = (0..n).map(|i| (i as f32 / n as f32) * 2.0 - 1.0).collect();
let bytes = quantize(&values, &[n], &[scale], &[n], &s);
let restored = dequantize(&bytes, &[n], &[scale], &[n], &s);
let max_err = scale + 1e-6;
for (got, want) in restored.iter().zip(&values) {
assert!(
(got - want).abs() <= max_err,
"{value:?}/{store:?}: dequant {got} too far from {want}",
);
}
}
#[test]
fn native_q8_is_one_to_one_i8() {
let values = vec![-1.0, -0.5, 0.0, 0.5, 1.0];
let scale = 1.0 / 127.0;
let bytes = quantize(
&values,
&[values.len()],
&[scale],
&[values.len()],
&scheme(QuantValue::Q8S, QuantStore::Native, QuantLevel::Tensor),
);
let got: Vec<i8> = bytes.iter().map(|&b| b as i8).collect();
assert_eq!(got, vec![-127, -64, 0, 64, 127]);
}
#[test]
fn round_trips_for_integer_schemes() {
assert_round_trips(QuantValue::Q8S, QuantStore::Native, 1.0 / 127.0);
assert_round_trips(QuantValue::Q8S, QuantStore::PackedU32(0), 1.0 / 127.0);
assert_round_trips(QuantValue::Q4S, QuantStore::PackedU32(0), 1.0 / 7.0);
assert_round_trips(QuantValue::Q2S, QuantStore::PackedU32(0), 1.0 / 1.0);
}
#[test]
fn round_trips_for_fp8_native() {
assert_round_trips(QuantValue::E4M3, QuantStore::Native, 1.0 / 8.0);
assert_round_trips(QuantValue::E5M2, QuantStore::Native, 1.0 / 4.0);
}
#[test]
fn block_level_uses_per_block_scale() {
let s = scheme(
QuantValue::Q8S,
QuantStore::Native,
QuantLevel::block([4u8]),
);
let values = vec![
0.1, 0.2, 0.3, 0.4, 10.0, 20.0, 30.0, 40.0, ];
let scales = vec![0.4 / 127.0, 40.0 / 127.0];
let bytes = quantize(&values, &[8], &scales, &[4], &s);
let got: Vec<i8> = bytes.iter().map(|&b| b as i8).collect();
assert_eq!(got[3], 127);
assert_eq!(got[7], 127);
let restored = dequantize(&bytes, &[8], &scales, &[4], &s);
for (got, want) in restored.iter().zip(&values) {
let step = if *want < 1.0 { scales[0] } else { scales[1] };
assert!((got - want).abs() <= step + 1e-6);
}
}
}