1#![warn(missing_docs)]
7
8use eredu_gguf::{Endian, GgmlType};
9use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
10
11pub mod artifact;
13pub mod composite;
15pub mod expert;
17pub mod gguf_store;
18pub mod recipe;
19pub mod safetensors;
21pub mod schema;
22pub mod store;
24pub mod validation;
26
27pub use recipe::{AtomicMatrixRecipeFamily, MatrixRecipeMember, RecipeAlias};
28
29#[derive(Debug, Clone, Eq, PartialEq)]
31pub enum StoredDtype {
32 Bool,
34 U8,
36 I8,
38 I16,
40 U16,
42 F16,
44 BF16,
46 I32,
48 U32,
50 F32,
52 F64,
54 I64,
56 U64,
58 C64,
60 F8E4M3,
62 F4,
64 F8E8M0,
66 F8E5M2,
68 Other(String),
70}
71
72#[derive(Debug, Clone, Eq, PartialEq)]
77#[non_exhaustive]
78pub enum SourceTensorEncoding {
79 Safetensors(StoredDtype),
81 Gguf {
83 ggml_type: GgmlType,
85 endian: Endian,
87 },
88 RecipeOutput(StoredDtype),
91}
92
93impl SourceTensorEncoding {
94 pub fn scalar_dtype(&self) -> Option<StoredDtype> {
96 match self {
97 Self::Safetensors(dtype) | Self::RecipeOutput(dtype) => Some(dtype.clone()),
98 Self::Gguf { ggml_type, .. } => match ggml_type {
99 GgmlType::F16 => Some(StoredDtype::F16),
100 GgmlType::Bf16 => Some(StoredDtype::BF16),
101 GgmlType::F32 => Some(StoredDtype::F32),
102 _ => None,
103 },
104 }
105 }
106}
107
108#[derive(Debug, Clone, thiserror::Error, Eq, PartialEq)]
110#[error("{0}")]
111pub struct Error(String);
112
113impl Error {
114 pub fn invalid(message: impl Into<String>) -> Self {
116 Self(message.into())
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
122pub struct AffineQuantization {
123 pub group_size: i32,
125 pub bits: i32,
127 #[serde(default = "default_affine_mode")]
129 pub mode: AffineQuantizationMode,
130}
131
132impl Default for AffineQuantization {
133 fn default() -> Self {
134 Self {
135 group_size: 64,
136 bits: 4,
137 mode: AffineQuantizationMode::Affine,
138 }
139 }
140}
141
142impl AffineQuantization {
143 pub fn new(group_size: i32, bits: i32) -> Result<Self, Error> {
145 let value = Self {
146 group_size,
147 bits,
148 mode: AffineQuantizationMode::Affine,
149 };
150 value.validate()?;
151 Ok(value)
152 }
153
154 pub fn validate(self) -> Result<(), Error> {
156 if self.mode != AffineQuantizationMode::Affine {
157 return Err(Error::invalid(
158 "only affine integer quantization is supported",
159 ));
160 }
161 if self.group_size != 16 && (self.group_size <= 0 || self.group_size % 32 != 0) {
162 return Err(Error::invalid(format!(
163 "group_size must be 16 or a positive multiple of 32, got {}",
164 self.group_size
165 )));
166 }
167 if !matches!(self.bits, 2 | 3 | 4 | 5 | 6 | 8) {
168 return Err(Error::invalid(format!(
169 "bits must be one of 2, 3, 4, 5, 6, or 8, got {}",
170 self.bits
171 )));
172 }
173 Ok(())
174 }
175}
176
177const fn default_affine_mode() -> AffineQuantizationMode {
178 AffineQuantizationMode::Affine
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(rename_all = "lowercase")]
184pub enum AffineQuantizationMode {
185 Affine,
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum WeightQuantization {
193 Affine(AffineQuantization),
195 MxFp4,
197 GgufIQuant {
199 ggml_type: GgmlType,
201 endian: Endian,
203 },
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum BlockFp8ScaleEncoding {
209 FloatingPoint,
211 Ue8m0,
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub struct BlockFp8Format {
218 pub block_rows: i32,
220 pub block_columns: i32,
222 pub scale_encoding: BlockFp8ScaleEncoding,
224}
225
226impl BlockFp8Format {
227 pub fn new(
229 block_rows: i32,
230 block_columns: i32,
231 scale_encoding: BlockFp8ScaleEncoding,
232 ) -> Result<Self, Error> {
233 let format = Self {
234 block_rows,
235 block_columns,
236 scale_encoding,
237 };
238 format.validate()?;
239 Ok(format)
240 }
241
242 pub fn validate(self) -> Result<(), Error> {
244 if self.block_rows <= 0 || self.block_columns <= 0 {
245 return Err(Error::invalid(format!(
246 "block-FP8 geometry must be positive, got [{}, {}]",
247 self.block_rows, self.block_columns
248 )));
249 }
250 Ok(())
251 }
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub enum LinearFormat {
261 Dense,
263 Affine(AffineQuantization),
265 MxFp4,
267 GgufIQuant {
269 ggml_type: GgmlType,
271 endian: Endian,
273 },
274 E4M3BlockFp8(BlockFp8Format),
276}
277
278impl LinearFormat {
279 pub fn validate(self) -> Result<(), Error> {
281 match self {
282 Self::Dense => Ok(()),
283 Self::Affine(config) => config.validate(),
284 Self::MxFp4 => WeightQuantization::MxFp4.validate(),
285 Self::GgufIQuant { ggml_type, endian } => {
286 WeightQuantization::GgufIQuant { ggml_type, endian }.validate()
287 }
288 Self::E4M3BlockFp8(format) => format.validate(),
289 }
290 }
291
292 pub const fn weight_quantization(self) -> Option<WeightQuantization> {
295 match self {
296 Self::Dense | Self::E4M3BlockFp8(_) => None,
297 Self::Affine(config) => Some(WeightQuantization::Affine(config)),
298 Self::MxFp4 => Some(WeightQuantization::MxFp4),
299 Self::GgufIQuant { ggml_type, endian } => {
300 Some(WeightQuantization::GgufIQuant { ggml_type, endian })
301 }
302 }
303 }
304}
305
306impl From<WeightQuantization> for LinearFormat {
307 fn from(value: WeightQuantization) -> Self {
308 match value {
309 WeightQuantization::Affine(config) => Self::Affine(config),
310 WeightQuantization::MxFp4 => Self::MxFp4,
311 WeightQuantization::GgufIQuant { ggml_type, endian } => {
312 Self::GgufIQuant { ggml_type, endian }
313 }
314 }
315 }
316}
317
318impl From<Option<WeightQuantization>> for LinearFormat {
319 fn from(value: Option<WeightQuantization>) -> Self {
320 value.map_or(Self::Dense, Into::into)
321 }
322}
323
324impl WeightQuantization {
325 pub const MXFP4_GROUP_SIZE: i32 = 32;
327 pub const MXFP4_BITS: i32 = 4;
329
330 pub fn group_size(self) -> i32 {
332 match self {
333 Self::Affine(config) => config.group_size,
334 Self::MxFp4 => Self::MXFP4_GROUP_SIZE,
335 Self::GgufIQuant { ggml_type, .. } => {
336 ggml_type.block_and_bytes().expect("validated GGML type").0 as i32
337 }
338 }
339 }
340
341 pub fn bits(self) -> i32 {
343 match self {
344 Self::Affine(config) => config.bits,
345 Self::MxFp4 => Self::MXFP4_BITS,
346 Self::GgufIQuant { ggml_type, .. } => {
347 ggml_type.block_and_bytes().expect("validated GGML type").1 as i32
348 }
349 }
350 }
351
352 pub const fn has_biases(self) -> bool {
354 matches!(self, Self::Affine(_))
355 }
356
357 pub const fn gguf_iquant(self) -> Option<(GgmlType, Endian)> {
359 match self {
360 Self::GgufIQuant { ggml_type, endian } => Some((ggml_type, endian)),
361 _ => None,
362 }
363 }
364
365 pub fn validate(self) -> Result<(), Error> {
367 match self {
368 Self::Affine(config) => config.validate(),
369 Self::MxFp4 => Ok(()),
370 Self::GgufIQuant { ggml_type, .. } => ggml_type
371 .block_and_bytes()
372 .map(|_| ())
373 .map_err(|error| Error::invalid(error.to_string())),
374 }
375 }
376}
377
378impl From<AffineQuantization> for WeightQuantization {
379 fn from(value: AffineQuantization) -> Self {
380 Self::Affine(value)
381 }
382}
383
384#[derive(Serialize, Deserialize)]
385struct WeightQuantizationMetadata {
386 group_size: i32,
387 bits: i32,
388 #[serde(default = "default_quantization_mode")]
389 mode: String,
390}
391
392fn default_quantization_mode() -> String {
393 "affine".into()
394}
395
396impl Serialize for WeightQuantization {
397 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
398 where
399 S: Serializer,
400 {
401 let mode = match self {
402 Self::Affine(_) => "affine",
403 Self::MxFp4 => "mxfp4",
404 Self::GgufIQuant { .. } => {
405 return Err(serde::ser::Error::custom(
406 "checkpoint-native GGML block metadata is not serializable",
407 ))
408 }
409 };
410 WeightQuantizationMetadata {
411 group_size: self.group_size(),
412 bits: self.bits(),
413 mode: mode.into(),
414 }
415 .serialize(serializer)
416 }
417}
418
419impl<'de> Deserialize<'de> for WeightQuantization {
420 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
421 where
422 D: Deserializer<'de>,
423 {
424 let metadata = WeightQuantizationMetadata::deserialize(deserializer)?;
425 match metadata.mode.as_str() {
426 "affine" => AffineQuantization::new(metadata.group_size, metadata.bits)
427 .map(Self::Affine)
428 .map_err(de::Error::custom),
429 "mxfp4"
430 if metadata.group_size == Self::MXFP4_GROUP_SIZE
431 && metadata.bits == Self::MXFP4_BITS =>
432 {
433 Ok(Self::MxFp4)
434 }
435 "mxfp4" => Err(de::Error::custom(format!(
436 "MXFP4 requires group_size=32 and bits=4, got group_size={} bits={}",
437 metadata.group_size, metadata.bits
438 ))),
439 mode => Err(de::Error::custom(format!(
440 "unsupported quantization mode {mode:?}"
441 ))),
442 }
443 }
444}