use cubecl::{
client::Client,
features::TypeUsage,
prelude::*,
std::tensor::TensorHandle,
{zspace::Shape, zspace::shape},
};
use cubecl_common::e4m3;
use cubek_quant::scheme::{QuantMode, QuantScheme, QuantStore, QuantValue, ScaleDtype};
use super::harness::{dequantize, f32_tensor, quant_outputs, quantize, scale_shape};
const M: usize = 8;
const N: usize = 32;
const BLOCK: usize = 32;
const OUTER: f32 = 1.0 / (127.0 * 448.0);
fn two_level(store: QuantStore, block_dtype: ScaleDtype) -> QuantScheme {
QuantScheme::default()
.per_block([BLOCK as u8], block_dtype)
.per_tensor(ScaleDtype::F32)
.with_value(QuantValue::Q8S)
.with_store(store)
.with_mode(QuantMode::Symmetric)
}
fn one_level(store: QuantStore, block_dtype: ScaleDtype) -> QuantScheme {
QuantScheme::default()
.per_block([BLOCK as u8], block_dtype)
.with_value(QuantValue::Q8S)
.with_store(store)
.with_mode(QuantMode::Symmetric)
}
fn packed_f32() -> QuantScheme {
two_level(QuantStore::PackedU32(0), ScaleDtype::F32)
}
struct Fixture {
client: Client,
shape: Shape,
input: TensorHandle,
scale: TensorHandle,
global: TensorHandle,
data: Vec<f32>,
}
fn fixture(scheme: &QuantScheme) -> Fixture {
let num_blocks = M * N / BLOCK;
let scales: Vec<f32> = (0..num_blocks).map(|b| 1.0 + b as f32 * 64.0).collect();
fixture_with(scheme, OUTER, scales, |i| (i % 9) as f32 - 4.0)
}
fn fixture_with(
scheme: &QuantScheme,
global: f32,
scales: Vec<f32>,
quant_level: impl Fn(usize) -> f32,
) -> Fixture {
let client = cubecl::test_device().client();
let shape = shape![M, N];
let data: Vec<f32> = (0..M * N)
.map(|i| quant_level(i) * global * scales[i / BLOCK])
.collect();
Fixture {
input: f32_tensor(&client, &data, shape.clone()),
scale: f32_tensor(&client, &scales, scale_shape(scheme, &shape)),
global: f32_tensor(&client, &[global], shape![1]),
shape,
data,
client,
}
}
fn round_trip_to_f16(f: &Fixture, scheme: &QuantScheme) -> Vec<half::f16> {
let (values, scales, out_global) = quantize(
&f.client,
scheme,
&f.input,
&f.scale,
Some(&f.global),
&f.shape,
);
let out = dequantize(
&f.client,
scheme,
&values,
&scales,
out_global.as_ref(),
&f.shape,
half::f16::elem_type_native(),
);
let computed = f
.client
.read_one_unchecked_tensor(out.into_copy_descriptor());
half::f16::from_bytes(&computed).to_vec()
}
#[test]
#[should_panic(expected = "takes as many scale bindings, but 1 were provided")]
fn quantize_rejects_a_missing_global_scale() {
let scheme = packed_f32();
let f = fixture(&scheme);
quantize(&f.client, &scheme, &f.input, &f.scale, None, &f.shape);
}
#[test]
#[should_panic(expected = "takes as many scale bindings, but 2 were provided")]
fn quantize_rejects_an_unexpected_global_scale() {
let scheme = one_level(QuantStore::PackedU32(0), ScaleDtype::F32);
let f = fixture(&scheme);
quantize(
&f.client,
&scheme,
&f.input,
&f.scale,
Some(&f.global),
&f.shape,
);
}
#[test]
#[should_panic(expected = "takes as many scale bindings, but 1 were provided")]
fn dequantize_rejects_a_missing_global_scale() {
let scheme = packed_f32();
let f = fixture(&scheme);
let (values, scales) = quant_outputs(&f.client, &scheme, &f.shape);
dequantize(
&f.client,
&scheme,
&values,
&scales,
None,
&f.shape,
f32::elem_type_native(),
);
}
#[test]
fn the_global_scale_is_read_as_f32_not_as_the_compute_type() {
let scheme = packed_f32();
let f = fixture(&scheme);
let restored = round_trip_to_f16(&f, &scheme);
assert_eq!(restored.len(), f.data.len());
for (i, (&actual, &expected)) in restored.iter().zip(f.data.iter()).enumerate() {
let actual = actual.to_f32();
assert!(
(actual - expected).abs() <= 1e-2,
"index {i}: got {actual}, expected {expected}"
);
}
}
#[test]
fn an_effective_scale_below_f16_still_scales_an_f16_output() {
let scheme = packed_f32();
let global = 1e-6;
let block = 0.01;
let effective = global * block;
let num_blocks = M * N / BLOCK;
let f = fixture_with(&scheme, global, vec![block; num_blocks], |i| {
(i % 255) as f32 - 127.0
});
let restored = round_trip_to_f16(&f, &scheme);
let ulp = half::f16::from_bits(1).to_f32();
assert_eq!(restored.len(), f.data.len());
assert!(
restored.iter().any(|v| v.to_f32() != 0.0),
"every value came back zero, the effective scale {effective} was flushed"
);
for (i, (&actual, &expected)) in restored.iter().zip(f.data.iter()).enumerate() {
let actual = actual.to_f32();
assert!(
(actual - expected).abs() <= ulp,
"index {i}: got {actual}, expected {expected} within {ulp}"
);
}
}
#[test]
fn ue4m3_block_scales_with_an_f32_global_scale_round_trip() {
const EXACT: [f32; 4] = [1.0, 1.5, 2.0, 3.0];
let client = cubecl::test_device().client();
if !i8::supported_uses(&client).contains(TypeUsage::Conversion)
|| !e4m3::supported_uses(&client).contains(TypeUsage::Conversion)
{
println!("i8 or e4m3 unsupported on this runtime, nothing checked");
return;
}
let scheme = two_level(QuantStore::Native, ScaleDtype::UE4M3);
let num_blocks = M * N / BLOCK;
let scales: Vec<f32> = (0..num_blocks).map(|b| EXACT[b % EXACT.len()]).collect();
let f = fixture_with(&scheme, 0.01, scales.clone(), |i| (i % BLOCK) as f32 - 15.0);
let (values, stored, out_global) = quantize(
&f.client,
&scheme,
&f.input,
&f.scale,
Some(&f.global),
&f.shape,
);
let out = dequantize(
&f.client,
&scheme,
&values,
&stored,
out_global.as_ref(),
&f.shape,
f32::elem_type_native(),
);
let bytes = f
.client
.read_one_unchecked_tensor(stored.into_copy_descriptor());
for (block, &expected) in scales.iter().enumerate() {
let got = e4m3::from_bits(bytes[block]).to_f32();
assert_eq!(
got, expected,
"block {block} scale stored as {got}, expected {expected}"
);
}
let written = f
.client
.read_one_unchecked_tensor(f.global.clone().into_copy_descriptor());
assert_eq!(f32::from_bytes(&written)[0], 0.01);
let computed = f
.client
.read_one_unchecked_tensor(out.into_copy_descriptor());
let restored = f32::from_bytes(&computed);
assert_eq!(restored.len(), f.data.len());
for (i, (&actual, &expected)) in restored.iter().zip(f.data.iter()).enumerate() {
let tolerance = expected.abs() * 1e-5;
assert!(
(actual - expected).abs() <= tolerance,
"index {i}: got {actual}, expected {expected}"
);
}
}