metrale_config/parsers/
glm5_next.rs1use anyhow::{Context, Result, bail};
20
21use super::super::{Glm5NextRouterMode, LayerType, ModelConfig, finalize_config};
22mod parse;
23pub use parse::parse_glm5_next;
24
25pub fn glm5_next_mtp_layer_index(config: &ModelConfig) -> usize {
28 config.num_hidden_layers
29}
30
31fn text_config(raw: &serde_json::Value) -> &serde_json::Value {
32 raw.get("text_config").unwrap_or(raw)
33}
34
35pub const GLM5NEXT_SPARSE_ATTN: &str = "deepseek_sparse_attention";
38
39fn parse_glm5_next_vision(raw: &serde_json::Value) -> Option<super::super::VisionConfig> {
48 let vc = raw.get("vision_config")?;
49 let u = |k: &str| {
50 vc.get(k)
51 .and_then(serde_json::Value::as_u64)
52 .map(|v| v as usize)
53 };
54 let f = |k: &str| {
55 vc.get(k)
56 .and_then(serde_json::Value::as_f64)
57 .map(|v| v as f32)
58 };
59 let rope_theta = vc
60 .get("rope_parameters")
61 .and_then(|r| r.get("rope_theta"))
62 .and_then(serde_json::Value::as_f64)
63 .map(|v| v as f32)
64 .unwrap_or(10_000.0);
65 let stats = |k: &str, fallback: [f32; 3]| -> [f32; 3] {
66 let Some(arr) = vc.get(k).and_then(serde_json::Value::as_array) else {
67 return fallback;
68 };
69 let v: Vec<f32> = arr
70 .iter()
71 .filter_map(serde_json::Value::as_f64)
72 .map(|x| x as f32)
73 .collect();
74 match v.len() {
75 3 => [v[0], v[1], v[2]],
76 _ => fallback,
77 }
78 };
79 Some(super::super::VisionConfig {
80 depth: u("depth")?,
81 hidden_size: u("hidden_size")?,
82 num_heads: u("num_heads")?,
83 patch_size: u("patch_size")?,
84 temporal_patch_size: u("temporal_patch_size")?,
85 spatial_merge_size: u("spatial_merge_size")?,
86 intermediate_size: u("intermediate_size")?,
87 out_hidden_size: u("out_hidden_size")?,
88 deepstack_visual_indexes: Vec::new(),
89 image_pad_token_id: raw
90 .get("image_token_id")
91 .and_then(serde_json::Value::as_u64)
92 .unwrap_or(0) as u32,
93 video_pad_token_id: raw
94 .get("video_token_id")
95 .and_then(serde_json::Value::as_u64)
96 .unwrap_or(0) as u32,
97 max_pixels: None,
100 hidden_act: vc
101 .get("hidden_act")
102 .and_then(serde_json::Value::as_str)
103 .unwrap_or("silu")
104 .to_string(),
105 rms_norm_eps: f("rms_norm_eps").unwrap_or(1e-5),
106 attention_bias: vc
107 .get("attention_bias")
108 .and_then(serde_json::Value::as_bool)
109 .unwrap_or(true),
110 projection_intermediate_size: u("projection_intermediate_size")?,
111 swiglu_limit: f("swiglu_limit").unwrap_or(10.0),
112 rope_theta,
113 image_mean: stats("image_mean", [0.481_454_67, 0.457_827_5, 0.408_210_72]),
117 image_std: stats("image_std", [0.268_629_55, 0.261_302_6, 0.275_777_1]),
118 min_image_tokens: u("min_image_tokens").unwrap_or(16),
119 max_image_tokens: u("max_image_tokens").unwrap_or(8000),
120 block_major_patches: true,
121 })
122}
123
124fn build_mlp_only_layers(text: &serde_json::Value, n_layers: usize) -> Result<Vec<usize>> {
130 let first_k = text
131 .get("first_k_dense_replace")
132 .and_then(|v| v.as_u64())
133 .map(|v| v as usize);
134 let textual: Option<Vec<usize>> =
135 text.get("mlp_layer_types")
136 .and_then(|v| v.as_array())
137 .map(|a| {
138 a.iter()
139 .enumerate()
140 .filter(|(_, v)| v.as_str() != Some("sparse"))
141 .map(|(i, _)| i)
142 .collect()
143 });
144
145 match (first_k, textual) {
146 (Some(k), t) => {
147 if k > n_layers {
148 bail!("glm5_next: first_k_dense_replace {k} exceeds num_hidden_layers {n_layers}");
149 }
150 let derived: Vec<usize> = (0..k).collect();
151 if let Some(t) = t
152 && t != derived
153 {
154 bail!(
155 "glm5_next: first_k_dense_replace={k} implies dense layers \
156 {derived:?}, but mlp_layer_types says {t:?}"
157 );
158 }
159 Ok(derived)
160 }
161 (None, Some(t)) => Ok(t),
162 (None, None) => bail!(
163 "glm5_next: neither first_k_dense_replace nor mlp_layer_types present; \
164 refusing to guess which layers are dense"
165 ),
166 }
167}
168
169fn build_layer_types(text: &serde_json::Value, n_layers: usize) -> Result<Vec<LayerType>> {
175 let idx_list = |key: &str| -> Option<Vec<usize>> {
176 text.get("linear_attn_config")?
177 .get(key)?
178 .as_array()
179 .map(|a| {
180 a.iter()
181 .filter_map(|v| v.as_u64())
182 .map(|v| v as usize)
183 .collect()
184 })
185 };
186
187 let kda = idx_list("kda_layers");
188 let full = idx_list("full_attn_layers");
189
190 let mut types = vec![LayerType::FullAttention; n_layers];
191 match (kda, full) {
192 (Some(kda), Some(full)) => {
193 if kda.len() + full.len() != n_layers {
194 bail!(
195 "glm5_next: kda_layers ({}) + full_attn_layers ({}) != num_hidden_layers ({})",
196 kda.len(),
197 full.len(),
198 n_layers
199 );
200 }
201 for i in &kda {
202 if *i >= n_layers {
203 bail!("glm5_next: kda_layers index {i} out of range for {n_layers} layers");
204 }
205 types[*i] = LayerType::LinearAttention;
206 }
207 let textual = text.get("layer_types").and_then(|v| v.as_array());
210 for i in &full {
211 if *i >= n_layers {
212 bail!("glm5_next: full_attn_layers index {i} out of range");
213 }
214 if types[*i] == LayerType::LinearAttention {
215 bail!("glm5_next: layer {i} listed as BOTH kda and full attention");
216 }
217 types[*i] = match textual.and_then(|a| a.get(*i)).and_then(|v| v.as_str()) {
218 Some(GLM5NEXT_SPARSE_ATTN) => LayerType::SparseAttention,
219 _ => LayerType::FullAttention,
220 };
221 }
222 }
223 _ => {
224 let arr = text
225 .get("layer_types")
226 .and_then(|v| v.as_array())
227 .context("glm5_next: neither linear_attn_config lists nor layer_types present")?;
228 if arr.len() != n_layers {
229 bail!(
230 "glm5_next: layer_types has {} entries, expected {n_layers}",
231 arr.len()
232 );
233 }
234 for (i, v) in arr.iter().enumerate() {
235 types[i] = match v.as_str().unwrap_or("") {
236 "linear_attention" => LayerType::LinearAttention,
237 GLM5NEXT_SPARSE_ATTN => LayerType::SparseAttention,
238 "full_attention" => LayerType::FullAttention,
239 other => bail!("glm5_next: unknown layer_type {other:?} at layer {i}"),
240 };
241 }
242 }
243 }
244
245 if let Some(arr) = text.get("layer_types").and_then(|v| v.as_array())
246 && arr.len() == n_layers
247 {
248 for (i, v) in arr.iter().enumerate() {
249 let want = match v.as_str().unwrap_or("") {
250 "linear_attention" => LayerType::LinearAttention,
251 GLM5NEXT_SPARSE_ATTN => LayerType::SparseAttention,
252 _ => LayerType::FullAttention,
253 };
254 if types[i] != want {
255 bail!(
256 "glm5_next: layer {i} disagrees — index lists say {:?}, layer_types says {want:?}",
257 types[i]
258 );
259 }
260 }
261 }
262 Ok(types)
263}
264
265fn refuse_shared_indexer(text: &serde_json::Value, config: &ModelConfig) -> Result<()> {
278 let n = config.num_hidden_layers;
279 let modes: Vec<String> = if let Some(arr) = text.get("indexer_types").and_then(|v| v.as_array())
280 {
281 if arr.len() != n {
282 bail!(
283 "indexer_types has {} entries for {n} layers; a length mismatch would \
284 silently misalign every layer's indexer mode",
285 arr.len()
286 );
287 }
288 arr.iter()
289 .map(|v| {
290 v.as_str()
291 .map(str::to_string)
292 .context("indexer_types entry is not a string")
293 })
294 .collect::<Result<_>>()?
295 } else if let Some(pat) = text.get("index_topk_pattern").and_then(|v| v.as_str()) {
296 if pat.chars().count() != n {
297 bail!(
298 "index_topk_pattern has {} chars for {n} layers",
299 pat.chars().count()
300 );
301 }
302 pat.chars()
303 .map(|c| match c {
304 'F' => Ok("full".to_string()),
305 'S' => Ok("shared".to_string()),
306 other => bail!("index_topk_pattern: unknown char {other:?}, expected F or S"),
307 })
308 .collect::<Result<_>>()?
309 } else {
310 let freq = text
311 .get("index_topk_freq")
312 .and_then(|v| v.as_u64())
313 .unwrap_or(1)
314 .max(1) as i64;
315 let offset = text
316 .get("index_skip_topk_offset")
317 .and_then(|v| v.as_i64())
318 .unwrap_or(2);
319 (0..n)
320 .map(|i| {
321 let shifted = ((i as i64) - offset + 1).max(0);
322 if shifted % freq == 0 {
323 "full"
324 } else {
325 "shared"
326 }
327 .to_string()
328 })
329 .collect()
330 };
331
332 for (i, m) in modes.iter().enumerate() {
333 if m != "full" && m != "shared" {
334 bail!("layer {i}: unknown indexer mode {m:?}, expected \"full\" or \"shared\"");
335 }
336 }
337
338 let shared: Vec<usize> = config
339 .layer_types
340 .iter()
341 .enumerate()
342 .filter(|(i, t)| {
343 **t != LayerType::LinearAttention && modes.get(*i).is_some_and(|m| m == "shared")
344 })
345 .map(|(i, _)| i)
346 .collect();
347 if !shared.is_empty() {
348 bail!(
349 "DSA layer(s) {shared:?} use SHARED indexing (reuse the previous full layer's \
350 top-k). Metrale Engine runs a per-layer indexer and does not propagate selections, so \
351 these layers would attend to the wrong token set — a wrong answer, not a \
352 crash. GLM-5.3-Flash-NVFP4 is entirely \"full\"; implement prev_topk_indices \
353 propagation before serving a checkpoint that is not."
354 );
355 }
356 Ok(())
357}
358
359fn validate_glm5_next(config: &ModelConfig) -> Result<()> {
360 if config.qk_rope_head_dim != 0 {
361 bail!(
362 "glm5_next: expected NoPE (qk_rope_head_dim == 0), got {}. \
363 A non-zero value means this is not the GLM-5.3 geometry we support.",
364 config.qk_rope_head_dim
365 );
366 }
367 if config.head_dim == 0 {
368 bail!("glm5_next: head_dim resolved to 0");
369 }
370 let linear = config
371 .layer_types
372 .iter()
373 .filter(|t| **t == LayerType::LinearAttention)
374 .count();
375 let full = config.layer_types.len() - linear;
376 if linear == 0 || full == 0 {
377 bail!("glm5_next: degenerate layer map — {linear} linear / {full} full");
378 }
379 Ok(())
380}
381
382#[cfg(test)]
383mod tests;