Skip to main content

logicaffeine_compile/model/hf/
mod.rs

1//! A Hugging Face checkpoint directory read in place: `config.json` (its `text_config` merged over it as a multimodal
2//! checkpoint nests it), the safetensors shards its index names, and the tokenizer files. It is presented as the GGUF
3//! llama.cpp's converter (`convert_hf_to_gguf.py`) writes from it — the same architecture keys, tensor names, ggml dim
4//! order and element order — so [`ModelSpec::from_source`](super::spec::ModelSpec::from_source), the binder and the
5//! reference read it unchanged.
6//!
7//! Nothing is read whole: opening reads the shards' headers, and a tensor's rows are read when they are asked for. Most
8//! tensors are their bytes as stored, dims reversed into ggml's order; a dtype other than f32, f16 and bf16 (the
9//! 8-bit floats, the integers) is widened to f32, as the converter widens it. The converter's rearrangements are views
10//! over the stored bytes, kept in the checkpoint's own dtype:
11//!
12//!   • a row permutation — Llama's query and key rows into interleaved rotation pairs; the value heads of a Gated
13//!     DeltaNet block from grouped-by-key-head to tiled order;
14//!   • a column permutation or pick — the same reorder over an output projection's inputs, gpt-oss's interleaved
15//!     gate and up halves;
16//!   • a transpose of the last two axes — gpt-oss's expert matrices, latent attention's key half;
17//!   • a stack — a mixture's experts, one tensor per projection.
18//!
19//! Two converter steps change values: a norm stored as an offset from one (Gemma 3, Qwen3.5: the forward multiplies
20//! by `1 + w`) and Gated DeltaNet's decay stored as its logarithm (`A = -exp(A_log)`). The converter computes them in
21//! the dtype it holds the tensor in — f16 and f32 as stored, every other dtype widened to f32 first — and the
22//! checkpoint's own forward computes them in f32 from the stored value. The two differ only for an f16 checkpoint,
23//! whose converter rounds each result to f16; [`Recompute`] chooses which the reader presents: the converter's by
24//! default, so a directory reads as its converted GGUF, or the forward's. Either way `1 + w` is exact and the
25//! exponential correctly rounded; the converter's own exponential is torch's on the CPU (MKL's on x86 builds, chosen
26//! by the processor), which lands one unit in the last place off the correctly rounded value now and then.
27//!
28//! A configuration key is either read, known to leave the computation unchanged, or refused by name: a checkpoint is
29//! never read as something other than what it says it is.
30//!
31//! The tokenizer files are read as the converter reads them ([`HfCheckpoint::tokenizer_meta`]): the vocabulary, merges,
32//! special tokens and chat template it writes, the byte-pair pre-tokenizer named by the digest the converter's own
33//! probe gives under the directory's `tokenizer.json`, run as the `tokenizers` library runs it ([`HfTokenizer`]).
34
35use std::collections::{BTreeMap, BTreeSet, HashMap};
36use std::io;
37use std::path::{Path, PathBuf};
38use std::sync::OnceLock;
39
40use serde_json::{Map, Value};
41
42use super::gguf::MetaValue;
43use super::quant::{e4m3fn_to_f32, e5m2_to_f32, f16_to_f32, f32_to_f16, GgmlType, ScaleGrid};
44use super::reference::WeightSource;
45use super::safetensors::{self, Dtype, Entry, Shard};
46use super::spec::SpecSource;
47
48mod encode;
49mod onig;
50mod tiktoken;
51mod unicode_data;
52mod vocab;
53
54pub use encode::{nfc, HfPreTokenizer, HfTokenizer};
55pub use tiktoken::{split as tiktoken_split, Tiktoken};
56pub use unicode_data::{NORMAL_FORM_VERSION, PATTERN_VERSION};
57pub use vocab::{pre_tokenizer_named, PRE_TOKENIZER_DIGESTS, PRE_TOKENIZER_PROBE};
58
59fn invalid(msg: impl Into<String>) -> io::Error {
60    io::Error::new(io::ErrorKind::InvalidData, msg.into())
61}
62
63/// An elementwise step the converter applies to a tensor's values.
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub enum Op {
66    /// `1 + w`: a norm weight stored as its offset from one.
67    AddOne,
68    /// `-exp(w)`: a decay stored as its logarithm.
69    NegExp,
70}
71
72/// How the values a converter step computes (a norm's offset from one, a decay's logarithm) are presented.
73#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
74pub enum Recompute {
75    /// In the precision the converter computes them in: rounded to f16 for an f16 tensor, f32 otherwise. The converted
76    /// GGUF's values.
77    #[default]
78    AsConverted,
79    /// In f32 from the stored value, as the checkpoint's own forward computes them.
80    AsForward,
81}
82
83/// How a presented tensor's elements are found in the checkpoint.
84#[derive(Clone, Debug, PartialEq)]
85pub enum Form {
86    /// Source tensor `src` viewed as `[outer, m, n]`: output element `(b, i, j)` is source `(b, rows[i], cols[j])`,
87    /// or with `transpose` output `(b, j, i)` is.
88    Gather {
89        src: String,
90        outer: usize,
91        m: usize,
92        n: usize,
93        rows: Vec<usize>,
94        cols: Vec<usize>,
95        transpose: bool,
96    },
97    /// The forms' elements one after another: a stack along a new outermost axis.
98    Concat(Vec<Form>),
99    /// The forms side by side: each output row the parts' rows of that index one after another, every part as many rows.
100    Beside(Vec<Form>),
101    /// `op` applied to the inner form's values, in f32.
102    Map { inner: Box<Form>, op: Op },
103    /// Values the converter computes rather than reads (Llama 3's rotary factors), in f32.
104    Values(Vec<f32>),
105}
106
107impl Form {
108    /// The whole of source `src`, `m × n` (outermost dims folded into `m`).
109    fn whole(src: &str, entry: &Entry) -> Form {
110        let n = entry.shape.last().copied().unwrap_or(1) as usize;
111        let m = (entry.elements() as usize).checked_div(n).unwrap_or(0);
112        Form::Gather {
113            src: src.to_string(),
114            outer: 1,
115            m,
116            n,
117            rows: (0..m).collect(),
118            cols: (0..n).collect(),
119            transpose: false,
120        }
121    }
122
123    fn elements(&self) -> usize {
124        match self {
125            Form::Gather { outer, rows, cols, .. } => outer * rows.len() * cols.len(),
126            Form::Concat(parts) | Form::Beside(parts) => parts.iter().map(Form::elements).sum(),
127            Form::Map { inner, .. } => inner.elements(),
128            Form::Values(v) => v.len(),
129        }
130    }
131}
132
133/// One tensor as the converted GGUF holds it.
134#[derive(Clone, Debug)]
135struct View {
136    name: String,
137    /// ggml order: `dims[0]` the fastest-varying.
138    dims: Vec<u64>,
139    form: Form,
140}
141
142/// A Hugging Face checkpoint directory, presented as its converted GGUF.
143pub struct HfCheckpoint {
144    dir: PathBuf,
145    recompute: Recompute,
146    family: Family,
147    hparams: Hparams,
148    tokenizer: OnceLock<BTreeMap<String, MetaValue>>,
149    arch: String,
150    meta: BTreeMap<String, MetaValue>,
151    views: Vec<View>,
152    index: HashMap<String, usize>,
153    shards: Vec<Shard>,
154    located: HashMap<String, usize>,
155    /// An FP8 checkpoint's scaled weights, by name: each read as the converter's `dequant_simple` writes it.
156    scaled: HashMap<String, Scaled>,
157    /// The tensors the converter removes once it has dequantized: the scales, and activation scales nothing reads.
158    consumed: BTreeSet<String>,
159    unread: Vec<String>,
160    skipped: Vec<String>,
161}
162
163/// A weight stored beside its scale (an FP8 checkpoint's `<name>_scale_inv`), and the block its scale entries cover.
164struct Scaled {
165    scale: String,
166    block: Option<Vec<usize>>,
167    grid: OnceLock<Result<ScaleGrid, String>>,
168}
169
170/// The scaled weights and consumed tensors of a checkpoint whose `quantization_config` is FP8 (`quant_method` fp8), as
171/// the converter's `dequant_model` finds them: a `<name>_scale_inv` scales `<name>`, a Mistral `<stem>.qscale_weight`
172/// scales `<stem>.weight`, and activation scales (`.activation_scale`, `_activation_scale`, `.qscale_act`) are dropped.
173fn fp8_scales(h: &Hparams, located: &HashMap<String, usize>) -> io::Result<(HashMap<String, Scaled>, BTreeSet<String>)> {
174    let mut scaled = HashMap::new();
175    let mut consumed = BTreeSet::new();
176    let Some(q) = h.map.get("quantization_config").filter(|q| q.get("quant_method").and_then(Value::as_str) == Some("fp8")) else {
177        return Ok((scaled, consumed));
178    };
179    let block = match q.get("weight_block_size") {
180        None | Some(Value::Null) => None,
181        Some(Value::Array(b)) => Some(
182            b.iter()
183                .map(|x| x.as_u64().map(|x| x as usize).ok_or_else(|| invalid(format!("config.json: weight_block_size {b:?}"))))
184                .collect::<io::Result<Vec<usize>>>()?,
185        ),
186        Some(other) => return Err(invalid(format!("config.json: weight_block_size is {other}"))),
187    };
188    let mut names: Vec<&String> = located.keys().collect();
189    names.sort();
190    for name in names {
191        let weight = if let Some(w) = name.strip_suffix("_scale_inv") {
192            Some(w.to_string())
193        } else {
194            name.strip_suffix(".qscale_weight").map(|stem| format!("{stem}.weight"))
195        };
196        if let Some(weight) = weight {
197            if !located.contains_key(&weight) {
198                return Err(invalid(format!("{name} scales {weight}, which the checkpoint does not hold")));
199            }
200            scaled.insert(weight, Scaled { scale: name.clone(), block: block.clone(), grid: OnceLock::new() });
201            consumed.insert(name.clone());
202        } else if name.ends_with(".activation_scale") || name.ends_with("_activation_scale") || name.ends_with(".qscale_act") {
203            consumed.insert(name.clone());
204        }
205    }
206    Ok((scaled, consumed))
207}
208
209/// The merged hyperparameters, read as the converter's `find_hparam` reads them: the first key present wins, and a
210/// present `null` reads as absent.
211struct Hparams {
212    map: Map<String, Value>,
213    /// The checkpoint's class, as the configuration's top level names it (`Qwen3_5MoeForCausalLM`).
214    architecture: String,
215}
216
217impl Hparams {
218    fn find(&self, keys: &[&str]) -> Option<&Value> {
219        keys.iter().find_map(|k| self.map.get(*k)).filter(|v| !v.is_null())
220    }
221    fn u64(&self, keys: &[&str]) -> Option<u64> {
222        self.find(keys).and_then(Value::as_u64)
223    }
224    fn f64(&self, keys: &[&str]) -> Option<f64> {
225        self.find(keys).and_then(Value::as_f64)
226    }
227    fn need_u64(&self, key: &str) -> io::Result<u64> {
228        self.u64(&[key]).ok_or_else(|| invalid(format!("config.json: no `{key}`")))
229    }
230}
231
232/// The GGUF architecture keys the converter writes, in its order of writing; a later write of one key replaces it.
233struct Writer {
234    arch: String,
235    meta: BTreeMap<String, MetaValue>,
236}
237
238impl Writer {
239    fn put(&mut self, suffix: &str, v: MetaValue) {
240        self.meta.insert(format!("{}.{suffix}", self.arch), v);
241    }
242    fn u32(&mut self, suffix: &str, v: u64) {
243        self.put(suffix, MetaValue::U32(v as u32));
244    }
245    fn f32(&mut self, suffix: &str, v: f64) {
246        self.put(suffix, MetaValue::F32(v as f32));
247    }
248    fn bool(&mut self, suffix: &str, v: bool) {
249        self.put(suffix, MetaValue::Bool(v));
250    }
251    fn str(&mut self, suffix: &str, v: &str) {
252        self.put(suffix, MetaValue::Str(v.as_bytes().to_vec()));
253    }
254}
255
256/// The families this reader presents, by the converter class each checkpoint's architecture selects.
257#[derive(Clone, Copy, Debug, PartialEq, Eq)]
258enum Family {
259    Llama,
260    Qwen2,
261    Qwen3,
262    Qwen3Moe,
263    Qwen2Moe,
264    Qwen35,
265    Qwen35Moe,
266    Qwen3Next,
267    Gemma,
268    Gemma2,
269    Gemma3,
270    GptOss,
271    Deepseek2,
272    Llama4,
273    MinimaxM2,
274    Phi3,
275    Glm4,
276    Glm4Moe,
277    Qwen4Exp,
278}
279
280impl Family {
281    fn of(architecture: &str) -> Option<Family> {
282        Some(match architecture {
283            "LlamaForCausalLM" | "MistralForCausalLM" | "MixtralForCausalLM" => Family::Llama,
284            "Qwen2ForCausalLM" => Family::Qwen2,
285            "Qwen3ForCausalLM" => Family::Qwen3,
286            "Qwen3MoeForCausalLM" => Family::Qwen3Moe,
287            "Qwen2MoeForCausalLM" => Family::Qwen2Moe,
288            "Qwen3_5ForConditionalGeneration" | "Qwen3_5ForCausalLM" => Family::Qwen35,
289            "Qwen3_5MoeForConditionalGeneration" | "Qwen3_5MoeForCausalLM" => Family::Qwen35Moe,
290            "Qwen3NextForCausalLM" => Family::Qwen3Next,
291            "GemmaForCausalLM" => Family::Gemma,
292            "Gemma2ForCausalLM" => Family::Gemma2,
293            "Gemma3ForCausalLM" | "Gemma3ForConditionalGeneration" => Family::Gemma3,
294            "GptOssForCausalLM" => Family::GptOss,
295            "DeepseekV2ForCausalLM" | "DeepseekV3ForCausalLM" => Family::Deepseek2,
296            "Llama4ForCausalLM" | "Llama4ForConditionalGeneration" => Family::Llama4,
297            "MiniMaxM2ForCausalLM" => Family::MinimaxM2,
298            "Phi3ForCausalLM" => Family::Phi3,
299            "Glm4ForCausalLM" => Family::Glm4,
300            "Glm4MoeForCausalLM" => Family::Glm4Moe,
301            "Qwen4ExpForConditionalGeneration" | "Qwen4ExpForCausalLM" => Family::Qwen4Exp,
302            _ => return None,
303        })
304    }
305
306    fn arch(self) -> &'static str {
307        match self {
308            Family::Llama => "llama",
309            Family::Qwen2 => "qwen2",
310            Family::Qwen3 => "qwen3",
311            Family::Qwen3Moe => "qwen3moe",
312            Family::Qwen2Moe => "qwen2moe",
313            Family::Qwen35 => "qwen35",
314            Family::Qwen35Moe => "qwen35moe",
315            Family::Qwen3Next => "qwen3next",
316            Family::Gemma => "gemma",
317            Family::Gemma2 => "gemma2",
318            Family::Gemma3 => "gemma3",
319            Family::GptOss => "gpt-oss",
320            Family::Deepseek2 => "deepseek2",
321            Family::Llama4 => "llama4",
322            Family::MinimaxM2 => "minimax-m2",
323            Family::Phi3 => "phi3",
324            Family::Glm4 => "glm4",
325            Family::Glm4Moe => "glm4moe",
326            Family::Qwen4Exp => "qwen4exp",
327        }
328    }
329
330    /// Qwen3.5's hybrid of gated attention and Gated DeltaNet, dense or a mixture, and Qwen3-Next's: their layout rules.
331    fn hybrid(self) -> bool {
332        matches!(self, Family::Qwen35 | Family::Qwen35Moe | Family::Qwen3Next | Family::Qwen4Exp)
333    }
334
335    /// Qwen's mixture: a softmax router beside a shared expert scaled by its own gate.
336    fn qwen_mixture(self) -> bool {
337        matches!(self, Family::Qwen2Moe | Family::Qwen35Moe | Family::Qwen3Next | Family::Qwen4Exp)
338    }
339}
340
341/// Keys every family may carry that do not change what the checkpoint computes: identity, provenance, generation
342/// defaults, training-only settings, and the multimodal wrapper's token ids and tower configuration (the tower's
343/// tensors are skipped as the converter skips them).
344const INERT: &[&str] = &[
345    "_name_or_path",
346    "architectures",
347    "model_type",
348    "transformers_version",
349    "torch_dtype",
350    "dtype",
351    "use_cache",
352    "initializer_range",
353    "bos_token_id",
354    "eos_token_id",
355    "pad_token_id",
356    "attention_dropout",
357    "output_router_logits",
358    "router_aux_loss_coef",
359    "router_jitter_noise",
360    "pretraining_tp",
361    "unsloth_fixed",
362    "unsloth_version",
363    "boi_token_index",
364    "eoi_token_index",
365    "image_token_index",
366    "mm_tokens_per_image",
367    "cache_implementation",
368    "image_token_id",
369    "video_token_id",
370    "vision_start_token_id",
371    "vision_end_token_id",
372    "vision_config",
373    "text_config",
374    // Which of a multimodal checkpoint's towers a server loads (Qwen3.8's); the reader reads the language model alone.
375    "language_model_only",
376    "tie_word_embeddings",
377    "max_window_layers",
378    "_sliding_window_pattern",
379    "mamba_ssm_dtype",
380    "mtp_use_dedicated_embeddings",
381    "num_nextn_predict_layers",
382    "attention_bias",
383    "qkv_bias",
384    // Where a checkpoint's code lives, and settings only training reads (DeepSeek's and Kimi's).
385    "auto_map",
386    "aux_loss_alpha",
387    "seq_aux",
388    "ep_size",
389    // `PretrainedConfig`'s own attributes, which transformers 4 serializes whole (Kimi K2's later releases): generation
390    // defaults, label maps and framework switches. `pruned_heads`, `add_cross_attention` and `is_encoder_decoder`
391    // change the model and are refused unless at their defaults (`validate`).
392    "_attn_implementation_autoset",
393    "add_cross_attention",
394    "bad_words_ids",
395    "begin_suppress_tokens",
396    "chunk_size_feed_forward",
397    "cross_attention_hidden_size",
398    "decoder_start_token_id",
399    "diversity_penalty",
400    "do_sample",
401    "early_stopping",
402    "encoder_no_repeat_ngram_size",
403    "exponential_decay_length_penalty",
404    "finetuning_task",
405    "forced_bos_token_id",
406    "forced_eos_token_id",
407    "id2label",
408    "is_decoder",
409    "is_encoder_decoder",
410    "label2id",
411    "length_penalty",
412    "max_length",
413    "min_length",
414    "no_repeat_ngram_size",
415    "num_beam_groups",
416    "num_beams",
417    "num_return_sequences",
418    "output_attentions",
419    "output_hidden_states",
420    "output_scores",
421    "prefix",
422    "problem_type",
423    "pruned_heads",
424    "remove_invalid_values",
425    "repetition_penalty",
426    "return_dict",
427    "return_dict_in_generate",
428    "sep_token_id",
429    "suppress_tokens",
430    "task_specific_params",
431    "temperature",
432    "tf_legacy_loss",
433    "tie_encoder_decoder",
434    "tokenizer_class",
435    "top_k",
436    "top_p",
437    "torchscript",
438    "typical_p",
439    "use_bfloat16",
440];
441
442/// The keys a family's converter reads, beyond [`INERT`].
443fn read_keys(f: Family) -> &'static [&'static str] {
444    const DECODER: &[&str] = &[
445        "hidden_size",
446        "intermediate_size",
447        "num_hidden_layers",
448        "num_attention_heads",
449        "num_key_value_heads",
450        "head_dim",
451        "rms_norm_eps",
452        "max_position_embeddings",
453        "vocab_size",
454        "rope_theta",
455        "rope_scaling",
456        "rope_parameters",
457        "hidden_act",
458    ];
459    let own: &[&str] = match f {
460        Family::Llama => &["mlp_bias", "num_local_experts", "num_experts_per_tok", "sliding_window"],
461        Family::Qwen2 => &["use_sliding_window", "sliding_window", "layer_types"],
462        Family::Qwen3 => &["use_sliding_window", "sliding_window", "layer_types"],
463        Family::Qwen3Moe => &[
464            "use_sliding_window",
465            "sliding_window",
466            "layer_types",
467            "moe_intermediate_size",
468            "num_experts",
469            "num_local_experts",
470            "num_experts_per_tok",
471            "norm_topk_prob",
472            "decoder_sparse_step",
473            "mlp_only_layers",
474        ],
475        Family::Qwen2Moe => &[
476            "use_sliding_window",
477            "sliding_window",
478            "layer_types",
479            "moe_intermediate_size",
480            "shared_expert_intermediate_size",
481            "num_experts",
482            "num_experts_per_tok",
483            "norm_topk_prob",
484            "decoder_sparse_step",
485            "mlp_only_layers",
486        ],
487        Family::Qwen3Next => &[
488            "decoder_sparse_step",
489            "full_attention_interval",
490            "layer_types",
491            "linear_conv_kernel_dim",
492            "linear_key_head_dim",
493            "linear_num_key_heads",
494            "linear_num_value_heads",
495            "linear_value_head_dim",
496            "mlp_only_layers",
497            "moe_intermediate_size",
498            "norm_topk_prob",
499            "num_experts",
500            "num_experts_per_tok",
501            "partial_rotary_factor",
502            "shared_expert_intermediate_size",
503            "use_sliding_window",
504            "output_gate_type",
505        ],
506        Family::Qwen4Exp => &[
507            "moe_intermediate_size",
508            "shared_expert_intermediate_size",
509            "num_experts",
510            "num_experts_per_tok",
511            "norm_topk_prob",
512            "full_attention_interval",
513            "layer_types",
514            "linear_conv_kernel_dim",
515            "linear_key_head_dim",
516            "linear_num_key_heads",
517            "linear_num_value_heads",
518            "linear_value_head_dim",
519            "mtp_num_hidden_layers",
520            "mtp",
521            "partial_rotary_factor",
522            "output_gate_type",
523            "hc_count",
524            "hc_lowrank",
525            "indexer_n_heads",
526            "indexer_kv_heads",
527            "indexer_head_dim",
528            "indexer_budget",
529            "indexer_compress_ratio",
530            "ple_layer_ids",
531            "ple_embed_dim",
532            "ple_conv_kernel_size",
533            "ngram_size",
534            "heads_per_ngram",
535            "ngram_vocab_size_base",
536            "make_ngram_vocab_size_divisible_by",
537            "seed",
538            "split_ngram_parts",
539            "number_of_conv_states",
540        ],
541        Family::Qwen35 | Family::Qwen35Moe => &[
542            "moe_intermediate_size",
543            "shared_expert_intermediate_size",
544            "num_experts",
545            "num_experts_per_tok",
546            "attn_output_gate",
547            "full_attention_interval",
548            "layer_types",
549            "linear_conv_kernel_dim",
550            "linear_key_head_dim",
551            "linear_num_key_heads",
552            "linear_num_value_heads",
553            "linear_value_head_dim",
554            "mtp_num_hidden_layers",
555            "mlp_only_layers",
556            "partial_rotary_factor",
557            "output_gate_type",
558        ],
559        Family::Gemma => &["hidden_activation", "use_bidirectional_attention"],
560        Family::Gemma2 => &[
561            "hidden_activation",
562            "use_bidirectional_attention",
563            "query_pre_attn_scalar",
564            "sliding_window",
565            "sliding_window_size",
566            "layer_types",
567            "attn_logit_softcapping",
568            "final_logit_softcapping",
569        ],
570        Family::Gemma3 => &[
571            "hidden_activation",
572            "query_pre_attn_scalar",
573            "sliding_window",
574            "sliding_window_pattern",
575            "layer_types",
576            "rope_local_base_freq",
577            "attn_logit_softcapping",
578            "final_logit_softcapping",
579            "use_bidirectional_attention",
580        ],
581        Family::GptOss => &[
582            "num_local_experts",
583            "num_experts_per_tok",
584            "experts_per_token",
585            "initial_context_length",
586            "sliding_window",
587            "layer_types",
588            "swiglu_limit",
589            "swiglu_alpha",
590        ],
591        Family::Deepseek2 => &[
592            "moe_intermediate_size",
593            "n_routed_experts",
594            "n_shared_experts",
595            "num_experts_per_tok",
596            "n_group",
597            "topk_group",
598            "first_k_dense_replace",
599            "kv_lora_rank",
600            "q_lora_rank",
601            "qk_nope_head_dim",
602            "qk_rope_head_dim",
603            "qk_head_dim",
604            "v_head_dim",
605            "routed_scaling_factor",
606            "norm_topk_prob",
607            "scoring_func",
608            "topk_method",
609            "rope_interleave",
610            "moe_layer_freq",
611        ],
612        Family::Llama4 => &[
613            "intermediate_size_mlp",
614            "num_local_experts",
615            "num_experts_per_tok",
616            "interleave_moe_layer_step",
617            "moe_layers",
618            "use_qk_norm",
619            "no_rope_layers",
620            "no_rope_layer_interval",
621            "attention_chunk_size",
622            "layer_types",
623            "attn_temperature_tuning",
624            "floor_scale",
625            "attn_scale",
626            "attention_bias",
627            "for_llm_compressor",
628        ],
629        Family::Glm4 => &["partial_rotary_factor"],
630        Family::Glm4Moe => &[
631            "partial_rotary_factor",
632            "moe_intermediate_size",
633            "n_routed_experts",
634            "n_shared_experts",
635            "num_experts_per_tok",
636            "first_k_dense_replace",
637            "routed_scaling_factor",
638            "norm_topk_prob",
639            "n_group",
640            "topk_group",
641            "use_qk_norm",
642        ],
643        Family::Phi3 => &[
644            "partial_rotary_factor",
645            "original_max_position_embeddings",
646            "sliding_window",
647            "resid_pdrop",
648            "embd_pdrop",
649            "lm_head_bias",
650            "mlp_bias",
651            "full_attn_mod",
652            "interpolate_factor",
653        ],
654        Family::MinimaxM2 => &[
655            "num_local_experts",
656            "num_experts_per_tok",
657            "rotary_dim",
658            "partial_rotary_factor",
659            "use_qk_norm",
660            "qk_norm_type",
661            "scoring_func",
662            "use_routing_bias",
663            "shared_intermediate_size",
664            "shared_moe_mode",
665            "mlp_intermediate_size",
666            "attn_type_list",
667            "layernorm_full_attention_beta",
668            "layernorm_linear_attention_beta",
669            "layernorm_mlp_beta",
670            "sliding_window",
671            "use_mtp",
672            "num_mtp_modules",
673            "mtp_transformer_layers",
674        ],
675    };
676    own.iter().copied().chain(DECODER.iter().copied()).collect::<Vec<_>>().leak()
677}
678
679/// Refuse a configuration this reader would read as something other than what it says: an unknown key, or a known
680/// key whose value the converter's GGUF (and so the spec) does not carry.
681fn validate(f: Family, h: &Hparams) -> io::Result<()> {
682    let known: BTreeSet<&str> =
683        INERT.iter().copied().chain(read_keys(f).iter().copied()).chain(["quantization_config"]).collect();
684    let mut unknown: Vec<&str> = h.map.keys().map(String::as_str).filter(|k| !known.contains(k)).collect();
685    unknown.sort();
686    if let Some(k) = unknown.first() {
687        return Err(invalid(format!("config.json: `{k}` is a key the {} intake does not read; unknown keys: {unknown:?}", f.arch())));
688    }
689    // An FP8 checkpoint's weights are read beside their scales (`fp8_scales`); every other quantized format is refused.
690    if let Some(q) = h.map.get("quantization_config").filter(|q| !q.is_null()) {
691        let method = q.get("quant_method").and_then(Value::as_str).unwrap_or("unnamed");
692        if method != "fp8" {
693            return Err(invalid(format!(
694                "config.json: `quantization_config` names a `{method}` checkpoint, a format the quantized-format intake does not read yet"
695            )));
696        }
697    }
698    let refuse = |k: &str, why: &str| Err(invalid(format!("config.json: `{k}` {why}")));
699    let get = |k: &str| h.map.get(k).filter(|v| !v.is_null());
700    let act = h.find(&["hidden_act", "hidden_activation"]).and_then(Value::as_str);
701    // llama.cpp fixes a Gemma's score scale by the stack's depth: `1/√(d/heads)` at the 27B stack (46 blocks for Gemma
702    // 2, 62 for Gemma 3), `1/√head_dim` otherwise; the converter writes no key for it.
703    let gemma_scale = |deep: u64| -> io::Result<()> {
704        let Some(scalar) = get("query_pre_attn_scalar").and_then(Value::as_u64) else { return Ok(()) };
705        let layers = h.need_u64("num_hidden_layers")?;
706        let fixed = if layers == deep {
707            h.need_u64("hidden_size")? / h.need_u64("num_attention_heads")?
708        } else {
709            h.u64(&["head_dim"]).unwrap_or(256)
710        };
711        if scalar != fixed {
712            return refuse(
713                "query_pre_attn_scalar",
714                &format!("is {scalar}; llama.cpp scales this stack's scores by 1/√{fixed}, and its GGUF says no other"),
715            );
716        }
717        Ok(())
718    };
719    if matches!(f, Family::Gemma | Family::Gemma2 | Family::Gemma3) && get("use_bidirectional_attention").and_then(Value::as_bool) == Some(true) {
720        return refuse("use_bidirectional_attention", "is true; a causal decoder is read here");
721    }
722    match f {
723        // transformers reads Gemma's legacy `gelu` as the tanh approximation, the activation llama.cpp computes.
724        Family::Gemma => {
725            for key in ["hidden_act", "hidden_activation"] {
726                if get(key).and_then(Value::as_str).is_some_and(|a| a != "gelu_pytorch_tanh" && a != "gelu") {
727                    return refuse(key, "is not gelu_pytorch_tanh (or its legacy name gelu), the activation the gemma spec reads");
728                }
729            }
730            h.need_u64("head_dim")?;
731        }
732        Family::Gemma2 => {
733            for key in ["hidden_act", "hidden_activation"] {
734                if get(key).and_then(Value::as_str).is_some_and(|a| a != "gelu_pytorch_tanh") {
735                    return refuse(key, "is not gelu_pytorch_tanh, the activation the gemma2 spec reads");
736                }
737            }
738            gemma_scale(46)?;
739            h.need_u64("head_dim")?;
740            for key in ["attn_logit_softcapping", "final_logit_softcapping"] {
741                if !get(key).and_then(Value::as_f64).is_some_and(|c| c > 0.0) {
742                    return refuse(key, "is not a positive number; llama.cpp caps every Gemma 2, and its GGUF says no other");
743                }
744            }
745            let window = h.need_u64("sliding_window")?;
746            if get("sliding_window_size").and_then(Value::as_u64).is_some_and(|w| w != window) {
747                return refuse("sliding_window_size", &format!("is not sliding_window ({window}), the one window the GGUF carries"));
748            }
749            if let Some(Value::Array(types)) = h.map.get("layer_types") {
750                let alternating = types.len() as u64 == h.need_u64("num_hidden_layers")?
751                    && types.iter().enumerate().all(|(l, t)| t.as_str() == Some(if l % 2 == 0 { "sliding_attention" } else { "full_attention" }));
752                if !alternating {
753                    return refuse("layer_types", "is not a window on every other layer from the first, the pattern llama.cpp fixes for Gemma 2");
754                }
755            }
756        }
757        Family::Gemma3 => {
758            if act.is_some_and(|a| a != "gelu_pytorch_tanh") {
759                return refuse("hidden_activation", "is not gelu_pytorch_tanh, the activation the gemma3 spec reads");
760            }
761            gemma_scale(62)?;
762            if get("attn_logit_softcapping").is_some() {
763                return refuse("attn_logit_softcapping", "is set; Gemma 3 attention has no cap");
764            }
765        }
766        // transformers 4's flag for experts stored one linear each (for llm-compressor), where the releases fuse them.
767        Family::Llama4 => {
768            if get("for_llm_compressor").and_then(Value::as_bool) == Some(true) {
769                return refuse("for_llm_compressor", "is true; experts stored one linear each are a layout the llama4 intake does not read");
770            }
771        }
772        Family::GptOss => {
773            if act.is_some_and(|a| a != "silu") {
774                return refuse("hidden_act", "is not silu");
775            }
776            if get("swiglu_limit").and_then(Value::as_f64).is_some_and(|v| v != 7.0) {
777                return refuse("swiglu_limit", "is not 7.0, the clamp the gpt-oss spec reads");
778            }
779            if get("swiglu_alpha").and_then(Value::as_f64).is_some_and(|v| v != 1.702) {
780                return refuse("swiglu_alpha", "is not 1.702, the slope the gpt-oss spec reads");
781            }
782            // OpenAI's own names for two settings the converter reads under transformers' names: each must agree.
783            if get("experts_per_token").is_some_and(|v| Some(v) != get("num_experts_per_tok")) {
784                return refuse("experts_per_token", "differs from num_experts_per_tok, which the converter reads");
785            }
786            let original = rope_parameters(h).get("original_max_position_embeddings").cloned();
787            if get("initial_context_length").is_some_and(|v| original.as_ref().is_some_and(|o| o != v)) {
788                return refuse("initial_context_length", "differs from the rotation's original_max_position_embeddings, which the converter reads");
789            }
790        }
791        _ => {
792            if act.is_some_and(|a| a != "silu") {
793                return refuse("hidden_act", "is not silu, the activation this family's spec reads");
794            }
795        }
796    }
797    if get("use_sliding_window").and_then(Value::as_bool) == Some(true) {
798        return refuse("use_sliding_window", "is true; this family's GGUF carries no window");
799    }
800    if get("pruned_heads").is_some_and(|v| v.as_object().is_none_or(|o| !o.is_empty())) {
801        return refuse("pruned_heads", "names pruned heads, which the converter's GGUF does not carry");
802    }
803    for k in ["add_cross_attention", "is_encoder_decoder"] {
804        if get(k).and_then(Value::as_bool) == Some(true) {
805            return refuse(k, "is true; a decoder alone is read here");
806        }
807    }
808    if f == Family::Deepseek2 && get("moe_layer_freq").is_some_and(|v| v.as_u64() != Some(1)) {
809        return refuse("moe_layer_freq", "is not 1; llama.cpp's deepseek2 reads every block past the dense lead as a mixture");
810    }
811    if get("mlp_bias").and_then(Value::as_bool) == Some(true) {
812        return refuse("mlp_bias", "is true; the spec reads no FFN biases");
813    }
814    if let Some(a) = get("mlp_only_layers").and_then(Value::as_array) {
815        if !a.is_empty() {
816            return refuse("mlp_only_layers", "names dense layers inside a mixture, which the spec does not read");
817        }
818    }
819    if get("decoder_sparse_step").and_then(Value::as_u64).is_some_and(|s| s != 1) {
820        return refuse("decoder_sparse_step", "is not 1; every layer is read as a mixture");
821    }
822    // llama.cpp fixes by architecture whether the kept top-k weights are renormalized, and the converter writes no key
823    // for it: Qwen2-MoE's never are, Qwen3-MoE's always (transformers' default for both is not to).
824    let renormalized = get("norm_topk_prob").and_then(Value::as_bool).unwrap_or(f == Family::Qwen4Exp);
825    match f {
826        Family::Qwen2Moe if renormalized => {
827            return refuse("norm_topk_prob", "is true; llama.cpp's qwen2moe keeps the top-k softmax weights as they are")
828        }
829        Family::Qwen3Moe | Family::Qwen3Next | Family::Qwen4Exp if !renormalized => {
830            return refuse("norm_topk_prob", "is not true; llama.cpp's qwen3moe renormalizes the top-k softmax weights")
831        }
832        _ => {}
833    }
834    if f.hybrid() && get("attn_output_gate").and_then(Value::as_bool) == Some(false) {
835        return refuse("attn_output_gate", "is false; Qwen3.5's attention is read with its output gate");
836    }
837    // Gated DeltaNet's gated norm multiplies by its gate's activation: SiLU (swish, as Qwen3.8 names it) in the
838    // converter's qwen35 and qwen3next GGUFs, the sigmoid llama.cpp's qwen4exp fixes. Transformers gates a qwen4_exp
839    // configuration that names none by `hidden_act`, which no qwen4exp GGUF can say.
840    if f == Family::Qwen4Exp {
841        match get("output_gate_type").and_then(Value::as_str) {
842            Some("sigmoid") => {}
843            Some(other) => return refuse("output_gate_type", &format!("is `{other}`; llama.cpp's qwen4exp gates Gated DeltaNet by the sigmoid")),
844            None => return refuse("output_gate_type", "is unset; transformers gates by `hidden_act`, llama.cpp's qwen4exp by the sigmoid"),
845        }
846        if get("indexer_kv_heads").and_then(Value::as_u64).is_some_and(|k| k != 1) {
847            return refuse("indexer_kv_heads", "is not 1; QSA scores every index query against one key head");
848        }
849        if get("ple_layer_ids").and_then(Value::as_array).is_some_and(|l| l.len() > 1) {
850            return refuse("ple_layer_ids", "names more than one layer; the converter's GGUF holds one n-gram table");
851        }
852        // Transformers derives its cache's convolution states from the n-gram layers (the history and the dilated
853        // convolution beside Gated DeltaNet's) and saves the count.
854        let ple = get("ple_layer_ids").and_then(Value::as_array).is_some_and(|l| !l.is_empty());
855        if get("number_of_conv_states").and_then(Value::as_u64).is_some_and(|n| n != if ple { 3 } else { 1 }) {
856            return refuse("number_of_conv_states", "is not the count the n-gram layers give (3 with them, 1 without)");
857        }
858        if let Some(mtp) = get("mtp") {
859            let stated = get("mtp_num_hidden_layers").and_then(Value::as_u64);
860            if mtp.get("num_hidden_layers").and_then(Value::as_u64).is_some_and(|n| Some(n) != stated) {
861                return refuse("mtp", "counts draft layers other than `mtp_num_hidden_layers`");
862            }
863        }
864    } else if let Some(gate) = get("output_gate_type").filter(|g| !matches!(g.as_str(), Some("silu" | "swish"))) {
865        return refuse("output_gate_type", &format!("is {gate}; Gated DeltaNet's norm is read gated by SiLU (swish)"));
866    }
867    if f == Family::Llama && get("sliding_window").is_some() {
868        return refuse("sliding_window", "is set; the llama GGUF carries no window, and llama.cpp attends every position");
869    }
870    if matches!(f, Family::Qwen2 | Family::Qwen3 | Family::Qwen3Moe | Family::Qwen2Moe) {
871        if let Some(types) = get("layer_types").and_then(Value::as_array) {
872            if types.iter().any(|t| t.as_str() != Some("full_attention")) {
873                return refuse("layer_types", "names a layer that is not full attention");
874            }
875        }
876    }
877    if f == Family::Llama4 {
878        // The converter's GGUF carries none of these: llama.cpp fixes them, so a configuration that differs is a model
879        // the GGUF cannot say.
880        let n = |k: &str| get(k).and_then(Value::as_f64);
881        if n("attention_chunk_size").is_some_and(|c| c != 8192.0) {
882            return refuse("attention_chunk_size", "is not 8192, the chunk llama.cpp's llama4 attends within");
883        }
884        if n("floor_scale").is_some_and(|c| c != 8192.0) {
885            return refuse("floor_scale", "is not 8192, the attention temperature's floor llama.cpp fixes");
886        }
887        if n("attn_scale").is_some_and(|c| c != 0.1) {
888            return refuse("attn_scale", "is not 0.1, the attention temperature's scale llama.cpp fixes");
889        }
890        if get("attn_temperature_tuning").and_then(Value::as_bool) == Some(false) {
891            return refuse("attn_temperature_tuning", "is false; llama.cpp tunes every rotation-free layer's temperature");
892        }
893        if get("attention_bias").and_then(Value::as_bool) == Some(true) {
894            return refuse("attention_bias", "is true; the spec reads no attention biases");
895        }
896        let experts = get("num_local_experts").and_then(Value::as_u64).unwrap_or(0);
897        if get("use_qk_norm").and_then(Value::as_bool).is_some_and(|q| q != (experts != 128)) {
898            return refuse("use_qk_norm", "disagrees with llama.cpp's rule: a QK norm unless the model has 128 experts");
899        }
900        let layers = get("num_hidden_layers").and_then(Value::as_u64).unwrap_or(0) as usize;
901        if let Some(a) = get("no_rope_layers").and_then(Value::as_array) {
902            let every_fourth: Vec<u64> = (0..layers).map(|l| u64::from((l + 1) % 4 != 0)).collect();
903            if a.iter().map(Value::as_u64).collect::<Option<Vec<u64>>>() != Some(every_fourth) {
904                return refuse("no_rope_layers", "is not every fourth layer unrotated, the pattern llama.cpp fixes");
905            }
906        }
907        let step = get("interleave_moe_layer_step").and_then(Value::as_u64).unwrap_or(1) as usize;
908        if let Some(a) = get("moe_layers").and_then(Value::as_array) {
909            let stepped: Vec<u64> = (0..layers).filter(|l| step > 0 && (l + 1) % step == 0).map(|l| l as u64).collect();
910            if a.iter().map(Value::as_u64).collect::<Option<Vec<u64>>>() != Some(stepped) {
911                return refuse("moe_layers", "is not every interleave_moe_layer_step-th layer, the layers the GGUF names");
912            }
913        }
914    }
915    if f == Family::Deepseek2 && get("rope_interleave").and_then(Value::as_bool) == Some(false) {
916        return refuse("rope_interleave", "is false; latent attention is read with interleaved rotation");
917    }
918    if matches!(f, Family::Glm4 | Family::Glm4Moe) && rope_parameters(h).contains_key("mrope_section") {
919        return refuse("rope_parameters", "names `mrope_section`: a GLM vision model's multimodal rotation is not read yet");
920    }
921    if f == Family::Phi3 {
922        if get("lm_head_bias").and_then(Value::as_bool) == Some(true) {
923            return refuse("lm_head_bias", "is true; the spec reads no head bias");
924        }
925        for (k, one) in [("full_attn_mod", 1.0), ("interpolate_factor", 1.0)] {
926            if get(k).and_then(Value::as_f64).is_some_and(|v| v != one) {
927                return refuse(k, "is not 1; the converter's Phi-3 class reads no such setting");
928            }
929        }
930        let longrope = h.map.get("rope_scaling").or_else(|| h.map.get("rope_parameters")).and_then(|r| {
931            r.get("rope_type").or_else(|| r.get("type")).and_then(Value::as_str).map(|t| t == "longrope" || t == "su")
932        });
933        if longrope == Some(true) {
934            return refuse("rope_scaling", "is LongRoPE (Phi-3.5, Phi-4-mini), whose factor tables are not read yet");
935        }
936    }
937    if f == Family::MinimaxM2 {
938        // The converter's MiniMaxM2 class writes none of these: a value other than the one its graph fixes is a model the
939        // GGUF cannot say.
940        if let Some(types) = get("attn_type_list").and_then(Value::as_array) {
941            if types.iter().any(|t| t.as_u64() != Some(1)) {
942                return refuse("attn_type_list", "names a linear-attention layer (MiniMax-01's); every MiniMax-M2 layer is full attention");
943            }
944        }
945        for beta in ["layernorm_full_attention_beta", "layernorm_linear_attention_beta", "layernorm_mlp_beta"] {
946            if get(beta).and_then(Value::as_f64).is_some_and(|b| b != 1.0) {
947                return refuse(beta, "is not 1.0; the residual adds each sublayer's output unscaled");
948            }
949        }
950        if get("shared_intermediate_size").and_then(Value::as_u64).is_some_and(|w| w > 0) {
951            return refuse("shared_intermediate_size", "is not 0; the converter's MiniMax-M2 writes no shared expert");
952        }
953        if get("sliding_window").is_some() {
954            return refuse("sliding_window", "is set; MiniMax-M2 attends every position");
955        }
956        let rotary = get("rotary_dim").and_then(Value::as_u64);
957        let head = get("head_dim").and_then(Value::as_u64);
958        let factor = h.map.get("partial_rotary_factor").or_else(|| h.map.get("rope_parameters").and_then(|r| r.get("partial_rotary_factor")));
959        if let (Some(r), Some(hd), Some(f)) = (rotary, head, factor.and_then(Value::as_f64)) {
960            if (r as f64 - f * hd as f64).abs() > 0.5 {
961                return refuse("rotary_dim", "disagrees with partial_rotary_factor of head_dim");
962            }
963        }
964    }
965    Ok(())
966}
967
968impl HfCheckpoint {
969    /// Read the checkpoint in `dir` as its converted GGUF.
970    pub fn open(dir: &Path) -> io::Result<HfCheckpoint> {
971        HfCheckpoint::open_with(dir, Recompute::AsConverted)
972    }
973
974    /// The architecture and the metadata `dir`'s `config.json` alone writes, refused as [`HfCheckpoint::open`] refuses
975    /// it, before any weight is read: a published configuration checked without its checkpoint. Draft blocks only the
976    /// tensors count (Qwen3-Next's) are left uncounted.
977    pub fn read_configuration(dir: &Path) -> io::Result<(String, BTreeMap<String, MetaValue>)> {
978        let (family, h) = configuration(dir)?;
979        let mut w = Writer { arch: family.arch().to_string(), meta: BTreeMap::new() };
980        let rope = rope_parameters(&h);
981        let block_count = h.need_u64("num_hidden_layers")? + stated_draft_blocks(family, &h).unwrap_or(0);
982        base_parameters(&h, &rope, &mut w, block_count, family)?;
983        family_parameters(family, &h, &rope, &mut w, block_count)?;
984        Ok((family.arch().to_string(), w.meta))
985    }
986
987    /// The tokenizer keys `dir`'s tokenizer files and configuration write (`tokenizer.*`, with `general.architecture`),
988    /// before any weight is read: a published tokenizer checked without its checkpoint.
989    pub fn read_tokenizer(dir: &Path) -> io::Result<BTreeMap<String, MetaValue>> {
990        let (family, h) = configuration(dir)?;
991        vocab::tokenizer_meta(dir, &h, family)
992    }
993
994    /// Read the checkpoint in `dir`, presenting computed values as `recompute` says.
995    pub fn open_with(dir: &Path, recompute: Recompute) -> io::Result<HfCheckpoint> {
996        let (family, h) = configuration(dir)?;
997
998        let paths = safetensors::shard_paths(dir)?;
999        let shards: Vec<Shard> = paths.iter().map(|p| Shard::open(p)).collect::<io::Result<_>>()?;
1000        let mut located = HashMap::new();
1001        for (i, s) in shards.iter().enumerate() {
1002            for name in s.tensors.keys() {
1003                if located.insert(name.clone(), i).is_some() {
1004                    return Err(invalid(format!("{}: tensor {name} is in two shards", dir.display())));
1005                }
1006            }
1007        }
1008        let (scaled, consumed) = fp8_scales(&h, &located)?;
1009        let mut c = HfCheckpoint {
1010            dir: dir.to_path_buf(),
1011            recompute,
1012            family,
1013            hparams: Hparams { map: Map::new(), architecture: String::new() },
1014            tokenizer: OnceLock::new(),
1015            arch: family.arch().to_string(),
1016            meta: BTreeMap::new(),
1017            views: Vec::new(),
1018            index: HashMap::new(),
1019            shards,
1020            located,
1021            scaled,
1022            consumed,
1023            unread: Vec::new(),
1024            skipped: Vec::new(),
1025        };
1026        let mut w = Writer { arch: c.arch.clone(), meta: BTreeMap::new() };
1027        let rope = rope_parameters(&h);
1028        let block_count = h.need_u64("num_hidden_layers")? + stated_draft_blocks(family, &h).unwrap_or_else(|| draft_layers(&c));
1029        base_parameters(&h, &rope, &mut w, block_count, family)?;
1030        family_parameters(family, &h, &rope, &mut w, block_count)?;
1031        c.ple_parameters(&mut w)?;
1032        c.meta = w.meta;
1033        c.build_views(family, &h, &rope)?;
1034        c.hparams = h;
1035        Ok(c)
1036    }
1037
1038    /// The tokenizer keys the converter writes from the directory's tokenizer files (`tokenizer.*`), with
1039    /// `general.architecture`: what [`Vocab::from_meta`](super::tokenizer::Vocab::from_meta) reads. Read on first use.
1040    pub fn tokenizer_meta(&self) -> io::Result<&BTreeMap<String, MetaValue>> {
1041        if let Some(m) = self.tokenizer.get() {
1042            return Ok(m);
1043        }
1044        let m = vocab::tokenizer_meta(&self.dir, &self.hparams, self.family)?;
1045        Ok(self.tokenizer.get_or_init(|| m))
1046    }
1047
1048    /// Tensors of the checkpoint nothing presented reads and the converter does not skip.
1049    pub fn unread(&self) -> &[String] {
1050        &self.unread
1051    }
1052
1053    /// Tensors the converter skips (a multimodal checkpoint's tower), which this reader skips too.
1054    pub fn skipped(&self) -> &[String] {
1055        &self.skipped
1056    }
1057
1058    /// Tensors whose values a converter step computes from the stored ones, rather than reads.
1059    pub fn recomputed(&self) -> Vec<&str> {
1060        self.views.iter().filter(|v| matches!(v.form, Form::Map { .. })).map(|v| v.name.as_str()).collect()
1061    }
1062
1063    /// The safetensors files read.
1064    pub fn shard_count(&self) -> usize {
1065        self.shards.len()
1066    }
1067
1068    pub fn dir(&self) -> &Path {
1069        &self.dir
1070    }
1071
1072    fn entry(&self, name: &str) -> Option<&Entry> {
1073        self.located.get(name).map(|&i| &self.shards[i].tensors[name])
1074    }
1075
1076    fn need(&self, name: &str) -> io::Result<&Entry> {
1077        self.entry(name).ok_or_else(|| invalid(format!("{}: no tensor {name}", self.dir.display())))
1078    }
1079
1080    fn present(&mut self, name: String, form: Form) -> io::Result<()> {
1081        let dims = self.dims_of(&form)?;
1082        self.push(name, dims, form)
1083    }
1084
1085    fn push(&mut self, name: String, dims: Vec<u64>, form: Form) -> io::Result<()> {
1086        let n: u64 = dims.iter().product();
1087        if n as usize != form.elements() {
1088            return Err(invalid(format!("{name}: dims {dims:?} hold {n} elements, its view {}", form.elements())));
1089        }
1090        self.index.insert(name.clone(), self.views.len());
1091        self.views.push(View { name, dims, form });
1092        Ok(())
1093    }
1094
1095    /// ggml dims of a form: a one-dimensional source stays one-dimensional; a whole tensor keeps its own shape; a
1096    /// rearranged one is `[outer, rows, cols]` (or `[outer, cols, rows]` transposed), an outer or row axis of one
1097    /// dropped; a stack adds its count outermost.
1098    fn dims_of(&self, form: &Form) -> io::Result<Vec<u64>> {
1099        Ok(match form {
1100            Form::Gather { src, outer, m, n, rows, cols, transpose } => {
1101                let e = self.need(src)?;
1102                let identity = *outer == 1
1103                    && !*transpose
1104                    && rows.len() == *m
1105                    && cols.len() == *n
1106                    && rows.iter().enumerate().all(|(i, &r)| i == r)
1107                    && cols.iter().enumerate().all(|(i, &c)| i == c);
1108                if e.shape.len() <= 1 {
1109                    vec![form.elements() as u64]
1110                } else if identity {
1111                    e.shape.iter().rev().copied().collect()
1112                } else {
1113                    let (r, c) = if *transpose { (cols.len(), rows.len()) } else { (rows.len(), cols.len()) };
1114                    let mut torch = Vec::new();
1115                    if *outer > 1 {
1116                        torch.push(*outer as u64);
1117                    }
1118                    if r > 1 || *outer == 1 {
1119                        torch.push(r as u64);
1120                    }
1121                    torch.push(c as u64);
1122                    torch.into_iter().rev().collect()
1123                }
1124            }
1125            Form::Concat(parts) => {
1126                let mut d = self.dims_of(&parts[0])?;
1127                d.push(parts.len() as u64);
1128                d
1129            }
1130            Form::Beside(parts) => {
1131                let dims = parts.iter().map(|p| self.dims_of(p)).collect::<io::Result<Vec<_>>>()?;
1132                if dims.iter().any(|d| d.len() != 2 || d[1] != dims[0][1]) {
1133                    return Err(invalid(format!("parts side by side of different rows: {dims:?}")));
1134                }
1135                vec![dims.iter().map(|d| d[0]).sum(), dims[0][1]]
1136            }
1137            Form::Map { inner, .. } => self.dims_of(inner)?,
1138            Form::Values(v) => vec![v.len() as u64],
1139        })
1140    }
1141
1142    /// The element type of a form: its source's, or f32 for a computed one.
1143    fn ty_of(&self, form: &Form) -> io::Result<GgmlType> {
1144        Ok(match form {
1145            Form::Gather { src, .. } => self.presented(src)?,
1146            Form::Concat(parts) | Form::Beside(parts) => {
1147                let t = self.ty_of(&parts[0])?;
1148                for p in &parts[1..] {
1149                    if self.ty_of(p)? != t {
1150                        return Err(invalid("a stack of tensors of different dtypes"));
1151                    }
1152                }
1153                t
1154            }
1155            Form::Map { .. } | Form::Values(_) => GgmlType::F32,
1156        })
1157    }
1158
1159    /// The type a stored tensor is presented as: a scaled weight's f32, every other [`ggml_type`]'s.
1160    fn presented(&self, src: &str) -> io::Result<GgmlType> {
1161        Ok(if self.scaled.contains_key(src) { GgmlType::F32 } else { ggml_type(self.need(src)?.dtype) })
1162    }
1163
1164    /// `count` elements of stored tensor `src` from element `first` (row-major), as [`Self::presented`] gives them: a
1165    /// scaled weight widened and multiplied by its scale, as the converter's `dequant_simple` writes it; every other
1166    /// widened.
1167    fn source(&self, src: &str, first: usize, count: usize) -> io::Result<Vec<u8>> {
1168        let entry = self.need(src)?;
1169        let es = entry.dtype.bytes();
1170        let raw = self.shards[self.located[src]].read(src, (first * es) as u64, count * es)?;
1171        let Some(scaled) = self.scaled.get(src) else { return Ok(widen(entry.dtype, raw)) };
1172        let grid = scaled.grid.get_or_init(|| self.scale_grid(src, scaled)).as_ref().map_err(|e| invalid(e.clone()))?;
1173        let mut values = Vec::with_capacity(count);
1174        ggml_type(entry.dtype).dequantize(&widen(entry.dtype, raw), &mut values);
1175        grid.apply(first, &mut values);
1176        Ok(values.iter().flat_map(|v| v.to_le_bytes()).collect())
1177    }
1178
1179    /// The scale of weight `src` over its elements, read whole.
1180    fn scale_grid(&self, src: &str, scaled: &Scaled) -> Result<ScaleGrid, String> {
1181        let (weight, scale) = (self.need(src).map_err(|e| e.to_string())?, self.need(&scaled.scale).map_err(|e| e.to_string())?);
1182        let raw = self.shards[self.located[scaled.scale.as_str()]]
1183            .read(&scaled.scale, 0, scale.elements() as usize * scale.dtype.bytes())
1184            .map_err(|e| e.to_string())?;
1185        let mut values = Vec::with_capacity(scale.elements() as usize);
1186        ggml_type(scale.dtype).dequantize(&widen(scale.dtype, raw), &mut values);
1187        let axes = |e: &Entry| e.shape.iter().map(|&d| d as usize).collect::<Vec<usize>>();
1188        ScaleGrid::new(&axes(weight), &axes(scale), values, scaled.block.as_deref()).map_err(|e| format!("{src} scaled by {}: {e}", scaled.scale))
1189    }
1190
1191    /// `count` elements of `form` from element `start`, as bytes of its element type.
1192    fn read(&self, form: &Form, start: usize, count: usize) -> io::Result<Vec<u8>> {
1193        match form {
1194            Form::Gather { src, outer, m, n, rows, cols, transpose } => {
1195                let es = self.presented(src)?.block_bytes();
1196                let all_cols = cols.len() == *n && cols.iter().enumerate().all(|(i, &c)| i == c);
1197                let identity = !*transpose && all_cols && rows.len() == *m && rows.iter().enumerate().all(|(i, &r)| i == r);
1198                if identity {
1199                    return self.source(src, start, count);
1200                }
1201                let mut out = Vec::with_capacity(count * es);
1202                if !*transpose {
1203                    // Each output row is one source row (or columns of it): read only the rows asked for.
1204                    let (out_c, per) = (cols.len(), rows.len() * cols.len());
1205                    let mut e = start;
1206                    while e < start + count {
1207                        let (b, rem) = (e / per, e % per);
1208                        let (i, j0) = (rem / out_c, rem % out_c);
1209                        let take = (out_c - j0).min(start + count - e);
1210                        let source_row = b * m + rows[i];
1211                        if all_cols {
1212                            out.extend(self.source(src, source_row * n + j0, take)?);
1213                        } else {
1214                            let row = self.source(src, source_row * n, *n)?;
1215                            for &c in &cols[j0..j0 + take] {
1216                                out.extend_from_slice(&row[c * es..(c + 1) * es]);
1217                            }
1218                        }
1219                        e += take;
1220                    }
1221                    return Ok(out);
1222                }
1223                // A transposed output row is a source column: read the source matrix it lies in once.
1224                let (out_r, out_c) = (cols.len(), rows.len());
1225                let per = out_r * out_c;
1226                let mut block: Option<(usize, Vec<u8>)> = None;
1227                for e in start..start + count {
1228                    let b = e / per;
1229                    let rem = e % per;
1230                    let (i, j) = (rem / out_c, rem % out_c);
1231                    let (r, c) = (rows[j], cols[i]);
1232                    if block.as_ref().map(|(bb, _)| *bb) != Some(b) {
1233                        debug_assert!(b < *outer);
1234                        block = Some((b, self.source(src, b * m * n, m * n)?));
1235                    }
1236                    let at = (r * n + c) * es;
1237                    out.extend_from_slice(&block.as_ref().unwrap().1[at..at + es]);
1238                }
1239                Ok(out)
1240            }
1241            Form::Concat(parts) => {
1242                let mut out = Vec::new();
1243                let mut base = 0;
1244                for p in parts {
1245                    let len = p.elements();
1246                    let lo = start.max(base);
1247                    let hi = (start + count).min(base + len);
1248                    if lo < hi {
1249                        out.extend(self.read(p, lo - base, hi - lo)?);
1250                    }
1251                    base += len;
1252                }
1253                Ok(out)
1254            }
1255            Form::Beside(parts) => {
1256                let widths =
1257                    parts.iter().map(|p| Ok(self.dims_of(p)?[0] as usize)).collect::<io::Result<Vec<usize>>>()?;
1258                let row = widths.iter().sum::<usize>();
1259                let mut out = Vec::new();
1260                let mut e = start;
1261                while e < start + count {
1262                    let (r, mut c) = (e / row, e % row);
1263                    let mut part = 0;
1264                    while c >= widths[part] {
1265                        c -= widths[part];
1266                        part += 1;
1267                    }
1268                    let take = (widths[part] - c).min(start + count - e);
1269                    out.extend(self.read(&parts[part], r * widths[part] + c, take)?);
1270                    e += take;
1271                }
1272                Ok(out)
1273            }
1274            Form::Values(v) => Ok(v[start..start + count].iter().flat_map(|x| x.to_le_bytes()).collect()),
1275            Form::Map { inner, op } => {
1276                let ty = self.ty_of(inner)?;
1277                let raw = self.read(inner, start, count)?;
1278                let mut vals = Vec::with_capacity(count);
1279                ty.dequantize(&raw, &mut vals);
1280                let round = |x: f32| match (self.recompute, ty) {
1281                    (Recompute::AsConverted, GgmlType::F16) => f16_to_f32(f32_to_f16(x)),
1282                    _ => x,
1283                };
1284                Ok(vals
1285                    .into_iter()
1286                    .map(|x| match op {
1287                        Op::AddOne => 1.0f32 + x,
1288                        // Correctly rounded: the exponential in f64, rounded to f32 once.
1289                        Op::NegExp => -((x as f64).exp() as f32),
1290                    })
1291                    .map(round)
1292                    .flat_map(f32::to_le_bytes)
1293                    .collect())
1294            }
1295        }
1296    }
1297
1298    /// Present every tensor, and record what nothing reads.
1299    fn build_views(&mut self, f: Family, h: &Hparams, rope: &Map<String, Value>) -> io::Result<()> {
1300        let names: Vec<String> = {
1301            let mut v: Vec<String> = self.located.keys().cloned().collect();
1302            v.sort();
1303            v
1304        };
1305        // The scales an FP8 checkpoint's weights are read with, and the activation scales the converter drops.
1306        let mut read: BTreeSet<String> = self.consumed.clone();
1307        let p = decoder_prefix(&h.architecture, &names);
1308        let head = if p == "language_model.model." { "language_model.lm_head.weight" } else { "lm_head.weight" };
1309        let n_layers = h.need_u64("num_hidden_layers")? as usize;
1310        let tied = h.find(&["tie_word_embeddings"]).and_then(Value::as_bool).unwrap_or(false);
1311
1312        // The converter skips a multimodal checkpoint's tower and, for DeepSeek, the draft layers past the stack.
1313        for name in &names {
1314            let s = name.strip_prefix("model.").unwrap_or(name);
1315            let tower = s.contains("visual.") || s.contains("vision.") || s.starts_with("vision_") || s.starts_with("multi_modal_projector.");
1316            let deepseek_draft = f == Family::Deepseek2
1317                && layer_of(name, "model.layers.").is_some_and(|(l, _)| l >= n_layers);
1318            // Gemma's and Gemma 2's converters drop a head the checkpoint carries: llama.cpp ties it to the embedding.
1319            let tied_head = name == head && (tied && f == Family::Deepseek2 || matches!(f, Family::Gemma | Family::Gemma2));
1320            if tower || deepseek_draft || tied_head {
1321                self.skipped.push(name.clone());
1322                read.insert(name.clone());
1323            }
1324        }
1325
1326        if f == Family::Llama || f == Family::Llama4 {
1327            if let Some(factors) = llama3_rope_factors(h, rope) {
1328                self.present("rope_freqs.weight".into(), Form::Values(factors))?;
1329            }
1330        }
1331
1332        // Globals.
1333        let embed = format!("{p}embed_tokens.weight");
1334        let e = self.need(&embed)?.clone();
1335        let mut embed_form = Form::whole(&embed, &e);
1336        let mut embed_dims: Vec<u64> = e.shape.iter().rev().copied().collect();
1337        if f == Family::Gemma3 {
1338            // The converter keeps only the vocabulary's real rows.
1339            let real = gemma_vocab(&self.dir, h)?;
1340            if let Form::Gather { rows, .. } = &mut embed_form {
1341                if real < rows.len() {
1342                    rows.truncate(real);
1343                    embed_dims[1] = real as u64;
1344                }
1345            }
1346        }
1347        self.push("token_embd.weight".into(), embed_dims, embed_form)?;
1348        read.insert(embed);
1349        if f == Family::Qwen4Exp {
1350            // Hyper-connections replace every norm: the last mix of the lanes feeds the head.
1351            for (stored, gguf) in [
1352                ("hc_norm.weight", "output_hc_norm.weight"),
1353                ("input_mix_weight_down.weight", "output_hc_down.weight"),
1354                ("input_mix_weight_up.weight", "output_hc_up.weight"),
1355            ] {
1356                let name = format!("{p}hyper_connection_mixer.{stored}");
1357                let form = if stored == "hc_norm.weight" { self.norm_form(f, &name)? } else { Form::whole(&name, self.need(&name)?) };
1358                self.present(gguf.into(), form)?;
1359                read.insert(name);
1360            }
1361        } else {
1362            let norm = format!("{p}norm.weight");
1363            let nf = self.norm_form(f, &norm)?;
1364            self.present("output_norm.weight".into(), nf)?;
1365            read.insert(norm);
1366        }
1367        if !self.skipped.iter().any(|s| s == head) {
1368            if let Some(entry) = self.entry(head).cloned() {
1369                self.present("output.weight".into(), Form::whole(head, &entry))?;
1370                read.insert(head.into());
1371            }
1372        }
1373
1374        // Blocks.
1375        let mut experts: BTreeMap<(usize, &'static str), BTreeMap<usize, String>> = BTreeMap::new();
1376        // Qwen3.8-Flash-Next's n-gram table, stored in shards by row: each shard's name by its index.
1377        let mut ngram_shards: BTreeMap<usize, String> = BTreeMap::new();
1378        for name in &names {
1379            if read.contains(name) {
1380                continue;
1381            }
1382            let (l, rest) = if let Some((l, rest)) = layer_of(name, &format!("{p}layers.")) {
1383                (l, rest.to_string())
1384            } else if f.hybrid() && name.starts_with("mtp.") {
1385                // The draft block sits after the stack: its layer is block `num_hidden_layers`, its projection and norms
1386                // that block's `nextn` tensors.
1387                let r = &name["mtp.".len()..];
1388                if let Some((0, rest)) = layer_of(name, "mtp.layers.") {
1389                    (n_layers, rest.to_string())
1390                } else if f == Family::Qwen4Exp && matches!(r, "fc_embedding.weight" | "fc_hidden.weight") {
1391                    // `eh_proj([e ; h]) = fc_embedding(e) + fc_hidden(h)`: the two projections side by side.
1392                    if r == "fc_embedding.weight" {
1393                        let (e, hd) = ("mtp.fc_embedding.weight".to_string(), "mtp.fc_hidden.weight".to_string());
1394                        let form = Form::Beside(vec![Form::whole(&e, self.need(&e)?), Form::whole(&hd, self.need(&hd)?)]);
1395                        self.present(format!("blk.{n_layers}.nextn.eh_proj.weight"), form)?;
1396                        read.insert(e);
1397                        read.insert(hd);
1398                    }
1399                    continue;
1400                } else {
1401                    let nextn = match r {
1402                        "hyper_connection_mixer.hc_norm.weight" if f == Family::Qwen4Exp => "nextn.hc_head_norm.weight",
1403                        "hyper_connection_mixer.input_mix_weight_down.weight" if f == Family::Qwen4Exp => "nextn.hc_head_down.weight",
1404                        "hyper_connection_mixer.input_mix_weight_up.weight" if f == Family::Qwen4Exp => "nextn.hc_head_up.weight",
1405                        "fc.weight" => "nextn.eh_proj.weight",
1406                        "norm.weight" => "nextn.shared_head_norm.weight",
1407                        "pre_fc_norm_embedding.weight" => "nextn.enorm.weight",
1408                        "pre_fc_norm_hidden.weight" => "nextn.hnorm.weight",
1409                        _ => continue,
1410                    };
1411                    let form = if nextn.ends_with("norm.weight") {
1412                        self.norm_form(f, name)?
1413                    } else {
1414                        Form::whole(name, &self.need(name)?.clone())
1415                    };
1416                    self.present(format!("blk.{n_layers}.{nextn}"), form)?;
1417                    read.insert(name.clone());
1418                    continue;
1419                }
1420            } else {
1421                continue;
1422            };
1423            let blk = |s: &str| format!("blk.{l}.{s}");
1424            if let Some(proj) = expert_of(&rest) {
1425                // `mlp.experts.<x>.<proj>.weight`: gathered, stacked once the block is read.
1426                experts.entry((l, proj.1)).or_default().insert(proj.0, name.clone());
1427                read.insert(name.clone());
1428                continue;
1429            }
1430            let entry = self.need(name)?.clone();
1431            let whole = Form::whole(name, &entry);
1432            let gguf: Option<(String, Form)> = match (f, rest.as_str()) {
1433                (_, "input_layernorm.weight") => Some((blk("attn_norm.weight"), self.norm_form(f, name)?)),
1434                (Family::Gemma2 | Family::Gemma3 | Family::Qwen35 | Family::Qwen35Moe | Family::Qwen3Next | Family::GptOss | Family::Glm4Moe, "post_attention_layernorm.weight") => {
1435                    Some((blk("post_attention_norm.weight"), self.norm_form(f, name)?))
1436                }
1437                (_, "post_attention_layernorm.weight") => Some((blk("ffn_norm.weight"), self.norm_form(f, name)?)),
1438                (Family::Gemma2 | Family::Gemma3, "pre_feedforward_layernorm.weight") => Some((blk("ffn_norm.weight"), self.norm_form(f, name)?)),
1439                (Family::Gemma2 | Family::Gemma3, "post_feedforward_layernorm.weight") => Some((blk("post_ffw_norm.weight"), self.norm_form(f, name)?)),
1440                (_, "self_attn.q_norm.weight") => Some((blk("attn_q_norm.weight"), self.norm_form(f, name)?)),
1441                (_, "self_attn.k_norm.weight") => Some((blk("attn_k_norm.weight"), self.norm_form(f, name)?)),
1442                (Family::Llama, "self_attn.q_proj.weight" | "self_attn.q_proj.bias") => {
1443                    let heads = h.need_u64("num_attention_heads")? as usize;
1444                    let suffix = if rest.ends_with("bias") { "attn_q.bias" } else { "attn_q.weight" };
1445                    Some((blk(suffix), llama_permute(name, &entry, heads)))
1446                }
1447                (Family::Llama, "self_attn.k_proj.weight" | "self_attn.k_proj.bias") => {
1448                    let kv = h.u64(&["num_key_value_heads"]).unwrap_or(h.need_u64("num_attention_heads")?) as usize;
1449                    let suffix = if rest.ends_with("bias") { "attn_k.bias" } else { "attn_k.weight" };
1450                    Some((blk(suffix), llama_permute(name, &entry, kv)))
1451                }
1452                (_, "self_attn.q_proj.weight") => Some((blk("attn_q.weight"), whole)),
1453                (_, "self_attn.k_proj.weight") => Some((blk("attn_k.weight"), whole)),
1454                (_, "self_attn.v_proj.weight") => Some((blk("attn_v.weight"), whole)),
1455                (_, "self_attn.o_proj.weight") => Some((blk("attn_output.weight"), whole)),
1456                (_, "self_attn.q_proj.bias") => Some((blk("attn_q.bias"), whole)),
1457                (_, "self_attn.k_proj.bias") => Some((blk("attn_k.bias"), whole)),
1458                (_, "self_attn.v_proj.bias") => Some((blk("attn_v.bias"), whole)),
1459                (_, "self_attn.o_proj.bias") => Some((blk("attn_output.bias"), whole)),
1460                (Family::GptOss, "self_attn.sinks") => Some((blk("attn_sinks.weight"), whole)),
1461                (_, "mlp.gate_proj.weight") => Some((blk("ffn_gate.weight"), whole)),
1462                (_, "mlp.up_proj.weight") => Some((blk("ffn_up.weight"), whole)),
1463                (_, "mlp.down_proj.weight") => Some((blk("ffn_down.weight"), whole)),
1464                (
1465                    Family::Qwen3Moe
1466                    | Family::Qwen2Moe
1467                    | Family::Qwen35Moe
1468                    | Family::Qwen3Next
1469                    | Family::Qwen4Exp
1470                    | Family::Deepseek2
1471                    | Family::Glm4Moe,
1472                    "mlp.gate.weight",
1473                ) => {
1474                    Some((blk("ffn_gate_inp.weight"), whole))
1475                }
1476                (f, "mlp.shared_expert.gate_proj.weight") if f.qwen_mixture() => Some((blk("ffn_gate_shexp.weight"), whole)),
1477                (f, "mlp.shared_expert.up_proj.weight") if f.qwen_mixture() => Some((blk("ffn_up_shexp.weight"), whole)),
1478                (f, "mlp.shared_expert.down_proj.weight") if f.qwen_mixture() => Some((blk("ffn_down_shexp.weight"), whole)),
1479                // The shared expert's gate, one output row over the input: `[1, d]`, ggml `[d, 1]`.
1480                (f, "mlp.shared_expert_gate.weight") if f.qwen_mixture() => Some((blk("ffn_gate_inp_shexp.weight"), whole)),
1481                // A Qwen mixture's experts stored as one gate-and-up tensor `[experts, 2·ff, d]`, the gate's rows first:
1482                // each half a stack the converter splits out.
1483                (Family::Qwen3Moe | Family::Qwen2Moe | Family::Qwen35Moe | Family::Qwen3Next | Family::Qwen4Exp, "mlp.experts.gate_up_proj") => {
1484                    let (e, two, d) = dims3(&entry, name)?;
1485                    let ff = two / 2;
1486                    let half = |from: usize| Form::Gather {
1487                        src: name.clone(),
1488                        outer: e,
1489                        m: two,
1490                        n: d,
1491                        rows: (from..from + ff).collect(),
1492                        cols: (0..d).collect(),
1493                        transpose: false,
1494                    };
1495                    self.present(blk("ffn_gate_exps.weight"), half(0))?;
1496                    Some((blk("ffn_up_exps.weight"), half(ff)))
1497                }
1498                (Family::Qwen3Moe | Family::Qwen2Moe | Family::Qwen35Moe | Family::Qwen3Next | Family::Qwen4Exp, "mlp.experts.down_proj") => {
1499                    Some((blk("ffn_down_exps.weight"), whole))
1500                }
1501                (Family::Llama | Family::MinimaxM2, "block_sparse_moe.gate.weight") => Some((blk("ffn_gate_inp.weight"), whole)),
1502                (Family::MinimaxM2, "block_sparse_moe.e_score_correction_bias") => Some((blk("exp_probs_b.bias"), whole)),
1503                (Family::Phi3, "self_attn.qkv_proj.weight") => Some((blk("attn_qkv.weight"), whole)),
1504                (Family::Phi3 | Family::Glm4, "mlp.gate_up_proj.weight") => Some((blk("ffn_up.weight"), whole)),
1505                (Family::Glm4, "post_self_attn_layernorm.weight") => Some((blk("post_attention_norm.weight"), self.norm_form(f, name)?)),
1506                (Family::Glm4, "post_mlp_layernorm.weight") => Some((blk("post_ffw_norm.weight"), self.norm_form(f, name)?)),
1507                (Family::Deepseek2 | Family::Glm4Moe, "mlp.gate.e_score_correction_bias") => Some((blk("exp_probs_b.bias"), whole)),
1508                (Family::Deepseek2 | Family::Glm4Moe, "mlp.shared_experts.gate_proj.weight") => Some((blk("ffn_gate_shexp.weight"), whole)),
1509                (Family::Deepseek2 | Family::Glm4Moe, "mlp.shared_experts.up_proj.weight") => Some((blk("ffn_up_shexp.weight"), whole)),
1510                (Family::Deepseek2 | Family::Glm4Moe, "mlp.shared_experts.down_proj.weight") => Some((blk("ffn_down_shexp.weight"), whole)),
1511                // GLM-4.5's draft block after the stack: its projection of [embedding | hidden], norms, embedding and head.
1512                (Family::Glm4Moe, "eh_proj.weight") => Some((blk("nextn.eh_proj.weight"), whole)),
1513                (Family::Glm4Moe, "embed_tokens.weight") => Some((blk("nextn.embed_tokens.weight"), whole)),
1514                (Family::Glm4Moe, "shared_head.head.weight") => Some((blk("nextn.shared_head_head.weight"), whole)),
1515                (Family::Glm4Moe, "enorm.weight") => Some((blk("nextn.enorm.weight"), self.norm_form(f, name)?)),
1516                (Family::Glm4Moe, "hnorm.weight") => Some((blk("nextn.hnorm.weight"), self.norm_form(f, name)?)),
1517                (Family::Glm4Moe, "shared_head.norm.weight") => Some((blk("nextn.shared_head_norm.weight"), self.norm_form(f, name)?)),
1518                (Family::Deepseek2, "self_attn.q_a_proj.weight") => Some((blk("attn_q_a.weight"), whole)),
1519                (Family::Deepseek2, "self_attn.q_a_layernorm.weight") => Some((blk("attn_q_a_norm.weight"), whole)),
1520                (Family::Deepseek2, "self_attn.q_b_proj.weight") => Some((blk("attn_q_b.weight"), whole)),
1521                (Family::Deepseek2, "self_attn.kv_a_proj_with_mqa.weight") => Some((blk("attn_kv_a_mqa.weight"), whole)),
1522                (Family::Deepseek2, "self_attn.kv_a_layernorm.weight") => Some((blk("attn_kv_a_norm.weight"), whole)),
1523                (Family::Deepseek2, "self_attn.kv_b_proj.weight") => {
1524                    let heads = h.u64(&["num_key_value_heads"]).unwrap_or(h.need_u64("num_attention_heads")?) as usize;
1525                    let nope = h.need_u64("qk_nope_head_dim")? as usize;
1526                    let v = h.need_u64("v_head_dim")? as usize;
1527                    let lora = *entry.shape.last().unwrap_or(&0) as usize;
1528                    if entry.elements() as usize != heads * (nope + v) * lora {
1529                        return Err(invalid(format!("{name}: {:?} is not {heads} heads of {nope}+{v} by {lora}", entry.shape)));
1530                    }
1531                    let k_b = Form::Gather {
1532                        src: name.clone(),
1533                        outer: heads,
1534                        m: nope + v,
1535                        n: lora,
1536                        rows: (0..nope).collect(),
1537                        cols: (0..lora).collect(),
1538                        transpose: true,
1539                    };
1540                    let v_b = Form::Gather {
1541                        src: name.clone(),
1542                        outer: heads,
1543                        m: nope + v,
1544                        n: lora,
1545                        rows: (nope..nope + v).collect(),
1546                        cols: (0..lora).collect(),
1547                        transpose: false,
1548                    };
1549                    self.present(blk("attn_k_b.weight"), k_b)?;
1550                    Some((blk("attn_v_b.weight"), v_b))
1551                }
1552                (Family::Llama4, "feed_forward.gate_proj.weight") => Some((blk("ffn_gate.weight"), whole)),
1553                (Family::Llama4, "feed_forward.up_proj.weight") => Some((blk("ffn_up.weight"), whole)),
1554                (Family::Llama4, "feed_forward.down_proj.weight") => Some((blk("ffn_down.weight"), whole)),
1555                (Family::Llama4, "feed_forward.router.weight") => Some((blk("ffn_gate_inp.weight"), whole)),
1556                (Family::Llama4, "feed_forward.shared_expert.gate_proj.weight") => Some((blk("ffn_gate_shexp.weight"), whole)),
1557                (Family::Llama4, "feed_forward.shared_expert.up_proj.weight") => Some((blk("ffn_up_shexp.weight"), whole)),
1558                (Family::Llama4, "feed_forward.shared_expert.down_proj.weight") => Some((blk("ffn_down_shexp.weight"), whole)),
1559                // The experts' gate and up projections in one `[experts, d, 2·ff]` tensor, the gate's columns first: each
1560                // half, transposed, is a stack the converter splits out.
1561                (Family::Llama4, "feed_forward.experts.gate_up_proj") => {
1562                    let (e, d, two) = dims3(&entry, name)?;
1563                    let ff = two / 2;
1564                    let half = |from: usize| Form::Gather {
1565                        src: name.clone(),
1566                        outer: e,
1567                        m: d,
1568                        n: two,
1569                        rows: (0..d).collect(),
1570                        cols: (from..from + ff).collect(),
1571                        transpose: true,
1572                    };
1573                    self.present(blk("ffn_gate_exps.weight"), half(0))?;
1574                    Some((blk("ffn_up_exps.weight"), half(ff)))
1575                }
1576                (Family::Llama4, "feed_forward.experts.down_proj") => {
1577                    let (e, ff, d) = dims3(&entry, name)?;
1578                    Some((
1579                        blk("ffn_down_exps.weight"),
1580                        Form::Gather {
1581                            src: name.clone(),
1582                            outer: e,
1583                            m: ff,
1584                            n: d,
1585                            rows: (0..ff).collect(),
1586                            cols: (0..d).collect(),
1587                            transpose: true,
1588                        },
1589                    ))
1590                }
1591                (Family::GptOss, "mlp.router.weight") => Some((blk("ffn_gate_inp.weight"), whole)),
1592                (Family::GptOss, "mlp.router.bias") => Some((blk("ffn_gate_inp.bias"), whole)),
1593                (Family::GptOss, "mlp.experts.gate_up_proj") => {
1594                    let (e, d, two) = dims3(&entry, name)?;
1595                    let ff = two / 2;
1596                    let pick = |o: usize| Form::Gather {
1597                        src: name.clone(),
1598                        outer: e,
1599                        m: d,
1600                        n: two,
1601                        rows: (0..d).collect(),
1602                        cols: (0..ff).map(|j| 2 * j + o).collect(),
1603                        transpose: true,
1604                    };
1605                    self.present(blk("ffn_gate_exps.weight"), pick(0))?;
1606                    Some((blk("ffn_up_exps.weight"), pick(1)))
1607                }
1608                (Family::GptOss, "mlp.experts.gate_up_proj_bias") => {
1609                    let (e, two) = (entry.shape[0] as usize, entry.shape[1] as usize);
1610                    let pick = |o: usize| Form::Gather {
1611                        src: name.clone(),
1612                        outer: e,
1613                        m: 1,
1614                        n: two,
1615                        rows: vec![0],
1616                        cols: (0..two / 2).map(|j| 2 * j + o).collect(),
1617                        transpose: false,
1618                    };
1619                    self.present(blk("ffn_gate_exps.bias"), pick(0))?;
1620                    Some((blk("ffn_up_exps.bias"), pick(1)))
1621                }
1622                (Family::GptOss, "mlp.experts.down_proj") => {
1623                    let (e, ff, d) = dims3(&entry, name)?;
1624                    Some((
1625                        blk("ffn_down_exps.weight"),
1626                        Form::Gather {
1627                            src: name.clone(),
1628                            outer: e,
1629                            m: ff,
1630                            n: d,
1631                            rows: (0..ff).collect(),
1632                            cols: (0..d).collect(),
1633                            transpose: true,
1634                        },
1635                    ))
1636                }
1637                (Family::GptOss, "mlp.experts.down_proj_bias") => Some((blk("ffn_down_exps.bias"), whole)),
1638                (Family::Qwen4Exp, r) if r.starts_with("attn_hyper_connection.") || r.starts_with("mlp_hyper_connection.") => {
1639                    let (sub, part) = r.split_once('.').unwrap();
1640                    let which = if sub == "attn_hyper_connection" { "hc_attn" } else { "hc_ffn" };
1641                    let (suffix, form) = match part {
1642                        "hc_norm.weight" => ("norm", self.norm_form(f, name)?),
1643                        "input_mix_weight_down.weight" => ("down", whole),
1644                        "input_mix_weight_up.weight" => ("up", whole),
1645                        "block_inject_weight.weight" => ("inject", whole),
1646                        other => return Err(invalid(format!("{name}: a hyper-connection tensor `{other}` this reader does not present"))),
1647                    };
1648                    Some((blk(&format!("{which}_{suffix}.weight")), form))
1649                }
1650                (Family::Qwen4Exp, "self_attn.indexer.index_qk_proj.weight") => {
1651                    // One projection feeds the index queries and the index key: split it as the converter does.
1652                    let heads = h.need_u64("indexer_n_heads")? as usize;
1653                    let hd = h.need_u64("indexer_head_dim")? as usize;
1654                    let n = *entry.shape.last().unwrap() as usize;
1655                    let rows = |rows: std::ops::Range<usize>| Form::Gather {
1656                        src: name.clone(),
1657                        outer: 1,
1658                        m: (heads + 1) * hd,
1659                        n,
1660                        rows: rows.collect(),
1661                        cols: (0..n).collect(),
1662                        transpose: false,
1663                    };
1664                    if entry.elements() as usize != (heads + 1) * hd * n {
1665                        return Err(invalid(format!("{name}: {:?} is not {heads} index queries and a key of {hd}", entry.shape)));
1666                    }
1667                    self.present(blk("indexer.q_proj.weight"), rows(0..heads * hd))?;
1668                    Some((blk("indexer.k_proj.weight"), rows(heads * hd..(heads + 1) * hd)))
1669                }
1670                (Family::Qwen4Exp, "self_attn.indexer.q_layernorm.weight") => Some((blk("indexer.q_norm.weight"), self.norm_form(f, name)?)),
1671                (Family::Qwen4Exp, "self_attn.indexer.k_layernorm.weight") => Some((blk("indexer.k_norm.weight"), self.norm_form(f, name)?)),
1672                (Family::Qwen4Exp, "ple.key_proj.weight") => Some((blk("ple_key.weight"), whole)),
1673                (Family::Qwen4Exp, "ple.value_proj.weight") => Some((blk("ple_value.weight"), whole)),
1674                (Family::Qwen4Exp, "ple.norm_key.weight") => Some((blk("ple_norm_key.weight"), self.norm_form(f, name)?)),
1675                (Family::Qwen4Exp, "ple.norm_query.weight") => Some((blk("ple_norm_query.weight"), self.norm_form(f, name)?)),
1676                (Family::Qwen4Exp, "ple.norm_conv.weight") => Some((blk("ple_norm_conv.weight"), self.norm_form(f, name)?)),
1677                (Family::Qwen4Exp, "ple.conv1d.weight") => Some((blk("ple_conv1d.weight"), whole)),
1678                (Family::Qwen4Exp, r) if r.starts_with("ple.ple_embedding.ngram_embedding.shard_") => {
1679                    let index = r["ple.ple_embedding.ngram_embedding.shard_".len()..]
1680                        .strip_suffix(".weight")
1681                        .and_then(|i| i.parse::<usize>().ok())
1682                        .ok_or_else(|| invalid(format!("{name}: an n-gram shard without its index")))?;
1683                    ngram_shards.insert(index, name.clone());
1684                    read.insert(name.clone());
1685                    None
1686                }
1687                // The hash constants: metadata ([`Self::ple_parameters`]).
1688                (
1689                    Family::Qwen4Exp,
1690                    "ple.ple_embedding.layer_multipliers" | "ple.ple_embedding.ngram_heads_offsets" | "ple.ple_embedding.ngram_heads_vocab_sizes",
1691                ) => {
1692                    read.insert(name.clone());
1693                    None
1694                }
1695                (Family::Qwen3Next, r) if r.starts_with("linear_attn.") => {
1696                    Some(self.grouped_gated_delta(h, name, &entry, &r["linear_attn.".len()..], l)?)
1697                }
1698                (f, r) if f.hybrid() && r.starts_with("linear_attn.") => {
1699                    Some(self.gated_delta(h, name, &entry, &r["linear_attn.".len()..], l)?)
1700                }
1701                _ => None,
1702            };
1703            if let Some((g, form)) = gguf {
1704                if g.ends_with("ssm_conv1d.weight") || g.ends_with("ple_conv1d.weight") {
1705                    // `[channels, 1, taps]` squeezed: ggml `[taps, channels]`.
1706                    let taps = *entry.shape.last().unwrap();
1707                    let channels = entry.elements() / taps.max(1);
1708                    self.push(g, vec![taps, channels], form)?;
1709                } else {
1710                    self.present(g, form)?;
1711                }
1712                read.insert(name.clone());
1713            }
1714        }
1715
1716        // Stacks: every expert of a block, one tensor per projection, in expert order.
1717        let expert_count = h.u64(&["num_local_experts", "num_experts", "n_routed_experts"]).unwrap_or(0) as usize;
1718        for ((l, proj), by_expert) in experts {
1719            if by_expert.len() != expert_count || by_expert.keys().copied().ne(0..expert_count) {
1720                return Err(invalid(format!(
1721                    "block {l}: {} experts' {proj} of {expert_count}",
1722                    by_expert.len()
1723                )));
1724            }
1725            let parts: Vec<Form> = by_expert
1726                .values()
1727                .map(|n| Ok(Form::whole(n, self.need(n)?)))
1728                .collect::<io::Result<_>>()?;
1729            let g = match proj {
1730                "gate_proj" => "ffn_gate_exps.weight",
1731                "up_proj" => "ffn_up_exps.weight",
1732                _ => "ffn_down_exps.weight",
1733            };
1734            self.present(format!("blk.{l}.{g}"), Form::Concat(parts))?;
1735        }
1736
1737        // The n-gram table: its shards' rows one after another, read a row at a time (it is the size of the model).
1738        if !ngram_shards.is_empty() {
1739            if ngram_shards.keys().copied().ne(0..ngram_shards.len()) {
1740                return Err(invalid(format!("the n-gram table's shards are not 0 to {}", ngram_shards.len() - 1)));
1741            }
1742            let mut rows = 0u64;
1743            let mut width = None;
1744            let mut parts = Vec::with_capacity(ngram_shards.len());
1745            for shard in ngram_shards.values() {
1746                let e = self.need(shard)?.clone();
1747                let (r, w) = (e.shape[0], *e.shape.last().unwrap());
1748                if *width.get_or_insert(w) != w {
1749                    return Err(invalid(format!("{shard}: rows of {w}, where the table's are {}", width.unwrap())));
1750                }
1751                rows += r;
1752                parts.push(Form::whole(shard, &e));
1753            }
1754            self.push("per_layer_token_embd.weight".into(), vec![width.unwrap(), rows], Form::Concat(parts))?;
1755        }
1756
1757        self.unread = names.into_iter().filter(|n| !read.contains(n)).collect();
1758        Ok(())
1759    }
1760
1761    /// Qwen3.8-Flash-Next's per-layer embedding keys the checkpoint's tensors hold: its hash constants, stored as int64
1762    /// buffers (exact: 45-bit multipliers), and the table's row width.
1763    fn ple_parameters(&self, w: &mut Writer) -> io::Result<()> {
1764        let find = |suffix: &str| self.located.keys().find(|n| n.ends_with(suffix)).cloned();
1765        let Some(multipliers) = find("ple.ple_embedding.layer_multipliers") else { return Ok(()) };
1766        let ints = |name: &str| -> io::Result<Vec<MetaValue>> {
1767            let e = self.need(name)?;
1768            if e.dtype != Dtype::I64 {
1769                return Err(invalid(format!("{name}: stored {:?}, where the hash constants are int64", e.dtype)));
1770            }
1771            let raw = self.shards[self.located[name]].read(name, 0, e.elements() as usize * 8)?;
1772            Ok(raw.chunks(8).map(|c| MetaValue::I64(i64::from_le_bytes(c.try_into().unwrap()))).collect())
1773        };
1774        let array = |items: Vec<MetaValue>| MetaValue::Array { ty: 11, items };
1775        w.put("ple.layer_multipliers", array(ints(&multipliers)?));
1776        let offsets = find("ple.ple_embedding.ngram_heads_offsets").ok_or_else(|| invalid("the n-gram heads' offsets are missing"))?;
1777        w.put("ple.head_offsets", array(ints(&offsets)?));
1778        let sizes = find("ple.ple_embedding.ngram_heads_vocab_sizes").ok_or_else(|| invalid("the n-gram heads' moduli are missing"))?;
1779        w.put("ple.head_vocab_sizes", array(ints(&sizes)?));
1780        let shard = find("ple.ple_embedding.ngram_embedding.shard_0.weight").ok_or_else(|| invalid("the n-gram table's first shard is missing"))?;
1781        w.u32("embedding_length_per_layer_input", *self.need(&shard)?.shape.last().unwrap());
1782        Ok(())
1783    }
1784
1785    /// A norm weight as the forward multiplies by it: every Gemma and Qwen3.5 store it as an offset from one (Qwen3.5's
1786    /// gated-delta norm excepted).
1787    fn norm_form(&self, f: Family, name: &str) -> io::Result<Form> {
1788        let whole = Form::whole(name, self.need(name)?);
1789        let offset = (matches!(f, Family::Gemma | Family::Gemma2 | Family::Gemma3) || f.hybrid()) && !name.ends_with("linear_attn.norm.weight");
1790        Ok(if offset { Form::Map { inner: Box::new(whole), op: Op::AddOne } } else { whole })
1791    }
1792
1793    /// A Qwen3-Next Gated DeltaNet tensor (`linear_attn.<rest>`) in the layout its converter writes: value heads grouped
1794    /// by key head, each key head's betas and alphas one `ssm_ba`. `model::layout` presents it in Qwen3.5's.
1795    fn grouped_gated_delta(&mut self, h: &Hparams, name: &str, entry: &Entry, rest: &str, l: usize) -> io::Result<(String, Form)> {
1796        let kh = h.need_u64("linear_num_key_heads")? as usize;
1797        let vh = h.need_u64("linear_num_value_heads")? as usize;
1798        let kd = h.need_u64("linear_key_head_dim")? as usize;
1799        let vd = h.need_u64("linear_value_head_dim")? as usize;
1800        let blk = |s: &str| format!("blk.{l}.{s}");
1801        let whole = Form::whole(name, entry);
1802        // `in_proj_qkvz` holds, for each key head, its query, its key, its value heads and their gates.
1803        let group = 2 * kd + 2 * (vh / kh.max(1)) * vd;
1804        let vz = (vh / kh.max(1)) * vd;
1805        let rows_with = |rows: Vec<usize>, n: usize| Form::Gather {
1806            src: name.to_string(),
1807            outer: 1,
1808            m: rows.len(),
1809            n,
1810            rows,
1811            cols: (0..n).collect(),
1812            transpose: false,
1813        };
1814        let part = |off: usize, len: usize| -> Vec<usize> { (0..kh).flat_map(|k| (0..len).map(move |e| k * group + off + e)).collect() };
1815        Ok(match rest {
1816            "in_proj_qkvz.weight" => {
1817                let n = *entry.shape.last().unwrap() as usize;
1818                let qkv: Vec<usize> = part(0, kd).into_iter().chain(part(kd, kd)).chain(part(2 * kd, vz)).collect();
1819                self.present(blk("attn_gate.weight"), rows_with(part(2 * kd + vz, vz), n))?;
1820                (blk("attn_qkv.weight"), rows_with(qkv, n))
1821            }
1822            "in_proj_ba.weight" => (blk("ssm_ba.weight"), whole),
1823            "A_log" => (blk("ssm_a"), Form::Map { inner: Box::new(whole), op: Op::NegExp }),
1824            "dt_bias" => (blk("ssm_dt.bias"), whole),
1825            "conv1d.weight" => (blk("ssm_conv1d.weight"), whole),
1826            "norm.weight" => (blk("ssm_norm.weight"), whole),
1827            "out_proj.weight" => (blk("ssm_out.weight"), whole),
1828            other => return Err(invalid(format!("{name}: a Gated DeltaNet tensor `{other}` this reader does not present"))),
1829        })
1830    }
1831
1832    fn gated_delta(&self, h: &Hparams, name: &str, entry: &Entry, rest: &str, l: usize) -> io::Result<(String, Form)> {
1833        let kh = h.need_u64("linear_num_key_heads")? as usize;
1834        let vh = h.need_u64("linear_num_value_heads")? as usize;
1835        let kd = h.need_u64("linear_key_head_dim")? as usize;
1836        let vd = h.need_u64("linear_value_head_dim")? as usize;
1837        let reorder = kh != vh;
1838        let per_k = vh / kh.max(1);
1839        // Index `o` of the tiled order reads index `tiled(o)` of the grouped one: `[k, r, d] → [r, k, d]`.
1840        let tiled = |o: usize, d: usize| -> usize {
1841            let r = o / (kh * d);
1842            let k = (o / d) % kh;
1843            k * per_k * d + r * d + o % d
1844        };
1845        let perm = |len: usize, d: usize| -> Vec<usize> {
1846            (0..len).map(|o| if reorder { tiled(o, d) } else { o }).collect()
1847        };
1848        let blk = |s: &str| format!("blk.{l}.{s}");
1849        let whole = Form::whole(name, entry);
1850        let rows_with = |rows: Vec<usize>, n: usize| Form::Gather {
1851            src: name.to_string(),
1852            outer: 1,
1853            m: rows.len(),
1854            n,
1855            rows,
1856            cols: (0..n).collect(),
1857            transpose: false,
1858        };
1859        Ok(match rest {
1860            "in_proj_qkv.weight" => {
1861                let n = *entry.shape.last().unwrap() as usize;
1862                let qk = 2 * kh * kd;
1863                let rows: Vec<usize> = (0..qk).chain(perm(vh * vd, vd).into_iter().map(|o| qk + o)).collect();
1864                (blk("attn_qkv.weight"), rows_with(rows, n))
1865            }
1866            "in_proj_z.weight" => {
1867                let n = *entry.shape.last().unwrap() as usize;
1868                (blk("attn_gate.weight"), rows_with(perm(vh * vd, vd), n))
1869            }
1870            "in_proj_a.weight" | "in_proj_b.weight" => {
1871                let n = *entry.shape.last().unwrap() as usize;
1872                let g = if rest == "in_proj_a.weight" { "ssm_alpha.weight" } else { "ssm_beta.weight" };
1873                (blk(g), rows_with(perm(vh, 1), n))
1874            }
1875            "A_log" => (
1876                blk("ssm_a"),
1877                Form::Map { inner: Box::new(rows_with(perm(vh, 1), 1)), op: Op::NegExp },
1878            ),
1879            "dt_bias" => (blk("ssm_dt.bias"), rows_with(perm(vh, 1), 1)),
1880            "conv1d.weight" => {
1881                // `[channels, 1, taps]`, squeezed to `[channels, taps]`; only the value channels move.
1882                let taps = *entry.shape.last().unwrap() as usize;
1883                let qk = 2 * kh * kd;
1884                let rows: Vec<usize> = (0..qk).chain(perm(vh * vd, vd).into_iter().map(|o| qk + o)).collect();
1885                (blk("ssm_conv1d.weight"), rows_with(rows, taps))
1886            }
1887            "norm.weight" => (blk("ssm_norm.weight"), whole),
1888            "out_proj.weight" => {
1889                let m = entry.shape[0] as usize;
1890                (
1891                    blk("ssm_out.weight"),
1892                    Form::Gather {
1893                        src: name.to_string(),
1894                        outer: 1,
1895                        m,
1896                        n: vh * vd,
1897                        rows: (0..m).collect(),
1898                        cols: perm(vh * vd, vd),
1899                        transpose: false,
1900                    },
1901                )
1902            }
1903            other => return Err(invalid(format!("{name}: a Gated DeltaNet tensor `{other}` this reader does not present"))),
1904        })
1905    }
1906}
1907
1908/// Where a checkpoint of the class `architecture`, holding the tensors `names`, keeps its decoder's tensors: a multimodal
1909/// checkpoint nests its text model under `model.language_model.` (as transformers writes every wrapper, and Qwen3.5's
1910/// releases are published) or under `language_model.model.` (as Gemma 3's and Llama 4's were), the converter reading
1911/// either with `language_model.` removed; a text model alone (Qwen3.8-2.4T's `Qwen3_5MoeForCausalLM`) keeps it under
1912/// `model.`.
1913fn decoder_prefix(architecture: &str, names: &[String]) -> &'static str {
1914    if !architecture.ends_with("ForConditionalGeneration") {
1915        "model."
1916    } else if names.iter().any(|n| n.starts_with("language_model.model.")) {
1917        "language_model.model."
1918    } else {
1919        "model.language_model."
1920    }
1921}
1922
1923/// `dir`'s configuration, its text configuration merged in, its family and its keys refused as the reader refuses them.
1924fn configuration(dir: &Path) -> io::Result<(Family, Hparams)> {
1925    let config_path = dir.join("config.json");
1926    let root: Value = serde_json::from_str(
1927        &std::fs::read_to_string(&config_path).map_err(|e| io::Error::new(e.kind(), format!("{}: {e}", config_path.display())))?,
1928    )
1929    .map_err(|e| invalid(format!("{}: {e}", config_path.display())))?;
1930    let mut map = root.as_object().cloned().ok_or_else(|| invalid("config.json is not an object"))?;
1931    let architecture =
1932        map.get("architectures").and_then(Value::as_array).and_then(|a| a.first()).and_then(Value::as_str).unwrap_or_default().to_string();
1933    if let Some(Value::Object(text)) = map.get("text_config").cloned() {
1934        for (k, v) in text {
1935            map.insert(k, v);
1936        }
1937    }
1938    let family =
1939        Family::of(&architecture).ok_or_else(|| invalid(format!("config.json: architecture `{architecture}` is not one this reader presents")))?;
1940    let h = Hparams { map, architecture };
1941    validate(family, &h)?;
1942    Ok((family, h))
1943}
1944
1945/// The draft blocks the converter counts in the stack, as the configuration states them: Qwen3.5's (`mtp.*`) and
1946/// GLM-4.5's (`model.layers.<n>.*`); `None` where only the tensors count them (a hybrid with no
1947/// `mtp_num_hidden_layers`).
1948fn stated_draft_blocks(family: Family, h: &Hparams) -> Option<u64> {
1949    match family {
1950        f if f.hybrid() => h.u64(&["mtp_num_hidden_layers"]),
1951        Family::Glm4Moe => Some(h.u64(&["num_nextn_predict_layers"]).unwrap_or(0)),
1952        _ => Some(0),
1953    }
1954}
1955
1956/// The draft blocks a checkpoint stores after its stack (`mtp.layers.<n>.*`) when its configuration does not count them,
1957/// as Qwen3-Next's does not: the converter counts them from the tensors.
1958fn draft_layers(c: &HfCheckpoint) -> u64 {
1959    let layers: BTreeSet<usize> = c.located.keys().filter_map(|n| layer_of(n, "mtp.layers.").map(|(l, _)| l)).collect();
1960    layers.len() as u64
1961}
1962
1963/// The ggml type a stored dtype is presented as: its own for f32, f16 and bf16; every other dtype widened to f32, as
1964/// the converter widens it before it writes anything.
1965fn ggml_type(d: Dtype) -> GgmlType {
1966    match d {
1967        Dtype::F16 => GgmlType::F16,
1968        Dtype::Bf16 => GgmlType::Bf16,
1969        _ => GgmlType::F32,
1970    }
1971}
1972
1973/// Elements stored as `d`, as the type [`ggml_type`] presents them: f32, f16 and bf16 as they are, every other dtype
1974/// converted to f32 as `torch`'s `.to(float32)` converts it.
1975fn widen(d: Dtype, raw: Vec<u8>) -> Vec<u8> {
1976    let f32s = |it: &mut dyn Iterator<Item = f32>| it.flat_map(f32::to_le_bytes).collect::<Vec<u8>>();
1977    match d {
1978        Dtype::F32 | Dtype::F16 | Dtype::Bf16 => raw,
1979        Dtype::F64 => f32s(&mut raw.chunks_exact(8).map(|c| f64::from_le_bytes(c.try_into().unwrap()) as f32)),
1980        Dtype::F8E4m3 => f32s(&mut raw.iter().map(|&b| e4m3fn_to_f32(b))),
1981        Dtype::F8E5m2 => f32s(&mut raw.iter().map(|&b| e5m2_to_f32(b))),
1982        Dtype::I64 => f32s(&mut raw.chunks_exact(8).map(|c| i64::from_le_bytes(c.try_into().unwrap()) as f32)),
1983        Dtype::I32 => f32s(&mut raw.chunks_exact(4).map(|c| i32::from_le_bytes(c.try_into().unwrap()) as f32)),
1984        Dtype::I16 => f32s(&mut raw.chunks_exact(2).map(|c| i16::from_le_bytes(c.try_into().unwrap()) as f32)),
1985        Dtype::I8 => f32s(&mut raw.iter().map(|&b| b as i8 as f32)),
1986        Dtype::U8 => f32s(&mut raw.iter().map(|&b| b as f32)),
1987        Dtype::Bool => f32s(&mut raw.iter().map(|&b| f32::from(b != 0))),
1988    }
1989}
1990
1991/// `name`'s layer and the rest of its name after `prefix<layer>.`.
1992fn layer_of<'a>(name: &'a str, prefix: &str) -> Option<(usize, &'a str)> {
1993    let rest = name.strip_prefix(prefix)?;
1994    let (l, tail) = rest.split_once('.')?;
1995    Some((l.parse().ok()?, tail))
1996}
1997
1998/// `mlp.experts.<x>.<proj>.weight` → `(x, proj)`.
1999fn expert_of(rest: &str) -> Option<(usize, &'static str)> {
2000    let r = rest.strip_prefix("mlp.experts.").or_else(|| rest.strip_prefix("block_sparse_moe.experts."))?;
2001    let (x, tail) = r.split_once('.')?;
2002    let x: usize = x.parse().ok()?;
2003    // Mixtral names an expert's gate `w1`, its down `w2` and its up `w3`.
2004    let proj = match tail {
2005        "gate_proj.weight" | "w1.weight" => "gate_proj",
2006        "up_proj.weight" | "w3.weight" => "up_proj",
2007        "down_proj.weight" | "w2.weight" => "down_proj",
2008        _ => return None,
2009    };
2010    Some((x, proj))
2011}
2012
2013/// A three-dimensional tensor's shape.
2014fn dims3(e: &Entry, name: &str) -> io::Result<(usize, usize, usize)> {
2015    match e.shape[..] {
2016        [a, b, c] => Ok((a as usize, b as usize, c as usize)),
2017        _ => Err(invalid(format!("{name}: {:?} is not three-dimensional", e.shape))),
2018    }
2019}
2020
2021/// Llama's query or key rows (or bias) in the converter's order: each head's `[2, half]` halves interleaved into
2022/// `[half, 2]` pairs, the rotation pairing llama.cpp's interleaved rope turns.
2023fn llama_permute(name: &str, e: &Entry, heads: usize) -> Form {
2024    let rows = e.shape[0] as usize;
2025    let n = if e.shape.len() > 1 { (e.elements() as usize) / rows } else { 1 };
2026    let per = rows / heads.max(1);
2027    let half = per / 2;
2028    let src_row = |o: usize| (o / per) * per + (o % 2) * half + (o % per) / 2;
2029    Form::Gather {
2030        src: name.to_string(),
2031        outer: 1,
2032        m: rows,
2033        n,
2034        rows: (0..rows).map(src_row).collect(),
2035        cols: (0..n).collect(),
2036        transpose: false,
2037    }
2038}
2039
2040/// The rotary parameters as the converter assembles them: `rope_parameters` (or `rope_scaling`), with the global
2041/// theta, the sliding-window theta, the type, the partial factor and the original context mirrored in when the
2042/// checkpoint states them beside it.
2043fn rope_parameters(h: &Hparams) -> Map<String, Value> {
2044    let mut rope = match h.map.get("rope_parameters").or_else(|| h.map.get("rope_scaling")) {
2045        Some(Value::Object(o)) => o.clone(),
2046        _ => Map::new(),
2047    };
2048    if !rope.contains_key("full_attention") && !rope.contains_key("sliding_attention") {
2049        let local = h.find(&["local_rope_theta", "rope_local_theta", "rope_theta_local", "swa_rope_theta", "rope_local_base_freq"]);
2050        if let Some(local) = local {
2051            let mut sliding = Map::new();
2052            sliding.insert("rope_theta".into(), local.clone());
2053            rope.insert("sliding_attention".into(), Value::Object(sliding));
2054        }
2055        let theta = h.find(&["global_rope_theta", "rope_global_theta", "rope_theta_global", "rope_theta", "rotary_emb_base"]);
2056        if !rope.contains_key("rope_theta") {
2057            if let Some(t) = theta {
2058                rope.insert("rope_theta".into(), t.clone());
2059            }
2060        }
2061        if !rope.contains_key("rope_type") {
2062            if let Some(t) = rope.get("type").cloned() {
2063                rope.insert("rope_type".into(), t);
2064            }
2065        }
2066        if !rope.contains_key("partial_rotary_factor") {
2067            if let Some(p) = h.find(&["partial_rotary_factor", "rope_pct", "rope_percent"]) {
2068                rope.insert("partial_rotary_factor".into(), p.clone());
2069            }
2070        }
2071        if !rope.contains_key("original_max_position_embeddings") {
2072            if let Some(o) = h.find(&["original_max_position_embeddings"]) {
2073                rope.insert("original_max_position_embeddings".into(), o.clone());
2074            }
2075        }
2076    }
2077    rope
2078}
2079
2080/// The parameters the rotation of full-attention layers reads.
2081fn full_rope(rope: &Map<String, Value>) -> Map<String, Value> {
2082    match rope.get("full_attention") {
2083        Some(Value::Object(o)) => o.clone(),
2084        _ => rope.clone(),
2085    }
2086}
2087
2088/// `TextModel.set_gguf_parameters`: the keys every decoder writes.
2089fn base_parameters(h: &Hparams, rope: &Map<String, Value>, w: &mut Writer, block_count: u64, f: Family) -> io::Result<()> {
2090    w.u32("block_count", block_count);
2091    if let Some(n) = h.u64(&["max_position_embeddings", "n_ctx", "n_positions", "max_length", "max_sequence_length", "model_max_length"]) {
2092        w.u32("context_length", n);
2093    }
2094    if let Some(n) = h.u64(&["hidden_size", "n_embd", "dim"]) {
2095        w.u32("embedding_length", n);
2096    }
2097    if let Some(n) = h.u64(&["prefix_dense_intermediate_size", "dense_intermediate_size", "intermediate_size", "n_inner", "hidden_dim"]) {
2098        w.u32("feed_forward_length", n);
2099    }
2100    if let Some(n) = h.u64(&["num_attention_heads", "n_head", "n_heads"]) {
2101        w.u32("attention.head_count", n);
2102    }
2103    // Latent attention is read as one key-value head that every query head expands.
2104    let kv = if f == Family::Deepseek2 { Some(1) } else { h.u64(&["num_key_value_heads", "n_kv_heads"]) };
2105    if let Some(n) = kv {
2106        w.u32("attention.head_count_kv", n);
2107    }
2108    if h.map.get("is_causal").and_then(Value::as_bool) == Some(false) {
2109        w.bool("attention.causal", false);
2110    }
2111    let rp = full_rope(rope);
2112    if let Some(kind) = rp.get("rope_type").and_then(Value::as_str) {
2113        let factor = rp.get("factor").and_then(Value::as_f64);
2114        match (kind, factor) {
2115            ("linear", Some(factor)) => {
2116                w.str("rope.scaling.type", "linear");
2117                w.f32("rope.scaling.factor", factor);
2118            }
2119            ("yarn", Some(factor)) => {
2120                w.str("rope.scaling.type", "yarn");
2121                w.f32("rope.scaling.factor", factor);
2122                let orig = rp
2123                    .get("original_max_position_embeddings")
2124                    .and_then(Value::as_u64)
2125                    .ok_or_else(|| invalid("config.json: a YaRN rotation without original_max_position_embeddings"))?;
2126                w.u32("rope.scaling.original_context_length", orig);
2127                if let Some(t) = rp.get("truncate").and_then(Value::as_bool) {
2128                    w.bool("rope.scaling.yarn_truncate", t);
2129                }
2130                if let Some(x) = rp.get("extrapolation_factor").and_then(Value::as_f64) {
2131                    w.f32("rope.scaling.yarn_ext_factor", x);
2132                }
2133                if let Some(x) = rp.get("attention_factor").or_else(|| rp.get("attn_factor")).and_then(Value::as_f64) {
2134                    w.f32("rope.scaling.yarn_attn_factor", x);
2135                }
2136                if let Some(x) = rp.get("beta_fast").and_then(Value::as_f64) {
2137                    w.f32("rope.scaling.yarn_beta_fast", x);
2138                }
2139                if let Some(x) = rp.get("beta_slow").and_then(Value::as_f64) {
2140                    w.f32("rope.scaling.yarn_beta_slow", x);
2141                }
2142            }
2143            ("su" | "longrope", _) => w.str("rope.scaling.type", "longrope"),
2144            _ => {}
2145        }
2146    }
2147    if let Some(Value::Array(sections)) = rope.get("mrope_section") {
2148        let mut v: Vec<i64> = sections.iter().filter_map(Value::as_i64).collect();
2149        while v.len() < 4 {
2150            v.push(0);
2151        }
2152        w.put(
2153            "rope.dimension_sections",
2154            MetaValue::Array { ty: 5, items: v[..4].iter().map(|&x| MetaValue::I32(x as i32)).collect() },
2155        );
2156    }
2157    if let Some(t) = rp.get("rope_theta").and_then(Value::as_f64) {
2158        w.f32("rope.freq_base", t);
2159    }
2160    if let Some(t) = rope.get("sliding_attention").and_then(|s| s.get("rope_theta")).and_then(Value::as_f64) {
2161        w.f32("rope.freq_base_swa", t);
2162    }
2163    let rms = if f == Family::Deepseek2 { Some(h.f64(&["rms_norm_eps"]).unwrap_or(1e-6)) } else { h.f64(&["rms_norm_eps", "norm_eps"]) };
2164    if let Some(e) = rms {
2165        w.f32("attention.layer_norm_rms_epsilon", e);
2166    }
2167    if let Some(e) = h.f64(&["layer_norm_eps", "layer_norm_epsilon", "norm_epsilon"]) {
2168        w.f32("attention.layer_norm_epsilon", e);
2169    }
2170    if let Some(n) = h.u64(&["num_local_experts", "num_experts", "n_routed_experts"]) {
2171        w.u32("expert_count", n);
2172    }
2173    if let Some(n) = h.u64(&["num_experts_per_tok", "num_experts_per_token", "top_k_experts"]) {
2174        w.u32("expert_used_count", n);
2175    }
2176    if let Some(n) = h.map.get("n_group").and_then(Value::as_u64) {
2177        w.u32("expert_group_count", n);
2178    }
2179    if let Some(n) = h.map.get("topk_group").and_then(Value::as_u64) {
2180        w.u32("expert_group_used_count", n);
2181    }
2182    let score = h.find(&[
2183        "score_function",
2184        "scoring_func",
2185        "score_func",
2186        "moe_router_activation",
2187        "moe_router_activation_func",
2188        "expert_selection_fn",
2189        "router_score_func",
2190    ]);
2191    if let Some(s) = score.and_then(Value::as_str) {
2192        let id = match s {
2193            "softmax" => 1,
2194            "sigmoid" => 2,
2195            "sqrtsoftplus" => 4,
2196            other => return Err(invalid(format!("config.json: expert score function `{other}` is not one the converter writes"))),
2197        };
2198        w.u32("expert_gating_func", id);
2199    }
2200    if let Some(hd) = h.map.get("head_dim").and_then(Value::as_u64) {
2201        w.u32("attention.key_length", hd);
2202        w.u32("attention.value_length", hd);
2203    }
2204    Ok(())
2205}
2206
2207/// Qwen3.8-Flash-Next's own keys (`Qwen4ExpTextModel.set_gguf_parameters`): the hyper-connections, QSA's indexer and
2208/// every block's compression ratio (its full-attention layers' and the draft block's), and the per-layer embedding's
2209/// layer, n-grams, convolution and end of document. Its hash constants and table width are the checkpoint's tensors'
2210/// ([`HfCheckpoint::ple_parameters`]).
2211fn qwen4exp_parameters(h: &Hparams, w: &mut Writer, block_count: u64, n_layers: u64) -> io::Result<()> {
2212    w.u32("hyper_connection.count", h.need_u64("hc_count")?);
2213    w.u32("hyper_connection.low_rank", h.need_u64("hc_lowrank")?);
2214    w.u32("attention.indexer.head_count", h.need_u64("indexer_n_heads")?);
2215    w.u32("attention.indexer.key_length", h.need_u64("indexer_head_dim")?);
2216    w.u32("attention.indexer.top_k", h.need_u64("indexer_budget")?);
2217    let ratio = h.need_u64("indexer_compress_ratio")?;
2218    // Transformers saves a QSA layer as `indexed_attention`, Qwen's configurations name it `full_attention`; QSA selects
2219    // on both. The converter, reading the configuration through transformers, matches `full_attention` alone and so
2220    // writes every QSA layer of the stack a ratio of 0 (llama.cpp then attends it densely): this writes the model.
2221    let full: Vec<bool> = match h.map.get("layer_types") {
2222        Some(Value::Array(types)) => {
2223            types.iter().map(|t| matches!(t.as_str(), Some("full_attention" | "indexed_attention"))).collect()
2224        }
2225        _ => {
2226            let every = h.u64(&["full_attention_interval"]).unwrap_or(4);
2227            (0..n_layers).map(|l| (l + 1) % every == 0).collect()
2228        }
2229    };
2230    let ratios: Vec<MetaValue> = (0..block_count as usize)
2231        .map(|l| MetaValue::U32(if l >= n_layers as usize || full[l] { ratio as u32 } else { 0 }))
2232        .collect();
2233    w.put("attention.compress_ratios", MetaValue::Array { ty: 4, items: ratios });
2234    let layers: Vec<u64> = match h.map.get("ple_layer_ids") {
2235        Some(Value::Array(ids)) => ids.iter().filter_map(Value::as_u64).map(|l| l - 1).collect(),
2236        _ => Vec::new(),
2237    };
2238    if layers.is_empty() {
2239        return Ok(());
2240    }
2241    w.put("ple.layers", MetaValue::Array { ty: 4, items: layers.iter().map(|&l| MetaValue::U32(l as u32)).collect() });
2242    w.u32("ple.ngram_size", h.need_u64("ngram_size")?);
2243    w.u32("ple.heads_per_ngram", h.need_u64("heads_per_ngram")?);
2244    w.u32("ple.conv_kernel", h.need_u64("ple_conv_kernel_size")?);
2245    // The hash resets its n-grams on the primary end of document: the last of a list.
2246    let eos = match h.map.get("eos_token_id") {
2247        Some(Value::Array(ids)) => ids.last().and_then(Value::as_u64),
2248        Some(v) => v.as_u64(),
2249        None => None,
2250    }
2251    .ok_or_else(|| invalid("config.json: eos_token_id is required: the n-gram hash resets on it"))?;
2252    w.u32("ple.eos_token_id", eos);
2253    if let Some(image) = h.map.get("image_token_id").and_then(Value::as_u64) {
2254        w.u32("ple.image_token_id", image);
2255    }
2256    Ok(())
2257}
2258
2259/// Each family's own keys, written after the base's (a repeated key replaces the base's value).
2260fn family_parameters(f: Family, h: &Hparams, rope: &Map<String, Value>, w: &mut Writer, block_count: u64) -> io::Result<()> {
2261    let head_dim = || -> io::Result<u64> {
2262        Ok(match h.u64(&["head_dim"]) {
2263            Some(d) => d,
2264            None => h.need_u64("hidden_size")? / h.need_u64("num_attention_heads")?,
2265        })
2266    };
2267    match f {
2268        Family::Llama => {
2269            w.u32("vocab_size", h.need_u64("vocab_size")?);
2270            w.u32("rope.dimension_count", head_dim()?);
2271        }
2272        Family::Qwen2 | Family::Qwen3 => {}
2273        Family::Qwen3Moe | Family::Qwen2Moe => {
2274            if let Some(n) = h.u64(&["moe_intermediate_size"]) {
2275                w.u32("expert_feed_forward_length", n);
2276            }
2277            if let Some(n) = h.u64(&["shared_expert_intermediate_size"]) {
2278                w.u32("expert_shared_feed_forward_length", n);
2279            }
2280        }
2281        Family::Qwen35 | Family::Qwen35Moe | Family::Qwen3Next | Family::Qwen4Exp => {
2282            if let Some(n) = h.u64(&["moe_intermediate_size"]) {
2283                w.u32("expert_feed_forward_length", n);
2284            }
2285            if let Some(n) = h.u64(&["shared_expert_intermediate_size"]) {
2286                w.u32("expert_shared_feed_forward_length", n);
2287            }
2288            let n_layers = h.need_u64("num_hidden_layers")?;
2289            if block_count > n_layers {
2290                w.u32("nextn_predict_layers", block_count - n_layers);
2291            }
2292            w.u32("ssm.conv_kernel", h.need_u64("linear_conv_kernel_dim")?);
2293            w.u32("ssm.state_size", h.need_u64("linear_key_head_dim")?);
2294            w.u32("ssm.group_count", h.need_u64("linear_num_key_heads")?);
2295            w.u32("ssm.time_step_rank", h.need_u64("linear_num_value_heads")?);
2296            w.u32("ssm.inner_size", h.need_u64("linear_value_head_dim")? * h.need_u64("linear_num_value_heads")?);
2297            if let Some(Value::Array(types)) = h.map.get("layer_types") {
2298                if types.len() as u64 != n_layers {
2299                    return Err(invalid(format!("config.json: layer_types has {} entries for {n_layers} layers", types.len())));
2300                }
2301                let mut recurrent: Vec<MetaValue> =
2302                    types.iter().map(|t| MetaValue::Bool(t.as_str() == Some("linear_attention"))).collect();
2303                recurrent.resize(block_count as usize, MetaValue::Bool(false));
2304                w.put("attention.recurrent_layers", MetaValue::Array { ty: 7, items: recurrent });
2305            }
2306            w.u32("full_attention_interval", h.u64(&["full_attention_interval"]).unwrap_or(4));
2307            let factor = rope
2308                .get("partial_rotary_factor")
2309                .or_else(|| h.map.get("partial_rotary_factor"))
2310                .and_then(Value::as_f64)
2311                .unwrap_or(0.25);
2312            w.u32("rope.dimension_count", (head_dim()? as f64 * factor) as u64);
2313            // Qwen3.5 rotates in multimodal sections; Qwen3-Next's rotation is a plain NEOX one.
2314            if f != Family::Qwen3Next && !rope.contains_key("mrope_section") {
2315                w.put(
2316                    "rope.dimension_sections",
2317                    MetaValue::Array { ty: 5, items: [11, 11, 10, 0].iter().map(|&x| MetaValue::I32(x)).collect() },
2318                );
2319            }
2320            if f == Family::Qwen4Exp {
2321                qwen4exp_parameters(h, w, block_count, n_layers)?;
2322            }
2323        }
2324        // `GemmaModel.set_gguf_parameters` writes nothing past what `base_parameters` writes.
2325        Family::Gemma => {}
2326        // `Gemma2Model.set_gguf_parameters`: both caps and the window. `base_parameters` writes the heads, the head
2327        // width and the rotation base, which the converter leaves out and llama.cpp reads where it is written.
2328        Family::Gemma2 => {
2329            for key in ["attn_logit_softcapping", "final_logit_softcapping"] {
2330                w.f32(key, h.f64(&[key]).ok_or_else(|| invalid(format!("config.json: no `{key}`")))?);
2331            }
2332            w.u32("attention.sliding_window", h.need_u64("sliding_window")?);
2333        }
2334        Family::Gemma3 => {
2335            let get = |k: &str| h.map.get(k).filter(|v| !v.is_null());
2336            w.u32("context_length", get("max_position_embeddings").and_then(Value::as_u64).unwrap_or(131072));
2337            w.u32("attention.head_count", get("num_attention_heads").and_then(Value::as_u64).unwrap_or(8));
2338            w.f32("attention.layer_norm_rms_epsilon", get("rms_norm_eps").and_then(Value::as_f64).unwrap_or(1e-6));
2339            let hd = get("head_dim").and_then(Value::as_u64).unwrap_or(256);
2340            w.u32("attention.key_length", hd);
2341            w.u32("attention.value_length", hd);
2342            let theta = full_rope(rope).get("rope_theta").and_then(Value::as_f64).unwrap_or(1_000_000.0);
2343            w.f32("rope.freq_base", theta);
2344            if let Some(cap) = get("final_logit_softcapping").and_then(Value::as_f64).filter(|c| *c != 0.0) {
2345                w.f32("final_logit_softcapping", cap);
2346            }
2347            if get("sliding_window_pattern").and_then(Value::as_u64) != Some(1) {
2348                let window = get("sliding_window")
2349                    .and_then(Value::as_u64)
2350                    .ok_or_else(|| invalid("config.json: gemma3 without sliding_window"))?;
2351                w.u32("attention.sliding_window", window);
2352            }
2353            w.u32("attention.head_count_kv", get("num_key_value_heads").and_then(Value::as_u64).unwrap_or(4));
2354        }
2355        Family::GptOss => {
2356            w.u32("attention.sliding_window", h.need_u64("sliding_window")?);
2357            w.u32("expert_feed_forward_length", h.need_u64("intermediate_size")?);
2358        }
2359        Family::Glm4 => {
2360            // `int(head_dim · partial_rotary_factor)` rotated dimensions of each head (half, the converter's default).
2361            let partial = rope.get("partial_rotary_factor").and_then(Value::as_f64).unwrap_or(0.5);
2362            w.u32("rope.dimension_count", (head_dim()? as f64 * partial) as u64);
2363        }
2364        Family::Glm4Moe => {
2365            // Its rotation as GLM-4's; DeepSeek's mixture keys, the experts always scored by sigmoids; the draft blocks
2366            // counted in the stack and named.
2367            let partial = rope.get("partial_rotary_factor").and_then(Value::as_f64).unwrap_or(0.5);
2368            w.u32("rope.dimension_count", (head_dim()? as f64 * partial) as u64);
2369            if let Some(n) = h.u64(&["moe_intermediate_size"]) {
2370                w.u32("expert_feed_forward_length", n);
2371            }
2372            if let Some(n) = h.u64(&["n_shared_experts"]) {
2373                w.u32("expert_shared_count", n);
2374            }
2375            if let Some(n) = h.u64(&["first_k_dense_replace"]) {
2376                w.u32("leading_dense_block_count", n);
2377            }
2378            w.u32("expert_gating_func", 2);
2379            if let Some(s) = h.f64(&["routed_scaling_factor"]) {
2380                w.f32("expert_weights_scale", s);
2381            }
2382            if let Some(b) = h.map.get("norm_topk_prob").and_then(Value::as_bool) {
2383                w.bool("expert_weights_norm", b);
2384            }
2385            if let Some(n) = h.u64(&["num_nextn_predict_layers"]) {
2386                w.u32("nextn_predict_layers", n);
2387            }
2388        }
2389        Family::Phi3 => {
2390            // `int(partial_rotary_factor · hidden_size) // heads` rotated dimensions, the original context (both read
2391            // from the rope parameters, the configuration's own keys where they name none), and a window of 0 where the
2392            // configuration names none (Phi-4).
2393            let (d, heads) = (h.need_u64("hidden_size")?, h.need_u64("num_attention_heads")?);
2394            let partial = rope.get("partial_rotary_factor").and_then(Value::as_f64).unwrap_or(1.0);
2395            w.u32("rope.dimension_count", (partial * d as f64) as u64 / heads);
2396            let original = rope
2397                .get("original_max_position_embeddings")
2398                .and_then(Value::as_u64)
2399                .ok_or_else(|| invalid("config.json: no original_max_position_embeddings, which the converter's Phi-3 class reads"))?;
2400            w.u32("rope.scaling.original_context_length", original);
2401            w.u32("attention.sliding_window", h.u64(&["sliding_window"]).unwrap_or(0));
2402        }
2403        Family::MinimaxM2 => {
2404            w.u32("expert_feed_forward_length", h.need_u64("intermediate_size")?);
2405            w.u32("rope.dimension_count", h.need_u64("rotary_dim")?);
2406        }
2407        Family::Llama4 => {
2408            // The converter reads `intermediate_size_mlp` as the dense FFN's and `intermediate_size` as an expert's.
2409            w.u32("vocab_size", h.need_u64("vocab_size")?);
2410            w.u32("rope.dimension_count", head_dim()?);
2411            w.u32("feed_forward_length", h.need_u64("intermediate_size_mlp")?);
2412            w.u32("interleave_moe_layer_step", h.need_u64("interleave_moe_layer_step")?);
2413            w.u32("expert_feed_forward_length", h.need_u64("intermediate_size")?);
2414            if let Some(Value::Array(types)) = h.map.get("layer_types") {
2415                if types.iter().all(|t| t.as_str() == Some("full_attention")) {
2416                    w.u32("attention.sliding_window", 0);
2417                }
2418            }
2419        }
2420        Family::Deepseek2 => {
2421            let n_layers = h.need_u64("num_hidden_layers")?;
2422            let moe = h.map.get("n_routed_experts").is_some_and(|v| !v.is_null());
2423            let dense = h.u64(&["first_k_dense_replace"]).unwrap_or(if moe { 0 } else { n_layers });
2424            w.u32("leading_dense_block_count", dense);
2425            let kv_lora = h.u64(&["kv_lora_rank"]).unwrap_or(512);
2426            w.u32("vocab_size", h.need_u64("vocab_size")?);
2427            if let Some(q) = h.map.get("q_lora_rank").and_then(Value::as_u64) {
2428                w.u32("attention.q_lora_rank", q);
2429            }
2430            let rope_dim = h.need_u64("qk_rope_head_dim")?;
2431            let nope = h.need_u64("qk_nope_head_dim")?;
2432            w.u32("attention.kv_lora_rank", kv_lora);
2433            w.u32("attention.key_length", kv_lora + rope_dim);
2434            w.u32("attention.value_length", kv_lora);
2435            w.u32("attention.key_length_mla", nope + rope_dim);
2436            w.u32("attention.value_length_mla", h.need_u64("v_head_dim")?);
2437            let expert_ff = h
2438                .u64(&["moe_intermediate_size", "intermediate_size"])
2439                .ok_or_else(|| invalid("config.json: no moe_intermediate_size or intermediate_size"))?;
2440            w.u32("expert_feed_forward_length", expert_ff);
2441            if let Some(n) = h.map.get("n_routed_experts").and_then(Value::as_u64) {
2442                w.u32("expert_count", n);
2443            }
2444            w.u32("expert_shared_count", h.map.get("n_shared_experts").and_then(Value::as_u64).unwrap_or(0));
2445            if let Some(s) = h.map.get("routed_scaling_factor").and_then(Value::as_f64) {
2446                w.f32("expert_weights_scale", s);
2447            }
2448            if h.map.get("norm_topk_prob").and_then(Value::as_bool) == Some(true) {
2449                w.bool("expert_weights_norm", true);
2450            }
2451            w.u32("rope.dimension_count", rope_dim);
2452            if let Some(m) = rope.get("mscale_all_dim").and_then(Value::as_f64) {
2453                w.f32("rope.scaling.yarn_log_multiplier", 0.1 * m);
2454            }
2455        }
2456    }
2457    Ok(())
2458}
2459
2460/// Llama 3's per-frequency rotary factors, as the converter computes them in f32: `1` above the high-frequency
2461/// wavelength, `factor` below the low one, and the smooth blend between.
2462fn llama3_rope_factors(h: &Hparams, rope: &Map<String, Value>) -> Option<Vec<f32>> {
2463    let rp = full_rope(rope);
2464    if !rp.get("rope_type").and_then(Value::as_str).is_some_and(|t| t.eq_ignore_ascii_case("llama3")) {
2465        return None;
2466    }
2467    let get = |k: &str, d: f64| rp.get(k).and_then(Value::as_f64).unwrap_or(d);
2468    let base = get("rope_theta", 10000.0) as f32;
2469    let dim = match h.u64(&["head_dim"]) {
2470        Some(d) => d,
2471        None => h.u64(&["hidden_size"])? / h.u64(&["num_attention_heads"])?,
2472    } as f32;
2473    let factor = get("factor", 8.0);
2474    let low = get("low_freq_factor", 1.0);
2475    let high = get("high_freq_factor", 4.0);
2476    let old = rp.get("original_max_position_embeddings").and_then(Value::as_f64).unwrap_or(8192.0);
2477    let low_wavelen = old / low;
2478    let high_wavelen = old / high;
2479    let two_pi = (2.0 * std::f64::consts::PI) as f32;
2480    Some(
2481        (0..(dim as usize).div_ceil(2))
2482            .map(|i| {
2483                let freq = 1.0f32 / base.powf((2 * i) as f32 / dim);
2484                let wavelen = two_pi / freq;
2485                if (wavelen as f64) < high_wavelen {
2486                    1.0
2487                } else if (wavelen as f64) > low_wavelen {
2488                    factor as f32
2489                } else {
2490                    let smooth = ((old as f32 / wavelen) - low as f32) / ((high - low) as f32);
2491                    1.0f32 / ((1.0f32 - smooth) / factor as f32 + smooth)
2492                }
2493            })
2494            .collect(),
2495    )
2496}
2497
2498/// The number of real vocabulary rows Gemma's converter keeps: the configured vocabulary when the checkpoint ships a
2499/// sentencepiece model (its pieces past that are ignored), else the tokenizer's vocabulary and added tokens.
2500fn gemma_vocab(dir: &Path, h: &Hparams) -> io::Result<usize> {
2501    if dir.join("tokenizer.model").is_file() {
2502        if let Some(n) = h.u64(&["vocab_size_per_layer_input", "vocab_size"]) {
2503            return Ok(n as usize);
2504        }
2505        return sentencepiece_pieces(&std::fs::read(dir.join("tokenizer.model"))?);
2506    }
2507    let path = dir.join("tokenizer.json");
2508    let json: Value = serde_json::from_str(&std::fs::read_to_string(&path)?).map_err(|e| invalid(format!("{}: {e}", path.display())))?;
2509    let vocab = json["model"]["vocab"].as_object().map_or(0, Map::len);
2510    let added = json["added_tokens"].as_array().map_or(0, Vec::len);
2511    Ok(vocab + added)
2512}
2513
2514/// The pieces of a sentencepiece `ModelProto`: its top-level field 1 entries.
2515fn sentencepiece_pieces(bytes: &[u8]) -> io::Result<usize> {
2516    let varint = |at: &mut usize| -> io::Result<u64> {
2517        let mut v = 0u64;
2518        for shift in (0..64).step_by(7) {
2519            let b = *bytes.get(*at).ok_or_else(|| invalid("tokenizer.model ends inside a varint"))?;
2520            *at += 1;
2521            v |= u64::from(b & 0x7f) << shift;
2522            if b & 0x80 == 0 {
2523                return Ok(v);
2524            }
2525        }
2526        Err(invalid("tokenizer.model holds an overlong varint"))
2527    };
2528    let (mut at, mut pieces) = (0usize, 0usize);
2529    while at < bytes.len() {
2530        let key = varint(&mut at)?;
2531        match key & 7 {
2532            0 => {
2533                varint(&mut at)?;
2534            }
2535            1 => at += 8,
2536            2 => {
2537                let len = varint(&mut at)? as usize;
2538                at += len;
2539            }
2540            5 => at += 4,
2541            w => return Err(invalid(format!("tokenizer.model: wire type {w}"))),
2542        }
2543        if key >> 3 == 1 {
2544            pieces += 1;
2545        }
2546    }
2547    Ok(pieces)
2548}
2549
2550impl SpecSource for HfCheckpoint {
2551    fn arch(&self) -> &str {
2552        &self.arch
2553    }
2554    fn arch_meta(&self, suffix: &str) -> Option<&MetaValue> {
2555        self.meta.get(&format!("{}.{suffix}", self.arch))
2556    }
2557    fn dims(&self, name: &str) -> Option<&[u64]> {
2558        self.index.get(name).map(|&i| self.views[i].dims.as_slice())
2559    }
2560    fn tensor_names(&self) -> Vec<&str> {
2561        self.views.iter().map(|v| v.name.as_str()).collect()
2562    }
2563}
2564
2565impl WeightSource for HfCheckpoint {
2566    fn describe(&self, name: &str) -> io::Result<(GgmlType, Vec<u64>)> {
2567        let v = self.index.get(name).map(|&i| &self.views[i]).ok_or_else(|| invalid(format!("no tensor {name}")))?;
2568        Ok((self.ty_of(&v.form)?, v.dims.clone()))
2569    }
2570    fn row_bytes(&mut self, name: &str, first: usize, count: usize) -> io::Result<Vec<u8>> {
2571        let v = self.index.get(name).map(|&i| &self.views[i]).ok_or_else(|| invalid(format!("no tensor {name}")))?;
2572        let row = v.dims.first().copied().unwrap_or(1) as usize;
2573        self.read(&v.form, first * row, count * row)
2574    }
2575    fn has(&self, name: &str) -> bool {
2576        self.index.contains_key(name)
2577    }
2578}