Skip to main content

logicaffeine_compile/model/
gguf.rs

1//! A read-only GGUF reader: the whole metadata table, typed, the tensor directory, and every tensor's weights in
2//! the formats [`super::quant::GgmlType`] reads.
3//!
4//! GGUF layout: `"GGUF"` magic, u32 version, u64 tensor count, u64 metadata count, the metadata key/value pairs,
5//! the tensor infos (name, dims, ggml type, offset), then — aligned to `general.alignment` — the tensor data.
6//! Metadata keys of the model's architecture are prefixed by `general.architecture` (`qwen3.block_count`); the
7//! tokenizer's live under `tokenizer.ggml.*`. Nothing is skipped: a tokenizer, a chat template and every
8//! architecture key are readable by name.
9
10use std::collections::BTreeMap;
11use std::io::{Read, Seek, SeekFrom};
12
13use super::quant::{f16_to_f32, GgmlType};
14
15/// One metadata value.
16#[derive(Clone, Debug, PartialEq)]
17pub enum MetaValue {
18    U8(u8),
19    I8(i8),
20    U16(u16),
21    I16(i16),
22    U32(u32),
23    I32(i32),
24    U64(u64),
25    I64(i64),
26    F32(f32),
27    F64(f64),
28    Bool(bool),
29    /// A GGUF string: bytes, which a vocabulary's tokens need not keep UTF-8.
30    Str(Vec<u8>),
31    /// An array, with the GGUF type of its elements (which an empty array still has).
32    Array { ty: u32, items: Vec<MetaValue> },
33}
34
35impl MetaValue {
36    /// The value as an unsigned integer, when it is a non-negative integer.
37    pub fn as_u64(&self) -> Option<u64> {
38        match *self {
39            MetaValue::U8(v) => Some(v as u64),
40            MetaValue::U16(v) => Some(v as u64),
41            MetaValue::U32(v) => Some(v as u64),
42            MetaValue::U64(v) => Some(v),
43            MetaValue::I8(v) if v >= 0 => Some(v as u64),
44            MetaValue::I16(v) if v >= 0 => Some(v as u64),
45            MetaValue::I32(v) if v >= 0 => Some(v as u64),
46            MetaValue::I64(v) if v >= 0 => Some(v as u64),
47            _ => None,
48        }
49    }
50
51    /// The value as a signed integer, when it is an integer that fits.
52    pub fn as_i64(&self) -> Option<i64> {
53        match *self {
54            MetaValue::I8(v) => Some(v as i64),
55            MetaValue::I16(v) => Some(v as i64),
56            MetaValue::I32(v) => Some(v as i64),
57            MetaValue::I64(v) => Some(v),
58            MetaValue::U8(v) => Some(v as i64),
59            MetaValue::U16(v) => Some(v as i64),
60            MetaValue::U32(v) => Some(v as i64),
61            MetaValue::U64(v) => i64::try_from(v).ok(),
62            _ => None,
63        }
64    }
65
66    /// The value as a float, when it is a float.
67    pub fn as_f64(&self) -> Option<f64> {
68        match *self {
69            MetaValue::F32(v) => Some(v as f64),
70            MetaValue::F64(v) => Some(v),
71            _ => None,
72        }
73    }
74
75    /// The value as text, when it is a UTF-8 string.
76    pub fn as_str(&self) -> Option<&str> {
77        match self {
78            MetaValue::Str(s) => std::str::from_utf8(s).ok(),
79            _ => None,
80        }
81    }
82
83    /// The value's bytes, when it is a string.
84    pub fn as_bytes(&self) -> Option<&[u8]> {
85        match self {
86            MetaValue::Str(s) => Some(s),
87            _ => None,
88        }
89    }
90
91    pub fn as_bool(&self) -> Option<bool> {
92        match *self {
93            MetaValue::Bool(b) => Some(b),
94            _ => None,
95        }
96    }
97
98    pub fn as_array(&self) -> Option<&[MetaValue]> {
99        match self {
100            MetaValue::Array { items, .. } => Some(items),
101            _ => None,
102        }
103    }
104}
105
106/// The transformer architecture fields the hand-written model lanes read from GGUF metadata. Every field is a
107/// key under the architecture prefix and reads 0 (or the documented default) when absent.
108#[derive(Debug, Clone)]
109pub struct Arch {
110    pub arch: String,
111    pub block_count: u64,
112    /// `context_length` — the trained context window (Qwen3.8: 262144).
113    pub context_length: u64,
114    pub embedding_length: u64,
115    pub feed_forward_length: u64,
116    pub head_count: u64,
117    pub head_count_kv: u64,
118    pub head_dim: u64,
119    pub rms_eps: f32,
120    pub rope_freq_base: f32,
121    /// `attention.value_length` — V head dim, which need not equal the K head dim.
122    pub value_length: u64,
123    /// `rope.dimension_count` — how many of each head's dims actually rotate. Qwen3.8 rotates **64 of 256**;
124    /// treating this as the full head_dim silently corrupts three quarters of every query and key.
125    pub rope_dimension_count: u64,
126    /// `full_attention_interval` — every Nth block is full attention, the rest are linear (Gated DeltaNet).
127    /// Qwen3.8: 4, so blocks 3, 7, 11 … are attention and the other 48 are GDN.
128    pub full_attention_interval: u64,
129    /// `nextn_predict_layers` — MTP draft blocks appended to `block_count`. Qwen3.8 ships 1, inside the same
130    /// file, so `block_count` (65) is one MORE than the model's real depth (64).
131    pub nextn_predict_layers: u64,
132    /// `ssm.conv_kernel` — taps in GDN's depthwise causal conv (4).
133    pub ssm_conv_kernel: u64,
134    /// `ssm.state_size` — the D of each head's D×D associative memory (128).
135    pub ssm_state_size: u64,
136    /// `ssm.group_count` — GDN **key** heads (16).
137    pub ssm_group_count: u64,
138    /// `ssm.time_step_rank` — GDN **value** heads (48). Three value heads share each key head.
139    pub ssm_time_step_rank: u64,
140    /// `ssm.inner_size` — the GDN value width, `time_step_rank · state_size` (6144).
141    pub ssm_inner_size: u64,
142}
143
144impl Arch {
145    /// Blocks that are actually part of the model — `block_count` minus the MTP draft blocks appended to it.
146    pub fn model_layers(&self) -> u64 {
147        self.block_count.saturating_sub(self.nextn_predict_layers)
148    }
149
150    /// Is block `l` a full-attention block? With `full_attention_interval = N`, every Nth block is, counting
151    /// from 1, i.e. indices `N-1, 2N-1, …`. A checkpoint without the key is uniformly full attention.
152    pub fn is_full_attention_layer(&self, l: u64) -> bool {
153        self.full_attention_interval == 0 || (l + 1) % self.full_attention_interval == 0
154    }
155
156    fn from_meta(meta: &BTreeMap<String, MetaValue>) -> Arch {
157        let arch = meta
158            .get("general.architecture")
159            .and_then(MetaValue::as_str)
160            .unwrap_or("")
161            .to_string();
162        let g = |suffix: &str| -> u64 {
163            meta.get(&format!("{arch}.{suffix}"))
164                .and_then(MetaValue::as_u64)
165                .unwrap_or(0)
166        };
167        let gf = |suffix: &str, d: f32| -> f32 {
168            meta.get(&format!("{arch}.{suffix}"))
169                .and_then(MetaValue::as_f64)
170                .map_or(d, |v| v as f32)
171        };
172        Arch {
173            block_count: g("block_count"),
174            context_length: g("context_length"),
175            embedding_length: g("embedding_length"),
176            feed_forward_length: g("feed_forward_length"),
177            head_count: g("attention.head_count"),
178            head_count_kv: g("attention.head_count_kv"),
179            head_dim: g("attention.key_length"),
180            rms_eps: gf("attention.layer_norm_rms_epsilon", 1e-6),
181            rope_freq_base: gf("rope.freq_base", 10000.0),
182            value_length: g("attention.value_length"),
183            rope_dimension_count: g("rope.dimension_count"),
184            full_attention_interval: g("full_attention_interval"),
185            nextn_predict_layers: g("nextn_predict_layers"),
186            ssm_conv_kernel: g("ssm.conv_kernel"),
187            ssm_state_size: g("ssm.state_size"),
188            ssm_group_count: g("ssm.group_count"),
189            ssm_time_step_rank: g("ssm.time_step_rank"),
190            ssm_inner_size: g("ssm.inner_size"),
191            arch,
192        }
193    }
194}
195
196/// One tensor's entry in the directory.
197#[derive(Debug, Clone)]
198pub struct TensorInfo {
199    pub name: String,
200    /// ggml order: `dims[0]` is the fastest-varying (a weight's in-features).
201    pub dims: Vec<u64>,
202    /// The GGUF type id (read with [`GgmlType::from_id`]).
203    pub ggml_type: u32,
204    /// Offset of the tensor's bytes from the start of the data section.
205    pub offset: u64,
206}
207
208impl TensorInfo {
209    pub fn elements(&self) -> usize {
210        self.dims.iter().map(|&d| d as usize).product()
211    }
212}
213
214/// A parsed GGUF file: the metadata, the tensor directory, and where the data section starts.
215pub struct Gguf {
216    file: std::fs::File,
217    pub version: u32,
218    pub meta: BTreeMap<String, MetaValue>,
219    /// The metadata keys in the order the file lists them.
220    pub order: Vec<String>,
221    pub arch: Arch,
222    tensors: Vec<TensorInfo>,
223    data_start: u64,
224}
225
226/// FNV-1a over 64 bits.
227pub fn fnv1a64(bytes: &[u8]) -> u64 {
228    bytes.iter().fold(0xcbf2_9ce4_8422_2325u64, |h, &b| (h ^ u64::from(b)).wrapping_mul(0x0000_0100_0000_01b3))
229}
230
231fn invalid(msg: String) -> std::io::Error {
232    std::io::Error::new(std::io::ErrorKind::InvalidData, msg)
233}
234
235fn not_found(name: &str) -> std::io::Error {
236    std::io::Error::new(std::io::ErrorKind::NotFound, format!("no tensor {name}"))
237}
238
239fn ru8(f: &mut impl Read) -> std::io::Result<u8> {
240    let mut b = [0u8; 1];
241    f.read_exact(&mut b)?;
242    Ok(b[0])
243}
244fn ru16(f: &mut impl Read) -> std::io::Result<u16> {
245    let mut b = [0u8; 2];
246    f.read_exact(&mut b)?;
247    Ok(u16::from_le_bytes(b))
248}
249fn ru32(f: &mut impl Read) -> std::io::Result<u32> {
250    let mut b = [0u8; 4];
251    f.read_exact(&mut b)?;
252    Ok(u32::from_le_bytes(b))
253}
254fn ru64(f: &mut impl Read) -> std::io::Result<u64> {
255    let mut b = [0u8; 8];
256    f.read_exact(&mut b)?;
257    Ok(u64::from_le_bytes(b))
258}
259fn rbytes(f: &mut impl Read) -> std::io::Result<Vec<u8>> {
260    let n = ru64(f)? as usize;
261    let mut b = vec![0u8; n];
262    f.read_exact(&mut b)?;
263    Ok(b)
264}
265
266/// A key or tensor name, which must be UTF-8.
267fn rstr(f: &mut impl Read) -> std::io::Result<String> {
268    String::from_utf8(rbytes(f)?).map_err(|e| invalid(format!("a metadata key or tensor name is not UTF-8: {e}")))
269}
270
271/// Read one metadata value of GGUF type `t`.
272fn read_value(f: &mut impl Read, t: u32) -> std::io::Result<MetaValue> {
273    Ok(match t {
274        0 => MetaValue::U8(ru8(f)?),
275        1 => MetaValue::I8(ru8(f)? as i8),
276        2 => MetaValue::U16(ru16(f)?),
277        3 => MetaValue::I16(ru16(f)? as i16),
278        4 => MetaValue::U32(ru32(f)?),
279        5 => MetaValue::I32(ru32(f)? as i32),
280        6 => MetaValue::F32(f32::from_bits(ru32(f)?)),
281        7 => MetaValue::Bool(ru8(f)? != 0),
282        8 => MetaValue::Str(rbytes(f)?),
283        9 => {
284            let et = ru32(f)?;
285            let n = ru64(f)? as usize;
286            let mut items = Vec::with_capacity(n.min(1 << 24));
287            for _ in 0..n {
288                items.push(read_value(f, et)?);
289            }
290            MetaValue::Array { ty: et, items }
291        }
292        10 => MetaValue::U64(ru64(f)?),
293        11 => MetaValue::I64(ru64(f)? as i64),
294        12 => MetaValue::F64(f64::from_bits(ru64(f)?)),
295        other => return Err(invalid(format!("unknown GGUF metadata type {other}"))),
296    })
297}
298
299impl Gguf {
300    /// Open and parse a GGUF file's header, metadata and tensor directory. The weights stay on disk until read.
301    pub fn open(path: &std::path::Path) -> std::io::Result<Gguf> {
302        let file = std::fs::File::open(path)?;
303        let mut r = std::io::BufReader::new(file);
304        let mut magic = [0u8; 4];
305        r.read_exact(&mut magic)?;
306        if &magic != b"GGUF" {
307            return Err(invalid(format!("{} is not a GGUF file", path.display())));
308        }
309        let version = ru32(&mut r)?;
310        if version < 2 {
311            return Err(invalid(format!(
312                "GGUF version {version} (32-bit counts) is not read; versions 2 and 3 are"
313            )));
314        }
315        let n_tensors = ru64(&mut r)?;
316        let n_kv = ru64(&mut r)?;
317        let mut meta = BTreeMap::new();
318        let mut order = Vec::with_capacity(n_kv as usize);
319        for _ in 0..n_kv {
320            let key = rstr(&mut r)?;
321            let t = ru32(&mut r)?;
322            order.push(key.clone());
323            meta.insert(key, read_value(&mut r, t)?);
324        }
325        let mut tensors = Vec::with_capacity(n_tensors as usize);
326        for _ in 0..n_tensors {
327            let name = rstr(&mut r)?;
328            let ndims = ru32(&mut r)? as usize;
329            let dims = (0..ndims)
330                .map(|_| ru64(&mut r))
331                .collect::<std::io::Result<Vec<u64>>>()?;
332            let ggml_type = ru32(&mut r)?;
333            let offset = ru64(&mut r)?;
334            tensors.push(TensorInfo {
335                name,
336                dims,
337                ggml_type,
338                offset,
339            });
340        }
341        let align = meta
342            .get("general.alignment")
343            .and_then(MetaValue::as_u64)
344            .unwrap_or(32);
345        let pos = r.stream_position()?;
346        let data_start = pos.div_ceil(align) * align;
347        let arch = Arch::from_meta(&meta);
348        Ok(Gguf {
349            file: r.into_inner(),
350            version,
351            meta,
352            order,
353            arch,
354            tensors,
355            data_start,
356        })
357    }
358
359    /// Where the data section starts: a tensor's bytes sit at `data_start() + offset`.
360    pub fn data_start(&self) -> u64 {
361        self.data_start
362    }
363
364    /// The FNV-1a 64 digest of everything before the data section — the header, the metadata and the tensor
365    /// directory: a program compiled against this checkpoint checks it reads the same file.
366    pub fn header_digest(&mut self) -> std::io::Result<u64> {
367        let mut head = vec![0u8; self.data_start as usize];
368        self.file.seek(SeekFrom::Start(0))?;
369        self.file.read_exact(&mut head)?;
370        Ok(fnv1a64(&head))
371    }
372
373    /// A metadata value by its full key.
374    pub fn meta(&self, key: &str) -> Option<&MetaValue> {
375        self.meta.get(key)
376    }
377
378    /// A metadata value under the architecture prefix (`{arch}.{suffix}`).
379    pub fn arch_meta(&self, suffix: &str) -> Option<&MetaValue> {
380        self.meta.get(&format!("{}.{suffix}", self.arch.arch))
381    }
382
383    /// The bytes of weights one decode step reads for a token. That is every tensor of the model's blocks and around
384    /// them, with three exceptions:
385    /// - the token embedding, where an output matrix of its own stands beside it (the step reads one of its rows);
386    /// - each expert matrix, counted in the share of its experts a token is routed to (`expert_used_count` of
387    ///   `expert_count`);
388    /// - the draft blocks appended for multi-token prediction, which count nothing.
389    /// A tensor in a format this crate does not read is an error.
390    pub fn decode_read_bytes(&self) -> std::io::Result<u64> {
391        let untied = self.info("output.weight").is_some();
392        let experts = self.arch_meta("expert_count").and_then(MetaValue::as_u64).unwrap_or(0);
393        let routed = self.arch_meta("expert_used_count").and_then(MetaValue::as_u64).unwrap_or(0);
394        let layers = self.arch.model_layers();
395        let mut total = 0u128;
396        for t in &self.tensors {
397            if untied && t.name == "token_embd.weight" {
398                continue;
399            }
400            let block = t.name.strip_prefix("blk.").and_then(|r| r.split('.').next()).and_then(|b| b.parse::<u64>().ok());
401            if block.is_some_and(|b| b >= layers) {
402                continue;
403            }
404            let ty = self.tensor_type(&t.name)?;
405            let bytes = ty.bytes_for(t.elements()) as u128;
406            total += if t.name.contains("_exps.") && experts > 0 {
407                bytes * routed as u128 / experts as u128
408            } else {
409                bytes
410            };
411        }
412        Ok(total as u64)
413    }
414
415    /// The tensor directory, in file order.
416    pub fn tensors(&self) -> &[TensorInfo] {
417        &self.tensors
418    }
419
420    /// One tensor's directory entry.
421    pub fn info(&self, name: &str) -> Option<&TensorInfo> {
422        self.tensors.iter().find(|t| t.name == name)
423    }
424
425    /// The format of a tensor, or why it is not one this crate reads.
426    pub fn tensor_type(&self, name: &str) -> std::io::Result<GgmlType> {
427        let t = self.info(name).ok_or_else(|| not_found(name))?;
428        GgmlType::from_id(t.ggml_type).map_err(|id| {
429            invalid(format!(
430                "{name} is ggml type {id}, a format this crate does not read"
431            ))
432        })
433    }
434
435    /// The raw bytes of rows `[first, first + count)` of a tensor, a row being its `dims[0]` fastest elements.
436    pub fn row_bytes(&mut self, name: &str, first: usize, count: usize) -> std::io::Result<Vec<u8>> {
437        let ty = self.tensor_type(name)?;
438        let t = self.info(name).ok_or_else(|| not_found(name))?.clone();
439        let row = t.dims.first().copied().unwrap_or(1) as usize;
440        let rows = t.elements() / row.max(1);
441        if first + count > rows {
442            return Err(invalid(format!(
443                "{name} has {rows} rows; rows {first}..{} were asked for",
444                first + count
445            )));
446        }
447        let row_bytes = ty.bytes_for(row);
448        self.file.seek(SeekFrom::Start(
449            self.data_start + t.offset + (first * row_bytes) as u64,
450        ))?;
451        let mut raw = vec![0u8; count * row_bytes];
452        self.file.read_exact(&mut raw)?;
453        Ok(raw)
454    }
455
456    /// Rows `[first, first + count)` of a tensor dequantized to `f32`, whatever its format.
457    pub fn dequant_rows(&mut self, name: &str, first: usize, count: usize) -> std::io::Result<Vec<f32>> {
458        let ty = self.tensor_type(name)?;
459        let raw = self.row_bytes(name, first, count)?;
460        let mut out = Vec::new();
461        ty.dequantize(&raw, &mut out);
462        Ok(out)
463    }
464
465    /// A whole tensor dequantized to `f32`, whatever its format.
466    pub fn dequant_tensor(&mut self, name: &str) -> std::io::Result<Vec<f32>> {
467        let t = self.info(name).ok_or_else(|| not_found(name))?.clone();
468        let row = t.dims.first().copied().unwrap_or(1) as usize;
469        self.dequant_rows(name, 0, t.elements() / row.max(1))
470    }
471
472    /// `(in = ne0, out = ne1)` for a 2-D weight tensor (`y = W·x` uses `out×in`).
473    pub fn dims2(&self, name: &str) -> Option<(usize, usize)> {
474        self.info(name).and_then(|t| match t.dims.as_slice() {
475            [ne0, ne1] => Some((*ne0 as usize, *ne1 as usize)),
476            _ => None,
477        })
478    }
479
480    /// The `[ne0, ne1, …]` dims of a tensor (ggml order: `ne0` is the fastest / in-features).
481    pub fn dims(&self, name: &str) -> Option<&[u64]> {
482        self.info(name).map(|t| t.dims.as_slice())
483    }
484
485    /// Every tensor's `(name, dims, ggml_type)` — for inspecting an unfamiliar checkpoint's layout.
486    pub fn tensor_names(&self) -> Vec<(String, Vec<u64>, u32)> {
487        self.tensors
488            .iter()
489            .map(|t| (t.name.clone(), t.dims.clone(), t.ggml_type))
490            .collect()
491    }
492
493    /// `true` if the named tensor exists.
494    pub fn has(&self, name: &str) -> bool {
495        self.info(name).is_some()
496    }
497
498    fn exactly(&self, name: &str, want: GgmlType) -> std::io::Result<TensorInfo> {
499        let ty = self.tensor_type(name)?;
500        if ty != want {
501            return Err(invalid(format!("{name} is {}, not {}", ty.name(), want.name())));
502        }
503        Ok(self.info(name).ok_or_else(|| not_found(name))?.clone())
504    }
505
506    /// Read an **F32** tensor's data in full (the RMSNorm weights).
507    pub fn f32_tensor(&mut self, name: &str) -> std::io::Result<Vec<f32>> {
508        self.exactly(name, GgmlType::F32)?;
509        self.dequant_tensor(name)
510    }
511
512    /// Read an **F16** tensor, widened to `f32`. The Qwen3.8 vision tower (`mmproj-*.gguf`) stores every weight
513    /// as F16.
514    pub fn f16_tensor(&mut self, name: &str) -> std::io::Result<Vec<f32>> {
515        self.exactly(name, GgmlType::F16)?;
516        self.dequant_tensor(name)
517    }
518
519    /// Read an F32 **or** F16 tensor as `f32`, whichever the checkpoint stored.
520    pub fn float_tensor(&mut self, name: &str) -> std::io::Result<Vec<f32>> {
521        match self.tensor_type(name)? {
522            GgmlType::F32 | GgmlType::F16 => self.dequant_tensor(name),
523            other => Err(invalid(format!("{name} is {}, not F32/F16", other.name()))),
524        }
525    }
526
527    /// Read a whole **Q8_0** tensor as `(quants i8 [out*in] row-major, scales f32 [out*in/32])`.
528    /// `dequant W[o][t] = quants[o*in+t] * scales[o*nb + t/32]`. Works for 2-D `[in, out]` AND 3-D expert stacks
529    /// `[in, out_per_expert, n_expert]` — for the latter `out` folds to `out_per_expert · n_expert`, so
530    /// `ffn_gate_exps`/`ffn_up_exps` come out as the stacked `[E·dff, d]` (and `ffn_down_exps` as `[E·d, dff]`)
531    /// layout `emit_moe_forward` consumes, expert `e`'s rows at `e·dff`.
532    pub fn q8_0_matrix(&mut self, name: &str) -> std::io::Result<(Vec<i8>, Vec<f32>)> {
533        let t = self.exactly(name, GgmlType::Q8_0)?;
534        let in_f = t.dims[0] as usize;
535        let out_f = t.elements() / in_f;
536        let raw = self.row_bytes(name, 0, out_f)?;
537        let nb = in_f / 32;
538        let mut quants = Vec::with_capacity(out_f * in_f);
539        let mut scales = Vec::with_capacity(out_f * nb);
540        for block in raw.chunks_exact(34) {
541            scales.push(f16_to_f32(u16::from_le_bytes([block[0], block[1]])));
542            quants.extend(block[2..].iter().map(|&q| q as i8));
543        }
544        Ok((quants, scales))
545    }
546
547    /// Dequantize a single **Q8_0** row to f32 (a token embedding row).
548    pub fn q8_0_dequant_row(&mut self, name: &str, row: usize) -> std::io::Result<Vec<f32>> {
549        self.exactly(name, GgmlType::Q8_0)?;
550        self.dequant_rows(name, row, 1)
551    }
552
553    /// Read a `rows` × `cols` **Q8_0** sub-tile as `(quants, scales)`: the int8 values (`rows*cols`, row-major)
554    /// AND the per-32-block f32 scales (`rows * cols/32`). The full Q8_0 weight (`dequant = quant * scale`), the
555    /// form a real block-scaled GEMM consumes.
556    pub fn q8_0_tile_blocks(
557        &mut self,
558        name: &str,
559        rows: usize,
560        cols: usize,
561    ) -> std::io::Result<(Vec<i32>, Vec<f32>)> {
562        let t = self.exactly(name, GgmlType::Q8_0)?;
563        let ne0 = t.dims[0] as usize;
564        assert!(cols % 32 == 0, "cols must be a multiple of the Q8_0 block (32)");
565        assert!(
566            cols <= ne0 && rows <= t.dims.get(1).copied().unwrap_or(1) as usize,
567            "tile exceeds tensor"
568        );
569        let raw = self.row_bytes(name, 0, rows)?;
570        let row_bytes = ne0 / 32 * 34;
571        let mut quants = Vec::with_capacity(rows * cols);
572        let mut scales = Vec::with_capacity(rows * cols / 32);
573        for r in 0..rows {
574            for block in raw[r * row_bytes..].chunks_exact(34).take(cols / 32) {
575                scales.push(f16_to_f32(u16::from_le_bytes([block[0], block[1]])));
576                quants.extend(block[2..].iter().map(|&q| q as i8 as i32));
577            }
578        }
579        Ok((quants, scales))
580    }
581
582    /// The raw int8 weights of the first `rows` × `cols` sub-tile of a **Q8_0** 2-D tensor, row-major over the
583    /// tensor's ggml layout. `cols` must be ≤ `ne0` and a multiple of 32, `rows` ≤ `ne1`.
584    pub fn q8_0_tile(&mut self, name: &str, rows: usize, cols: usize) -> std::io::Result<Vec<i32>> {
585        Ok(self.q8_0_tile_blocks(name, rows, cols)?.0)
586    }
587}
588
589impl MetaValue {
590    /// The GGUF type code of the value.
591    pub fn gguf_type(&self) -> u32 {
592        match self {
593            MetaValue::U8(_) => 0,
594            MetaValue::I8(_) => 1,
595            MetaValue::U16(_) => 2,
596            MetaValue::I16(_) => 3,
597            MetaValue::U32(_) => 4,
598            MetaValue::I32(_) => 5,
599            MetaValue::F32(_) => 6,
600            MetaValue::Bool(_) => 7,
601            MetaValue::Str(_) => 8,
602            MetaValue::Array { .. } => 9,
603            MetaValue::U64(_) => 10,
604            MetaValue::I64(_) => 11,
605            MetaValue::F64(_) => 12,
606        }
607    }
608
609    fn write_to(&self, out: &mut impl std::io::Write) -> std::io::Result<()> {
610        match self {
611            MetaValue::U8(v) => out.write_all(&[*v]),
612            MetaValue::I8(v) => out.write_all(&[*v as u8]),
613            MetaValue::U16(v) => out.write_all(&v.to_le_bytes()),
614            MetaValue::I16(v) => out.write_all(&v.to_le_bytes()),
615            MetaValue::U32(v) => out.write_all(&v.to_le_bytes()),
616            MetaValue::I32(v) => out.write_all(&v.to_le_bytes()),
617            MetaValue::F32(v) => out.write_all(&v.to_bits().to_le_bytes()),
618            MetaValue::Bool(v) => out.write_all(&[*v as u8]),
619            MetaValue::Str(b) => {
620                out.write_all(&(b.len() as u64).to_le_bytes())?;
621                out.write_all(b)
622            }
623            MetaValue::Array { ty, items } => {
624                out.write_all(&ty.to_le_bytes())?;
625                out.write_all(&(items.len() as u64).to_le_bytes())?;
626                items.iter().try_for_each(|v| v.write_to(out))
627            }
628            MetaValue::U64(v) => out.write_all(&v.to_le_bytes()),
629            MetaValue::I64(v) => out.write_all(&v.to_le_bytes()),
630            MetaValue::F64(v) => out.write_all(&v.to_bits().to_le_bytes()),
631        }
632    }
633}
634
635/// One tensor a written GGUF holds: its name, dims (`dims[0]` the fastest) and format.
636#[derive(Clone, Debug)]
637pub struct TensorOut {
638    pub name: String,
639    pub dims: Vec<u64>,
640    pub ty: GgmlType,
641}
642
643/// Write a GGUF v3 file: the metadata in the order given, the tensor directory, then each tensor's bytes — asked
644/// of `data` one tensor at a time, so a checkpoint is never held whole — each aligned to `general.alignment`
645/// (32 when absent).
646pub fn write(
647    path: &std::path::Path,
648    meta: &[(String, MetaValue)],
649    tensors: &[TensorOut],
650    mut data: impl FnMut(usize) -> std::io::Result<Vec<u8>>,
651) -> std::io::Result<()> {
652    use std::io::Write;
653    let align = meta
654        .iter()
655        .find(|(k, _)| k == "general.alignment")
656        .and_then(|(_, v)| v.as_u64())
657        .unwrap_or(32);
658    let mut out = std::io::BufWriter::new(std::fs::File::create(path)?);
659    let put_str = |out: &mut std::io::BufWriter<std::fs::File>, s: &[u8]| -> std::io::Result<()> {
660        out.write_all(&(s.len() as u64).to_le_bytes())?;
661        out.write_all(s)
662    };
663    out.write_all(b"GGUF")?;
664    out.write_all(&3u32.to_le_bytes())?;
665    out.write_all(&(tensors.len() as u64).to_le_bytes())?;
666    out.write_all(&(meta.len() as u64).to_le_bytes())?;
667    for (k, v) in meta {
668        put_str(&mut out, k.as_bytes())?;
669        out.write_all(&v.gguf_type().to_le_bytes())?;
670        v.write_to(&mut out)?;
671    }
672    let mut offset = 0u64;
673    let mut offsets = Vec::with_capacity(tensors.len());
674    for t in tensors {
675        put_str(&mut out, t.name.as_bytes())?;
676        out.write_all(&(t.dims.len() as u32).to_le_bytes())?;
677        for d in &t.dims {
678            out.write_all(&d.to_le_bytes())?;
679        }
680        out.write_all(&t.ty.id().to_le_bytes())?;
681        out.write_all(&offset.to_le_bytes())?;
682        offsets.push(offset);
683        let n = t.dims.iter().product::<u64>() as usize;
684        offset = (offset + t.ty.bytes_for(n) as u64).div_ceil(align) * align;
685    }
686    let mut written = 4 + 4 + 8 + 8
687        + meta.iter().map(|(k, v)| 8 + k.len() as u64 + 4 + encoded_len(v)).sum::<u64>()
688        + tensors.iter().map(|t| 8 + t.name.len() as u64 + 4 + 8 * t.dims.len() as u64 + 4 + 8).sum::<u64>();
689    let pad = |out: &mut std::io::BufWriter<std::fs::File>, written: &mut u64| -> std::io::Result<()> {
690        let to = written.div_ceil(align) * align;
691        out.write_all(&vec![0u8; (to - *written) as usize])?;
692        *written = to;
693        Ok(())
694    };
695    pad(&mut out, &mut written)?;
696    let data_start = written;
697    for (i, t) in tensors.iter().enumerate() {
698        debug_assert_eq!(written - data_start, offsets[i], "{} lands where its directory entry says", t.name);
699        let bytes = data(i)?;
700        let n = t.dims.iter().product::<u64>() as usize;
701        if bytes.len() != t.ty.bytes_for(n) {
702            return Err(invalid(format!("{}: {} bytes for {n} {} weights", t.name, bytes.len(), t.ty.name())));
703        }
704        out.write_all(&bytes)?;
705        written += bytes.len() as u64;
706        if i + 1 < tensors.len() {
707            pad(&mut out, &mut written)?;
708        }
709    }
710    out.flush()
711}
712
713/// The bytes `v` takes in a GGUF file, its type code excluded.
714fn encoded_len(v: &MetaValue) -> u64 {
715    match v {
716        MetaValue::U8(_) | MetaValue::I8(_) | MetaValue::Bool(_) => 1,
717        MetaValue::U16(_) | MetaValue::I16(_) => 2,
718        MetaValue::U32(_) | MetaValue::I32(_) | MetaValue::F32(_) => 4,
719        MetaValue::U64(_) | MetaValue::I64(_) | MetaValue::F64(_) => 8,
720        MetaValue::Str(b) => 8 + b.len() as u64,
721        MetaValue::Array { items, .. } => 4 + 8 + items.iter().map(encoded_len).sum::<u64>(),
722    }
723}