use std::collections::HashMap;
use crate::{
array::Array,
dtype::Dtype,
error::{
Error, InvariantViolationPayload, OutOfRangePayload, RankMismatchPayload, Result,
ShapePairMismatchPayload, UnknownEnumValuePayload, UnsupportedDtypePayload,
},
ops::quantized,
};
use smol_str::format_smolstr;
const MODE_AFFINE: &str = "affine";
const MODE_MXFP4: &str = "mxfp4";
const MODE_MXFP8: &str = "mxfp8";
const MODE_NVFP4: &str = "nvfp4";
const KNOWN_MODES: &[&str] = &[MODE_AFFINE, MODE_MXFP4, MODE_MXFP8, MODE_NVFP4];
pub(crate) fn validate_quantized_triple(
context: &'static str,
weight: &Array,
scales: &Array,
biases: Option<&Array>,
group_size: i32,
bits: i32,
mode: &str,
) -> Result<()> {
if bits <= 0 {
return Err(Error::OutOfRange(OutOfRangePayload::new(
context,
"bits must be > 0 (per-mode value tables validated by mlx-c)",
format_smolstr!("{bits}"),
)));
}
if group_size <= 0 {
return Err(Error::OutOfRange(OutOfRangePayload::new(
context,
"group_size must be > 0 (per-mode value tables validated by mlx-c)",
format_smolstr!("{group_size}"),
)));
}
let w_shape = weight.shape();
if w_shape.len() != 2 {
return Err(Error::RankMismatch(RankMismatchPayload::new(
context,
w_shape.len() as u32,
w_shape.to_vec(),
)));
}
if weight.dtype()? != Dtype::U32 {
return Err(Error::InvariantViolation(InvariantViolationPayload::new(
context,
"weight must be `uint32` (the mlx-quantized-weight dtype; quantized ops reject non-`uint32` weights)",
)));
}
let out_features = w_shape[0];
let s_shape = scales.shape();
if s_shape.len() != w_shape.len() {
return Err(Error::RankMismatch(RankMismatchPayload::new(
context,
s_shape.len() as u32,
s_shape.to_vec(),
)));
}
if s_shape[0] != out_features {
return Err(Error::ShapePairMismatch(ShapePairMismatchPayload::new(
context,
vec![out_features],
vec![s_shape[0]],
)));
}
let weight_in = (w_shape[1] as i64) * 32 / i64::from(bits);
let scales_in = (s_shape[1] as i64) * i64::from(group_size);
if weight_in != scales_in {
return Err(Error::ShapePairMismatch(ShapePairMismatchPayload::new(
context,
vec![weight_in.max(0) as usize],
vec![scales_in.max(0) as usize],
)));
}
if let Some(b) = biases {
let b_shape = b.shape();
if b_shape.len() != s_shape.len() {
return Err(Error::RankMismatch(RankMismatchPayload::new(
context,
b_shape.len() as u32,
b_shape.to_vec(),
)));
}
if b_shape != s_shape {
return Err(Error::ShapePairMismatch(ShapePairMismatchPayload::new(
context,
s_shape.to_vec(),
b_shape.to_vec(),
)));
}
}
match mode {
MODE_AFFINE => {
if biases.is_none() {
return Err(Error::InvariantViolation(InvariantViolationPayload::new(
context,
"`affine` mode requires per-group biases (mlx `affine_quantize` always writes {w_q, scales, biases})",
)));
}
}
MODE_MXFP4 | MODE_MXFP8 | MODE_NVFP4 => {
if biases.is_some() {
return Err(Error::InvariantViolation(InvariantViolationPayload::new(
context,
"mxfp4 / mxfp8 / nvfp4 mode is scale-only (mlx `fp_quantize` writes {w_q, scales} with no biases); got a stale `biases`",
)));
}
let s_dtype = scales.dtype()?;
if s_dtype != Dtype::U8 {
return Err(Error::UnsupportedDtype(UnsupportedDtypePayload::new(
context,
s_dtype,
&[Dtype::U8],
)));
}
}
other => {
return Err(Error::UnknownEnumValue(UnknownEnumValuePayload::new(
context,
other.to_string(),
KNOWN_MODES,
)));
}
}
Ok(())
}
#[derive(Debug)]
pub struct Linear {
weight: Array,
bias: Option<Array>,
}
impl Linear {
pub fn new(weight: Array, bias: Option<Array>) -> Self {
Self { weight, bias }
}
pub fn forward(&self, x: &Array) -> Result<Array> {
let wt = self.weight.transpose()?;
match &self.bias {
Some(b) => x.addmm(b, &wt, 1.0, 1.0),
None => x.matmul(&wt),
}
}
#[inline(always)]
pub fn weight_ref(&self) -> &Array {
&self.weight
}
#[inline(always)]
pub fn bias(&self) -> Option<&Array> {
self.bias.as_ref()
}
}
#[derive(Debug)]
pub struct QuantizedLinear {
weight: Array,
scales: Array,
quant_biases: Option<Array>,
bias: Option<Array>,
group_size: i32,
bits: i32,
mode: String,
}
impl QuantizedLinear {
#[allow(clippy::too_many_arguments)]
pub fn from_parts(
weight: Array,
scales: Array,
quant_biases: Option<Array>,
bias: Option<Array>,
group_size: i32,
bits: i32,
mode: impl Into<String>,
) -> Result<Self> {
let mode = mode.into();
validate_quantized_triple(
"QuantizedLinear::from_parts",
&weight,
&scales,
quant_biases.as_ref(),
group_size,
bits,
&mode,
)?;
if let Some(b) = &bias {
let out_features = weight.shape()[0];
let b_shape = b.shape();
if b_shape.len() != 1 {
return Err(Error::RankMismatch(RankMismatchPayload::new(
"QuantizedLinear::from_parts: bias must be rank-1 (out_features,)",
b_shape.len() as u32,
b_shape.to_vec(),
)));
}
if b_shape[0] != out_features {
return Err(Error::ShapePairMismatch(ShapePairMismatchPayload::new(
"QuantizedLinear::from_parts: bias length must equal out_features (the quantized weight's logical output dim)",
vec![out_features],
b_shape.to_vec(),
)));
}
}
Ok(Self {
weight,
scales,
quant_biases,
bias,
group_size,
bits,
mode,
})
}
pub fn forward(&self, x: &Array) -> Result<Array> {
let y = quantized::quantized_matmul(
x,
&self.weight,
&self.scales,
self.quant_biases.as_ref(),
true,
self.group_size,
self.bits,
&self.mode,
)?;
match &self.bias {
Some(b) => y.add(b),
None => Ok(y),
}
}
#[inline(always)]
pub fn weight_ref(&self) -> &Array {
&self.weight
}
#[inline(always)]
pub fn scales_ref(&self) -> &Array {
&self.scales
}
#[inline(always)]
pub fn quant_biases(&self) -> Option<&Array> {
self.quant_biases.as_ref()
}
#[inline(always)]
pub fn bias(&self) -> Option<&Array> {
self.bias.as_ref()
}
#[inline(always)]
pub fn group_size(&self) -> i32 {
self.group_size
}
#[inline(always)]
pub fn bits(&self) -> i32 {
self.bits
}
#[inline(always)]
pub fn mode(&self) -> &str {
&self.mode
}
}
#[derive(Debug)]
pub enum MaybeQuantizedLinear {
Dense(Linear),
Quantized(QuantizedLinear),
}
const SCALES_SUFFIX: &str = ".scales";
const BIASES_SUFFIX: &str = ".biases";
const WEIGHT_SUFFIX: &str = ".weight";
const BIAS_SUFFIX: &str = ".bias";
impl MaybeQuantizedLinear {
#[inline(always)]
pub fn is_quantized(&self) -> bool {
matches!(self, MaybeQuantizedLinear::Quantized(_))
}
pub fn logical_shape(&self) -> Result<(i32, i32)> {
let context = "MaybeQuantizedLinear::logical_shape";
let (out, in_features): (i64, i64) = match self {
MaybeQuantizedLinear::Dense(l) => {
let shape = l.weight_ref().shape();
if shape.len() != 2 {
return Err(Error::RankMismatch(RankMismatchPayload::new(
"MaybeQuantizedLinear::logical_shape: dense weight must be rank-2 (out_features, in_features)",
shape.len() as u32,
shape.to_vec(),
)));
}
(shape[0] as i64, shape[1] as i64)
}
MaybeQuantizedLinear::Quantized(q) => {
let w_rows = q.weight.shape()[0] as i64;
let s_shape = q.scales.shape();
let logical_in = (s_shape[1] as i64) * i64::from(q.group_size);
(w_rows, logical_in)
}
};
let to_i32 = |v: i64| -> Result<i32> {
i32::try_from(v).map_err(|_| {
Error::OutOfRange(OutOfRangePayload::new(
context,
"logical linear dimension exceeds i32::MAX",
format_smolstr!("{v}"),
))
})
};
Ok((to_i32(out)?, to_i32(in_features)?))
}
pub fn weight_dtype(&self) -> Result<Dtype> {
match self {
MaybeQuantizedLinear::Dense(l) => l.weight_ref().dtype(),
MaybeQuantizedLinear::Quantized(q) => q.scales_ref().dtype(),
}
}
pub fn forward(&self, x: &Array) -> Result<Array> {
match self {
MaybeQuantizedLinear::Dense(l) => l.forward(x),
MaybeQuantizedLinear::Quantized(q) => q.forward(x),
}
}
pub fn from_weights(
weights: &mut HashMap<String, Array>,
prefix: &str,
quant: Option<(i32, i32, &str)>,
) -> Result<Self> {
let scales_key = format!("{prefix}{SCALES_SUFFIX}");
if weights.contains_key(&scales_key) {
let Some((group_size, bits, mode)) = quant else {
return Err(Error::InvariantViolation(InvariantViolationPayload::new(
"MaybeQuantizedLinear::from_weights: checkpoint carries a `.scales` sibling for this layer but no quantization config resolved scheme parameters",
"a quantized layer requires (group_size, bits, mode) from the config `quantization` block",
)));
};
let weight = take_required(weights, prefix, WEIGHT_SUFFIX)?;
let scales = take_required(weights, prefix, SCALES_SUFFIX)?;
let quant_biases = weights.remove(&format!("{prefix}{BIASES_SUFFIX}"));
let bias = weights.remove(&format!("{prefix}{BIAS_SUFFIX}"));
let q =
QuantizedLinear::from_parts(weight, scales, quant_biases, bias, group_size, bits, mode)?;
Ok(MaybeQuantizedLinear::Quantized(q))
} else {
let weight = take_required(weights, prefix, WEIGHT_SUFFIX)?;
let bias = weights.remove(&format!("{prefix}{BIAS_SUFFIX}"));
Ok(MaybeQuantizedLinear::Dense(Linear::new(weight, bias)))
}
}
pub fn from_weights_with_bias(
weights: &mut HashMap<String, Array>,
prefix: &str,
quant: Option<(i32, i32, &str)>,
bias: Option<Array>,
) -> Result<Self> {
let scales_key = format!("{prefix}{SCALES_SUFFIX}");
if weights.contains_key(&scales_key) {
let Some((group_size, bits, mode)) = quant else {
return Err(Error::InvariantViolation(InvariantViolationPayload::new(
"MaybeQuantizedLinear::from_weights_with_bias: checkpoint carries a `.scales` sibling for this layer but no quantization config resolved scheme parameters",
"a quantized layer requires (group_size, bits, mode) from the config `quantization` block",
)));
};
let weight = take_required(weights, prefix, WEIGHT_SUFFIX)?;
let scales = take_required(weights, prefix, SCALES_SUFFIX)?;
let quant_biases = weights.remove(&format!("{prefix}{BIASES_SUFFIX}"));
let q =
QuantizedLinear::from_parts(weight, scales, quant_biases, bias, group_size, bits, mode)?;
Ok(MaybeQuantizedLinear::Quantized(q))
} else {
let weight = take_required(weights, prefix, WEIGHT_SUFFIX)?;
Ok(MaybeQuantizedLinear::Dense(Linear::new(weight, bias)))
}
}
}
fn take_required(
weights: &mut HashMap<String, Array>,
prefix: &str,
suffix: &str,
) -> Result<Array> {
let key = format!("{prefix}{suffix}");
weights.remove(&key).ok_or_else(|| {
Error::MissingKey(crate::error::MissingKeyPayload::new(
"MaybeQuantizedLinear::from_weights: required weight not found in checkpoint",
key,
))
})
}
#[derive(Debug)]
pub struct QuantizedEmbedding {
weight: Array,
scales: Array,
biases: Option<Array>,
group_size: i32,
bits: i32,
mode: String,
}
impl QuantizedEmbedding {
#[inline(always)]
pub fn weight_ref(&self) -> &Array {
&self.weight
}
#[inline(always)]
pub fn scales_ref(&self) -> &Array {
&self.scales
}
#[inline(always)]
pub fn biases(&self) -> Option<&Array> {
self.biases.as_ref()
}
#[inline(always)]
pub fn group_size(&self) -> i32 {
self.group_size
}
#[inline(always)]
pub fn bits(&self) -> i32 {
self.bits
}
#[inline(always)]
pub fn mode(&self) -> &str {
&self.mode
}
}
#[derive(Debug)]
pub enum MaybeQuantizedEmbedding {
Dense(Array),
Quantized(QuantizedEmbedding),
}
impl MaybeQuantizedEmbedding {
#[inline(always)]
pub fn dense(weight: Array) -> Self {
MaybeQuantizedEmbedding::Dense(weight)
}
pub fn from_parts(
weight: Array,
scales: Array,
biases: Option<Array>,
group_size: i32,
bits: i32,
mode: impl Into<String>,
) -> Result<Self> {
let mode = mode.into();
validate_quantized_triple(
"MaybeQuantizedEmbedding::from_parts",
&weight,
&scales,
biases.as_ref(),
group_size,
bits,
&mode,
)?;
Ok(MaybeQuantizedEmbedding::Quantized(QuantizedEmbedding {
weight,
scales,
biases,
group_size,
bits,
mode,
}))
}
#[inline(always)]
pub fn is_quantized(&self) -> bool {
matches!(self, MaybeQuantizedEmbedding::Quantized(_))
}
pub fn logical_shape(&self) -> Result<(i32, i32)> {
let context = "MaybeQuantizedEmbedding::logical_shape";
let (rows, dim): (i64, i64) = match self {
MaybeQuantizedEmbedding::Dense(weight) => {
let shape = weight.shape();
if shape.len() != 2 {
return Err(Error::RankMismatch(RankMismatchPayload::new(
"MaybeQuantizedEmbedding::logical_shape: dense table must be rank-2 (num_embeddings, dim)",
shape.len() as u32,
shape.to_vec(),
)));
}
(shape[0] as i64, shape[1] as i64)
}
MaybeQuantizedEmbedding::Quantized(q) => {
let w_rows = q.weight.shape()[0] as i64;
let s_shape = q.scales.shape();
let logical_dim = (s_shape[1] as i64) * i64::from(q.group_size);
(w_rows, logical_dim)
}
};
let to_i32 = |v: i64| -> Result<i32> {
i32::try_from(v).map_err(|_| {
Error::OutOfRange(OutOfRangePayload::new(
context,
"logical embedding dimension exceeds i32::MAX",
format_smolstr!("{v}"),
))
})
};
Ok((to_i32(rows)?, to_i32(dim)?))
}
pub fn gather(&self, ids: &Array) -> Result<Array> {
match self {
MaybeQuantizedEmbedding::Dense(weight) => weight.take_axis(ids, 0),
MaybeQuantizedEmbedding::Quantized(q) => {
let w_rows = q.weight.take_axis(ids, 0)?;
let s_rows = q.scales.take_axis(ids, 0)?;
let b_rows = match &q.biases {
Some(b) => Some(b.take_axis(ids, 0)?),
None => None,
};
quantized::dequantize(
&w_rows,
&s_rows,
b_rows.as_ref(),
q.group_size,
q.bits,
&q.mode,
None,
None,
)
}
}
}
pub fn dense_table(&self, dtype: Option<Dtype>) -> Result<Array> {
match self {
MaybeQuantizedEmbedding::Dense(weight) => weight.try_clone(),
MaybeQuantizedEmbedding::Quantized(q) => quantized::dequantize(
&q.weight,
&q.scales,
q.biases.as_ref(),
q.group_size,
q.bits,
&q.mode,
None,
dtype,
),
}
}
pub fn from_weights(
weights: &mut HashMap<String, Array>,
prefix: &str,
quant: Option<(i32, i32, &str)>,
) -> Result<Self> {
let scales_key = format!("{prefix}{SCALES_SUFFIX}");
if weights.contains_key(&scales_key) {
let Some((group_size, bits, mode)) = quant else {
return Err(Error::InvariantViolation(InvariantViolationPayload::new(
"MaybeQuantizedEmbedding::from_weights: checkpoint carries a `.scales` sibling for this embedding but no quantization config resolved scheme parameters",
"a quantized embedding requires (group_size, bits, mode) from the config `quantization` block",
)));
};
let weight = take_required(weights, prefix, WEIGHT_SUFFIX)?;
let scales = take_required(weights, prefix, SCALES_SUFFIX)?;
let biases = weights.remove(&format!("{prefix}{BIASES_SUFFIX}"));
Self::from_parts(weight, scales, biases, group_size, bits, mode)
} else {
let weight = take_required(weights, prefix, WEIGHT_SUFFIX)?;
Ok(MaybeQuantizedEmbedding::Dense(weight))
}
}
}
#[cfg(test)]
mod tests;