metrale_config/
model_config.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: `ModelConfig`, the parsed checkpoint config: one field per model dimension or
4//! switch the engine reads, plus the fields serve sets at startup.
5//!
6//! Owner: config.
7//! Invariants: none beyond the types.
8
9use super::*;
10
11#[derive(Debug, Clone, Deserialize)]
12pub struct ModelConfig {
13    pub hidden_size: usize,
14    #[serde(default)]
15    pub num_hidden_layers: usize,
16    #[serde(default)]
17    pub intermediate_size: usize,
18    #[serde(default)]
19    pub vocab_size: usize,
20
21    #[serde(default)]
22    pub num_attention_heads: usize,
23    /// 2026-09-26: Per-layer Q-head counts (the laguna parser fills it). When non-empty,
24    /// `validate_config` requires one entry per layer.
25    #[serde(default)]
26    pub num_attention_heads_per_layer: Vec<usize>,
27    #[serde(default)]
28    pub num_key_value_heads: usize,
29    #[serde(default)]
30    pub head_dim: usize,
31    /// 2026-09-26: Fraction of `head_dim` that `rotary_dim()` rotates when `rotary_dim` is 0;
32    /// 1.0 when absent.
33    #[serde(default = "default_partial_rotary")]
34    pub partial_rotary_factor: f64,
35
36    #[serde(default)]
37    pub linear_num_key_heads: usize,
38    #[serde(default)]
39    pub linear_key_head_dim: usize,
40    #[serde(default)]
41    pub linear_num_value_heads: usize,
42    #[serde(default)]
43    pub linear_value_head_dim: usize,
44    #[serde(default = "default_conv_kernel")]
45    pub linear_conv_kernel_dim: usize,
46
47    #[serde(default)]
48    pub num_experts: usize,
49    #[serde(default)]
50    pub zero_expert_num: usize,
51    #[serde(default = "default_one")]
52    pub num_experts_per_tok: usize,
53    #[serde(default)]
54    pub moe_intermediate_size: usize,
55    #[serde(default)]
56    pub shared_expert_intermediate_size: usize,
57    #[serde(default)]
58    pub norm_topk_prob: bool,
59    #[serde(default = "default_one")]
60    pub decoder_sparse_step: usize,
61
62    /// 2026-09-26: Per-layer kind; when empty, `layer_type()` falls back to
63    /// `full_attention_interval`.
64    #[serde(default)]
65    pub layer_types: Vec<LayerType>,
66    /// 2026-09-26: Per-layer kind of the MTP / NextN layers past `num_hidden_layers`; empty
67    /// when there are none. They stay out of `layer_types`, whose length `validate_config`
68    /// checks against `num_hidden_layers`; [`ModelConfig::layer_type_at`] reads both.
69    #[serde(default)]
70    pub mtp_layer_types: Vec<LayerType>,
71    /// 2026-09-26: With `layer_types` empty, every `full_attention_interval`-th layer is
72    /// `FullAttention` and the rest `LinearAttention`.
73    #[serde(default = "default_one")]
74    pub full_attention_interval: usize,
75    /// 2026-09-26: 0 when absent or JSON null.
76    #[serde(default, deserialize_with = "nullable_u32")]
77    pub sliding_window: u32,
78
79    #[serde(default)]
80    pub max_position_embeddings: usize,
81    #[serde(default = "default_rope_theta")]
82    pub rope_theta: f64,
83
84    #[serde(default = "default_rms_eps")]
85    pub rms_norm_eps: f64,
86
87    /// 2026-09-26: JSON null gives 0.
88    #[serde(default, deserialize_with = "nullable_u32")]
89    pub bos_token_id: u32,
90    /// 2026-09-26: See [`Glm5NextRouterMode`]; `HfFp32` when absent.
91    #[serde(default)]
92    pub glm5next_router_mode: Glm5NextRouterMode,
93    /// 2026-09-26: The primary stop token; [`ModelConfig::eos_ids`] gives the full set.
94    #[serde(default, deserialize_with = "eos_token_id_field")]
95    pub eos_token_id: u32,
96    /// 2026-09-26: Every declared stop id, primary first. `parse_config` fills it for every
97    /// family. Empty means not populated (a hand-built `ModelConfig`), not "no stop tokens",
98    /// so read it through [`ModelConfig::eos_ids`].
99    #[serde(default)]
100    pub eos_token_ids: Vec<u32>,
101    #[serde(default)]
102    pub tie_word_embeddings: bool,
103    /// 2026-09-26: Set at serve startup from `--lm-head-dtype`.
104    /// `Some(true)` forces a BF16 LM head, `Some(false)` a quantized one, and `None` leaves
105    /// the choice to `skip_lm_head_quantization()`'s model rules.
106    #[serde(default)]
107    pub lm_head_bf16_override: Option<bool>,
108    /// 2026-09-26: Set by `--lm-head-dtype fp8`. When `skip_lm_head_quantization()` is false
109    /// the LM head is FP8 (the checkpoint's own FP8 head, else a quantized copy) instead of
110    /// NVFP4; a pre-packed NVFP4 head is used whatever this says.
111    #[serde(default)]
112    pub lm_head_fp8: bool,
113
114    #[serde(default)]
115    pub model_type: String,
116
117    #[serde(default)]
118    pub mtp_num_hidden_layers: usize,
119
120    #[serde(default)]
121    pub dspark_block_size: usize,
122    #[serde(default)]
123    pub dspark_noise_token_id: u32,
124    #[serde(default)]
125    pub dspark_target_layer_ids: Vec<usize>,
126    #[serde(default)]
127    pub dspark_markov_rank: usize,
128
129    #[serde(default)]
130    pub hybrid_override_pattern: String,
131    #[serde(default)]
132    pub mamba_num_heads: usize,
133    #[serde(default)]
134    pub mamba_head_dim: usize,
135    #[serde(default)]
136    pub ssm_state_size: usize,
137    #[serde(default)]
138    pub n_groups: usize,
139    #[serde(default)]
140    pub expand: usize,
141    /// 2026-09-26: Nemotron-H name; the nemotron_h arm of `parse_config` copies it into
142    /// `num_experts` when that is 0.
143    #[serde(default)]
144    pub n_routed_experts: usize,
145    /// 2026-09-26: Nemotron-H name; copied into `rms_norm_eps` when that is still the default.
146    #[serde(default)]
147    pub norm_eps: f64,
148    /// 2026-09-26: Nemotron-H name; copied into `linear_conv_kernel_dim` when that is still the
149    /// default.
150    #[serde(default)]
151    pub conv_kernel: usize,
152    /// 2026-09-26: Nemotron-H name; copied into `shared_expert_intermediate_size` when that is 0.
153    #[serde(default)]
154    pub moe_shared_expert_intermediate_size: usize,
155    #[serde(default = "default_one_f64")]
156    pub routed_scaling_factor: f64,
157    /// 2026-09-26: KDA forget-gate lower bound (`linear_attn_config.gate_lower_bound`). The
158    /// glm5_next parser refuses a config without it; for Kimi K3, 0.0 (absent) makes the host
159    /// KDA path run unbounded.
160    #[serde(default)]
161    pub linear_gate_lower_bound: f32,
162    /// 2026-09-26: SwiGLU clamp bound (`swiglu_limit`); 0.0 means no clamp. The glm5_next
163    /// parser refuses a config without a positive one.
164    #[serde(default)]
165    pub swiglu_limit: f32,
166    #[serde(default)]
167    pub mlp_only_layers: Vec<usize>,
168    #[serde(default)]
169    pub moe_latent_size: usize,
170    /// 2026-09-26: Per-layer routed-expert intermediate sizes, set by the Puzzle parser (one
171    /// entry per layer, 0 for non-MoE layers) and never read from JSON. Empty means the scalar
172    /// `moe_intermediate_size` for every layer.
173    #[serde(default, skip_deserializing, skip_serializing)]
174    pub moe_intermediate_sizes: Vec<usize>,
175    /// 2026-09-26: Per-layer top-k expert counts, same layout as `moe_intermediate_sizes`.
176    /// Empty means the scalar `num_experts_per_tok`.
177    #[serde(default, skip_deserializing, skip_serializing)]
178    pub num_experts_per_toks: Vec<usize>,
179
180    /// 2026-09-26: 0 means standard attention; non-zero makes `capabilities()` report MLA.
181    #[serde(default)]
182    pub kv_lora_rank: usize,
183    /// 2026-09-26: Per-layer KV cache `(num_kv_heads, head_dim)`, set by the model factory from
184    /// the weight loader; never read from JSON.
185    #[serde(default, skip_deserializing, skip_serializing)]
186    pub kv_layer_dims: Vec<(usize, usize)>,
187    #[serde(default)]
188    pub q_lora_rank: usize,
189    #[serde(default)]
190    pub qk_nope_head_dim: usize,
191    #[serde(default)]
192    pub qk_rope_head_dim: usize,
193    #[serde(default)]
194    pub v_head_dim: usize,
195
196    #[serde(default)]
197    pub ngram_vocab_size_ratio: usize,
198    #[serde(default)]
199    pub emb_neighbor_num: usize,
200    #[serde(default)]
201    pub emb_split_num: usize,
202    #[serde(default)]
203    pub ngram_vocab_size_base: usize,
204    #[serde(default)]
205    pub ngram_split_parts: usize,
206    #[serde(default)]
207    pub ple_layer_ids: Vec<usize>,
208    #[serde(default)]
209    pub ple_conv_kernel_size: usize,
210
211    #[serde(default)]
212    pub o_lora_rank: usize,
213    #[serde(default)]
214    pub o_groups: usize,
215    #[serde(default)]
216    pub yarn_mscale: f32,
217    #[serde(default)]
218    pub yarn_mscale_all_dim: f32,
219    #[serde(default)]
220    pub hc_mult: usize,
221    #[serde(default)]
222    pub hc_sinkhorn_iters: usize,
223    #[serde(default)]
224    pub hc_eps: f32,
225    #[serde(default)]
226    pub hc_lowrank: usize,
227    #[serde(default)]
228    pub final_norm_identity: bool,
229    /// 2026-09-26: Per-layer attention compression ratios; 0 means uncompressed. The GGUF
230    /// builder accepts more entries than `num_hidden_layers`, never fewer.
231    #[serde(default)]
232    pub compress_ratios: Vec<usize>,
233    #[serde(default)]
234    pub index_n_heads: usize,
235    #[serde(default)]
236    pub index_head_dim: usize,
237    #[serde(default)]
238    pub index_topk: usize,
239    /// 2026-09-26: The QSA indexer's compression ratio. The qwen4_exp parser sets it and
240    /// leaves `compress_ratios` empty; 0 means no indexer.
241    #[serde(default)]
242    pub index_compress_ratio: usize,
243    #[serde(default)]
244    pub index_kpool: usize,
245    #[serde(default)]
246    pub index_kpool_always_select_tail: bool,
247    #[serde(default)]
248    pub num_hash_layers: usize,
249
250    /// 2026-09-26: The compressor's RoPE base, separate from `rope_theta`; the GGUF builder
251    /// reads `attention.compress_rope_freq_base`.
252    #[serde(default)]
253    pub compress_rope_theta: f32,
254    #[serde(default)]
255    pub kv_source_layer_ids: Vec<usize>,
256    #[serde(default)]
257    pub index_source_layer_ids: Vec<usize>,
258    #[serde(default)]
259    pub candidate_source_layer_id: Option<usize>,
260    #[serde(default)]
261    pub candidate_topk_blocks: usize,
262    #[serde(default)]
263    pub candidate_block_size: usize,
264
265    #[serde(default)]
266    pub engram_layer_ids: Vec<usize>,
267    #[serde(default)]
268    pub engram_num_embeddings: Vec<u64>,
269    #[serde(default)]
270    pub engram_max_ngram_size: usize,
271    #[serde(default)]
272    pub engram_vocab_size: usize,
273    #[serde(default)]
274    pub engram_n_heads: usize,
275    #[serde(default)]
276    pub engram_head_dim: usize,
277    #[serde(default)]
278    pub engram_pad_token_id: u32,
279    #[serde(default)]
280    pub engram_compressed_vocab_size: usize,
281    #[serde(default)]
282    pub engram_multipliers: Vec<u64>,
283    #[serde(default)]
284    pub engram_primes: Vec<u64>,
285    #[serde(default)]
286    pub engram_offsets: Vec<u64>,
287
288    #[serde(default)]
289    pub yarn_factor: f32,
290    #[serde(default)]
291    pub yarn_beta_slow: f32,
292    #[serde(default)]
293    pub yarn_beta_fast: f32,
294    #[serde(default)]
295    pub yarn_original_max_position_embeddings: usize,
296    #[serde(default = "default_one_f32")]
297    pub yarn_attention_factor: f32,
298    #[serde(default)]
299    pub llama_4_scaling_beta: f32,
300    #[serde(default)]
301    pub llama_4_scaling_original_max_position_embeddings: usize,
302
303    /// 2026-09-26: `None` for text-only models, and for GLM-5.3 unless `glm_vision_enabled()`.
304    #[serde(skip)]
305    pub vision: Option<VisionConfig>,
306
307    /// 2026-09-26: Set by `finalize_config` from `quantization_config`. When that leaves it
308    /// `None`, serve fills it from a sibling `hf_quant_config.json`
309    /// (`merge_sidecar_quant_config`).
310    #[serde(skip)]
311    pub quantization_config: Option<QuantizationConfig>,
312
313    /// 2026-09-26: Whether the Q projection carries an output gate. The parsers set it: false
314    /// for `qwen3_vl_moe`, Nemotron-H, and the GGUF llama/qwen2 mapping.
315    #[serde(skip)]
316    pub attn_gated: bool,
317    /// 2026-09-26: The GDN gated norm uses a sigmoid gate instead of SiLU; the qwen4_exp
318    /// parser sets it for `output_gate_type: "sigmoid"`.
319    #[serde(default)]
320    pub gdn_norm_sigmoid: bool,
321    /// 2026-09-26: The text model's fields sit under a nested key such as `text_config`.
322    /// Weight-prefix detection reads it when `weight_prefix` is empty.
323    #[serde(skip)]
324    pub nested_config: bool,
325    /// 2026-09-26: MRoPE section sizes; `[0, 0, 0]` when the config declares none.
326    #[serde(skip)]
327    pub mrope_section: [usize; 3],
328    #[serde(skip)]
329    pub mrope_interleaved: bool,
330
331    #[serde(skip)]
332    pub weight_prefix: String,
333
334    /// 2026-09-26: Set from serve's `--profile`.
335    #[serde(skip)]
336    pub profile: bool,
337
338    #[serde(skip)]
339    pub ep_rank: usize,
340    #[serde(skip)]
341    pub ep_world_size: usize,
342
343    #[serde(skip)]
344    pub tp_rank: usize,
345    #[serde(skip)]
346    pub tp_world_size: usize,
347
348    /// 2026-09-26: The serve's `--max-seq-len`, set at startup. It is 0 when nothing set it (a
349    /// unit test, an offline tool), which readers must treat as unknown, not as zero context.
350    #[serde(skip)]
351    pub serve_max_seq_len: usize,
352
353    #[serde(skip)]
354    pub fp8_kv_calibration_tokens: usize,
355    #[serde(skip)]
356    pub fp8_kv_headroom: f32,
357
358    #[serde(skip)]
359    pub final_logit_softcapping: f32,
360    #[serde(skip)]
361    pub embed_scale: f32,
362
363    #[serde(default)]
364    pub scoring_func: String,
365    #[serde(default)]
366    pub use_routing_bias: bool,
367    #[serde(default)]
368    pub qk_norm_type: String,
369    #[serde(default)]
370    pub num_mtp_modules: usize,
371    #[serde(default)]
372    pub mtp_transformer_layers: usize,
373    /// 2026-09-26: When non-zero, `rotary_dim()` returns it instead of
374    /// `partial_rotary_factor * head_dim`.
375    #[serde(default)]
376    pub rotary_dim: usize,
377    /// 2026-09-29: Full attention's positional encoding. Never read from JSON: a family parser
378    /// whose attention has none declares it, and `resolve_attn_position_encoding` settles
379    /// every other config to RoPE. Read it through [`ModelConfig::attn_position_encoding`].
380    #[serde(skip)]
381    pub attn_position_encoding: Option<AttnPositionEncoding>,
382
383    #[serde(default)]
384    pub dflash_capture_layers: Vec<usize>,
385    /// 2026-09-26: The DFlash drafter γ, set by the model factory with `dflash_capture_layers`;
386    /// `None` when DFlash is inactive.
387    pub dflash_gamma: Option<usize>,
388
389    /// 2026-09-26: `--max-lora-rank`, set by the model factory. `BufferSizes` sizes the LoRA
390    /// scratch from it and allocates none when it is 0.
391    #[serde(default)]
392    pub adapter_max_rank: usize,
393
394    /// 2026-09-26: `attn_res_block_size`; the kimi_k3 parser requires it.
395    #[serde(default)]
396    pub attn_res_block_size: usize,
397    /// 2026-09-26: `linear_attn_config.use_full_rank_gate`.
398    #[serde(default)]
399    pub use_full_rank_gate: bool,
400    #[serde(default)]
401    pub mla_use_nope: bool,
402    #[serde(default)]
403    pub mla_use_output_gate: bool,
404    #[serde(default)]
405    pub latent_moe_use_norm: bool,
406    /// 2026-09-26: HF `hidden_act`; empty when absent.
407    #[serde(default)]
408    pub hidden_act: String,
409    #[serde(default)]
410    pub activation_situ_beta: f32,
411    #[serde(default)]
412    pub activation_situ_linear_beta: f32,
413    /// 2026-09-26: `num_shared_experts`, else `n_shared_experts` (kimi_k3 parser).
414    #[serde(default)]
415    pub n_shared_experts: usize,
416}