Skip to main content

trueno/inference/
gguf.rs

1//! GGUF file reader — loads tensor data for inference.
2//!
3//! Reads GGUF v3 files (llama.cpp compatible). Parses header, metadata,
4//! tensor info, then memory-maps or reads tensor data bytes.
5//!
6//! # Format
7//!
8//! ```text
9//! [magic: u32] [version: u32] [tensor_count: u64] [metadata_kv_count: u64]
10//! [metadata KV pairs...]
11//! [tensor info entries...]
12//! [alignment padding]
13//! [tensor data (contiguous)]
14//! ```
15
16use std::collections::HashMap;
17use std::io::{self, Read, Seek};
18use std::path::Path;
19
20use crate::error::TruenoError;
21
22const GGUF_MAGIC: u32 = 0x4655_4747; // "GGUF" in little-endian
23
24// PMAT-3430 M1 Phase 3: the enum that used to be declared here is now the
25// workspace's ONE ggml tensor-type enum, re-exported from the leaf crate.
26//
27// compute's copy listed 15 ids and carried the block geometry for them. Every
28// one of those 15 rows agreed with upstream ggml exactly — it was the only one
29// of the three in-tree enums that did — so nothing about sizes changes here;
30// `block_bytes`, `block_size` and `tensor_bytes` now read the same numbers out
31// of `TRAITS`, which is extracted from ggml at the pinned commit rather than
32// typed. `crates/aprender-compute/src/inference/gguf_characterization_tests.rs`
33// asserts all 15 rows and the rounding are unchanged.
34pub use trueno_quant::GgmlType;
35
36/// The ids compute ACCEPTS at its GGUF parse boundary.
37///
38/// The leaf knows all 35 live ggml types; this crate knew 15 and must keep
39/// knowing exactly 15 (#3430 Q1-c: M1 changes no crate's admitted set, because
40/// widening one silently is how a loader starts decoding bytes it has no kernel
41/// for). Adding a row here is a deliberate act with its own ticket, not a
42/// side effect of the enum growing.
43const ADMITTED: [GgmlType; 15] = [
44    GgmlType::F32,
45    GgmlType::F16,
46    GgmlType::Q4_0,
47    GgmlType::Q4_1,
48    GgmlType::Q5_0,
49    GgmlType::Q5_1,
50    GgmlType::Q8_0,
51    GgmlType::Q8_1,
52    GgmlType::Q2K,
53    GgmlType::Q3K,
54    GgmlType::Q4K,
55    GgmlType::Q5K,
56    GgmlType::Q6K,
57    GgmlType::Q8K,
58    GgmlType::BF16,
59];
60
61/// The GGUF parse boundary: an id becomes a type only if compute admits it.
62///
63/// This was `GgmlType::from_u32`, an inherent method. An inherent method cannot
64/// follow a type that is now defined in another crate, so it survives as a free
65/// function under the same name — the one call-site change #3430 Q1-c named in
66/// advance (`GgmlType::from_u32(x)` -> `from_u32(x)`).
67fn from_u32(v: u32) -> Option<GgmlType> {
68    let t = GgmlType::from_id(v)?;
69    let mut i = 0;
70    while i < ADMITTED.len() {
71        if ADMITTED[i] as u32 == t as u32 {
72            return Some(t);
73        }
74        i += 1;
75    }
76    None
77}
78
79/// Info about a single tensor in the GGUF file.
80#[derive(Debug, Clone)]
81pub struct TensorInfo {
82    pub name: String,
83    pub dtype: GgmlType,
84    pub dims: Vec<u64>,
85    /// Offset from start of data section (NOT from file start).
86    pub offset: u64,
87}
88
89impl TensorInfo {
90    pub fn n_elements(&self) -> u64 {
91        self.dims.iter().product::<u64>().max(1)
92    }
93
94    pub fn byte_size(&self) -> usize {
95        self.dtype.tensor_bytes(self.n_elements() as usize)
96    }
97}
98
99/// Parsed GGUF file ready for tensor extraction.
100pub struct GgufFile {
101    pub tensor_count: u64,
102    pub metadata: HashMap<String, MetadataValue>,
103    pub tensors: Vec<TensorInfo>,
104    /// Offset in bytes from file start where tensor data begins.
105    pub data_offset: u64,
106    /// Raw file bytes (memory mapped or loaded).
107    data: Vec<u8>,
108}
109
110#[derive(Debug, Clone)]
111pub enum MetadataValue {
112    U8(u8),
113    I8(i8),
114    U16(u16),
115    I16(i16),
116    U32(u32),
117    I32(i32),
118    U64(u64),
119    I64(i64),
120    F32(f32),
121    F64(f64),
122    Bool(bool),
123    String(String),
124    Array(Vec<MetadataValue>),
125}
126
127impl MetadataValue {
128    pub fn as_u32(&self) -> Option<u32> {
129        match self {
130            Self::U32(v) => Some(*v),
131            Self::U64(v) => Some(*v as u32),
132            Self::I32(v) => Some(*v as u32),
133            _ => None,
134        }
135    }
136
137    pub fn as_f32(&self) -> Option<f32> {
138        match self {
139            Self::F32(v) => Some(*v),
140            Self::F64(v) => Some(*v as f32),
141            _ => None,
142        }
143    }
144
145    pub fn as_str(&self) -> Option<&str> {
146        match self {
147            Self::String(s) => Some(s),
148            _ => None,
149        }
150    }
151}
152
153impl GgufFile {
154    /// Load and parse a GGUF file.
155    pub fn load(path: &Path) -> Result<Self, TruenoError> {
156        let data = std::fs::read(path).map_err(|e| {
157            TruenoError::InvalidInput(format!("Failed to read GGUF file {}: {e}", path.display()))
158        })?;
159
160        Self::parse(data)
161    }
162
163    /// Parse GGUF from raw bytes.
164    pub fn parse(data: Vec<u8>) -> Result<Self, TruenoError> {
165        let mut cursor = io::Cursor::new(&data);
166
167        // Header
168        let magic = read_u32(&mut cursor)?;
169        if magic != GGUF_MAGIC {
170            return Err(TruenoError::InvalidInput(format!(
171                "Not a GGUF file: magic=0x{magic:08x}, expected 0x{GGUF_MAGIC:08x}"
172            )));
173        }
174        let version = read_u32(&mut cursor)?;
175        if !(2..=3).contains(&version) {
176            return Err(TruenoError::InvalidInput(format!(
177                "Unsupported GGUF version {version} (need 2 or 3)"
178            )));
179        }
180        let tensor_count = read_u64(&mut cursor)?;
181        let metadata_kv_count = read_u64(&mut cursor)?;
182
183        // Metadata
184        let mut metadata = HashMap::new();
185        for _ in 0..metadata_kv_count {
186            let key = read_gguf_string(&mut cursor)?;
187            let value = read_metadata_value(&mut cursor)?;
188            metadata.insert(key, value);
189        }
190
191        // Tensor info
192        let mut tensors = Vec::with_capacity(tensor_count as usize);
193        for _ in 0..tensor_count {
194            let name = read_gguf_string(&mut cursor)?;
195            let n_dims = read_u32(&mut cursor)? as usize;
196            let mut dims = Vec::with_capacity(n_dims);
197            for _ in 0..n_dims {
198                dims.push(read_u64(&mut cursor)?);
199            }
200            let dtype_u32 = read_u32(&mut cursor)?;
201            let dtype = from_u32(dtype_u32).ok_or_else(|| {
202                TruenoError::InvalidInput(format!(
203                    "Unknown GGML type {dtype_u32} for tensor '{name}'"
204                ))
205            })?;
206            let offset = read_u64(&mut cursor)?;
207            tensors.push(TensorInfo { name, dtype, dims, offset });
208        }
209
210        // Data section starts at next alignment boundary (default 32 bytes)
211        let alignment =
212            metadata.get("general.alignment").and_then(|v| v.as_u32()).unwrap_or(32) as u64;
213        let pos = cursor.position();
214        let data_offset = (pos + alignment - 1) / alignment * alignment;
215
216        Ok(Self { tensor_count, metadata, tensors, data_offset, data })
217    }
218
219    /// Get raw bytes for a tensor by name.
220    pub fn tensor_data(&self, name: &str) -> Option<&[u8]> {
221        let info = self.tensors.iter().find(|t| t.name == name)?;
222        let start = self.data_offset as usize + info.offset as usize;
223        let end = start + info.byte_size();
224        if end <= self.data.len() {
225            Some(&self.data[start..end])
226        } else {
227            None
228        }
229    }
230
231    /// Get tensor info by name.
232    pub fn tensor_info(&self, name: &str) -> Option<&TensorInfo> {
233        self.tensors.iter().find(|t| t.name == name)
234    }
235
236    /// Get a metadata string value.
237    pub fn meta_str(&self, key: &str) -> Option<&str> {
238        self.metadata.get(key)?.as_str()
239    }
240
241    /// Get a metadata u32 value.
242    pub fn meta_u32(&self, key: &str) -> Option<u32> {
243        self.metadata.get(key)?.as_u32()
244    }
245
246    /// Get a metadata f32 value.
247    pub fn meta_f32(&self, key: &str) -> Option<f32> {
248        self.metadata.get(key)?.as_f32()
249    }
250}
251
252// ── Binary readers ──
253
254fn read_u8<R: Read>(r: &mut R) -> Result<u8, TruenoError> {
255    let mut buf = [0u8; 1];
256    r.read_exact(&mut buf)
257        .map_err(|e| TruenoError::InvalidInput(format!("GGUF read error: {e}")))?;
258    Ok(buf[0])
259}
260
261fn read_u16<R: Read>(r: &mut R) -> Result<u16, TruenoError> {
262    let mut buf = [0u8; 2];
263    r.read_exact(&mut buf)
264        .map_err(|e| TruenoError::InvalidInput(format!("GGUF read error: {e}")))?;
265    Ok(u16::from_le_bytes(buf))
266}
267
268fn read_u32<R: Read>(r: &mut R) -> Result<u32, TruenoError> {
269    let mut buf = [0u8; 4];
270    r.read_exact(&mut buf)
271        .map_err(|e| TruenoError::InvalidInput(format!("GGUF read error: {e}")))?;
272    Ok(u32::from_le_bytes(buf))
273}
274
275fn read_i32<R: Read>(r: &mut R) -> Result<i32, TruenoError> {
276    let mut buf = [0u8; 4];
277    r.read_exact(&mut buf)
278        .map_err(|e| TruenoError::InvalidInput(format!("GGUF read error: {e}")))?;
279    Ok(i32::from_le_bytes(buf))
280}
281
282fn read_u64<R: Read>(r: &mut R) -> Result<u64, TruenoError> {
283    let mut buf = [0u8; 8];
284    r.read_exact(&mut buf)
285        .map_err(|e| TruenoError::InvalidInput(format!("GGUF read error: {e}")))?;
286    Ok(u64::from_le_bytes(buf))
287}
288
289fn read_i64<R: Read>(r: &mut R) -> Result<i64, TruenoError> {
290    let mut buf = [0u8; 8];
291    r.read_exact(&mut buf)
292        .map_err(|e| TruenoError::InvalidInput(format!("GGUF read error: {e}")))?;
293    Ok(i64::from_le_bytes(buf))
294}
295
296fn read_f32_val<R: Read>(r: &mut R) -> Result<f32, TruenoError> {
297    let mut buf = [0u8; 4];
298    r.read_exact(&mut buf)
299        .map_err(|e| TruenoError::InvalidInput(format!("GGUF read error: {e}")))?;
300    Ok(f32::from_le_bytes(buf))
301}
302
303fn read_f64_val<R: Read>(r: &mut R) -> Result<f64, TruenoError> {
304    let mut buf = [0u8; 8];
305    r.read_exact(&mut buf)
306        .map_err(|e| TruenoError::InvalidInput(format!("GGUF read error: {e}")))?;
307    Ok(f64::from_le_bytes(buf))
308}
309
310fn read_gguf_string<R: Read>(r: &mut R) -> Result<String, TruenoError> {
311    let len = read_u64(r)? as usize;
312    if len > 1_000_000 {
313        return Err(TruenoError::InvalidInput(format!("GGUF string too long: {len}")));
314    }
315    let mut buf = vec![0u8; len];
316    r.read_exact(&mut buf)
317        .map_err(|e| TruenoError::InvalidInput(format!("GGUF string read error: {e}")))?;
318    String::from_utf8(buf)
319        .map_err(|e| TruenoError::InvalidInput(format!("GGUF string not UTF-8: {e}")))
320}
321
322fn read_metadata_value<R: Read + Seek>(r: &mut R) -> Result<MetadataValue, TruenoError> {
323    let value_type = read_u32(r)?;
324    match value_type {
325        0 => Ok(MetadataValue::U8(read_u8(r)?)),
326        1 => Ok(MetadataValue::I8(read_u8(r)? as i8)),
327        2 => Ok(MetadataValue::U16(read_u16(r)?)),
328        3 => Ok(MetadataValue::I16(read_u16(r)? as i16)),
329        4 => Ok(MetadataValue::U32(read_u32(r)?)),
330        5 => Ok(MetadataValue::I32(read_i32(r)?)),
331        6 => Ok(MetadataValue::F32(read_f32_val(r)?)),
332        7 => Ok(MetadataValue::Bool(read_u8(r)? != 0)),
333        8 => Ok(MetadataValue::String(read_gguf_string(r)?)),
334        9 => {
335            // Array
336            let elem_type = read_u32(r)?;
337            let count = read_u64(r)? as usize;
338            if count > 10_000_000 {
339                return Err(TruenoError::InvalidInput(format!("GGUF array too large: {count}")));
340            }
341            let mut items = Vec::with_capacity(count.min(1024));
342            for _ in 0..count {
343                // Read elements of the declared type
344                let item = match elem_type {
345                    0 => MetadataValue::U8(read_u8(r)?),
346                    1 => MetadataValue::I8(read_u8(r)? as i8),
347                    4 => MetadataValue::U32(read_u32(r)?),
348                    5 => MetadataValue::I32(read_i32(r)?),
349                    6 => MetadataValue::F32(read_f32_val(r)?),
350                    8 => MetadataValue::String(read_gguf_string(r)?),
351                    10 => MetadataValue::U64(read_u64(r)?),
352                    11 => MetadataValue::I64(read_i64(r)?),
353                    12 => MetadataValue::F64(read_f64_val(r)?),
354                    _ => {
355                        return Err(TruenoError::InvalidInput(format!(
356                            "Unsupported GGUF array element type {elem_type}"
357                        )))
358                    }
359                };
360                items.push(item);
361            }
362            Ok(MetadataValue::Array(items))
363        }
364        10 => Ok(MetadataValue::U64(read_u64(r)?)),
365        11 => Ok(MetadataValue::I64(read_i64(r)?)),
366        12 => Ok(MetadataValue::F64(read_f64_val(r)?)),
367        _ => Err(TruenoError::InvalidInput(format!("Unknown GGUF metadata type {value_type}"))),
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn test_ggml_type_q4k_properties() {
377        let q4k = GgmlType::Q4K;
378        assert_eq!(q4k.block_size(), 256);
379        assert_eq!(q4k.block_bytes(), 144);
380        // 4096 weights = 16 blocks × 144 bytes = 2304
381        assert_eq!(q4k.tensor_bytes(4096), 2304);
382    }
383
384    #[test]
385    fn test_ggml_type_f32_properties() {
386        let f32t = GgmlType::F32;
387        assert_eq!(f32t.block_size(), 1);
388        assert_eq!(f32t.block_bytes(), 4);
389        assert_eq!(f32t.tensor_bytes(1024), 4096);
390    }
391
392    #[test]
393    fn test_gguf_magic_check() {
394        let bad_data = vec![0u8; 32];
395        let result = GgufFile::parse(bad_data);
396        assert!(result.is_err());
397    }
398
399    #[test]
400    fn test_minimal_gguf() {
401        // Build a minimal valid GGUF v3 with 0 tensors, 0 metadata
402        let mut data = Vec::new();
403        data.extend_from_slice(&GGUF_MAGIC.to_le_bytes()); // magic
404        data.extend_from_slice(&3u32.to_le_bytes()); // version
405        data.extend_from_slice(&0u64.to_le_bytes()); // tensor_count
406        data.extend_from_slice(&0u64.to_le_bytes()); // metadata_kv_count
407                                                     // Pad to 32-byte alignment
408        data.resize(32, 0);
409
410        let file = GgufFile::parse(data).expect("valid minimal GGUF");
411        assert_eq!(file.tensor_count, 0);
412        assert_eq!(file.tensors.len(), 0);
413    }
414}
415
416// PMAT-3430 Q1-c: the characterization snapshot for compute's one id boundary
417// and its block geometry. A child module, so it reaches the private `from_u32`
418// without widening it. This `mod` line is the only non-test edit Phase 1 makes.
419#[cfg(test)]
420#[path = "gguf_characterization_tests.rs"]
421mod gguf_characterization_tests;