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 "arcee",
214 "ernie4_5",
215 "ernie4_5-moe",
216 "bailingmoe",
217 "nanbeige",
218 "plm",
219 ] {
220 v.push(gqa_norm(n));
221 }
222 for n in [
223 "olmoe", "qwen", "qwen2", "qwen2moe", "stablelm", "mistral",
224 "mixtral", "olmo2", "bitnet", "jais2",
225 "grok", "dbrx", "exaone4", "yi",
226 "gpt-oss",
232 "afmoe",
242 "apertus",
243 "bailingmoe2",
244 "codeshell",
245 "dots1",
246 "exaone",
247 "exaone-moe",
248 "grovemoe",
249 "hunyuan-dense",
250 "hunyuan-moe",
251 "laguna",
252 "mellum",
253 "mimo2",
254 "minicpm3",
255 "nemotron",
256 "openelm",
257 "orion",
258 "plamo3",
259 "seed_oss",
260 "smallthinker",
261 "starcoder2",
262 "step35",
263 "talkie",
264 ] {
265 v.push(gqa_neox(n));
266 }
267 for (n, reason) in [
284 (
285 "smollm3",
286 "a NoPE layer pattern: llama.cpp hardcodes \
287 `hparams.n_no_rope_layer_step = 4` (src/models/smollm3.cpp:5) and \
288 skips RoPE where `(il + 1) % 4 == 0` (:69), so 9 of a 36-layer \
289 SmolLM3-3B's layers get NO rotation at all. There is NO GGUF key \
290 for it, so no metadata gate could see it: the tensor set matches \
291 the generic llama set exactly and the file loads clean. The \
292 generic decoder rotates every layer, which is a different model. \
293 Same shape as the ALiBi group below, and found the same way",
294 ),
295 (
296 "gpt2",
297 "learned absolute position embeddings (`position_embd.weight`, \
298 src/models/gpt2.cpp:19,74) and no RoPE; the generic decoder has no \
299 slot for them and rotates instead",
300 ),
301 (
302 "mpt",
303 "ALiBi attention bias (src/models/mpt.cpp:6), plus an optional \
304 learned `position_embd` and an optional QKV clamp; the generic \
305 decoder implements none of the three and applies RoPE instead",
306 ),
307 (
308 "refact",
309 "ALiBi attention bias, hardcoded `f_max_alibi_bias = 8.0f` with no \
310 GGUF key to detect it (src/models/refact.cpp:12); the generic \
311 decoder applies RoPE instead",
312 ),
313 (
314 "bloom",
315 "ALiBi attention bias, hardcoded `f_max_alibi_bias = 8.0f` with no \
316 GGUF key (src/models/bloom.cpp:18), plus a `token_embd_norm` the \
317 generic decoder never applies; RoPE is applied instead",
318 ),
319 (
320 "jais",
321 "ALiBi attention bias (src/models/jais.cpp:5); the generic decoder \
322 applies RoPE instead",
323 ),
324 ] {
325 v.push(prof(
326 n,
327 TextGeneration,
328 StandardGqa,
329 KvGqa,
330 Norm,
336 ArchPath::DedicatedOnly { reason },
337 WholeVector,
338 ));
339 }
340 v.push(prof(
341 "qwen3",
342 TextGeneration,
343 Qwen3Family,
344 KvGqa,
345 Neox,
346 ArchPath::GenericGqa { rope: Neox },
347 PerHead,
348 ));
349 v.push(prof(
350 "qwen3moe",
351 TextGeneration,
352 Qwen3Family,
353 KvGqa,
354 Neox,
355 ArchPath::GenericGqa { rope: Neox },
356 PerHead,
357 ));
358 v.push(prof(
359 "gemma",
360 TextGeneration,
361 GemmaFamily,
362 KvGqa,
363 Neox,
364 ArchPath::GenericGqa { rope: Neox },
365 PerHead,
366 ));
367 v.push(prof(
368 "gemma2",
369 TextGeneration,
370 GemmaFamily,
371 KvIswa,
372 Neox,
373 ArchPath::GenericGqa { rope: Neox },
374 PerHead,
375 ));
376 v.push(prof(
377 "gemma3",
378 TextGeneration,
379 GemmaFamily,
380 KvIswa,
381 Neox,
382 ArchPath::GenericGqa { rope: Neox },
383 PerHead,
384 ));
385 for n in ["gemma4", "gemma4-assistant"] {
389 v.push(prof(
390 n,
391 TextGeneration,
392 GemmaFamily,
393 KvIswa,
394 Neox,
395 ArchPath::DedicatedOnly {
396 reason: "use load_gemma4_engine_from_path / ServedEngine::Gemma4",
397 },
398 PerHead,
399 ));
400 }
401 const PARALLEL_RESIDUAL: &str =
408 "parallel attention+FFN residual -- llama.cpp feeds both branches the *same* \
409 normed input and sums `inpL + attn_out + ffn_out` once; the generic decoder \
410 computes the sequential form, which is a different graph";
411 for (n, rope, fam) in [
412 ("command-r", Norm, StandardGqa),
416 ("cohere2", Norm, StandardGqa),
417 ("cohere2moe", Norm, StandardGqa),
418 ("falcon", Neox, StandardGqa),
421 ("gptneox", Neox, StandardGqa),
425 ("phi2", Neox, PhiFamily),
427 ("plamo", Neox, StandardGqa),
428 ] {
429 v.push(prof(
430 n,
431 TextGeneration,
432 fam,
433 KvGqa,
434 rope,
435 ArchPath::DedicatedOnly {
436 reason: PARALLEL_RESIDUAL,
437 },
438 WholeVector,
439 ));
440 }
441 v.push(prof(
450 "minicpm",
451 TextGeneration,
452 StandardGqa,
453 KvGqa,
454 Norm,
455 ArchPath::DedicatedOnly {
456 reason: "unconditional embedding/residual/logit multipliers that llama.cpp \
457 applies even when the GGUF omits every key; not applied by the \
458 generic decoder",
459 },
460 WholeVector,
461 ));
462 for (n, fam) in [("phi3", PhiFamily), ("phimoe", PhiFamily)] {
463 v.push(prof(
464 n,
465 TextGeneration,
466 fam,
467 KvGqa,
468 Neox,
469 ArchPath::GenericGqa { rope: Neox },
470 WholeVector,
471 ));
472 }
473 v.push(prof(
478 "phi4",
479 TextGeneration,
480 PhiFamily,
481 KvGqa,
482 Neox,
483 ArchPath::GenericGqa { rope: Neox },
484 WholeVector,
485 ));
486 v.push(prof(
489 "llama4",
490 TextGeneration,
491 Dedicated,
492 KvGqa,
493 Norm,
494 ArchPath::DedicatedOnly {
495 reason: "llama4 MoE + non-GQA attn — see llama4_engine.rs tensor list",
496 },
497 WholeVector,
498 ));
499 for n in ["minimax-m2", "minimax-m3"] {
501 v.push(prof(
502 n,
503 TextGeneration,
504 Dedicated,
505 KvGqa,
506 Neox,
511 ArchPath::DedicatedOnly {
512 reason: "MiniMax 256-expert sigmoid MoE + MTP — see minimax_engine.rs",
513 },
514 WholeVector,
515 ));
516 }
517 v.push(prof(
518 "deepseek2",
519 TextGeneration,
520 Mla,
521 KvMla,
522 Norm,
523 ArchPath::DedicatedOnly {
524 reason: "DeepSeek-2 MLA needs the MLA engine, not generic GQA",
525 },
526 WholeVector,
527 ));
528 v.push(prof(
529 "deepseek32",
530 TextGeneration,
531 Mla,
532 KvDsa,
533 Norm,
534 ArchPath::DedicatedOnly {
535 reason: "DeepSeek-3.2 DSA/MLA needs the dedicated sparse/MLA stack",
536 },
537 WholeVector,
538 ));
539 v.push(prof(
540 "mistral4",
541 TextGeneration,
542 Mla,
543 KvMla,
544 Norm,
545 ArchPath::DedicatedOnly {
546 reason: "mistral4 reuses DeepSeek-2 MLA loader/graph in llama.cpp",
547 },
548 WholeVector,
549 ));
550 v.push(dedicated(
551 "glm-dsa",
552 "use ferrox_models::glm52_decoder / glm52_gguf_loader (DSA), not the generic GQA Decoder",
553 ));
554 v.push(dedicated(
555 "glm4",
556 "use ferrox_models::glm52_decoder / glm52_gguf_loader, not the generic GQA Decoder",
557 ));
558 v.push(dedicated(
559 "glm4moe",
560 "use ferrox_models::glm52_decoder / glm52_gguf_loader, not the generic GQA Decoder",
561 ));
562 v.push(dedicated(
563 "deepseek4",
564 "DeepSeek V4 needs CSA/HCA + mHC assembly; generic GQA Decoder is not valid",
565 ));
566 v.push(dedicated(
567 "kimi-linear",
568 "use ferrox_models::kimi_decoder / kimi_loader, not the generic GQA Decoder",
569 ));
570 v.push(dedicated(
571 "kimi_k3",
572 "use ferrox_models::kimi_decoder / kimi_loader, not the generic GQA Decoder",
573 ));
574 for (n, rope) in [
575 ("jamba", Neox),
576 ("falcon-h1", Neox),
577 ("plamo2", Neox),
578 ("granitehybrid", Norm),
579 ("granite-hybrid", Norm),
580 ("lfm2", Neox),
581 ("lfm2moe", Neox),
582 ("nemotron_h", Neox),
583 ("nemotron_h_moe", Neox),
584 ("qwen3next", Neox),
585 ("qwen35", Neox),
586 ("qwen35moe", Neox),
587 ] {
588 let qk = if n.starts_with("qwen3") {
589 PerHead
590 } else {
591 WholeVector
592 };
593 v.push(prof(
594 n,
595 TextGeneration,
596 DecoderFamily::Hybrid,
597 MemoryKind::Hybrid,
598 rope,
599 ArchPath::DedicatedOnly {
600 reason: "hybrid attn+SSM/delta-net engine not yet on the serve path",
601 },
602 qk,
603 ));
604 }
605 for n in ["mamba", "mamba2", "rwkv6", "rwkv6qwen2", "rwkv7", "arwkv7"] {
606 v.push(prof(
607 n,
608 TextGeneration,
609 DecoderFamily::Recurrent,
610 MemoryKind::Recurrent,
611 Neox,
612 ArchPath::DedicatedOnly {
613 reason: "recurrent engine not yet on the serve path",
614 },
615 WholeVector,
616 ));
617 }
618 v.push(prof(
619 "t5",
620 TextGeneration,
621 EncoderDecoder,
622 None,
623 Neox,
624 ArchPath::DedicatedOnly {
625 reason: "T5 encoder-decoder engine not yet on the serve path",
626 },
627 WholeVector,
628 ));
629 for (n, scope, reason) in [
630 (
631 "t5encoder",
632 DeferredEncoderEmbedding,
633 "encoder-only; deferred from text-generation parity",
634 ),
635 ("bert", DeferredEncoderEmbedding, "encoder/embedding; deferred"),
636 (
637 "modern-bert",
638 DeferredEncoderEmbedding,
639 "encoder/embedding; deferred",
640 ),
641 (
642 "nomic-bert",
643 DeferredEncoderEmbedding,
644 "encoder/embedding; deferred",
645 ),
646 (
647 "nomic-bert-moe",
648 DeferredEncoderEmbedding,
649 "encoder/embedding; deferred",
650 ),
651 (
652 "neo-bert",
653 DeferredEncoderEmbedding,
654 "encoder/embedding; deferred",
655 ),
656 (
657 "jina-bert-v2",
658 DeferredEncoderEmbedding,
659 "encoder/embedding; deferred",
660 ),
661 (
662 "jina-bert-v3",
663 DeferredEncoderEmbedding,
664 "encoder/embedding; deferred",
665 ),
666 (
667 "eurobert",
668 DeferredEncoderEmbedding,
669 "encoder/embedding; deferred",
670 ),
671 (
672 "llama-embed",
673 DeferredEncoderEmbedding,
674 "embedding variant; deferred",
675 ),
676 (
677 "gemma-embedding",
678 DeferredEncoderEmbedding,
679 "embedding variant; deferred",
680 ),
681 (
682 "pangu-embedded",
683 DeferredEncoderEmbedding,
684 "embedding variant; deferred",
685 ),
686 ("yi-vl", DeferredMultimodal, "Yi vision-language; deferred"),
687 ("qwen2vl", DeferredMultimodal, "vision-language; deferred"),
688 ("qwen3vl", DeferredMultimodal, "vision-language; deferred"),
689 ("qwen3vlmoe", DeferredMultimodal, "vision-language; deferred"),
690 ("cogvlm", DeferredMultimodal, "vision-language; deferred"),
691 ("chameleon", DeferredMultimodal, "multimodal; deferred"),
692 ("hunyuan_vl", DeferredMultimodal, "vision-language; deferred"),
693 ("paddleocr", DeferredMultimodal, "OCR multimodal; deferred"),
694 ("hy_v3", DeferredMultimodal, "multimodal; deferred"),
695 ("deepseek2-ocr", DeferredMultimodal, "OCR multimodal; deferred"),
696 ("dream", DeferredDiffusion, "diffusion LM; deferred"),
697 ("llada", DeferredDiffusion, "diffusion LM; deferred"),
698 ("llada-moe", DeferredDiffusion, "diffusion LM; deferred"),
699 ("rnd1", DeferredDiffusion, "diffusion LM; deferred"),
700 (
701 "wavtokenizer-dec",
702 DeferredAudio,
703 "audio tokenizer; deferred",
704 ),
705 (
706 "eagle3",
707 EnumOnly,
708 "speculative draft head; not a standalone decoder target",
709 ),
710 (
711 "dflash",
712 EnumOnly,
713 "speculative draft head; not a standalone decoder target",
714 ),
715 ("clip", EnumOnly, "quantize dummy only"),
716 ("gptj", EnumOnly, "enum-only in llama.cpp factory gap"),
717 ("(unknown)", EnumOnly, "llama.cpp unknown sentinel"),
718 ] {
719 v.push(deferred_scope(n, scope, reason));
720 }
721 v.push(prof(
722 "gemma3n",
723 TextGeneration,
724 GemmaFamily,
725 KvIswa,
726 Neox,
727 ArchPath::DedicatedOnly {
728 reason: "gemma3n AltUp/Laurel tensors not implemented in the generic decoder",
729 },
730 PerHead,
731 ));
732 for n in ["ferroxtest", "ferroxtestmoe", "ferroxtestmixed"] {
733 v.push(prof(
734 n,
735 TextGeneration,
736 TestFixture,
737 KvGqa,
738 Neox,
739 ArchPath::TestFixture { rope: Neox },
740 WholeVector,
741 ));
742 }
743 v
744 })
745 .as_slice()
746}
747
748pub fn resolve_profile(arch: &str) -> Option<&'static ArchProfile> {
750 architecture_catalog().iter().find(|p| p.gguf_name == arch)
751}
752
753pub fn resolve_architecture(arch: &str) -> Option<ArchPath> {
756 resolve_profile(arch).map(|p| p.path)
757}
758
759pub fn default_swa_pattern(arch: &str) -> Option<usize> {
777 match arch {
778 "gpt-oss" => Some(2),
780 "gemma2" => Some(2),
782 "gemma3" => Some(6),
784 "gemma3n" => Some(5),
788 "cohere2" | "exaone4" | "olmo2" => Some(4),
790 "mellum" => Some(4),
799 "exaone-moe" => Some(4),
803 "afmoe" => Some(4),
807 "plamo3" => Some(8),
808 _ => None,
809 }
810}
811
812pub fn swa_rope_base_follows_model(arch: &str) -> bool {
824 matches!(
825 arch,
826 "afmoe"
827 | "cohere2"
828 | "cohere2moe"
829 | "dflash"
830 | "exaone-moe"
831 | "exaone4"
832 | "gemma2"
833 | "laguna"
834 | "llama4"
835 | "mellum"
836 | "olmo2"
837 | "gpt-oss"
838 | "smallthinker"
839 )
840}
841
842pub fn unsupported_feature_keys(arch: &str) -> Vec<(String, &'static str)> {
846 let profile = resolve_profile(arch);
847 if matches!(profile.map(|p| p.family), Some(DecoderFamily::GemmaFamily)) {
849 return Vec::new();
850 }
851 let key = |suffix: &str| format!("{arch}.{suffix}");
852 vec![
853 (
854 key("attention.logit_softcapping"),
855 "attention logit soft-capping (Gemma 2+); not implemented in the generic decoder",
856 ),
857 (
858 key("final_logit_softcapping"),
859 "final logit soft-capping (Gemma 2+); not implemented in the generic decoder",
860 ),
861 (
862 key("attention.sliding_window_pattern"),
863 "alternating sliding-window pattern (Gemma 2+); not implemented in the generic decoder",
864 ),
865 ]
866}
867
868pub fn unsupported_scaling_keys(arch: &str) -> Vec<(String, &'static str, f32)> {
896 let profile = resolve_profile(arch);
897 if matches!(profile.map(|p| p.family), Some(DecoderFamily::GemmaFamily)) {
899 return Vec::new();
900 }
901 let key = |suffix: &str| format!("{arch}.{suffix}");
902 vec![
903 (
904 key("logit_scale"),
905 "logit multiplier (Granite / Command-R `logits_scaling`); not applied by the generic decoder",
906 1.0,
907 ),
908 (
909 key("residual_scale"),
910 "residual multiplier (Granite `residual_multiplier`); not applied by the generic decoder",
911 1.0,
912 ),
913 (
914 key("embedding_scale"),
915 "embedding multiplier (Granite / MiniCPM `embedding_multiplier`); the generic decoder only scales embeddings for the Gemma family",
916 1.0,
917 ),
918 (
919 key("attention.scale"),
920 "explicit attention score scale (Granite `attention_multiplier`); the generic decoder always uses 1/sqrt(head_dim)",
921 0.0,
922 ),
923 ]
924}
925
926pub fn coverage_report_markdown() -> String {
928 let mut lines = vec![
929 "# Architecture coverage manifest".to_string(),
930 String::new(),
931 "Generated from `ferrox_models::capability::architecture_catalog`.".to_string(),
932 "Source of truth for names: pinned llama.cpp `LLM_ARCH_NAMES`.".to_string(),
933 String::new(),
934 "| GGUF arch | Scope | Family | Memory | Path |".to_string(),
935 "|---|---|---|---|---|".to_string(),
936 ];
937 for p in architecture_catalog() {
938 let path = match p.path {
939 ArchPath::GenericGqa { .. } => "generic-gqa",
940 ArchPath::TestFixture { .. } => "test-fixture",
941 ArchPath::DedicatedOnly { .. } => "dedicated",
942 ArchPath::Deferred { .. } => "deferred",
943 };
944 lines.push(format!(
945 "| `{}` | {:?} | {:?} | {:?} | {} |",
946 p.gguf_name, p.scope, p.family, p.memory, path
947 ));
948 }
949 lines.push(String::new());
950 lines.join("\n")
951}
952
953#[cfg(test)]
954mod tests {
955 use super::*;
956
957 #[test]
958 fn known_mainstream_families_resolve() {
959 assert_eq!(
960 resolve_architecture("llama"),
961 Some(ArchPath::GenericGqa {
962 rope: RopeLayout::Norm
963 })
964 );
965 assert_eq!(
966 resolve_architecture("qwen2moe"),
967 Some(ArchPath::GenericGqa {
968 rope: RopeLayout::Neox
969 })
970 );
971 assert_eq!(
972 resolve_architecture("mistral"),
973 Some(ArchPath::GenericGqa {
974 rope: RopeLayout::Neox
975 })
976 );
977 assert_eq!(
978 resolve_architecture("yi"),
979 Some(ArchPath::GenericGqa {
980 rope: RopeLayout::Neox
981 })
982 );
983 assert_eq!(
984 resolve_architecture("mixtral"),
985 Some(ArchPath::GenericGqa {
986 rope: RopeLayout::Neox
987 })
988 );
989 assert_eq!(
990 resolve_architecture("phi3"),
991 Some(ArchPath::GenericGqa {
992 rope: RopeLayout::Neox
993 })
994 );
995 assert_eq!(
996 resolve_architecture("phi4"),
997 Some(ArchPath::GenericGqa {
998 rope: RopeLayout::Neox
999 })
1000 );
1001 assert_eq!(
1002 resolve_profile("phi4").map(|p| p.family),
1003 Some(DecoderFamily::PhiFamily)
1004 );
1005 assert_eq!(
1006 resolve_architecture("gemma3"),
1007 Some(ArchPath::GenericGqa {
1008 rope: RopeLayout::Neox
1009 })
1010 );
1011 for arch in ["gemma4", "gemma4-assistant"] {
1012 assert!(
1013 matches!(
1014 resolve_architecture(arch),
1015 Some(ArchPath::DedicatedOnly { .. })
1016 ),
1017 "{arch} uses dedicated Gemma4 engine"
1018 );
1019 assert_eq!(
1020 resolve_profile(arch).map(|p| p.family),
1021 Some(DecoderFamily::GemmaFamily)
1022 );
1023 }
1024 assert!(matches!(
1025 resolve_architecture("gemma3n"),
1026 Some(ArchPath::DedicatedOnly { .. })
1027 ));
1028 assert_eq!(
1029 resolve_architecture("deepseek"),
1030 Some(ArchPath::GenericGqa {
1031 rope: RopeLayout::Norm
1032 })
1033 );
1034 assert_eq!(
1035 resolve_profile("qwen3").map(|p| p.qk_norm),
1036 Some(QkNormStyle::PerHead)
1037 );
1038 }
1039
1040 #[test]
1041 fn deepseek2_is_dedicated_mla_not_generic() {
1042 assert!(matches!(
1043 resolve_architecture("deepseek2"),
1044 Some(ArchPath::DedicatedOnly { .. })
1045 ));
1046 }
1047
1048 #[test]
1049 fn unknown_architecture_is_none() {
1050 assert_eq!(resolve_architecture("totally-unknown-arch"), None);
1051 assert!(matches!(
1053 resolve_architecture("t5"),
1054 Some(ArchPath::DedicatedOnly { .. })
1055 ));
1056 }
1057
1058 #[test]
1059 fn dedicated_paths_are_not_generic() {
1060 assert!(matches!(
1061 resolve_architecture("glm-dsa"),
1062 Some(ArchPath::DedicatedOnly { .. })
1063 ));
1064 assert!(matches!(
1065 resolve_architecture("deepseek4"),
1066 Some(ArchPath::DedicatedOnly { .. })
1067 ));
1068 for arch in ["minimax-m2", "minimax-m3"] {
1069 assert!(
1070 matches!(
1071 resolve_architecture(arch),
1072 Some(ArchPath::DedicatedOnly {
1073 reason: "MiniMax 256-expert sigmoid MoE + MTP — see minimax_engine.rs"
1074 })
1075 ),
1076 "{arch} must fail closed, not silent generic GQA"
1077 );
1078 }
1079 assert!(
1080 matches!(
1081 resolve_architecture("llama4"),
1082 Some(ArchPath::DedicatedOnly {
1083 reason: "llama4 MoE + non-GQA attn — see llama4_engine.rs tensor list"
1084 })
1085 ),
1086 "llama4 must fail closed, not silent generic GQA"
1087 );
1088 assert!(matches!(
1089 resolve_architecture("glm4"),
1090 Some(ArchPath::DedicatedOnly { .. })
1091 ));
1092 assert!(matches!(
1093 resolve_architecture("glm4moe"),
1094 Some(ArchPath::DedicatedOnly { .. })
1095 ));
1096 }
1097
1098 #[test]
1099 fn test_fixtures_remain_loadable() {
1100 for arch in ["ferroxtest", "ferroxtestmoe", "ferroxtestmixed"] {
1101 assert!(matches!(
1102 resolve_architecture(arch),
1103 Some(ArchPath::TestFixture { .. })
1104 ));
1105 }
1106 }
1107
1108 #[test]
1109 fn catalog_has_unique_names() {
1110 let mut seen = std::collections::HashSet::new();
1111 for p in architecture_catalog() {
1112 assert!(
1113 seen.insert(p.gguf_name),
1114 "duplicate arch name {}",
1115 p.gguf_name
1116 );
1117 }
1118 }
1119
1120 #[test]
1121 fn gemma_family_does_not_fail_closed_on_softcap_keys() {
1122 assert!(unsupported_feature_keys("gemma3").is_empty());
1123 assert!(!unsupported_feature_keys("llama").is_empty());
1124 }
1125
1126 #[test]
1132 fn architectures_with_a_different_residual_topology_are_refused() {
1133 for arch in [
1134 "command-r",
1135 "cohere2",
1136 "cohere2moe",
1137 "falcon",
1138 "gptneox",
1139 "phi2",
1140 "plamo",
1141 "minicpm",
1142 ] {
1143 match resolve_architecture(arch) {
1144 Some(ArchPath::DedicatedOnly { reason }) => {
1145 assert!(!reason.is_empty(), "{arch} must say why");
1146 }
1147 other => panic!("{arch} must be refused, got {other:?}"),
1148 }
1149 }
1150 for arch in ["phi3", "phimoe", "plamo3", "starcoder2", "nemotron"] {
1153 assert!(
1154 matches!(
1155 resolve_architecture(arch),
1156 Some(ArchPath::GenericGqa { .. })
1157 ),
1158 "{arch} must stay generic"
1159 );
1160 }
1161 }
1162
1163 #[test]
1167 fn no_architecture_is_listed_twice() {
1168 let mut seen = std::collections::HashSet::new();
1169 for p in architecture_catalog() {
1170 assert!(seen.insert(p.gguf_name), "{} listed twice", p.gguf_name);
1171 }
1172 }
1173}