Skip to main content

kernel/resolution/
gguf.rs

1//! A minimal GGUF header reader: enough to pull the architecture, context
2//! length, chat-template presence, and file type without loading the weights.
3//!
4//! Values are little-endian. The reader streams over a buffered file handle and
5//! seeks past values it does not need, so it never reads the tensor data.
6
7use std::collections::BTreeMap;
8use std::fs::File;
9use std::io::{BufReader, Read, Seek, SeekFrom};
10use std::path::Path;
11
12use crate::resolution::format::GgufFacts;
13
14const MAX_KV_PAIRS: u64 = 512;
15const MAX_STRING_LEN: u64 = 1 << 16;
16const MAX_STRING_ARRAY_LEN: u64 = 1 << 24;
17
18const TYPE_UINT32: u32 = 4;
19const TYPE_INT32: u32 = 5;
20const TYPE_STRING: u32 = 8;
21const TYPE_ARRAY: u32 = 9;
22const TYPE_UINT64: u32 = 10;
23const TYPE_INT64: u32 = 11;
24
25/// Whether the file begins with the GGUF magic bytes.
26pub fn has_gguf_magic(path: &Path) -> bool {
27    first_four(path) == Some(*b"GGUF")
28}
29
30/// Whether the file begins with the legacy GGML magic bytes (`lmgg` is the
31/// little-endian byte order of the `ggml` magic).
32pub fn has_ggml_magic(path: &Path) -> bool {
33    first_four(path) == Some(*b"lmgg")
34}
35
36/// The `general.architecture` value from a GGUF header, if any.
37pub fn gguf_general_architecture(path: &Path) -> Option<String> {
38    gguf_facts(path)?.architecture
39}
40
41/// Read the architecture, context length, chat-template presence, and the
42/// quantization from a GGUF header. Returns `None` if the file is not a valid
43/// GGUF (v2+) header.
44pub fn gguf_facts(path: &Path) -> Option<GgufFacts> {
45    let mut reader = Reader::open(path)?;
46    if reader.read_array::<4>()? != *b"GGUF" {
47        return None;
48    }
49    let version = reader.read_u32()?;
50    if version < 2 {
51        return None;
52    }
53    let _tensor_count = reader.read_u64()?;
54    let kv_count = reader.read_u64()?;
55
56    let mut architecture: Option<String> = None;
57    let mut context_lengths: BTreeMap<String, i64> = BTreeMap::new();
58    let mut has_chat_template = false;
59    let mut file_type: Option<i64> = None;
60
61    for _ in 0..kv_count.min(MAX_KV_PAIRS) {
62        let Some(key) = reader.read_string() else {
63            break;
64        };
65        let Some(value_type) = reader.read_u32() else {
66            break;
67        };
68
69        if key == "general.architecture" {
70            if value_type == TYPE_STRING {
71                let Some(value) = reader.read_string() else {
72                    break;
73                };
74                architecture = Some(value);
75            } else if !reader.skip_value(value_type) {
76                break;
77            }
78        } else if key == "tokenizer.chat_template" {
79            has_chat_template = true;
80            if !reader.skip_value(value_type) {
81                break;
82            }
83        } else if key == "general.file_type" {
84            match read_integer(&mut reader, value_type) {
85                Some(value) => file_type = Some(value),
86                None => {
87                    if !reader.skip_value(value_type) {
88                        break;
89                    }
90                }
91            }
92        } else if key.ends_with(".context_length") {
93            match read_integer(&mut reader, value_type) {
94                Some(value) => {
95                    if value > 0 {
96                        context_lengths.insert(key, value);
97                    }
98                }
99                None => {
100                    if !reader.skip_value(value_type) {
101                        break;
102                    }
103                }
104            }
105        } else if !reader.skip_value(value_type) {
106            break;
107        }
108    }
109
110    let context_length = architecture
111        .as_ref()
112        .and_then(|arch| {
113            context_lengths
114                .get(&format!("{arch}.context_length"))
115                .copied()
116        })
117        .or_else(|| {
118            if context_lengths.len() == 1 {
119                context_lengths.values().copied().next()
120            } else {
121                None
122            }
123        });
124
125    Some(GgufFacts {
126        architecture,
127        context_length,
128        has_chat_template,
129        quantization: file_type.and_then(file_type_name).map(str::to_owned),
130    })
131}
132
133/// llama.cpp's guessed-type bit: set when the converter inferred the file type
134/// rather than being told it, and no part of the type itself.
135const FILE_TYPE_GUESSED: i64 = 1024;
136
137/// The name llama.cpp gives a `general.file_type` value (its `llama_ftype`
138/// enum); `None` for a value the table does not know, the retired ones
139/// included.
140fn file_type_name(value: i64) -> Option<&'static str> {
141    Some(match value & !FILE_TYPE_GUESSED {
142        0 => "F32",
143        1 => "F16",
144        2 => "Q4_0",
145        3 => "Q4_1",
146        7 => "Q8_0",
147        8 => "Q5_0",
148        9 => "Q5_1",
149        10 => "Q2_K",
150        11 => "Q3_K_S",
151        12 => "Q3_K_M",
152        13 => "Q3_K_L",
153        14 => "Q4_K_S",
154        15 => "Q4_K_M",
155        16 => "Q5_K_S",
156        17 => "Q5_K_M",
157        18 => "Q6_K",
158        19 => "IQ2_XXS",
159        20 => "IQ2_XS",
160        21 => "Q2_K_S",
161        22 => "IQ3_XS",
162        23 => "IQ3_XXS",
163        24 => "IQ1_S",
164        25 => "IQ4_NL",
165        26 => "IQ3_S",
166        27 => "IQ3_M",
167        28 => "IQ2_S",
168        29 => "IQ2_M",
169        30 => "IQ4_XS",
170        31 => "IQ1_M",
171        32 => "BF16",
172        36 => "TQ1_0",
173        37 => "TQ2_0",
174        38 => "MXFP4_MOE",
175        _ => return None,
176    })
177}
178
179fn first_four(path: &Path) -> Option<[u8; 4]> {
180    let mut file = File::open(path).ok()?;
181    let mut buffer = [0u8; 4];
182    file.read_exact(&mut buffer).ok()?;
183    Some(buffer)
184}
185
186fn read_integer(reader: &mut Reader, value_type: u32) -> Option<i64> {
187    match value_type {
188        TYPE_UINT32 => reader.read_u32().map(i64::from),
189        TYPE_INT32 => reader.read_i32().map(i64::from),
190        TYPE_UINT64 => reader
191            .read_u64()
192            .map(|value| value.min(i64::MAX as u64) as i64),
193        TYPE_INT64 => reader.read_i64(),
194        _ => None,
195    }
196}
197
198fn scalar_width(value_type: u32) -> Option<u64> {
199    match value_type {
200        0 | 1 | 7 => Some(1), // uint8 / int8 / bool
201        2..=3 => Some(2),     // uint16 / int16
202        4..=6 => Some(4),     // uint32 / int32 / float32
203        10..=12 => Some(8),   // uint64 / int64 / float64
204        _ => None,
205    }
206}
207
208struct Reader {
209    inner: BufReader<File>,
210}
211
212impl Reader {
213    fn open(path: &Path) -> Option<Self> {
214        Some(Self {
215            inner: BufReader::new(File::open(path).ok()?),
216        })
217    }
218
219    fn read_bytes(&mut self, count: usize) -> Option<Vec<u8>> {
220        let mut buffer = vec![0u8; count];
221        self.inner.read_exact(&mut buffer).ok()?;
222        Some(buffer)
223    }
224
225    fn read_array<const N: usize>(&mut self) -> Option<[u8; N]> {
226        let mut buffer = [0u8; N];
227        self.inner.read_exact(&mut buffer).ok()?;
228        Some(buffer)
229    }
230
231    fn read_u32(&mut self) -> Option<u32> {
232        self.read_array::<4>().map(u32::from_le_bytes)
233    }
234
235    fn read_i32(&mut self) -> Option<i32> {
236        self.read_array::<4>().map(i32::from_le_bytes)
237    }
238
239    fn read_u64(&mut self) -> Option<u64> {
240        self.read_array::<8>().map(u64::from_le_bytes)
241    }
242
243    fn read_i64(&mut self) -> Option<i64> {
244        self.read_array::<8>().map(i64::from_le_bytes)
245    }
246
247    fn read_string(&mut self) -> Option<String> {
248        let length = self.read_u64()?;
249        if length > MAX_STRING_LEN {
250            return None;
251        }
252        let bytes = self.read_bytes(length as usize)?;
253        Some(String::from_utf8_lossy(&bytes).into_owned())
254    }
255
256    fn skip(&mut self, count: u64) -> bool {
257        if count == 0 {
258            return true;
259        }
260        if count > i64::MAX as u64 {
261            return false;
262        }
263        self.inner.seek(SeekFrom::Current(count as i64)).is_ok()
264    }
265
266    fn skip_value(&mut self, value_type: u32) -> bool {
267        if let Some(width) = scalar_width(value_type) {
268            return self.skip(width);
269        }
270        match value_type {
271            TYPE_STRING => match self.read_u64() {
272                Some(length) => self.skip(length),
273                None => false,
274            },
275            TYPE_ARRAY => self.skip_array(),
276            _ => false,
277        }
278    }
279
280    fn skip_array(&mut self) -> bool {
281        let Some(element_type) = self.read_u32() else {
282            return false;
283        };
284        let Some(count) = self.read_u64() else {
285            return false;
286        };
287        if let Some(width) = scalar_width(element_type) {
288            return match count.checked_mul(width) {
289                Some(total) => self.skip(total),
290                None => false,
291            };
292        }
293        if element_type != TYPE_STRING || count > MAX_STRING_ARRAY_LEN {
294            return false;
295        }
296        for _ in 0..count {
297            let Some(length) = self.read_u64() else {
298                return false;
299            };
300            if !self.skip(length) {
301                return false;
302            }
303        }
304        true
305    }
306}