#![warn(missing_docs)]
use eredu_gguf::{Endian, GgmlType};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
pub mod composite;
pub mod expert;
pub mod gguf_store;
pub mod recipe;
pub mod safetensors;
pub mod schema;
pub mod store;
pub mod validation;
pub use recipe::{AtomicMatrixRecipeFamily, MatrixRecipeMember, RecipeAlias};
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum StoredDtype {
Bool,
U8,
I8,
I16,
U16,
F16,
BF16,
I32,
U32,
F32,
F64,
I64,
U64,
C64,
F8E4M3,
F4,
F8E8M0,
F8E5M2,
Other(String),
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum SourceTensorEncoding {
Safetensors(StoredDtype),
Gguf {
ggml_type: GgmlType,
endian: Endian,
},
}
#[derive(Debug, Clone, thiserror::Error, Eq, PartialEq)]
#[error("{0}")]
pub struct Error(String);
impl Error {
pub fn invalid(message: impl Into<String>) -> Self {
Self(message.into())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct AffineQuantization {
pub group_size: i32,
pub bits: i32,
#[serde(default = "default_affine_mode")]
pub mode: AffineQuantizationMode,
}
impl Default for AffineQuantization {
fn default() -> Self {
Self {
group_size: 64,
bits: 4,
mode: AffineQuantizationMode::Affine,
}
}
}
impl AffineQuantization {
pub fn new(group_size: i32, bits: i32) -> Result<Self, Error> {
let value = Self {
group_size,
bits,
mode: AffineQuantizationMode::Affine,
};
value.validate()?;
Ok(value)
}
pub fn validate(self) -> Result<(), Error> {
if self.mode != AffineQuantizationMode::Affine {
return Err(Error::invalid(
"only affine integer quantization is supported",
));
}
if self.group_size != 16 && (self.group_size <= 0 || self.group_size % 32 != 0) {
return Err(Error::invalid(format!(
"group_size must be 16 or a positive multiple of 32, got {}",
self.group_size
)));
}
if !matches!(self.bits, 2 | 3 | 4 | 5 | 6 | 8) {
return Err(Error::invalid(format!(
"bits must be one of 2, 3, 4, 5, 6, or 8, got {}",
self.bits
)));
}
Ok(())
}
}
const fn default_affine_mode() -> AffineQuantizationMode {
AffineQuantizationMode::Affine
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AffineQuantizationMode {
Affine,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WeightQuantization {
Affine(AffineQuantization),
MxFp4,
GgufIQuant {
ggml_type: GgmlType,
endian: Endian,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockFp8ScaleEncoding {
FloatingPoint,
Ue8m0,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlockFp8Format {
pub block_rows: i32,
pub block_columns: i32,
pub scale_encoding: BlockFp8ScaleEncoding,
}
impl BlockFp8Format {
pub fn new(
block_rows: i32,
block_columns: i32,
scale_encoding: BlockFp8ScaleEncoding,
) -> Result<Self, Error> {
let format = Self {
block_rows,
block_columns,
scale_encoding,
};
format.validate()?;
Ok(format)
}
pub fn validate(self) -> Result<(), Error> {
if self.block_rows <= 0 || self.block_columns <= 0 {
return Err(Error::invalid(format!(
"block-FP8 geometry must be positive, got [{}, {}]",
self.block_rows, self.block_columns
)));
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinearFormat {
Dense,
Affine(AffineQuantization),
MxFp4,
GgufIQuant {
ggml_type: GgmlType,
endian: Endian,
},
E4M3BlockFp8(BlockFp8Format),
}
impl LinearFormat {
pub fn validate(self) -> Result<(), Error> {
match self {
Self::Dense => Ok(()),
Self::Affine(config) => config.validate(),
Self::MxFp4 => WeightQuantization::MxFp4.validate(),
Self::GgufIQuant { ggml_type, endian } => {
WeightQuantization::GgufIQuant { ggml_type, endian }.validate()
}
Self::E4M3BlockFp8(format) => format.validate(),
}
}
pub const fn weight_quantization(self) -> Option<WeightQuantization> {
match self {
Self::Dense | Self::E4M3BlockFp8(_) => None,
Self::Affine(config) => Some(WeightQuantization::Affine(config)),
Self::MxFp4 => Some(WeightQuantization::MxFp4),
Self::GgufIQuant { ggml_type, endian } => {
Some(WeightQuantization::GgufIQuant { ggml_type, endian })
}
}
}
}
impl From<WeightQuantization> for LinearFormat {
fn from(value: WeightQuantization) -> Self {
match value {
WeightQuantization::Affine(config) => Self::Affine(config),
WeightQuantization::MxFp4 => Self::MxFp4,
WeightQuantization::GgufIQuant { ggml_type, endian } => {
Self::GgufIQuant { ggml_type, endian }
}
}
}
}
impl From<Option<WeightQuantization>> for LinearFormat {
fn from(value: Option<WeightQuantization>) -> Self {
value.map_or(Self::Dense, Into::into)
}
}
impl WeightQuantization {
pub const MXFP4_GROUP_SIZE: i32 = 32;
pub const MXFP4_BITS: i32 = 4;
pub fn group_size(self) -> i32 {
match self {
Self::Affine(config) => config.group_size,
Self::MxFp4 => Self::MXFP4_GROUP_SIZE,
Self::GgufIQuant { ggml_type, .. } => {
ggml_type.block_and_bytes().expect("validated GGML type").0 as i32
}
}
}
pub fn bits(self) -> i32 {
match self {
Self::Affine(config) => config.bits,
Self::MxFp4 => Self::MXFP4_BITS,
Self::GgufIQuant { ggml_type, .. } => {
ggml_type.block_and_bytes().expect("validated GGML type").1 as i32
}
}
}
pub const fn has_biases(self) -> bool {
matches!(self, Self::Affine(_))
}
pub const fn gguf_iquant(self) -> Option<(GgmlType, Endian)> {
match self {
Self::GgufIQuant { ggml_type, endian } => Some((ggml_type, endian)),
_ => None,
}
}
pub fn validate(self) -> Result<(), Error> {
match self {
Self::Affine(config) => config.validate(),
Self::MxFp4 => Ok(()),
Self::GgufIQuant { ggml_type, .. } => ggml_type
.block_and_bytes()
.map(|_| ())
.map_err(|error| Error::invalid(error.to_string())),
}
}
}
impl From<AffineQuantization> for WeightQuantization {
fn from(value: AffineQuantization) -> Self {
Self::Affine(value)
}
}
#[derive(Serialize, Deserialize)]
struct WeightQuantizationMetadata {
group_size: i32,
bits: i32,
#[serde(default = "default_quantization_mode")]
mode: String,
}
fn default_quantization_mode() -> String {
"affine".into()
}
impl Serialize for WeightQuantization {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mode = match self {
Self::Affine(_) => "affine",
Self::MxFp4 => "mxfp4",
Self::GgufIQuant { .. } => {
return Err(serde::ser::Error::custom(
"checkpoint-native GGML block metadata is not serializable",
))
}
};
WeightQuantizationMetadata {
group_size: self.group_size(),
bits: self.bits(),
mode: mode.into(),
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for WeightQuantization {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let metadata = WeightQuantizationMetadata::deserialize(deserializer)?;
match metadata.mode.as_str() {
"affine" => AffineQuantization::new(metadata.group_size, metadata.bits)
.map(Self::Affine)
.map_err(de::Error::custom),
"mxfp4"
if metadata.group_size == Self::MXFP4_GROUP_SIZE
&& metadata.bits == Self::MXFP4_BITS =>
{
Ok(Self::MxFp4)
}
"mxfp4" => Err(de::Error::custom(format!(
"MXFP4 requires group_size=32 and bits=4, got group_size={} bits={}",
metadata.group_size, metadata.bits
))),
mode => Err(de::Error::custom(format!(
"unsupported quantization mode {mode:?}"
))),
}
}
}