Skip to main content

combs_formats/
gguf.rs

1//! GGUF adapter (llama.cpp ecosystem).
2//!
3//! Reads GGUF v2/v3 files: header, metadata KV pairs, tensor infos and the
4//! aligned tensor data section (mmap-backed). Implements [`ModelSource`] by
5//! mapping ggml names to HF names (`blk.0.attn_q.weight` →
6//! `model.layers.0.self_attn.q_proj.weight`) and ggml dimensions to HF
7//! layout (`[in, out]` → `[out, in]`, which for row-major data is just a
8//! shape reversal — no data movement).
9//!
10//! Supported tensor types: F32, F16, BF16, Q4_0, Q4_1, Q5_0, Q5_1, Q8_0,
11//! Q4_K, Q5_K, Q6_K.
12//! Quantized tensors are dequantized on load (CPU scalar path) — wiring
13//! `QuantizedLinear` to keep them packed in VRAM is a follow-up. K-quant
14//! superblock layouts follow ggml exactly (256-value blocks, 6-bit packed
15//! scales/mins for 4/5_K, per-16 i8 scales for 6_K).
16//!
17//! Tokenizer: a sibling `tokenizer.json` is used when present; otherwise a
18//! BPE `tokenizer.json` is synthesized from the GGUF tokenizer metadata
19//! (tokens/scores/types/merges) and cached next to the model.
20
21use std::collections::HashMap;
22use std::path::{Path, PathBuf};
23
24use memmap2::Mmap;
25
26use crate::metadata::ModelMetadata;
27use crate::source::{ModelSource, SamplerConfig, TensorDtype, TensorReader};
28use crate::tokenizer::TokenizerSpec;
29use crate::{FormatError, Result};
30
31const GGUF_MAGIC: u32 = 0x4655_4747; // "GGUF"
32
33#[derive(Debug, Clone)]
34enum MetaValue {
35    U32(u32),
36    I32(i32),
37    U64(u64),
38    F32(f32),
39    Bool(bool),
40    String(String),
41    Strings(Vec<String>),
42    F32s(Vec<f32>),
43    I32s(Vec<i32>),
44}
45
46#[derive(Debug, Clone)]
47struct TensorInfo {
48    name: String,
49    dims: Vec<usize>, // ggml order (fastest dim first)
50    ggml_type: u32,
51    offset: usize, // relative to data section
52}
53
54/// A parsed GGUF file.
55pub struct GgufSource {
56    path: PathBuf,
57    mmap: Mmap,
58    metadata: ModelMetadata,
59    kv: HashMap<String, MetaValue>,
60    tensors: HashMap<String, TensorInfo>,
61    data_start: usize,
62    tokenizer_json: PathBuf,
63    added_tokens: HashMap<u32, String>,
64    eos_ids: Vec<u32>,
65    bos_id: Option<u32>,
66}
67
68// ---------------------------------------------------------------------------
69// parsing helpers (little-endian cursor)
70
71struct Cursor<'a> {
72    buf: &'a [u8],
73    pos: usize,
74}
75
76impl<'a> Cursor<'a> {
77    fn new(buf: &'a [u8]) -> Self {
78        Cursor { buf, pos: 0 }
79    }
80    fn take(&mut self, n: usize) -> Result<&'a [u8]> {
81        if self.pos + n > self.buf.len() {
82            return Err(FormatError::Safetensors("gguf: unexpected end of file".into()));
83        }
84        let out = &self.buf[self.pos..self.pos + n];
85        self.pos += n;
86        Ok(out)
87    }
88    fn u8(&mut self) -> Result<u8> {
89        Ok(self.take(1)?[0])
90    }
91    fn u16(&mut self) -> Result<u16> {
92        Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
93    }
94    fn u32(&mut self) -> Result<u32> {
95        Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
96    }
97    fn i32(&mut self) -> Result<i32> {
98        Ok(i32::from_le_bytes(self.take(4)?.try_into().unwrap()))
99    }
100    fn u64(&mut self) -> Result<u64> {
101        Ok(u64::from_le_bytes(self.take(8)?.try_into().unwrap()))
102    }
103    fn i64(&mut self) -> Result<i64> {
104        Ok(i64::from_le_bytes(self.take(8)?.try_into().unwrap()))
105    }
106    fn f32(&mut self) -> Result<f32> {
107        Ok(f32::from_le_bytes(self.take(4)?.try_into().unwrap()))
108    }
109    fn string(&mut self) -> Result<String> {
110        let len = self.u64()? as usize;
111        let bytes = self.take(len)?;
112        String::from_utf8(bytes.to_vec())
113            .map_err(|e| FormatError::Safetensors(format!("gguf: bad utf8 string: {e}")))
114    }
115}
116
117const META_U8: u32 = 0;
118const META_I8: u32 = 1;
119const META_U16: u32 = 2;
120const META_I16: u32 = 3;
121const META_U32: u32 = 4;
122const META_I32: u32 = 5;
123const META_F32: u32 = 6;
124const META_BOOL: u32 = 7;
125const META_STRING: u32 = 8;
126const META_ARRAY: u32 = 9;
127const META_U64: u32 = 10;
128const META_I64: u32 = 11;
129const META_F64: u32 = 12;
130
131fn read_meta_value(c: &mut Cursor, ty: u32) -> Result<MetaValue> {
132    Ok(match ty {
133        META_U8 => MetaValue::U32(c.u8()? as u32),
134        META_I8 => MetaValue::I32(c.u8()? as i8 as i32),
135        META_U16 => MetaValue::U32(c.u16()? as u32),
136        META_I16 => MetaValue::I32(c.u16()? as i16 as i32),
137        META_U32 => MetaValue::U32(c.u32()?),
138        META_I32 => MetaValue::I32(c.i32()?),
139        META_U64 => MetaValue::U64(c.u64()?),
140        META_I64 => MetaValue::U64(c.i64()? as u64),
141        META_F32 => MetaValue::F32(c.f32()?),
142        META_F64 => MetaValue::F32(f64::from_le_bytes(c.take(8)?.try_into().unwrap()) as f32),
143        META_BOOL => MetaValue::Bool(c.u8()? != 0),
144        META_STRING => MetaValue::String(c.string()?),
145        META_ARRAY => {
146            let elem_ty = c.u32()?;
147            let len = c.u64()? as usize;
148            match elem_ty {
149                META_STRING => {
150                    let mut out = Vec::with_capacity(len);
151                    for _ in 0..len {
152                        out.push(c.string()?);
153                    }
154                    MetaValue::Strings(out)
155                }
156                META_F32 => {
157                    let mut out = Vec::with_capacity(len);
158                    for _ in 0..len {
159                        out.push(c.f32()?);
160                    }
161                    MetaValue::F32s(out)
162                }
163                META_I32 | META_I16 | META_I8 => {
164                    let mut out = Vec::with_capacity(len);
165                    for _ in 0..len {
166                        out.push(read_meta_value(c, elem_ty).map(|v| match v {
167                            MetaValue::I32(i) => i,
168                            _ => 0,
169                        })?);
170                    }
171                    MetaValue::I32s(out)
172                }
173                META_U32 | META_U16 | META_U8 => {
174                    let mut out = Vec::with_capacity(len);
175                    for _ in 0..len {
176                        out.push(read_meta_value(c, elem_ty).map(|v| match v {
177                            MetaValue::U32(u) => u as i32,
178                            _ => 0,
179                        })?);
180                    }
181                    MetaValue::I32s(out)
182                }
183                other => {
184                    return Err(FormatError::Safetensors(format!(
185                        "gguf: unsupported metadata array element type {other}"
186                    )));
187                }
188            }
189        }
190        other => {
191            return Err(FormatError::Safetensors(format!(
192                "gguf: unsupported metadata type {other}"
193            )));
194        }
195    })
196}
197
198// ggml tensor types we support.
199const GGML_F32: u32 = 0;
200const GGML_F16: u32 = 1;
201const GGML_Q4_0: u32 = 2;
202const GGML_Q4_1: u32 = 3;
203const GGML_Q5_0: u32 = 6;
204const GGML_Q5_1: u32 = 7;
205const GGML_Q8_0: u32 = 8;
206const GGML_Q4_K: u32 = 12;
207const GGML_Q5_K: u32 = 13;
208const GGML_Q6_K: u32 = 14;
209const GGML_BF16: u32 = 30;
210
211impl GgufSource {
212    /// Opens and parses a `.gguf` file.
213    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
214        let path = path.as_ref().to_path_buf();
215        let file = std::fs::File::open(&path)?;
216        let mmap = unsafe { Mmap::map(&file)? };
217        let mut c = Cursor::new(&mmap);
218
219        if c.u32()? != GGUF_MAGIC {
220            return Err(FormatError::Safetensors("gguf: bad magic".into()));
221        }
222        let version = c.u32()?;
223        if !(2..=3).contains(&version) {
224            return Err(FormatError::Safetensors(format!(
225                "gguf: unsupported version {version}"
226            )));
227        }
228        let tensor_count = c.u64()? as usize;
229        let kv_count = c.u64()? as usize;
230
231        let mut kv = HashMap::with_capacity(kv_count);
232        for _ in 0..kv_count {
233            let key = c.string()?;
234            let ty = c.u32()?;
235            let value = read_meta_value(&mut c, ty)?;
236            kv.insert(key, value);
237        }
238
239        let mut tensors = HashMap::with_capacity(tensor_count);
240        for _ in 0..tensor_count {
241            let name = c.string()?;
242            let n_dims = c.u32()? as usize;
243            let mut dims = Vec::with_capacity(n_dims);
244            for _ in 0..n_dims {
245                dims.push(c.u64()? as usize);
246            }
247            let ggml_type = c.u32()?;
248            let offset = c.u64()? as usize;
249            tensors.insert(name.clone(), TensorInfo { name, dims, ggml_type, offset });
250        }
251
252        // Tensor data starts after the info section, aligned to 32 bytes.
253        let alignment = match kv.get("general.alignment") {
254            Some(MetaValue::U32(a)) => *a as usize,
255            _ => 32,
256        };
257        let data_start = c.pos.div_ceil(alignment) * alignment;
258
259        // Split GGUFs (llama.cpp `gguf-split` shards, `…-00001-of-0000N`)
260        // carry only a slice of the tensors; loading one would fail later
261        // with a baffling missing-tensor error and a wrong tied-head guess.
262        if let Some(MetaValue::U32(count)) = kv.get("split.count") {
263            if *count > 1 {
264                let no = match kv.get("split.no") {
265                    Some(MetaValue::U32(n)) => *n + 1,
266                    _ => 1,
267                };
268                return Err(FormatError::Safetensors(format!(
269                    "split GGUF: this file is shard {no} of {count} — \
270                     multi-file GGUF loading is not supported yet; pull a \
271                     single-file quant or merge the shards with llama.cpp's \
272                     `llama-gguf-split --merge`"
273                )));
274            }
275        }
276
277        let metadata = build_model_metadata(&kv)?;
278        let (eos_ids, bos_id, added_tokens) = tokenizer_ids(&kv);
279        let tokenizer_json = ensure_tokenizer_json(&path, &kv)?;
280
281        let mut source = GgufSource {
282            path,
283            mmap,
284            metadata,
285            kv,
286            tensors,
287            data_start,
288            tokenizer_json,
289            added_tokens,
290            eos_ids,
291            bos_id,
292        };
293        // GGUF llama files usually include output.weight; if absent, lm_head
294        // is tied to the embedding matrix.
295        source.metadata.tie_word_embeddings = !source.tensors.contains_key("output.weight");
296
297        // A tensor the map doesn't know is a loud one-line warning, never a
298        // silent drop — unmapped weights mean wrong output, not no output.
299        let arch = source.metadata.architecture.clone();
300        let mut unmapped: Vec<&str> = source
301            .tensors
302            .keys()
303            .filter(|k| {
304                map_tensor_name(k, &arch).is_none()
305                    && !KNOWN_UNMAPPED.contains(&k.as_str())
306                    && !is_fused_source(k, &arch)
307            })
308            .map(String::as_str)
309            .collect();
310        if !unmapped.is_empty() {
311            unmapped.sort();
312            eprintln!(
313                "[gguf] {} unmapped tensors (arch {arch}); first: {}",
314                unmapped.len(),
315                unmapped[0]
316            );
317        }
318        Ok(source)
319    }
320
321    fn kv_u64(&self, key: &str) -> Option<u64> {
322        match self.kv.get(key) {
323            Some(MetaValue::U32(v)) => Some(*v as u64),
324            Some(MetaValue::U64(v)) => Some(*v),
325            Some(MetaValue::I32(v)) => Some(*v as u64),
326            _ => None,
327        }
328    }
329}
330
331fn build_model_metadata(kv: &HashMap<String, MetaValue>) -> Result<ModelMetadata> {
332    let get_u64 = |key: &str| -> Option<u64> {
333        match kv.get(key) {
334            Some(MetaValue::U32(v)) => Some(*v as u64),
335            Some(MetaValue::U64(v)) => Some(*v),
336            Some(MetaValue::I32(v)) => Some(*v as u64),
337            _ => None,
338        }
339    };
340    let get_f32 = |key: &str| -> Option<f32> {
341        match kv.get(key) {
342            Some(MetaValue::F32(v)) => Some(*v),
343            Some(MetaValue::U32(v)) => Some(*v as f32),
344            _ => None,
345        }
346    };
347    let get_str = |key: &str| -> Option<String> {
348        match kv.get(key) {
349            Some(MetaValue::String(s)) => Some(s.clone()),
350            _ => None,
351        }
352    };
353
354    let arch = get_str("general.architecture")
355        .ok_or_else(|| FormatError::MissingField("general.architecture".into()))?;
356    let prefix = arch.clone();
357    let field = |name: &str| get_u64(&format!("{prefix}.{name}"));
358
359    let hidden = field("embedding_length")
360        .ok_or_else(|| FormatError::MissingField("embedding_length".into()))? as usize;
361    let heads = field("attention.head_count")
362        .ok_or_else(|| FormatError::MissingField("attention.head_count".into()))?
363        as usize;
364    let kv_heads = field("attention.head_count_kv").unwrap_or(heads as u64) as usize;
365    let layers = field("block_count")
366        .ok_or_else(|| FormatError::MissingField("block_count".into()))? as usize;
367    let ctx = field("context_length").unwrap_or(2048) as usize;
368    let ffn = field("feed_forward_length").unwrap_or((hidden * 4) as u64) as usize;
369    let vocab = match kv.get(&format!("{prefix}.vocab_size")) {
370        Some(MetaValue::U32(v)) => *v as usize,
371        Some(MetaValue::U64(v)) => *v as usize,
372        _ => match kv.get("tokenizer.ggml.tokens") {
373            Some(MetaValue::Strings(t)) => t.len(),
374            _ => 0,
375        },
376    };
377    let (eos_ids, bos_id, _) = tokenizer_ids(kv);
378
379    Ok(ModelMetadata {
380        architecture: arch,
381        hidden_size: hidden,
382        intermediate_size: ffn,
383        num_hidden_layers: layers,
384        num_attention_heads: heads,
385        num_key_value_heads: kv_heads,
386        vocab_size: vocab,
387        max_position_embeddings: ctx,
388        rms_norm_eps: get_f32(&format!("{prefix}.attention.layer_norm_rms_epsilon"))
389            .unwrap_or(1e-5) as f64,
390        rope_theta: get_f32(&format!("{prefix}.rope.freq_base")).unwrap_or(10000.0) as f64,
391        // GGUF files usually include output.weight; if absent, the head is tied.
392        tie_word_embeddings: false, // refined in load() via tensor presence
393        // Explicit head size when present — gemma3 (256) and qwen3 (128)
394        // decouple it from hidden/heads.
395        head_dim: field("attention.key_length")
396            .map(|v| v as usize)
397            .unwrap_or(hidden / heads),
398        // Bias loading is presence-driven (the loader probes bias tensors),
399        // so this flag is informational only for GGUF.
400        attention_bias: false,
401        bos_token_id: bos_id,
402        eos_token_ids: eos_ids,
403        vision: None,
404        // Sliding window when the file declares one (phi3 writes
405        // `attention.sliding_window`); the remaining gemma layout keys
406        // (pattern, local rope base) land with the arch-aware map.
407        attention_pattern: crate::metadata::AttentionPattern {
408            sliding_window: field("attention.sliding_window").map(|v| v as usize),
409            ..Default::default()
410        },
411        // GGUF stores no activation key; the per-arch resolver supplies it.
412        activation: crate::metadata::Activation::default(),
413        rope_scaling: gguf_rope_scaling(kv, &prefix)?,
414    })
415}
416
417/// GGUF `rope.scaling.*` keys (llama.cpp writes `linear`/`yarn`; llama3
418/// scaling is baked as a `rope_freqs.weight` tensor instead — that tensor
419/// is a separate follow-up and absent from our cached files).
420fn gguf_rope_scaling(
421    kv: &HashMap<String, MetaValue>,
422    prefix: &str,
423) -> Result<crate::metadata::RopeScaling> {
424    use crate::metadata::RopeScaling;
425    let get_f32 = |key: String| match kv.get(&key) {
426        Some(MetaValue::F32(v)) => Some(*v as f64),
427        _ => None,
428    };
429    let kind = match kv.get(&format!("{prefix}.rope.scaling.type")) {
430        Some(MetaValue::String(s)) => s.clone(),
431        _ => return Ok(RopeScaling::None),
432    };
433    let factor = get_f32(format!("{prefix}.rope.scaling.factor")).unwrap_or(1.0);
434    let orig = match kv.get(&format!("{prefix}.rope.scaling.original_context_length")) {
435        Some(MetaValue::U32(v)) => *v as usize,
436        Some(MetaValue::U64(v)) => *v as usize,
437        _ => 32768,
438    };
439    match kind.as_str() {
440        "none" => Ok(RopeScaling::None),
441        "linear" => Ok(RopeScaling::Linear { factor }),
442        "yarn" => Ok(RopeScaling::Yarn {
443            factor,
444            original_max_position_embeddings: orig,
445            beta_fast: get_f32(format!("{prefix}.rope.scaling.yarn_beta_fast")).unwrap_or(32.0),
446            beta_slow: get_f32(format!("{prefix}.rope.scaling.yarn_beta_slow")).unwrap_or(1.0),
447            attention_factor: None,
448        }),
449        other => Err(FormatError::MissingField(format!(
450            "unsupported GGUF rope scaling type {other:?}"
451        ))),
452    }
453}
454
455/// End-of-generation control tokens that chat finetunes emit to terminate a
456/// turn while the file's declared eos stays the base `<|endoftext|>`-style id
457/// (phi-3's `<|end|>`, llama-3's `<|eot_id|>`, gemma's `<end_of_turn>`).
458/// Mirrors llama.cpp's `special_eog_ids` name scan.
459const EOG_TOKENS: &[&str] = &[
460    "<|end|>",
461    "<|eot_id|>",
462    "<|eom_id|>",
463    "<|im_end|>",
464    "<end_of_turn>",
465    "<|end_of_text|>",
466    "<|endoftext|>",
467    "<EOT>",
468];
469
470fn tokenizer_ids(kv: &HashMap<String, MetaValue>) -> (Vec<u32>, Option<u32>, HashMap<u32, String>) {
471    let mut eos = Vec::new();
472    let mut bos = None;
473    let mut added = HashMap::new();
474    if let Some(MetaValue::U32(v)) = kv.get("tokenizer.ggml.eos_token_id") {
475        eos.push(*v);
476    }
477    if let Some(MetaValue::U32(v)) = kv.get("tokenizer.ggml.bos_token_id") {
478        bos = Some(*v);
479    }
480    // Mark special tokens from the token_type array (3 = control).
481    if let (Some(MetaValue::Strings(tokens)), Some(MetaValue::I32s(types))) =
482        (kv.get("tokenizer.ggml.tokens"), kv.get("tokenizer.ggml.token_type"))
483    {
484        for (i, (tok, ty)) in tokens.iter().zip(types.iter()).enumerate() {
485            if *ty == 3 {
486                added.insert(i as u32, tok.clone());
487            }
488            if *ty == 3 && EOG_TOKENS.contains(&tok.as_str()) && !eos.contains(&(i as u32)) {
489                eos.push(i as u32);
490            }
491        }
492    }
493    (eos, bos, added)
494}
495
496/// Counts GGUF special tokens (token_type 3 = control, 4 = user-defined).
497fn special_token_count(kv: &HashMap<String, MetaValue>) -> usize {
498    match kv.get("tokenizer.ggml.token_type") {
499        Some(MetaValue::I32s(types)) => types.iter().filter(|t| **t == 3 || **t == 4).count(),
500        _ => 0,
501    }
502}
503
504/// BPE split regex for the synthesized tokenizer, selected by the GGUF
505/// `tokenizer.ggml.pre` family. Qwen2 splits digit runs into single digits
506/// (`\p{N}`) where the GPT-2/llama-bpe families group up to three
507/// (`\p{N}{1,3}`) — digit tokenization drift is user-visible on coder models.
508fn pretokenizer_regex(kv: &HashMap<String, MetaValue>) -> &'static str {
509    const DEFAULT: &str = "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+";
510    const QWEN2: &str = "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+";
511    match kv.get("tokenizer.ggml.pre") {
512        Some(MetaValue::String(pre)) if pre == "qwen2" => QWEN2,
513        _ => DEFAULT,
514    }
515}
516
517/// Builds a minimal HF BPE tokenizer.json from GGUF tokenizer metadata when
518/// no sibling tokenizer.json exists (cached alongside the model file).
519///
520/// Staleness: v1 syntheses wrote `"added_tokens": []`, which BPE-shredded
521/// ChatML control tokens (`<|im_start|>`/`<|im_end|>`) — the "garbled GGUF"
522/// bug — and the poisoned cache was sticky. The cached file is regenerated
523/// whenever its added_tokens count disagrees with the GGUF metadata. (No
524/// in-file version marker: the tokenizers crate rejects unknown top-level
525/// keys.)
526fn ensure_tokenizer_json(path: &Path, kv: &HashMap<String, MetaValue>) -> Result<PathBuf> {
527    let sibling = path.with_file_name("tokenizer.json");
528    if sibling.exists() {
529        return Ok(sibling);
530    }
531    let cached = path.with_extension("tokenizer.json");
532    if cached.exists() {
533        let parsed = std::fs::read_to_string(&cached)
534            .ok()
535            .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
536        let have_added = parsed
537            .as_ref()
538            .and_then(|v| v.get("added_tokens")?.as_array().map(Vec::len));
539        // Older syntheses hardcoded the GPT-2 split regex for every family;
540        // regenerate when the cached regex disagrees with the `pre` family.
541        let have_regex = parsed.as_ref().and_then(|v| {
542            v.get("pre_tokenizer")?
543                .get("pretokenizers")?
544                .get(0)?
545                .get("pattern")?
546                .get("Regex")?
547                .as_str()
548                .map(str::to_string)
549        });
550        if have_added == Some(special_token_count(kv))
551            && have_regex.as_deref() == Some(pretokenizer_regex(kv))
552        {
553            return Ok(cached);
554        }
555        // Stale synthesis — regenerate below.
556    }
557
558    let tokens = match kv.get("tokenizer.ggml.tokens") {
559        Some(MetaValue::Strings(t)) => t,
560        _ => {
561            return Err(FormatError::MissingField(
562                "tokenizer.ggml.tokens (and no sibling tokenizer.json)".into(),
563            ));
564        }
565    };
566    let scores: Vec<f32> = match kv.get("tokenizer.ggml.scores") {
567        Some(MetaValue::F32s(s)) => s.clone(),
568        _ => vec![0.0; tokens.len()],
569    };
570    let merges: Vec<String> = match kv.get("tokenizer.ggml.merges") {
571        Some(MetaValue::Strings(m)) => m.clone(),
572        _ => vec![],
573    };
574
575    let mut vocab = serde_json::Map::new();
576    for (i, tok) in tokens.iter().enumerate() {
577        vocab.insert(tok.clone(), serde_json::Value::from(i));
578    }
579    let mut ordered: Vec<usize> = (0..tokens.len()).collect();
580    ordered.sort_by(|&a, &b| {
581        scores[a].partial_cmp(&scores[b]).unwrap_or(std::cmp::Ordering::Equal)
582    });
583    let mut ranks = serde_json::Map::new();
584    for (rank, id) in ordered.iter().enumerate() {
585        ranks.insert(id.to_string(), serde_json::Value::from(rank));
586    }
587
588    // Special tokens (GGUF token_type 3 = control, 4 = user-defined) must
589    // be registered as added_tokens so the tokenizer keeps them atomic
590    // instead of BPE-shredding them (e.g. <|im_start|>/<|im_end|>).
591    let mut added_tokens = Vec::new();
592    if let Some(MetaValue::I32s(types)) = kv.get("tokenizer.ggml.token_type") {
593        for (i, (tok, ty)) in tokens.iter().zip(types.iter()).enumerate() {
594            if *ty == 3 || *ty == 4 {
595                added_tokens.push(serde_json::json!({
596                    "id": i,
597                    "content": tok,
598                    "single_word": false,
599                    "lstrip": false,
600                    "rstrip": false,
601                    "normalized": false,
602                    "special": true,
603                }));
604            }
605        }
606    }
607
608    let json = serde_json::json!({
609        "version": "1.0",
610        "truncation": null,
611        "padding": null,
612        "added_tokens": added_tokens,
613        "normalizer": null,
614        "pre_tokenizer": {
615            "type": "Sequence",
616            "pretokenizers": [
617                {"type": "Split", "pattern": {"Regex": pretokenizer_regex(kv)}, "behavior": "Isolated", "invert": false},
618                {"type": "ByteLevel", "add_prefix_space": false, "trim_offsets": true, "use_regex": false}
619            ]
620        },
621        "post_processor": null,
622        "decoder": {"type": "ByteLevel", "add_prefix_space": true, "trim_offsets": true, "use_regex": true},
623        "model": {
624            "type": "BPE",
625            "dropout": null,
626            "unk_token": null,
627            "continuing_subword_prefix": null,
628            "end_of_word_suffix": null,
629            "fuse_unk": false,
630            "byte_fallback": false,
631            "vocab": vocab,
632            "merges": merges,
633        }
634    });
635    let serialized = serde_json::to_string(&json).map_err(|e| FormatError::Safetensors(format!("tokenizer json: {e}")))?;
636    std::fs::write(&cached, serialized)?;
637    Ok(cached)
638}
639
640/// Maps a ggml tensor name to its HF equivalent (`None` = skip tensor).
641/// Arch-keyed where a ggml name means different HF norms per family:
642/// llama's `ffn_norm` is the pre-MLP `post_attention_layernorm`, but
643/// gemma3's `ffn_norm` is `pre_feedforward_layernorm` (its
644/// `post_attention_norm`/`post_ffw_norm` are the sandwich norms).
645fn map_tensor_name(ggml: &str, arch: &str) -> Option<String> {
646    if ggml == "token_embd.weight" {
647        return Some("model.embed_tokens.weight".into());
648    }
649    if ggml == "output.weight" {
650        return Some("lm_head.weight".into());
651    }
652    if ggml == "output_norm.weight" {
653        return Some("model.norm.weight".into());
654    }
655    let rest = ggml.strip_prefix("blk.")?;
656    let (layer, rest) = rest.split_once('.')?;
657    let gemma = matches!(arch, "gemma3" | "gemma3_text");
658    let hf = match rest {
659        "attn_norm.weight" => "input_layernorm.weight",
660        "ffn_norm.weight" if gemma => "pre_feedforward_layernorm.weight",
661        "post_attention_norm.weight" if gemma => "post_attention_layernorm.weight",
662        "post_ffw_norm.weight" if gemma => "post_feedforward_layernorm.weight",
663        "ffn_norm.weight" => "post_attention_layernorm.weight",
664        // Per-head QK norms (qwen3, gemma3); the loader probes these only
665        // when the resolved spec asks for them.
666        "attn_q_norm.weight" => "self_attn.q_norm.weight",
667        "attn_k_norm.weight" => "self_attn.k_norm.weight",
668        "attn_q.weight" => "self_attn.q_proj.weight",
669        "attn_k.weight" => "self_attn.k_proj.weight",
670        "attn_v.weight" => "self_attn.v_proj.weight",
671        "attn_output.weight" => "self_attn.o_proj.weight",
672        "attn_q.bias" => "self_attn.q_proj.bias",
673        "attn_k.bias" => "self_attn.k_proj.bias",
674        "attn_v.bias" => "self_attn.v_proj.bias",
675        "attn_output.bias" => "self_attn.o_proj.bias",
676        "ffn_gate.weight" => "mlp.gate_proj.weight",
677        "ffn_up.weight" => "mlp.up_proj.weight",
678        "ffn_down.weight" => "mlp.down_proj.weight",
679        _ => return None,
680    };
681    Some(format!("model.layers.{layer}.{hf}"))
682}
683
684/// Tensors some converters emit that the engine deliberately does not map
685/// (suppressed from the unmapped-tensor warning).
686const KNOWN_UNMAPPED: &[&str] = &[
687    // llama-3.1+ long-rope frequency table; scaling comes from metadata
688    // keys when present.
689    "rope_freqs.weight",
690];
691
692/// Fused ggml tensors served through `fused_slice` instead of the name map
693/// (phi3's `attn_qkv`; its fused `ffn_up` also feeds gate/up slices but
694/// maps directly as `up_proj`). Consumed, so not "unmapped".
695fn is_fused_source(ggml: &str, arch: &str) -> bool {
696    arch == "phi3"
697        && ggml
698            .strip_prefix("blk.")
699            .and_then(|r| r.split_once('.'))
700            .is_some_and(|(_, rest)| rest == "attn_qkv.weight" || rest == "attn_qkv.bias")
701}
702
703/// Number of elements in a ggml tensor.
704fn num_elements(dims: &[usize]) -> usize {
705    dims.iter().product()
706}
707
708/// Byte size of a ggml tensor's data.
709fn tensor_byte_size(info: &TensorInfo) -> Result<usize> {
710    let n = num_elements(&info.dims);
711    let block = |bs: usize| -> Result<usize> {
712        if n % 32 != 0 {
713            return Err(FormatError::Safetensors(format!(
714                "gguf tensor {}: {n} elements not divisible by block size 32",
715                info.name
716            )));
717        }
718        Ok(n / 32 * bs)
719    };
720    let superblock = |bs: usize| -> Result<usize> {
721        if n % 256 != 0 {
722            return Err(FormatError::Safetensors(format!(
723                "gguf tensor {}: {n} elements not divisible by K-quant superblock 256",
724                info.name
725            )));
726        }
727        Ok(n / 256 * bs)
728    };
729    Ok(match info.ggml_type {
730        GGML_F32 => n * 4,
731        GGML_F16 | GGML_BF16 => n * 2,
732        GGML_Q4_0 => block(18)?, // 2-byte scale + 16 bytes per 32 values
733        GGML_Q4_1 => block(20)?, // f16 scale + f16 min + 16 bytes
734        GGML_Q5_0 => block(22)?, // f16 scale + u32 high bits + 16 bytes
735        GGML_Q5_1 => block(24)?, // f16 scale + f16 min + u32 high bits + 16 bytes
736        GGML_Q8_0 => block(34)?, // 2-byte scale + 32 int8 per 32 values
737        // K-quants: 256-value superblocks.
738        GGML_Q4_K => superblock(144)?, // 2×f16 + 12B packed scales/mins + 128B quants
739        GGML_Q5_K => superblock(176)?, // + 32B high bits
740        GGML_Q6_K => superblock(210)?, // 128B ql + 64B qh + 16×i8 scales + f16
741        other => {
742            return Err(FormatError::UnsupportedDtype {
743                tensor: info.name.clone(),
744                dtype: format!("ggml_type {other}"),
745            });
746        }
747    })
748}
749
750/// Dequantizes a Q4_0 tensor to f32. Blocks of 32 values: f16 scale + 16
751/// bytes; value i (i<16) is the low nibble of byte i, value i+16 the high.
752///
753/// Public via [`crate::quants`]: this scalar path is the harmony reference
754/// the fused CubeCL kernels validate against.
755pub fn dequantize_q4_0(data: &[u8], n: usize) -> Result<Vec<f32>> {
756    let mut fixed = Vec::with_capacity(n);
757    for block in data.chunks_exact(18) {
758        let d = half::f16::from_le_bytes([block[0], block[1]]).to_f32();
759        let mut vals = [0f32; 32];
760        for j in 0..16 {
761            let byte = block[2 + j];
762            vals[j] = ((byte & 0x0F) as i32 - 8) as f32 * d;
763            vals[j + 16] = ((byte >> 4) as i32 - 8) as f32 * d;
764        }
765        fixed.extend_from_slice(&vals);
766    }
767    if fixed.len() != n {
768        return Err(FormatError::Safetensors(format!(
769            "q4_0 dequant size mismatch: {} != {n}",
770            fixed.len()
771        )));
772    }
773    Ok(fixed)
774}
775
776/// Dequantizes a Q8_0 tensor to f32.
777///
778/// Public via [`crate::quants`] as the harmony reference for the GPU kernel.
779pub fn dequantize_q8_0(data: &[u8], n: usize) -> Result<Vec<f32>> {
780    let mut out = Vec::with_capacity(n);
781    for block in data.chunks_exact(34) {
782        let d = half::f16::from_le_bytes([block[0], block[1]]).to_f32();
783        for &b in &block[2..34] {
784            out.push((b as i8) as f32 * d);
785        }
786    }
787    if out.len() != n {
788        return Err(FormatError::Safetensors(format!(
789            "q8_0 dequant size mismatch: {} != {n}",
790            out.len()
791        )));
792    }
793    Ok(out)
794}
795
796/// Dequantizes a Q4_1 tensor to f32. Like Q4_0 but asymmetric:
797/// value = q·d + m (f16 scale + f16 min per 32-value block).
798fn dequantize_q4_1(data: &[u8], n: usize) -> Result<Vec<f32>> {
799    let mut out = Vec::with_capacity(n);
800    for block in data.chunks_exact(20) {
801        let d = half::f16::from_le_bytes([block[0], block[1]]).to_f32();
802        let m = half::f16::from_le_bytes([block[2], block[3]]).to_f32();
803        let mut vals = [0f32; 32];
804        for j in 0..16 {
805            let byte = block[4 + j];
806            vals[j] = (byte & 0x0F) as f32 * d + m;
807            vals[j + 16] = (byte >> 4) as f32 * d + m;
808        }
809        out.extend_from_slice(&vals);
810    }
811    if out.len() != n {
812        return Err(FormatError::Safetensors(format!(
813            "q4_1 dequant size mismatch: {} != {n}",
814            out.len()
815        )));
816    }
817    Ok(out)
818}
819
820/// Dequantizes a Q5_0 tensor to f32. 32-value blocks (22 bytes): f16
821/// scale, u32 high bits (bit j → value j, bit j+16 → value j+16), 16
822/// packed nibble bytes; values biased by -16.
823///
824/// Public via [`crate::quants`] as the harmony reference for the GPU kernel.
825pub fn dequantize_q5_0(data: &[u8], n: usize) -> Result<Vec<f32>> {
826    let mut out = Vec::with_capacity(n);
827    for block in data.chunks_exact(22) {
828        let d = half::f16::from_le_bytes([block[0], block[1]]).to_f32();
829        let qh = u32::from_le_bytes([block[2], block[3], block[4], block[5]]);
830        let mut vals = [0f32; 32];
831        for j in 0..16 {
832            let byte = block[6 + j];
833            let lo = ((byte & 0x0F) as u32 | (((qh >> j) & 1) << 4)) as i32 - 16;
834            let hi = ((byte >> 4) as u32 | (((qh >> (j + 16)) & 1) << 4)) as i32 - 16;
835            vals[j] = lo as f32 * d;
836            vals[j + 16] = hi as f32 * d;
837        }
838        out.extend_from_slice(&vals);
839    }
840    if out.len() != n {
841        return Err(FormatError::Safetensors(format!(
842            "q5_0 dequant size mismatch: {} != {n}",
843            out.len()
844        )));
845    }
846    Ok(out)
847}
848
849/// Dequantizes a Q5_1 tensor to f32. Like Q5_0 but asymmetric (scale +
850/// min, no bias): value = q·d + m. 24-byte blocks.
851fn dequantize_q5_1(data: &[u8], n: usize) -> Result<Vec<f32>> {
852    let mut out = Vec::with_capacity(n);
853    for block in data.chunks_exact(24) {
854        let d = half::f16::from_le_bytes([block[0], block[1]]).to_f32();
855        let m = half::f16::from_le_bytes([block[2], block[3]]).to_f32();
856        let qh = u32::from_le_bytes([block[4], block[5], block[6], block[7]]);
857        let mut vals = [0f32; 32];
858        for j in 0..16 {
859            let byte = block[8 + j];
860            let lo = (byte & 0x0F) as u32 | (((qh >> j) & 1) << 4);
861            let hi = (byte >> 4) as u32 | (((qh >> (j + 16)) & 1) << 4);
862            vals[j] = lo as f32 * d + m;
863            vals[j + 16] = hi as f32 * d + m;
864        }
865        out.extend_from_slice(&vals);
866    }
867    if out.len() != n {
868        return Err(FormatError::Safetensors(format!(
869            "q5_1 dequant size mismatch: {} != {n}",
870            out.len()
871        )));
872    }
873    Ok(out)
874}
875
876/// K-quant 6-bit scale/min unpacking (ggml `get_scale_min_k4`): the 12
877/// scale bytes pack eight 6-bit scales and eight 6-bit mins, with the top
878/// 2 bits of bytes 0..8 carrying the high bits of sub-blocks 4..8.
879fn scale_min_k4(j: usize, q: &[u8]) -> (u8, u8) {
880    if j < 4 {
881        (q[j] & 63, q[j + 4] & 63)
882    } else {
883        (
884            (q[j + 4] & 0x0F) | ((q[j - 4] >> 6) << 4),
885            (q[j + 4] >> 4) | ((q[j] >> 6) << 4),
886        )
887    }
888}
889
890/// Dequantizes a Q4_K tensor to f32. 256-value superblocks (144 bytes):
891/// f16 d, f16 dmin, 12B packed scales/mins, 128B 4-bit quants.
892///
893/// Public via [`crate::quants`] as the harmony reference for the GPU kernel.
894pub fn dequantize_q4_k(data: &[u8], n: usize) -> Result<Vec<f32>> {
895    let mut out = Vec::with_capacity(n);
896    for sb in data.chunks_exact(144) {
897        let d = half::f16::from_le_bytes([sb[0], sb[1]]).to_f32();
898        let dmin = half::f16::from_le_bytes([sb[2], sb[3]]).to_f32();
899        let scales = &sb[4..16];
900        let qs = &sb[16..144];
901        for j in 0..4 {
902            // Each 64-value group uses 32 qs bytes; low nibbles → first
903            // sub-block, high nibbles → second.
904            let (sc1, m1) = scale_min_k4(2 * j, scales);
905            let (sc2, m2) = scale_min_k4(2 * j + 1, scales);
906            let (d1, fmin1) = (d * sc1 as f32, dmin * m1 as f32);
907            let (d2, fmin2) = (d * sc2 as f32, dmin * m2 as f32);
908            for l in 0..32 {
909                let byte = qs[32 * j + l];
910                out.push(d1 * (byte & 0x0F) as f32 - fmin1);
911            }
912            for l in 0..32 {
913                let byte = qs[32 * j + l];
914                out.push(d2 * (byte >> 4) as f32 - fmin2);
915            }
916        }
917    }
918    if out.len() != n {
919        return Err(FormatError::Safetensors(format!(
920            "q4_k dequant size mismatch: {} != {n}",
921            out.len()
922        )));
923    }
924    Ok(out)
925}
926
927/// Dequantizes a Q5_K tensor to f32. Like Q4_K plus 32 high-bit bytes:
928/// group j reads bit 2j (low sub-block) / 2j+1 (high sub-block) of qh.
929pub fn dequantize_q5_k(data: &[u8], n: usize) -> Result<Vec<f32>> {
930    let mut out = Vec::with_capacity(n);
931    for sb in data.chunks_exact(176) {
932        let d = half::f16::from_le_bytes([sb[0], sb[1]]).to_f32();
933        let dmin = half::f16::from_le_bytes([sb[2], sb[3]]).to_f32();
934        let scales = &sb[4..16];
935        let qh = &sb[16..48];
936        let qs = &sb[48..176];
937        for j in 0..4 {
938            let (sc1, m1) = scale_min_k4(2 * j, scales);
939            let (sc2, m2) = scale_min_k4(2 * j + 1, scales);
940            let (d1, fmin1) = (d * sc1 as f32, dmin * m1 as f32);
941            let (d2, fmin2) = (d * sc2 as f32, dmin * m2 as f32);
942            for l in 0..32 {
943                let lo = (qs[32 * j + l] & 0x0F) as u32;
944                let hi = ((qh[l] >> (2 * j)) & 1) as u32;
945                out.push(d1 * ((lo | (hi << 4)) as f32) - fmin1);
946            }
947            for l in 0..32 {
948                let lo = (qs[32 * j + l] >> 4) as u32;
949                let hi = ((qh[l] >> (2 * j + 1)) & 1) as u32;
950                out.push(d2 * ((lo | (hi << 4)) as f32) - fmin2);
951            }
952        }
953    }
954    if out.len() != n {
955        return Err(FormatError::Safetensors(format!(
956            "q5_k dequant size mismatch: {} != {n}",
957            out.len()
958        )));
959    }
960    Ok(out)
961}
962
963/// Dequantizes a Q6_K tensor to f32. 256-value superblocks (210 bytes):
964/// 128B low nibbles, 64B high 2-bit fields, 16 i8 scales (one per 16
965/// values), f16 super-scale. Values are biased by -32.
966///
967/// Public via [`crate::quants`] as the harmony reference for the GPU kernel.
968pub fn dequantize_q6_k(data: &[u8], n: usize) -> Result<Vec<f32>> {
969    let mut out = Vec::with_capacity(n);
970    for sb in data.chunks_exact(210) {
971        let d = half::f16::from_le_bytes([sb[208], sb[209]]).to_f32();
972        // Process the superblock as two 128-value halves.
973        for half_idx in 0..2 {
974            let ql = &sb[64 * half_idx..64 * half_idx + 64];
975            let qh = &sb[128 + 32 * half_idx..128 + 32 * half_idx + 32];
976            let scales = &sb[192 + 8 * half_idx..192 + 8 * half_idx + 8];
977            let mut vals = [0f32; 128];
978            for l in 0..32 {
979                let is = l / 16;
980                let q1 = ((ql[l] & 0x0F) | ((qh[l] & 0x03) << 4)) as i8 as i32 - 32;
981                let q2 = ((ql[l + 32] & 0x0F) | ((qh[l] & 0x0C) >> 2 << 4)) as i8 as i32 - 32;
982                let q3 = ((ql[l] >> 4) | ((qh[l] & 0x30) >> 4 << 4)) as i8 as i32 - 32;
983                let q4 = ((ql[l + 32] >> 4) | ((qh[l] & 0xC0) >> 6 << 4)) as i8 as i32 - 32;
984                vals[l] = d * (scales[is] as i8) as f32 * q1 as f32;
985                vals[l + 32] = d * (scales[is + 2] as i8) as f32 * q2 as f32;
986                vals[l + 64] = d * (scales[is + 4] as i8) as f32 * q3 as f32;
987                vals[l + 96] = d * (scales[is + 6] as i8) as f32 * q4 as f32;
988            }
989            out.extend_from_slice(&vals);
990        }
991    }
992    if out.len() != n {
993        return Err(FormatError::Safetensors(format!(
994            "q6_k dequant size mismatch: {} != {n}",
995            out.len()
996        )));
997    }
998    Ok(out)
999}
1000
1001/// llama.cpp's `convert_hf_to_gguf` permutes each attention head's
1002/// `attn_q`/`attn_k` rows for llama-family architectures — HF rotate-half
1003/// halves `[0..d/2 | d/2..d]` become ggml interleaved pairs
1004/// `[0, d/2, 1, d/2+1, …]`. This engine applies HF rotate-half RoPE, so the
1005/// rows must be de-interleaved back on load (transformers'
1006/// `_reverse_permute_weights`). Returns, for each HF row, the source ggml
1007/// row: `hf[j] = ggml[2j]` for `j < d/2`, else `ggml[2(j − d/2) + 1]`,
1008/// per head.
1009fn rope_depermute_src_rows(rows: usize, n_head: usize) -> Vec<usize> {
1010    let d = rows / n_head;
1011    let half = d / 2;
1012    let mut map = Vec::with_capacity(rows);
1013    for h in 0..n_head {
1014        let base = h * d;
1015        for j in 0..d {
1016            let src = if j < half { 2 * j } else { 2 * (j - half) + 1 };
1017            map.push(base + src);
1018        }
1019    }
1020    map
1021}
1022
1023/// Reorders `values` (f32, `rows` equal-length rows) by the de-permute map.
1024fn depermute_rows_f32(values: Vec<f32>, rows: usize, n_head: usize) -> Vec<f32> {
1025    let row_len = values.len() / rows;
1026    let map = rope_depermute_src_rows(rows, n_head);
1027    let mut out = Vec::with_capacity(values.len());
1028    for src in map {
1029        out.extend_from_slice(&values[src * row_len..(src + 1) * row_len]);
1030    }
1031    out
1032}
1033
1034impl GgufSource {
1035    /// Fused-projection resolution: phi3 GGUFs store `attn_qkv` = `[q|k|v]`
1036    /// rows and `ffn_up` = `[gate|up]` rows (HF phi3 order). Given a split
1037    /// HF name, returns the fused ggml name plus the `(start, len)` row
1038    /// range to slice. Row slicing is exact for every supported dtype:
1039    /// packed formats store whole blocks per row (the column count is a
1040    /// multiple of the 256-value superblock), so a row range is a
1041    /// contiguous byte range.
1042    fn fused_slice(&self, name: &str) -> Option<(String, usize, usize)> {
1043        if self.metadata.architecture != "phi3" {
1044            return None;
1045        }
1046        let m = &self.metadata;
1047        let rest = name.strip_prefix("model.layers.")?;
1048        let (layer, rest) = rest.split_once('.')?;
1049        let q_rows = m.num_attention_heads * m.head_dim;
1050        let kv_rows = m.num_key_value_heads * m.head_dim;
1051        let ffn = m.intermediate_size;
1052        let (fused, start, len) = match rest {
1053            "self_attn.q_proj.weight" => ("attn_qkv.weight", 0, q_rows),
1054            "self_attn.k_proj.weight" => ("attn_qkv.weight", q_rows, kv_rows),
1055            "self_attn.v_proj.weight" => ("attn_qkv.weight", q_rows + kv_rows, kv_rows),
1056            "mlp.gate_proj.weight" => ("ffn_up.weight", 0, ffn),
1057            "mlp.up_proj.weight" => ("ffn_up.weight", ffn, ffn),
1058            _ => return None,
1059        };
1060        Some((format!("blk.{layer}.{fused}"), start, len))
1061    }
1062
1063    /// Looks up the ggml tensor serving HF `name`, with the row range to
1064    /// slice when it lives inside a fused projection (`None` range = whole
1065    /// tensor).
1066    fn resolve_tensor(&self, name: &str) -> Option<(&String, &TensorInfo, Option<(usize, usize)>)> {
1067        if let Some((fused, start, len)) = self.fused_slice(name) {
1068            if let Some((k, info)) = self.tensors.get_key_value(&fused) {
1069                return Some((k, info, Some((start, len))));
1070            }
1071        }
1072        let arch = self.metadata.architecture.as_str();
1073        self.tensors
1074            .iter()
1075            .find(|(k, _)| map_tensor_name(k, arch).as_deref() == Some(name))
1076            .map(|(k, info)| (k, info, None))
1077    }
1078
1079    /// Head count to de-permute `ggml_name` with, when this file's arch
1080    /// stores Q/K in llama.cpp's interleaved-pairs layout. `None` = serve
1081    /// the tensor verbatim.
1082    fn depermute_heads(&self, ggml_name: &str) -> Option<usize> {
1083        if !matches!(self.metadata.architecture.as_str(), "llama" | "mistral") {
1084            return None;
1085        }
1086        let rest = ggml_name.strip_prefix("blk.")?;
1087        let (_, rest) = rest.split_once('.')?;
1088        match rest {
1089            "attn_q.weight" | "attn_q.bias" => Some(self.metadata.num_attention_heads),
1090            "attn_k.weight" | "attn_k.bias" => Some(self.metadata.num_key_value_heads),
1091            _ => None,
1092        }
1093    }
1094}
1095
1096impl ModelSource for GgufSource {
1097    fn metadata(&self) -> &ModelMetadata {
1098        &self.metadata
1099    }
1100
1101    fn tensor_names(&self) -> Vec<String> {
1102        let arch = self.metadata.architecture.as_str();
1103        self.tensors
1104            .keys()
1105            .filter_map(|k| map_tensor_name(k, arch))
1106            .collect()
1107    }
1108
1109    fn open_tensor(&self, name: &str) -> Result<TensorReader<'_>> {
1110        // Find the ggml tensor mapping to this HF name (possibly a row
1111        // range of a fused projection).
1112        let (ggml_name, info, slice) = self
1113            .resolve_tensor(name)
1114            .ok_or_else(|| FormatError::TensorNotFound(name.to_string()))?;
1115
1116        let size = tensor_byte_size(info)?;
1117        let start = self.data_start + info.offset;
1118        let data = self
1119            .mmap
1120            .get(start..start + size)
1121            .ok_or_else(|| FormatError::Safetensors(format!("gguf tensor {} out of bounds", info.name)))?;
1122
1123        // HF layout = ggml dims reversed (row-major data needs no movement).
1124        let mut shape: Vec<usize> = info.dims.iter().rev().copied().collect();
1125        let data = match slice {
1126            None => data,
1127            Some((row_start, row_len)) => {
1128                let rows_total = shape.first().copied().unwrap_or(1).max(1);
1129                if size % rows_total != 0 {
1130                    return Err(FormatError::Safetensors(format!(
1131                        "gguf tensor {}: rows not byte-addressable for fused split",
1132                        info.name
1133                    )));
1134                }
1135                let row_bytes = size / rows_total;
1136                shape[0] = row_len;
1137                &data[row_start * row_bytes..(row_start + row_len) * row_bytes]
1138            }
1139        };
1140        let n: usize = shape.iter().product();
1141        let rows = shape.first().copied().unwrap_or(1).max(1);
1142        // Fused slices and RoPE de-permutation never co-occur (phi3 vs
1143        // llama/mistral arch gates).
1144        let permute = self.depermute_heads(ggml_name);
1145        // llama.cpp's gemma converters bake the `(1+w)` into stored norm
1146        // weights (their graph applies plain x̂·w); the engine keeps HF
1147        // semantics (x̂·(1+w) via the gemma norm flavor), so the +1 is
1148        // removed here — the same normalize-at-the-adapter rule as the
1149        // RoPE de-permutation. Verified: gemma-3-1b GGUF norms are exactly
1150        // safetensors + 1.0.
1151        let gemma_norm_offset = matches!(
1152            self.metadata.architecture.as_str(),
1153            "gemma3" | "gemma3_text"
1154        ) && ggml_name.ends_with("norm.weight");
1155
1156        // Passthrough dtypes: serve the mmap slice, unless the rows must be
1157        // de-interleaved (RoPE de-permutation) — then copy row-reordered.
1158        if gemma_norm_offset {
1159            if info.ggml_type != GGML_F32 {
1160                return Err(FormatError::UnsupportedDtype {
1161                    tensor: info.name.clone(),
1162                    dtype: format!("gemma norm must be F32, got ggml_type {}", info.ggml_type),
1163                });
1164            }
1165            let values: Vec<f32> = data
1166                .chunks_exact(4)
1167                .map(|b| f32::from_le_bytes(b.try_into().unwrap()) - 1.0)
1168                .collect();
1169            let bytes: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
1170            return Ok(TensorReader::owned(name.to_string(), shape, bytes));
1171        }
1172        if let GGML_F32 | GGML_F16 | GGML_BF16 = info.ggml_type {
1173            let dtype = match info.ggml_type {
1174                GGML_F32 => TensorDtype::F32,
1175                GGML_F16 => TensorDtype::F16,
1176                _ => TensorDtype::BF16,
1177            };
1178            return Ok(match permute {
1179                None => TensorReader::new(name.to_string(), shape, dtype, data),
1180                Some(n_head) => {
1181                    let row_bytes = size / rows;
1182                    let map = rope_depermute_src_rows(rows, n_head);
1183                    let mut out = Vec::with_capacity(size);
1184                    for src in map {
1185                        out.extend_from_slice(&data[src * row_bytes..(src + 1) * row_bytes]);
1186                    }
1187                    TensorReader::owned_with_dtype(name.to_string(), shape, dtype, out)
1188                }
1189            });
1190        }
1191
1192        let values = match info.ggml_type {
1193            GGML_Q4_0 => dequantize_q4_0(data, n)?,
1194            GGML_Q4_1 => dequantize_q4_1(data, n)?,
1195            GGML_Q5_0 => dequantize_q5_0(data, n)?,
1196            GGML_Q5_1 => dequantize_q5_1(data, n)?,
1197            GGML_Q8_0 => dequantize_q8_0(data, n)?,
1198            GGML_Q4_K => dequantize_q4_k(data, n)?,
1199            GGML_Q5_K => dequantize_q5_k(data, n)?,
1200            GGML_Q6_K => dequantize_q6_k(data, n)?,
1201            other => {
1202                return Err(FormatError::UnsupportedDtype {
1203                    tensor: info.name.clone(),
1204                    dtype: format!("ggml_type {other}"),
1205                });
1206            }
1207        };
1208        let values = match permute {
1209            Some(n_head) => depermute_rows_f32(values, rows, n_head),
1210            None => values,
1211        };
1212        let bytes: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
1213        Ok(TensorReader::owned(name.to_string(), shape, bytes))
1214    }
1215
1216    fn tokenizer(&self) -> Result<TokenizerSpec> {
1217        let add_bos = match self.kv.get("tokenizer.ggml.add_bos_token") {
1218            Some(MetaValue::Bool(b)) => Some(*b),
1219            _ => None,
1220        };
1221        let chat_template = match self.kv.get("tokenizer.chat_template") {
1222            Some(MetaValue::String(t)) => Some(t.clone()),
1223            _ => None,
1224        };
1225        Ok(TokenizerSpec {
1226            tokenizer_json: self.tokenizer_json.clone(),
1227            added_tokens: self.added_tokens.clone(),
1228            chat_template,
1229            add_bos,
1230        })
1231    }
1232
1233    fn open_tensor_quant(&self, name: &str) -> Result<Option<crate::QuantTensor<'_>>> {
1234        let Some((ggml_name, info, slice)) = self.resolve_tensor(name) else {
1235            return Ok(None);
1236        };
1237        let format = match info.ggml_type {
1238            GGML_Q4_0 => crate::QuantFormat::Q4_0,
1239            GGML_Q5_0 => crate::QuantFormat::Q5_0,
1240            GGML_Q8_0 => crate::QuantFormat::Q8_0,
1241            GGML_Q4_K => crate::QuantFormat::Q4K,
1242            GGML_Q5_K => crate::QuantFormat::Q5K,
1243            GGML_Q6_K => crate::QuantFormat::Q6K,
1244            _ => return Ok(None),
1245        };
1246        let size = tensor_byte_size(info)?;
1247        let start = self.data_start + info.offset;
1248        let data = self.mmap.get(start..start + size).ok_or_else(|| {
1249            FormatError::Safetensors(format!("gguf tensor {} out of bounds", info.name))
1250        })?;
1251        let mut shape: Vec<usize> = info.dims.iter().rev().copied().collect();
1252        // Row range of a fused projection: a contiguous packed byte range
1253        // (whole blocks per row), so the mmap slice narrows in place.
1254        let data = match slice {
1255            None => data,
1256            Some((row_start, row_len)) => {
1257                let rows_total = shape.first().copied().unwrap_or(1).max(1);
1258                if size % rows_total != 0 {
1259                    return Ok(None);
1260                }
1261                let row_bytes = size / rows_total;
1262                shape[0] = row_len;
1263                &data[row_start * row_bytes..(row_start + row_len) * row_bytes]
1264            }
1265        };
1266
1267        // RoPE de-permutation for packed weights: every supported block
1268        // format stores whole blocks per row, so reordering the packed
1269        // stream row-chunk-wise is exact. If the packed rows aren't
1270        // cleanly addressable, fall back to the dense path (which
1271        // de-permutes after dequantization).
1272        let data = match self.depermute_heads(ggml_name) {
1273            None => std::borrow::Cow::Borrowed(data),
1274            Some(n_head) => {
1275                let rows = shape.first().copied().unwrap_or(1).max(1);
1276                if size % rows != 0 {
1277                    return Ok(None);
1278                }
1279                let row_bytes = size / rows;
1280                let map = rope_depermute_src_rows(rows, n_head);
1281                let mut out = Vec::with_capacity(size);
1282                for src in map {
1283                    out.extend_from_slice(&data[src * row_bytes..(src + 1) * row_bytes]);
1284                }
1285                std::borrow::Cow::Owned(out)
1286            }
1287        };
1288        Ok(Some(crate::QuantTensor { format, shape, data }))
1289    }
1290
1291    fn sampler_defaults(&self) -> Option<SamplerConfig> {
1292        None
1293    }
1294}
1295
1296impl GgufSource {
1297    /// End-of-sequence ids from tokenizer metadata.
1298    pub fn eos_token_ids(&self) -> &[u32] {
1299        &self.eos_ids
1300    }
1301
1302    /// Model file path.
1303    pub fn path(&self) -> &Path {
1304        &self.path
1305    }
1306}
1307
1308#[cfg(test)]
1309mod depermute_tests {
1310    use super::rope_depermute_src_rows;
1311
1312    /// llama.cpp's forward permute: per head, rows [2, d/2] -> swap -> flat,
1313    /// i.e. ggml[2i] = hf[i], ggml[2i+1] = hf[d/2 + i].
1314    fn forward_permute(rows: &[Vec<u32>], n_head: usize) -> Vec<Vec<u32>> {
1315        let d = rows.len() / n_head;
1316        let mut out = Vec::with_capacity(rows.len());
1317        for h in 0..n_head {
1318            let head = &rows[h * d..(h + 1) * d];
1319            for i in 0..d / 2 {
1320                out.push(head[i].clone());
1321                out.push(head[d / 2 + i].clone());
1322            }
1323        }
1324        out
1325    }
1326
1327    #[test]
1328    fn depermute_inverts_llama_cpp_permute() {
1329        // 2 heads x head_dim 8 = 16 rows, each row tagged by its HF index.
1330        let hf: Vec<Vec<u32>> = (0..16).map(|i| vec![i, 100 + i]).collect();
1331        let ggml = forward_permute(&hf, 2);
1332        assert_ne!(hf, ggml, "permute must actually move rows");
1333        let map = rope_depermute_src_rows(16, 2);
1334        let recovered: Vec<Vec<u32>> = map.iter().map(|&src| ggml[src].clone()).collect();
1335        assert_eq!(recovered, hf, "de-permute must invert llama.cpp's layout");
1336    }
1337}
1338
1339#[cfg(test)]
1340mod quant_access_tests {
1341    use super::*;
1342    use crate::ModelSource;
1343
1344    /// Scratch diagnostic against a locally cached model; not part of CI.
1345    #[test]
1346    #[ignore]
1347    fn real_gguf_quant_access() {
1348        let path = dirs_home().join(".cache/combs/models/llama-3.2-1b-instruct-gguf/model.gguf");
1349        let src = GgufSource::load(&path).unwrap();
1350        for name in [
1351            "model.layers.0.mlp.gate_proj.weight",
1352            "model.layers.0.self_attn.q_proj.weight",
1353        ] {
1354            let qt = src.open_tensor_quant(name).unwrap();
1355            println!("{name}: {:?}", qt.map(|q| (q.format, q.shape, q.data.len())));
1356        }
1357    }
1358
1359    fn dirs_home() -> std::path::PathBuf {
1360        std::path::PathBuf::from(std::env::var("HOME").unwrap())
1361    }
1362}