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 "minicpm",
204 "xverse",
205 "command-r",
206 "cohere2",
207 "cohere2moe",
208 "olmo",
209 "arctic",
210 "deepseek",
211 "chatglm",
212 "granite",
213 "granitemoe",
214 "granite-moe",
215 "mistral3",
216 "maincoder",
217 "smollm3",
218 "arcee",
219 "ernie4_5",
220 "ernie4_5-moe",
221 "seed_oss",
222 "dots1",
223 "bailingmoe",
224 "bailingmoe2",
225 "orion",
226 "codeshell",
227 "openelm",
228 "nemotron",
229 "exaone",
230 "exaone-moe",
231 "apertus",
232 "laguna",
233 "nanbeige",
234 "talkie",
235 "step35",
236 "mimo2",
237 "mellum",
238 "hunyuan-dense",
239 "hunyuan-moe",
240 "afmoe",
241 "grovemoe",
242 "smallthinker",
243 "plamo",
244 "plamo3",
245 "minicpm3",
246 "starcoder2",
247 "plm",
248 ] {
249 v.push(gqa_norm(n));
250 }
251 for n in [
252 "olmoe", "qwen", "qwen2", "qwen2moe", "falcon", "gptneox", "stablelm", "mistral",
253 "mixtral", "olmo2", "gpt2", "bloom", "mpt", "refact", "bitnet", "jais", "jais2",
254 "grok", "dbrx", "exaone4", "yi",
255 "gpt-oss",
261 ] {
262 v.push(gqa_neox(n));
263 }
264 v.push(prof(
265 "qwen3",
266 TextGeneration,
267 Qwen3Family,
268 KvGqa,
269 Neox,
270 ArchPath::GenericGqa { rope: Neox },
271 PerHead,
272 ));
273 v.push(prof(
274 "qwen3moe",
275 TextGeneration,
276 Qwen3Family,
277 KvGqa,
278 Neox,
279 ArchPath::GenericGqa { rope: Neox },
280 PerHead,
281 ));
282 v.push(prof(
283 "gemma",
284 TextGeneration,
285 GemmaFamily,
286 KvGqa,
287 Neox,
288 ArchPath::GenericGqa { rope: Neox },
289 PerHead,
290 ));
291 v.push(prof(
292 "gemma2",
293 TextGeneration,
294 GemmaFamily,
295 KvIswa,
296 Neox,
297 ArchPath::GenericGqa { rope: Neox },
298 PerHead,
299 ));
300 v.push(prof(
301 "gemma3",
302 TextGeneration,
303 GemmaFamily,
304 KvIswa,
305 Neox,
306 ArchPath::GenericGqa { rope: Neox },
307 PerHead,
308 ));
309 for n in ["gemma4", "gemma4-assistant"] {
313 v.push(prof(
314 n,
315 TextGeneration,
316 GemmaFamily,
317 KvIswa,
318 Neox,
319 ArchPath::DedicatedOnly {
320 reason: "use load_gemma4_engine_from_path / ServedEngine::Gemma4",
321 },
322 PerHead,
323 ));
324 }
325 for (n, fam) in [("phi2", PhiFamily), ("phi3", PhiFamily), ("phimoe", PhiFamily)] {
326 v.push(prof(
327 n,
328 TextGeneration,
329 fam,
330 KvGqa,
331 Neox,
332 ArchPath::GenericGqa { rope: Neox },
333 WholeVector,
334 ));
335 }
336 v.push(prof(
341 "phi4",
342 TextGeneration,
343 PhiFamily,
344 KvGqa,
345 Neox,
346 ArchPath::GenericGqa { rope: Neox },
347 WholeVector,
348 ));
349 v.push(prof(
352 "llama4",
353 TextGeneration,
354 Dedicated,
355 KvGqa,
356 Norm,
357 ArchPath::DedicatedOnly {
358 reason: "llama4 MoE + non-GQA attn — see llama4_engine.rs tensor list",
359 },
360 WholeVector,
361 ));
362 for n in ["minimax-m2", "minimax-m3"] {
364 v.push(prof(
365 n,
366 TextGeneration,
367 Dedicated,
368 KvGqa,
369 Norm,
370 ArchPath::DedicatedOnly {
371 reason: "MiniMax 256-expert sigmoid MoE + MTP — see minimax_engine.rs",
372 },
373 WholeVector,
374 ));
375 }
376 v.push(prof(
377 "deepseek2",
378 TextGeneration,
379 Mla,
380 KvMla,
381 Norm,
382 ArchPath::DedicatedOnly {
383 reason: "DeepSeek-2 MLA needs the MLA engine, not generic GQA",
384 },
385 WholeVector,
386 ));
387 v.push(prof(
388 "deepseek32",
389 TextGeneration,
390 Mla,
391 KvDsa,
392 Norm,
393 ArchPath::DedicatedOnly {
394 reason: "DeepSeek-3.2 DSA/MLA needs the dedicated sparse/MLA stack",
395 },
396 WholeVector,
397 ));
398 v.push(prof(
399 "mistral4",
400 TextGeneration,
401 Mla,
402 KvMla,
403 Norm,
404 ArchPath::DedicatedOnly {
405 reason: "mistral4 reuses DeepSeek-2 MLA loader/graph in llama.cpp",
406 },
407 WholeVector,
408 ));
409 v.push(dedicated(
410 "glm-dsa",
411 "use ferrox_models::glm52_decoder / glm52_gguf_loader (DSA), not the generic GQA Decoder",
412 ));
413 v.push(dedicated(
414 "glm4",
415 "use ferrox_models::glm52_decoder / glm52_gguf_loader, not the generic GQA Decoder",
416 ));
417 v.push(dedicated(
418 "glm4moe",
419 "use ferrox_models::glm52_decoder / glm52_gguf_loader, not the generic GQA Decoder",
420 ));
421 v.push(dedicated(
422 "deepseek4",
423 "DeepSeek V4 needs CSA/HCA + mHC assembly; generic GQA Decoder is not valid",
424 ));
425 v.push(dedicated(
426 "kimi-linear",
427 "use ferrox_models::kimi_decoder / kimi_loader, not the generic GQA Decoder",
428 ));
429 v.push(dedicated(
430 "kimi_k3",
431 "use ferrox_models::kimi_decoder / kimi_loader, not the generic GQA Decoder",
432 ));
433 for (n, rope) in [
434 ("jamba", Neox),
435 ("falcon-h1", Neox),
436 ("plamo2", Neox),
437 ("granitehybrid", Norm),
438 ("granite-hybrid", Norm),
439 ("lfm2", Neox),
440 ("lfm2moe", Neox),
441 ("nemotron_h", Neox),
442 ("nemotron_h_moe", Neox),
443 ("qwen3next", Neox),
444 ("qwen35", Neox),
445 ("qwen35moe", Neox),
446 ] {
447 let qk = if n.starts_with("qwen3") {
448 PerHead
449 } else {
450 WholeVector
451 };
452 v.push(prof(
453 n,
454 TextGeneration,
455 DecoderFamily::Hybrid,
456 MemoryKind::Hybrid,
457 rope,
458 ArchPath::DedicatedOnly {
459 reason: "hybrid attn+SSM/delta-net engine not yet on the serve path",
460 },
461 qk,
462 ));
463 }
464 for n in ["mamba", "mamba2", "rwkv6", "rwkv6qwen2", "rwkv7", "arwkv7"] {
465 v.push(prof(
466 n,
467 TextGeneration,
468 DecoderFamily::Recurrent,
469 MemoryKind::Recurrent,
470 Neox,
471 ArchPath::DedicatedOnly {
472 reason: "recurrent engine not yet on the serve path",
473 },
474 WholeVector,
475 ));
476 }
477 v.push(prof(
478 "t5",
479 TextGeneration,
480 EncoderDecoder,
481 None,
482 Neox,
483 ArchPath::DedicatedOnly {
484 reason: "T5 encoder-decoder engine not yet on the serve path",
485 },
486 WholeVector,
487 ));
488 for (n, scope, reason) in [
489 (
490 "t5encoder",
491 DeferredEncoderEmbedding,
492 "encoder-only; deferred from text-generation parity",
493 ),
494 ("bert", DeferredEncoderEmbedding, "encoder/embedding; deferred"),
495 (
496 "modern-bert",
497 DeferredEncoderEmbedding,
498 "encoder/embedding; deferred",
499 ),
500 (
501 "nomic-bert",
502 DeferredEncoderEmbedding,
503 "encoder/embedding; deferred",
504 ),
505 (
506 "nomic-bert-moe",
507 DeferredEncoderEmbedding,
508 "encoder/embedding; deferred",
509 ),
510 (
511 "neo-bert",
512 DeferredEncoderEmbedding,
513 "encoder/embedding; deferred",
514 ),
515 (
516 "jina-bert-v2",
517 DeferredEncoderEmbedding,
518 "encoder/embedding; deferred",
519 ),
520 (
521 "jina-bert-v3",
522 DeferredEncoderEmbedding,
523 "encoder/embedding; deferred",
524 ),
525 (
526 "eurobert",
527 DeferredEncoderEmbedding,
528 "encoder/embedding; deferred",
529 ),
530 (
531 "llama-embed",
532 DeferredEncoderEmbedding,
533 "embedding variant; deferred",
534 ),
535 (
536 "gemma-embedding",
537 DeferredEncoderEmbedding,
538 "embedding variant; deferred",
539 ),
540 (
541 "pangu-embedded",
542 DeferredEncoderEmbedding,
543 "embedding variant; deferred",
544 ),
545 ("yi-vl", DeferredMultimodal, "Yi vision-language; deferred"),
546 ("qwen2vl", DeferredMultimodal, "vision-language; deferred"),
547 ("qwen3vl", DeferredMultimodal, "vision-language; deferred"),
548 ("qwen3vlmoe", DeferredMultimodal, "vision-language; deferred"),
549 ("cogvlm", DeferredMultimodal, "vision-language; deferred"),
550 ("chameleon", DeferredMultimodal, "multimodal; deferred"),
551 ("hunyuan_vl", DeferredMultimodal, "vision-language; deferred"),
552 ("paddleocr", DeferredMultimodal, "OCR multimodal; deferred"),
553 ("hy_v3", DeferredMultimodal, "multimodal; deferred"),
554 ("deepseek2-ocr", DeferredMultimodal, "OCR multimodal; deferred"),
555 ("dream", DeferredDiffusion, "diffusion LM; deferred"),
556 ("llada", DeferredDiffusion, "diffusion LM; deferred"),
557 ("llada-moe", DeferredDiffusion, "diffusion LM; deferred"),
558 ("rnd1", DeferredDiffusion, "diffusion LM; deferred"),
559 (
560 "wavtokenizer-dec",
561 DeferredAudio,
562 "audio tokenizer; deferred",
563 ),
564 (
565 "eagle3",
566 EnumOnly,
567 "speculative draft head; not a standalone decoder target",
568 ),
569 (
570 "dflash",
571 EnumOnly,
572 "speculative draft head; not a standalone decoder target",
573 ),
574 ("clip", EnumOnly, "quantize dummy only"),
575 ("gptj", EnumOnly, "enum-only in llama.cpp factory gap"),
576 ("(unknown)", EnumOnly, "llama.cpp unknown sentinel"),
577 ] {
578 v.push(deferred_scope(n, scope, reason));
579 }
580 v.push(prof(
581 "gemma3n",
582 TextGeneration,
583 GemmaFamily,
584 KvIswa,
585 Neox,
586 ArchPath::DedicatedOnly {
587 reason: "gemma3n AltUp/Laurel tensors not implemented in the generic decoder",
588 },
589 PerHead,
590 ));
591 for n in ["ferroxtest", "ferroxtestmoe", "ferroxtestmixed"] {
592 v.push(prof(
593 n,
594 TextGeneration,
595 TestFixture,
596 KvGqa,
597 Neox,
598 ArchPath::TestFixture { rope: Neox },
599 WholeVector,
600 ));
601 }
602 v
603 })
604 .as_slice()
605}
606
607pub fn resolve_profile(arch: &str) -> Option<&'static ArchProfile> {
609 architecture_catalog().iter().find(|p| p.gguf_name == arch)
610}
611
612pub fn resolve_architecture(arch: &str) -> Option<ArchPath> {
615 resolve_profile(arch).map(|p| p.path)
616}
617
618pub fn default_swa_pattern(arch: &str) -> Option<usize> {
636 match arch {
637 "gpt-oss" => Some(2),
639 "gemma2" => Some(2),
641 "gemma3" | "gemma3n" => Some(6),
643 "cohere2" | "exaone4" | "olmo2" => Some(4),
645 _ => None,
646 }
647}
648
649pub fn swa_rope_base_follows_model(arch: &str) -> bool {
661 matches!(
662 arch,
663 "afmoe"
664 | "cohere2"
665 | "cohere2moe"
666 | "dflash"
667 | "exaone-moe"
668 | "exaone4"
669 | "gemma2"
670 | "laguna"
671 | "llama4"
672 | "mellum"
673 | "olmo2"
674 | "gpt-oss"
675 | "smallthinker"
676 )
677}
678
679pub fn unsupported_feature_keys(arch: &str) -> Vec<(String, &'static str)> {
683 let profile = resolve_profile(arch);
684 if matches!(profile.map(|p| p.family), Some(DecoderFamily::GemmaFamily)) {
686 return Vec::new();
687 }
688 let key = |suffix: &str| format!("{arch}.{suffix}");
689 vec![
690 (
691 key("attention.logit_softcapping"),
692 "attention logit soft-capping (Gemma 2+); not implemented in the generic decoder",
693 ),
694 (
695 key("final_logit_softcapping"),
696 "final logit soft-capping (Gemma 2+); not implemented in the generic decoder",
697 ),
698 (
699 key("attention.sliding_window_pattern"),
700 "alternating sliding-window pattern (Gemma 2+); not implemented in the generic decoder",
701 ),
702 ]
703}
704
705pub fn coverage_report_markdown() -> String {
707 let mut lines = vec![
708 "# Architecture coverage manifest".to_string(),
709 String::new(),
710 "Generated from `ferrox_models::capability::architecture_catalog`.".to_string(),
711 "Source of truth for names: pinned llama.cpp `LLM_ARCH_NAMES`.".to_string(),
712 String::new(),
713 "| GGUF arch | Scope | Family | Memory | Path |".to_string(),
714 "|---|---|---|---|---|".to_string(),
715 ];
716 for p in architecture_catalog() {
717 let path = match p.path {
718 ArchPath::GenericGqa { .. } => "generic-gqa",
719 ArchPath::TestFixture { .. } => "test-fixture",
720 ArchPath::DedicatedOnly { .. } => "dedicated",
721 ArchPath::Deferred { .. } => "deferred",
722 };
723 lines.push(format!(
724 "| `{}` | {:?} | {:?} | {:?} | {} |",
725 p.gguf_name, p.scope, p.family, p.memory, path
726 ));
727 }
728 lines.push(String::new());
729 lines.join("\n")
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735
736 #[test]
737 fn known_mainstream_families_resolve() {
738 assert_eq!(
739 resolve_architecture("llama"),
740 Some(ArchPath::GenericGqa {
741 rope: RopeLayout::Norm
742 })
743 );
744 assert_eq!(
745 resolve_architecture("qwen2moe"),
746 Some(ArchPath::GenericGqa {
747 rope: RopeLayout::Neox
748 })
749 );
750 assert_eq!(
751 resolve_architecture("mistral"),
752 Some(ArchPath::GenericGqa {
753 rope: RopeLayout::Neox
754 })
755 );
756 assert_eq!(
757 resolve_architecture("yi"),
758 Some(ArchPath::GenericGqa {
759 rope: RopeLayout::Neox
760 })
761 );
762 assert_eq!(
763 resolve_architecture("mixtral"),
764 Some(ArchPath::GenericGqa {
765 rope: RopeLayout::Neox
766 })
767 );
768 assert_eq!(
769 resolve_architecture("phi3"),
770 Some(ArchPath::GenericGqa {
771 rope: RopeLayout::Neox
772 })
773 );
774 assert_eq!(
775 resolve_architecture("phi4"),
776 Some(ArchPath::GenericGqa {
777 rope: RopeLayout::Neox
778 })
779 );
780 assert_eq!(
781 resolve_profile("phi4").map(|p| p.family),
782 Some(DecoderFamily::PhiFamily)
783 );
784 assert_eq!(
785 resolve_architecture("gemma3"),
786 Some(ArchPath::GenericGqa {
787 rope: RopeLayout::Neox
788 })
789 );
790 for arch in ["gemma4", "gemma4-assistant"] {
791 assert!(
792 matches!(
793 resolve_architecture(arch),
794 Some(ArchPath::DedicatedOnly { .. })
795 ),
796 "{arch} uses dedicated Gemma4 engine"
797 );
798 assert_eq!(
799 resolve_profile(arch).map(|p| p.family),
800 Some(DecoderFamily::GemmaFamily)
801 );
802 }
803 assert!(matches!(
804 resolve_architecture("gemma3n"),
805 Some(ArchPath::DedicatedOnly { .. })
806 ));
807 assert_eq!(
808 resolve_architecture("deepseek"),
809 Some(ArchPath::GenericGqa {
810 rope: RopeLayout::Norm
811 })
812 );
813 assert_eq!(
814 resolve_profile("qwen3").map(|p| p.qk_norm),
815 Some(QkNormStyle::PerHead)
816 );
817 }
818
819 #[test]
820 fn deepseek2_is_dedicated_mla_not_generic() {
821 assert!(matches!(
822 resolve_architecture("deepseek2"),
823 Some(ArchPath::DedicatedOnly { .. })
824 ));
825 }
826
827 #[test]
828 fn unknown_architecture_is_none() {
829 assert_eq!(resolve_architecture("totally-unknown-arch"), None);
830 assert!(matches!(
832 resolve_architecture("t5"),
833 Some(ArchPath::DedicatedOnly { .. })
834 ));
835 }
836
837 #[test]
838 fn dedicated_paths_are_not_generic() {
839 assert!(matches!(
840 resolve_architecture("glm-dsa"),
841 Some(ArchPath::DedicatedOnly { .. })
842 ));
843 assert!(matches!(
844 resolve_architecture("deepseek4"),
845 Some(ArchPath::DedicatedOnly { .. })
846 ));
847 for arch in ["minimax-m2", "minimax-m3"] {
848 assert!(
849 matches!(
850 resolve_architecture(arch),
851 Some(ArchPath::DedicatedOnly {
852 reason: "MiniMax 256-expert sigmoid MoE + MTP — see minimax_engine.rs"
853 })
854 ),
855 "{arch} must fail closed, not silent generic GQA"
856 );
857 }
858 assert!(
859 matches!(
860 resolve_architecture("llama4"),
861 Some(ArchPath::DedicatedOnly {
862 reason: "llama4 MoE + non-GQA attn — see llama4_engine.rs tensor list"
863 })
864 ),
865 "llama4 must fail closed, not silent generic GQA"
866 );
867 assert!(matches!(
868 resolve_architecture("glm4"),
869 Some(ArchPath::DedicatedOnly { .. })
870 ));
871 assert!(matches!(
872 resolve_architecture("glm4moe"),
873 Some(ArchPath::DedicatedOnly { .. })
874 ));
875 }
876
877 #[test]
878 fn test_fixtures_remain_loadable() {
879 for arch in ["ferroxtest", "ferroxtestmoe", "ferroxtestmixed"] {
880 assert!(matches!(
881 resolve_architecture(arch),
882 Some(ArchPath::TestFixture { .. })
883 ));
884 }
885 }
886
887 #[test]
888 fn catalog_has_unique_names() {
889 let mut seen = std::collections::HashSet::new();
890 for p in architecture_catalog() {
891 assert!(
892 seen.insert(p.gguf_name),
893 "duplicate arch name {}",
894 p.gguf_name
895 );
896 }
897 }
898
899 #[test]
900 fn gemma_family_does_not_fail_closed_on_softcap_keys() {
901 assert!(unsupported_feature_keys("gemma3").is_empty());
902 assert!(!unsupported_feature_keys("llama").is_empty());
903 }
904}