1use std::fmt;
15use std::str::FromStr;
16
17use crate::ops::imp::{astype, broadcast_to, expand_dims, multiply, reshape, squeeze};
18use crate::primitive::Primitive;
19use crate::{Array, Category, DType, Error, Result, promote_types, view};
20
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
23#[non_exhaustive]
24pub enum QuantMode {
25 #[default]
27 Affine,
28 Mxfp4,
30 Mxfp8,
32 Nvfp4,
34}
35
36impl QuantMode {
37 pub const fn name(self) -> &'static str {
39 match self {
40 Self::Affine => "affine",
41 Self::Mxfp4 => "mxfp4",
42 Self::Mxfp8 => "mxfp8",
43 Self::Nvfp4 => "nvfp4",
44 }
45 }
46
47 const fn defaults(self) -> (i32, i32) {
49 match self {
50 Self::Affine => (64, 4),
51 Self::Mxfp4 => (32, 4),
52 Self::Mxfp8 => (32, 8),
53 Self::Nvfp4 => (16, 4),
54 }
55 }
56}
57
58impl fmt::Display for QuantMode {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.write_str(self.name())
61 }
62}
63
64impl FromStr for QuantMode {
65 type Err = Error;
66
67 fn from_str(s: &str) -> Result<Self> {
68 Ok(match s {
69 "affine" => Self::Affine,
70 "mxfp4" => Self::Mxfp4,
71 "mxfp8" => Self::Mxfp8,
72 "nvfp4" => Self::Nvfp4,
73 _ => {
74 return Err(Error::InvalidArgument(format!(
75 "invalid quantization mode '{s}' (affine, mxfp4, mxfp8 or nvfp4)"
76 )));
77 }
78 })
79 }
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
94pub struct Quant {
95 pub mode: QuantMode,
97 pub group_size: i32,
99 pub bits: i32,
101}
102
103impl Quant {
104 pub const fn new(mode: QuantMode) -> Self {
107 let (group_size, bits) = mode.defaults();
108 Self {
109 mode,
110 group_size,
111 bits,
112 }
113 }
114
115 pub const fn affine(group_size: i32, bits: i32) -> Self {
117 Self {
118 mode: QuantMode::Affine,
119 group_size,
120 bits,
121 }
122 }
123
124 #[must_use]
126 pub const fn group_size(self, group_size: i32) -> Self {
127 Self { group_size, ..self }
128 }
129
130 #[must_use]
132 pub const fn bits(self, bits: i32) -> Self {
133 Self { bits, ..self }
134 }
135
136 pub(crate) fn checked(self, op: &str) -> Result<(usize, usize)> {
138 let invalid = |msg: String| Err(Error::InvalidArgument(format!("{op}: {msg}")));
139 let (group_size, bits) = (self.group_size, self.bits);
140 if self.mode == QuantMode::Affine {
141 if !matches!(group_size, 32 | 64 | 128) {
142 return invalid(format!(
143 "group size {group_size} is not supported (32, 64 or 128)"
144 ));
145 }
146 if !matches!(bits, 1..=6 | 8) {
147 return invalid(format!(
148 "{bits} bits are not supported (1, 2, 3, 4, 5, 6 or 8)"
149 ));
150 }
151 } else {
152 let (expected_group, expected_bits) = self.mode.defaults();
153 if group_size != expected_group {
154 return invalid(format!(
155 "{} quantization requires group size {expected_group} but got {group_size}",
156 self.mode
157 ));
158 }
159 if bits != expected_bits {
160 return invalid(format!(
161 "{} quantization requires bits to be {expected_bits} but got {bits}",
162 self.mode
163 ));
164 }
165 }
166 Ok((group_size as usize, bits as usize))
167 }
168}
169
170impl Default for Quant {
171 fn default() -> Self {
173 Self::new(QuantMode::Affine)
174 }
175}
176
177impl From<QuantMode> for Quant {
178 fn from(mode: QuantMode) -> Self {
179 Self::new(mode)
180 }
181}
182
183#[derive(Clone, Debug)]
195pub struct Quantized {
196 pub weight: Array,
198 pub scales: Array,
200 pub biases: Option<Array>,
202 pub quant: Quant,
204}
205
206impl Quantized {
207 pub fn new(
209 weight: Array,
210 scales: Array,
211 biases: Option<Array>,
212 quant: impl Into<Quant>,
213 ) -> Self {
214 Self {
215 weight,
216 scales,
217 biases,
218 quant: quant.into(),
219 }
220 }
221
222 #[track_caller]
225 pub fn dequantize(&self, dtype: impl Into<Option<DType>>) -> Array {
226 crate::ops::dequantize(self, dtype)
227 }
228
229 pub fn check(&self) -> Result<()> {
231 for a in self.arrays() {
232 let _ = a.clone().check()?;
233 }
234 Ok(())
235 }
236
237 pub(crate) fn arrays(&self) -> impl Iterator<Item = &Array> {
239 [&self.weight, &self.scales].into_iter().chain(&self.biases)
240 }
241}
242
243impl crate::tree::Static for QuantMode {
244 fn hash_static(&self, mut h: &mut dyn std::hash::Hasher) {
245 std::hash::Hash::hash(self, &mut h);
246 }
247}
248
249impl crate::tree::Static for Quant {
250 fn hash_static(&self, mut h: &mut dyn std::hash::Hasher) {
251 std::hash::Hash::hash(self, &mut h);
252 }
253}
254
255impl crate::tree::Leaves for Quantized {
259 fn visit(&self, v: &mut crate::tree::Visit<'_>) {
260 v.buffer("weight", &self.weight);
261 v.buffer("scales", &self.scales);
262 v.buffer("biases", &self.biases);
263 v.static_field("quant", &self.quant);
264 }
265}
266
267impl crate::tree::Tree for Quantized {
268 fn visit_mut(&mut self, v: &mut crate::tree::VisitMut<'_>) {
269 v.buffer("weight", &mut self.weight);
270 v.buffer("scales", &mut self.scales);
271 v.buffer("biases", &mut self.biases);
272 }
273}
274
275#[derive(Clone, Copy, Debug)]
278#[non_exhaustive]
279pub struct GatherQmm<'a> {
280 pub transpose: bool,
282 pub sorted_indices: bool,
285 pub global_scale: Option<&'a Array>,
288}
289
290impl Default for GatherQmm<'_> {
291 fn default() -> Self {
292 Self {
293 transpose: true,
294 sorted_indices: false,
295 global_scale: None,
296 }
297 }
298}
299
300impl<'a> GatherQmm<'a> {
301 pub fn groups(rows: usize, experts: usize) -> bool {
307 grouped(rows, experts)
308 }
309
310 #[must_use]
312 pub fn transpose(self, transpose: bool) -> Self {
313 Self { transpose, ..self }
314 }
315
316 #[must_use]
318 pub fn sorted_indices(self, sorted_indices: bool) -> Self {
319 Self {
320 sorted_indices,
321 ..self
322 }
323 }
324
325 #[must_use]
327 pub fn global_scale(self, global_scale: &'a Array) -> Self {
328 Self {
329 global_scale: Some(global_scale),
330 ..self
331 }
332 }
333}
334
335fn check_groups(op: &str, mode: QuantMode, scales: &Array, biases: Option<&Array>) -> Result<()> {
338 let invalid = |msg: String| Err(Error::InvalidArgument(format!("{op}: {msg}")));
339 match (mode, biases) {
340 (QuantMode::Affine, None) => {
341 invalid("biases must be provided for affine quantization".into())
342 }
343 (QuantMode::Affine, Some(_)) => Ok(()),
344 (_, Some(_)) => invalid(format!(
345 "biases must be null for quantization mode '{mode}'"
346 )),
347 (_, None) if scales.dtype() != DType::U8 => invalid(format!(
348 "scale type must be uint8 but received type {}",
349 scales.dtype()
350 )),
351 (_, None) => Ok(()),
352 }
353}
354
355pub(crate) fn quantize(w: &Array, quant: Quant) -> Result<(Array, Array, Option<Array>)> {
360 quant.checked("quantize")?;
361 let mut out = Array::from_op(Primitive::Quantize { quant }, vec![w.clone()])?;
362 let biases = (quant.mode == QuantMode::Affine).then(|| out.pop().expect("three outputs"));
363 let scales = out.pop().expect("scales");
364 Ok((out.pop().expect("codes"), scales, biases))
365}
366
367pub(crate) fn dequantize(
370 w: &Array,
371 scales: &Array,
372 biases: Option<&Array>,
373 quant: Quant,
374 dtype: Option<DType>,
375) -> Result<Array> {
376 quant.checked("dequantize")?;
377 check_groups("dequantize", quant.mode, scales, biases)?;
378 if let Some(d) = dtype
379 && d.category() != Category::Float
380 {
381 return Err(Error::InvalidArgument(format!(
382 "dequantize: only real floating types are supported but the output dtype is {d}"
383 )));
384 }
385 match biases {
386 Some(b) => {
387 let out = Array::from_op(
388 Primitive::Dequantize {
389 quant,
390 dtype: scales.dtype(),
391 },
392 vec![w.clone(), scales.clone(), b.clone()],
393 )?
394 .swap_remove(0);
395 match dtype {
396 Some(d) if d != out.dtype() => astype(&out, d),
397 _ => Ok(out),
398 }
399 }
400 None => {
401 let dtype = dtype.unwrap_or(DType::BF16);
402 if !matches!(dtype, DType::F16 | DType::BF16 | DType::F32) {
403 return Err(Error::unsupported_dtype("dequantize", dtype));
404 }
405 let primitive = Primitive::Dequantize { quant, dtype };
406 Ok(Array::from_op(primitive, vec![w.clone(), scales.clone()])?.swap_remove(0))
407 }
408 }
409}
410
411pub(crate) fn quantized_matmul(
419 x: &Array,
420 w: &Array,
421 scales: &Array,
422 biases: Option<&Array>,
423 transpose: bool,
424 quant: Quant,
425) -> Result<Array> {
426 quant.checked("quantized_matmul")?;
427 check_groups("quantized_matmul", quant.mode, scales, biases)?;
428 let dtype = match biases {
429 Some(b) => promote_types(promote_types(x.dtype(), scales.dtype()), b.dtype()),
430 None => x.dtype(),
431 };
432 if dtype.category() != Category::Float || dtype.size() > 4 {
433 return Err(Error::unsupported_dtype("quantized_matmul", dtype));
434 }
435 let x = astype(x, dtype)?;
436 let mut groups = vec![w.clone()];
437 match biases {
438 Some(b) => groups.extend([astype(scales, dtype)?, astype(b, dtype)?]),
439 None => groups.push(scales.clone()),
440 }
441 let vector = x.ndim() == 1;
442 let mut x = if vector { expand_dims(&x, 0)? } else { x };
443 let (xn, wn) = (x.ndim(), w.ndim());
444 if wn > 2 && xn >= 2 {
445 let batch =
448 view::broadcast_shapes("quantized_matmul", &x.dims()[..xn - 2], &w.dims()[..wn - 2])?;
449 let full = |a: &Array| -> Result<Array> {
450 let mut shape = batch.to_vec();
451 shape.extend_from_slice(&a.dims()[a.ndim() - 2..]);
452 broadcast_to(a, &shape)
453 };
454 x = full(&x)?;
455 for g in &mut groups {
456 *g = full(g)?;
457 }
458 }
459 let mut inputs = vec![x];
460 inputs.extend(groups);
461 let primitive = Primitive::QuantizedMatmul { quant, transpose };
462 let out = Array::from_op(primitive, inputs)?.swap_remove(0);
463 if vector {
464 squeeze(&out, &[-2])
465 } else {
466 Ok(out)
467 }
468}
469
470pub(crate) struct GatherOperands<'a> {
473 pub x: &'a Array,
474 pub w: &'a Array,
475 pub scales: &'a Array,
476 pub biases: Option<&'a Array>,
477 pub global_scale: Option<&'a Array>,
478 pub lhs: &'a Array,
479 pub rhs: &'a Array,
480 pub offsets: Option<&'a Array>,
481}
482
483impl<'a> GatherOperands<'a> {
484 pub(crate) fn arity(mode: QuantMode, global_scale: bool) -> usize {
486 5 + usize::from(mode == QuantMode::Affine) + usize::from(global_scale)
487 }
488
489 pub(crate) fn split(inputs: &'a [Array], mode: QuantMode, global_scale: bool) -> Self {
491 let mut i = 3;
492 let mut next = |present: bool| {
493 present.then(|| {
494 i += 1;
495 &inputs[i - 1]
496 })
497 };
498 let biases = next(mode == QuantMode::Affine);
499 let global_scale = next(global_scale);
500 let (lhs, rhs) = (&inputs[i], &inputs[i + 1]);
501 Self {
502 x: &inputs[0],
503 w: &inputs[1],
504 scales: &inputs[2],
505 biases,
506 global_scale,
507 lhs,
508 rhs,
509 offsets: inputs.get(i + 2),
510 }
511 }
512}
513
514#[allow(clippy::too_many_arguments)]
520pub(crate) fn gather_qmm(
521 x: &Array,
522 w: &Array,
523 scales: &Array,
524 biases: Option<&Array>,
525 lhs_indices: Option<&Array>,
526 rhs_indices: Option<&Array>,
527 quant: Quant,
528 options: GatherQmm<'_>,
529) -> Result<Array> {
530 let invalid = |msg: String| Err(Error::InvalidArgument(format!("gather_qmm: {msg}")));
531 quant.checked("gather_qmm")?;
532 if lhs_indices.is_none() && rhs_indices.is_none() {
533 if options.global_scale.is_some() {
534 return invalid("global scale is not supported without indices".into());
535 }
536 return quantized_matmul(x, w, scales, biases, options.transpose, quant);
537 }
538 check_groups("gather_qmm", quant.mode, scales, biases)?;
539 let dtype = match biases {
540 Some(b) => promote_types(promote_types(x.dtype(), scales.dtype()), b.dtype()),
541 None => x.dtype(),
542 };
543 if dtype.category() != Category::Float || dtype.size() > 4 {
544 return Err(Error::unsupported_dtype("gather_qmm", dtype));
545 }
546 if x.ndim() < 2 || w.ndim() < 2 || biases.is_some_and(|b| scales.dims() != b.dims()) {
547 return invalid(format!(
548 "x must be [..., M, K] and w [..., N, K'], got {:?} and {:?}",
549 x.dims(),
550 w.dims()
551 ));
552 }
553 if let Some(gs) = options.global_scale {
554 if quant.mode != QuantMode::Nvfp4 {
555 return invalid("global scale is only supported for 'nvfp4' quantization mode".into());
556 }
557 if gs.dtype() != DType::F32 {
558 return invalid(format!(
559 "global scale must have dtype float32 but got {}",
560 gs.dtype()
561 ));
562 }
563 let experts = &w.dims()[..w.ndim() - 2];
564 if gs.dims() != experts {
565 return invalid(format!(
566 "global scale must have one entry per expert with shape {experts:?} but got {:?}",
567 gs.dims()
568 ));
569 }
570 }
571 let x = astype(x, dtype)?;
572 let (lhs, rhs) = gather_indices("gather_qmm", &x, w, lhs_indices, rhs_indices)?;
573 let mut inputs = vec![x.clone(), w.clone()];
574 match biases {
575 Some(b) => inputs.extend([astype(scales, dtype)?, astype(b, dtype)?]),
576 None => inputs.push(scales.clone()),
577 }
578 inputs.extend(options.global_scale.cloned());
579 inputs.extend([lhs, rhs.clone()]);
580 let experts: usize = w.dims()[..w.ndim() - 2].iter().product();
585 let sorted = lhs_indices.is_none() && options.sorted_indices && grouped(rhs.size(), experts);
586 if let Some(offsets) = sorted_offsets(&x, w, &rhs, sorted)? {
587 inputs.push(offsets);
588 }
589 let op = crate::fast::imp::FastOp::GatherQmm {
590 quant,
591 transpose: options.transpose,
592 global_scale: options.global_scale.is_some(),
593 };
594 let kernel = (options.transpose || quant.mode != QuantMode::Affine)
596 && crate::fast::imp::on_gpu(dtype, x.size());
597 let out = if kernel || options.global_scale.is_some() {
601 Array::from_op(Primitive::Fast(op), inputs)?.swap_remove(0)
602 } else {
603 op.fallback(&inputs)?.swap_remove(0)
604 };
605 Ok(out)
606}
607
608pub(crate) fn gather_qmm_graph(
613 inputs: &[Array],
614 quant: Quant,
615 transpose: bool,
616 global_scale: bool,
617) -> Result<Array> {
618 let ops = GatherOperands::split(inputs, quant.mode, global_scale);
619 let rows = |a: &Array, indices: &Array| -> Result<Array> {
620 let nd = a.ndim();
621 let mut shape = vec![-1];
622 shape.extend(a.dims()[nd.saturating_sub(2)..].iter().map(|&d| d as isize));
623 crate::indexing::take(&reshape(a, &shape)?, indices, Some(0))
624 };
625 let (x, w, s) = (
626 rows(ops.x, ops.lhs)?,
627 rows(ops.w, ops.rhs)?,
628 rows(ops.scales, ops.rhs)?,
629 );
630 let Some(gs) = ops.global_scale else {
631 let b = ops.biases.map(|b| rows(b, ops.rhs)).transpose()?;
632 return quantized_matmul(&x, &w, &s, b.as_ref(), transpose, quant);
633 };
634 let factor = crate::ops::imp::divide(
635 &rows(&expand_dims(&expand_dims(gs, -1)?, -1)?, ops.rhs)?,
636 crate::fp8::E4M3_MAX * crate::fp8::E2M1_MAX,
637 )?;
638 let dense = multiply(&dequantize(&w, &s, None, quant, Some(DType::F32))?, &factor)?;
639 let dense = if transpose {
640 let n = dense.ndim();
641 let mut axes: Vec<usize> = (0..n).collect();
642 axes.swap(n - 2, n - 1);
643 crate::ops::imp::transpose(&dense, &axes)?
644 } else {
645 dense
646 };
647 let product = crate::ops::imp::matmul(&astype(&x, DType::F32)?, &dense)?;
648 astype(&product, x.dtype())
649}
650
651pub(crate) fn grouped(rows: usize, experts: usize) -> bool {
655 let least = if crate::row_exact() {
656 ROW_TOKENS + 1
657 } else {
658 16
659 };
660 rows >= least && rows / experts.max(1) >= 4
661}
662
663pub(crate) const ROW_TOKENS: usize = crate::ROW_INDEPENDENT_ROWS * 8;
666
667fn gather_indices(
670 op: &'static str,
671 a: &Array,
672 b: &Array,
673 lhs: Option<&Array>,
674 rhs: Option<&Array>,
675) -> Result<(Array, Array)> {
676 let indices = |x: &Array, given: Option<&Array>| -> Result<Array> {
677 let batch = &x.dims()[..x.ndim() - 2];
678 match given {
679 Some(i)
680 if i.dtype().category() == Category::Float || i.dtype() == crate::DType::Bool =>
681 {
682 Err(Error::InvalidArgument(format!(
683 "{op}: indices must be integers"
684 )))
685 }
686 Some(i) => astype(i, crate::DType::U32),
687 None => {
688 let n: usize = batch.iter().product();
689 let all = crate::ops::imp::arange(0.0, n as f64, 1.0, crate::DType::U32)?;
690 all.try_reshape(&batch.iter().map(|&d| d as isize).collect::<Vec<_>>())
691 }
692 }
693 };
694 let (l, r) = (indices(a, lhs)?, indices(b, rhs)?);
695 let batch = view::broadcast_shapes(op, l.dims(), r.dims())?;
696 Ok((broadcast_to(&l, &batch)?, broadcast_to(&r, &batch)?))
697}
698
699fn sorted_offsets(a: &Array, b: &Array, rhs: &Array, sorted: bool) -> Result<Option<Array>> {
703 let m = a.dims()[a.ndim() - 2];
704 if !(sorted && m == 1 && a.ndim() == 3 && rhs.ndim() == 1) {
705 return Ok(None);
706 }
707 let groups: usize = b.dims()[..b.ndim() - 2].iter().product();
708 let primitive = crate::primitive::Primitive::SortedOffsets { groups };
709 Ok(Some(
710 Array::from_op(primitive, vec![rhs.clone()])?.swap_remove(0),
711 ))
712}
713
714pub(crate) fn gather_mm(
720 a: &Array,
721 b: &Array,
722 lhs_indices: Option<&Array>,
723 rhs_indices: Option<&Array>,
724 sorted_indices: bool,
725) -> Result<Array> {
726 let dtype = promote_types(a.dtype(), b.dtype());
727 if dtype.category() != Category::Float {
728 return Err(Error::unsupported_dtype("gather_mm", dtype));
729 }
730 if a.ndim() < 2 || b.ndim() < 2 || a.dims()[a.ndim() - 1] != b.dims()[b.ndim() - 2] {
731 return Err(Error::InvalidArgument(format!(
732 "gather_mm: a must be [..., M, K] and b [..., K, N], got {:?} and {:?}",
733 a.dims(),
734 b.dims()
735 )));
736 }
737 let (a, b) = (astype(a, dtype)?, astype(b, dtype)?);
738 let (lhs, rhs) = gather_indices("gather_mm", &a, &b, lhs_indices, rhs_indices)?;
739 let mut inputs = vec![a.clone(), b.clone(), lhs, rhs.clone()];
740 let grouped = rhs.size() > ROW_TOKENS;
741 if let Some(offsets) = sorted_offsets(
742 &a,
743 &b,
744 &rhs,
745 lhs_indices.is_none() && sorted_indices && grouped,
746 )? {
747 inputs.push(offsets);
748 }
749 Ok(
750 crate::fast::imp::record_or_fallback(crate::fast::imp::FastOp::GatherMm, inputs)?
751 .swap_remove(0),
752 )
753}