1use crate::config::RopeLayout;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ArchScope {
20 TextGeneration,
22 DeferredEncoderEmbedding,
24 DeferredMultimodal,
26 DeferredDiffusion,
28 DeferredAudio,
30 EnumOnly,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum DecoderFamily {
37 StandardGqa,
39 Qwen3Family,
41 GemmaFamily,
43 PhiFamily,
45 Mla,
47 Hybrid,
49 Recurrent,
51 EncoderDecoder,
53 Dedicated,
55 TestFixture,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum MemoryKind {
62 KvGqa,
63 KvIswa,
64 KvMla,
65 KvDsa,
66 KvDsv4,
67 Recurrent,
68 Hybrid,
69 None,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
74pub enum QkNormStyle {
75 #[default]
77 WholeVector,
78 PerHead,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum ArchPath {
86 GenericGqa { rope: RopeLayout },
88 TestFixture { rope: RopeLayout },
90 DedicatedOnly { reason: &'static str },
93 Deferred { reason: &'static str },
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub struct ArchProfile {
100 pub gguf_name: &'static str,
101 pub scope: ArchScope,
102 pub family: DecoderFamily,
103 pub memory: MemoryKind,
104 pub rope: RopeLayout,
105 pub path: ArchPath,
106 pub qk_norm: QkNormStyle,
109}
110
111fn prof(
112 name: &'static str,
113 scope: ArchScope,
114 fam: DecoderFamily,
115 mem: MemoryKind,
116 rope: RopeLayout,
117 path: ArchPath,
118 qk: QkNormStyle,
119) -> ArchProfile {
120 ArchProfile {
121 gguf_name: name,
122 scope,
123 family: fam,
124 memory: mem,
125 rope,
126 path,
127 qk_norm: qk,
128 }
129}
130
131fn gqa_norm(name: &'static str) -> ArchProfile {
132 prof(
133 name,
134 ArchScope::TextGeneration,
135 DecoderFamily::StandardGqa,
136 MemoryKind::KvGqa,
137 RopeLayout::Norm,
138 ArchPath::GenericGqa {
139 rope: RopeLayout::Norm,
140 },
141 QkNormStyle::WholeVector,
142 )
143}
144
145fn gqa_neox(name: &'static str) -> ArchProfile {
146 prof(
147 name,
148 ArchScope::TextGeneration,
149 DecoderFamily::StandardGqa,
150 MemoryKind::KvGqa,
151 RopeLayout::Neox,
152 ArchPath::GenericGqa {
153 rope: RopeLayout::Neox,
154 },
155 QkNormStyle::WholeVector,
156 )
157}
158
159fn dedicated(name: &'static str, reason: &'static str) -> ArchProfile {
160 prof(
161 name,
162 ArchScope::TextGeneration,
163 DecoderFamily::Dedicated,
164 MemoryKind::KvGqa,
165 RopeLayout::Norm,
166 ArchPath::DedicatedOnly { reason },
167 QkNormStyle::WholeVector,
168 )
169}
170
171fn deferred_scope(name: &'static str, scope: ArchScope, reason: &'static str) -> ArchProfile {
172 prof(
173 name,
174 scope,
175 DecoderFamily::StandardGqa,
176 MemoryKind::None,
177 RopeLayout::Neox,
178 ArchPath::Deferred { reason },
179 QkNormStyle::WholeVector,
180 )
181}
182
183pub fn architecture_catalog() -> &'static [ArchProfile] {
186 use std::sync::OnceLock;
187 use ArchScope::*;
188 use DecoderFamily::*;
189 use MemoryKind::*;
190 use QkNormStyle::*;
191 use RopeLayout::*;
192
193 static CAT: OnceLock<Vec<ArchProfile>> = OnceLock::new();
194 CAT.get_or_init(|| {
195 let mut v = Vec::with_capacity(160);
196 for n in [
198 "llama",
199 "deci",
200 "baichuan",
201 "starcoder",
202 "internlm2",
203 "xverse",
204 "olmo",
205 "arctic",
206 "deepseek",
207 "chatglm",
208 "granite",
209 "granitemoe",
210 "granite-moe",
211 "mistral3",
212 "maincoder",
213 "smollm3",
214 "arcee",
215 "ernie4_5",
216 "ernie4_5-moe",
217 "bailingmoe",
218 "nanbeige",
219 "plm",
220 ] {
221 v.push(gqa_norm(n));
222 }
223 for n in [
224 "olmoe", "qwen", "qwen2", "qwen2moe", "stablelm", "mistral",
225 "mixtral", "olmo2", "gpt2", "bloom", "mpt", "refact", "bitnet", "jais", "jais2",
226 "grok", "dbrx", "exaone4", "yi",
227 "gpt-oss",
233 "afmoe",
243 "apertus",
244 "bailingmoe2",
245 "codeshell",
246 "dots1",
247 "exaone",
248 "exaone-moe",
249 "grovemoe",
250 "hunyuan-dense",
251 "hunyuan-moe",
252 "laguna",
253 "mellum",
254 "mimo2",
255 "minicpm3",
256 "nemotron",
257 "openelm",
258 "orion",
259 "plamo3",
260 "seed_oss",
261 "smallthinker",
262 "starcoder2",
263 "step35",
264 "talkie",
265 ] {
266 v.push(gqa_neox(n));
267 }
268 v.push(prof(
269 "qwen3",
270 TextGeneration,
271 Qwen3Family,
272 KvGqa,
273 Neox,
274 ArchPath::GenericGqa { rope: Neox },
275 PerHead,
276 ));
277 v.push(prof(
278 "qwen3moe",
279 TextGeneration,
280 Qwen3Family,
281 KvGqa,
282 Neox,
283 ArchPath::GenericGqa { rope: Neox },
284 PerHead,
285 ));
286 v.push(prof(
287 "gemma",
288 TextGeneration,
289 GemmaFamily,
290 KvGqa,
291 Neox,
292 ArchPath::GenericGqa { rope: Neox },
293 PerHead,
294 ));
295 v.push(prof(
296 "gemma2",
297 TextGeneration,
298 GemmaFamily,
299 KvIswa,
300 Neox,
301 ArchPath::GenericGqa { rope: Neox },
302 PerHead,
303 ));
304 v.push(prof(
305 "gemma3",
306 TextGeneration,
307 GemmaFamily,
308 KvIswa,
309 Neox,
310 ArchPath::GenericGqa { rope: Neox },
311 PerHead,
312 ));
313 for n in ["gemma4", "gemma4-assistant"] {
317 v.push(prof(
318 n,
319 TextGeneration,
320 GemmaFamily,
321 KvIswa,
322 Neox,
323 ArchPath::DedicatedOnly {
324 reason: "use load_gemma4_engine_from_path / ServedEngine::Gemma4",
325 },
326 PerHead,
327 ));
328 }
329 const PARALLEL_RESIDUAL: &str =
336 "parallel attention+FFN residual -- llama.cpp feeds both branches the *same* \
337 normed input and sums `inpL + attn_out + ffn_out` once; the generic decoder \
338 computes the sequential form, which is a different graph";
339 for (n, rope, fam) in [
340 ("command-r", Norm, StandardGqa),
344 ("cohere2", Norm, StandardGqa),
345 ("cohere2moe", Norm, StandardGqa),
346 ("falcon", Neox, StandardGqa),
349 ("gptneox", Neox, StandardGqa),
353 ("phi2", Neox, PhiFamily),
355 ("plamo", Neox, StandardGqa),
356 ] {
357 v.push(prof(
358 n,
359 TextGeneration,
360 fam,
361 KvGqa,
362 rope,
363 ArchPath::DedicatedOnly {
364 reason: PARALLEL_RESIDUAL,
365 },
366 WholeVector,
367 ));
368 }
369 v.push(prof(
378 "minicpm",
379 TextGeneration,
380 StandardGqa,
381 KvGqa,
382 Norm,
383 ArchPath::DedicatedOnly {
384 reason: "unconditional embedding/residual/logit multipliers that llama.cpp \
385 applies even when the GGUF omits every key; not applied by the \
386 generic decoder",
387 },
388 WholeVector,
389 ));
390 for (n, fam) in [("phi3", PhiFamily), ("phimoe", PhiFamily)] {
391 v.push(prof(
392 n,
393 TextGeneration,
394 fam,
395 KvGqa,
396 Neox,
397 ArchPath::GenericGqa { rope: Neox },
398 WholeVector,
399 ));
400 }
401 v.push(prof(
406 "phi4",
407 TextGeneration,
408 PhiFamily,
409 KvGqa,
410 Neox,
411 ArchPath::GenericGqa { rope: Neox },
412 WholeVector,
413 ));
414 v.push(prof(
417 "llama4",
418 TextGeneration,
419 Dedicated,
420 KvGqa,
421 Norm,
422 ArchPath::DedicatedOnly {
423 reason: "llama4 MoE + non-GQA attn — see llama4_engine.rs tensor list",
424 },
425 WholeVector,
426 ));
427 for n in ["minimax-m2", "minimax-m3"] {
429 v.push(prof(
430 n,
431 TextGeneration,
432 Dedicated,
433 KvGqa,
434 Neox,
439 ArchPath::DedicatedOnly {
440 reason: "MiniMax 256-expert sigmoid MoE + MTP — see minimax_engine.rs",
441 },
442 WholeVector,
443 ));
444 }
445 v.push(prof(
446 "deepseek2",
447 TextGeneration,
448 Mla,
449 KvMla,
450 Norm,
451 ArchPath::DedicatedOnly {
452 reason: "DeepSeek-2 MLA needs the MLA engine, not generic GQA",
453 },
454 WholeVector,
455 ));
456 v.push(prof(
457 "deepseek32",
458 TextGeneration,
459 Mla,
460 KvDsa,
461 Norm,
462 ArchPath::DedicatedOnly {
463 reason: "DeepSeek-3.2 DSA/MLA needs the dedicated sparse/MLA stack",
464 },
465 WholeVector,
466 ));
467 v.push(prof(
468 "mistral4",
469 TextGeneration,
470 Mla,
471 KvMla,
472 Norm,
473 ArchPath::DedicatedOnly {
474 reason: "mistral4 reuses DeepSeek-2 MLA loader/graph in llama.cpp",
475 },
476 WholeVector,
477 ));
478 v.push(dedicated(
479 "glm-dsa",
480 "use ferrox_models::glm52_decoder / glm52_gguf_loader (DSA), not the generic GQA Decoder",
481 ));
482 v.push(dedicated(
483 "glm4",
484 "use ferrox_models::glm52_decoder / glm52_gguf_loader, not the generic GQA Decoder",
485 ));
486 v.push(dedicated(
487 "glm4moe",
488 "use ferrox_models::glm52_decoder / glm52_gguf_loader, not the generic GQA Decoder",
489 ));
490 v.push(dedicated(
491 "deepseek4",
492 "DeepSeek V4 needs CSA/HCA + mHC assembly; generic GQA Decoder is not valid",
493 ));
494 v.push(dedicated(
495 "kimi-linear",
496 "use ferrox_models::kimi_decoder / kimi_loader, not the generic GQA Decoder",
497 ));
498 v.push(dedicated(
499 "kimi_k3",
500 "use ferrox_models::kimi_decoder / kimi_loader, not the generic GQA Decoder",
501 ));
502 for (n, rope) in [
503 ("jamba", Neox),
504 ("falcon-h1", Neox),
505 ("plamo2", Neox),
506 ("granitehybrid", Norm),
507 ("granite-hybrid", Norm),
508 ("lfm2", Neox),
509 ("lfm2moe", Neox),
510 ("nemotron_h", Neox),
511 ("nemotron_h_moe", Neox),
512 ("qwen3next", Neox),
513 ("qwen35", Neox),
514 ("qwen35moe", Neox),
515 ] {
516 let qk = if n.starts_with("qwen3") {
517 PerHead
518 } else {
519 WholeVector
520 };
521 v.push(prof(
522 n,
523 TextGeneration,
524 DecoderFamily::Hybrid,
525 MemoryKind::Hybrid,
526 rope,
527 ArchPath::DedicatedOnly {
528 reason: "hybrid attn+SSM/delta-net engine not yet on the serve path",
529 },
530 qk,
531 ));
532 }
533 for n in ["mamba", "mamba2", "rwkv6", "rwkv6qwen2", "rwkv7", "arwkv7"] {
534 v.push(prof(
535 n,
536 TextGeneration,
537 DecoderFamily::Recurrent,
538 MemoryKind::Recurrent,
539 Neox,
540 ArchPath::DedicatedOnly {
541 reason: "recurrent engine not yet on the serve path",
542 },
543 WholeVector,
544 ));
545 }
546 v.push(prof(
547 "t5",
548 TextGeneration,
549 EncoderDecoder,
550 None,
551 Neox,
552 ArchPath::DedicatedOnly {
553 reason: "T5 encoder-decoder engine not yet on the serve path",
554 },
555 WholeVector,
556 ));
557 for (n, scope, reason) in [
558 (
559 "t5encoder",
560 DeferredEncoderEmbedding,
561 "encoder-only; deferred from text-generation parity",
562 ),
563 ("bert", DeferredEncoderEmbedding, "encoder/embedding; deferred"),
564 (
565 "modern-bert",
566 DeferredEncoderEmbedding,
567 "encoder/embedding; deferred",
568 ),
569 (
570 "nomic-bert",
571 DeferredEncoderEmbedding,
572 "encoder/embedding; deferred",
573 ),
574 (
575 "nomic-bert-moe",
576 DeferredEncoderEmbedding,
577 "encoder/embedding; deferred",
578 ),
579 (
580 "neo-bert",
581 DeferredEncoderEmbedding,
582 "encoder/embedding; deferred",
583 ),
584 (
585 "jina-bert-v2",
586 DeferredEncoderEmbedding,
587 "encoder/embedding; deferred",
588 ),
589 (
590 "jina-bert-v3",
591 DeferredEncoderEmbedding,
592 "encoder/embedding; deferred",
593 ),
594 (
595 "eurobert",
596 DeferredEncoderEmbedding,
597 "encoder/embedding; deferred",
598 ),
599 (
600 "llama-embed",
601 DeferredEncoderEmbedding,
602 "embedding variant; deferred",
603 ),
604 (
605 "gemma-embedding",
606 DeferredEncoderEmbedding,
607 "embedding variant; deferred",
608 ),
609 (
610 "pangu-embedded",
611 DeferredEncoderEmbedding,
612 "embedding variant; deferred",
613 ),
614 ("yi-vl", DeferredMultimodal, "Yi vision-language; deferred"),
615 ("qwen2vl", DeferredMultimodal, "vision-language; deferred"),
616 ("qwen3vl", DeferredMultimodal, "vision-language; deferred"),
617 ("qwen3vlmoe", DeferredMultimodal, "vision-language; deferred"),
618 ("cogvlm", DeferredMultimodal, "vision-language; deferred"),
619 ("chameleon", DeferredMultimodal, "multimodal; deferred"),
620 ("hunyuan_vl", DeferredMultimodal, "vision-language; deferred"),
621 ("paddleocr", DeferredMultimodal, "OCR multimodal; deferred"),
622 ("hy_v3", DeferredMultimodal, "multimodal; deferred"),
623 ("deepseek2-ocr", DeferredMultimodal, "OCR multimodal; deferred"),
624 ("dream", DeferredDiffusion, "diffusion LM; deferred"),
625 ("llada", DeferredDiffusion, "diffusion LM; deferred"),
626 ("llada-moe", DeferredDiffusion, "diffusion LM; deferred"),
627 ("rnd1", DeferredDiffusion, "diffusion LM; deferred"),
628 (
629 "wavtokenizer-dec",
630 DeferredAudio,
631 "audio tokenizer; deferred",
632 ),
633 (
634 "eagle3",
635 EnumOnly,
636 "speculative draft head; not a standalone decoder target",
637 ),
638 (
639 "dflash",
640 EnumOnly,
641 "speculative draft head; not a standalone decoder target",
642 ),
643 ("clip", EnumOnly, "quantize dummy only"),
644 ("gptj", EnumOnly, "enum-only in llama.cpp factory gap"),
645 ("(unknown)", EnumOnly, "llama.cpp unknown sentinel"),
646 ] {
647 v.push(deferred_scope(n, scope, reason));
648 }
649 v.push(prof(
650 "gemma3n",
651 TextGeneration,
652 GemmaFamily,
653 KvIswa,
654 Neox,
655 ArchPath::DedicatedOnly {
656 reason: "gemma3n AltUp/Laurel tensors not implemented in the generic decoder",
657 },
658 PerHead,
659 ));
660 for n in ["ferroxtest", "ferroxtestmoe", "ferroxtestmixed"] {
661 v.push(prof(
662 n,
663 TextGeneration,
664 TestFixture,
665 KvGqa,
666 Neox,
667 ArchPath::TestFixture { rope: Neox },
668 WholeVector,
669 ));
670 }
671 v
672 })
673 .as_slice()
674}
675
676pub fn resolve_profile(arch: &str) -> Option<&'static ArchProfile> {
678 architecture_catalog().iter().find(|p| p.gguf_name == arch)
679}
680
681pub fn resolve_architecture(arch: &str) -> Option<ArchPath> {
684 resolve_profile(arch).map(|p| p.path)
685}
686
687pub fn default_swa_pattern(arch: &str) -> Option<usize> {
705 match arch {
706 "gpt-oss" => Some(2),
708 "gemma2" => Some(2),
710 "gemma3" | "gemma3n" => Some(6),
712 "cohere2" | "exaone4" | "olmo2" => Some(4),
714 _ => None,
715 }
716}
717
718pub fn swa_rope_base_follows_model(arch: &str) -> bool {
730 matches!(
731 arch,
732 "afmoe"
733 | "cohere2"
734 | "cohere2moe"
735 | "dflash"
736 | "exaone-moe"
737 | "exaone4"
738 | "gemma2"
739 | "laguna"
740 | "llama4"
741 | "mellum"
742 | "olmo2"
743 | "gpt-oss"
744 | "smallthinker"
745 )
746}
747
748pub fn unsupported_feature_keys(arch: &str) -> Vec<(String, &'static str)> {
752 let profile = resolve_profile(arch);
753 if matches!(profile.map(|p| p.family), Some(DecoderFamily::GemmaFamily)) {
755 return Vec::new();
756 }
757 let key = |suffix: &str| format!("{arch}.{suffix}");
758 vec![
759 (
760 key("attention.logit_softcapping"),
761 "attention logit soft-capping (Gemma 2+); not implemented in the generic decoder",
762 ),
763 (
764 key("final_logit_softcapping"),
765 "final logit soft-capping (Gemma 2+); not implemented in the generic decoder",
766 ),
767 (
768 key("attention.sliding_window_pattern"),
769 "alternating sliding-window pattern (Gemma 2+); not implemented in the generic decoder",
770 ),
771 ]
772}
773
774pub fn unsupported_scaling_keys(arch: &str) -> Vec<(String, &'static str, f32)> {
802 let profile = resolve_profile(arch);
803 if matches!(profile.map(|p| p.family), Some(DecoderFamily::GemmaFamily)) {
805 return Vec::new();
806 }
807 let key = |suffix: &str| format!("{arch}.{suffix}");
808 vec![
809 (
810 key("logit_scale"),
811 "logit multiplier (Granite / Command-R `logits_scaling`); not applied by the generic decoder",
812 1.0,
813 ),
814 (
815 key("residual_scale"),
816 "residual multiplier (Granite `residual_multiplier`); not applied by the generic decoder",
817 1.0,
818 ),
819 (
820 key("embedding_scale"),
821 "embedding multiplier (Granite / MiniCPM `embedding_multiplier`); the generic decoder only scales embeddings for the Gemma family",
822 1.0,
823 ),
824 (
825 key("attention.scale"),
826 "explicit attention score scale (Granite `attention_multiplier`); the generic decoder always uses 1/sqrt(head_dim)",
827 0.0,
828 ),
829 ]
830}
831
832pub fn coverage_report_markdown() -> String {
834 let mut lines = vec![
835 "# Architecture coverage manifest".to_string(),
836 String::new(),
837 "Generated from `ferrox_models::capability::architecture_catalog`.".to_string(),
838 "Source of truth for names: pinned llama.cpp `LLM_ARCH_NAMES`.".to_string(),
839 String::new(),
840 "| GGUF arch | Scope | Family | Memory | Path |".to_string(),
841 "|---|---|---|---|---|".to_string(),
842 ];
843 for p in architecture_catalog() {
844 let path = match p.path {
845 ArchPath::GenericGqa { .. } => "generic-gqa",
846 ArchPath::TestFixture { .. } => "test-fixture",
847 ArchPath::DedicatedOnly { .. } => "dedicated",
848 ArchPath::Deferred { .. } => "deferred",
849 };
850 lines.push(format!(
851 "| `{}` | {:?} | {:?} | {:?} | {} |",
852 p.gguf_name, p.scope, p.family, p.memory, path
853 ));
854 }
855 lines.push(String::new());
856 lines.join("\n")
857}
858
859#[cfg(test)]
860mod tests {
861 use super::*;
862
863 #[test]
864 fn known_mainstream_families_resolve() {
865 assert_eq!(
866 resolve_architecture("llama"),
867 Some(ArchPath::GenericGqa {
868 rope: RopeLayout::Norm
869 })
870 );
871 assert_eq!(
872 resolve_architecture("qwen2moe"),
873 Some(ArchPath::GenericGqa {
874 rope: RopeLayout::Neox
875 })
876 );
877 assert_eq!(
878 resolve_architecture("mistral"),
879 Some(ArchPath::GenericGqa {
880 rope: RopeLayout::Neox
881 })
882 );
883 assert_eq!(
884 resolve_architecture("yi"),
885 Some(ArchPath::GenericGqa {
886 rope: RopeLayout::Neox
887 })
888 );
889 assert_eq!(
890 resolve_architecture("mixtral"),
891 Some(ArchPath::GenericGqa {
892 rope: RopeLayout::Neox
893 })
894 );
895 assert_eq!(
896 resolve_architecture("phi3"),
897 Some(ArchPath::GenericGqa {
898 rope: RopeLayout::Neox
899 })
900 );
901 assert_eq!(
902 resolve_architecture("phi4"),
903 Some(ArchPath::GenericGqa {
904 rope: RopeLayout::Neox
905 })
906 );
907 assert_eq!(
908 resolve_profile("phi4").map(|p| p.family),
909 Some(DecoderFamily::PhiFamily)
910 );
911 assert_eq!(
912 resolve_architecture("gemma3"),
913 Some(ArchPath::GenericGqa {
914 rope: RopeLayout::Neox
915 })
916 );
917 for arch in ["gemma4", "gemma4-assistant"] {
918 assert!(
919 matches!(
920 resolve_architecture(arch),
921 Some(ArchPath::DedicatedOnly { .. })
922 ),
923 "{arch} uses dedicated Gemma4 engine"
924 );
925 assert_eq!(
926 resolve_profile(arch).map(|p| p.family),
927 Some(DecoderFamily::GemmaFamily)
928 );
929 }
930 assert!(matches!(
931 resolve_architecture("gemma3n"),
932 Some(ArchPath::DedicatedOnly { .. })
933 ));
934 assert_eq!(
935 resolve_architecture("deepseek"),
936 Some(ArchPath::GenericGqa {
937 rope: RopeLayout::Norm
938 })
939 );
940 assert_eq!(
941 resolve_profile("qwen3").map(|p| p.qk_norm),
942 Some(QkNormStyle::PerHead)
943 );
944 }
945
946 #[test]
947 fn deepseek2_is_dedicated_mla_not_generic() {
948 assert!(matches!(
949 resolve_architecture("deepseek2"),
950 Some(ArchPath::DedicatedOnly { .. })
951 ));
952 }
953
954 #[test]
955 fn unknown_architecture_is_none() {
956 assert_eq!(resolve_architecture("totally-unknown-arch"), None);
957 assert!(matches!(
959 resolve_architecture("t5"),
960 Some(ArchPath::DedicatedOnly { .. })
961 ));
962 }
963
964 #[test]
965 fn dedicated_paths_are_not_generic() {
966 assert!(matches!(
967 resolve_architecture("glm-dsa"),
968 Some(ArchPath::DedicatedOnly { .. })
969 ));
970 assert!(matches!(
971 resolve_architecture("deepseek4"),
972 Some(ArchPath::DedicatedOnly { .. })
973 ));
974 for arch in ["minimax-m2", "minimax-m3"] {
975 assert!(
976 matches!(
977 resolve_architecture(arch),
978 Some(ArchPath::DedicatedOnly {
979 reason: "MiniMax 256-expert sigmoid MoE + MTP — see minimax_engine.rs"
980 })
981 ),
982 "{arch} must fail closed, not silent generic GQA"
983 );
984 }
985 assert!(
986 matches!(
987 resolve_architecture("llama4"),
988 Some(ArchPath::DedicatedOnly {
989 reason: "llama4 MoE + non-GQA attn — see llama4_engine.rs tensor list"
990 })
991 ),
992 "llama4 must fail closed, not silent generic GQA"
993 );
994 assert!(matches!(
995 resolve_architecture("glm4"),
996 Some(ArchPath::DedicatedOnly { .. })
997 ));
998 assert!(matches!(
999 resolve_architecture("glm4moe"),
1000 Some(ArchPath::DedicatedOnly { .. })
1001 ));
1002 }
1003
1004 #[test]
1005 fn test_fixtures_remain_loadable() {
1006 for arch in ["ferroxtest", "ferroxtestmoe", "ferroxtestmixed"] {
1007 assert!(matches!(
1008 resolve_architecture(arch),
1009 Some(ArchPath::TestFixture { .. })
1010 ));
1011 }
1012 }
1013
1014 #[test]
1015 fn catalog_has_unique_names() {
1016 let mut seen = std::collections::HashSet::new();
1017 for p in architecture_catalog() {
1018 assert!(
1019 seen.insert(p.gguf_name),
1020 "duplicate arch name {}",
1021 p.gguf_name
1022 );
1023 }
1024 }
1025
1026 #[test]
1027 fn gemma_family_does_not_fail_closed_on_softcap_keys() {
1028 assert!(unsupported_feature_keys("gemma3").is_empty());
1029 assert!(!unsupported_feature_keys("llama").is_empty());
1030 }
1031
1032 #[test]
1038 fn architectures_with_a_different_residual_topology_are_refused() {
1039 for arch in [
1040 "command-r",
1041 "cohere2",
1042 "cohere2moe",
1043 "falcon",
1044 "gptneox",
1045 "phi2",
1046 "plamo",
1047 "minicpm",
1048 ] {
1049 match resolve_architecture(arch) {
1050 Some(ArchPath::DedicatedOnly { reason }) => {
1051 assert!(!reason.is_empty(), "{arch} must say why");
1052 }
1053 other => panic!("{arch} must be refused, got {other:?}"),
1054 }
1055 }
1056 for arch in ["phi3", "phimoe", "plamo3", "starcoder2", "nemotron"] {
1059 assert!(
1060 matches!(
1061 resolve_architecture(arch),
1062 Some(ArchPath::GenericGqa { .. })
1063 ),
1064 "{arch} must stay generic"
1065 );
1066 }
1067 }
1068
1069 #[test]
1073 fn no_architecture_is_listed_twice() {
1074 let mut seen = std::collections::HashSet::new();
1075 for p in architecture_catalog() {
1076 assert!(seen.insert(p.gguf_name), "{} listed twice", p.gguf_name);
1077 }
1078 }
1079}