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 affine(weight: Array, scales: Array, biases: Array, group_size: i32, bits: i32) -> Self {
211 Self::new(
212 weight,
213 scales,
214 Some(biases),
215 Quant::affine(group_size, bits),
216 )
217 }
218
219 pub fn fp(weight: Array, scales: Array, mode: QuantMode) -> Self {
222 Self::new(weight, scales, None, Quant::new(mode))
223 }
224
225 pub fn new(
230 weight: Array,
231 scales: Array,
232 biases: Option<Array>,
233 quant: impl Into<Quant>,
234 ) -> Self {
235 Self {
236 weight,
237 scales,
238 biases,
239 quant: quant.into(),
240 }
241 }
242
243 #[track_caller]
246 pub fn dequantize(&self, dtype: impl Into<Option<DType>>) -> Array {
247 crate::ops::dequantize(self, dtype)
248 }
249
250 pub fn check(&self) -> Result<()> {
252 for a in self.arrays() {
253 let _ = a.clone().check()?;
254 }
255 Ok(())
256 }
257
258 pub(crate) fn arrays(&self) -> impl Iterator<Item = &Array> {
260 [&self.weight, &self.scales].into_iter().chain(&self.biases)
261 }
262}
263
264impl crate::tree::Static for QuantMode {
265 fn hash_static(&self, mut h: &mut dyn std::hash::Hasher) {
266 std::hash::Hash::hash(self, &mut h);
267 }
268}
269
270impl crate::tree::Static for Quant {
271 fn hash_static(&self, mut h: &mut dyn std::hash::Hasher) {
272 std::hash::Hash::hash(self, &mut h);
273 }
274}
275
276impl crate::tree::Leaves for Quantized {
280 fn visit(&self, v: &mut crate::tree::Visit<'_>) {
281 v.buffer("weight", &self.weight);
282 v.buffer("scales", &self.scales);
283 v.buffer("biases", &self.biases);
284 v.static_field("quant", &self.quant);
285 }
286}
287
288impl crate::tree::Tree for Quantized {
289 fn visit_mut(&mut self, v: &mut crate::tree::VisitMut<'_>) {
290 v.buffer("weight", &mut self.weight);
291 v.buffer("scales", &mut self.scales);
292 v.buffer("biases", &mut self.biases);
293 }
294}
295
296#[derive(Clone, Copy, Debug)]
299#[non_exhaustive]
300pub struct GatherQmm<'a> {
301 pub transpose: bool,
303 pub sorted_indices: bool,
306 pub global_scale: Option<&'a Array>,
309}
310
311impl Default for GatherQmm<'_> {
312 fn default() -> Self {
313 Self {
314 transpose: true,
315 sorted_indices: false,
316 global_scale: None,
317 }
318 }
319}
320
321impl<'a> GatherQmm<'a> {
322 pub fn groups(rows: usize, experts: usize) -> bool {
328 grouped(rows, experts)
329 }
330
331 #[must_use]
333 pub fn transpose(self, transpose: bool) -> Self {
334 Self { transpose, ..self }
335 }
336
337 #[must_use]
339 pub fn sorted_indices(self, sorted_indices: bool) -> Self {
340 Self {
341 sorted_indices,
342 ..self
343 }
344 }
345
346 #[must_use]
348 pub fn global_scale(self, global_scale: &'a Array) -> Self {
349 Self {
350 global_scale: Some(global_scale),
351 ..self
352 }
353 }
354}
355
356fn check_groups(op: &str, mode: QuantMode, scales: &Array, biases: Option<&Array>) -> Result<()> {
359 let invalid = |msg: String| Err(Error::InvalidArgument(format!("{op}: {msg}")));
360 match (mode, biases) {
361 (QuantMode::Affine, None) => {
362 invalid("biases must be provided for affine quantization".into())
363 }
364 (QuantMode::Affine, Some(_)) => Ok(()),
365 (_, Some(_)) => invalid(format!(
366 "biases must be null for quantization mode '{mode}'"
367 )),
368 (_, None) if scales.dtype() != DType::U8 => invalid(format!(
369 "scale type must be uint8 but received type {}",
370 scales.dtype()
371 )),
372 (_, None) => Ok(()),
373 }
374}
375
376pub(crate) fn quantize(w: &Array, quant: Quant) -> Result<(Array, Array, Option<Array>)> {
381 quant.checked("quantize")?;
382 let mut out = Array::from_op(Primitive::Quantize { quant }, vec![w.clone()])?;
383 let biases = (quant.mode == QuantMode::Affine).then(|| out.pop().expect("three outputs"));
384 let scales = out.pop().expect("scales");
385 Ok((out.pop().expect("codes"), scales, biases))
386}
387
388pub(crate) fn dequantize(
391 w: &Array,
392 scales: &Array,
393 biases: Option<&Array>,
394 quant: Quant,
395 dtype: Option<DType>,
396) -> Result<Array> {
397 quant.checked("dequantize")?;
398 check_groups("dequantize", quant.mode, scales, biases)?;
399 if let Some(d) = dtype
400 && d.category() != Category::Float
401 {
402 return Err(Error::InvalidArgument(format!(
403 "dequantize: only real floating types are supported but the output dtype is {d}"
404 )));
405 }
406 match biases {
407 Some(b) => {
408 let out = Array::from_op(
409 Primitive::Dequantize {
410 quant,
411 dtype: scales.dtype(),
412 },
413 vec![w.clone(), scales.clone(), b.clone()],
414 )?
415 .swap_remove(0);
416 match dtype {
417 Some(d) if d != out.dtype() => astype(&out, d),
418 _ => Ok(out),
419 }
420 }
421 None => {
422 let dtype = dtype.unwrap_or(DType::BF16);
423 if !matches!(dtype, DType::F16 | DType::BF16 | DType::F32) {
424 return Err(Error::unsupported_dtype("dequantize", dtype));
425 }
426 let primitive = Primitive::Dequantize { quant, dtype };
427 Ok(Array::from_op(primitive, vec![w.clone(), scales.clone()])?.swap_remove(0))
428 }
429 }
430}
431
432pub(crate) fn quantized_matmul(
440 x: &Array,
441 w: &Array,
442 scales: &Array,
443 biases: Option<&Array>,
444 transpose: bool,
445 quant: Quant,
446) -> Result<Array> {
447 quant.checked("quantized_matmul")?;
448 check_groups("quantized_matmul", quant.mode, scales, biases)?;
449 let dtype = match biases {
450 Some(b) => promote_types(promote_types(x.dtype(), scales.dtype()), b.dtype()),
451 None => x.dtype(),
452 };
453 if dtype.category() != Category::Float || dtype.size() > 4 {
454 return Err(Error::unsupported_dtype("quantized_matmul", dtype));
455 }
456 let x = astype(x, dtype)?;
457 let mut groups = vec![w.clone()];
458 match biases {
459 Some(b) => groups.extend([astype(scales, dtype)?, astype(b, dtype)?]),
460 None => groups.push(scales.clone()),
461 }
462 let vector = x.ndim() == 1;
463 let mut x = if vector { expand_dims(&x, 0)? } else { x };
464 let (xn, wn) = (x.ndim(), w.ndim());
465 if wn > 2 && xn >= 2 {
466 let batch =
469 view::broadcast_shapes("quantized_matmul", &x.dims()[..xn - 2], &w.dims()[..wn - 2])?;
470 let full = |a: &Array| -> Result<Array> {
471 let mut shape = batch.to_vec();
472 shape.extend_from_slice(&a.dims()[a.ndim() - 2..]);
473 broadcast_to(a, &shape)
474 };
475 x = full(&x)?;
476 for g in &mut groups {
477 *g = full(g)?;
478 }
479 }
480 let mut inputs = vec![x];
481 inputs.extend(groups);
482 let primitive = Primitive::QuantizedMatmul { quant, transpose };
483 let out = Array::from_op(primitive, inputs)?.swap_remove(0);
484 if vector {
485 squeeze(&out, &[-2])
486 } else {
487 Ok(out)
488 }
489}
490
491pub(crate) struct GatherOperands<'a> {
494 pub x: &'a Array,
495 pub w: &'a Array,
496 pub scales: &'a Array,
497 pub biases: Option<&'a Array>,
498 pub global_scale: Option<&'a Array>,
499 pub lhs: &'a Array,
500 pub rhs: &'a Array,
501 pub offsets: Option<&'a Array>,
502}
503
504impl<'a> GatherOperands<'a> {
505 pub(crate) fn arity(mode: QuantMode, global_scale: bool) -> usize {
507 5 + usize::from(mode == QuantMode::Affine) + usize::from(global_scale)
508 }
509
510 pub(crate) fn split(inputs: &'a [Array], mode: QuantMode, global_scale: bool) -> Self {
512 let mut i = 3;
513 let mut next = |present: bool| {
514 present.then(|| {
515 i += 1;
516 &inputs[i - 1]
517 })
518 };
519 let biases = next(mode == QuantMode::Affine);
520 let global_scale = next(global_scale);
521 let (lhs, rhs) = (&inputs[i], &inputs[i + 1]);
522 Self {
523 x: &inputs[0],
524 w: &inputs[1],
525 scales: &inputs[2],
526 biases,
527 global_scale,
528 lhs,
529 rhs,
530 offsets: inputs.get(i + 2),
531 }
532 }
533}
534
535#[allow(clippy::too_many_arguments)]
541pub(crate) fn gather_qmm(
542 x: &Array,
543 w: &Array,
544 scales: &Array,
545 biases: Option<&Array>,
546 lhs_indices: Option<&Array>,
547 rhs_indices: Option<&Array>,
548 quant: Quant,
549 options: GatherQmm<'_>,
550) -> Result<Array> {
551 let invalid = |msg: String| Err(Error::InvalidArgument(format!("gather_qmm: {msg}")));
552 quant.checked("gather_qmm")?;
553 if lhs_indices.is_none() && rhs_indices.is_none() {
554 if options.global_scale.is_some() {
555 return invalid("global scale is not supported without indices".into());
556 }
557 return quantized_matmul(x, w, scales, biases, options.transpose, quant);
558 }
559 check_groups("gather_qmm", quant.mode, scales, biases)?;
560 let dtype = match biases {
561 Some(b) => promote_types(promote_types(x.dtype(), scales.dtype()), b.dtype()),
562 None => x.dtype(),
563 };
564 if dtype.category() != Category::Float || dtype.size() > 4 {
565 return Err(Error::unsupported_dtype("gather_qmm", dtype));
566 }
567 if x.ndim() < 2 || w.ndim() < 2 || biases.is_some_and(|b| scales.dims() != b.dims()) {
568 return invalid(format!(
569 "x must be [..., M, K] and w [..., N, K'], got {:?} and {:?}",
570 x.dims(),
571 w.dims()
572 ));
573 }
574 if let Some(gs) = options.global_scale {
575 if quant.mode != QuantMode::Nvfp4 {
576 return invalid("global scale is only supported for 'nvfp4' quantization mode".into());
577 }
578 if gs.dtype() != DType::F32 {
579 return invalid(format!(
580 "global scale must have dtype float32 but got {}",
581 gs.dtype()
582 ));
583 }
584 let experts = &w.dims()[..w.ndim() - 2];
585 if gs.dims() != experts {
586 return invalid(format!(
587 "global scale must have one entry per expert with shape {experts:?} but got {:?}",
588 gs.dims()
589 ));
590 }
591 }
592 let x = astype(x, dtype)?;
593 let (lhs, rhs) = gather_indices("gather_qmm", &x, w, lhs_indices, rhs_indices)?;
594 let mut inputs = vec![x.clone(), w.clone()];
595 match biases {
596 Some(b) => inputs.extend([astype(scales, dtype)?, astype(b, dtype)?]),
597 None => inputs.push(scales.clone()),
598 }
599 inputs.extend(options.global_scale.cloned());
600 inputs.extend([lhs, rhs.clone()]);
601 let experts: usize = w.dims()[..w.ndim() - 2].iter().product();
606 let sorted = lhs_indices.is_none() && options.sorted_indices && grouped(rhs.size(), experts);
607 if let Some(offsets) = sorted_offsets(&x, w, &rhs, sorted)? {
608 inputs.push(offsets);
609 }
610 let op = crate::fast::imp::FastOp::GatherQmm {
611 quant,
612 transpose: options.transpose,
613 global_scale: options.global_scale.is_some(),
614 };
615 let kernel = (options.transpose || quant.mode != QuantMode::Affine)
617 && crate::fast::imp::on_gpu(dtype, x.size());
618 let out = if kernel || options.global_scale.is_some() {
622 Array::from_op(Primitive::Fast(op), inputs)?.swap_remove(0)
623 } else {
624 op.fallback(&inputs)?.swap_remove(0)
625 };
626 Ok(out)
627}
628
629pub(crate) fn gather_qmm_graph(
634 inputs: &[Array],
635 quant: Quant,
636 transpose: bool,
637 global_scale: bool,
638) -> Result<Array> {
639 let ops = GatherOperands::split(inputs, quant.mode, global_scale);
640 let rows = |a: &Array, indices: &Array| -> Result<Array> {
641 let nd = a.ndim();
642 let mut shape = vec![-1];
643 shape.extend(a.dims()[nd.saturating_sub(2)..].iter().map(|&d| d as isize));
644 crate::indexing::take(&reshape(a, &shape)?, indices, Some(0))
645 };
646 let (x, w, s) = (
647 rows(ops.x, ops.lhs)?,
648 rows(ops.w, ops.rhs)?,
649 rows(ops.scales, ops.rhs)?,
650 );
651 let Some(gs) = ops.global_scale else {
652 let b = ops.biases.map(|b| rows(b, ops.rhs)).transpose()?;
653 return quantized_matmul(&x, &w, &s, b.as_ref(), transpose, quant);
654 };
655 let factor = crate::ops::imp::divide(
656 &rows(&expand_dims(&expand_dims(gs, -1)?, -1)?, ops.rhs)?,
657 crate::fp8::E4M3_MAX * crate::fp8::E2M1_MAX,
658 )?;
659 let dense = multiply(&dequantize(&w, &s, None, quant, Some(DType::F32))?, &factor)?;
660 let dense = if transpose {
661 let n = dense.ndim();
662 let mut axes: Vec<usize> = (0..n).collect();
663 axes.swap(n - 2, n - 1);
664 crate::ops::imp::transpose(&dense, &axes)?
665 } else {
666 dense
667 };
668 let product = crate::ops::imp::matmul(&astype(&x, DType::F32)?, &dense)?;
669 astype(&product, x.dtype())
670}
671
672pub(crate) fn grouped(rows: usize, experts: usize) -> bool {
676 let least = if crate::row_exact() {
677 ROW_TOKENS + 1
678 } else {
679 16
680 };
681 rows >= least && rows / experts.max(1) >= 4
682}
683
684pub(crate) const ROW_TOKENS: usize = crate::ROW_INDEPENDENT_ROWS * 8;
687
688fn gather_indices(
691 op: &'static str,
692 a: &Array,
693 b: &Array,
694 lhs: Option<&Array>,
695 rhs: Option<&Array>,
696) -> Result<(Array, Array)> {
697 let indices = |x: &Array, given: Option<&Array>| -> Result<Array> {
698 let batch = &x.dims()[..x.ndim() - 2];
699 match given {
700 Some(i)
701 if i.dtype().category() == Category::Float || i.dtype() == crate::DType::Bool =>
702 {
703 Err(Error::InvalidArgument(format!(
704 "{op}: indices must be integers"
705 )))
706 }
707 Some(i) => astype(i, crate::DType::U32),
708 None => {
709 let n: usize = batch.iter().product();
710 let all = crate::ops::imp::arange(0.0, n as f64, 1.0, crate::DType::U32)?;
711 all.try_reshape(&batch.iter().map(|&d| d as isize).collect::<Vec<_>>())
712 }
713 }
714 };
715 let (l, r) = (indices(a, lhs)?, indices(b, rhs)?);
716 let batch = view::broadcast_shapes(op, l.dims(), r.dims())?;
717 Ok((broadcast_to(&l, &batch)?, broadcast_to(&r, &batch)?))
718}
719
720fn sorted_offsets(a: &Array, b: &Array, rhs: &Array, sorted: bool) -> Result<Option<Array>> {
724 let m = a.dims()[a.ndim() - 2];
725 if !(sorted && m == 1 && a.ndim() == 3 && rhs.ndim() == 1) {
726 return Ok(None);
727 }
728 let groups: usize = b.dims()[..b.ndim() - 2].iter().product();
729 let primitive = crate::primitive::Primitive::SortedOffsets { groups };
730 Ok(Some(
731 Array::from_op(primitive, vec![rhs.clone()])?.swap_remove(0),
732 ))
733}
734
735pub(crate) fn gather_mm(
741 a: &Array,
742 b: &Array,
743 lhs_indices: Option<&Array>,
744 rhs_indices: Option<&Array>,
745 sorted_indices: bool,
746) -> Result<Array> {
747 let dtype = promote_types(a.dtype(), b.dtype());
748 if dtype.category() != Category::Float {
749 return Err(Error::unsupported_dtype("gather_mm", dtype));
750 }
751 if a.ndim() < 2 || b.ndim() < 2 || a.dims()[a.ndim() - 1] != b.dims()[b.ndim() - 2] {
752 return Err(Error::InvalidArgument(format!(
753 "gather_mm: a must be [..., M, K] and b [..., K, N], got {:?} and {:?}",
754 a.dims(),
755 b.dims()
756 )));
757 }
758 let (a, b) = (astype(a, dtype)?, astype(b, dtype)?);
759 let (lhs, rhs) = gather_indices("gather_mm", &a, &b, lhs_indices, rhs_indices)?;
760 let mut inputs = vec![a.clone(), b.clone(), lhs, rhs.clone()];
761 let grouped = rhs.size() > ROW_TOKENS;
762 if let Some(offsets) = sorted_offsets(
763 &a,
764 &b,
765 &rhs,
766 lhs_indices.is_none() && sorted_indices && grouped,
767 )? {
768 inputs.push(offsets);
769 }
770 Ok(
771 crate::fast::imp::record_or_fallback(crate::fast::imp::FastOp::GatherMm, inputs)?
772 .swap_remove(0),
773 )
774}