use crate::config::RopeLayout;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArchScope {
TextGeneration,
DeferredEncoderEmbedding,
DeferredMultimodal,
DeferredDiffusion,
DeferredAudio,
EnumOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecoderFamily {
StandardGqa,
Qwen3Family,
GemmaFamily,
PhiFamily,
Mla,
Hybrid,
Recurrent,
EncoderDecoder,
Dedicated,
TestFixture,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemoryKind {
KvGqa,
KvIswa,
KvMla,
KvDsa,
KvDsv4,
Recurrent,
Hybrid,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum QkNormStyle {
#[default]
WholeVector,
PerHead,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArchPath {
GenericGqa { rope: RopeLayout },
TestFixture { rope: RopeLayout },
DedicatedOnly { reason: &'static str },
Deferred { reason: &'static str },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ArchProfile {
pub gguf_name: &'static str,
pub scope: ArchScope,
pub family: DecoderFamily,
pub memory: MemoryKind,
pub rope: RopeLayout,
pub path: ArchPath,
pub qk_norm: QkNormStyle,
}
fn prof(
name: &'static str,
scope: ArchScope,
fam: DecoderFamily,
mem: MemoryKind,
rope: RopeLayout,
path: ArchPath,
qk: QkNormStyle,
) -> ArchProfile {
ArchProfile {
gguf_name: name,
scope,
family: fam,
memory: mem,
rope,
path,
qk_norm: qk,
}
}
fn gqa_norm(name: &'static str) -> ArchProfile {
prof(
name,
ArchScope::TextGeneration,
DecoderFamily::StandardGqa,
MemoryKind::KvGqa,
RopeLayout::Norm,
ArchPath::GenericGqa {
rope: RopeLayout::Norm,
},
QkNormStyle::WholeVector,
)
}
fn gqa_neox(name: &'static str) -> ArchProfile {
prof(
name,
ArchScope::TextGeneration,
DecoderFamily::StandardGqa,
MemoryKind::KvGqa,
RopeLayout::Neox,
ArchPath::GenericGqa {
rope: RopeLayout::Neox,
},
QkNormStyle::WholeVector,
)
}
fn dedicated(name: &'static str, reason: &'static str) -> ArchProfile {
prof(
name,
ArchScope::TextGeneration,
DecoderFamily::Dedicated,
MemoryKind::KvGqa,
RopeLayout::Norm,
ArchPath::DedicatedOnly { reason },
QkNormStyle::WholeVector,
)
}
fn deferred_scope(name: &'static str, scope: ArchScope, reason: &'static str) -> ArchProfile {
prof(
name,
scope,
DecoderFamily::StandardGqa,
MemoryKind::None,
RopeLayout::Neox,
ArchPath::Deferred { reason },
QkNormStyle::WholeVector,
)
}
pub fn architecture_catalog() -> &'static [ArchProfile] {
use std::sync::OnceLock;
use ArchScope::*;
use DecoderFamily::*;
use MemoryKind::*;
use QkNormStyle::*;
use RopeLayout::*;
static CAT: OnceLock<Vec<ArchProfile>> = OnceLock::new();
CAT.get_or_init(|| {
let mut v = Vec::with_capacity(160);
for n in [
"llama",
"deci",
"baichuan",
"starcoder",
"internlm2",
"xverse",
"olmo",
"arctic",
"deepseek",
"chatglm",
"granite",
"granitemoe",
"granite-moe",
"mistral3",
"maincoder",
"smollm3",
"arcee",
"ernie4_5",
"ernie4_5-moe",
"bailingmoe",
"nanbeige",
"plm",
] {
v.push(gqa_norm(n));
}
for n in [
"olmoe", "qwen", "qwen2", "qwen2moe", "stablelm", "mistral",
"mixtral", "olmo2", "gpt2", "bloom", "mpt", "refact", "bitnet", "jais", "jais2",
"grok", "dbrx", "exaone4", "yi",
"gpt-oss",
"afmoe",
"apertus",
"bailingmoe2",
"codeshell",
"dots1",
"exaone",
"exaone-moe",
"grovemoe",
"hunyuan-dense",
"hunyuan-moe",
"laguna",
"mellum",
"mimo2",
"minicpm3",
"nemotron",
"openelm",
"orion",
"plamo3",
"seed_oss",
"smallthinker",
"starcoder2",
"step35",
"talkie",
] {
v.push(gqa_neox(n));
}
v.push(prof(
"qwen3",
TextGeneration,
Qwen3Family,
KvGqa,
Neox,
ArchPath::GenericGqa { rope: Neox },
PerHead,
));
v.push(prof(
"qwen3moe",
TextGeneration,
Qwen3Family,
KvGqa,
Neox,
ArchPath::GenericGqa { rope: Neox },
PerHead,
));
v.push(prof(
"gemma",
TextGeneration,
GemmaFamily,
KvGqa,
Neox,
ArchPath::GenericGqa { rope: Neox },
PerHead,
));
v.push(prof(
"gemma2",
TextGeneration,
GemmaFamily,
KvIswa,
Neox,
ArchPath::GenericGqa { rope: Neox },
PerHead,
));
v.push(prof(
"gemma3",
TextGeneration,
GemmaFamily,
KvIswa,
Neox,
ArchPath::GenericGqa { rope: Neox },
PerHead,
));
for n in ["gemma4", "gemma4-assistant"] {
v.push(prof(
n,
TextGeneration,
GemmaFamily,
KvIswa,
Neox,
ArchPath::DedicatedOnly {
reason: "use load_gemma4_engine_from_path / ServedEngine::Gemma4",
},
PerHead,
));
}
const PARALLEL_RESIDUAL: &str =
"parallel attention+FFN residual -- llama.cpp feeds both branches the *same* \
normed input and sums `inpL + attn_out + ffn_out` once; the generic decoder \
computes the sequential form, which is a different graph";
for (n, rope, fam) in [
("command-r", Norm, StandardGqa),
("cohere2", Norm, StandardGqa),
("cohere2moe", Norm, StandardGqa),
("falcon", Neox, StandardGqa),
("gptneox", Neox, StandardGqa),
("phi2", Neox, PhiFamily),
("plamo", Neox, StandardGqa),
] {
v.push(prof(
n,
TextGeneration,
fam,
KvGqa,
rope,
ArchPath::DedicatedOnly {
reason: PARALLEL_RESIDUAL,
},
WholeVector,
));
}
v.push(prof(
"minicpm",
TextGeneration,
StandardGqa,
KvGqa,
Norm,
ArchPath::DedicatedOnly {
reason: "unconditional embedding/residual/logit multipliers that llama.cpp \
applies even when the GGUF omits every key; not applied by the \
generic decoder",
},
WholeVector,
));
for (n, fam) in [("phi3", PhiFamily), ("phimoe", PhiFamily)] {
v.push(prof(
n,
TextGeneration,
fam,
KvGqa,
Neox,
ArchPath::GenericGqa { rope: Neox },
WholeVector,
));
}
v.push(prof(
"phi4",
TextGeneration,
PhiFamily,
KvGqa,
Neox,
ArchPath::GenericGqa { rope: Neox },
WholeVector,
));
v.push(prof(
"llama4",
TextGeneration,
Dedicated,
KvGqa,
Norm,
ArchPath::DedicatedOnly {
reason: "llama4 MoE + non-GQA attn — see llama4_engine.rs tensor list",
},
WholeVector,
));
for n in ["minimax-m2", "minimax-m3"] {
v.push(prof(
n,
TextGeneration,
Dedicated,
KvGqa,
Neox,
ArchPath::DedicatedOnly {
reason: "MiniMax 256-expert sigmoid MoE + MTP — see minimax_engine.rs",
},
WholeVector,
));
}
v.push(prof(
"deepseek2",
TextGeneration,
Mla,
KvMla,
Norm,
ArchPath::DedicatedOnly {
reason: "DeepSeek-2 MLA needs the MLA engine, not generic GQA",
},
WholeVector,
));
v.push(prof(
"deepseek32",
TextGeneration,
Mla,
KvDsa,
Norm,
ArchPath::DedicatedOnly {
reason: "DeepSeek-3.2 DSA/MLA needs the dedicated sparse/MLA stack",
},
WholeVector,
));
v.push(prof(
"mistral4",
TextGeneration,
Mla,
KvMla,
Norm,
ArchPath::DedicatedOnly {
reason: "mistral4 reuses DeepSeek-2 MLA loader/graph in llama.cpp",
},
WholeVector,
));
v.push(dedicated(
"glm-dsa",
"use ferrox_models::glm52_decoder / glm52_gguf_loader (DSA), not the generic GQA Decoder",
));
v.push(dedicated(
"glm4",
"use ferrox_models::glm52_decoder / glm52_gguf_loader, not the generic GQA Decoder",
));
v.push(dedicated(
"glm4moe",
"use ferrox_models::glm52_decoder / glm52_gguf_loader, not the generic GQA Decoder",
));
v.push(dedicated(
"deepseek4",
"DeepSeek V4 needs CSA/HCA + mHC assembly; generic GQA Decoder is not valid",
));
v.push(dedicated(
"kimi-linear",
"use ferrox_models::kimi_decoder / kimi_loader, not the generic GQA Decoder",
));
v.push(dedicated(
"kimi_k3",
"use ferrox_models::kimi_decoder / kimi_loader, not the generic GQA Decoder",
));
for (n, rope) in [
("jamba", Neox),
("falcon-h1", Neox),
("plamo2", Neox),
("granitehybrid", Norm),
("granite-hybrid", Norm),
("lfm2", Neox),
("lfm2moe", Neox),
("nemotron_h", Neox),
("nemotron_h_moe", Neox),
("qwen3next", Neox),
("qwen35", Neox),
("qwen35moe", Neox),
] {
let qk = if n.starts_with("qwen3") {
PerHead
} else {
WholeVector
};
v.push(prof(
n,
TextGeneration,
DecoderFamily::Hybrid,
MemoryKind::Hybrid,
rope,
ArchPath::DedicatedOnly {
reason: "hybrid attn+SSM/delta-net engine not yet on the serve path",
},
qk,
));
}
for n in ["mamba", "mamba2", "rwkv6", "rwkv6qwen2", "rwkv7", "arwkv7"] {
v.push(prof(
n,
TextGeneration,
DecoderFamily::Recurrent,
MemoryKind::Recurrent,
Neox,
ArchPath::DedicatedOnly {
reason: "recurrent engine not yet on the serve path",
},
WholeVector,
));
}
v.push(prof(
"t5",
TextGeneration,
EncoderDecoder,
None,
Neox,
ArchPath::DedicatedOnly {
reason: "T5 encoder-decoder engine not yet on the serve path",
},
WholeVector,
));
for (n, scope, reason) in [
(
"t5encoder",
DeferredEncoderEmbedding,
"encoder-only; deferred from text-generation parity",
),
("bert", DeferredEncoderEmbedding, "encoder/embedding; deferred"),
(
"modern-bert",
DeferredEncoderEmbedding,
"encoder/embedding; deferred",
),
(
"nomic-bert",
DeferredEncoderEmbedding,
"encoder/embedding; deferred",
),
(
"nomic-bert-moe",
DeferredEncoderEmbedding,
"encoder/embedding; deferred",
),
(
"neo-bert",
DeferredEncoderEmbedding,
"encoder/embedding; deferred",
),
(
"jina-bert-v2",
DeferredEncoderEmbedding,
"encoder/embedding; deferred",
),
(
"jina-bert-v3",
DeferredEncoderEmbedding,
"encoder/embedding; deferred",
),
(
"eurobert",
DeferredEncoderEmbedding,
"encoder/embedding; deferred",
),
(
"llama-embed",
DeferredEncoderEmbedding,
"embedding variant; deferred",
),
(
"gemma-embedding",
DeferredEncoderEmbedding,
"embedding variant; deferred",
),
(
"pangu-embedded",
DeferredEncoderEmbedding,
"embedding variant; deferred",
),
("yi-vl", DeferredMultimodal, "Yi vision-language; deferred"),
("qwen2vl", DeferredMultimodal, "vision-language; deferred"),
("qwen3vl", DeferredMultimodal, "vision-language; deferred"),
("qwen3vlmoe", DeferredMultimodal, "vision-language; deferred"),
("cogvlm", DeferredMultimodal, "vision-language; deferred"),
("chameleon", DeferredMultimodal, "multimodal; deferred"),
("hunyuan_vl", DeferredMultimodal, "vision-language; deferred"),
("paddleocr", DeferredMultimodal, "OCR multimodal; deferred"),
("hy_v3", DeferredMultimodal, "multimodal; deferred"),
("deepseek2-ocr", DeferredMultimodal, "OCR multimodal; deferred"),
("dream", DeferredDiffusion, "diffusion LM; deferred"),
("llada", DeferredDiffusion, "diffusion LM; deferred"),
("llada-moe", DeferredDiffusion, "diffusion LM; deferred"),
("rnd1", DeferredDiffusion, "diffusion LM; deferred"),
(
"wavtokenizer-dec",
DeferredAudio,
"audio tokenizer; deferred",
),
(
"eagle3",
EnumOnly,
"speculative draft head; not a standalone decoder target",
),
(
"dflash",
EnumOnly,
"speculative draft head; not a standalone decoder target",
),
("clip", EnumOnly, "quantize dummy only"),
("gptj", EnumOnly, "enum-only in llama.cpp factory gap"),
("(unknown)", EnumOnly, "llama.cpp unknown sentinel"),
] {
v.push(deferred_scope(n, scope, reason));
}
v.push(prof(
"gemma3n",
TextGeneration,
GemmaFamily,
KvIswa,
Neox,
ArchPath::DedicatedOnly {
reason: "gemma3n AltUp/Laurel tensors not implemented in the generic decoder",
},
PerHead,
));
for n in ["ferroxtest", "ferroxtestmoe", "ferroxtestmixed"] {
v.push(prof(
n,
TextGeneration,
TestFixture,
KvGqa,
Neox,
ArchPath::TestFixture { rope: Neox },
WholeVector,
));
}
v
})
.as_slice()
}
pub fn resolve_profile(arch: &str) -> Option<&'static ArchProfile> {
architecture_catalog().iter().find(|p| p.gguf_name == arch)
}
pub fn resolve_architecture(arch: &str) -> Option<ArchPath> {
resolve_profile(arch).map(|p| p.path)
}
pub fn default_swa_pattern(arch: &str) -> Option<usize> {
match arch {
"gpt-oss" => Some(2),
"gemma2" => Some(2),
"gemma3" | "gemma3n" => Some(6),
"cohere2" | "exaone4" | "olmo2" => Some(4),
_ => None,
}
}
pub fn swa_rope_base_follows_model(arch: &str) -> bool {
matches!(
arch,
"afmoe"
| "cohere2"
| "cohere2moe"
| "dflash"
| "exaone-moe"
| "exaone4"
| "gemma2"
| "laguna"
| "llama4"
| "mellum"
| "olmo2"
| "gpt-oss"
| "smallthinker"
)
}
pub fn unsupported_feature_keys(arch: &str) -> Vec<(String, &'static str)> {
let profile = resolve_profile(arch);
if matches!(profile.map(|p| p.family), Some(DecoderFamily::GemmaFamily)) {
return Vec::new();
}
let key = |suffix: &str| format!("{arch}.{suffix}");
vec![
(
key("attention.logit_softcapping"),
"attention logit soft-capping (Gemma 2+); not implemented in the generic decoder",
),
(
key("final_logit_softcapping"),
"final logit soft-capping (Gemma 2+); not implemented in the generic decoder",
),
(
key("attention.sliding_window_pattern"),
"alternating sliding-window pattern (Gemma 2+); not implemented in the generic decoder",
),
]
}
pub fn unsupported_scaling_keys(arch: &str) -> Vec<(String, &'static str, f32)> {
let profile = resolve_profile(arch);
if matches!(profile.map(|p| p.family), Some(DecoderFamily::GemmaFamily)) {
return Vec::new();
}
let key = |suffix: &str| format!("{arch}.{suffix}");
vec![
(
key("logit_scale"),
"logit multiplier (Granite / Command-R `logits_scaling`); not applied by the generic decoder",
1.0,
),
(
key("residual_scale"),
"residual multiplier (Granite `residual_multiplier`); not applied by the generic decoder",
1.0,
),
(
key("embedding_scale"),
"embedding multiplier (Granite / MiniCPM `embedding_multiplier`); the generic decoder only scales embeddings for the Gemma family",
1.0,
),
(
key("attention.scale"),
"explicit attention score scale (Granite `attention_multiplier`); the generic decoder always uses 1/sqrt(head_dim)",
0.0,
),
]
}
pub fn coverage_report_markdown() -> String {
let mut lines = vec![
"# Architecture coverage manifest".to_string(),
String::new(),
"Generated from `ferrox_models::capability::architecture_catalog`.".to_string(),
"Source of truth for names: pinned llama.cpp `LLM_ARCH_NAMES`.".to_string(),
String::new(),
"| GGUF arch | Scope | Family | Memory | Path |".to_string(),
"|---|---|---|---|---|".to_string(),
];
for p in architecture_catalog() {
let path = match p.path {
ArchPath::GenericGqa { .. } => "generic-gqa",
ArchPath::TestFixture { .. } => "test-fixture",
ArchPath::DedicatedOnly { .. } => "dedicated",
ArchPath::Deferred { .. } => "deferred",
};
lines.push(format!(
"| `{}` | {:?} | {:?} | {:?} | {} |",
p.gguf_name, p.scope, p.family, p.memory, path
));
}
lines.push(String::new());
lines.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_mainstream_families_resolve() {
assert_eq!(
resolve_architecture("llama"),
Some(ArchPath::GenericGqa {
rope: RopeLayout::Norm
})
);
assert_eq!(
resolve_architecture("qwen2moe"),
Some(ArchPath::GenericGqa {
rope: RopeLayout::Neox
})
);
assert_eq!(
resolve_architecture("mistral"),
Some(ArchPath::GenericGqa {
rope: RopeLayout::Neox
})
);
assert_eq!(
resolve_architecture("yi"),
Some(ArchPath::GenericGqa {
rope: RopeLayout::Neox
})
);
assert_eq!(
resolve_architecture("mixtral"),
Some(ArchPath::GenericGqa {
rope: RopeLayout::Neox
})
);
assert_eq!(
resolve_architecture("phi3"),
Some(ArchPath::GenericGqa {
rope: RopeLayout::Neox
})
);
assert_eq!(
resolve_architecture("phi4"),
Some(ArchPath::GenericGqa {
rope: RopeLayout::Neox
})
);
assert_eq!(
resolve_profile("phi4").map(|p| p.family),
Some(DecoderFamily::PhiFamily)
);
assert_eq!(
resolve_architecture("gemma3"),
Some(ArchPath::GenericGqa {
rope: RopeLayout::Neox
})
);
for arch in ["gemma4", "gemma4-assistant"] {
assert!(
matches!(
resolve_architecture(arch),
Some(ArchPath::DedicatedOnly { .. })
),
"{arch} uses dedicated Gemma4 engine"
);
assert_eq!(
resolve_profile(arch).map(|p| p.family),
Some(DecoderFamily::GemmaFamily)
);
}
assert!(matches!(
resolve_architecture("gemma3n"),
Some(ArchPath::DedicatedOnly { .. })
));
assert_eq!(
resolve_architecture("deepseek"),
Some(ArchPath::GenericGqa {
rope: RopeLayout::Norm
})
);
assert_eq!(
resolve_profile("qwen3").map(|p| p.qk_norm),
Some(QkNormStyle::PerHead)
);
}
#[test]
fn deepseek2_is_dedicated_mla_not_generic() {
assert!(matches!(
resolve_architecture("deepseek2"),
Some(ArchPath::DedicatedOnly { .. })
));
}
#[test]
fn unknown_architecture_is_none() {
assert_eq!(resolve_architecture("totally-unknown-arch"), None);
assert!(matches!(
resolve_architecture("t5"),
Some(ArchPath::DedicatedOnly { .. })
));
}
#[test]
fn dedicated_paths_are_not_generic() {
assert!(matches!(
resolve_architecture("glm-dsa"),
Some(ArchPath::DedicatedOnly { .. })
));
assert!(matches!(
resolve_architecture("deepseek4"),
Some(ArchPath::DedicatedOnly { .. })
));
for arch in ["minimax-m2", "minimax-m3"] {
assert!(
matches!(
resolve_architecture(arch),
Some(ArchPath::DedicatedOnly {
reason: "MiniMax 256-expert sigmoid MoE + MTP — see minimax_engine.rs"
})
),
"{arch} must fail closed, not silent generic GQA"
);
}
assert!(
matches!(
resolve_architecture("llama4"),
Some(ArchPath::DedicatedOnly {
reason: "llama4 MoE + non-GQA attn — see llama4_engine.rs tensor list"
})
),
"llama4 must fail closed, not silent generic GQA"
);
assert!(matches!(
resolve_architecture("glm4"),
Some(ArchPath::DedicatedOnly { .. })
));
assert!(matches!(
resolve_architecture("glm4moe"),
Some(ArchPath::DedicatedOnly { .. })
));
}
#[test]
fn test_fixtures_remain_loadable() {
for arch in ["ferroxtest", "ferroxtestmoe", "ferroxtestmixed"] {
assert!(matches!(
resolve_architecture(arch),
Some(ArchPath::TestFixture { .. })
));
}
}
#[test]
fn catalog_has_unique_names() {
let mut seen = std::collections::HashSet::new();
for p in architecture_catalog() {
assert!(
seen.insert(p.gguf_name),
"duplicate arch name {}",
p.gguf_name
);
}
}
#[test]
fn gemma_family_does_not_fail_closed_on_softcap_keys() {
assert!(unsupported_feature_keys("gemma3").is_empty());
assert!(!unsupported_feature_keys("llama").is_empty());
}
#[test]
fn architectures_with_a_different_residual_topology_are_refused() {
for arch in [
"command-r",
"cohere2",
"cohere2moe",
"falcon",
"gptneox",
"phi2",
"plamo",
"minicpm",
] {
match resolve_architecture(arch) {
Some(ArchPath::DedicatedOnly { reason }) => {
assert!(!reason.is_empty(), "{arch} must say why");
}
other => panic!("{arch} must be refused, got {other:?}"),
}
}
for arch in ["phi3", "phimoe", "plamo3", "starcoder2", "nemotron"] {
assert!(
matches!(
resolve_architecture(arch),
Some(ArchPath::GenericGqa { .. })
),
"{arch} must stay generic"
);
}
}
#[test]
fn no_architecture_is_listed_twice() {
let mut seen = std::collections::HashSet::new();
for p in architecture_catalog() {
assert!(seen.insert(p.gguf_name), "{} listed twice", p.gguf_name);
}
}
}