1use anyhow::Result;
10use serde::Deserialize;
11
12fn nullable_u32<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result<u32, D::Error> {
14 Option::<u32>::deserialize(d).map(|v| v.unwrap_or(0))
15}
16
17fn eos_token_id_field<'de, D: serde::Deserializer<'de>>(
23 d: D,
24) -> std::result::Result<u32, D::Error> {
25 #[derive(Deserialize)]
26 #[serde(untagged)]
27 enum OneOrMany {
28 One(u32),
29 Many(Vec<u32>),
30 }
31 Ok(match Option::<OneOrMany>::deserialize(d)? {
32 None => 0,
33 Some(OneOrMany::One(v)) => v,
34 Some(OneOrMany::Many(v)) => v.first().copied().unwrap_or(0),
35 })
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum Glm5NextRouterMode {
43 #[default]
45 HfFp32,
46 VllmBf16,
48}
49
50impl Glm5NextRouterMode {
51 pub fn from_config_str(s: &str) -> Option<Self> {
54 match s {
55 "float32" | "fp32" => Some(Self::HfFp32),
56 "bfloat16" | "bf16" => Some(Self::VllmBf16),
57 _ => None,
58 }
59 }
60
61 pub fn is_fp32(self) -> bool {
62 matches!(self, Self::HfFp32)
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum LayerType {
69 FullAttention,
70 SlidingAttention,
71 LinearAttention,
72 Moe,
75 SparseAttention,
78}
79
80impl LayerType {
81 pub fn is_attention(self) -> bool {
82 matches!(
83 self,
84 Self::FullAttention | Self::SlidingAttention | Self::SparseAttention
85 )
86 }
87
88 pub fn hf_name(self) -> &'static str {
89 match self {
90 Self::FullAttention => "full_attention",
91 Self::SlidingAttention => "sliding_attention",
92 Self::LinearAttention => "linear_attention",
93 Self::Moe => "moe",
94 Self::SparseAttention => "deepseek_sparse_attention",
95 }
96 }
97}
98
99#[derive(Debug, Clone)]
104pub struct QuantizationConfig {
105 pub quant_method: String,
108 pub quant_algo: String,
111 pub format: String,
113 pub ignore_modules: Vec<String>,
116 pub precision: precision_plan::DeclaredPrecisionPlan,
119}
120
121pub fn glm_vision_enabled() -> bool {
127 glm_vision_enabled_from(std::env::var("METRALE_GLM_VISION").ok().as_deref())
128}
129
130pub fn glm_vision_enabled_from(value: Option<&str>) -> bool {
134 matches!(
135 value.map(|v| v.trim().to_ascii_lowercase()).as_deref(),
136 Some("1" | "true" | "yes" | "on")
137 )
138}
139
140#[cfg(test)]
141mod glm_vision_gate_tests {
142 use super::glm_vision_enabled_from;
143
144 #[test]
145 fn only_an_explicit_affirmative_enables_the_tower() {
146 for off in [
147 None,
148 Some(""),
149 Some("0"),
150 Some("off"),
151 Some("false"),
152 Some("no"),
153 Some("yes please"),
154 ] {
155 assert!(!glm_vision_enabled_from(off), "{off:?} must not enable");
156 }
157 for on in ["1", "true", "TRUE", "Yes", " on "] {
158 assert!(glm_vision_enabled_from(Some(on)), "{on:?} must enable");
159 }
160 }
161}
162
163#[derive(Debug, Clone)]
166pub struct VisionConfig {
167 pub depth: usize,
168 pub hidden_size: usize,
169 pub num_heads: usize,
170 pub patch_size: usize,
171 pub temporal_patch_size: usize,
172 pub spatial_merge_size: usize,
173 pub intermediate_size: usize,
174 pub out_hidden_size: usize,
175 pub deepstack_visual_indexes: Vec<usize>,
176 pub image_pad_token_id: u32,
180 pub video_pad_token_id: u32,
183 pub max_pixels: Option<usize>,
187
188 pub hidden_act: String,
189 pub rms_norm_eps: f32,
190 pub attention_bias: bool,
191 pub projection_intermediate_size: usize,
192 pub swiglu_limit: f32,
193 pub rope_theta: f32,
194 pub image_mean: [f32; 3],
195 pub image_std: [f32; 3],
196 pub min_image_tokens: usize,
197 pub max_image_tokens: usize,
198 pub block_major_patches: bool,
201}
202
203impl Default for VisionConfig {
204 fn default() -> Self {
205 Self {
206 depth: 0,
207 hidden_size: 0,
208 num_heads: 0,
209 patch_size: 0,
210 temporal_patch_size: 0,
211 spatial_merge_size: 0,
212 intermediate_size: 0,
213 out_hidden_size: 0,
214 deepstack_visual_indexes: Vec::new(),
215 image_pad_token_id: 0,
216 video_pad_token_id: 0,
217 max_pixels: None,
218 hidden_act: "gelu_pytorch_tanh".to_string(),
219 rms_norm_eps: 1e-6,
220 attention_bias: true,
221 projection_intermediate_size: 0,
222 swiglu_limit: 0.0,
223 rope_theta: 10_000.0,
224 image_mean: [0.5, 0.5, 0.5],
225 image_std: [0.5, 0.5, 0.5],
226 min_image_tokens: 0,
227 max_image_tokens: 0,
228 block_major_patches: false,
229 }
230 }
231}
232
233impl VisionConfig {
234 pub fn merger_input_size(&self) -> usize {
235 self.spatial_merge_size * self.spatial_merge_size * self.hidden_size
236 }
237}
238
239pub(crate) fn default_one() -> usize {
240 1
241}
242pub(crate) fn default_one_f64() -> f64 {
243 1.0
244}
245pub(crate) fn default_one_f32() -> f32 {
246 1.0
247}
248pub(crate) fn default_rope_theta() -> f64 {
249 10000.0
250}
251pub(crate) fn default_rms_eps() -> f64 {
252 1e-6
253}
254pub(crate) fn default_partial_rotary() -> f64 {
255 1.0
256}
257pub(crate) fn default_conv_kernel() -> usize {
258 4
259}
260
261pub mod activation_quantization;
262mod dispatch;
263mod factory;
264mod gguf;
265mod kv_completeness;
266#[cfg(test)]
267mod kv_completeness_tests;
268pub mod levers;
269mod methods;
270mod model_config;
271mod parsers;
272mod position_encoding;
273pub mod precision_plan;
274pub mod recipe_yaml;
275#[cfg(test)]
276mod tests;
277pub mod weight_quantization;
278
279pub use activation_quantization::{ActQuantFormat, ActivationQuantization, ProjFamily};
280pub use dispatch::parse_config;
281pub use gguf::{GgufConfigInputs, GgufMeta, config_from_gguf};
282pub use model_config::ModelConfig;
283pub use parsers::{
284 PEFT_SUPPORTED_TARGET_MODULES, PeftAdapterConfig, allow_partial_targets,
285 glm5_next_mtp_layer_index, parse_mistral_params, parse_peft_adapter_config,
286 parse_quantization_config,
287};
288pub(crate) use parsers::{
289 parse_deepseek_v4, parse_gemma4_params, parse_glm5_next, parse_kimi_k3, parse_laguna,
290 parse_longcat_ngram, parse_minimax_m2, parse_qwen4_exp, parse_step3p7, parse_vision_config,
291 sanitize_kimi_k3_eos,
292};
293pub use position_encoding::AttnPositionEncoding;
294pub(crate) use position_encoding::resolve_attn_position_encoding;
295pub use precision_plan::{DeclaredPrecisionPlan, LayerPrecision};
296pub use weight_quantization::{
297 Nvfp4Act, W4a4Downcast, WeightQuantPolicy, WeightQuantTier, WeightQuantization,
298};
299
300pub(crate) fn finalize_config(config: &mut ModelConfig, raw: &serde_json::Value) -> Result<()> {
301 if config.quantization_config.is_none() {
302 config.quantization_config = parse_quantization_config(raw)?;
303 }
304 validate_config(config)
305}
306
307pub(crate) fn validate_config(config: &ModelConfig) -> Result<()> {
310 if !config.layer_types.is_empty() && config.layer_types.len() != config.num_hidden_layers {
311 anyhow::bail!(
312 "layer_types length ({}) doesn't match num_hidden_layers ({}) in config.json",
313 config.layer_types.len(),
314 config.num_hidden_layers,
315 );
316 }
317
318 if !config.num_attention_heads_per_layer.is_empty()
319 && config.num_attention_heads_per_layer.len() != config.num_hidden_layers
320 {
321 anyhow::bail!(
322 "num_attention_heads_per_layer length ({}) doesn't match num_hidden_layers ({}) in config.json",
323 config.num_attention_heads_per_layer.len(),
324 config.num_hidden_layers,
325 );
326 }
327
328 let has_ssm =
329 config.layer_types.contains(&LayerType::LinearAttention) || config.linear_num_key_heads > 0;
330 if has_ssm && config.linear_num_key_heads == 0 && config.mamba_num_heads == 0 {
331 anyhow::bail!(
332 "SSM model detected but linear_num_key_heads is 0 in config.json. \
333 This field is required for SSM/GDN layer initialization."
334 );
335 }
336
337 if config.mamba_num_heads > 0 {
338 if config.mamba_head_dim == 0 {
339 anyhow::bail!("mamba_head_dim must be greater than zero");
340 }
341 if config.ssm_state_size == 0 {
342 anyhow::bail!("ssm_state_size must be greater than zero");
343 }
344 if config.n_groups == 0 {
345 anyhow::bail!("n_groups must be greater than zero");
346 }
347 if !config.mamba2_d_inner().is_multiple_of(config.n_groups) {
348 anyhow::bail!("mamba_num_heads * mamba_head_dim must be divisible by n_groups");
349 }
350 }
351
352 Ok(())
353}
354
355pub mod capabilities;