1#![warn(missing_docs)]
7
8use eredu_gguf::{Endian, GgmlType};
9use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
10
11pub mod composite;
13pub mod expert;
15pub mod gguf_store;
16pub mod recipe;
17pub mod safetensors;
19pub mod schema;
20pub mod store;
22pub mod validation;
24
25pub use recipe::{AtomicMatrixRecipeFamily, MatrixRecipeMember, RecipeAlias};
26
27#[derive(Debug, Clone, Eq, PartialEq)]
29pub enum StoredDtype {
30 Bool,
32 U8,
34 I8,
36 I16,
38 U16,
40 F16,
42 BF16,
44 I32,
46 U32,
48 F32,
50 F64,
52 I64,
54 U64,
56 C64,
58 F8E4M3,
60 F4,
62 F8E8M0,
64 F8E5M2,
66 Other(String),
68}
69
70#[derive(Debug, Clone, Eq, PartialEq)]
75#[non_exhaustive]
76pub enum SourceTensorEncoding {
77 Safetensors(StoredDtype),
79 Gguf {
81 ggml_type: GgmlType,
83 endian: Endian,
85 },
86}
87
88#[derive(Debug, Clone, thiserror::Error, Eq, PartialEq)]
90#[error("{0}")]
91pub struct Error(String);
92
93impl Error {
94 pub fn invalid(message: impl Into<String>) -> Self {
96 Self(message.into())
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
102pub struct AffineQuantization {
103 pub group_size: i32,
105 pub bits: i32,
107 #[serde(default = "default_affine_mode")]
109 pub mode: AffineQuantizationMode,
110}
111
112impl Default for AffineQuantization {
113 fn default() -> Self {
114 Self {
115 group_size: 64,
116 bits: 4,
117 mode: AffineQuantizationMode::Affine,
118 }
119 }
120}
121
122impl AffineQuantization {
123 pub fn new(group_size: i32, bits: i32) -> Result<Self, Error> {
125 let value = Self {
126 group_size,
127 bits,
128 mode: AffineQuantizationMode::Affine,
129 };
130 value.validate()?;
131 Ok(value)
132 }
133
134 pub fn validate(self) -> Result<(), Error> {
136 if self.mode != AffineQuantizationMode::Affine {
137 return Err(Error::invalid(
138 "only affine integer quantization is supported",
139 ));
140 }
141 if self.group_size != 16 && (self.group_size <= 0 || self.group_size % 32 != 0) {
142 return Err(Error::invalid(format!(
143 "group_size must be 16 or a positive multiple of 32, got {}",
144 self.group_size
145 )));
146 }
147 if !matches!(self.bits, 2 | 3 | 4 | 5 | 6 | 8) {
148 return Err(Error::invalid(format!(
149 "bits must be one of 2, 3, 4, 5, 6, or 8, got {}",
150 self.bits
151 )));
152 }
153 Ok(())
154 }
155}
156
157const fn default_affine_mode() -> AffineQuantizationMode {
158 AffineQuantizationMode::Affine
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "lowercase")]
164pub enum AffineQuantizationMode {
165 Affine,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub enum WeightQuantization {
173 Affine(AffineQuantization),
175 MxFp4,
177 GgufIQuant {
179 ggml_type: GgmlType,
181 endian: Endian,
183 },
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum BlockFp8ScaleEncoding {
189 FloatingPoint,
191 Ue8m0,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub struct BlockFp8Format {
198 pub block_rows: i32,
200 pub block_columns: i32,
202 pub scale_encoding: BlockFp8ScaleEncoding,
204}
205
206impl BlockFp8Format {
207 pub fn new(
209 block_rows: i32,
210 block_columns: i32,
211 scale_encoding: BlockFp8ScaleEncoding,
212 ) -> Result<Self, Error> {
213 let format = Self {
214 block_rows,
215 block_columns,
216 scale_encoding,
217 };
218 format.validate()?;
219 Ok(format)
220 }
221
222 pub fn validate(self) -> Result<(), Error> {
224 if self.block_rows <= 0 || self.block_columns <= 0 {
225 return Err(Error::invalid(format!(
226 "block-FP8 geometry must be positive, got [{}, {}]",
227 self.block_rows, self.block_columns
228 )));
229 }
230 Ok(())
231 }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub enum LinearFormat {
241 Dense,
243 Affine(AffineQuantization),
245 MxFp4,
247 GgufIQuant {
249 ggml_type: GgmlType,
251 endian: Endian,
253 },
254 E4M3BlockFp8(BlockFp8Format),
256}
257
258impl LinearFormat {
259 pub fn validate(self) -> Result<(), Error> {
261 match self {
262 Self::Dense => Ok(()),
263 Self::Affine(config) => config.validate(),
264 Self::MxFp4 => WeightQuantization::MxFp4.validate(),
265 Self::GgufIQuant { ggml_type, endian } => {
266 WeightQuantization::GgufIQuant { ggml_type, endian }.validate()
267 }
268 Self::E4M3BlockFp8(format) => format.validate(),
269 }
270 }
271
272 pub const fn weight_quantization(self) -> Option<WeightQuantization> {
275 match self {
276 Self::Dense | Self::E4M3BlockFp8(_) => None,
277 Self::Affine(config) => Some(WeightQuantization::Affine(config)),
278 Self::MxFp4 => Some(WeightQuantization::MxFp4),
279 Self::GgufIQuant { ggml_type, endian } => {
280 Some(WeightQuantization::GgufIQuant { ggml_type, endian })
281 }
282 }
283 }
284}
285
286impl From<WeightQuantization> for LinearFormat {
287 fn from(value: WeightQuantization) -> Self {
288 match value {
289 WeightQuantization::Affine(config) => Self::Affine(config),
290 WeightQuantization::MxFp4 => Self::MxFp4,
291 WeightQuantization::GgufIQuant { ggml_type, endian } => {
292 Self::GgufIQuant { ggml_type, endian }
293 }
294 }
295 }
296}
297
298impl From<Option<WeightQuantization>> for LinearFormat {
299 fn from(value: Option<WeightQuantization>) -> Self {
300 value.map_or(Self::Dense, Into::into)
301 }
302}
303
304impl WeightQuantization {
305 pub const MXFP4_GROUP_SIZE: i32 = 32;
307 pub const MXFP4_BITS: i32 = 4;
309
310 pub fn group_size(self) -> i32 {
312 match self {
313 Self::Affine(config) => config.group_size,
314 Self::MxFp4 => Self::MXFP4_GROUP_SIZE,
315 Self::GgufIQuant { ggml_type, .. } => {
316 ggml_type.block_and_bytes().expect("validated GGML type").0 as i32
317 }
318 }
319 }
320
321 pub fn bits(self) -> i32 {
323 match self {
324 Self::Affine(config) => config.bits,
325 Self::MxFp4 => Self::MXFP4_BITS,
326 Self::GgufIQuant { ggml_type, .. } => {
327 ggml_type.block_and_bytes().expect("validated GGML type").1 as i32
328 }
329 }
330 }
331
332 pub const fn has_biases(self) -> bool {
334 matches!(self, Self::Affine(_))
335 }
336
337 pub const fn gguf_iquant(self) -> Option<(GgmlType, Endian)> {
339 match self {
340 Self::GgufIQuant { ggml_type, endian } => Some((ggml_type, endian)),
341 _ => None,
342 }
343 }
344
345 pub fn validate(self) -> Result<(), Error> {
347 match self {
348 Self::Affine(config) => config.validate(),
349 Self::MxFp4 => Ok(()),
350 Self::GgufIQuant { ggml_type, .. } => ggml_type
351 .block_and_bytes()
352 .map(|_| ())
353 .map_err(|error| Error::invalid(error.to_string())),
354 }
355 }
356}
357
358impl From<AffineQuantization> for WeightQuantization {
359 fn from(value: AffineQuantization) -> Self {
360 Self::Affine(value)
361 }
362}
363
364#[derive(Serialize, Deserialize)]
365struct WeightQuantizationMetadata {
366 group_size: i32,
367 bits: i32,
368 #[serde(default = "default_quantization_mode")]
369 mode: String,
370}
371
372fn default_quantization_mode() -> String {
373 "affine".into()
374}
375
376impl Serialize for WeightQuantization {
377 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
378 where
379 S: Serializer,
380 {
381 let mode = match self {
382 Self::Affine(_) => "affine",
383 Self::MxFp4 => "mxfp4",
384 Self::GgufIQuant { .. } => {
385 return Err(serde::ser::Error::custom(
386 "checkpoint-native GGML block metadata is not serializable",
387 ))
388 }
389 };
390 WeightQuantizationMetadata {
391 group_size: self.group_size(),
392 bits: self.bits(),
393 mode: mode.into(),
394 }
395 .serialize(serializer)
396 }
397}
398
399impl<'de> Deserialize<'de> for WeightQuantization {
400 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
401 where
402 D: Deserializer<'de>,
403 {
404 let metadata = WeightQuantizationMetadata::deserialize(deserializer)?;
405 match metadata.mode.as_str() {
406 "affine" => AffineQuantization::new(metadata.group_size, metadata.bits)
407 .map(Self::Affine)
408 .map_err(de::Error::custom),
409 "mxfp4"
410 if metadata.group_size == Self::MXFP4_GROUP_SIZE
411 && metadata.bits == Self::MXFP4_BITS =>
412 {
413 Ok(Self::MxFp4)
414 }
415 "mxfp4" => Err(de::Error::custom(format!(
416 "MXFP4 requires group_size=32 and bits=4, got group_size={} bits={}",
417 metadata.group_size, metadata.bits
418 ))),
419 mode => Err(de::Error::custom(format!(
420 "unsupported quantization mode {mode:?}"
421 ))),
422 }
423 }
424}