use std::ffi::CStr;
use mlx_internal_macros::generate_macro;
use crate::{
error::Result,
utils::{guard::Guarded, VectorArray},
Array, Stream,
};
const DEFAULT_MODE: &CStr = c"affine";
const DEFAULT_GROUP_SIZE: i32 = 64;
const DEFAULT_BITS: i32 = 4;
fn optional_int(value: Option<i32>, default: i32) -> mlx_sys::mlx_optional_int {
mlx_sys::mlx_optional_int {
value: value.unwrap_or(default),
has_value: value.is_some(),
}
}
fn optional_dtype_none() -> mlx_sys::mlx_optional_dtype {
mlx_sys::mlx_optional_dtype {
value: mlx_sys::mlx_dtype__MLX_FLOAT32, has_value: false,
}
}
pub fn quantize(
w: impl AsRef<Array>,
group_size: impl Into<Option<i32>>,
bits: impl Into<Option<i32>>,
) -> Result<(Array, Array, Array)> {
let stream = Stream::thread_local_or_default();
let group_size = optional_int(group_size.into(), DEFAULT_GROUP_SIZE);
let bits = optional_int(bits.into(), DEFAULT_BITS);
let global_scale = unsafe { Array::from_ptr(mlx_sys::mlx_array_new()) };
let result = VectorArray::try_from_op(|res| unsafe {
mlx_sys::mlx_quantize(
res,
w.as_ref().as_ptr(),
group_size,
bits,
DEFAULT_MODE.as_ptr(),
global_scale.as_ptr(),
stream.as_ref().as_ptr(),
)
})?;
let arrays: Vec<Array> = result.try_into_values()?;
if arrays.len() != 3 {
return Err(crate::error::Exception::custom(format!(
"Expected 3 arrays from quantize, got {}",
arrays.len()
)));
}
let mut iter = arrays.into_iter();
Ok((
iter.next().unwrap(),
iter.next().unwrap(),
iter.next().unwrap(),
))
}
#[generate_macro(customize(forwarding_shim = true))]
#[deprecated(
since = "0.26.0",
note = "use `with_stream` or `with_device` around `quantize`"
)]
pub fn quantize_device(
w: impl AsRef<Array>,
#[optional] group_size: impl Into<Option<i32>>,
#[optional] bits: impl Into<Option<i32>>,
#[optional] stream: impl AsRef<Stream>,
) -> Result<(Array, Array, Array)> {
crate::with_stream(stream.as_ref(), || quantize(w, group_size, bits))
}
#[allow(clippy::too_many_arguments)]
pub fn quantized_matmul<'a>(
x: impl AsRef<Array>,
w: impl AsRef<Array>,
scales: impl AsRef<Array>,
biases: impl Into<Option<&'a Array>>,
transpose: impl Into<Option<bool>>,
group_size: impl Into<Option<i32>>,
bits: impl Into<Option<i32>>,
) -> Result<Array> {
let stream = Stream::thread_local_or_default();
let transpose = transpose.into().unwrap_or(false);
let group_size = optional_int(group_size.into(), DEFAULT_GROUP_SIZE);
let bits = optional_int(bits.into(), DEFAULT_BITS);
<Array as Guarded>::try_from_op(|res| unsafe {
mlx_sys::mlx_quantized_matmul(
res,
x.as_ref().as_ptr(),
w.as_ref().as_ptr(),
scales.as_ref().as_ptr(),
biases
.into()
.map(|a| a.as_ptr())
.unwrap_or(mlx_sys::mlx_array_new()),
transpose,
group_size,
bits,
DEFAULT_MODE.as_ptr(),
stream.as_ref().as_ptr(),
)
})
}
#[allow(clippy::too_many_arguments)]
#[generate_macro(customize(forwarding_shim = true))]
#[deprecated(
since = "0.26.0",
note = "use `with_stream` or `with_device` around `quantized_matmul`"
)]
pub fn quantized_matmul_device<'a>(
x: impl AsRef<Array>,
w: impl AsRef<Array>,
scales: impl AsRef<Array>,
#[optional] biases: impl Into<Option<&'a Array>>,
#[optional] transpose: impl Into<Option<bool>>,
#[optional] group_size: impl Into<Option<i32>>,
#[optional] bits: impl Into<Option<i32>>,
#[optional] stream: impl AsRef<Stream>,
) -> Result<Array> {
crate::with_stream(stream.as_ref(), || {
quantized_matmul(x, w, scales, biases, transpose, group_size, bits)
})
}
pub fn dequantize<'a>(
w: impl AsRef<Array>,
scales: impl AsRef<Array>,
biases: impl Into<Option<&'a Array>>,
group_size: impl Into<Option<i32>>,
bits: impl Into<Option<i32>>,
) -> Result<Array> {
let stream = Stream::thread_local_or_default();
let group_size = optional_int(group_size.into(), DEFAULT_GROUP_SIZE);
let bits = optional_int(bits.into(), DEFAULT_BITS);
let global_scale = unsafe { Array::from_ptr(mlx_sys::mlx_array_new()) };
<Array as Guarded>::try_from_op(|res| unsafe {
mlx_sys::mlx_dequantize(
res,
w.as_ref().as_ptr(),
scales.as_ref().as_ptr(),
biases
.into()
.map(|a| a.as_ptr())
.unwrap_or(mlx_sys::mlx_array_new()),
group_size,
bits,
DEFAULT_MODE.as_ptr(),
global_scale.as_ptr(),
optional_dtype_none(),
stream.as_ref().as_ptr(),
)
})
}
#[generate_macro(customize(forwarding_shim = true))]
#[deprecated(
since = "0.26.0",
note = "use `with_stream` or `with_device` around `dequantize`"
)]
pub fn dequantize_device<'a>(
w: impl AsRef<Array>,
scales: impl AsRef<Array>,
#[optional] biases: impl Into<Option<&'a Array>>,
#[optional] group_size: impl Into<Option<i32>>,
#[optional] bits: impl Into<Option<i32>>,
#[optional] stream: impl AsRef<Stream>,
) -> Result<Array> {
crate::with_stream(stream.as_ref(), || {
dequantize(w, scales, biases, group_size, bits)
})
}
#[allow(clippy::too_many_arguments)]
pub fn gather_qmm<'b, 'lhs, 'rhs>(
x: impl AsRef<Array>,
w: impl AsRef<Array>,
scales: impl AsRef<Array>,
biases: impl Into<Option<&'b Array>>,
lhs_indices: impl Into<Option<&'lhs Array>>,
rhs_indices: impl Into<Option<&'rhs Array>>,
transpose: impl Into<Option<bool>>,
group_size: impl Into<Option<i32>>,
bits: impl Into<Option<i32>>,
sorted_indices: impl Into<Option<bool>>,
) -> Result<Array> {
let stream = Stream::thread_local_or_default();
let transpose = transpose.into().unwrap_or(true);
let group_size = optional_int(group_size.into(), DEFAULT_GROUP_SIZE);
let bits = optional_int(bits.into(), DEFAULT_BITS);
let sorted = sorted_indices.into().unwrap_or(false);
unsafe {
let biases_ptr = biases
.into()
.map(|a| a.as_ptr())
.unwrap_or(mlx_sys::mlx_array_new());
let lhs_ptr = lhs_indices
.into()
.map(|i| i.as_ptr())
.unwrap_or(mlx_sys::mlx_array_new());
let rhs_ptr = rhs_indices
.into()
.map(|i| i.as_ptr())
.unwrap_or(mlx_sys::mlx_array_new());
<Array as Guarded>::try_from_op(|res| {
mlx_sys::mlx_gather_qmm(
res,
x.as_ref().as_ptr(),
w.as_ref().as_ptr(),
scales.as_ref().as_ptr(),
biases_ptr,
lhs_ptr,
rhs_ptr,
transpose,
group_size,
bits,
DEFAULT_MODE.as_ptr(),
sorted,
stream.as_ref().as_ptr(),
)
})
}
}
#[allow(clippy::too_many_arguments)]
#[generate_macro(customize(forwarding_shim = true))]
#[deprecated(
since = "0.26.0",
note = "use `with_stream` or `with_device` around `gather_qmm`"
)]
pub fn gather_qmm_device<'b, 'lhs, 'rhs>(
x: impl AsRef<Array>,
w: impl AsRef<Array>,
scales: impl AsRef<Array>,
#[optional] biases: impl Into<Option<&'b Array>>,
#[optional] lhs_indices: impl Into<Option<&'lhs Array>>,
#[optional] rhs_indices: impl Into<Option<&'rhs Array>>,
#[optional] transpose: impl Into<Option<bool>>,
#[optional] group_size: impl Into<Option<i32>>,
#[optional] bits: impl Into<Option<i32>>,
#[optional] sorted_indices: impl Into<Option<bool>>,
#[optional] stream: impl AsRef<Stream>,
) -> Result<Array> {
crate::with_stream(stream.as_ref(), || {
gather_qmm(
x,
w,
scales,
biases,
lhs_indices,
rhs_indices,
transpose,
group_size,
bits,
sorted_indices,
)
})
}
#[cfg(not(target_os = "macos"))]
#[allow(clippy::too_many_arguments)]
pub fn qqmm<'a>(
x: impl AsRef<Array>,
w: impl AsRef<Array>,
w_scales: impl Into<Option<&'a Array>>,
group_size: impl Into<Option<i32>>,
bits: impl Into<Option<i32>>,
mode: impl Into<Option<&'a str>>,
) -> Result<Array> {
let stream = Stream::thread_local_or_default();
let mode_str = mode.into().unwrap_or("nvfp4");
let mode_cstr = std::ffi::CString::new(mode_str).expect("Invalid mode string");
let (default_group_size, default_bits) = match mode_str {
"nvfp4" => (16, 4),
"mxfp8" => (32, 8),
_ => (16, 4), };
let group_size = optional_int(group_size.into(), default_group_size);
let bits = optional_int(bits.into(), default_bits);
let global_scale_x = unsafe { Array::from_ptr(mlx_sys::mlx_array_new()) };
let global_scale_w = unsafe { Array::from_ptr(mlx_sys::mlx_array_new()) };
<Array as Guarded>::try_from_op(|res| unsafe {
mlx_sys::mlx_qqmm(
res,
x.as_ref().as_ptr(),
w.as_ref().as_ptr(),
w_scales
.into()
.map(|a| a.as_ptr())
.unwrap_or(mlx_sys::mlx_array_new()),
group_size,
bits,
mode_cstr.as_ptr(),
global_scale_x.as_ptr(),
global_scale_w.as_ptr(),
stream.as_ref().as_ptr(),
)
})
}
#[cfg(not(target_os = "macos"))]
#[allow(clippy::too_many_arguments)]
#[generate_macro(customize(forwarding_shim = true))]
#[deprecated(
since = "0.26.0",
note = "use `with_stream` or `with_device` around `qqmm`"
)]
pub fn qqmm_device<'a>(
x: impl AsRef<Array>,
w: impl AsRef<Array>,
#[optional] w_scales: impl Into<Option<&'a Array>>,
#[optional] group_size: impl Into<Option<i32>>,
#[optional] bits: impl Into<Option<i32>>,
#[optional] mode: impl Into<Option<&'a str>>,
#[optional] stream: impl AsRef<Stream>,
) -> Result<Array> {
crate::with_stream(stream.as_ref(), || {
qqmm(x, w, w_scales, group_size, bits, mode)
})
}
#[cfg(test)]
mod tests {
use crate::{
ops::{dequantize, expand_dims, quantize, quantized_matmul},
random, Array,
};
#[test]
fn test_quantize_dequantize() {
let x1 = Array::ones::<f32>(&[128, 1]).unwrap();
let x2 = expand_dims(Array::arange::<_, f32>(0, 512, None).unwrap(), 0).unwrap();
let x = x1 * x2;
for i in [2, 4, 8].iter() {
let el_per_int = 32 / i;
let (x_q, scales, biases) = quantize(&x, 128, *i).unwrap();
assert_eq!(x_q.shape(), [128, 512 / el_per_int]);
assert_eq!(scales.shape(), [128, 4]);
assert_eq!(biases.shape(), [128, 4]);
let x_hat = dequantize(&x_q, &scales, &biases, 128, *i).unwrap();
let max_diff = ((&x - &x_hat).abs().unwrap().max(None).unwrap()).item_exact::<f32>();
assert!(max_diff <= 127.0 / (1 << i) as f32);
}
}
#[test]
fn test_quantized_matmul() {
random::seed(0).unwrap();
let group_size = 64;
let bits = 4;
let m = 32;
let n = 128;
let k = 128;
let scale = 1.0 / (k as f32).sqrt();
let x = random::normal::<f32>(&[m, k], None, None, None).unwrap() * scale;
let w = random::normal::<f32>(&[k, n], None, None, None).unwrap() * scale;
let (w_q, scales, biases) = quantize(&w, group_size, bits).unwrap();
let w_hat = dequantize(&w_q, &scales, &biases, group_size, bits).unwrap();
let y_q = quantized_matmul(&x, &w_q, &scales, &biases, false, group_size, bits).unwrap();
let y_hat = x.matmul(&w_hat).unwrap();
assert_eq!(y_q.shape(), y_hat.shape());
let max_diff = ((&y_q - &y_hat).abs().unwrap().max(None).unwrap()).item_exact::<f32>();
assert!(max_diff < 1e-3, "max_diff: {}", max_diff);
}
#[test]
fn test_gather_qmm() {
use crate::ops::{gather_mm, gather_qmm, swap_axes};
random::seed(0).unwrap();
let group_size = 64;
let bits = 4;
fn quantize_with_transpose(
w: &Array,
transpose: bool,
group_size: i32,
bits: i32,
) -> (Array, Array, Array, Array) {
let (w_q, scales, biases) = quantize(w, group_size, bits).unwrap();
let mut w_hat = dequantize(&w_q, &scales, &biases, group_size, bits).unwrap();
if transpose {
w_hat = swap_axes(&w_hat, -1, -2).unwrap();
}
(w_hat, w_q, scales, biases)
}
let m = 32;
let n = 64;
let k = 64;
let x = random::normal::<f32>(&[1, m, k], None, None, None).unwrap();
let w = random::normal::<f32>(&[3, n, k], None, None, None).unwrap(); let (w_hat, w_q, scales, biases) = quantize_with_transpose(&w, true, group_size, bits);
let lhs_indices = Array::from_slice(&[0u32], &[1]);
let rhs_indices = Array::from_slice(&[2u32, 1], &[2]);
let c1 = gather_mm(&x, &w_hat, &lhs_indices, &rhs_indices, None).unwrap();
let c2 = gather_qmm(
&x,
&w_q,
&scales,
&biases,
&lhs_indices,
&rhs_indices,
true,
group_size,
bits,
None,
)
.unwrap();
assert!(
c1.all_close(&c2, 1e-4, 1e-4, None).unwrap(),
"gather_qmm test case 1 failed"
);
let x = random::normal::<f32>(&[5, m, k], None, None, None).unwrap();
let lhs_indices = Array::from_slice(&[0u32, 2], &[2]);
let c1 = gather_mm(&x, &w_hat, &lhs_indices, &rhs_indices, None).unwrap();
let c2 = gather_qmm(
&x,
&w_q,
&scales,
&biases,
&lhs_indices,
&rhs_indices,
true,
group_size,
bits,
None,
)
.unwrap();
assert!(
c1.all_close(&c2, 1e-4, 1e-4, None).unwrap(),
"gather_qmm test case 2 failed"
);
}
}