use cubecl_common::quant::scheme::{QuantScheme, ScaleDtype};
use cubecl_core::prelude::Scalar;
pub trait RunWithQuantType {
type Output;
fn execute<Q: Scalar, S: Scalar>(self) -> Self::Output;
}
pub fn check_scale_bindings(scheme: &QuantScheme, bindings: usize) {
let levels = scheme.num_levels();
assert!(
bindings == levels,
"a scheme with {levels} scale level(s) takes as many scale bindings, but {bindings} were provided",
);
check_global_levels(scheme);
}
pub fn check_global_levels(scheme: &QuantScheme) {
if scheme.block_scale().is_some()
&& let Some(tensor) = scheme.tensor_scale()
{
assert!(
tensor == ScaleDtype::F32,
"an global scale binds as f32, but the scheme stores it as {tensor:?}",
);
}
}
pub fn check_table_bindings(scheme: &QuantScheme, table_provided: bool) {
use cubecl_common::quant::scheme::{QuantMode, QuantStore, QuantValue};
match (scheme.mode, table_provided) {
(QuantMode::Lookup, false) => {
panic!(
"{:?} takes a lookup table, but none was provided",
scheme.mode
)
}
(QuantMode::Lookup, true) => {
assert!(
matches!(scheme.store, QuantStore::PackedU32(_)),
"lookup decode is only wired for packed-u32 storage, got {:?}",
scheme.store
);
assert!(
!matches!(
scheme.value,
QuantValue::E5M2 | QuantValue::E4M3 | QuantValue::E2M1
),
"a lookup field is an index, so a minifloat value ({:?}) has nothing to mean; \
use the integer value of the same width",
scheme.value
);
}
(_, true) => {
panic!(
"a lookup table was provided, but {:?} does not take one",
scheme.mode
)
}
(_, false) => {}
}
}
#[cfg(test)]
mod tests {
use super::{check_scale_bindings, check_table_bindings};
use cubecl_common::quant::scheme::{
QuantMode, QuantScheme, QuantStore, QuantValue, ScaleDtype,
};
#[test]
fn a_one_level_scheme_takes_one_binding() {
check_scale_bindings(&QuantScheme::default().per_tensor(ScaleDtype::F32), 1);
check_scale_bindings(&QuantScheme::default().per_block([32], ScaleDtype::F32), 1);
}
#[test]
fn a_two_level_scheme_takes_two_bindings() {
check_scale_bindings(
&QuantScheme::default()
.per_block([32], ScaleDtype::F32)
.per_tensor(ScaleDtype::F32),
2,
);
}
#[test]
#[should_panic(expected = "binds as f32, but")]
fn a_two_level_scheme_storing_the_tensor_scale_narrower_is_rejected() {
check_scale_bindings(
&QuantScheme::default()
.per_block([32], ScaleDtype::F32)
.per_tensor(ScaleDtype::BF16),
2,
);
}
#[test]
#[should_panic(expected = "takes as many scale bindings, but 1 were provided")]
fn a_two_level_scheme_with_one_binding_is_rejected() {
check_scale_bindings(
&QuantScheme::default()
.per_block([32], ScaleDtype::F32)
.per_tensor(ScaleDtype::F32),
1,
);
}
#[test]
#[should_panic(expected = "takes as many scale bindings, but 2 were provided")]
fn a_one_level_scheme_with_two_bindings_is_rejected() {
check_scale_bindings(&QuantScheme::default().per_tensor(ScaleDtype::F32), 2);
}
fn lookup_scheme() -> QuantScheme {
QuantScheme::default()
.with_value(QuantValue::Q4F)
.with_mode(QuantMode::Lookup)
}
#[test]
fn a_lookup_scheme_takes_a_table() {
check_table_bindings(&lookup_scheme(), true);
}
#[test]
fn a_symmetric_scheme_takes_no_table() {
check_table_bindings(&QuantScheme::default(), false);
}
#[test]
#[should_panic(expected = "takes a lookup table, but none was provided")]
fn a_lookup_scheme_without_a_table_is_rejected() {
check_table_bindings(&lookup_scheme(), false);
}
#[test]
#[should_panic(expected = "does not take one")]
fn a_symmetric_scheme_with_a_table_is_rejected() {
check_table_bindings(&QuantScheme::default(), true);
}
#[test]
#[should_panic(expected = "only wired for packed-u32 storage")]
fn a_native_lookup_scheme_is_rejected() {
let scheme = QuantScheme::default()
.with_value(QuantValue::Q8F)
.with_store(QuantStore::Native)
.with_mode(QuantMode::Lookup);
check_table_bindings(&scheme, true);
}
#[test]
#[should_panic(expected = "a lookup field is an index")]
fn a_minifloat_lookup_scheme_is_rejected() {
let scheme = QuantScheme::default()
.with_value(QuantValue::E4M3)
.with_mode(QuantMode::Lookup);
check_table_bindings(&scheme, true);
}
}