1use anyhow::{Context, Result, bail};
27
28use super::super::{Glm5NextRouterMode, LayerType, ModelConfig, finalize_config};
29
30pub fn glm5_next_mtp_layer_index(config: &ModelConfig) -> usize {
37 config.num_hidden_layers
38}
39
40fn text_config(raw: &serde_json::Value) -> &serde_json::Value {
41 raw.get("text_config").unwrap_or(raw)
42}
43
44pub const GLM5NEXT_SPARSE_ATTN: &str = "deepseek_sparse_attention";
53
54pub fn parse_glm5_next(json: &str) -> Result<ModelConfig> {
55 let raw: serde_json::Value =
56 serde_json::from_str(json).context("Invalid JSON in GLM-5.3 (glm5_next) config.json")?;
57
58 let text = text_config(&raw).clone();
62
63 let mut text_for_struct = text.clone();
70 if let Some(obj) = text_for_struct.as_object_mut() {
71 obj.remove("layer_types");
72 }
81 let text_json =
82 serde_json::to_string(&text_for_struct).context("re-serialize glm5_next text_config")?;
83 let mut config: ModelConfig =
84 serde_json::from_str(&text_json).context("Failed to parse glm5_next text_config")?;
85 let text = &text;
86
87 config.model_type = "glm5_next".to_string();
90
91 let qk_rope = text
95 .get("qk_rope_head_dim")
96 .and_then(|v| v.as_u64())
97 .map(|v| v as usize);
98 match qk_rope {
99 Some(v) => config.qk_rope_head_dim = v,
100 None => bail!(
103 "glm5_next config.json has no qk_rope_head_dim; refusing to guess \
104 whether this checkpoint is NoPE"
105 ),
106 }
107 if let Some(v) = text.get("qk_nope_head_dim").and_then(|v| v.as_u64()) {
108 config.qk_nope_head_dim = v as usize;
109 }
110 if let Some(v) = text.get("v_head_dim").and_then(|v| v.as_u64()) {
111 config.v_head_dim = v as usize;
112 }
113 if config.head_dim == 0 {
118 config.head_dim = text
119 .get("qk_head_dim")
120 .and_then(|v| v.as_u64())
121 .map(|v| v as usize)
122 .unwrap_or(config.qk_nope_head_dim + config.qk_rope_head_dim);
123 }
124 config.partial_rotary_factor = if config.head_dim > 0 {
126 config.qk_rope_head_dim as f64 / config.head_dim as f64
127 } else {
128 0.0
129 };
130
131 if config.num_experts == 0 && config.n_routed_experts > 0 {
133 config.num_experts = config.n_routed_experts;
134 }
135 let n_shared = text
136 .get("n_shared_experts")
137 .and_then(|v| v.as_u64())
138 .unwrap_or(0) as usize;
139 if config.shared_expert_intermediate_size == 0 && n_shared > 0 {
140 config.shared_expert_intermediate_size = n_shared * config.moe_intermediate_size;
141 }
142
143 let lac = text.get("linear_attn_config");
147 if let Some(lac) = lac {
148 let g = |k: &str| lac.get(k).and_then(|v| v.as_u64()).map(|v| v as usize);
149 if let Some(v) = g("num_heads") {
150 config.linear_num_key_heads = v;
151 config.linear_num_value_heads = v;
152 }
153 if let Some(v) = g("head_dim") {
154 config.linear_key_head_dim = v;
155 config.linear_value_head_dim = v;
156 }
157 if let Some(v) = g("short_conv_kernel_size") {
158 config.linear_conv_kernel_dim = v;
159 }
160 match lac.get("gate_lower_bound").and_then(|v| v.as_f64()) {
163 Some(v) => config.linear_gate_lower_bound = v as f32,
164 None => bail!(
165 "glm5_next: linear_attn_config has no gate_lower_bound; refusing to guess the \
166 KDA decay bound (GLM-5.3-Flash declares -5.0)"
167 ),
168 }
169 }
170
171 if config.index_topk == 0
175 && let Some(v) = text.get("index_topk").and_then(|v| v.as_u64())
176 {
177 config.index_topk = v as usize;
178 }
179 if let Some(v) = text.get("index_kpool").and_then(|v| v.as_u64()) {
183 config.index_kpool = v as usize;
184 }
185 if let Some(v) = text
186 .get("index_kpool_always_select_tail")
187 .and_then(|v| v.as_bool())
188 {
189 config.index_kpool_always_select_tail = v;
190 }
191
192 config.layer_types = build_layer_types(text, config.num_hidden_layers)?;
194
195 let n_mtp = text
206 .get("num_nextn_predict_layers")
207 .and_then(|v| v.as_u64())
208 .unwrap_or(0) as usize;
209 if n_mtp > 0 {
210 let kind = if config.layer_types.contains(&LayerType::SparseAttention) {
211 LayerType::SparseAttention
212 } else {
213 LayerType::FullAttention
214 };
215 config.mtp_layer_types = vec![kind; n_mtp];
216 }
217
218 config.mlp_only_layers = build_mlp_only_layers(text, config.num_hidden_layers)?;
226
227 config.swiglu_limit = match text.get("swiglu_limit").and_then(|v| v.as_f64()) {
233 Some(v) if v > 0.0 => v as f32,
234 Some(v) => bail!("glm5_next: swiglu_limit is {v}, which cannot bound anything"),
235 None => bail!(
236 "glm5_next config.json has no swiglu_limit; refusing to guess whether this \
237 checkpoint clamps its SwiGLU (GLM-5.3-Flash declares 10.0)"
238 ),
239 };
240
241 for key in ["n_group", "topk_group"] {
247 if let Some(v) = text.get(key).and_then(|v| v.as_u64())
248 && v != 1
249 {
250 bail!(
251 "glm5_next: {key} = {v}. Grouped expert routing is not implemented — \
252 glm5next_router_topk ranks every expert in one group."
253 );
254 }
255 }
256
257 config.glm5next_router_mode = match text.get("moe_router_dtype").and_then(|v| v.as_str()) {
262 None => Glm5NextRouterMode::HfFp32,
263 Some(s) => Glm5NextRouterMode::from_config_str(s).ok_or_else(|| {
264 anyhow::anyhow!(
265 "glm5_next: unknown moe_router_dtype {s:?}; expected float32 or bfloat16"
266 )
267 })?,
268 };
269
270 config.vision = match super::super::glm_vision_enabled() {
280 true => parse_glm5_next_vision(&raw),
281 false => None,
282 };
283
284 finalize_config(&mut config, &raw).context("glm5_next: finalize_config")?;
285 refuse_shared_indexer(text, &config).context("glm5_next: indexer_types")?;
286 validate_glm5_next(&config)?;
287 Ok(config)
288}
289
290fn parse_glm5_next_vision(raw: &serde_json::Value) -> Option<super::super::VisionConfig> {
302 let vc = raw.get("vision_config")?;
303 let u = |k: &str| {
304 vc.get(k)
305 .and_then(serde_json::Value::as_u64)
306 .map(|v| v as usize)
307 };
308 let f = |k: &str| {
309 vc.get(k)
310 .and_then(serde_json::Value::as_f64)
311 .map(|v| v as f32)
312 };
313 let rope_theta = vc
317 .get("rope_parameters")
318 .and_then(|r| r.get("rope_theta"))
319 .and_then(serde_json::Value::as_f64)
320 .map(|v| v as f32)
321 .unwrap_or(10_000.0);
322 let stats = |k: &str, fallback: [f32; 3]| -> [f32; 3] {
323 let Some(arr) = vc.get(k).and_then(serde_json::Value::as_array) else {
324 return fallback;
325 };
326 let v: Vec<f32> = arr
327 .iter()
328 .filter_map(serde_json::Value::as_f64)
329 .map(|x| x as f32)
330 .collect();
331 match v.len() {
332 3 => [v[0], v[1], v[2]],
333 _ => fallback,
334 }
335 };
336 Some(super::super::VisionConfig {
337 depth: u("depth")?,
338 hidden_size: u("hidden_size")?,
339 num_heads: u("num_heads")?,
340 patch_size: u("patch_size")?,
341 temporal_patch_size: u("temporal_patch_size")?,
342 spatial_merge_size: u("spatial_merge_size")?,
343 intermediate_size: u("intermediate_size")?,
344 out_hidden_size: u("out_hidden_size")?,
345 deepstack_visual_indexes: Vec::new(),
348 image_pad_token_id: raw
349 .get("image_token_id")
350 .and_then(serde_json::Value::as_u64)
351 .unwrap_or(0) as u32,
352 video_pad_token_id: raw
353 .get("video_token_id")
354 .and_then(serde_json::Value::as_u64)
355 .unwrap_or(0) as u32,
356 max_pixels: None,
358 hidden_act: vc
359 .get("hidden_act")
360 .and_then(serde_json::Value::as_str)
361 .unwrap_or("silu")
362 .to_string(),
363 rms_norm_eps: f("rms_norm_eps").unwrap_or(1e-5),
364 attention_bias: vc
365 .get("attention_bias")
366 .and_then(serde_json::Value::as_bool)
367 .unwrap_or(true),
368 projection_intermediate_size: u("projection_intermediate_size")?,
369 swiglu_limit: f("swiglu_limit").unwrap_or(10.0),
370 rope_theta,
371 image_mean: stats("image_mean", [0.481_454_67, 0.457_827_5, 0.408_210_72]),
389 image_std: stats("image_std", [0.268_629_55, 0.261_302_6, 0.275_777_1]),
390 min_image_tokens: u("min_image_tokens").unwrap_or(16),
391 max_image_tokens: u("max_image_tokens").unwrap_or(8000),
392 block_major_patches: true,
393 })
394}
395
396fn build_mlp_only_layers(text: &serde_json::Value, n_layers: usize) -> Result<Vec<usize>> {
402 let first_k = text
403 .get("first_k_dense_replace")
404 .and_then(|v| v.as_u64())
405 .map(|v| v as usize);
406 let textual: Option<Vec<usize>> =
407 text.get("mlp_layer_types")
408 .and_then(|v| v.as_array())
409 .map(|a| {
410 a.iter()
411 .enumerate()
412 .filter(|(_, v)| v.as_str() != Some("sparse"))
413 .map(|(i, _)| i)
414 .collect()
415 });
416
417 match (first_k, textual) {
418 (Some(k), t) => {
419 if k > n_layers {
420 bail!("glm5_next: first_k_dense_replace {k} exceeds num_hidden_layers {n_layers}");
421 }
422 let derived: Vec<usize> = (0..k).collect();
423 if let Some(t) = t
424 && t != derived
425 {
426 bail!(
427 "glm5_next: first_k_dense_replace={k} implies dense layers \
428 {derived:?}, but mlp_layer_types says {t:?}"
429 );
430 }
431 Ok(derived)
432 }
433 (None, Some(t)) => Ok(t),
436 (None, None) => bail!(
437 "glm5_next: neither first_k_dense_replace nor mlp_layer_types present; \
438 refusing to guess which layers are dense"
439 ),
440 }
441}
442
443fn build_layer_types(text: &serde_json::Value, n_layers: usize) -> Result<Vec<LayerType>> {
450 let idx_list = |key: &str| -> Option<Vec<usize>> {
451 text.get("linear_attn_config")?
452 .get(key)?
453 .as_array()
454 .map(|a| {
455 a.iter()
456 .filter_map(|v| v.as_u64())
457 .map(|v| v as usize)
458 .collect()
459 })
460 };
461
462 let kda = idx_list("kda_layers");
463 let full = idx_list("full_attn_layers");
464
465 let mut types = vec![LayerType::FullAttention; n_layers];
466 match (kda, full) {
467 (Some(kda), Some(full)) => {
468 if kda.len() + full.len() != n_layers {
469 bail!(
470 "glm5_next: kda_layers ({}) + full_attn_layers ({}) != num_hidden_layers ({})",
471 kda.len(),
472 full.len(),
473 n_layers
474 );
475 }
476 for i in &kda {
477 if *i >= n_layers {
478 bail!("glm5_next: kda_layers index {i} out of range for {n_layers} layers");
479 }
480 types[*i] = LayerType::LinearAttention;
481 }
482 let textual = text.get("layer_types").and_then(|v| v.as_array());
486 for i in &full {
487 if *i >= n_layers {
488 bail!("glm5_next: full_attn_layers index {i} out of range");
489 }
490 if types[*i] == LayerType::LinearAttention {
491 bail!("glm5_next: layer {i} listed as BOTH kda and full attention");
492 }
493 types[*i] = match textual.and_then(|a| a.get(*i)).and_then(|v| v.as_str()) {
494 Some(GLM5NEXT_SPARSE_ATTN) => LayerType::SparseAttention,
495 _ => LayerType::FullAttention,
496 };
497 }
498 }
499 _ => {
500 let arr = text
502 .get("layer_types")
503 .and_then(|v| v.as_array())
504 .context("glm5_next: neither linear_attn_config lists nor layer_types present")?;
505 if arr.len() != n_layers {
506 bail!(
507 "glm5_next: layer_types has {} entries, expected {n_layers}",
508 arr.len()
509 );
510 }
511 for (i, v) in arr.iter().enumerate() {
512 types[i] = match v.as_str().unwrap_or("") {
513 "linear_attention" => LayerType::LinearAttention,
514 GLM5NEXT_SPARSE_ATTN => LayerType::SparseAttention,
515 "full_attention" => LayerType::FullAttention,
516 other => bail!("glm5_next: unknown layer_type {other:?} at layer {i}"),
517 };
518 }
519 }
520 }
521
522 if let Some(arr) = text.get("layer_types").and_then(|v| v.as_array())
524 && arr.len() == n_layers
525 {
526 for (i, v) in arr.iter().enumerate() {
527 let want = match v.as_str().unwrap_or("") {
528 "linear_attention" => LayerType::LinearAttention,
529 GLM5NEXT_SPARSE_ATTN => LayerType::SparseAttention,
530 _ => LayerType::FullAttention,
531 };
532 if types[i] != want {
533 bail!(
534 "glm5_next: layer {i} disagrees — index lists say {:?}, layer_types says {want:?}",
535 types[i]
536 );
537 }
538 }
539 }
540 Ok(types)
541}
542
543fn refuse_shared_indexer(text: &serde_json::Value, config: &ModelConfig) -> Result<()> {
565 let n = config.num_hidden_layers;
566 let modes: Vec<String> = if let Some(arr) = text.get("indexer_types").and_then(|v| v.as_array())
567 {
568 if arr.len() != n {
569 bail!(
570 "indexer_types has {} entries for {n} layers; a length mismatch would \
571 silently misalign every layer's indexer mode",
572 arr.len()
573 );
574 }
575 arr.iter()
576 .map(|v| {
577 v.as_str()
578 .map(str::to_string)
579 .context("indexer_types entry is not a string")
580 })
581 .collect::<Result<_>>()?
582 } else if let Some(pat) = text.get("index_topk_pattern").and_then(|v| v.as_str()) {
583 if pat.chars().count() != n {
584 bail!(
585 "index_topk_pattern has {} chars for {n} layers",
586 pat.chars().count()
587 );
588 }
589 pat.chars()
590 .map(|c| match c {
591 'F' => Ok("full".to_string()),
592 'S' => Ok("shared".to_string()),
593 other => bail!("index_topk_pattern: unknown char {other:?}, expected F or S"),
594 })
595 .collect::<Result<_>>()?
596 } else {
597 let freq = text
598 .get("index_topk_freq")
599 .and_then(|v| v.as_u64())
600 .unwrap_or(1)
601 .max(1) as i64;
602 let offset = text
603 .get("index_skip_topk_offset")
604 .and_then(|v| v.as_i64())
605 .unwrap_or(2);
606 (0..n)
607 .map(|i| {
608 let shifted = ((i as i64) - offset + 1).max(0);
609 if shifted % freq == 0 {
610 "full"
611 } else {
612 "shared"
613 }
614 .to_string()
615 })
616 .collect()
617 };
618
619 for (i, m) in modes.iter().enumerate() {
620 if m != "full" && m != "shared" {
621 bail!("layer {i}: unknown indexer mode {m:?}, expected \"full\" or \"shared\"");
622 }
623 }
624
625 let shared: Vec<usize> = config
627 .layer_types
628 .iter()
629 .enumerate()
630 .filter(|(i, t)| {
631 **t != LayerType::LinearAttention && modes.get(*i).is_some_and(|m| m == "shared")
632 })
633 .map(|(i, _)| i)
634 .collect();
635 if !shared.is_empty() {
636 bail!(
637 "DSA layer(s) {shared:?} use SHARED indexing (reuse the previous full layer's \
638 top-k). Atlas runs a per-layer indexer and does not propagate selections, so \
639 these layers would attend to the wrong token set — a wrong answer, not a \
640 crash. GLM-5.3-Flash-NVFP4 is entirely \"full\"; implement prev_topk_indices \
641 propagation before serving a checkpoint that is not."
642 );
643 }
644 Ok(())
645}
646
647fn validate_glm5_next(config: &ModelConfig) -> Result<()> {
648 if config.qk_rope_head_dim != 0 {
649 bail!(
650 "glm5_next: expected NoPE (qk_rope_head_dim == 0), got {}. \
651 A non-zero value means this is not the GLM-5.3 geometry we support.",
652 config.qk_rope_head_dim
653 );
654 }
655 if config.head_dim == 0 {
656 bail!("glm5_next: head_dim resolved to 0");
657 }
658 let linear = config
659 .layer_types
660 .iter()
661 .filter(|t| **t == LayerType::LinearAttention)
662 .count();
663 let full = config.layer_types.len() - linear;
664 if linear == 0 || full == 0 {
665 bail!("glm5_next: degenerate layer map — {linear} linear / {full} full");
666 }
667 Ok(())
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673
674 fn glm53_config_json() -> String {
677 let kda: Vec<String> = (0..45)
680 .filter(|i| i % 4 != 3)
681 .map(|i| i.to_string())
682 .collect();
683 let full: Vec<String> = (0..45)
684 .filter(|i| i % 4 == 3)
685 .map(|i| i.to_string())
686 .collect();
687 let layer_types: Vec<String> = (0..45)
688 .map(|i| {
689 if i % 4 == 3 {
690 "\"deepseek_sparse_attention\"".to_string()
691 } else {
692 "\"linear_attention\"".to_string()
693 }
694 })
695 .collect();
696 format!(
697 r#"{{
698 "architectures": ["Glm5NextForConditionalGeneration"],
699 "model_type": "glm5_next",
700 "text_config": {{
701 "model_type": "glm5_next_text",
702 "num_hidden_layers": 45,
703 "num_nextn_predict_layers": 1,
704 "hidden_size": 4096,
705 "intermediate_size": 12288,
706 "num_attention_heads": 64,
707 "num_key_value_heads": 64,
708 "head_dim": 0,
709 "qk_head_dim": 256,
710 "qk_nope_head_dim": 256,
711 "qk_rope_head_dim": 0,
712 "v_head_dim": 256,
713 "kv_lora_rank": 512,
714 "q_lora_rank": 1536,
715 "mla_use_nope": true,
716 "index_topk": 2048,
717 "index_kpool": 4,
718 "index_n_heads": 32,
719 "index_head_dim": 128,
720 "hc_mult": 4,
721 "hc_sinkhorn_iters": 20,
722 "hc_eps": 1e-06,
723 "mhc": true,
724 "n_routed_experts": 288,
725 "n_shared_experts": 1,
726 "num_experts_per_tok": 8,
727 "moe_intermediate_size": 2048,
728 "first_k_dense_replace": 3,
729 "swiglu_limit": 10.0,
730 "routed_scaling_factor": 2.5,
731 "norm_topk_prob": true,
732 "n_group": 1,
733 "topk_group": 1,
734 "scoring_func": "sigmoid",
735 "topk_method": "noaux_tc",
736 "rms_norm_eps": 1e-05,
737 "vocab_size": 154880,
738 "max_position_embeddings": 1048576,
739 "linear_attn_config": {{
740 "num_heads": 64,
741 "head_dim": 128,
742 "short_conv_kernel_size": 4,
743 "gate_lower_bound": -5.0,
744 "kda_layers": [{kda}],
745 "full_attn_layers": [{full}]
746 }},
747 "layer_types": [{lt}]
748 }}
749}}"#,
750 kda = kda.join(","),
751 full = full.join(","),
752 lt = layer_types.join(",")
753 )
754 }
755
756 fn with_text_key(key: &str, value: serde_json::Value) -> String {
759 let mut raw: serde_json::Value =
760 serde_json::from_str(&glm53_config_json()).expect("fixture json");
761 raw["text_config"][key] = value;
762 raw.to_string()
763 }
764
765 #[test]
767 fn an_all_full_indexer_array_is_accepted() {
768 let all_full = serde_json::Value::from(vec!["full"; 45]);
769 let c = parse_glm5_next(&with_text_key("indexer_types", all_full)).expect("parse");
770 let dsa: Vec<usize> = c
772 .layer_types
773 .iter()
774 .enumerate()
775 .filter(|(_, t)| **t != LayerType::LinearAttention)
776 .map(|(i, _)| i)
777 .collect();
778 assert_eq!(dsa, vec![3, 7, 11, 15, 19, 23, 27, 31, 35, 39, 43]);
779 }
780
781 #[test]
785 fn a_shared_dsa_layer_is_refused() {
786 let mut modes = vec!["full"; 45];
787 modes[7] = "shared"; let e = parse_glm5_next(&with_text_key("indexer_types", modes.into()))
789 .expect_err("shared DSA indexing must be refused");
790 let msg = e.to_string() + &e.root_cause().to_string();
791 assert!(msg.contains('7'), "the error must name the layer: {msg}");
792 }
793
794 #[test]
797 fn a_shared_entry_on_a_linear_layer_is_inert() {
798 let mut modes = vec!["full"; 45];
799 modes[0] = "shared"; assert!(parse_glm5_next(&with_text_key("indexer_types", modes.into())).is_ok());
801 }
802
803 #[test]
805 fn a_wrong_length_indexer_array_is_refused() {
806 let short = serde_json::Value::from(vec!["full"; 44]);
807 assert!(parse_glm5_next(&with_text_key("indexer_types", short)).is_err());
808 }
809
810 #[test]
814 fn an_absent_array_is_derived_not_assumed_full() {
815 assert!(parse_glm5_next(&glm53_config_json()).is_ok());
817 let e = parse_glm5_next(&with_text_key("index_topk_freq", 4.into()))
819 .expect_err("a freq schedule that shares DSA layers must be refused");
820 assert!(e.root_cause().to_string().contains("SHARED"), "{e}");
821 }
822
823 #[test]
825 fn an_index_topk_pattern_is_honoured() {
826 let ok: String = "F".repeat(45);
828 assert!(parse_glm5_next(&with_text_key("index_topk_pattern", ok.into())).is_ok());
829 let mut bad: Vec<char> = "F".repeat(45).chars().collect();
830 bad[43] = 'S'; let bad: String = bad.into_iter().collect();
832 assert!(parse_glm5_next(&with_text_key("index_topk_pattern", bad.into())).is_err());
833 }
834
835 #[test]
836 fn parses_glm5_next() {
837 let c = parse_glm5_next(&glm53_config_json()).expect("parse");
838 assert_eq!(c.model_type, "glm5_next");
839 assert_eq!(c.num_hidden_layers, 45);
840 assert_eq!(c.hidden_size, 4096);
841 assert_eq!(c.n_routed_experts, 288);
842 assert_eq!(c.num_experts_per_tok, 8);
843 assert_eq!(c.moe_intermediate_size, 2048);
844 }
845
846 #[test]
848 fn nope_rope_dim_zero_survives_exactly() {
849 let c = parse_glm5_next(&glm53_config_json()).expect("parse");
850 assert_eq!(c.qk_rope_head_dim, 0, "NoPE zero must not be 'repaired'");
851 assert_eq!(c.qk_nope_head_dim, 256, "nope dim must come from the file");
852 assert_eq!(c.v_head_dim, 256);
853 assert_eq!(c.partial_rotary_factor, 0.0);
854 }
855
856 #[test]
872 fn is_mla_and_nope_simultaneously() {
873 let c = parse_glm5_next(&glm53_config_json()).expect("parse");
874 assert!(
875 c.kv_lora_rank > 0,
876 "GLM-5.3 is MLA: a latent KV cache of {} dims",
877 c.kv_lora_rank
878 );
879 assert_eq!(c.kv_lora_rank, 512);
880 assert_eq!(
881 c.qk_rope_head_dim, 0,
882 "...and simultaneously NoPE. `rope > 0` must never stand in for `is MLA`."
883 );
884 }
885
886 #[test]
889 fn head_dim_resolves_to_mla_width_not_hidden_over_heads() {
890 let c = parse_glm5_next(&glm53_config_json()).expect("parse");
891 assert_eq!(c.head_dim, 256);
892 assert_ne!(c.head_dim, 4096 / 64);
893 }
894
895 #[test]
899 fn layer_census_matches_reconciled_counts() {
900 let c = parse_glm5_next(&glm53_config_json()).expect("parse");
901 let kda = c
902 .layer_types
903 .iter()
904 .filter(|t| **t == LayerType::LinearAttention)
905 .count();
906 let dsa = c
909 .layer_types
910 .iter()
911 .filter(|t| **t == LayerType::SparseAttention)
912 .count();
913 let plain_full = c
914 .layer_types
915 .iter()
916 .filter(|t| **t == LayerType::FullAttention)
917 .count();
918 assert_eq!(c.layer_types.len(), 45);
919 assert_eq!(kda, 34, "KDA layers over text layers 0..44");
920 assert_eq!(dsa, 11, "DSA layers over text layers 0..44");
921 assert_eq!(plain_full, 0, "GLM-5.3 has no plain full-attention layer");
922 assert_eq!(c.layer_types[0], LayerType::LinearAttention);
924 assert_eq!(c.layer_types[3], LayerType::SparseAttention);
925 assert_eq!(c.layer_types[43], LayerType::SparseAttention);
926 assert_eq!(c.layer_types[44], LayerType::LinearAttention);
927 }
928
929 #[test]
933 fn layer_types_round_trip_to_the_checkpoint_strings() {
934 let c = parse_glm5_next(&glm53_config_json()).expect("parse");
935 let raw: serde_json::Value = serde_json::from_str(&glm53_config_json()).unwrap();
936 let want = raw["text_config"]["layer_types"]
937 .as_array()
938 .expect("layer_types");
939 assert_eq!(want.len(), c.layer_types.len());
940 for (i, w) in want.iter().enumerate() {
941 assert_eq!(
942 c.layer_types[i].hf_name(),
943 w.as_str().unwrap(),
944 "layer {i} does not round-trip"
945 );
946 }
947 }
948
949 #[test]
952 fn mtp_layer_is_represented_outside_the_text_stack() {
953 let c = parse_glm5_next(&glm53_config_json()).expect("parse");
954 assert_eq!(c.num_hidden_layers, 45);
955 assert_eq!(c.layer_types.len(), 45, "text stack stays 0..=44");
956 assert_eq!(c.mtp_layer_types, vec![LayerType::SparseAttention]);
957 assert_eq!(c.layer_type_at(45), Some(LayerType::SparseAttention));
959 assert_eq!(c.layer_type_at(46), None);
960 assert!(c.has_sparse_attention());
961 assert_eq!(c.sparse_attention_layers().len(), 11, "text stack only");
962 }
963
964 #[test]
965 fn kda_geometry_from_linear_attn_config() {
966 let c = parse_glm5_next(&glm53_config_json()).expect("parse");
967 assert_eq!(c.linear_num_key_heads, 64);
968 assert_eq!(c.linear_key_head_dim, 128);
969 assert_eq!(c.linear_conv_kernel_dim, 4);
970 }
971
972 #[test]
973 fn indexer_topk_is_2048_not_the_deepseek_default() {
974 let c = parse_glm5_next(&glm53_config_json()).expect("parse");
975 assert_eq!(c.index_topk, 2048);
976 }
977
978 #[test]
979 fn missing_rope_key_is_refused_not_guessed() {
980 let mut v: serde_json::Value = serde_json::from_str(&glm53_config_json()).unwrap();
981 v["text_config"]
982 .as_object_mut()
983 .unwrap()
984 .remove("qk_rope_head_dim");
985 let err = parse_glm5_next(&v.to_string()).unwrap_err();
986 assert!(
987 err.to_string().contains("refusing to guess"),
988 "unexpected error: {err}"
989 );
990 }
991
992 #[test]
996 fn a_missing_swiglu_limit_is_refused_not_defaulted() {
997 let mut v: serde_json::Value = serde_json::from_str(&glm53_config_json()).unwrap();
998 v["text_config"]
999 .as_object_mut()
1000 .unwrap()
1001 .remove("swiglu_limit");
1002 let err = parse_glm5_next(&v.to_string()).unwrap_err();
1003 assert!(
1004 err.to_string().contains("swiglu_limit"),
1005 "unexpected error: {err}"
1006 );
1007 }
1008
1009 #[test]
1010 fn the_swiglu_limit_is_read_verbatim() {
1011 let c = parse_glm5_next(&glm53_config_json()).unwrap();
1012 assert_eq!(c.swiglu_limit, 10.0);
1013 }
1014
1015 #[test]
1019 fn grouped_expert_routing_is_refused() {
1020 for key in ["n_group", "topk_group"] {
1021 let mut v: serde_json::Value = serde_json::from_str(&glm53_config_json()).unwrap();
1022 v["text_config"][key] = serde_json::json!(8);
1023 let err = parse_glm5_next(&v.to_string()).unwrap_err();
1024 assert!(
1025 err.to_string().contains("Grouped expert routing"),
1026 "{key}: unexpected error: {err}"
1027 );
1028 }
1029 }
1030
1031 #[test]
1032 fn contradictory_layer_maps_are_rejected() {
1033 let mut v: serde_json::Value = serde_json::from_str(&glm53_config_json()).unwrap();
1034 v["text_config"]["layer_types"][0] =
1037 serde_json::Value::String("deepseek_sparse_attention".into());
1038 let err = parse_glm5_next(&v.to_string()).unwrap_err();
1039 assert!(err.to_string().contains("disagrees"), "unexpected: {err}");
1040 }
1041
1042 #[test]
1048 fn glm_router_defaults_to_hf_fp32_when_the_config_is_silent() {
1049 let cfg = parse_glm5_next(&glm53_config_json()).unwrap();
1050 assert_eq!(cfg.glm5next_router_mode, Glm5NextRouterMode::HfFp32);
1051 assert!(cfg.glm5next_router_mode.is_fp32());
1052 }
1053
1054 #[test]
1057 fn glm_router_bf16_compat_mode_is_explicit_and_never_inferred() {
1058 let base = glm53_config_json();
1059 let with = base.replace(
1060 r#""hc_mult": 4,"#,
1061 r#""hc_mult": 4, "moe_router_dtype": "bfloat16","#,
1062 );
1063 assert_ne!(with, base, "fixture anchor moved");
1064 let cfg = parse_glm5_next(&with).unwrap();
1065 assert_eq!(cfg.glm5next_router_mode, Glm5NextRouterMode::VllmBf16);
1066 assert!(!cfg.glm5next_router_mode.is_fp32());
1067
1068 let fp32 = base.replace(
1069 r#""hc_mult": 4,"#,
1070 r#""hc_mult": 4, "moe_router_dtype": "float32","#,
1071 );
1072 assert_eq!(
1073 parse_glm5_next(&fp32).unwrap().glm5next_router_mode,
1074 Glm5NextRouterMode::HfFp32
1075 );
1076 }
1077
1078 #[test]
1081 fn an_unknown_router_dtype_is_refused_not_defaulted() {
1082 let bad = glm53_config_json().replace(
1083 r#""hc_mult": 4,"#,
1084 r#""hc_mult": 4, "moe_router_dtype": "fp8","#,
1085 );
1086 assert!(parse_glm5_next(&bad).is_err());
1087 }
1088}