1use 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
92pub 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
108pub 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
138const 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum ProductionExecutionKind {
198 CausalLanguage,
199}
200
201#[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
394pub 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 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 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 != ¤t)
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
673pub 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
745pub 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 ¤t != 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 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
802pub 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
811pub 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 = (®istry 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 = (®istry 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 = (®istry 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}