Skip to main content

ferrum_models/vnext/
mod.rs

1//! Production model-family packages consumed by the vNext planner and runtime.
2
3use std::collections::BTreeSet;
4use std::fs;
5use std::num::{NonZeroU64, NonZeroUsize};
6use std::path::Path;
7use std::sync::Arc;
8
9use ferrum_interfaces::vnext::{
10    AttributeId, ElementType, ExternalModelMetadataId, ModelFamilyRegistration,
11    ModelFamilyRegistry, ModelSourceKind, OriginalModelSource, OriginalModelSources,
12    PreparedModelFamily, ProgramNode, SemanticValue, StateCapacityDemand, StateLifetime,
13    TypedFamilyRegistration, WeightComponentSource, GPT_OSS_ROUTED_CLAMPED_SWIGLU_MOE_OPERATION_ID,
14    ROUTED_SHARED_SWIGLU_MOE_OPERATION_ID, ROUTED_SWIGLU_MOE_OPERATION_ID,
15};
16use ferrum_types::{
17    DataType, Device, ModelCapabilities, ModelId, ModelInfo, ModelOutputProtocol, ModelType,
18    MoeCapabilities, ReasoningEffortSupport,
19};
20use serde_json::Value;
21
22mod definition;
23pub mod gemma4;
24pub mod gpt_oss;
25mod hf_metadata;
26pub use definition::DefinedProductionModel;
27mod numerical;
28pub mod qwen35;
29pub mod qwen3_moe;
30pub mod source;
31#[cfg(test)]
32mod test_support;
33mod weight_layout;
34
35pub use source::{
36    huggingface_snapshot_identity, HuggingFaceSnapshotIdentity, ProductionModelSourceBundle,
37    ProductionWeightArtifact,
38};
39
40type DefineModel =
41    fn(Arc<ProductionModelSourceBundle>) -> ferrum_types::Result<DefinedProductionModel>;
42type ValidateSemanticConfig = fn(&ExternalModelMetadataId, &[u8]) -> ferrum_types::Result<()>;
43type CreateFamilyRegistration = fn() -> ferrum_types::Result<Box<dyn ModelFamilyRegistration>>;
44
45struct ModelLoaderRegistration {
46    external_metadata_ids: &'static [&'static str],
47    gguf_architectures: &'static [&'static str],
48    execution_kind: ProductionExecutionKind,
49    validate_semantic_config: ValidateSemanticConfig,
50    define: DefineModel,
51    create_family_registration: CreateFamilyRegistration,
52}
53
54const MODEL_LOADERS: &[ModelLoaderRegistration] = &[
55    ModelLoaderRegistration {
56        external_metadata_ids: &[gemma4::EXTERNAL_METADATA_ID],
57        gguf_architectures: &[],
58        execution_kind: ProductionExecutionKind::CausalLanguage,
59        validate_semantic_config: gemma4::validate_semantic_config,
60        define: gemma4::define_from_sources,
61        create_family_registration: gemma4::family_registration,
62    },
63    ModelLoaderRegistration {
64        external_metadata_ids: &[gpt_oss::EXTERNAL_METADATA_ID],
65        gguf_architectures: &[],
66        execution_kind: ProductionExecutionKind::CausalLanguage,
67        validate_semantic_config: gpt_oss::validate_semantic_config,
68        define: gpt_oss::define_from_sources,
69        create_family_registration: gpt_oss::family_registration,
70    },
71    ModelLoaderRegistration {
72        external_metadata_ids: &[
73            qwen35::EXTERNAL_METADATA_ID,
74            qwen35::MOE_EXTERNAL_METADATA_ID,
75        ],
76        gguf_architectures: &["qwen35", "qwen35moe"],
77        execution_kind: ProductionExecutionKind::CausalLanguage,
78        validate_semantic_config: qwen35::validate_semantic_config,
79        define: qwen35::define_from_sources,
80        create_family_registration: qwen35_family_registration,
81    },
82    ModelLoaderRegistration {
83        external_metadata_ids: &[qwen3_moe::EXTERNAL_METADATA_ID],
84        gguf_architectures: &["qwen3moe"],
85        execution_kind: ProductionExecutionKind::CausalLanguage,
86        validate_semantic_config: qwen3_moe::validate_semantic_config,
87        define: qwen3_moe::define_from_sources,
88        create_family_registration: qwen3_moe::family_registration,
89    },
90];
91
92/// Returns whether a GGUF architecture belongs to a family whose product
93/// execution has migrated to vNext. Direct files for these architectures must
94/// provide typed semantic and tokenizer sources instead of silently falling
95/// back to a legacy executor.
96pub fn gguf_architecture_requires_typed_product_sources(architecture: &str) -> bool {
97    MODEL_LOADERS
98        .iter()
99        .any(|registration| registration.gguf_architectures.contains(&architecture))
100}
101
102fn qwen35_family_registration() -> ferrum_types::Result<Box<dyn ModelFamilyRegistration>> {
103    let provider = qwen35::Qwen35FamilyProvider::new()
104        .map_err(|error| ferrum_types::FerrumError::model(error.to_string()))?;
105    Ok(Box::new(TypedFamilyRegistration::new(provider)))
106}
107
108/// Complete typed family registry for production vNext registrations.
109/// It is derived from `MODEL_LOADERS`, so family preparation and resolved-plan
110/// revalidation cannot drift into separate model-name switch statements.
111pub struct ProductionModelFamilyRegistry {
112    registrations: Vec<Box<dyn ModelFamilyRegistration>>,
113}
114
115impl ProductionModelFamilyRegistry {
116    pub fn new() -> ferrum_types::Result<Self> {
117        let registrations = MODEL_LOADERS
118            .iter()
119            .map(|registration| (registration.create_family_registration)())
120            .collect::<ferrum_types::Result<Vec<_>>>()?;
121        Ok(Self { registrations })
122    }
123}
124
125impl ModelFamilyRegistry for ProductionModelFamilyRegistry {
126    fn registrations(&self) -> Vec<&dyn ModelFamilyRegistration> {
127        self.registrations
128            .iter()
129            .map(|registration| registration.as_ref())
130            .collect()
131    }
132}
133
134struct LegacyModelRegistration {
135    external_metadata_id: &'static str,
136}
137
138/// Explicit migration ledger for safetensors families that still use the
139/// legacy executor registry. Moving a family to vNext requires deleting its
140/// row here and adding one `MODEL_LOADERS` row; unknown metadata never gains a
141/// legacy fallback implicitly.
142const LEGACY_MODELS: &[LegacyModelRegistration] = &[
143    LegacyModelRegistration {
144        external_metadata_id: "hf.architecture.LlamaForCausalLM",
145    },
146    LegacyModelRegistration {
147        external_metadata_id: "hf.architecture.Qwen2ForCausalLM",
148    },
149    LegacyModelRegistration {
150        external_metadata_id: "hf.architecture.Qwen3ForCausalLM",
151    },
152    LegacyModelRegistration {
153        external_metadata_id: "hf.architecture.Gemma3ForCausalLM",
154    },
155    LegacyModelRegistration {
156        external_metadata_id: "hf.architecture.Gemma3ForConditionalGeneration",
157    },
158    LegacyModelRegistration {
159        external_metadata_id: "hf.architecture.MistralForCausalLM",
160    },
161    LegacyModelRegistration {
162        external_metadata_id: "hf.architecture.PhiForCausalLM",
163    },
164    LegacyModelRegistration {
165        external_metadata_id: "hf.architecture.GPT2LMHeadModel",
166    },
167    LegacyModelRegistration {
168        external_metadata_id: "hf.architecture.BertModel",
169    },
170    LegacyModelRegistration {
171        external_metadata_id: "hf.architecture.BertForMaskedLM",
172    },
173    LegacyModelRegistration {
174        external_metadata_id: "hf.architecture.BertForSequenceClassification",
175    },
176    LegacyModelRegistration {
177        external_metadata_id: "hf.architecture.CLIPModel",
178    },
179    LegacyModelRegistration {
180        external_metadata_id: "hf.architecture.ChineseCLIPModel",
181    },
182    LegacyModelRegistration {
183        external_metadata_id: "hf.architecture.SiglipModel",
184    },
185    LegacyModelRegistration {
186        external_metadata_id: "hf.architecture.WhisperForConditionalGeneration",
187    },
188    LegacyModelRegistration {
189        external_metadata_id: "hf.architecture.Qwen3TTSForConditionalGeneration",
190    },
191];
192
193/// Product executor category selected by model semantics, never by a family
194/// name. New families using existing operations register against an existing
195/// kind without changing the engine composition root.
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum ProductionExecutionKind {
198    CausalLanguage,
199}
200
201/// Runtime-facing language-model facts produced by a typed family package.
202///
203/// These values replace the legacy engine's second parse through
204/// `ModelDefinition`. Non-zero fields and head compatibility are checked once
205/// while the package is prepared, before an execution plan is compiled.
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct CausalLanguageModelDescriptor {
208    architecture: String,
209    parameter_count: NonZeroU64,
210    hidden_size: NonZeroUsize,
211    layer_count: NonZeroUsize,
212    attention_head_count: NonZeroUsize,
213    kv_head_count: NonZeroUsize,
214    attention_head_dimension: NonZeroUsize,
215    vocabulary_size: NonZeroUsize,
216    maximum_sequence_tokens: NonZeroUsize,
217    execution_dtype: DataType,
218    output_protocol: ModelOutputProtocol,
219    reasoning_effort_support: ReasoningEffortSupport,
220    moe: Option<MoeCapabilities>,
221}
222
223impl CausalLanguageModelDescriptor {
224    #[allow(clippy::too_many_arguments)]
225    pub fn new(
226        architecture: impl Into<String>,
227        parameter_count: u64,
228        hidden_size: usize,
229        layer_count: usize,
230        attention_head_count: usize,
231        kv_head_count: usize,
232        attention_head_dimension: usize,
233        vocabulary_size: usize,
234        maximum_sequence_tokens: usize,
235        execution_dtype: DataType,
236    ) -> ferrum_types::Result<Self> {
237        let architecture = architecture.into();
238        if architecture.trim().is_empty() || architecture.trim() != architecture {
239            return Err(ferrum_types::FerrumError::model(
240                "architecture capability id must be non-empty and canonical",
241            ));
242        }
243        let parameter_count = NonZeroU64::new(parameter_count)
244            .ok_or_else(|| ferrum_types::FerrumError::model("parameter_count must be positive"))?;
245        let hidden_size = NonZeroUsize::new(hidden_size)
246            .ok_or_else(|| ferrum_types::FerrumError::model("hidden_size must be positive"))?;
247        let layer_count = NonZeroUsize::new(layer_count)
248            .ok_or_else(|| ferrum_types::FerrumError::model("layer_count must be positive"))?;
249        let attention_head_count = NonZeroUsize::new(attention_head_count).ok_or_else(|| {
250            ferrum_types::FerrumError::model("attention_head_count must be positive")
251        })?;
252        let kv_head_count = NonZeroUsize::new(kv_head_count)
253            .ok_or_else(|| ferrum_types::FerrumError::model("kv_head_count must be positive"))?;
254        let attention_head_dimension =
255            NonZeroUsize::new(attention_head_dimension).ok_or_else(|| {
256                ferrum_types::FerrumError::model("attention_head_dimension must be positive")
257            })?;
258        let vocabulary_size = NonZeroUsize::new(vocabulary_size)
259            .ok_or_else(|| ferrum_types::FerrumError::model("vocabulary_size must be positive"))?;
260        let maximum_sequence_tokens =
261            NonZeroUsize::new(maximum_sequence_tokens).ok_or_else(|| {
262                ferrum_types::FerrumError::model("maximum_sequence_tokens must be positive")
263            })?;
264        if kv_head_count.get() > attention_head_count.get() {
265            return Err(ferrum_types::FerrumError::model(format!(
266                "kv_head_count {} exceeds attention_head_count {}",
267                kv_head_count, attention_head_count
268            )));
269        }
270        if attention_head_count.get() % kv_head_count.get() != 0 {
271            return Err(ferrum_types::FerrumError::model(format!(
272                "attention_head_count {} is not divisible by kv_head_count {}",
273                attention_head_count, kv_head_count
274            )));
275        }
276        attention_head_count
277            .get()
278            .checked_mul(attention_head_dimension.get())
279            .ok_or_else(|| {
280                ferrum_types::FerrumError::model("attention projection width overflows usize")
281            })?;
282        if !execution_dtype.is_float() {
283            return Err(ferrum_types::FerrumError::model(format!(
284                "causal language execution dtype must be floating point, got {execution_dtype:?}"
285            )));
286        }
287        Ok(Self {
288            architecture,
289            parameter_count,
290            hidden_size,
291            layer_count,
292            attention_head_count,
293            kv_head_count,
294            attention_head_dimension,
295            vocabulary_size,
296            maximum_sequence_tokens,
297            execution_dtype,
298            output_protocol: ModelOutputProtocol::Text,
299            reasoning_effort_support: ReasoningEffortSupport::Unknown,
300            moe: None,
301        })
302    }
303
304    pub fn with_output_protocol(mut self, output_protocol: ModelOutputProtocol) -> Self {
305        self.output_protocol = output_protocol;
306        self
307    }
308
309    pub(super) fn with_moe(
310        mut self,
311        experts: u64,
312        active: u64,
313        intermediate: u64,
314    ) -> ferrum_types::Result<Self> {
315        let positive = |name, value| {
316            usize::try_from(value)
317                .ok()
318                .filter(|value| *value > 0)
319                .ok_or_else(|| {
320                    ferrum_types::FerrumError::model(format!(
321                        "{name} must be positive and fit usize"
322                    ))
323                })
324        };
325        let num_experts = positive("num_experts", experts)?;
326        let experts_per_token = positive("experts_per_token", active)?;
327        if experts_per_token > num_experts {
328            return Err(ferrum_types::FerrumError::model(
329                "experts_per_token exceeds num_experts",
330            ));
331        }
332        self.moe = Some(MoeCapabilities {
333            num_experts,
334            experts_per_token,
335            moe_intermediate_size: Some(positive("moe_intermediate_size", intermediate)?),
336        });
337        Ok(self)
338    }
339
340    pub fn with_reasoning_effort_support(mut self, support: ReasoningEffortSupport) -> Self {
341        self.reasoning_effort_support = support;
342        self
343    }
344
345    pub fn reasoning_effort_support(&self) -> &ReasoningEffortSupport {
346        &self.reasoning_effort_support
347    }
348
349    pub fn architecture(&self) -> &str {
350        &self.architecture
351    }
352
353    pub const fn parameter_count(&self) -> u64 {
354        self.parameter_count.get()
355    }
356
357    pub const fn hidden_size(&self) -> usize {
358        self.hidden_size.get()
359    }
360
361    pub const fn layer_count(&self) -> usize {
362        self.layer_count.get()
363    }
364
365    pub const fn attention_head_count(&self) -> usize {
366        self.attention_head_count.get()
367    }
368
369    pub const fn kv_head_count(&self) -> usize {
370        self.kv_head_count.get()
371    }
372
373    pub const fn attention_head_dimension(&self) -> usize {
374        self.attention_head_dimension.get()
375    }
376
377    pub const fn vocabulary_size(&self) -> usize {
378        self.vocabulary_size.get()
379    }
380
381    pub const fn maximum_sequence_tokens(&self) -> usize {
382        self.maximum_sequence_tokens.get()
383    }
384
385    pub const fn execution_dtype(&self) -> DataType {
386        self.execution_dtype
387    }
388
389    pub const fn output_protocol(&self) -> ModelOutputProtocol {
390        self.output_protocol
391    }
392}
393
394/// One immutable semantic family and the exact indexed weight source used to
395/// initialize its execution plan. Keeping them together prevents product code
396/// from resolving a family from one model directory and loading bytes from
397/// another.
398pub struct PreparedProductionModel {
399    family: PreparedModelFamily,
400    weights: Arc<dyn WeightComponentSource>,
401    descriptor: CausalLanguageModelDescriptor,
402    sources: Arc<ProductionModelSourceBundle>,
403}
404
405impl std::fmt::Debug for PreparedProductionModel {
406    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407        formatter
408            .debug_struct("PreparedProductionModel")
409            .field("family_id", self.family.family_id())
410            .field("descriptor", &self.descriptor)
411            .field("sources", &self.sources)
412            .finish_non_exhaustive()
413    }
414}
415
416impl PreparedProductionModel {
417    pub(super) fn new(
418        family: PreparedModelFamily,
419        weights: Arc<dyn WeightComponentSource>,
420        mut descriptor: CausalLanguageModelDescriptor,
421        sources: Arc<ProductionModelSourceBundle>,
422    ) -> ferrum_types::Result<Self> {
423        descriptor.execution_dtype = match family
424            .numerical_profile()
425            .activation_type()
426            .map_err(|error| ferrum_types::FerrumError::model(error.to_string()))?
427        {
428            ElementType::F16 => DataType::FP16,
429            ElementType::Bf16 => DataType::BF16,
430            ElementType::F32 => DataType::FP32,
431            _ => {
432                return Err(ferrum_types::FerrumError::model(
433                    "invalid primary activation dtype",
434                ))
435            }
436        };
437        if moe_capabilities_from_program(&family)? != descriptor.moe {
438            return Err(ferrum_types::FerrumError::model(
439                "prepared program MoE capabilities differ from typed definition",
440            ));
441        }
442        Ok(Self {
443            family,
444            weights,
445            descriptor,
446            sources,
447        })
448    }
449
450    pub fn family(&self) -> &PreparedModelFamily {
451        &self.family
452    }
453
454    pub fn weights(&self) -> &dyn WeightComponentSource {
455        self.weights.as_ref()
456    }
457
458    pub fn weight_source(&self) -> &Arc<dyn WeightComponentSource> {
459        &self.weights
460    }
461
462    pub fn descriptor(&self) -> &CausalLanguageModelDescriptor {
463        &self.descriptor
464    }
465
466    pub fn sources(&self) -> &Arc<ProductionModelSourceBundle> {
467        &self.sources
468    }
469
470    pub fn product_source_identity(
471        &self,
472        requested_model: impl Into<String>,
473        resolved_model: impl Into<String>,
474    ) -> ferrum_types::Result<ferrum_interfaces::vnext::ProductModelSourceIdentity> {
475        let template = &self.family.metadata().template;
476        self.sources.product_source_identity(
477            requested_model,
478            resolved_model,
479            &template.source_file,
480            &template.template,
481        )
482    }
483
484    pub const fn execution_kind(&self) -> ProductionExecutionKind {
485        ProductionExecutionKind::CausalLanguage
486    }
487
488    /// Projects the immutable typed package into startup auto-configuration.
489    /// Fixed sequence state comes directly from the semantic program; token-
490    /// scaled state (for example KV) remains under the plan/runtime capacity
491    /// policy and is deliberately not double-counted here.
492    pub fn model_capabilities(&self) -> ferrum_types::Result<ModelCapabilities> {
493        let estimated_weight_bytes = self.sources.weight_payload_bytes()?;
494        let recurrent_state_bytes_per_sequence = self
495            .family
496            .program()
497            .states()
498            .iter()
499            .filter(|state| {
500                state.lifetime == StateLifetime::Sequence
501                    && state.capacity_demand == StateCapacityDemand::FixedPerScope
502            })
503            .try_fold(0_u64, |total, state| {
504                let bytes = state.tensor.byte_len().map_err(|error| {
505                    ferrum_types::FerrumError::model(format!(
506                        "fixed per-sequence state {} has invalid storage size: {error}",
507                        state.id
508                    ))
509                })?;
510                total.checked_add(bytes).ok_or_else(|| {
511                    ferrum_types::FerrumError::model(
512                        "fixed per-sequence state byte size overflows u64",
513                    )
514                })
515            })?;
516        let quantization_formats = self.family.weight_schema().quantization_formats();
517        let quantization = (!quantization_formats.is_empty()).then(|| {
518            quantization_formats
519                .iter()
520                .map(ToString::to_string)
521                .collect::<Vec<_>>()
522                .join("+")
523        });
524        let mut supported_dtypes =
525            BTreeSet::from([data_type_label(self.descriptor.execution_dtype())]);
526        supported_dtypes.extend(
527            self.family
528                .program()
529                .states()
530                .iter()
531                .filter_map(|state| element_type_label(state.tensor.element_type)),
532        );
533        let moe = moe_capabilities_from_program(&self.family)?;
534
535        Ok(ModelCapabilities {
536            architecture: self.descriptor.architecture().to_owned(),
537            quantization,
538            moe,
539            max_context_len: Some(self.descriptor.maximum_sequence_tokens()),
540            num_hidden_layers: Some(self.descriptor.layer_count()),
541            head_dim: Some(self.descriptor.attention_head_dimension()),
542            kv_heads: Some(self.descriptor.kv_head_count()),
543            estimated_weight_bytes: (estimated_weight_bytes > 0).then_some(estimated_weight_bytes),
544            recurrent_state_bytes_per_sequence: (recurrent_state_bytes_per_sequence > 0)
545                .then_some(recurrent_state_bytes_per_sequence),
546            supported_dtypes: supported_dtypes.into_iter().collect(),
547            graph_safe_moe: false,
548        })
549    }
550
551    /// Projects typed package facts into the transitional `ModelExecutor`
552    /// metadata contract. The family id is descriptive only and never drives
553    /// executor selection.
554    pub fn model_info(&self, model_id: ModelId, device: Device) -> ModelInfo {
555        ModelInfo {
556            model_id,
557            model_type: ModelType::Custom(self.family.family_id().to_string()),
558            num_parameters: self.descriptor.parameter_count(),
559            hidden_size: self.descriptor.hidden_size(),
560            num_layers: self.descriptor.layer_count(),
561            num_heads: self.descriptor.attention_head_count(),
562            num_kv_heads: self.descriptor.kv_head_count(),
563            vocab_size: self.descriptor.vocabulary_size(),
564            max_sequence_length: self.descriptor.maximum_sequence_tokens(),
565            dtype: self.descriptor.execution_dtype(),
566            device,
567            version: None,
568            license: None,
569            metadata: Default::default(),
570        }
571    }
572}
573
574fn moe_capabilities_from_program(
575    family: &PreparedModelFamily,
576) -> ferrum_types::Result<Option<MoeCapabilities>> {
577    let mut capabilities = None;
578    for node in family
579        .program()
580        .blocks()
581        .iter()
582        .flat_map(|block| &block.nodes)
583        .filter(|node| {
584            matches!(
585                node.operation_id.as_str(),
586                ROUTED_SHARED_SWIGLU_MOE_OPERATION_ID
587                    | ROUTED_SWIGLU_MOE_OPERATION_ID
588                    | GPT_OSS_ROUTED_CLAMPED_SWIGLU_MOE_OPERATION_ID
589            )
590        })
591    {
592        let intermediate_attribute =
593            if node.operation_id.as_str() == GPT_OSS_ROUTED_CLAMPED_SWIGLU_MOE_OPERATION_ID {
594                "intermediate_size"
595            } else {
596                "routed_intermediate_size"
597            };
598        let current = MoeCapabilities {
599            num_experts: required_positive_node_attribute(node, "expert_count")?,
600            experts_per_token: required_positive_node_attribute(node, "experts_per_token")?,
601            moe_intermediate_size: Some(required_positive_node_attribute(
602                node,
603                intermediate_attribute,
604            )?),
605        };
606        if current.experts_per_token > current.num_experts {
607            return Err(ferrum_types::FerrumError::model(format!(
608                "MoE program node {} routes {} experts per token from only {} experts",
609                node.id, current.experts_per_token, current.num_experts
610            )));
611        }
612        if capabilities
613            .as_ref()
614            .is_some_and(|expected| expected != &current)
615        {
616            return Err(ferrum_types::FerrumError::model(format!(
617                "MoE program node {} has capabilities inconsistent with earlier layers",
618                node.id
619            )));
620        }
621        capabilities = Some(current);
622    }
623    Ok(capabilities)
624}
625
626fn required_positive_node_attribute(
627    node: &ProgramNode,
628    attribute: &str,
629) -> ferrum_types::Result<usize> {
630    let attribute_id = AttributeId::new(attribute).map_err(|error| {
631        ferrum_types::FerrumError::internal(format!(
632            "standard MoE attribute id {attribute:?} is invalid: {error}"
633        ))
634    })?;
635    let Some(SemanticValue::Unsigned(value)) = node.attributes.get(&attribute_id) else {
636        return Err(ferrum_types::FerrumError::model(format!(
637            "MoE program node {} lacks unsigned attribute {attribute:?}",
638            node.id
639        )));
640    };
641    let value = usize::try_from(*value).map_err(|_| {
642        ferrum_types::FerrumError::model(format!(
643            "MoE program node {} attribute {attribute:?} exceeds usize",
644            node.id
645        ))
646    })?;
647    if value == 0 {
648        return Err(ferrum_types::FerrumError::model(format!(
649            "MoE program node {} attribute {attribute:?} must be positive",
650            node.id
651        )));
652    }
653    Ok(value)
654}
655
656fn data_type_label(data_type: DataType) -> String {
657    data_type.to_string().to_ascii_lowercase()
658}
659
660fn element_type_label(element_type: ElementType) -> Option<String> {
661    match element_type {
662        ElementType::F16 => Some("fp16".to_owned()),
663        ElementType::Bf16 => Some("bf16".to_owned()),
664        ElementType::F32 => Some("fp32".to_owned()),
665        ElementType::Bool
666        | ElementType::U8
667        | ElementType::U32
668        | ElementType::I8
669        | ElementType::I32 => None,
670    }
671}
672
673/// A resolved static registration. Resolution reads only `config.json`; model
674/// weights are not opened until [`RegisteredProductionModel::prepare`] after
675/// the engine has selected a compatible backend composition.
676pub struct RegisteredProductionModel {
677    registration: &'static ModelLoaderRegistration,
678    external_metadata_id: ExternalModelMetadataId,
679}
680
681impl RegisteredProductionModel {
682    pub fn external_metadata_id(&self) -> &ExternalModelMetadataId {
683        &self.external_metadata_id
684    }
685
686    pub const fn execution_kind(&self) -> ProductionExecutionKind {
687        self.registration.execution_kind
688    }
689
690    pub fn validate_semantic_config(&self, raw: &[u8]) -> ferrum_types::Result<()> {
691        let current = external_metadata_id_from_bytes(raw, "config.json")?;
692        if current != self.external_metadata_id {
693            return Err(ferrum_types::FerrumError::model(format!(
694                "model metadata identity changed between registration and semantic preflight: expected {} got {}",
695                self.external_metadata_id, current
696            )));
697        }
698        (self.registration.validate_semantic_config)(&self.external_metadata_id, raw)
699    }
700
701    pub fn define(&self, model_dir: &Path) -> ferrum_types::Result<DefinedProductionModel> {
702        let original = OriginalModelSource {
703            kind: ModelSourceKind::LocalDirectory,
704            location: model_dir.display().to_string(),
705            requested_revision: None,
706        };
707        let sources = Arc::new(ProductionModelSourceBundle::open_with_semantic_preflight(
708            model_dir,
709            model_dir,
710            ProductionWeightArtifact::safetensors_directory(model_dir),
711            OriginalModelSources {
712                semantic: original.clone(),
713                tokenizer: original.clone(),
714                weights: original,
715            },
716            |raw| self.validate_semantic_config(raw),
717        )?);
718        self.define_from_sources(sources)
719    }
720
721    pub fn define_from_sources(
722        &self,
723        sources: Arc<ProductionModelSourceBundle>,
724    ) -> ferrum_types::Result<DefinedProductionModel> {
725        self.validate_semantic_config(sources.config_json())?;
726        let defined = (self.registration.define)(sources)?;
727        if defined.definition().external_metadata_id() != &self.external_metadata_id {
728            return Err(ferrum_types::FerrumError::model(format!(
729                "registered model loader returned metadata identity {} for resolved identity {}",
730                defined.definition().external_metadata_id(),
731                self.external_metadata_id
732            )));
733        }
734        if defined.execution_kind() != self.registration.execution_kind {
735            return Err(ferrum_types::FerrumError::model(format!(
736                "registered model loader returned execution kind {:?} for registered kind {:?}",
737                defined.execution_kind(),
738                self.registration.execution_kind
739            )));
740        }
741        Ok(defined)
742    }
743}
744
745/// Explicit migration result keyed only by external model metadata.
746///
747/// Product paths that require vNext must consume this through
748/// [`ProductionModelRegistration::into_required`]. A legacy result exists only
749/// for an explicit registry row; unknown metadata is rejected during
750/// resolution and can never enter the old architecture cascade implicitly.
751pub enum ProductionModelRegistration {
752    Registered(RegisteredProductionModel),
753    LegacyRegistered {
754        external_metadata_id: ExternalModelMetadataId,
755    },
756}
757
758impl ProductionModelRegistration {
759    pub fn external_metadata_id(&self) -> &ExternalModelMetadataId {
760        match self {
761            Self::Registered(registration) => registration.external_metadata_id(),
762            Self::LegacyRegistered {
763                external_metadata_id,
764                ..
765            } => external_metadata_id,
766        }
767    }
768
769    fn validate_semantic_config(&self, raw: &[u8]) -> ferrum_types::Result<()> {
770        match self {
771            Self::Registered(registration) => registration.validate_semantic_config(raw),
772            Self::LegacyRegistered {
773                external_metadata_id,
774                ..
775            } => {
776                let current = external_metadata_id_from_bytes(raw, "config.json")?;
777                if &current != external_metadata_id {
778                    return Err(ferrum_types::FerrumError::model(format!(
779                        "model metadata identity changed during semantic preflight: expected {external_metadata_id} got {current}"
780                    )));
781                }
782                Ok(())
783            }
784        }
785    }
786
787    /// Requires a registered vNext production package and fails closed instead
788    /// of allowing a product caller to fall back to a legacy executor.
789    pub fn into_required(self) -> ferrum_types::Result<RegisteredProductionModel> {
790        match self {
791            Self::Registered(registration) => Ok(registration),
792            Self::LegacyRegistered {
793                external_metadata_id,
794                ..
795            } => Err(ferrum_types::FerrumError::unsupported(format!(
796                "model family metadata {external_metadata_id} is registered for the legacy runtime only; vNext product fallback is forbidden"
797            ))),
798        }
799    }
800}
801
802/// Resolves and validates one semantic config without accessing its physical
803/// weight source. Family-specific validation is supplied by the same registry
804/// row that owns preparation, so product composition has no model-name switch.
805pub fn validate_registered_model_semantics(raw: &[u8]) -> ferrum_types::Result<()> {
806    let external_metadata_id = external_metadata_id_from_bytes(raw, "config.json")?;
807    let registration = resolve_registered_model(external_metadata_id)?;
808    registration.validate_semantic_config(raw)
809}
810
811/// Product source constructor with a hard semantic-before-weights ordering.
812/// The exact config bytes validated by the registry are retained in the
813/// returned immutable bundle.
814pub fn open_registered_product_sources(
815    semantic_root: impl AsRef<Path>,
816    tokenizer_root: impl AsRef<Path>,
817    weights: ProductionWeightArtifact,
818    original_sources: OriginalModelSources,
819) -> ferrum_types::Result<ProductionModelSourceBundle> {
820    ProductionModelSourceBundle::open_with_semantic_preflight(
821        semantic_root,
822        tokenizer_root,
823        weights,
824        original_sources,
825        validate_registered_model_semantics,
826    )
827}
828
829pub fn open_registered_colocated_safetensors(
830    model_dir: impl AsRef<Path>,
831) -> ferrum_types::Result<ProductionModelSourceBundle> {
832    let model_dir = model_dir.as_ref();
833    let original = OriginalModelSource {
834        kind: ModelSourceKind::LocalDirectory,
835        location: model_dir.display().to_string(),
836        requested_revision: None,
837    };
838    open_registered_product_sources(
839        model_dir,
840        model_dir,
841        ProductionWeightArtifact::safetensors_directory(model_dir),
842        OriginalModelSources {
843            semantic: original.clone(),
844            tokenizer: original.clone(),
845            weights: original,
846        },
847    )
848}
849
850pub fn resolve_registered_model_from_dir(
851    model_dir: &Path,
852) -> ferrum_types::Result<ProductionModelRegistration> {
853    let external_metadata_id = external_metadata_id_from_model_dir(model_dir)?;
854    resolve_registered_model(external_metadata_id)
855}
856
857pub fn resolve_registered_model_from_sources(
858    sources: &ProductionModelSourceBundle,
859) -> ferrum_types::Result<ProductionModelRegistration> {
860    let external_metadata_id =
861        external_metadata_id_from_bytes(sources.config_json(), "config.json")?;
862    resolve_registered_model(external_metadata_id)
863}
864
865fn resolve_registered_model(
866    external_metadata_id: ExternalModelMetadataId,
867) -> ferrum_types::Result<ProductionModelRegistration> {
868    let mut loaders = MODEL_LOADERS.iter().filter(|registration| {
869        registration
870            .external_metadata_ids
871            .contains(&external_metadata_id.as_str())
872    });
873    let loader = loaders.next();
874    if loaders.next().is_some() {
875        return Err(ferrum_types::FerrumError::internal(format!(
876            "model metadata {external_metadata_id} has duplicate vNext production registrations"
877        )));
878    }
879    let mut legacy_rows = LEGACY_MODELS
880        .iter()
881        .filter(|registration| registration.external_metadata_id == external_metadata_id.as_str());
882    let legacy = legacy_rows.next();
883    if legacy_rows.next().is_some() {
884        return Err(ferrum_types::FerrumError::internal(format!(
885            "model metadata {external_metadata_id} has duplicate legacy registrations"
886        )));
887    }
888
889    match (loader, legacy) {
890        (Some(_), Some(_)) => Err(ferrum_types::FerrumError::internal(format!(
891            "model metadata {external_metadata_id} is registered in both vNext and legacy registries"
892        ))),
893        (Some(registration), None) => Ok(ProductionModelRegistration::Registered(
894            RegisteredProductionModel {
895                registration,
896                external_metadata_id,
897            },
898        )),
899        (None, Some(_)) => Ok(ProductionModelRegistration::LegacyRegistered {
900            external_metadata_id,
901        }),
902        (None, None) => Err(ferrum_types::FerrumError::unsupported(format!(
903            "model metadata {external_metadata_id} is absent from both the vNext and explicit legacy registries; implicit architecture fallback is forbidden"
904        ))),
905    }
906}
907
908fn external_metadata_id_from_model_dir(
909    model_dir: &Path,
910) -> ferrum_types::Result<ExternalModelMetadataId> {
911    let path = model_dir.join("config.json");
912    let raw = fs::read(&path)
913        .map_err(|error| ferrum_types::FerrumError::model(format!("read {path:?}: {error}")))?;
914    external_metadata_id_from_bytes(&raw, &path.display().to_string())
915}
916
917fn external_metadata_id_from_bytes(
918    raw: &[u8],
919    source: &str,
920) -> ferrum_types::Result<ExternalModelMetadataId> {
921    let config: Value = serde_json::from_slice(raw)
922        .map_err(|error| ferrum_types::FerrumError::model(format!("parse {source}: {error}")))?;
923    let architectures = config
924        .get("architectures")
925        .and_then(Value::as_array)
926        .ok_or_else(|| {
927            ferrum_types::FerrumError::model(
928                "config.json must declare exactly one architectures entry for typed family resolution",
929            )
930        })?;
931    let architecture = match architectures.as_slice() {
932        [value] => value.as_str().filter(|value| !value.is_empty()),
933        _ => None,
934    }
935    .ok_or_else(|| {
936        ferrum_types::FerrumError::model(
937            "config.json must declare exactly one non-empty architecture identity",
938        )
939    })?;
940    ExternalModelMetadataId::new(format!("hf.architecture.{architecture}"))
941        .map_err(|error| ferrum_types::FerrumError::model(error.to_string()))
942}
943
944#[cfg(test)]
945mod tests {
946    use super::*;
947
948    #[test]
949    fn required_production_selection_rejects_legacy_family() {
950        let metadata = ExternalModelMetadataId::new("hf.architecture.LlamaForCausalLM").unwrap();
951        let selection = ProductionModelRegistration::LegacyRegistered {
952            external_metadata_id: metadata,
953        };
954
955        let error = match selection.into_required() {
956            Ok(_) => panic!("legacy family unexpectedly entered the vNext product path"),
957            Err(error) => error.to_string(),
958        };
959
960        assert!(
961            error.contains("hf.architecture.LlamaForCausalLM"),
962            "{error}"
963        );
964        assert!(
965            error.contains("vNext product fallback is forbidden"),
966            "{error}"
967        );
968    }
969
970    #[test]
971    fn registry_ids_are_unique_and_disjoint() {
972        let mut ids = std::collections::BTreeSet::new();
973        let mut gguf_architectures = std::collections::BTreeSet::new();
974        for registration in MODEL_LOADERS {
975            for external_metadata_id in registration.external_metadata_ids {
976                assert!(
977                    ids.insert(*external_metadata_id),
978                    "duplicate vNext registration {external_metadata_id}"
979                );
980            }
981            for architecture in registration.gguf_architectures {
982                assert!(
983                    gguf_architectures.insert(*architecture),
984                    "GGUF architecture {architecture} maps to more than one vNext registration"
985                );
986            }
987        }
988        for registration in LEGACY_MODELS {
989            assert!(
990                ids.insert(registration.external_metadata_id),
991                "registration {} appears in both or more than once",
992                registration.external_metadata_id
993            );
994        }
995    }
996
997    #[test]
998    fn migrated_gguf_architecture_requires_typed_product_sources() {
999        assert!(gguf_architecture_requires_typed_product_sources("qwen35"));
1000        assert!(gguf_architecture_requires_typed_product_sources(
1001            "qwen35moe"
1002        ));
1003        assert!(gguf_architecture_requires_typed_product_sources("qwen3moe"));
1004        assert!(!gguf_architecture_requires_typed_product_sources("qwen3"));
1005    }
1006
1007    #[test]
1008    fn production_family_registry_is_derived_from_loader_rows() {
1009        let registry = ProductionModelFamilyRegistry::new().unwrap();
1010        assert_eq!(registry.registrations().len(), MODEL_LOADERS.len());
1011        let metadata = ExternalModelMetadataId::new(qwen35::EXTERNAL_METADATA_ID).unwrap();
1012        let registration = (&registry as &dyn ModelFamilyRegistry)
1013            .resolve_external(&metadata)
1014            .unwrap();
1015        assert_eq!(registration.family_id().as_str(), qwen35::FAMILY_ID);
1016        let moe_metadata = ExternalModelMetadataId::new(qwen35::MOE_EXTERNAL_METADATA_ID).unwrap();
1017        let moe_registration = (&registry as &dyn ModelFamilyRegistry)
1018            .resolve_external(&moe_metadata)
1019            .unwrap();
1020        assert_eq!(moe_registration.family_id().as_str(), qwen35::FAMILY_ID);
1021        let qwen3_moe_metadata =
1022            ExternalModelMetadataId::new(qwen3_moe::EXTERNAL_METADATA_ID).unwrap();
1023        let qwen3_moe_registration = (&registry as &dyn ModelFamilyRegistry)
1024            .resolve_external(&qwen3_moe_metadata)
1025            .unwrap();
1026        assert_eq!(
1027            qwen3_moe_registration.family_id().as_str(),
1028            qwen3_moe::FAMILY_ID
1029        );
1030    }
1031
1032    #[test]
1033    fn qwen35_moe_metadata_resolves_to_vnext_product_loader() {
1034        let directory = tempfile::tempdir().unwrap();
1035        fs::write(
1036            directory.path().join("config.json"),
1037            r#"{"architectures":["Qwen3_5MoeForConditionalGeneration"]}"#,
1038        )
1039        .unwrap();
1040
1041        let registration = resolve_registered_model_from_dir(directory.path())
1042            .unwrap()
1043            .into_required()
1044            .unwrap();
1045        assert_eq!(
1046            registration.external_metadata_id().as_str(),
1047            qwen35::MOE_EXTERNAL_METADATA_ID
1048        );
1049    }
1050
1051    #[test]
1052    fn qwen3_moe_metadata_resolves_to_vnext_product_loader() {
1053        let directory = tempfile::tempdir().unwrap();
1054        fs::write(
1055            directory.path().join("config.json"),
1056            r#"{"architectures":["Qwen3MoeForCausalLM"]}"#,
1057        )
1058        .unwrap();
1059
1060        let registration = resolve_registered_model_from_dir(directory.path())
1061            .unwrap()
1062            .into_required()
1063            .unwrap();
1064        assert_eq!(
1065            registration.external_metadata_id().as_str(),
1066            qwen3_moe::EXTERNAL_METADATA_ID
1067        );
1068    }
1069
1070    #[test]
1071    fn unknown_metadata_cannot_enter_an_implicit_legacy_path() {
1072        let directory = tempfile::tempdir().unwrap();
1073        fs::write(
1074            directory.path().join("config.json"),
1075            r#"{"architectures":["UnregisteredQwenForConditionalGeneration"]}"#,
1076        )
1077        .unwrap();
1078
1079        let error = resolve_registered_model_from_dir(directory.path())
1080            .err()
1081            .expect("unknown architecture must fail closed")
1082            .to_string();
1083        assert!(
1084            error.contains("hf.architecture.UnregisteredQwenForConditionalGeneration"),
1085            "{error}"
1086        );
1087        assert!(
1088            error.contains("implicit architecture fallback is forbidden"),
1089            "{error}"
1090        );
1091    }
1092
1093    #[test]
1094    fn registered_source_open_rejects_nested_layout_before_weight_resolution() {
1095        let semantic = tempfile::tempdir().unwrap();
1096        let tokenizer = tempfile::tempdir().unwrap();
1097        fs::write(
1098            semantic.path().join("config.json"),
1099            r#"{
1100                "architectures":["Qwen3_5MoeForConditionalGeneration"],
1101                "model_type":"qwen3_5_moe",
1102                "text_config":{"model_type":"unsupported_nested_layout"}
1103            }"#,
1104        )
1105        .unwrap();
1106        fs::write(
1107            tokenizer.path().join("tokenizer.json"),
1108            br#"{"version":"1.0"}"#,
1109        )
1110        .unwrap();
1111        let original = OriginalModelSource {
1112            kind: ModelSourceKind::LocalDirectory,
1113            location: "fixture".to_owned(),
1114            requested_revision: None,
1115        };
1116
1117        let error = open_registered_product_sources(
1118            semantic.path(),
1119            tokenizer.path(),
1120            ProductionWeightArtifact::safetensors_directory(
1121                semantic.path().join("missing-weight-root"),
1122            ),
1123            OriginalModelSources {
1124                semantic: original.clone(),
1125                tokenizer: original.clone(),
1126                weights: original,
1127            },
1128        )
1129        .expect_err("invalid nested semantics must fail before resolving weights")
1130        .to_string();
1131
1132        assert!(
1133            error.contains("unsupported Qwen3.5 text model_type"),
1134            "{error}"
1135        );
1136        assert!(
1137            !error.contains("weight source is not a directory"),
1138            "{error}"
1139        );
1140    }
1141
1142    #[test]
1143    fn registered_semantic_preflight_accepts_reference_dense_and_moe_configs() {
1144        let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
1145        for name in [
1146            "qwen35_dense_min_reference.config.json",
1147            "qwen35_moe_shared_expert_reference.config.json",
1148        ] {
1149            let raw = fs::read(root.join(name)).unwrap();
1150            validate_registered_model_semantics(&raw)
1151                .unwrap_or_else(|error| panic!("{name}: {error}"));
1152        }
1153    }
1154
1155    #[test]
1156    fn registration_resolution_does_not_open_weights_and_rechecks_identity() {
1157        let directory = tempfile::tempdir().unwrap();
1158        let config_path = directory.path().join("config.json");
1159        fs::write(
1160            &config_path,
1161            r#"{"architectures":["Qwen3_5ForConditionalGeneration"]}"#,
1162        )
1163        .unwrap();
1164
1165        let registration = resolve_registered_model_from_dir(directory.path())
1166            .unwrap()
1167            .into_required()
1168            .unwrap();
1169        assert_eq!(
1170            registration.external_metadata_id().as_str(),
1171            qwen35::EXTERNAL_METADATA_ID
1172        );
1173        assert_eq!(
1174            registration.execution_kind(),
1175            ProductionExecutionKind::CausalLanguage
1176        );
1177
1178        fs::write(
1179            config_path,
1180            r#"{"architectures":["DifferentForConditionalGeneration"]}"#,
1181        )
1182        .unwrap();
1183        let error = match registration.define(directory.path()) {
1184            Ok(_) => panic!("changed metadata unexpectedly prepared"),
1185            Err(error) => error.to_string(),
1186        };
1187        assert!(error.contains("metadata identity changed"), "{error}");
1188        assert!(error.contains(qwen35::EXTERNAL_METADATA_ID), "{error}");
1189    }
1190
1191    #[test]
1192    fn causal_language_descriptor_requires_an_explicit_effort_declaration() {
1193        let descriptor =
1194            CausalLanguageModelDescriptor::new("test", 1, 16, 2, 2, 1, 4, 32, 128, DataType::FP16)
1195                .unwrap()
1196                .with_output_protocol(ModelOutputProtocol::HarmonyGptOss);
1197        assert_eq!(
1198            descriptor.reasoning_effort_support(),
1199            &ReasoningEffortSupport::Unknown
1200        );
1201
1202        let declared = ReasoningEffortSupport::Declared(BTreeSet::from([
1203            ferrum_types::ReasoningEffort::Low,
1204            ferrum_types::ReasoningEffort::High,
1205        ]));
1206        let descriptor = descriptor.with_reasoning_effort_support(declared.clone());
1207        assert_eq!(descriptor.reasoning_effort_support(), &declared);
1208    }
1209
1210    #[test]
1211    fn causal_language_descriptor_rejects_invalid_runtime_facts() {
1212        assert!(CausalLanguageModelDescriptor::new(
1213            "test",
1214            0,
1215            16,
1216            2,
1217            2,
1218            1,
1219            4,
1220            32,
1221            128,
1222            DataType::FP16,
1223        )
1224        .is_err());
1225        assert!(CausalLanguageModelDescriptor::new(
1226            "test",
1227            1,
1228            16,
1229            2,
1230            2,
1231            1,
1232            0,
1233            32,
1234            128,
1235            DataType::FP16,
1236        )
1237        .is_err());
1238        assert!(CausalLanguageModelDescriptor::new(
1239            "test",
1240            1,
1241            16,
1242            2,
1243            2,
1244            3,
1245            4,
1246            32,
1247            128,
1248            DataType::FP16,
1249        )
1250        .is_err());
1251        assert!(CausalLanguageModelDescriptor::new(
1252            "test",
1253            1,
1254            16,
1255            2,
1256            3,
1257            2,
1258            4,
1259            32,
1260            128,
1261            DataType::FP16,
1262        )
1263        .is_err());
1264        assert!(CausalLanguageModelDescriptor::new(
1265            "test",
1266            1,
1267            16,
1268            2,
1269            2,
1270            1,
1271            4,
1272            32,
1273            128,
1274            DataType::INT8,
1275        )
1276        .is_err());
1277        assert!(
1278            CausalLanguageModelDescriptor::new("test", 1, 15, 2, 2, 1, 4, 32, 128, DataType::FP16,)
1279                .is_ok(),
1280            "hidden width and explicit attention projection width are independent facts"
1281        );
1282    }
1283}