use cubecl::quant::scheme::QuantMode;
use cubecl_common::{
e2m1, e4m3, e5m2,
quant::scheme::{QuantScheme, QuantStore, QuantValue},
};
pub fn quantize(
values: &[f32],
shape: &[usize],
scales: &[f32],
block_dims: &[usize],
scheme: &QuantScheme,
) -> Vec<u8> {
let scales_shape = crate::quant_layout::scales_grid(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 = crate::quant_layout::scales_grid(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)];
let scaled = v / scale;
let snapped = if rounds_to_integers(scheme.value) {
scaled.round()
} else {
scaled
};
snapped.clamp(range_min, range_max)
})
.collect(),
QuantMode::Lookup => panic!("quantize stub: lookup takes exact indices via pack_q_values"),
}
}
fn rounds_to_integers(quant: QuantValue) -> bool {
match quant {
QuantValue::Q8F
| QuantValue::Q8S
| QuantValue::Q4F
| QuantValue::Q4S
| QuantValue::Q2F
| QuantValue::Q2S => true,
QuantValue::E5M2 | QuantValue::E4M3 | QuantValue::E2M1 => false,
}
}
#[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(),
QuantMode::Lookup => panic!(
"dequantize stub: lookup carries no table here; fold table[index] * scale into the \
test's own host reference"
),
}
}
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 = field_code(q, scheme.value) & mask;
acc | (bits << (p * size_quant))
})
})
.collect();
bytemuck::cast_slice(&packed).to_vec()
}
fn field_code(q: f32, value: QuantValue) -> u32 {
match value {
QuantValue::E2M1 => e2m1::from_f32(q).to_bits() as u32,
QuantValue::E4M3 => e4m3::from_f32(q).to_bits() as u32,
QuantValue::E5M2 => e5m2::from_f32(q).to_bits() as u32,
QuantValue::Q8F
| QuantValue::Q8S
| QuantValue::Q4F
| QuantValue::Q4S
| QuantValue::Q2F
| QuantValue::Q2S => q as i32 as u32,
}
}
#[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 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;
out.push(code_value(raw, size_quant, scheme.value));
}
}
out
}
fn code_value(raw: u32, size_quant: usize, value: QuantValue) -> f32 {
match value {
QuantValue::E2M1 => e2m1::from_bits(raw as u8).to_f32(),
QuantValue::E4M3 => e4m3::from_bits(raw as u8).to_f32(),
QuantValue::E5M2 => e5m2::from_bits(raw as u8).to_f32(),
QuantValue::Q8F
| QuantValue::Q8S
| QuantValue::Q4F
| QuantValue::Q4S
| QuantValue::Q2F
| QuantValue::Q2S => {
let mask = quant_mask(size_quant);
let sign_bit = 1u32 << (size_quant - 1);
let q = if raw & sign_bit != 0 {
(raw | !mask) as i32
} else {
raw as i32
};
q as f32
}
}
}
fn quant_mask(size_quant: usize) -> u32 {
if size_quant >= 32 {
u32::MAX
} else {
(1u32 << size_quant) - 1
}
}
pub(crate) fn assert_supported(scheme: &QuantScheme) {
assert!(
scheme.num_levels() <= 1,
"two-level quantization is not supported by the reference quantizer, got {scheme:?}"
);
}
pub(crate) fn block_dims(scheme: &QuantScheme, shape: &[usize]) -> Vec<usize> {
assert_supported(scheme);
crate::quant_layout::block_dims(scheme, shape)
}
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::{QuantMode, QuantStore, QuantValue, ScaleDtype};
fn scheme(value: QuantValue, store: QuantStore) -> QuantScheme {
QuantScheme::default()
.with_mode(QuantMode::Symmetric)
.with_value(value)
.with_store(store)
}
fn assert_round_trips(value: QuantValue, store: QuantStore, scale: f32) {
let s = scheme(value, store).per_tensor(ScaleDtype::F32);
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).per_tensor(ScaleDtype::F32),
);
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);
}
fn round_trip_at_unit_scale(value: QuantValue, store: QuantStore, values: &[f32]) -> Vec<f32> {
let s = scheme(value, store).per_tensor(ScaleDtype::F32);
let n = values.len();
let bytes = quantize(values, &[n], &[1.0], &[n], &s);
dequantize(&bytes, &[n], &[1.0], &[n], &s)
}
#[test]
fn packed_e2m1_holds_the_format_grid() {
let values = [0.0f32, 0.5, 1.0, 1.5, 2.0, 3.0, -0.5, -6.0];
let restored =
round_trip_at_unit_scale(QuantValue::E2M1, QuantStore::PackedU32(0), &values);
assert_eq!(
restored, values,
"an e2m1 code point did not come back as itself"
);
}
#[test]
fn packed_e4m3_holds_fractions_and_large_magnitudes() {
let values = [0.5f32, 1.5, -0.5, 0.0625, 288.0, -288.0, 256.0, 144.0];
let restored =
round_trip_at_unit_scale(QuantValue::E4M3, QuantStore::PackedU32(0), &values);
assert_eq!(
restored, values,
"an e4m3 code point did not come back as itself"
);
}
#[test]
fn block_level_uses_per_block_scale() {
let s = scheme(QuantValue::Q8S, QuantStore::Native).per_block([4u8], ScaleDtype::F32);
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);
}
}
}