atlas_core/capabilities.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Model capabilities — config-derived feature flags.
4//!
5//! Instead of checking `config.model_type == "qwen3_5_moe"` throughout the codebase,
6//! call sites use `config.capabilities().has_moe_layers` etc. Adding a new model
7//! only requires implementing the capability derivation, not updating every call site.
8
9/// SSM (State Space Model) architecture family.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum SsmArchitecture {
12 /// No SSM layers — pure attention (Mistral, Llama, etc.)
13 None,
14 /// Gated Delta Networks (Qwen3.5 family)
15 Gdn,
16 /// Mamba-2 (Nemotron-H family)
17 Mamba2,
18}
19
20/// Attention architecture family.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum AttentionType {
23 /// Standard multi-head or grouped-query attention (Qwen3.5, Llama, etc.)
24 Standard,
25 /// Multi-head Latent Attention — compressed KV via low-rank projection.
26 /// (Mistral Small 4, DeepSeek-V2/V3)
27 Mla,
28}
29
30/// Feature flags derived from model config at parse time.
31///
32/// These replace scattered `is_nemotron_h()`, `is_qwen35()` checks with
33/// model-agnostic predicates. New models get capabilities automatically
34/// from their config — no code changes needed.
35#[derive(Debug, Clone)]
36pub struct ModelCapabilities {
37 /// Model has SSM/recurrent layers (GDN or Mamba-2).
38 pub has_ssm_layers: bool,
39 /// Model has full attention layers.
40 pub has_attention_layers: bool,
41 /// Model has MoE (Mixture of Experts) layers.
42 pub has_moe_layers: bool,
43 /// Model supports `<think>` reasoning tokens.
44 /// Derived from tokenizer vocabulary, not model type name.
45 pub supports_thinking: bool,
46 /// Model has vision encoder (multimodal).
47 pub supports_vision: bool,
48 /// Model has MTP (Multi-Token Prediction) draft head.
49 pub has_mtp: bool,
50 /// SSM architecture family (determines state layout).
51 pub ssm_architecture: SsmArchitecture,
52 /// Attention architecture (standard GQA vs MLA compressed latent).
53 pub attention_type: AttentionType,
54 /// Model wraps language model config in a nested field (e.g., `text_config`).
55 pub has_nested_config: bool,
56}
57
58impl ModelCapabilities {
59 /// Derive capabilities from a ModelConfig.
60 ///
61 /// This is the SSOT for model feature detection. When adding a new model,
62 /// ensure its config fields populate the right capabilities here — then
63 /// all downstream code works automatically.
64 pub fn from_config(config: &super::config::ModelConfig) -> Self {
65 use super::config::LayerType;
66
67 let has_ssm = config
68 .layer_types
69 .iter()
70 .any(|t| matches!(t, LayerType::LinearAttention));
71 let has_mamba2 = config.mamba_num_heads > 0 && config.mamba_head_dim > 0;
72 let has_attention = config
73 .layer_types
74 .iter()
75 .any(|t| matches!(t, LayerType::FullAttention));
76 let has_moe = config.num_experts > 0;
77 let has_vision = config.vision.is_some();
78 let has_mtp = config.mtp_num_hidden_layers > 0;
79 let has_nested = config.nested_config;
80
81 let ssm_arch = if has_mamba2 {
82 SsmArchitecture::Mamba2
83 } else if has_ssm {
84 SsmArchitecture::Gdn
85 } else {
86 SsmArchitecture::None
87 };
88
89 Self {
90 has_ssm_layers: has_ssm || has_mamba2,
91 has_attention_layers: has_attention,
92 has_moe_layers: has_moe,
93 // Models with SSM layers support <think> tokens. Long-term this should
94 // be derived from tokenizer vocabulary, not architecture.
95 supports_thinking: has_ssm || has_mamba2,
96 supports_vision: has_vision,
97 has_mtp,
98 ssm_architecture: ssm_arch,
99 has_nested_config: has_nested,
100 attention_type: if config.kv_lora_rank > 0 {
101 AttentionType::Mla
102 } else {
103 AttentionType::Standard
104 },
105 }
106 }
107}