Skip to main content

mlx_native/gguf/
mod.rs

1//! GGUF v3 file format parser.
2//!
3//! Parses GGUF headers, metadata, and tensor info on open.  Tensor data is
4//! loaded lazily on demand into [`MlxBuffer`]s — either as raw GGML blocks
5//! (for GPU quantized matmul) or dequantized to F32 (for norm weights etc.).
6//!
7//! # Example
8//!
9//! ```ignore
10//! use mlx_native::gguf::GgufFile;
11//! use std::path::Path;
12//!
13//! let gguf = GgufFile::open(Path::new("model.gguf"))?;
14//! let names = gguf.tensor_names();
15//! let buf = gguf.load_tensor("blk.0.attn_q.weight", &device)?;
16//! let norm = gguf.load_tensor_f32("blk.0.attn_norm.weight", &device)?;
17//! ```
18
19use std::collections::HashMap;
20use std::io::{BufReader, Read, Seek, SeekFrom};
21use std::path::Path;
22use std::sync::Mutex;
23
24use half::f16;
25
26use crate::ops::quantized_matmul_ggml::GgmlType;
27use crate::{DType, MlxBuffer, MlxBufferPool, MlxDevice, MlxError, Result};
28
29// ---------------------------------------------------------------------------
30// GGUF constants
31// ---------------------------------------------------------------------------
32
33/// GGUF magic number: "GGUF" as little-endian u32 (bytes: 0x47 0x47 0x55 0x46).
34const GGUF_MAGIC: u32 = 0x4655_4747;
35
36/// GGUF version we support.
37const GGUF_VERSION: u32 = 3;
38
39/// Default alignment for the tensor data section.
40const GGUF_DEFAULT_ALIGNMENT: u64 = 32;
41
42/// Metadata key that overrides the default alignment.
43const GGUF_ALIGNMENT_KEY: &str = "general.alignment";
44
45// ---------------------------------------------------------------------------
46// GGUF metadata value type IDs
47// ---------------------------------------------------------------------------
48
49const GGUF_TYPE_UINT8: u32 = 0;
50const GGUF_TYPE_INT8: u32 = 1;
51const GGUF_TYPE_UINT16: u32 = 2;
52const GGUF_TYPE_INT16: u32 = 3;
53const GGUF_TYPE_UINT32: u32 = 4;
54const GGUF_TYPE_INT32: u32 = 5;
55const GGUF_TYPE_FLOAT32: u32 = 6;
56const GGUF_TYPE_BOOL: u32 = 7;
57const GGUF_TYPE_STRING: u32 = 8;
58const GGUF_TYPE_ARRAY: u32 = 9;
59const GGUF_TYPE_UINT64: u32 = 10;
60const GGUF_TYPE_INT64: u32 = 11;
61const GGUF_TYPE_FLOAT64: u32 = 12;
62
63// ---------------------------------------------------------------------------
64// GGML type IDs (from ggml.h)
65// ---------------------------------------------------------------------------
66
67const GGML_TYPE_F32: u32 = 0;
68const GGML_TYPE_F16: u32 = 1;
69const GGML_TYPE_Q4_0: u32 = 2;
70const GGML_TYPE_Q5_1: u32 = 7;
71const GGML_TYPE_Q8_0: u32 = 8;
72const GGML_TYPE_Q4_K: u32 = 12;
73const GGML_TYPE_Q5_K: u32 = 13;
74const GGML_TYPE_Q6_K: u32 = 14;
75const GGML_TYPE_I16: u32 = 17;
76const GGML_TYPE_IQ4_NL: u32 = 20;
77const GGML_TYPE_IQ4_XS: u32 = 23;
78
79/// IQ4_NL non-linear codebook constants. 16 signed entries selected by
80/// 4-bit indices in `block_iq4_nl::qs`. Verified byte-equal with
81/// `/opt/llama.cpp/ggml/src/ggml-common.h:1109-1112`. ADR-022 Phase 1.
82const KVALUES_IQ4_NL: [i8; 16] = [
83    -127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113,
84];
85
86// ---------------------------------------------------------------------------
87// Public types
88// ---------------------------------------------------------------------------
89
90/// GGUF metadata value types.
91#[derive(Debug, Clone)]
92pub enum MetadataValue {
93    Uint8(u8),
94    Int8(i8),
95    Uint16(u16),
96    Int16(i16),
97    Uint32(u32),
98    Int32(i32),
99    Float32(f32),
100    Bool(bool),
101    String(String),
102    Array(Vec<MetadataValue>),
103    Uint64(u64),
104    Int64(i64),
105    Float64(f64),
106}
107
108impl MetadataValue {
109    /// Try to interpret this value as a string reference.
110    pub fn as_str(&self) -> Option<&str> {
111        match self {
112            MetadataValue::String(s) => Some(s.as_str()),
113            _ => None,
114        }
115    }
116
117    /// Try to interpret this value as a u32.
118    pub fn as_u32(&self) -> Option<u32> {
119        match self {
120            MetadataValue::Uint32(v) => Some(*v),
121            MetadataValue::Uint8(v) => Some(*v as u32),
122            MetadataValue::Uint16(v) => Some(*v as u32),
123            MetadataValue::Int32(v) if *v >= 0 => Some(*v as u32),
124            _ => None,
125        }
126    }
127
128    /// Try to interpret this value as an f32.
129    pub fn as_f32(&self) -> Option<f32> {
130        match self {
131            MetadataValue::Float32(v) => Some(*v),
132            MetadataValue::Float64(v) => Some(*v as f32),
133            _ => None,
134        }
135    }
136}
137
138/// Information about a single tensor in the GGUF file.
139#[derive(Debug, Clone)]
140pub struct TensorInfo {
141    /// Tensor name (e.g. "blk.0.attn_q.weight").
142    pub name: String,
143    /// Tensor shape, innermost dimension first (as stored in GGUF).
144    pub shape: Vec<usize>,
145    /// GGML quantization type.
146    pub ggml_type: GgmlType,
147    /// Byte offset relative to the start of the tensor data section.
148    pub offset: u64,
149    /// Total byte length of this tensor's data.
150    pub byte_len: usize,
151}
152
153/// A parsed GGUF file, ready for lazy tensor loading.
154///
155/// The file is kept open so that tensor data can be read on demand via
156/// [`load_tensor`](GgufFile::load_tensor) and
157/// [`load_tensor_f32`](GgufFile::load_tensor_f32).
158pub struct GgufFile {
159    metadata: HashMap<String, MetadataValue>,
160    tensors: HashMap<String, TensorInfo>,
161    /// Absolute byte offset in the file where tensor data begins.
162    tensor_data_offset: u64,
163    reader: Mutex<BufReader<std::fs::File>>,
164}
165
166// ---------------------------------------------------------------------------
167// Low-level read helpers
168// ---------------------------------------------------------------------------
169
170/// Read a little-endian u8.
171fn read_u8<R: Read>(r: &mut R) -> Result<u8> {
172    let mut buf = [0u8; 1];
173    r.read_exact(&mut buf)
174        .map_err(|e| MlxError::GgufParseError(format!("read u8: {e}")))?;
175    Ok(buf[0])
176}
177
178/// Read a little-endian i8.
179fn read_i8<R: Read>(r: &mut R) -> Result<i8> {
180    Ok(read_u8(r)? as i8)
181}
182
183/// Read a little-endian u16.
184fn read_u16<R: Read>(r: &mut R) -> Result<u16> {
185    let mut buf = [0u8; 2];
186    r.read_exact(&mut buf)
187        .map_err(|e| MlxError::GgufParseError(format!("read u16: {e}")))?;
188    Ok(u16::from_le_bytes(buf))
189}
190
191/// Read a little-endian i16.
192fn read_i16<R: Read>(r: &mut R) -> Result<i16> {
193    let mut buf = [0u8; 2];
194    r.read_exact(&mut buf)
195        .map_err(|e| MlxError::GgufParseError(format!("read i16: {e}")))?;
196    Ok(i16::from_le_bytes(buf))
197}
198
199/// Read a little-endian u32.
200fn read_u32<R: Read>(r: &mut R) -> Result<u32> {
201    let mut buf = [0u8; 4];
202    r.read_exact(&mut buf)
203        .map_err(|e| MlxError::GgufParseError(format!("read u32: {e}")))?;
204    Ok(u32::from_le_bytes(buf))
205}
206
207/// Read a little-endian i32.
208fn read_i32<R: Read>(r: &mut R) -> Result<i32> {
209    let mut buf = [0u8; 4];
210    r.read_exact(&mut buf)
211        .map_err(|e| MlxError::GgufParseError(format!("read i32: {e}")))?;
212    Ok(i32::from_le_bytes(buf))
213}
214
215/// Read a little-endian u64.
216fn read_u64<R: Read>(r: &mut R) -> Result<u64> {
217    let mut buf = [0u8; 8];
218    r.read_exact(&mut buf)
219        .map_err(|e| MlxError::GgufParseError(format!("read u64: {e}")))?;
220    Ok(u64::from_le_bytes(buf))
221}
222
223/// Read a little-endian i64.
224fn read_i64<R: Read>(r: &mut R) -> Result<i64> {
225    let mut buf = [0u8; 8];
226    r.read_exact(&mut buf)
227        .map_err(|e| MlxError::GgufParseError(format!("read i64: {e}")))?;
228    Ok(i64::from_le_bytes(buf))
229}
230
231/// Read a little-endian f32.
232fn read_f32<R: Read>(r: &mut R) -> Result<f32> {
233    let mut buf = [0u8; 4];
234    r.read_exact(&mut buf)
235        .map_err(|e| MlxError::GgufParseError(format!("read f32: {e}")))?;
236    Ok(f32::from_le_bytes(buf))
237}
238
239/// Read a little-endian f64.
240fn read_f64<R: Read>(r: &mut R) -> Result<f64> {
241    let mut buf = [0u8; 8];
242    r.read_exact(&mut buf)
243        .map_err(|e| MlxError::GgufParseError(format!("read f64: {e}")))?;
244    Ok(f64::from_le_bytes(buf))
245}
246
247/// Read a GGUF-format string: u64 length followed by UTF-8 bytes (not
248/// null-terminated).
249fn read_gguf_string<R: Read>(r: &mut R) -> Result<String> {
250    let len = read_u64(r)? as usize;
251    if len > 256 * 1024 * 1024 {
252        return Err(MlxError::GgufParseError(format!(
253            "string length {len} exceeds 256 MiB safety limit"
254        )));
255    }
256    let mut buf = vec![0u8; len];
257    r.read_exact(&mut buf)
258        .map_err(|e| MlxError::GgufParseError(format!("read string bytes: {e}")))?;
259    String::from_utf8(buf)
260        .map_err(|e| MlxError::GgufParseError(format!("invalid UTF-8 in string: {e}")))
261}
262
263// ---------------------------------------------------------------------------
264// Metadata value parsing
265// ---------------------------------------------------------------------------
266
267/// Read a single metadata value of the given type.
268fn read_metadata_value<R: Read>(r: &mut R, value_type: u32) -> Result<MetadataValue> {
269    match value_type {
270        GGUF_TYPE_UINT8 => Ok(MetadataValue::Uint8(read_u8(r)?)),
271        GGUF_TYPE_INT8 => Ok(MetadataValue::Int8(read_i8(r)?)),
272        GGUF_TYPE_UINT16 => Ok(MetadataValue::Uint16(read_u16(r)?)),
273        GGUF_TYPE_INT16 => Ok(MetadataValue::Int16(read_i16(r)?)),
274        GGUF_TYPE_UINT32 => Ok(MetadataValue::Uint32(read_u32(r)?)),
275        GGUF_TYPE_INT32 => Ok(MetadataValue::Int32(read_i32(r)?)),
276        GGUF_TYPE_FLOAT32 => Ok(MetadataValue::Float32(read_f32(r)?)),
277        GGUF_TYPE_BOOL => {
278            let byte = read_u8(r)?;
279            Ok(MetadataValue::Bool(byte != 0))
280        }
281        GGUF_TYPE_STRING => Ok(MetadataValue::String(read_gguf_string(r)?)),
282        GGUF_TYPE_ARRAY => {
283            let elem_type = read_u32(r)?;
284            let count = read_u64(r)? as usize;
285            if count > 64 * 1024 * 1024 {
286                return Err(MlxError::GgufParseError(format!(
287                    "array count {count} exceeds 64M element safety limit"
288                )));
289            }
290            let mut elems = Vec::with_capacity(count);
291            for _ in 0..count {
292                elems.push(read_metadata_value(r, elem_type)?);
293            }
294            Ok(MetadataValue::Array(elems))
295        }
296        GGUF_TYPE_UINT64 => Ok(MetadataValue::Uint64(read_u64(r)?)),
297        GGUF_TYPE_INT64 => Ok(MetadataValue::Int64(read_i64(r)?)),
298        GGUF_TYPE_FLOAT64 => Ok(MetadataValue::Float64(read_f64(r)?)),
299        other => Err(MlxError::GgufParseError(format!(
300            "unknown metadata value type {other}"
301        ))),
302    }
303}
304
305// ---------------------------------------------------------------------------
306// GGML type mapping
307// ---------------------------------------------------------------------------
308
309/// Map a GGML type ID (u32 from the GGUF file) to our `GgmlType` enum.
310fn ggml_type_from_u32(id: u32) -> Result<GgmlType> {
311    match id {
312        GGML_TYPE_F32 => Ok(GgmlType::F32),
313        GGML_TYPE_F16 => Ok(GgmlType::F16),
314        GGML_TYPE_Q4_0 => Ok(GgmlType::Q4_0),
315        GGML_TYPE_Q5_1 => Ok(GgmlType::Q5_1),
316        GGML_TYPE_Q8_0 => Ok(GgmlType::Q8_0),
317        GGML_TYPE_Q4_K => Ok(GgmlType::Q4_K),
318        GGML_TYPE_Q5_K => Ok(GgmlType::Q5_K),
319        GGML_TYPE_Q6_K => Ok(GgmlType::Q6_K),
320        GGML_TYPE_I16 => Ok(GgmlType::I16),
321        GGML_TYPE_IQ4_NL => Ok(GgmlType::IQ4_NL),
322        GGML_TYPE_IQ4_XS => Ok(GgmlType::IQ4_XS),
323        other => Err(MlxError::GgufParseError(format!(
324            "unsupported GGML type ID {other}"
325        ))),
326    }
327}
328
329/// Compute the byte length of a tensor from its shape and GGML type.
330///
331/// For quantized types, the innermost dimension (shape[0] in GGUF's row-major
332/// convention) must be divisible by the block's element count.
333fn compute_byte_len(shape: &[usize], ggml_type: GgmlType) -> Result<usize> {
334    let total_elements: usize = shape.iter().product();
335    if total_elements == 0 {
336        return Ok(0);
337    }
338
339    let elems_per_block = ggml_type.block_values() as usize;
340    let bytes_per_block = ggml_type.block_bytes() as usize;
341
342    if total_elements % elems_per_block != 0 {
343        return Err(MlxError::GgufParseError(format!(
344            "total elements {total_elements} not divisible by block size {elems_per_block} \
345             for type {:?}",
346            ggml_type
347        )));
348    }
349
350    Ok((total_elements / elems_per_block) * bytes_per_block)
351}
352
353// ---------------------------------------------------------------------------
354// Dequantization
355// ---------------------------------------------------------------------------
356
357/// Convert a raw little-endian f16 (2 bytes) to f32.
358#[inline]
359fn f16_from_le_bytes(bytes: [u8; 2]) -> f32 {
360    f16::from_le_bytes(bytes).to_f32()
361}
362
363/// Dequantize Q4_0 blocks to f32.
364///
365/// Block layout (18 bytes, 32 elements):
366///   f16 d          — scale
367///   u8  qs[16]     — packed 4-bit values (low nibble = first 16, high nibble = last 16)
368fn dequantize_q4_0(data: &[u8], output: &mut [f32]) -> Result<()> {
369    const BLOCK_BYTES: usize = 18;
370    const BLOCK_ELEMS: usize = 32;
371
372    if data.len() % BLOCK_BYTES != 0 {
373        return Err(MlxError::GgufParseError(format!(
374            "Q4_0 data length {} not divisible by block size {BLOCK_BYTES}",
375            data.len()
376        )));
377    }
378
379    let num_blocks = data.len() / BLOCK_BYTES;
380    if output.len() < num_blocks * BLOCK_ELEMS {
381        return Err(MlxError::GgufParseError(
382            "Q4_0 output buffer too small".into(),
383        ));
384    }
385
386    for i in 0..num_blocks {
387        let block = &data[i * BLOCK_BYTES..(i + 1) * BLOCK_BYTES];
388        let d = f16_from_le_bytes([block[0], block[1]]);
389        let qs = &block[2..18]; // 16 bytes
390
391        let out = &mut output[i * BLOCK_ELEMS..(i + 1) * BLOCK_ELEMS];
392
393        for j in 0..16 {
394            let x0 = (qs[j] & 0x0F) as i16 - 8;
395            let x1 = (qs[j] >> 4) as i16 - 8;
396            out[j] = x0 as f32 * d;
397            out[j + 16] = x1 as f32 * d;
398        }
399    }
400    Ok(())
401}
402
403/// Dequantize Q8_0 blocks to f32.
404///
405/// Block layout (34 bytes, 32 elements):
406///   f16 d         — scale
407///   i8  qs[32]    — signed 8-bit quantized values
408fn dequantize_q8_0(data: &[u8], output: &mut [f32]) -> Result<()> {
409    const BLOCK_BYTES: usize = 34;
410    const BLOCK_ELEMS: usize = 32;
411
412    if data.len() % BLOCK_BYTES != 0 {
413        return Err(MlxError::GgufParseError(format!(
414            "Q8_0 data length {} not divisible by block size {BLOCK_BYTES}",
415            data.len()
416        )));
417    }
418
419    let num_blocks = data.len() / BLOCK_BYTES;
420    if output.len() < num_blocks * BLOCK_ELEMS {
421        return Err(MlxError::GgufParseError(
422            "Q8_0 output buffer too small".into(),
423        ));
424    }
425
426    for i in 0..num_blocks {
427        let block = &data[i * BLOCK_BYTES..(i + 1) * BLOCK_BYTES];
428        let d = f16_from_le_bytes([block[0], block[1]]);
429        let qs = &block[2..34]; // 32 bytes of i8
430
431        let out = &mut output[i * BLOCK_ELEMS..(i + 1) * BLOCK_ELEMS];
432
433        for j in 0..32 {
434            out[j] = (qs[j] as i8) as f32 * d;
435        }
436    }
437    Ok(())
438}
439
440/// Extract a (scale, min) pair for sub-block `j` from the 12-byte scales
441/// array used by Q4_K and Q5_K.
442///
443/// This matches `get_scale_min_k4` from candle / llama.cpp exactly:
444///
445/// For j < 4:
446///   scale = scales[j] & 63
447///   min   = scales[j + 4] & 63
448///
449/// For j >= 4:
450///   scale = (scales[j + 4] & 0xF) | ((scales[j - 4] >> 6) << 4)
451///   min   = (scales[j + 4] >> 4)  | ((scales[j]     >> 6) << 4)
452#[inline]
453fn get_scale_min_k4(j: usize, scales: &[u8]) -> (u8, u8) {
454    if j < 4 {
455        let sc = scales[j] & 63;
456        let m = scales[j + 4] & 63;
457        (sc, m)
458    } else {
459        let sc = (scales[j + 4] & 0xF) | ((scales[j - 4] >> 6) << 4);
460        let m = (scales[j + 4] >> 4) | ((scales[j] >> 6) << 4);
461        (sc, m)
462    }
463}
464
465/// Dequantize Q5_K blocks to f32.
466///
467/// Block layout (176 bytes, 256 elements):
468///   f16 d           — super-block scale      (offset 0,  2 bytes)
469///   f16 dmin        — super-block minimum     (offset 2,  2 bytes)
470///   u8  scales[12]  — packed 6-bit scales/mins (offset 4,  12 bytes; shared with Q4_K)
471///   u8  qh[32]      — high bits of quants      (offset 16, 32 bytes = QK_K/8)
472///   u8  qs[128]     — low 4 bits of quants     (offset 48, 128 bytes = QK_K/2)
473///
474/// 8 sub-blocks of 32 elements each. Dequantization walks pairs of
475/// sub-blocks (is, is+1), each pair consumes 32 bytes of qs (low nibble
476/// for is, high nibble for is+1). The qh array is SHARED across all 4
477/// pairs — the high bit per element is masked out of qh using shifting
478/// selector values `u1 = 1 << (2*pair_idx)` / `u2 = 2 << (2*pair_idx)`.
479///
480/// Spec source: derived from `ggml/src/ggml-quants.c::dequantize_row_q5_K`.
481/// No code copied — formula reproduced from the mathematical definition.
482fn dequantize_q5_k(data: &[u8], output: &mut [f32]) -> Result<()> {
483    const BLOCK_BYTES: usize = 176;
484    const BLOCK_ELEMS: usize = 256;
485
486    if data.len() % BLOCK_BYTES != 0 {
487        return Err(MlxError::GgufParseError(format!(
488            "Q5_K data length {} not divisible by block size {BLOCK_BYTES}",
489            data.len()
490        )));
491    }
492
493    let num_blocks = data.len() / BLOCK_BYTES;
494    if output.len() < num_blocks * BLOCK_ELEMS {
495        return Err(MlxError::GgufParseError(
496            "Q5_K output buffer too small".into(),
497        ));
498    }
499
500    for i in 0..num_blocks {
501        let block = &data[i * BLOCK_BYTES..(i + 1) * BLOCK_BYTES];
502
503        let d = f16_from_le_bytes([block[0], block[1]]);
504        let dmin = f16_from_le_bytes([block[2], block[3]]);
505        let scales = &block[4..16]; // 12 bytes
506        let qh = &block[16..48]; // 32 bytes — high bit of quants
507        let qs = &block[48..176]; // 128 bytes — low 4 bits
508
509        let out = &mut output[i * BLOCK_ELEMS..(i + 1) * BLOCK_ELEMS];
510
511        // Process 4 pairs of sub-blocks (256 values total).
512        // u1 / u2 are the high-bit selector masks: they shift left by 2 each
513        // iteration so the 4 pairs pick bits 0/1, 2/3, 4/5, 6/7 of each qh byte.
514        let mut is = 0usize;
515        let mut u1: u8 = 1;
516        let mut u2: u8 = 2;
517        let mut ys_index = 0usize;
518        let mut ql_off = 0usize;
519
520        while ql_off < 128 {
521            let ql = &qs[ql_off..ql_off + 32];
522
523            let (sc1, m1) = get_scale_min_k4(is, scales);
524            let d1 = d * sc1 as f32;
525            let m1 = dmin * m1 as f32;
526            let (sc2, m2) = get_scale_min_k4(is + 1, scales);
527            let d2 = d * sc2 as f32;
528            let m2 = dmin * m2 as f32;
529
530            // Sub-block `is` (low nibble + high bit from qh masked by u1).
531            for l in 0..32 {
532                let low = (ql[l] & 0x0F) as u32;
533                let high = if (qh[l] & u1) != 0 { 16 } else { 0 };
534                let q = low + high;
535                out[ys_index] = d1 * q as f32 - m1;
536                ys_index += 1;
537            }
538            // Sub-block `is + 1` (high nibble + high bit from qh masked by u2).
539            for l in 0..32 {
540                let low = (ql[l] >> 4) as u32;
541                let high = if (qh[l] & u2) != 0 { 16 } else { 0 };
542                let q = low + high;
543                out[ys_index] = d2 * q as f32 - m2;
544                ys_index += 1;
545            }
546
547            is += 2;
548            ql_off += 32;
549            u1 <<= 2;
550            u2 <<= 2;
551        }
552    }
553    Ok(())
554}
555
556/// Dequantize I16 tensors to f32.
557///
558/// Simple bitcast: `f32_val = i16_val as f32`. No scale metadata is used
559/// (apex GGUF convention — raw int16 values are meaningful as-is).
560///
561/// ADR-013 Decision 12 originally anticipated a per-tensor scale factor,
562/// but the apex GGUF does not emit one; values are stored as raw ints.
563/// If future GGUFs emit a scale, extend this with a scale parameter.
564fn dequantize_i16(data: &[u8], output: &mut [f32]) -> Result<()> {
565    if data.len() % 2 != 0 {
566        return Err(MlxError::GgufParseError(format!(
567            "I16 data length {} not even",
568            data.len()
569        )));
570    }
571    let num_elements = data.len() / 2;
572    if output.len() < num_elements {
573        return Err(MlxError::GgufParseError(
574            "I16 output buffer too small".into(),
575        ));
576    }
577    for i in 0..num_elements {
578        let v = i16::from_le_bytes([data[2 * i], data[2 * i + 1]]);
579        output[i] = v as f32;
580    }
581    Ok(())
582}
583
584/// Dequantize Q4_K blocks to f32.
585///
586/// Block layout (144 bytes, 256 elements):
587///   f16 d          — super-block scale          (offset 0,  2 bytes)
588///   f16 dmin       — super-block minimum         (offset 2,  2 bytes)
589///   u8  scales[12] — packed sub-block scales/mins (offset 4, 12 bytes)
590///   u8  qs[128]    — packed 4-bit quantized values (offset 16, 128 bytes)
591///
592/// 8 sub-blocks of 32 elements each.  Each pair of sub-blocks (64 elements)
593/// shares 32 bytes of qs — the low nibble gives the first sub-block, the
594/// high nibble gives the second.
595fn dequantize_q4_k(data: &[u8], output: &mut [f32]) -> Result<()> {
596    const BLOCK_BYTES: usize = 144;
597    const BLOCK_ELEMS: usize = 256;
598
599    if data.len() % BLOCK_BYTES != 0 {
600        return Err(MlxError::GgufParseError(format!(
601            "Q4_K data length {} not divisible by block size {BLOCK_BYTES}",
602            data.len()
603        )));
604    }
605
606    let num_blocks = data.len() / BLOCK_BYTES;
607    if output.len() < num_blocks * BLOCK_ELEMS {
608        return Err(MlxError::GgufParseError(
609            "Q4_K output buffer too small".into(),
610        ));
611    }
612
613    for i in 0..num_blocks {
614        let block = &data[i * BLOCK_BYTES..(i + 1) * BLOCK_BYTES];
615
616        let d = f16_from_le_bytes([block[0], block[1]]);
617        let dmin = f16_from_le_bytes([block[2], block[3]]);
618        let scales = &block[4..16];   // 12 bytes
619        let qs = &block[16..144];     // 128 bytes
620
621        let out = &mut output[i * BLOCK_ELEMS..(i + 1) * BLOCK_ELEMS];
622
623        // Process 4 pairs of sub-blocks (8 sub-blocks total, 256 elements).
624        // Each iteration handles 64 elements: sub-block `is` (low nibbles)
625        // and sub-block `is+1` (high nibbles) from 32 bytes of qs.
626        let mut is = 0usize;
627        let mut ys_index = 0usize;
628
629        // Step through the 256-element super-block in chunks of 64.
630        // j tracks the byte offset within qs.
631        let mut j = 0usize;
632        while j < 128 {
633            let q = &qs[j..j + 32];
634            let (sc1, m1) = get_scale_min_k4(is, scales);
635            let d1 = d * sc1 as f32;
636            let min1 = dmin * m1 as f32;
637            let (sc2, m2) = get_scale_min_k4(is + 1, scales);
638            let d2 = d * sc2 as f32;
639            let min2 = dmin * m2 as f32;
640
641            // Low nibbles: sub-block `is` (32 elements)
642            for byte in q.iter() {
643                out[ys_index] = d1 * (*byte & 0xF) as f32 - min1;
644                ys_index += 1;
645            }
646            // High nibbles: sub-block `is + 1` (32 elements)
647            for byte in q.iter() {
648                out[ys_index] = d2 * (*byte >> 4) as f32 - min2;
649                ys_index += 1;
650            }
651
652            is += 2;
653            j += 32;
654        }
655    }
656    Ok(())
657}
658
659/// Dequantize Q6_K blocks to f32.
660///
661/// Block layout (210 bytes, 256 elements):
662///   u8   ql[128]   — low 4 bits of quantized values  (offset 0, 128 bytes)
663///   u8   qh[64]    — high 2 bits of quantized values  (offset 128, 64 bytes)
664///   i8   scales[16] — sub-block scales                (offset 192, 16 bytes)
665///   f16  d          — super-block scale               (offset 208, 2 bytes)
666///
667/// 256 elements organized as 2 groups of 128.  Each group of 128 has its own
668/// ql[64], qh[32] region and produces 4 interleaved sub-groups of 32.
669fn dequantize_q6_k(data: &[u8], output: &mut [f32]) -> Result<()> {
670    const BLOCK_BYTES: usize = 210;
671    const BLOCK_ELEMS: usize = 256;
672
673    if data.len() % BLOCK_BYTES != 0 {
674        return Err(MlxError::GgufParseError(format!(
675            "Q6_K data length {} not divisible by block size {BLOCK_BYTES}",
676            data.len()
677        )));
678    }
679
680    let num_blocks = data.len() / BLOCK_BYTES;
681    if output.len() < num_blocks * BLOCK_ELEMS {
682        return Err(MlxError::GgufParseError(
683            "Q6_K output buffer too small".into(),
684        ));
685    }
686
687    for i in 0..num_blocks {
688        let block = &data[i * BLOCK_BYTES..(i + 1) * BLOCK_BYTES];
689
690        let ql = &block[0..128];
691        let qh = &block[128..192];
692        let sc = &block[192..208]; // i8 scales[16]
693        let d = f16_from_le_bytes([block[208], block[209]]);
694
695        let out = &mut output[i * BLOCK_ELEMS..(i + 1) * BLOCK_ELEMS];
696
697        // Process in two groups of 128 (idx = 0 and idx = 1).
698        for idx in 0..2 {
699            let ql_base = &ql[64 * idx..];
700            let qh_base = &qh[32 * idx..];
701            let sc_base = &sc[8 * idx..];
702            let out_base = &mut out[128 * idx..];
703
704            for l in 0..32 {
705                let is = l / 16; // 0 for l in 0..16, 1 for l in 16..32
706
707                let q1 = ((ql_base[l] & 0xF) | ((qh_base[l] & 3) << 4)) as i8 - 32_i8;
708                let q2 = ((ql_base[l + 32] & 0xF) | (((qh_base[l] >> 2) & 3) << 4)) as i8
709                    - 32_i8;
710                let q3 = ((ql_base[l] >> 4) | (((qh_base[l] >> 4) & 3) << 4)) as i8 - 32_i8;
711                let q4 = ((ql_base[l + 32] >> 4) | (((qh_base[l] >> 6) & 3) << 4)) as i8
712                    - 32_i8;
713
714                out_base[l] = d * sc_base[is] as i8 as f32 * q1 as f32;
715                out_base[l + 32] = d * sc_base[is + 2] as i8 as f32 * q2 as f32;
716                out_base[l + 64] = d * sc_base[is + 4] as i8 as f32 * q3 as f32;
717                out_base[l + 96] = d * sc_base[is + 6] as i8 as f32 * q4 as f32;
718            }
719        }
720    }
721    Ok(())
722}
723
724/// Dequantize F16 data to F32.
725fn dequantize_f16(data: &[u8], output: &mut [f32]) -> Result<()> {
726    if data.len() % 2 != 0 {
727        return Err(MlxError::GgufParseError(
728            "F16 data length not even".into(),
729        ));
730    }
731    let count = data.len() / 2;
732    if output.len() < count {
733        return Err(MlxError::GgufParseError(
734            "F16 output buffer too small".into(),
735        ));
736    }
737    for i in 0..count {
738        output[i] = f16_from_le_bytes([data[2 * i], data[2 * i + 1]]);
739    }
740    Ok(())
741}
742
743/// Reinterpret F32 little-endian bytes into the output slice.
744fn copy_f32(data: &[u8], output: &mut [f32]) -> Result<()> {
745    if data.len() % 4 != 0 {
746        return Err(MlxError::GgufParseError(
747            "F32 data length not multiple of 4".into(),
748        ));
749    }
750    let count = data.len() / 4;
751    if output.len() < count {
752        return Err(MlxError::GgufParseError(
753            "F32 output buffer too small".into(),
754        ));
755    }
756    for i in 0..count {
757        output[i] = f32::from_le_bytes([
758            data[4 * i],
759            data[4 * i + 1],
760            data[4 * i + 2],
761            data[4 * i + 3],
762        ]);
763    }
764    Ok(())
765}
766
767/// Dequantize Q5_1 blocks to f32.
768///
769/// Block layout (24 bytes, 32 elements):
770///   f16 d   — block scale            (offset 0,  2 bytes)
771///   f16 m   — block min term         (offset 2,  2 bytes)
772///   u32 qh  — high-bit pack          (offset 4,  4 bytes)
773///   u8  qs[16] — packed 4-bit lo nibbles (offset 8, 16 bytes)
774///
775/// Per-element: `out[j]      = d * x0 + m`, `out[j + 16] = d * x1 + m`,
776/// where `x0 = (qs[j] & 0x0F) | ((qh >> j) << 4) & 0x10`,
777///       `x1 = (qs[j] >> 4)  | ((qh >> (j + 12)) & 0x10)`.
778///
779/// Reference: `/opt/llama.cpp/ggml/src/ggml-quants.c:464` `dequantize_row_q5_1`.
780/// ADR-022 Phase 1.
781fn dequantize_q5_1(data: &[u8], output: &mut [f32]) -> Result<()> {
782    const BLOCK_BYTES: usize = 24;
783    const BLOCK_ELEMS: usize = 32;
784
785    if data.len() % BLOCK_BYTES != 0 {
786        return Err(MlxError::GgufParseError(format!(
787            "Q5_1 data length {} not divisible by block size {BLOCK_BYTES}",
788            data.len()
789        )));
790    }
791
792    let num_blocks = data.len() / BLOCK_BYTES;
793    if output.len() < num_blocks * BLOCK_ELEMS {
794        return Err(MlxError::GgufParseError(
795            "Q5_1 output buffer too small".into(),
796        ));
797    }
798
799    for i in 0..num_blocks {
800        let block = &data[i * BLOCK_BYTES..(i + 1) * BLOCK_BYTES];
801
802        let d = f16_from_le_bytes([block[0], block[1]]);
803        let m = f16_from_le_bytes([block[2], block[3]]);
804        let qh = u32::from_le_bytes([block[4], block[5], block[6], block[7]]);
805        let qs = &block[8..24]; // 16 bytes
806
807        let out = &mut output[i * BLOCK_ELEMS..(i + 1) * BLOCK_ELEMS];
808
809        for j in 0..(BLOCK_ELEMS / 2) {
810            // High-bit packed: bit j of qh contributes to position j;
811            // bit (j + 16) contributes to position j + 16. Mirrors
812            // `dequantize_row_q5_1` byte-for-byte.
813            let xh_0 = (((qh >> j) << 4) & 0x10) as u8;
814            let xh_1 = ((qh >> (j + 12)) & 0x10) as u8;
815            let x0 = ((qs[j] & 0x0F) | xh_0) as i32;
816            let x1 = ((qs[j] >> 4) | xh_1) as i32;
817            out[j] = (x0 as f32) * d + m;
818            out[j + BLOCK_ELEMS / 2] = (x1 as f32) * d + m;
819        }
820    }
821    Ok(())
822}
823
824/// Dequantize IQ4_NL blocks to f32.
825///
826/// Block layout (18 bytes, 32 elements):
827///   f16 d      — block scale                (offset 0,  2 bytes)
828///   u8  qs[16] — 16 × pair of 4-bit indices (offset 2, 16 bytes)
829///
830/// Per-element: `out[j]      = d * KVALUES_IQ4_NL[qs[j] & 0x0F]`,
831///              `out[j + 16] = d * KVALUES_IQ4_NL[qs[j] >> 4]`.
832///
833/// Reference: `/opt/llama.cpp/ggml/src/ggml-quants.c:2649` `dequantize_row_iq4_nl`.
834/// Codebook table verified against `ggml-common.h:1109-1112`. ADR-022 Phase 1.
835fn dequantize_iq4_nl(data: &[u8], output: &mut [f32]) -> Result<()> {
836    const BLOCK_BYTES: usize = 18;
837    const BLOCK_ELEMS: usize = 32;
838
839    if data.len() % BLOCK_BYTES != 0 {
840        return Err(MlxError::GgufParseError(format!(
841            "IQ4_NL data length {} not divisible by block size {BLOCK_BYTES}",
842            data.len()
843        )));
844    }
845
846    let num_blocks = data.len() / BLOCK_BYTES;
847    if output.len() < num_blocks * BLOCK_ELEMS {
848        return Err(MlxError::GgufParseError(
849            "IQ4_NL output buffer too small".into(),
850        ));
851    }
852
853    for i in 0..num_blocks {
854        let block = &data[i * BLOCK_BYTES..(i + 1) * BLOCK_BYTES];
855
856        let d = f16_from_le_bytes([block[0], block[1]]);
857        let qs = &block[2..18];
858
859        let out = &mut output[i * BLOCK_ELEMS..(i + 1) * BLOCK_ELEMS];
860
861        for j in 0..(BLOCK_ELEMS / 2) {
862            let lo = (qs[j] & 0x0F) as usize;
863            let hi = (qs[j] >> 4) as usize;
864            out[j] = d * KVALUES_IQ4_NL[lo] as f32;
865            out[j + BLOCK_ELEMS / 2] = d * KVALUES_IQ4_NL[hi] as f32;
866        }
867    }
868    Ok(())
869}
870
871/// Dequantize raw IQ4_XS bytes to f32. Pure-Rust mirror of
872/// `dequantize_row_iq4_xs` at `/opt/llama.cpp/ggml/src/ggml-quants.c:2667`.
873///
874/// Block layout (256-element super-block, 136 bytes):
875///   - d:        2 bytes f16 super-block scale
876///   - scales_h: 2 bytes u16, holds 8 × 2-bit sub-block scale tops
877///   - scales_l: 4 bytes, holds 8 × 4-bit sub-block scale low nibbles
878///     (two sub-blocks per byte: low nibble = sub-block 2k, high nibble = 2k+1)
879///   - qs:     128 bytes, nibble-packed 4-bit codebook indices
880///
881/// Per sub-block ib32 ∈ [0,7]:
882///   ls = (scales_l[ib32/2] >> 4*(ib32%2)) & 0xf
883///      | ((scales_h >> 2*ib32) & 3) << 4
884///   dl = d * (ls - 32)
885/// Per element pair (j in [0,15]) in sub-block ib32:
886///   out[j]      = dl * KVALUES_IQ4_NL[qs[16*ib32 + j] & 0xf]
887///   out[j + 16] = dl * KVALUES_IQ4_NL[qs[16*ib32 + j] >> 4]
888///
889/// Codebook is shared with IQ4_NL. ADR-033 §Pi 2026-05-22.
890fn dequantize_iq4_xs(data: &[u8], output: &mut [f32]) -> Result<()> {
891    const BLOCK_BYTES: usize = 136;
892    const BLOCK_ELEMS: usize = 256;
893
894    if data.len() % BLOCK_BYTES != 0 {
895        return Err(MlxError::GgufParseError(format!(
896            "IQ4_XS data length {} not divisible by block size {BLOCK_BYTES}",
897            data.len()
898        )));
899    }
900    let num_blocks = data.len() / BLOCK_BYTES;
901    if output.len() < num_blocks * BLOCK_ELEMS {
902        return Err(MlxError::GgufParseError(
903            "IQ4_XS output buffer too small".into(),
904        ));
905    }
906    for i in 0..num_blocks {
907        let block = &data[i * BLOCK_BYTES..(i + 1) * BLOCK_BYTES];
908        let d = f16_from_le_bytes([block[0], block[1]]);
909        let scales_h = u16::from_le_bytes([block[2], block[3]]);
910        let scales_l = &block[4..8];
911        let qs = &block[8..];
912        let out = &mut output[i * BLOCK_ELEMS..(i + 1) * BLOCK_ELEMS];
913        for ib32 in 0..(BLOCK_ELEMS / 32) {
914            let lo_nibble = (scales_l[ib32 / 2] >> (4 * (ib32 % 2))) & 0xf;
915            let hi_two = ((scales_h >> (2 * ib32)) & 0x3) as u8;
916            let ls = (lo_nibble | (hi_two << 4)) as i32;
917            let dl = d * ((ls - 32) as f32);
918            let qs_sub = &qs[16 * ib32..16 * (ib32 + 1)];
919            let out_sub = &mut out[32 * ib32..32 * (ib32 + 1)];
920            for j in 0..16 {
921                let lo = (qs_sub[j] & 0x0f) as usize;
922                let hi = (qs_sub[j] >> 4) as usize;
923                out_sub[j] = dl * KVALUES_IQ4_NL[lo] as f32;
924                out_sub[j + 16] = dl * KVALUES_IQ4_NL[hi] as f32;
925            }
926        }
927    }
928    Ok(())
929}
930
931/// Test-only export of `dequantize_q5_1` for ADR-022 parity tests in
932/// `/opt/mlx-native/tests/adr_022_phase1_dequant_parity.rs`. Hidden
933/// behind a doc(hidden) marker so it's not part of the public API but
934/// is accessible from integration tests via crate::gguf.
935#[doc(hidden)]
936pub fn test_only_dequantize_q5_1(data: &[u8], output: &mut [f32]) -> Result<()> {
937    dequantize_q5_1(data, output)
938}
939
940/// Test-only export of `dequantize_iq4_xs` for ADR-033 §Pi parity tests.
941#[doc(hidden)]
942pub fn test_only_dequantize_iq4_xs(data: &[u8], output: &mut [f32]) -> Result<()> {
943    dequantize_iq4_xs(data, output)
944}
945
946/// Test-only export of `dequantize_iq4_nl` for ADR-022 parity tests.
947#[doc(hidden)]
948pub fn test_only_dequantize_iq4_nl(data: &[u8], output: &mut [f32]) -> Result<()> {
949    dequantize_iq4_nl(data, output)
950}
951
952/// Test-only accessor for `KVALUES_IQ4_NL` so parity tests can pin the
953/// codebook bytes against the llama.cpp source of truth.
954#[doc(hidden)]
955pub fn test_only_kvalues_iq4_nl() -> [i8; 16] {
956    KVALUES_IQ4_NL
957}
958
959/// Test-only export of `dequantize_to_f32` for ADR-022 Phase-2 Q5_K
960/// dense parity tests. Routes through the same dispatch as the
961/// production load path. Hidden from rustdoc.
962#[doc(hidden)]
963pub fn test_only_dequantize(data: &[u8], ggml_type: GgmlType, output: &mut [f32]) -> Result<()> {
964    dequantize_to_f32(data, ggml_type, output)
965}
966
967/// Dequantize raw GGML block data to f32.
968fn dequantize_to_f32(data: &[u8], ggml_type: GgmlType, output: &mut [f32]) -> Result<()> {
969    match ggml_type {
970        GgmlType::F32 => copy_f32(data, output),
971        GgmlType::F16 => dequantize_f16(data, output),
972        GgmlType::Q4_0 => dequantize_q4_0(data, output),
973        GgmlType::Q8_0 => dequantize_q8_0(data, output),
974        GgmlType::Q4_K => dequantize_q4_k(data, output),
975        GgmlType::Q6_K => dequantize_q6_k(data, output),
976        GgmlType::Q5_K => dequantize_q5_k(data, output),
977        GgmlType::I16 => dequantize_i16(data, output),
978        GgmlType::Q5_1 => dequantize_q5_1(data, output),
979        GgmlType::IQ4_NL => dequantize_iq4_nl(data, output),
980        GgmlType::IQ4_XS => dequantize_iq4_xs(data, output),
981    }
982}
983
984// ---------------------------------------------------------------------------
985// GgufFile implementation
986// ---------------------------------------------------------------------------
987
988impl GgufFile {
989    /// Open and parse a GGUF v3 file.
990    ///
991    /// This reads the full header (magic, version, tensor count, metadata KV
992    /// pairs, tensor info entries) but does **not** read any tensor data.
993    /// Tensor data is loaded lazily via [`load_tensor`](Self::load_tensor) or
994    /// [`load_tensor_f32`](Self::load_tensor_f32).
995    ///
996    /// # Errors
997    ///
998    /// Returns `MlxError::IoError` if the file cannot be opened.
999    /// Returns `MlxError::GgufParseError` if the file is not valid GGUF v3.
1000    pub fn open(path: &Path) -> Result<Self> {
1001        let file = std::fs::File::open(path).map_err(|e| {
1002            MlxError::IoError(format!("cannot open GGUF file '{}': {e}", path.display()))
1003        })?;
1004        let mut reader = BufReader::new(file);
1005
1006        // --- Header ---
1007        let magic = read_u32(&mut reader)?;
1008        if magic != GGUF_MAGIC {
1009            return Err(MlxError::GgufParseError(format!(
1010                "bad magic: expected 0x{GGUF_MAGIC:08X}, got 0x{magic:08X}"
1011            )));
1012        }
1013
1014        let version = read_u32(&mut reader)?;
1015        if version != GGUF_VERSION {
1016            return Err(MlxError::GgufParseError(format!(
1017                "unsupported GGUF version {version} (only v3 is supported)"
1018            )));
1019        }
1020
1021        let tensor_count = read_u64(&mut reader)? as usize;
1022        let metadata_kv_count = read_u64(&mut reader)? as usize;
1023
1024        // Sanity limits to prevent OOM on corrupted files.
1025        if tensor_count > 100_000 {
1026            return Err(MlxError::GgufParseError(format!(
1027                "tensor_count {tensor_count} exceeds 100k safety limit"
1028            )));
1029        }
1030        if metadata_kv_count > 1_000_000 {
1031            return Err(MlxError::GgufParseError(format!(
1032                "metadata_kv_count {metadata_kv_count} exceeds 1M safety limit"
1033            )));
1034        }
1035
1036        // --- Metadata KV pairs ---
1037        let mut metadata = HashMap::with_capacity(metadata_kv_count);
1038        for _ in 0..metadata_kv_count {
1039            let key = read_gguf_string(&mut reader)?;
1040            let value_type = read_u32(&mut reader)?;
1041            let value = read_metadata_value(&mut reader, value_type)?;
1042            metadata.insert(key, value);
1043        }
1044
1045        // --- Determine alignment ---
1046        let alignment = metadata
1047            .get(GGUF_ALIGNMENT_KEY)
1048            .and_then(|v| v.as_u32())
1049            .map(|v| v as u64)
1050            .unwrap_or(GGUF_DEFAULT_ALIGNMENT);
1051
1052        if alignment == 0 || (alignment & (alignment - 1)) != 0 {
1053            return Err(MlxError::GgufParseError(format!(
1054                "alignment {alignment} is not a power of two"
1055            )));
1056        }
1057
1058        // --- Tensor info entries ---
1059        let mut tensors = HashMap::with_capacity(tensor_count);
1060        for _ in 0..tensor_count {
1061            let name = read_gguf_string(&mut reader)?;
1062            let n_dims = read_u32(&mut reader)? as usize;
1063
1064            if n_dims > 8 {
1065                return Err(MlxError::GgufParseError(format!(
1066                    "tensor '{name}' has {n_dims} dimensions (max 8)"
1067                )));
1068            }
1069
1070            let mut shape = Vec::with_capacity(n_dims);
1071            for _ in 0..n_dims {
1072                shape.push(read_u64(&mut reader)? as usize);
1073            }
1074            // GGUF stores dimensions innermost-first (column-major order).
1075            // Reverse to match the [rows, cols] convention used by candle
1076            // and by the rest of hf2q's weight loading code.
1077            shape.reverse();
1078
1079            let ggml_type_id = read_u32(&mut reader)?;
1080            let ggml_type = ggml_type_from_u32(ggml_type_id).map_err(|e| {
1081                MlxError::GgufParseError(format!("tensor '{name}': {e}"))
1082            })?;
1083
1084            let offset = read_u64(&mut reader)?;
1085            let byte_len = compute_byte_len(&shape, ggml_type).map_err(|e| {
1086                MlxError::GgufParseError(format!("tensor '{name}': {e}"))
1087            })?;
1088
1089            tensors.insert(
1090                name.clone(),
1091                TensorInfo {
1092                    name,
1093                    shape,
1094                    ggml_type,
1095                    offset,
1096                    byte_len,
1097                },
1098            );
1099        }
1100
1101        // --- Compute tensor_data_offset ---
1102        // The current file position is just past all tensor info entries.
1103        // Tensor data starts at the next alignment boundary.
1104        let pos = reader
1105            .stream_position()
1106            .map_err(|e| MlxError::GgufParseError(format!("stream_position: {e}")))?;
1107        let tensor_data_offset = align_offset(pos, alignment);
1108
1109        Ok(GgufFile {
1110            metadata,
1111            tensors,
1112            tensor_data_offset,
1113            reader: Mutex::new(reader),
1114        })
1115    }
1116
1117    // -----------------------------------------------------------------------
1118    // Metadata accessors
1119    // -----------------------------------------------------------------------
1120
1121    /// Look up a metadata value by key.
1122    pub fn metadata(&self, key: &str) -> Option<&MetadataValue> {
1123        self.metadata.get(key)
1124    }
1125
1126    /// Look up a metadata string value by key.
1127    pub fn metadata_string(&self, key: &str) -> Option<&str> {
1128        self.metadata.get(key).and_then(|v| v.as_str())
1129    }
1130
1131    /// Look up a metadata u32 value by key.
1132    pub fn metadata_u32(&self, key: &str) -> Option<u32> {
1133        self.metadata.get(key).and_then(|v| v.as_u32())
1134    }
1135
1136    /// Look up a metadata f32 value by key.
1137    pub fn metadata_f32(&self, key: &str) -> Option<f32> {
1138        self.metadata.get(key).and_then(|v| v.as_f32())
1139    }
1140
1141    // -----------------------------------------------------------------------
1142    // Tensor info accessors
1143    // -----------------------------------------------------------------------
1144
1145    /// Return the names of all tensors in the file.
1146    pub fn tensor_names(&self) -> Vec<&str> {
1147        self.tensors.keys().map(|s| s.as_str()).collect()
1148    }
1149
1150    /// Look up info for a specific tensor by name.
1151    pub fn tensor_info(&self, name: &str) -> Option<&TensorInfo> {
1152        self.tensors.get(name)
1153    }
1154
1155    /// Number of tensors in the file.
1156    pub fn tensor_count(&self) -> usize {
1157        self.tensors.len()
1158    }
1159
1160    /// Number of metadata key-value pairs.
1161    pub fn metadata_count(&self) -> usize {
1162        self.metadata.len()
1163    }
1164
1165    /// Absolute byte offset (from the start of the file) where the
1166    /// tensor data region begins. Add a [`TensorInfo::offset`] to this
1167    /// to get the absolute on-disk offset of a tensor's first byte —
1168    /// useful for tests that want to verify raw payload bytes without
1169    /// going through `load_tensor` (which requires an `MlxDevice`).
1170    pub fn tensor_data_offset(&self) -> u64 {
1171        self.tensor_data_offset
1172    }
1173
1174    // -----------------------------------------------------------------------
1175    // Tensor loading
1176    // -----------------------------------------------------------------------
1177
1178    /// Read raw tensor bytes from the file.
1179    ///
1180    /// This is a private helper that seeks to the tensor's location and reads
1181    /// `byte_len` bytes.
1182    fn read_tensor_bytes(&self, info: &TensorInfo) -> Result<Vec<u8>> {
1183        let abs_offset = self.tensor_data_offset + info.offset;
1184        let mut reader = self
1185            .reader
1186            .lock()
1187            .map_err(|_| MlxError::GgufParseError("reader mutex poisoned".into()))?;
1188
1189        reader
1190            .seek(SeekFrom::Start(abs_offset))
1191            .map_err(|e| MlxError::IoError(format!("seek to tensor '{}': {e}", info.name)))?;
1192
1193        let mut buf = vec![0u8; info.byte_len];
1194        reader.read_exact(&mut buf).map_err(|e| {
1195            MlxError::IoError(format!(
1196                "read tensor '{}' ({} bytes at offset {}): {e}",
1197                info.name, info.byte_len, abs_offset
1198            ))
1199        })?;
1200
1201        Ok(buf)
1202    }
1203
1204    /// Load a tensor as a raw buffer on the Metal device.
1205    ///
1206    /// For quantized types (Q4_0, Q8_0, Q4_K, Q6_K) the buffer contains raw
1207    /// GGML blocks with dtype `U8` — these are consumed directly by
1208    /// `quantized_matmul_ggml` kernels.
1209    ///
1210    /// For F32 and F16 tensors the buffer has the corresponding typed dtype.
1211    ///
1212    /// # Errors
1213    ///
1214    /// Returns an error if the tensor name is not found, or if reading fails.
1215    pub fn load_tensor(&self, name: &str, device: &MlxDevice) -> Result<MlxBuffer> {
1216        let info = self.tensors.get(name).ok_or_else(|| {
1217            MlxError::GgufParseError(format!("tensor '{name}' not found in GGUF file"))
1218        })?;
1219
1220        let data = self.read_tensor_bytes(info)?;
1221
1222        match info.ggml_type {
1223            GgmlType::F32 => {
1224                let mut buf =
1225                    device.alloc_buffer(info.byte_len, DType::F32, info.shape.clone())?;
1226                {
1227                    let slice: &mut [u8] = buf.as_mut_slice()?;
1228                    slice.copy_from_slice(&data);
1229                }
1230                Ok(buf)
1231            }
1232            GgmlType::F16 => {
1233                let mut buf =
1234                    device.alloc_buffer(info.byte_len, DType::F16, info.shape.clone())?;
1235                {
1236                    let slice: &mut [u8] = buf.as_mut_slice()?;
1237                    slice.copy_from_slice(&data);
1238                }
1239                Ok(buf)
1240            }
1241            GgmlType::Q4_0
1242            | GgmlType::Q8_0
1243            | GgmlType::Q4_K
1244            | GgmlType::Q5_K
1245            | GgmlType::Q6_K
1246            | GgmlType::I16
1247            | GgmlType::Q5_1
1248            | GgmlType::IQ4_NL
1249            // ADR-033 §Pi 2026-05-22 — IQ4_XS uses the same opaque-U8
1250            // on-device storage as the other quantized blocks; the
1251            // Metal matmul kernel reads them directly. (Kernel port:
1252            // Task #16. Until then, hf2q's runtime will surface a
1253            // typed "unsupported" at the dispatch site.)
1254            | GgmlType::IQ4_XS => {
1255                // Store raw GGML blocks as a U8 buffer. Where a Metal
1256                // quantized-matmul kernel exists for the type, it consumes
1257                // these blocks directly without an explicit dequant pass on
1258                // the GPU; otherwise the U8 view is opaque on-device storage
1259                // pending either a kernel port or a host-side dequant.
1260                //
1261                // Coverage status (ADR-022 in-flight; see ADR for the live
1262                // matrix). Per-type Metal kernel coverage is owned by
1263                // `quantized_matmul_ggml.rs` `kernel_name` / `mm_kernel_name`
1264                // / `mm_tensor_kernel_name` and the matmul-id counterparts;
1265                // host-side dequant for parity / no-kernel-yet paths is
1266                // wired into `dequantize_to_f32` directly above.
1267                let mut buf =
1268                    device.alloc_buffer(info.byte_len, DType::U8, info.shape.clone())?;
1269                {
1270                    let slice: &mut [u8] = buf.as_mut_slice()?;
1271                    slice.copy_from_slice(&data);
1272                }
1273                Ok(buf)
1274            }
1275        }
1276    }
1277
1278    /// Load a tensor, dequantizing to F32 on the CPU, then upload to the
1279    /// Metal device.
1280    ///
1281    /// This is used for norm weights, embedding tables, and other tensors
1282    /// where the inference kernels operate on F32 directly.
1283    ///
1284    /// # Errors
1285    ///
1286    /// Returns an error if the tensor name is not found, reading fails, or
1287    /// dequantization encounters malformed data.
1288    pub fn load_tensor_f32(&self, name: &str, device: &MlxDevice) -> Result<MlxBuffer> {
1289        let info = self.tensors.get(name).ok_or_else(|| {
1290            MlxError::GgufParseError(format!("tensor '{name}' not found in GGUF file"))
1291        })?;
1292
1293        let data = self.read_tensor_bytes(info)?;
1294        let total_elements: usize = info.shape.iter().product();
1295
1296        if total_elements == 0 {
1297            return Err(MlxError::GgufParseError(format!(
1298                "tensor '{name}' has zero elements"
1299            )));
1300        }
1301
1302        let f32_byte_len = total_elements * 4;
1303        let mut buf =
1304            device.alloc_buffer(f32_byte_len, DType::F32, info.shape.clone())?;
1305
1306        {
1307            let out_slice: &mut [f32] = buf.as_mut_slice()?;
1308            dequantize_to_f32(&data, info.ggml_type, out_slice)?;
1309        }
1310
1311        Ok(buf)
1312    }
1313
1314    /// Load a tensor and register its underlying Metal buffer with `pool`'s
1315    /// residency set, returning the [`MlxBuffer`] to the caller.
1316    ///
1317    /// This is functionally equivalent to:
1318    ///
1319    /// ```ignore
1320    /// let buf = gguf.load_tensor(name, device)?;
1321    /// pool.register_existing(device, &buf)?;
1322    /// ```
1323    ///
1324    /// but exists as a single call so callers don't need to reach for the
1325    /// underlying [`MlxBufferPool::register_existing`] API directly.  See
1326    /// that method's docs for the residency-set ownership contract.
1327    ///
1328    /// # Why a separate method instead of a `pool` parameter on `load_tensor`
1329    ///
1330    /// `load_tensor` has stable callers across the codebase that pass only
1331    /// `&MlxDevice`; making the pool registration optional via a new method
1332    /// keeps the existing signature wire-compatible.
1333    ///
1334    /// # Note on bucket-rounding
1335    ///
1336    /// The buffer is allocated at exactly `info.byte_len` via
1337    /// [`MlxDevice::alloc_buffer`](crate::MlxDevice::alloc_buffer) (no
1338    /// bucket-rounding) and added to the pool's residency set only —
1339    /// it is not placed in the recycling free list.  This is the path
1340    /// hf2q's static weight loader uses to gain MTLResidencySet hints
1341    /// without paying the 48% bucket-rounding tax that would have
1342    /// inflated 17 GB of weights to 25 GB.
1343    ///
1344    /// # Errors
1345    ///
1346    /// Same as [`load_tensor`](Self::load_tensor), plus any
1347    /// [`MlxError::InvalidArgument`] from
1348    /// [`MlxBufferPool::register_existing`].
1349    pub fn load_tensor_into_pool(
1350        &self,
1351        name: &str,
1352        device: &MlxDevice,
1353        pool: &mut MlxBufferPool,
1354    ) -> Result<MlxBuffer> {
1355        let buf = self.load_tensor(name, device)?;
1356        pool.register_existing(device, &buf)?;
1357        Ok(buf)
1358    }
1359}
1360
1361// ---------------------------------------------------------------------------
1362// Utility
1363// ---------------------------------------------------------------------------
1364
1365/// Round `offset` up to the next multiple of `alignment`.
1366fn align_offset(offset: u64, alignment: u64) -> u64 {
1367    let mask = alignment - 1;
1368    (offset + mask) & !mask
1369}