1use std::ffi::CStr;
2
3use mlx_internal_macros::generate_macro;
4
5use crate::{
6 error::Result,
7 utils::{guard::Guarded, VectorArray},
8 Array, Stream,
9};
10
11const DEFAULT_MODE: &CStr = c"affine";
12const DEFAULT_GROUP_SIZE: i32 = 64;
13const DEFAULT_BITS: i32 = 4;
14
15fn optional_int(value: Option<i32>, default: i32) -> mlx_sys::mlx_optional_int {
17 mlx_sys::mlx_optional_int {
18 value: value.unwrap_or(default),
19 has_value: value.is_some(),
20 }
21}
22
23fn optional_dtype_none() -> mlx_sys::mlx_optional_dtype {
25 mlx_sys::mlx_optional_dtype {
26 value: mlx_sys::mlx_dtype__MLX_FLOAT32, has_value: false,
28 }
29}
30
31pub fn quantize(
49 w: impl AsRef<Array>,
50 group_size: impl Into<Option<i32>>,
51 bits: impl Into<Option<i32>>,
52) -> Result<(Array, Array, Array)> {
53 let stream = Stream::thread_local_or_default();
54 let group_size = optional_int(group_size.into(), DEFAULT_GROUP_SIZE);
55 let bits = optional_int(bits.into(), DEFAULT_BITS);
56 let global_scale = unsafe { Array::from_ptr(mlx_sys::mlx_array_new()) };
57
58 let result = VectorArray::try_from_op(|res| unsafe {
59 mlx_sys::mlx_quantize(
60 res,
61 w.as_ref().as_ptr(),
62 group_size,
63 bits,
64 DEFAULT_MODE.as_ptr(),
65 global_scale.as_ptr(),
66 stream.as_ref().as_ptr(),
67 )
68 })?;
69
70 let arrays: Vec<Array> = result.try_into_values()?;
71 if arrays.len() != 3 {
72 return Err(crate::error::Exception::custom(format!(
73 "Expected 3 arrays from quantize, got {}",
74 arrays.len()
75 )));
76 }
77 let mut iter = arrays.into_iter();
78 Ok((
79 iter.next().unwrap(),
80 iter.next().unwrap(),
81 iter.next().unwrap(),
82 ))
83}
84
85#[generate_macro(customize(forwarding_shim = true))]
87#[deprecated(
88 since = "0.26.0",
89 note = "use `with_stream` or `with_device` around `quantize`"
90)]
91pub fn quantize_device(
92 w: impl AsRef<Array>,
93 #[optional] group_size: impl Into<Option<i32>>,
94 #[optional] bits: impl Into<Option<i32>>,
95 #[optional] stream: impl AsRef<Stream>,
96) -> Result<(Array, Array, Array)> {
97 crate::with_stream(stream.as_ref(), || quantize(w, group_size, bits))
98}
99
100#[allow(clippy::too_many_arguments)]
104pub fn quantized_matmul<'a>(
105 x: impl AsRef<Array>,
106 w: impl AsRef<Array>,
107 scales: impl AsRef<Array>,
108 biases: impl Into<Option<&'a Array>>,
109 transpose: impl Into<Option<bool>>,
110 group_size: impl Into<Option<i32>>,
111 bits: impl Into<Option<i32>>,
112) -> Result<Array> {
113 let stream = Stream::thread_local_or_default();
114 let transpose = transpose.into().unwrap_or(false);
115 let group_size = optional_int(group_size.into(), DEFAULT_GROUP_SIZE);
116 let bits = optional_int(bits.into(), DEFAULT_BITS);
117
118 <Array as Guarded>::try_from_op(|res| unsafe {
119 mlx_sys::mlx_quantized_matmul(
120 res,
121 x.as_ref().as_ptr(),
122 w.as_ref().as_ptr(),
123 scales.as_ref().as_ptr(),
124 biases
125 .into()
126 .map(|a| a.as_ptr())
127 .unwrap_or(mlx_sys::mlx_array_new()),
128 transpose,
129 group_size,
130 bits,
131 DEFAULT_MODE.as_ptr(),
132 stream.as_ref().as_ptr(),
133 )
134 })
135}
136
137#[allow(clippy::too_many_arguments)]
139#[generate_macro(customize(forwarding_shim = true))]
140#[deprecated(
141 since = "0.26.0",
142 note = "use `with_stream` or `with_device` around `quantized_matmul`"
143)]
144pub fn quantized_matmul_device<'a>(
145 x: impl AsRef<Array>,
146 w: impl AsRef<Array>,
147 scales: impl AsRef<Array>,
148 #[optional] biases: impl Into<Option<&'a Array>>,
149 #[optional] transpose: impl Into<Option<bool>>,
150 #[optional] group_size: impl Into<Option<i32>>,
151 #[optional] bits: impl Into<Option<i32>>,
152 #[optional] stream: impl AsRef<Stream>,
153) -> Result<Array> {
154 crate::with_stream(stream.as_ref(), || {
155 quantized_matmul(x, w, scales, biases, transpose, group_size, bits)
156 })
157}
158
159pub fn dequantize<'a>(
165 w: impl AsRef<Array>,
166 scales: impl AsRef<Array>,
167 biases: impl Into<Option<&'a Array>>,
168 group_size: impl Into<Option<i32>>,
169 bits: impl Into<Option<i32>>,
170) -> Result<Array> {
171 let stream = Stream::thread_local_or_default();
172 let group_size = optional_int(group_size.into(), DEFAULT_GROUP_SIZE);
173 let bits = optional_int(bits.into(), DEFAULT_BITS);
174 let global_scale = unsafe { Array::from_ptr(mlx_sys::mlx_array_new()) };
175
176 <Array as Guarded>::try_from_op(|res| unsafe {
177 mlx_sys::mlx_dequantize(
178 res,
179 w.as_ref().as_ptr(),
180 scales.as_ref().as_ptr(),
181 biases
182 .into()
183 .map(|a| a.as_ptr())
184 .unwrap_or(mlx_sys::mlx_array_new()),
185 group_size,
186 bits,
187 DEFAULT_MODE.as_ptr(),
188 global_scale.as_ptr(),
189 optional_dtype_none(),
190 stream.as_ref().as_ptr(),
191 )
192 })
193}
194
195#[generate_macro(customize(forwarding_shim = true))]
197#[deprecated(
198 since = "0.26.0",
199 note = "use `with_stream` or `with_device` around `dequantize`"
200)]
201pub fn dequantize_device<'a>(
202 w: impl AsRef<Array>,
203 scales: impl AsRef<Array>,
204 #[optional] biases: impl Into<Option<&'a Array>>,
205 #[optional] group_size: impl Into<Option<i32>>,
206 #[optional] bits: impl Into<Option<i32>>,
207 #[optional] stream: impl AsRef<Stream>,
208) -> Result<Array> {
209 crate::with_stream(stream.as_ref(), || {
210 dequantize(w, scales, biases, group_size, bits)
211 })
212}
213
214#[allow(clippy::too_many_arguments)]
232pub fn gather_qmm<'b, 'lhs, 'rhs>(
233 x: impl AsRef<Array>,
234 w: impl AsRef<Array>,
235 scales: impl AsRef<Array>,
236 biases: impl Into<Option<&'b Array>>,
237 lhs_indices: impl Into<Option<&'lhs Array>>,
238 rhs_indices: impl Into<Option<&'rhs Array>>,
239 transpose: impl Into<Option<bool>>,
240 group_size: impl Into<Option<i32>>,
241 bits: impl Into<Option<i32>>,
242 sorted_indices: impl Into<Option<bool>>,
243) -> Result<Array> {
244 let stream = Stream::thread_local_or_default();
245 let transpose = transpose.into().unwrap_or(true);
246 let group_size = optional_int(group_size.into(), DEFAULT_GROUP_SIZE);
247 let bits = optional_int(bits.into(), DEFAULT_BITS);
248 let sorted = sorted_indices.into().unwrap_or(false);
249
250 unsafe {
251 let biases_ptr = biases
252 .into()
253 .map(|a| a.as_ptr())
254 .unwrap_or(mlx_sys::mlx_array_new());
255 let lhs_ptr = lhs_indices
256 .into()
257 .map(|i| i.as_ptr())
258 .unwrap_or(mlx_sys::mlx_array_new());
259 let rhs_ptr = rhs_indices
260 .into()
261 .map(|i| i.as_ptr())
262 .unwrap_or(mlx_sys::mlx_array_new());
263
264 <Array as Guarded>::try_from_op(|res| {
265 mlx_sys::mlx_gather_qmm(
266 res,
267 x.as_ref().as_ptr(),
268 w.as_ref().as_ptr(),
269 scales.as_ref().as_ptr(),
270 biases_ptr,
271 lhs_ptr,
272 rhs_ptr,
273 transpose,
274 group_size,
275 bits,
276 DEFAULT_MODE.as_ptr(),
277 sorted,
278 stream.as_ref().as_ptr(),
279 )
280 })
281 }
282}
283
284#[allow(clippy::too_many_arguments)]
286#[generate_macro(customize(forwarding_shim = true))]
287#[deprecated(
288 since = "0.26.0",
289 note = "use `with_stream` or `with_device` around `gather_qmm`"
290)]
291pub fn gather_qmm_device<'b, 'lhs, 'rhs>(
292 x: impl AsRef<Array>,
293 w: impl AsRef<Array>,
294 scales: impl AsRef<Array>,
295 #[optional] biases: impl Into<Option<&'b Array>>,
296 #[optional] lhs_indices: impl Into<Option<&'lhs Array>>,
297 #[optional] rhs_indices: impl Into<Option<&'rhs Array>>,
298 #[optional] transpose: impl Into<Option<bool>>,
299 #[optional] group_size: impl Into<Option<i32>>,
300 #[optional] bits: impl Into<Option<i32>>,
301 #[optional] sorted_indices: impl Into<Option<bool>>,
302 #[optional] stream: impl AsRef<Stream>,
303) -> Result<Array> {
304 crate::with_stream(stream.as_ref(), || {
305 gather_qmm(
306 x,
307 w,
308 scales,
309 biases,
310 lhs_indices,
311 rhs_indices,
312 transpose,
313 group_size,
314 bits,
315 sorted_indices,
316 )
317 })
318}
319
320#[cfg(not(target_os = "macos"))]
337#[allow(clippy::too_many_arguments)]
338pub fn qqmm<'a>(
339 x: impl AsRef<Array>,
340 w: impl AsRef<Array>,
341 w_scales: impl Into<Option<&'a Array>>,
342 group_size: impl Into<Option<i32>>,
343 bits: impl Into<Option<i32>>,
344 mode: impl Into<Option<&'a str>>,
345) -> Result<Array> {
346 let stream = Stream::thread_local_or_default();
347 let mode_str = mode.into().unwrap_or("nvfp4");
348 let mode_cstr = std::ffi::CString::new(mode_str).expect("Invalid mode string");
349
350 let (default_group_size, default_bits) = match mode_str {
352 "nvfp4" => (16, 4),
353 "mxfp8" => (32, 8),
354 _ => (16, 4), };
356
357 let group_size = optional_int(group_size.into(), default_group_size);
358 let bits = optional_int(bits.into(), default_bits);
359 let global_scale_x = unsafe { Array::from_ptr(mlx_sys::mlx_array_new()) };
360 let global_scale_w = unsafe { Array::from_ptr(mlx_sys::mlx_array_new()) };
361
362 <Array as Guarded>::try_from_op(|res| unsafe {
363 mlx_sys::mlx_qqmm(
364 res,
365 x.as_ref().as_ptr(),
366 w.as_ref().as_ptr(),
367 w_scales
368 .into()
369 .map(|a| a.as_ptr())
370 .unwrap_or(mlx_sys::mlx_array_new()),
371 group_size,
372 bits,
373 mode_cstr.as_ptr(),
374 global_scale_x.as_ptr(),
375 global_scale_w.as_ptr(),
376 stream.as_ref().as_ptr(),
377 )
378 })
379}
380
381#[cfg(not(target_os = "macos"))]
383#[allow(clippy::too_many_arguments)]
384#[generate_macro(customize(forwarding_shim = true))]
385#[deprecated(
386 since = "0.26.0",
387 note = "use `with_stream` or `with_device` around `qqmm`"
388)]
389pub fn qqmm_device<'a>(
390 x: impl AsRef<Array>,
391 w: impl AsRef<Array>,
392 #[optional] w_scales: impl Into<Option<&'a Array>>,
393 #[optional] group_size: impl Into<Option<i32>>,
394 #[optional] bits: impl Into<Option<i32>>,
395 #[optional] mode: impl Into<Option<&'a str>>,
396 #[optional] stream: impl AsRef<Stream>,
397) -> Result<Array> {
398 crate::with_stream(stream.as_ref(), || {
399 qqmm(x, w, w_scales, group_size, bits, mode)
400 })
401}
402
403#[cfg(test)]
404mod tests {
405 use crate::{
406 ops::{dequantize, expand_dims, quantize, quantized_matmul},
407 random, Array,
408 };
409
410 #[test]
411 fn test_quantize_dequantize() {
412 let x1 = Array::ones::<f32>(&[128, 1]).unwrap();
413 let x2 = expand_dims(Array::arange::<_, f32>(0, 512, None).unwrap(), 0).unwrap();
414 let x = x1 * x2;
415
416 for i in [2, 4, 8].iter() {
417 let el_per_int = 32 / i;
418 let (x_q, scales, biases) = quantize(&x, 128, *i).unwrap();
419 assert_eq!(x_q.shape(), [128, 512 / el_per_int]);
420 assert_eq!(scales.shape(), [128, 4]);
421 assert_eq!(biases.shape(), [128, 4]);
422
423 let x_hat = dequantize(&x_q, &scales, &biases, 128, *i).unwrap();
424 let max_diff = ((&x - &x_hat).abs().unwrap().max(None).unwrap()).item_exact::<f32>();
425 assert!(max_diff <= 127.0 / (1 << i) as f32);
426 }
427 }
428
429 #[test]
431 fn test_quantized_matmul() {
432 random::seed(0).unwrap();
433
434 let group_size = 64;
435 let bits = 4;
436 let m = 32;
437 let n = 128;
438 let k = 128;
439
440 let scale = 1.0 / (k as f32).sqrt();
441 let x = random::normal::<f32>(&[m, k], None, None, None).unwrap() * scale;
442 let w = random::normal::<f32>(&[k, n], None, None, None).unwrap() * scale;
443
444 let (w_q, scales, biases) = quantize(&w, group_size, bits).unwrap();
445 let w_hat = dequantize(&w_q, &scales, &biases, group_size, bits).unwrap();
446
447 let y_q = quantized_matmul(&x, &w_q, &scales, &biases, false, group_size, bits).unwrap();
449 let y_hat = x.matmul(&w_hat).unwrap();
450
451 assert_eq!(y_q.shape(), y_hat.shape());
452 let max_diff = ((&y_q - &y_hat).abs().unwrap().max(None).unwrap()).item_exact::<f32>();
453 assert!(max_diff < 1e-3, "max_diff: {}", max_diff);
454 }
455
456 #[test]
458 fn test_gather_qmm() {
459 use crate::ops::{gather_mm, gather_qmm, swap_axes};
460
461 random::seed(0).unwrap();
462
463 let group_size = 64;
464 let bits = 4;
465
466 fn quantize_with_transpose(
468 w: &Array,
469 transpose: bool,
470 group_size: i32,
471 bits: i32,
472 ) -> (Array, Array, Array, Array) {
473 let (w_q, scales, biases) = quantize(w, group_size, bits).unwrap();
474 let mut w_hat = dequantize(&w_q, &scales, &biases, group_size, bits).unwrap();
475 if transpose {
476 w_hat = swap_axes(&w_hat, -1, -2).unwrap();
477 }
478 (w_hat, w_q, scales, biases)
479 }
480
481 let m = 32;
483 let n = 64;
484 let k = 64;
485
486 let x = random::normal::<f32>(&[1, m, k], None, None, None).unwrap();
487 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);
489
490 let lhs_indices = Array::from_slice(&[0u32], &[1]);
491 let rhs_indices = Array::from_slice(&[2u32, 1], &[2]);
492
493 let c1 = gather_mm(&x, &w_hat, &lhs_indices, &rhs_indices, None).unwrap();
495 let c2 = gather_qmm(
496 &x,
497 &w_q,
498 &scales,
499 &biases,
500 &lhs_indices,
501 &rhs_indices,
502 true,
503 group_size,
504 bits,
505 None,
506 )
507 .unwrap();
508 assert!(
509 c1.all_close(&c2, 1e-4, 1e-4, None).unwrap(),
510 "gather_qmm test case 1 failed"
511 );
512
513 let x = random::normal::<f32>(&[5, m, k], None, None, None).unwrap();
515 let lhs_indices = Array::from_slice(&[0u32, 2], &[2]);
516
517 let c1 = gather_mm(&x, &w_hat, &lhs_indices, &rhs_indices, None).unwrap();
518 let c2 = gather_qmm(
519 &x,
520 &w_q,
521 &scales,
522 &biases,
523 &lhs_indices,
524 &rhs_indices,
525 true,
526 group_size,
527 bits,
528 None,
529 )
530 .unwrap();
531 assert!(
532 c1.all_close(&c2, 1e-4, 1e-4, None).unwrap(),
533 "gather_qmm test case 2 failed"
534 );
535 }
536}