1use std::{
4 collections::HashMap,
5 ffi::{CStr, CString},
6 fmt,
7 marker::PhantomData,
8 path::Path,
9 rc::Rc,
10};
11
12use crate::{
13 error::Exception,
14 utils::{
15 guard::{Guard, MaybeUninitArray},
16 SUCCESS,
17 },
18 Array, Dtype, Stream,
19};
20
21const NOT_FOUND: i32 = 2;
22const WRONG_METADATA_KIND: i32 = 3;
23
24#[derive(Debug, Clone)]
33pub enum GgufMetadataValue {
34 Array(Array),
36 String(String),
38 Strings(Vec<String>),
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum GgufMetadataKind {
51 Array,
53 String,
55 Strings,
57}
58
59#[derive(Debug, thiserror::Error)]
64#[non_exhaustive]
65pub enum GgufError {
66 #[error("path must point to a local file")]
68 NotFile,
69
70 #[error("path is not valid UTF-8")]
72 InvalidPathUtf8,
73
74 #[error("path must have a .gguf extension")]
76 UnsupportedExtension,
77
78 #[error("GGUF text contains an interior null byte")]
80 InteriorNul,
81
82 #[error("GGUF text is not valid UTF-8")]
84 InvalidUtf8,
85
86 #[error("array key {key:?} already exists")]
88 ArrayKeyAlreadyExists {
89 key: String,
91 },
92
93 #[error("metadata key {key:?} already exists")]
95 MetadataKeyAlreadyExists {
96 key: String,
98 },
99
100 #[error("metadata {key:?} has kind {actual:?}, expected {expected:?}")]
102 WrongMetadataKind {
103 key: String,
105 expected: GgufMetadataKind,
107 actual: GgufMetadataKind,
109 },
110
111 #[error("tensor dtype {dtype:?} cannot be written as GGUF")]
113 UnsupportedTensorDtype {
114 dtype: Dtype,
116 },
117
118 #[error("metadata array dtype {dtype:?} cannot be written as GGUF")]
120 UnsupportedMetadataArrayDtype {
121 dtype: Dtype,
123 },
124
125 #[error("metadata arrays must be scalar or one-dimensional, found rank {rank}")]
127 InvalidMetadataArrayRank {
128 rank: usize,
130 },
131
132 #[error("metadata arrays cannot be empty")]
134 EmptyMetadataArray,
135
136 #[error(transparent)]
138 Exception(#[from] Exception),
139}
140
141pub struct GgufFile {
154 handle: GgufHandle,
155 thread_affinity: PhantomData<Rc<()>>,
156}
157
158impl fmt::Debug for GgufFile {
159 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
160 formatter.debug_struct("GgufFile").finish_non_exhaustive()
161 }
162}
163
164impl GgufMetadataValue {
165 pub fn kind(&self) -> GgufMetadataKind {
167 match self {
168 Self::Array(_) => GgufMetadataKind::Array,
169 Self::String(_) => GgufMetadataKind::String,
170 Self::Strings(_) => GgufMetadataKind::Strings,
171 }
172 }
173}
174
175impl From<Array> for GgufMetadataValue {
176 fn from(value: Array) -> Self {
177 Self::Array(value)
178 }
179}
180
181impl From<&Array> for GgufMetadataValue {
182 fn from(value: &Array) -> Self {
183 Self::Array(value.clone())
184 }
185}
186
187impl From<String> for GgufMetadataValue {
188 fn from(value: String) -> Self {
189 Self::String(value)
190 }
191}
192
193impl From<&str> for GgufMetadataValue {
194 fn from(value: &str) -> Self {
195 Self::String(value.to_owned())
196 }
197}
198
199impl From<Vec<String>> for GgufMetadataValue {
200 fn from(value: Vec<String>) -> Self {
201 Self::Strings(value)
202 }
203}
204
205impl GgufFile {
206 pub fn new() -> Result<Self, GgufError> {
208 install_error_handler();
209 let handle = GgufHandle::new()?;
210 Ok(Self {
211 handle,
212 thread_affinity: PhantomData,
213 })
214 }
215
216 pub fn load(path: impl AsRef<Path>) -> Result<Self, GgufError> {
226 let path = path.as_ref();
227 if !path.is_file() {
228 return Err(GgufError::NotFile);
229 }
230 let path = path_c_string(path)?;
231 let mut file = Self::new()?;
232 let stream = Stream::thread_local_or_cpu();
233 let status =
234 unsafe { mlx_sys::mlx_load_gguf(&mut file.handle.raw, path.as_ptr(), stream.as_ptr()) };
235 status_result(status, "loading a GGUF file")?;
236 Ok(file)
237 }
238
239 pub fn save(&self, path: impl AsRef<Path>) -> Result<(), GgufError> {
253 for array in self.arrays()?.into_values() {
254 validate_tensor_dtype(array.dtype())?;
255 }
256 let path = path_c_string(path.as_ref())?;
257 let status = unsafe { mlx_sys::mlx_save_gguf(path.as_ptr(), self.handle.raw) };
258 status_result(status, "saving a GGUF file")
259 }
260
261 pub fn array_keys(&self) -> Result<Vec<String>, GgufError> {
263 let mut keys = VectorStringGuard::new()?;
264 let status = unsafe { mlx_sys::mlx_io_gguf_get_keys(&mut keys.raw, self.handle.raw) };
265 status_result(status, "enumerating GGUF array keys")?;
266 let mut result = keys.to_vec()?;
267 result.sort();
268 Ok(result)
269 }
270
271 pub fn get_array(&self, key: &str) -> Result<Option<Array>, GgufError> {
273 let key = text_c_string(key)?;
274 let mut output = MaybeUninitArray::new();
275 let status = unsafe {
276 mlx_sys::mlx_io_gguf_get_array(output.as_mut_raw_ptr(), self.handle.raw, key.as_ptr())
277 };
278 match status {
279 SUCCESS => {
280 output.set_init_success(true);
281 Ok(Some(output.try_into_guarded()?))
282 }
283 NOT_FOUND => Ok(None),
284 _ => Err(status_exception(status, "getting a GGUF array")),
285 }
286 }
287
288 pub fn arrays(&self) -> Result<HashMap<String, Array>, GgufError> {
290 self.array_keys()?
291 .into_iter()
292 .map(|key| {
293 self.get_array(&key)?
294 .map(|array| (key, array))
295 .ok_or_else(|| status_exception(NOT_FOUND, "reading an enumerated GGUF array"))
296 })
297 .collect()
298 }
299
300 pub fn insert_array(&mut self, key: impl AsRef<str>, value: &Array) -> Result<(), GgufError> {
302 let key_text = key.as_ref();
303 if self.get_array(key_text)?.is_some() {
304 return Err(GgufError::ArrayKeyAlreadyExists {
305 key: key_text.to_owned(),
306 });
307 }
308 validate_tensor_dtype(value.dtype())?;
309 let key = text_c_string(key_text)?;
310 let status = unsafe {
311 mlx_sys::mlx_io_gguf_set_array(self.handle.raw, key.as_ptr(), value.as_ptr())
312 };
313 status_result(status, "inserting a GGUF array")
314 }
315
316 pub fn metadata_kind(&self, key: &str) -> Result<Option<GgufMetadataKind>, GgufError> {
318 let key = text_c_string(key)?;
319 let probes = [
320 (
321 GgufMetadataKind::Array,
322 mlx_sys::mlx_io_gguf_has_metadata_array
323 as unsafe extern "C" fn(
324 *mut bool,
325 mlx_sys::mlx_io_gguf,
326 *const std::ffi::c_char,
327 ) -> i32,
328 ),
329 (
330 GgufMetadataKind::String,
331 mlx_sys::mlx_io_gguf_has_metadata_string,
332 ),
333 (
334 GgufMetadataKind::Strings,
335 mlx_sys::mlx_io_gguf_has_metadata_vector_string,
336 ),
337 ];
338 for (kind, probe) in probes {
339 let mut present = false;
340 let status = unsafe { probe(&mut present, self.handle.raw, key.as_ptr()) };
341 match status {
342 SUCCESS if present => return Ok(Some(kind)),
343 SUCCESS => {}
344 NOT_FOUND => return Ok(None),
345 _ => return Err(status_exception(status, "checking GGUF metadata kind")),
346 }
347 }
348 Err(status_exception(
349 WRONG_METADATA_KIND,
350 "identifying GGUF metadata kind",
351 ))
352 }
353
354 pub fn get_metadata(&self, key: &str) -> Result<Option<GgufMetadataValue>, GgufError> {
356 match self.metadata_kind(key)? {
357 Some(GgufMetadataKind::Array) => Ok(self.get_metadata_array(key)?.map(Into::into)),
358 Some(GgufMetadataKind::String) => Ok(self.get_metadata_string(key)?.map(Into::into)),
359 Some(GgufMetadataKind::Strings) => Ok(self.get_metadata_strings(key)?.map(Into::into)),
360 None => Ok(None),
361 }
362 }
363
364 pub fn get_metadata_array(&self, key: &str) -> Result<Option<Array>, GgufError> {
366 let c_key = text_c_string(key)?;
367 let mut output = MaybeUninitArray::new();
368 let status = unsafe {
369 mlx_sys::mlx_io_gguf_get_metadata_array(
370 output.as_mut_raw_ptr(),
371 self.handle.raw,
372 c_key.as_ptr(),
373 )
374 };
375 match status {
376 SUCCESS => {
377 output.set_init_success(true);
378 Ok(Some(output.try_into_guarded()?))
379 }
380 NOT_FOUND => Ok(None),
381 WRONG_METADATA_KIND => Err(self.wrong_kind(key, GgufMetadataKind::Array)?),
382 _ => Err(status_exception(status, "getting GGUF array metadata")),
383 }
384 }
385
386 pub fn get_metadata_string(&self, key: &str) -> Result<Option<String>, GgufError> {
388 let c_key = text_c_string(key)?;
389 let mut output = StringGuard::new();
390 let status = unsafe {
391 mlx_sys::mlx_io_gguf_get_metadata_string(
392 &mut output.raw,
393 self.handle.raw,
394 c_key.as_ptr(),
395 )
396 };
397 match status {
398 SUCCESS => Ok(Some(output.to_string()?)),
399 NOT_FOUND => Ok(None),
400 WRONG_METADATA_KIND => Err(self.wrong_kind(key, GgufMetadataKind::String)?),
401 _ => Err(status_exception(status, "getting GGUF string metadata")),
402 }
403 }
404
405 pub fn get_metadata_strings(&self, key: &str) -> Result<Option<Vec<String>>, GgufError> {
407 let c_key = text_c_string(key)?;
408 let mut output = VectorStringGuard::new()?;
409 let status = unsafe {
410 mlx_sys::mlx_io_gguf_get_metadata_vector_string(
411 &mut output.raw,
412 self.handle.raw,
413 c_key.as_ptr(),
414 )
415 };
416 match status {
417 SUCCESS => Ok(Some(output.to_vec()?)),
418 NOT_FOUND => Ok(None),
419 WRONG_METADATA_KIND => Err(self.wrong_kind(key, GgufMetadataKind::Strings)?),
420 _ => Err(status_exception(
421 status,
422 "getting GGUF string-list metadata",
423 )),
424 }
425 }
426
427 pub fn insert_metadata<V>(&mut self, key: impl AsRef<str>, value: V) -> Result<(), GgufError>
429 where
430 V: Into<GgufMetadataValue>,
431 {
432 let value = value.into();
433 let key_text = key.as_ref();
434 if self.metadata_kind(key_text)?.is_some() {
435 return Err(GgufError::MetadataKeyAlreadyExists {
436 key: key_text.to_owned(),
437 });
438 }
439 if let GgufMetadataValue::Array(array) = &value {
440 validate_metadata_array(array)?;
441 }
442 let key = text_c_string(key_text)?;
443 match value {
444 GgufMetadataValue::Array(array) => {
445 let status = unsafe {
446 mlx_sys::mlx_io_gguf_set_metadata_array(
447 self.handle.raw,
448 key.as_ptr(),
449 array.as_ptr(),
450 )
451 };
452 status_result(status, "inserting GGUF array metadata")
453 }
454 GgufMetadataValue::String(value) => {
455 let value = text_c_string(&value)?;
456 let status = unsafe {
457 mlx_sys::mlx_io_gguf_set_metadata_string(
458 self.handle.raw,
459 key.as_ptr(),
460 value.as_ptr(),
461 )
462 };
463 status_result(status, "inserting GGUF string metadata")
464 }
465 GgufMetadataValue::Strings(values) => {
466 let strings = values
467 .iter()
468 .map(|value| text_c_string(value))
469 .collect::<Result<Vec<_>, _>>()?;
470 let mut pointers = strings
471 .iter()
472 .map(|value| value.as_ptr())
473 .collect::<Vec<_>>();
474 let vector = VectorStringGuard::from_data(&mut pointers)?;
475 let status = unsafe {
476 mlx_sys::mlx_io_gguf_set_metadata_vector_string(
477 self.handle.raw,
478 key.as_ptr(),
479 vector.raw,
480 )
481 };
482 status_result(status, "inserting GGUF string-list metadata")
483 }
484 }
485 }
486
487 fn wrong_kind(&self, key: &str, expected: GgufMetadataKind) -> Result<GgufError, GgufError> {
488 let actual = self
489 .metadata_kind(key)?
490 .ok_or_else(|| status_exception(NOT_FOUND, "resolving a present GGUF metadata key"))?;
491 Ok(GgufError::WrongMetadataKind {
492 key: key.to_owned(),
493 expected,
494 actual,
495 })
496 }
497}
498
499struct GgufHandle {
500 raw: mlx_sys::mlx_io_gguf,
501}
502
503impl GgufHandle {
504 fn new() -> Result<Self, GgufError> {
505 let raw = unsafe { mlx_sys::mlx_io_gguf_new() };
506 if raw.ctx.is_null() {
507 Err(status_exception(SUCCESS + 1, "creating a GGUF container"))
508 } else {
509 Ok(Self { raw })
510 }
511 }
512}
513
514impl Drop for GgufHandle {
515 fn drop(&mut self) {
516 let _ = unsafe { mlx_sys::mlx_io_gguf_free(self.raw) };
517 }
518}
519
520struct StringGuard {
521 raw: mlx_sys::mlx_string,
522}
523
524impl StringGuard {
525 fn new() -> Self {
526 let raw = unsafe { mlx_sys::mlx_string_new() };
529 Self { raw }
530 }
531
532 fn to_string(&self) -> Result<String, GgufError> {
533 let data = unsafe { mlx_sys::mlx_string_data(self.raw) };
534 if data.is_null() {
535 return Err(status_exception(SUCCESS + 1, "reading an MLX string"));
536 }
537 unsafe { CStr::from_ptr(data) }
538 .to_str()
539 .map(str::to_owned)
540 .map_err(|_| GgufError::InvalidUtf8)
541 }
542}
543
544impl Drop for StringGuard {
545 fn drop(&mut self) {
546 let _ = unsafe { mlx_sys::mlx_string_free(self.raw) };
547 }
548}
549
550struct VectorStringGuard {
551 raw: mlx_sys::mlx_vector_string,
552}
553
554impl VectorStringGuard {
555 fn new() -> Result<Self, GgufError> {
556 let raw = unsafe { mlx_sys::mlx_vector_string_new() };
557 Self::from_raw(raw, "creating an MLX string vector")
558 }
559
560 fn from_data(data: &mut [*const std::ffi::c_char]) -> Result<Self, GgufError> {
561 let raw = unsafe { mlx_sys::mlx_vector_string_new_data(data.as_mut_ptr(), data.len()) };
562 Self::from_raw(raw, "creating an MLX string vector from data")
563 }
564
565 fn from_raw(raw: mlx_sys::mlx_vector_string, operation: &str) -> Result<Self, GgufError> {
566 if raw.ctx.is_null() {
567 Err(status_exception(SUCCESS + 1, operation))
568 } else {
569 Ok(Self { raw })
570 }
571 }
572
573 fn to_vec(&self) -> Result<Vec<String>, GgufError> {
574 let len = unsafe { mlx_sys::mlx_vector_string_size(self.raw) };
575 (0..len)
576 .map(|index| {
577 let mut data = std::ptr::null_mut();
578 let status = unsafe { mlx_sys::mlx_vector_string_get(&mut data, self.raw, index) };
579 status_result(status, "reading an MLX string vector")?;
580 if data.is_null() {
581 return Err(status_exception(
582 SUCCESS + 1,
583 "reading an MLX string vector",
584 ));
585 }
586 unsafe { CStr::from_ptr(data) }
587 .to_str()
588 .map(str::to_owned)
589 .map_err(|_| GgufError::InvalidUtf8)
590 })
591 .collect()
592 }
593}
594
595impl Drop for VectorStringGuard {
596 fn drop(&mut self) {
597 let _ = unsafe { mlx_sys::mlx_vector_string_free(self.raw) };
598 }
599}
600
601fn install_error_handler() {
602 crate::error::INIT_ERR_HANDLER.call_once(crate::error::setup_mlx_error_handler);
603}
604
605fn status_exception(status: i32, operation: &str) -> GgufError {
606 GgufError::Exception(crate::error::exception_from_status(status, operation))
607}
608
609fn status_result(status: i32, operation: &str) -> Result<(), GgufError> {
610 if status == SUCCESS {
611 Ok(())
612 } else {
613 Err(status_exception(status, operation))
614 }
615}
616
617fn text_c_string(value: &str) -> Result<CString, GgufError> {
618 CString::new(value).map_err(|_| GgufError::InteriorNul)
619}
620
621fn path_c_string(path: &Path) -> Result<CString, GgufError> {
622 if path.extension().and_then(|extension| extension.to_str()) != Some("gguf") {
623 return Err(GgufError::UnsupportedExtension);
624 }
625 let path = path.to_str().ok_or(GgufError::InvalidPathUtf8)?;
626 text_c_string(path)
627}
628
629fn validate_tensor_dtype(dtype: Dtype) -> Result<(), GgufError> {
630 if matches!(
631 dtype,
632 Dtype::Float32 | Dtype::Float16 | Dtype::Int8 | Dtype::Int16 | Dtype::Int32
633 ) {
634 Ok(())
635 } else {
636 Err(GgufError::UnsupportedTensorDtype { dtype })
637 }
638}
639
640fn validate_metadata_array(array: &Array) -> Result<(), GgufError> {
641 let rank = array.ndim();
642 if rank > 1 {
643 return Err(GgufError::InvalidMetadataArrayRank { rank });
644 }
645 if array.size() == 0 {
646 return Err(GgufError::EmptyMetadataArray);
647 }
648 let dtype = array.dtype();
649 if matches!(
650 dtype,
651 Dtype::Bool
652 | Dtype::Int8
653 | Dtype::Int16
654 | Dtype::Int32
655 | Dtype::Int64
656 | Dtype::Uint8
657 | Dtype::Uint16
658 | Dtype::Uint32
659 | Dtype::Uint64
660 | Dtype::Float32
661 ) {
662 Ok(())
663 } else {
664 Err(GgufError::UnsupportedMetadataArrayDtype { dtype })
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671
672 #[test]
673 fn writer_prevalidation_rejects_unsupported_tensor_dtypes() {
674 for dtype in [
675 Dtype::Bool,
676 Dtype::Uint8,
677 Dtype::Uint16,
678 Dtype::Uint32,
679 Dtype::Uint64,
680 Dtype::Int64,
681 Dtype::Bfloat16,
682 Dtype::Complex64,
683 ] {
684 assert!(matches!(
685 validate_tensor_dtype(dtype),
686 Err(GgufError::UnsupportedTensorDtype { dtype: actual }) if actual == dtype
687 ));
688 }
689 }
690
691 #[test]
692 fn metadata_prevalidation_rejects_rank_empty_and_dtype() {
693 let rank = Array::from_slice(&[1_i32, 2, 3, 4], &[2, 2]);
694 assert!(matches!(
695 validate_metadata_array(&rank),
696 Err(GgufError::InvalidMetadataArrayRank { rank: 2 })
697 ));
698
699 let empty = Array::from_slice::<i32>(&[], &[0]);
700 assert!(matches!(
701 validate_metadata_array(&empty),
702 Err(GgufError::EmptyMetadataArray)
703 ));
704
705 for array in [
706 Array::from_slice(&[half::f16::from_f32(1.0)], &[1]),
707 Array::from_slice(&[half::bf16::from_f32(1.0)], &[1]),
708 Array::from_complex(crate::complex64::new(1.0, 0.0)),
709 ] {
710 assert!(matches!(
711 validate_metadata_array(&array),
712 Err(GgufError::UnsupportedMetadataArrayDtype { .. })
713 ));
714 }
715 }
716
717 #[test]
718 fn duplicate_keys_are_rejected_per_namespace() {
719 let mut file = GgufFile::new().unwrap();
720 let value = Array::from_int(1);
721 file.insert_array("same", &value).unwrap();
722 assert!(matches!(
723 file.insert_array("same", &value),
724 Err(GgufError::ArrayKeyAlreadyExists { key }) if key == "same"
725 ));
726
727 file.insert_metadata("same", "first").unwrap();
728 assert!(matches!(
729 file.insert_metadata("same", vec!["second".to_owned()]),
730 Err(GgufError::MetadataKeyAlreadyExists { key }) if key == "same"
731 ));
732 }
733
734 #[test]
735 fn paths_and_text_are_validated_before_ffi_entry() {
736 assert!(matches!(
737 GgufFile::load("definitely-absent.gguf"),
738 Err(GgufError::NotFile)
739 ));
740 let file = GgufFile::new().unwrap();
741 assert!(matches!(
742 file.save("wrong-extension.bin"),
743 Err(GgufError::UnsupportedExtension)
744 ));
745 let mut file = GgufFile::new().unwrap();
746 assert!(matches!(
747 file.insert_array("bad\0key", &Array::from_int(1)),
748 Err(GgufError::InteriorNul)
749 ));
750 }
751}