Skip to main content

ferrum_quantization/gguf/
file.rs

1//! `GgufFile`: mmap-backed reader for a single GGUF file.
2//!
3//! Lifecycle:
4//!   1. `GgufFile::open(path)` — mmaps the file and parses the header.
5//!      No tensor payloads are read at this stage.
6//!   2. `architecture()`, `metadata_*()`, `tensor_names()`, `tensor_info()` —
7//!      cheap lookups, all served from the parsed header in memory.
8//!   3. `read_tensor(name, device)` — slices the mmap at the right offset
9//!      and asks candle to materialise a `QTensor` (still quantized).
10//!
11//! Tensor reads only need a shared `&self` because the mmap is immutable; the
12//! file is safe to share across threads. (Candle's `Content::tensor` wants
13//! a `&mut R: Read + Seek`, but we satisfy it with a fresh `Cursor<&[u8]>`
14//! on each call — the cursor's mutable state is local to the call.)
15
16use std::fs::File;
17use std::io::Cursor;
18use std::path::Path;
19
20use candle_core::quantized::gguf_file::{Content, TensorInfo, Value};
21use candle_core::quantized::QTensor;
22use candle_core::{Device, Error as CandleError, Result as CandleResult};
23use memmap2::Mmap;
24
25/// Read-only handle to a memory-mapped GGUF file.
26pub struct GgufFile {
27    /// memory-mapped file payload. Kept alive for the lifetime of `self`
28    /// because `read_tensor` slices into it.
29    mmap: Mmap,
30    /// Parsed header / metadata / tensor descriptors. No payload bytes.
31    content: Content,
32}
33
34impl GgufFile {
35    /// Open and parse the header of a GGUF file.
36    ///
37    /// Returns immediately after the descriptor table is read — no tensor
38    /// data is materialised. `read_tensor` lazy-loads individual tensors.
39    pub fn open(path: impl AsRef<Path>) -> CandleResult<Self> {
40        let path_ref = path.as_ref();
41        let file = File::open(path_ref).map_err(|e| {
42            CandleError::Msg(format!(
43                "failed to open GGUF file '{}': {e}",
44                path_ref.display()
45            ))
46        })?;
47        // SAFETY: `Mmap::map` requires that the underlying file is not modified
48        // while the mapping is live. We treat the file as read-only for the
49        // entire lifetime of `self`. `Mmap` itself only exposes `&[u8]`.
50        let mmap = unsafe { Mmap::map(&file) }.map_err(|e| {
51            CandleError::Msg(format!(
52                "failed to mmap GGUF file '{}': {e}",
53                path_ref.display()
54            ))
55        })?;
56        let mut cursor = Cursor::new(&mmap[..]);
57        let content = Content::read(&mut cursor)?;
58        if content
59            .metadata
60            .keys()
61            .any(|key| super::hadamard::declares_transform(key))
62        {
63            return Err(super::hadamard::unsupported_execution());
64        }
65        Ok(Self { mmap, content })
66    }
67
68    /// Raw access to candle's parsed header — for callers that need the full
69    /// `metadata` / `tensor_infos` maps. Prefer the typed accessors below.
70    pub fn content(&self) -> &Content {
71        &self.content
72    }
73
74    // ── Metadata: typed accessors ─────────────────────────────────────────
75    //
76    // GGUF metadata keys are conventionally `<scope>.<field>` strings, e.g.
77    // `general.architecture` or `qwen3.block_count`. Different model families
78    // namespace under their architecture id. `architecture()` is the one key
79    // that's always present and tells you which scope to read the rest from.
80
81    /// Architecture string, e.g. `"qwen3"`, `"llama"`. Read from
82    /// `general.architecture`. Errors if the key is missing or non-string.
83    pub fn architecture(&self) -> CandleResult<&str> {
84        self.metadata_string("general.architecture")
85    }
86
87    /// Raw metadata value lookup. Returns `None` if the key is absent.
88    pub fn metadata(&self, key: &str) -> Option<&Value> {
89        self.content.metadata.get(key)
90    }
91
92    /// Read a string-typed metadata field. Errors if missing or wrong type.
93    pub fn metadata_string(&self, key: &str) -> CandleResult<&str> {
94        self.require_metadata(key)?.to_string().map(|s| s.as_str())
95    }
96
97    /// Read a u32-typed metadata field. Errors if missing or wrong type.
98    pub fn metadata_u32(&self, key: &str) -> CandleResult<u32> {
99        self.require_metadata(key)?.to_u32()
100    }
101
102    /// Read a u64-typed metadata field. Errors if missing or wrong type.
103    pub fn metadata_u64(&self, key: &str) -> CandleResult<u64> {
104        self.require_metadata(key)?.to_u64()
105    }
106
107    /// Read an f32-typed metadata field. Errors if missing or wrong type.
108    pub fn metadata_f32(&self, key: &str) -> CandleResult<f32> {
109        self.require_metadata(key)?.to_f32()
110    }
111
112    /// Read a bool-typed metadata field. Errors if missing or wrong type.
113    pub fn metadata_bool(&self, key: &str) -> CandleResult<bool> {
114        self.require_metadata(key)?.to_bool()
115    }
116
117    fn require_metadata(&self, key: &str) -> CandleResult<&Value> {
118        self.metadata(key)
119            .ok_or_else(|| CandleError::Msg(format!("GGUF metadata key missing: '{key}'")))
120    }
121
122    // ── Tensor enumeration ────────────────────────────────────────────────
123
124    /// Total number of tensors declared in the header.
125    pub fn tensor_count(&self) -> usize {
126        self.content.tensor_infos.len()
127    }
128
129    /// Iterate over every tensor name in the file. Order is whatever the
130    /// underlying `HashMap` yields — do not rely on it being deterministic.
131    pub fn tensor_names(&self) -> impl Iterator<Item = &str> {
132        self.content.tensor_infos.keys().map(|s| s.as_str())
133    }
134
135    /// Look up a tensor descriptor (shape, dtype, byte offset) without
136    /// touching the payload. `None` if the tensor isn't in the file.
137    pub fn tensor_info(&self, name: &str) -> Option<&TensorInfo> {
138        self.content.tensor_infos.get(name)
139    }
140
141    /// Whether a tensor with `name` is declared in the header.
142    pub fn has_tensor(&self, name: &str) -> bool {
143        self.content.tensor_infos.contains_key(name)
144    }
145
146    // ── Tensor read ───────────────────────────────────────────────────────
147
148    /// Materialise a tensor as a candle `QTensor` on the target device.
149    ///
150    /// The returned tensor is **still quantized** — no dequant happens here.
151    /// Wrap it in `QMatMul::from_qtensor` for inference, or call
152    /// `QTensor::dequantize(device)` to get a fp32 `Tensor`.
153    ///
154    /// **Beware:** candle copies the bytes into an owned `Vec<u8>` (see
155    /// `TensorInfo::read`). For the steady-state weight upload path use
156    /// [`Self::tensor_byte_slice`] instead — it returns a slice directly
157    /// into the mmap with no allocation.
158    pub fn read_tensor(&self, name: &str, device: &Device) -> CandleResult<QTensor> {
159        let mut cursor = Cursor::new(&self.mmap[..]);
160        self.content.tensor(&mut cursor, name, device)
161    }
162
163    /// Whole mmap region as a byte slice. Used to wrap the file as a single
164    /// zero-copy `MTLBuffer` on Metal — the lifetime of the slice is tied to
165    /// `&self`, so the caller is expected to keep an `Arc<GgufFile>` alive
166    /// for as long as anything references the mmap.
167    pub fn mmap_bytes(&self) -> &[u8] {
168        &self.mmap[..]
169    }
170
171    /// Byte slice covering exactly tensor `name` inside the mmap. The slice
172    /// points into the file mapping, so reads are demand-paged and there is
173    /// no heap allocation. Returns `None` if the tensor isn't declared.
174    ///
175    /// The byte length is computed from the tensor's `(elem_count, ggml_dtype)`
176    /// using candle's `block_size()` / `type_size()`. For raw quant tensors
177    /// (Q4K / Q6K / etc.), these bytes are exactly what `QTensor::data()`
178    /// would return — but with no copy.
179    pub fn tensor_byte_slice(&self, name: &str) -> Option<&[u8]> {
180        let info = self.content.tensor_infos.get(name)?;
181        let elem_count = info.shape.elem_count();
182        let block_size = info.ggml_dtype.block_size();
183        if !elem_count.is_multiple_of(block_size) {
184            return None;
185        }
186        let size_in_bytes = elem_count / block_size * info.ggml_dtype.type_size();
187        let abs_start = (self.content.tensor_data_offset + info.offset) as usize;
188        let abs_end = abs_start.checked_add(size_in_bytes)?;
189        if abs_end > self.mmap.len() {
190            return None;
191        }
192        Some(&self.mmap[abs_start..abs_end])
193    }
194
195    /// `(byte_offset_in_mmap, byte_length)` for tensor `name`. Same
196    /// computation as [`Self::tensor_byte_slice`] but returns the indices
197    /// rather than the slice — useful when the caller already has the
198    /// mmap base pointer (e.g. when binding a region of a shared buffer
199    /// at a given offset).
200    pub fn tensor_byte_range(&self, name: &str) -> Option<(usize, usize)> {
201        let info = self.content.tensor_infos.get(name)?;
202        let elem_count = info.shape.elem_count();
203        let block_size = info.ggml_dtype.block_size();
204        if !elem_count.is_multiple_of(block_size) {
205            return None;
206        }
207        let size_in_bytes = elem_count / block_size * info.ggml_dtype.type_size();
208        let abs_start = (self.content.tensor_data_offset + info.offset) as usize;
209        let abs_end = abs_start.checked_add(size_in_bytes)?;
210        if abs_end > self.mmap.len() {
211            return None;
212        }
213        Some((abs_start, size_in_bytes))
214    }
215}
216
217impl std::fmt::Debug for GgufFile {
218    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219        f.debug_struct("GgufFile")
220            .field("size_bytes", &self.mmap.len())
221            .field("metadata_keys", &self.content.metadata.len())
222            .field("tensor_count", &self.content.tensor_infos.len())
223            .field("tensor_data_offset", &self.content.tensor_data_offset)
224            .finish()
225    }
226}