1use std::path::Path;
58
59use indexmap::IndexMap;
60use kopitiam_core::{DType, Error, Result, Shape};
61
62use crate::byte_source::ByteSource;
63use crate::metadata::{GgufMetadata, GgufValue, ModelMetadata};
64use crate::model::{LoadedModel, ModelLoader, TensorEntry};
65
66const FORMAT: &str = "gguf";
67const MAGIC: [u8; 4] = *b"GGUF";
68const DEFAULT_ALIGNMENT: u64 = 32;
69const MAX_TENSOR_DIMS: u32 = 8;
74const MAX_ARRAY_NESTING_DEPTH: u32 = 32;
79
80fn malformed(reason: impl Into<String>) -> Error {
81 Error::MalformedModel { format: FORMAT, reason: reason.into() }
82}
83
84fn unsupported(feature: impl Into<String>) -> Error {
85 Error::UnsupportedModelFeature { format: FORMAT, feature: feature.into() }
86}
87
88struct Cursor<'a> {
97 bytes: &'a [u8],
98 pos: usize,
99}
100
101impl<'a> Cursor<'a> {
102 fn new(bytes: &'a [u8]) -> Self {
103 Self { bytes, pos: 0 }
104 }
105
106 fn position(&self) -> usize {
107 self.pos
108 }
109
110 fn take(&mut self, n: usize) -> Result<&'a [u8]> {
111 let end = self
112 .pos
113 .checked_add(n)
114 .ok_or_else(|| malformed("byte offset overflow while reading"))?;
115 let slice = self.bytes.get(self.pos..end).ok_or_else(|| {
116 malformed(format!(
117 "unexpected end of file: needed {n} bytes at offset {}, file has {} bytes",
118 self.pos,
119 self.bytes.len()
120 ))
121 })?;
122 self.pos = end;
123 Ok(slice)
124 }
125
126 fn u8(&mut self) -> Result<u8> {
127 Ok(self.take(1)?[0])
128 }
129
130 fn i8(&mut self) -> Result<i8> {
131 Ok(self.take(1)?[0] as i8)
132 }
133
134 fn u16(&mut self) -> Result<u16> {
135 Ok(u16::from_le_bytes(self.take(2)?.try_into().expect("take(2) returns 2 bytes")))
136 }
137
138 fn i16(&mut self) -> Result<i16> {
139 Ok(i16::from_le_bytes(self.take(2)?.try_into().expect("take(2) returns 2 bytes")))
140 }
141
142 fn u32(&mut self) -> Result<u32> {
143 Ok(u32::from_le_bytes(self.take(4)?.try_into().expect("take(4) returns 4 bytes")))
144 }
145
146 fn i32(&mut self) -> Result<i32> {
147 Ok(i32::from_le_bytes(self.take(4)?.try_into().expect("take(4) returns 4 bytes")))
148 }
149
150 fn u64(&mut self) -> Result<u64> {
151 Ok(u64::from_le_bytes(self.take(8)?.try_into().expect("take(8) returns 8 bytes")))
152 }
153
154 fn i64(&mut self) -> Result<i64> {
155 Ok(i64::from_le_bytes(self.take(8)?.try_into().expect("take(8) returns 8 bytes")))
156 }
157
158 fn f32(&mut self) -> Result<f32> {
159 Ok(f32::from_le_bytes(self.take(4)?.try_into().expect("take(4) returns 4 bytes")))
160 }
161
162 fn f64(&mut self) -> Result<f64> {
163 Ok(f64::from_le_bytes(self.take(8)?.try_into().expect("take(8) returns 8 bytes")))
164 }
165
166 fn bool(&mut self) -> Result<bool> {
167 match self.u8()? {
168 0 => Ok(false),
169 1 => Ok(true),
170 other => Err(malformed(format!(
171 "invalid bool byte {other:#04x} (must be 0x00 or 0x01)"
172 ))),
173 }
174 }
175
176 fn string(&mut self) -> Result<String> {
179 let len = self.u64()?;
180 let len = usize::try_from(len)
181 .map_err(|_| malformed(format!("string length {len} does not fit in memory")))?;
182 let bytes = self.take(len)?;
187 String::from_utf8(bytes.to_vec())
188 .map_err(|_| malformed("metadata string is not valid UTF-8"))
189 }
190}
191
192fn read_value(cursor: &mut Cursor, value_type: u32, depth: u32) -> Result<GgufValue> {
197 match value_type {
198 0 => Ok(GgufValue::U8(cursor.u8()?)),
199 1 => Ok(GgufValue::I8(cursor.i8()?)),
200 2 => Ok(GgufValue::U16(cursor.u16()?)),
201 3 => Ok(GgufValue::I16(cursor.i16()?)),
202 4 => Ok(GgufValue::U32(cursor.u32()?)),
203 5 => Ok(GgufValue::I32(cursor.i32()?)),
204 6 => Ok(GgufValue::F32(cursor.f32()?)),
205 7 => Ok(GgufValue::Bool(cursor.bool()?)),
206 8 => Ok(GgufValue::String(cursor.string()?)),
207 9 => {
208 if depth >= MAX_ARRAY_NESTING_DEPTH {
209 return Err(malformed("array nesting exceeds sanity limit"));
210 }
211 let elem_type = cursor.u32()?;
212 let len = cursor.u64()?;
213 let len = usize::try_from(len)
214 .map_err(|_| malformed(format!("array length {len} does not fit in memory")))?;
215 let mut values = Vec::with_capacity(len.min(1024));
220 for _ in 0..len {
221 values.push(read_value(cursor, elem_type, depth + 1)?);
222 }
223 Ok(GgufValue::Array(values))
224 }
225 10 => Ok(GgufValue::U64(cursor.u64()?)),
226 11 => Ok(GgufValue::I64(cursor.i64()?)),
227 12 => Ok(GgufValue::F64(cursor.f64()?)),
228 other => Err(unsupported(format!("metadata value type id {other}"))),
229 }
230}
231
232fn dtype_from_ggml_type(ggml_type: u32) -> Result<DType> {
245 match ggml_type {
246 0 => Ok(DType::F32),
247 1 => Ok(DType::F16),
248 2 => Ok(DType::Q4_0),
249 3 => Ok(DType::Q4_1),
250 6 => Ok(DType::Q5_0),
251 7 => Ok(DType::Q5_1),
252 8 => Ok(DType::Q8_0),
253 10 => Ok(DType::Q2_K),
254 11 => Ok(DType::Q3_K),
255 12 => Ok(DType::Q4_K),
256 13 => Ok(DType::Q5_K),
257 14 => Ok(DType::Q6_K),
258 15 => Ok(DType::Q8_K),
259 30 => Ok(DType::BF16),
260 other => Err(unsupported(format!("ggml tensor type id {other}"))),
261 }
262}
263
264fn align_up(offset: u64, alignment: u64) -> Result<u64> {
267 if alignment == 0 {
268 return Err(malformed("general.alignment must not be zero"));
269 }
270 let remainder = offset % alignment;
271 if remainder == 0 {
272 return Ok(offset);
273 }
274 offset
275 .checked_add(alignment - remainder)
276 .ok_or_else(|| malformed("alignment padding overflows a u64 offset"))
277}
278
279struct RawTensorInfo {
284 name: String,
285 dims: Vec<u64>,
287 ggml_type: u32,
288 relative_offset: u64,
290}
291
292fn parse(source: ByteSource) -> Result<LoadedModel> {
294 let bytes = source.as_slice();
295 let mut cursor = Cursor::new(bytes);
296
297 let magic = cursor.take(4)?;
298 if magic != MAGIC {
299 return Err(malformed(format!(
300 "bad magic {magic:02x?}, expected {MAGIC:02x?} (\"GGUF\")"
301 )));
302 }
303
304 let version = cursor.u32()?;
305 if version != 2 && version != 3 {
306 return Err(unsupported(format!(
307 "gguf version {version} (only v2 and v3 are supported; see module docs re: v1)"
308 )));
309 }
310
311 let tensor_count = cursor.u64()?;
312 let metadata_kv_count = cursor.u64()?;
313
314 let mut kv_map = IndexMap::new();
315 for _ in 0..metadata_kv_count {
316 let key = cursor.string()?;
317 let value_type = cursor.u32()?;
318 let value = read_value(&mut cursor, value_type, 0)?;
319 if kv_map.insert(key.clone(), value).is_some() {
320 return Err(malformed(format!("duplicate metadata key {key:?}")));
326 }
327 }
328 let kv = GgufMetadata(kv_map);
329
330 let mut raw_infos = Vec::new();
331 for _ in 0..tensor_count {
332 let name = cursor.string()?;
333 let n_dims = cursor.u32()?;
334 if n_dims > MAX_TENSOR_DIMS {
335 return Err(malformed(format!(
336 "tensor {name:?} declares {n_dims} dimensions, more than the {MAX_TENSOR_DIMS} this loader accepts"
337 )));
338 }
339 let mut dims = Vec::with_capacity(n_dims as usize);
340 for _ in 0..n_dims {
341 dims.push(cursor.u64()?);
342 }
343 let ggml_type = cursor.u32()?;
344 let relative_offset = cursor.u64()?;
345 raw_infos.push(RawTensorInfo { name, dims, ggml_type, relative_offset });
346 }
347
348 let alignment = kv
352 .get_u32("general.alignment")
353 .map(u64::from)
354 .unwrap_or(DEFAULT_ALIGNMENT);
355 if alignment == 0 || !alignment.is_multiple_of(8) {
356 return Err(malformed(format!(
357 "general.alignment {alignment} must be a positive multiple of 8"
358 )));
359 }
360
361 let tensor_data_start = align_up(cursor.position() as u64, alignment)?;
362 let file_len = bytes.len() as u64;
363
364 let mut tensors = IndexMap::new();
365 for info in raw_infos {
366 let dtype = dtype_from_ggml_type(info.ggml_type)?;
367
368 let mut dims = Vec::with_capacity(info.dims.len());
372 for &d in info.dims.iter().rev() {
373 let d = usize::try_from(d).map_err(|_| {
374 malformed(format!(
375 "tensor {:?} has a dimension ({d}) too large to represent",
376 info.name
377 ))
378 })?;
379 dims.push(d);
380 }
381 let shape = Shape::new(dims);
382
383 let elem_count = shape.elem_count();
384 let byte_len = dtype.storage_bytes(elem_count).ok_or(Error::PartialQuantizedBlock {
385 dtype,
386 count: elem_count,
387 block_size: dtype.block_size(),
388 })?;
389
390 if !info.relative_offset.is_multiple_of(alignment) {
391 return Err(malformed(format!(
392 "tensor {:?} offset {} is not a multiple of alignment {alignment}",
393 info.name, info.relative_offset
394 )));
395 }
396
397 let abs_offset = tensor_data_start.checked_add(info.relative_offset).ok_or_else(|| {
398 malformed(format!("tensor {:?} data offset overflows a u64", info.name))
399 })?;
400 let abs_end = abs_offset.checked_add(byte_len as u64).ok_or_else(|| {
401 malformed(format!("tensor {:?} data end offset overflows a u64", info.name))
402 })?;
403 if abs_end > file_len {
404 return Err(malformed(format!(
405 "tensor {:?} data range [{abs_offset}, {abs_end}) extends past end of file ({file_len} bytes)",
406 info.name
407 )));
408 }
409 let abs_offset = usize::try_from(abs_offset)
410 .map_err(|_| malformed(format!("tensor {:?} offset does not fit in memory", info.name)))?;
411
412 let entry = TensorEntry {
413 name: info.name.clone(),
414 dtype,
415 shape,
416 offset: abs_offset,
417 len: byte_len,
418 };
419 if tensors.insert(info.name.clone(), entry).is_some() {
420 return Err(malformed(format!("duplicate tensor name {:?}", info.name)));
421 }
422 }
423
424 let metadata = build_metadata(kv);
425
426 Ok(LoadedModel { metadata, tensors, source, format: FORMAT })
427}
428
429fn build_metadata(kv: GgufMetadata) -> ModelMetadata {
434 let architecture = kv.get_str("general.architecture").map(str::to_owned);
435 let name = kv.get_str("general.name").map(str::to_owned);
436
437 let prefixed = |suffix: &str| architecture.as_deref().map(|arch| format!("{arch}.{suffix}"));
442
443 let n_layers = prefixed("block_count").and_then(|k| kv.get_u64(&k));
444 let n_heads = prefixed("attention.head_count").and_then(|k| kv.get_u64(&k));
445 let n_kv_heads = prefixed("attention.head_count_kv").and_then(|k| kv.get_u64(&k));
446 let embedding_length = prefixed("embedding_length").and_then(|k| kv.get_u64(&k));
447 let feed_forward_length = prefixed("feed_forward_length").and_then(|k| kv.get_u64(&k));
448 let context_length = prefixed("context_length").and_then(|k| kv.get_u64(&k));
449 let rope_theta = prefixed("rope.freq_base").and_then(|k| kv.get_f32(&k));
450 let rope_dimension_count = prefixed("rope.dimension_count").and_then(|k| kv.get_u64(&k));
451 let norm_epsilon = prefixed("attention.layer_norm_rms_epsilon")
452 .and_then(|k| kv.get_f32(&k))
453 .or_else(|| prefixed("attention.layer_norm_epsilon").and_then(|k| kv.get_f32(&k)));
454
455 let vocab_size = kv
459 .get_array("tokenizer.ggml.tokens")
460 .map(|tokens| tokens.len() as u64)
461 .or_else(|| prefixed("vocab_size").and_then(|k| kv.get_u64(&k)));
462
463 let quantization_version = kv.get_u32("general.quantization_version");
464 let file_type = kv.get_u32("general.file_type");
465
466 ModelMetadata {
467 architecture,
468 name,
469 n_layers,
470 n_heads,
471 n_kv_heads,
472 embedding_length,
473 feed_forward_length,
474 context_length,
475 vocab_size,
476 rope_theta,
477 rope_dimension_count,
478 norm_epsilon,
479 quantization_version,
480 file_type,
481 raw: kv,
482 }
483}
484
485pub struct GgufLoader;
488
489impl ModelLoader for GgufLoader {
490 fn format_name(&self) -> &'static str {
491 FORMAT
492 }
493
494 fn probe(&self, bytes: &[u8]) -> bool {
495 bytes.len() >= MAGIC.len() && bytes[..MAGIC.len()] == MAGIC
496 }
497
498 fn load(&self, path: &Path) -> Result<LoadedModel> {
499 let source = ByteSource::open(path)?;
500 parse(source)
501 }
502}