Skip to main content

qualia_core_db/inference/gguf_sharder/
hyperparams.rs

1//! Architecture hyper-parameters parsed from the GGUF KV section, plus the
2//! architecture id / feature-flag constants and the `general.architecture` mapper.
3
4/// Default RoPE base for Llama 3 / SmolLM2 when GGUF omits `llama.rope.freq_base`.
5pub const DEFAULT_ROPE_FREQ_BASE: f32 = 100_000.0;
6
7/// Architecture id (stored in P64 hparams + used for support gating).
8pub const ARCH_UNKNOWN: u32 = 0;
9pub const ARCH_LLAMA: u32 = 1;
10pub const ARCH_GEMMA: u32 = 2;
11pub const ARCH_GEMMA2: u32 = 3;
12pub const ARCH_GEMMA3: u32 = 4;
13/// Gemma 4 (E2B/E4B/…): dual head-dim SWA+global, PLE, shared KV — **not** standard Llama-shape.
14pub const ARCH_GEMMA4: u32 = 5;
15pub const ARCH_QWEN2: u32 = 6;
16pub const ARCH_GLM4: u32 = 7;
17pub const ARCH_PHI4: u32 = 8;
18pub const ARCH_DEEPSEEK_MOE: u32 = 9;
19pub const ARCH_OTHER: u32 = 255;
20
21/// Feature flags on [`GgufHyperparams::arch_flags`].
22pub const ARCH_FLAG_HAS_PLE: u32 = 1 << 0;
23pub const ARCH_FLAG_HAS_SWA: u32 = 1 << 1;
24pub const ARCH_FLAG_HAS_SHARED_KV: u32 = 1 << 2;
25pub const ARCH_FLAG_HAS_QK_NORM: u32 = 1 << 3;
26pub const ARCH_FLAG_HAS_SOFTCAP: u32 = 1 << 4;
27
28/// Architecture hyper-parameters parsed from the GGUF KV section.
29#[derive(Debug, Clone, Copy, Default, PartialEq)]
30pub struct GgufHyperparams {
31    pub n_layer: u32,
32    pub n_embd: u32,
33    pub n_head: u32,
34    /// Grouped-query KV heads; `0` means MHA (`n_kv_head == n_head`).
35    pub n_kv_head: u32,
36    /// `llama.rope.freq_base` (FLOAT32 in GGUF); `0` → [`DEFAULT_ROPE_FREQ_BASE`].
37    pub rope_freq_base: f32,
38    /// Linear RoPE scale from `llama.rope.scale_linear` / `llama.rope.scaling.factor`; `0` → `1.0`.
39    pub rope_scale: f32,
40    /// Explicit head dim (`*.attention.key_length`); `0` → derive `n_embd / n_head`.
41    pub head_dim: u32,
42    /// SWA / local-attention head dim (`*.attention.key_length_swa`); `0` → same as `head_dim`.
43    pub head_dim_swa: u32,
44    /// Sliding-window size in tokens; `0` → full context attention only.
45    pub sliding_window: u32,
46    /// Last N layers share KV from the last non-shared layer of the same type (Gemma 4).
47    pub shared_kv_layers: u32,
48    /// Final logit softcapping (Gemma 2+); `0` → disabled.
49    pub logit_softcap: f32,
50    /// [`ARCH_*`] id from `general.architecture` (and tensor-feature refinement).
51    pub architecture: u32,
52    /// [`ARCH_FLAG_*`] bitmask.
53    pub arch_flags: u32,
54}
55
56impl GgufHyperparams {
57    pub fn effective_rope_freq_base(&self) -> f32 {
58        if self.rope_freq_base > 0.0 && self.rope_freq_base.is_finite() {
59            self.rope_freq_base
60        } else {
61            DEFAULT_ROPE_FREQ_BASE
62        }
63    }
64
65    /// Effective position divisor for RoPE (`scaled_pos = pos / scale`).
66    pub fn effective_rope_scale(&self) -> f32 {
67        if self.rope_scale > 0.0 && self.rope_scale.is_finite() {
68            self.rope_scale
69        } else {
70            1.0
71        }
72    }
73
74    /// Nominal `head_dim` (`n_embd / n_head` if `head_dim == 0`).
75    pub fn effective_head_dim(&self) -> u32 {
76        if self.head_dim > 0 {
77            self.head_dim
78        } else if self.n_head > 0 {
79            self.n_embd / self.n_head
80        } else {
81            128
82        }
83    }
84
85    pub fn head_dim(&self) -> u32 {
86        self.effective_head_dim()
87    }
88
89    pub fn effective_n_kv_head(&self) -> u32 {
90        if self.n_kv_head > 0 {
91            self.n_kv_head
92        } else {
93            self.n_head.max(1)
94        }
95    }
96
97    pub fn q_heads_per_kv(&self) -> u32 {
98        let kv = self.effective_n_kv_head();
99        if kv == 0 {
100            1
101        } else {
102            (self.n_head / kv).max(1)
103        }
104    }
105
106    pub fn gqa_ratio(&self) -> u32 {
107        self.q_heads_per_kv()
108    }
109
110    pub fn head_dim_swa_or(&self) -> u32 {
111        if self.head_dim_swa > 0 {
112            self.head_dim_swa
113        } else {
114            self.effective_head_dim()
115        }
116    }
117
118    /// Human-readable architecture name for logs / errors.
119    pub fn architecture_name(&self) -> &'static str {
120        match self.architecture {
121            ARCH_LLAMA => "llama",
122            ARCH_GEMMA => "gemma",
123            ARCH_GEMMA2 => "gemma2",
124            ARCH_GEMMA3 => "gemma3",
125            ARCH_GEMMA4 => "gemma4",
126            ARCH_QWEN2 => "qwen2",
127            ARCH_GLM4 => "glm4",
128            ARCH_PHI4 => "phi4",
129            ARCH_DEEPSEEK_MOE => "deepseek_moe",
130            ARCH_OTHER => "other",
131            _ => "unknown",
132        }
133    }
134
135    /// Whether the native decode path can run this architecture coherently.
136    ///
137    /// Gemma 4 (E2B/E4B) requires PLE, dual-RoPE SWA/global head dims, QK-norm, post-norms,
138    /// variable FFN width, and shared KV — none of which the Llama-shaped decode path implements.
139    /// Running it produces multilingual garbage (measured 2026-07-09 on gemma-4-E2B-it-Q4_K_M).
140    /// Override with `QUALIA_LLM_FORCE_UNSUPPORTED_ARCH=1` only for bring-up.
141    pub fn decode_supported(&self) -> Result<(), String> {
142        if std::env::var_os("QUALIA_LLM_FORCE_UNSUPPORTED_ARCH").is_some() {
143            return Ok(());
144        }
145        if self.architecture == ARCH_GEMMA4
146            || (self.arch_flags & ARCH_FLAG_HAS_PLE) != 0
147            || (self.arch_flags & ARCH_FLAG_HAS_SHARED_KV) != 0
148        {
149            let mut missing = Vec::new();
150            if (self.arch_flags & ARCH_FLAG_HAS_PLE) != 0 {
151                missing.push("per-layer embeddings (PLE)");
152            }
153            if (self.arch_flags & ARCH_FLAG_HAS_SWA) != 0 {
154                missing.push("sliding-window + dual head_dim");
155            }
156            if (self.arch_flags & ARCH_FLAG_HAS_SHARED_KV) != 0 {
157                missing.push("shared KV layers");
158            }
159            if (self.arch_flags & ARCH_FLAG_HAS_QK_NORM) != 0 {
160                missing.push("QK-norm");
161            }
162            if missing.is_empty() {
163                missing.push("gemma4 decoder graph");
164            }
165            return Err(format!(
166                "architecture '{}' is not supported by the native Llama-shaped decode path yet \
167                 (missing: {}). Convert/activate to p64 still works; coherent inference needs the \
168                 gemma4 graph. Set QUALIA_LLM_FORCE_UNSUPPORTED_ARCH=1 to force (will be garbage).",
169                self.architecture_name(),
170                missing.join(", ")
171            ));
172        }
173        Ok(())
174    }
175}
176
177/// Map `general.architecture` GGUF string → [`ARCH_*`].
178pub fn parse_architecture_id(name: &str) -> u32 {
179    let n = name.trim().to_ascii_lowercase();
180    match n.as_str() {
181        "llama" | "llama2" | "llama3" => ARCH_LLAMA,
182        "gemma" => ARCH_GEMMA,
183        "gemma2" => ARCH_GEMMA2,
184        "gemma3" => ARCH_GEMMA3,
185        "gemma4" => ARCH_GEMMA4,
186        "qwen2" | "qwen2vl" | "qwen3" | "qwen3.5" | "qwen3.6" => ARCH_QWEN2,
187        "glm" | "glm4" | "glm4.7" | "chatglm" => ARCH_GLM4,
188        "phi" | "phi3" | "phi4" => ARCH_PHI4,
189        "deepseek" | "deepseek2" | "deepseek3" | "deepseek_moe" => ARCH_DEEPSEEK_MOE,
190        "" => ARCH_UNKNOWN,
191        _ => ARCH_OTHER,
192    }
193}