1use ferrox_core::expert_store::{ExpertKey, ExpertSource, ExpertStore};
26use ferrox_core::tensor::Tensor;
27use ferrox_core::weight_matrix::quant_kind_for;
28use ferrox_core::weight_matrix::{QuantKind, WeightBytes, WeightMatrix};
29use ferrox_gguf::{GgmlType, GgufError, GgufValue, ShardedGguf, TensorInfo, TensorSource};
30use ferrox_moe::{ExpertWeights, GatingFunction, MoeLayerConfig};
31use std::sync::Arc;
32use thiserror::Error;
33
34use crate::config::ModelConfig;
35#[cfg(feature = "metal")]
36use crate::decoder::MoePackedQ4Planes;
37use crate::decoder::{AttnWeights, Decoder, ExpertBacking, LayerWeights, MoeWeights};
38
39#[derive(Debug, Error)]
40pub enum LoadError {
41 #[error(transparent)]
42 Gguf(#[from] GgufError),
43 #[error(transparent)]
44 Shard(#[from] ferrox_gguf::ShardError),
45 #[error("tensor '{0}' has unsupported dtype {1:?}")]
46 UnsupportedDtype(String, GgmlType),
47 #[error(
48 "MoE tensor '{0}' is not 3D or its expert count {1} does not match config n_experts {2}"
49 )]
50 ExpertCountMismatch(String, usize, usize),
51 #[error("GGUF file is missing required hparam metadata key '{0}'")]
52 MissingHparam(String),
53 #[error(
56 "unsupported GGUF architecture '{0}': not in ferrox's capability registry \
57 (unknown required features fail closed; see ferrox_models::capability)"
58 )]
59 UnsupportedArchitecture(String),
60 #[error("architecture '{0}' cannot use the generic Decoder: {1}")]
62 DedicatedArchitectureRequired(String, &'static str),
63 #[error("architecture '{0}' requires unimplemented feature: {1}")]
65 UnsupportedFeature(String, String),
66 #[error(
67 "architecture '{0}' has never been verified against llama.cpp. It would run on \
68 ferrox's shared generic-GQA path, which ASSUMES plain GQA with {1:?} RoPE and no \
69 ALiBi, no learned position embeddings and no per-layer rope skipping. That \
70 assumption has already been wrong for gpt2, mpt, refact, bloom and jais, each of \
71 which loaded clean and answered as a different model. {2} Set \
72 FERROX_ALLOW_UNAUDITED_ARCH=1 to run it anyway and compare the output against \
73 llama.cpp yourself"
74 )]
75 UnauditedArchitecture(String, crate::config::RopeLayout, String),
76 #[error(
80 "checkpoint carries {0} tensor(s) this build never reads, so its graph is not the one \
81 ferrox would run: {1}. This is a missing feature, not a corrupt file. Override with \
82 FERROX_ALLOW_UNKNOWN_TENSORS=1 to load anyway and accept wrong output."
83 )]
84 UnconsumedTensors(usize, String),
85 #[error("{0}")]
91 StrictKernels(String),
92}
93
94const SIGMOID_GATING_ARCHITECTURES: &[&str] =
114 &["afmoe", "deepseek2", "glm4moe", "laguna", "step35"];
115
116#[cfg(test)]
139const DEDICATED_OWNS_ITS_BEHAVIOUR: &[(&str, &str)] = &[
140 ("deepseek2", "mla_gguf_loader"),
143 ("glm4moe", "refused today, see capability::unaudited_triage"),
147];
148
149const NO_TOPK_RENORMALIZE_ARCHITECTURES: &[&str] = &["deepseek", "olmoe", "qwen2moe"];
176
177const PRE_FFN_NORM_IS_POST_ATTENTION_NORM: &[&str] = &["gpt-oss", "seed_oss"];
199
200fn pre_ffn_norm_is_post_attention_norm(arch: &str) -> bool {
204 PRE_FFN_NORM_IS_POST_ATTENTION_NORM.contains(&arch)
205}
206
207const LEADING_DENSE_KEY_IS_INERT: &[&str] = &["bailingmoe"];
231
232const QK_NORM_AFTER_ROPE_ARCHITECTURES: &[&str] = &["hunyuan-moe", "maincoder"];
251
252fn metadata_u64_any(file: &impl TensorSource, keys: &[String]) -> Option<u64> {
253 keys.iter().find_map(|k| file.metadata_u64(k))
254}
255
256fn metadata_f32_any(file: &impl TensorSource, keys: &[String]) -> Option<f32> {
257 keys.iter()
258 .find_map(|k| file.metadata(k).and_then(GgufValue::as_f32))
259}
260
261impl ModelConfig {
262 pub fn from_gguf(file: &impl TensorSource) -> Result<Self, LoadError> {
276 let arch = file
277 .metadata_str("general.architecture")
278 .ok_or_else(|| LoadError::MissingHparam("general.architecture".to_string()))?
279 .to_string();
280 let arch_profile = crate::capability::resolve_profile(&arch)
281 .ok_or_else(|| LoadError::UnsupportedArchitecture(arch.clone()))?;
282 let rope_layout = match arch_profile.path {
283 crate::capability::ArchPath::GenericGqa { rope }
284 | crate::capability::ArchPath::TestFixture { rope } => rope,
285 crate::capability::ArchPath::DedicatedOnly { reason } => {
286 return Err(LoadError::DedicatedArchitectureRequired(
287 arch.clone(),
288 reason,
289 ));
290 }
291 crate::capability::ArchPath::Deferred { reason } => {
292 return Err(LoadError::UnsupportedFeature(
293 arch.clone(),
294 format!("architecture deferred from Ferrox text-generation scope: {reason}"),
295 ));
296 }
297 };
298 let qk_norm_style = arch_profile.qk_norm;
299 for (meta_key, feature) in crate::capability::unsupported_feature_keys(&arch) {
300 if let Some(v) = metadata_f32_any(file, std::slice::from_ref(&meta_key)) {
301 if v > 0.0 {
302 return Err(LoadError::UnsupportedFeature(
303 arch.clone(),
304 format!("{feature} (metadata {meta_key}={v})"),
305 ));
306 }
307 }
308 if let Some(v) = metadata_u64_any(file, std::slice::from_ref(&meta_key)) {
309 if v > 0 {
310 return Err(LoadError::UnsupportedFeature(
311 arch.clone(),
312 feature.to_string(),
313 ));
314 }
315 }
316 }
317 for (meta_key, feature, no_op) in crate::capability::unsupported_scaling_keys(&arch) {
323 if let Some(v) = metadata_f32_any(file, std::slice::from_ref(&meta_key)) {
324 if (v - no_op).abs() > 1e-6 {
325 return Err(LoadError::UnsupportedFeature(
326 arch.clone(),
327 format!("{feature} (metadata {meta_key}={v})"),
328 ));
329 }
330 }
331 }
332 let key = |suffix: &str| format!("{arch}.{suffix}");
333
334 let name: &'static str = Box::leak(
335 file.metadata_str("general.name")
336 .unwrap_or(&arch)
337 .to_string()
338 .into_boxed_str(),
339 );
340
341 let n_layers =
342 file.metadata_u64(&key("block_count"))
343 .ok_or_else(|| LoadError::MissingHparam(key("block_count")))? as usize;
344 if arch == "baichuan" && n_layers == 40 {
355 return Err(LoadError::UnsupportedFeature(
356 arch.clone(),
357 "Baichuan-13B (block_count=40) uses ALiBi and no RoPE, decided by layer \
358 count with no GGUF key to declare it; the generic decoder would rotate \
359 every Q/K head instead. Baichuan-7B (block_count=32) is unaffected"
360 .to_string(),
361 ));
362 }
363 let hidden_dim = file
364 .metadata_u64(&key("embedding_length"))
365 .ok_or_else(|| LoadError::MissingHparam(key("embedding_length")))?
366 as usize;
367 let n_heads = file
368 .metadata_u64(&key("attention.head_count"))
369 .ok_or_else(|| LoadError::MissingHparam(key("attention.head_count")))?
370 as usize;
371
372 let mut best_effort_fields: Vec<&'static str> = Vec::new();
373
374 let n_kv_heads = file
375 .metadata_u64(&key("attention.head_count_kv"))
376 .map(|v| v as usize)
377 .unwrap_or_else(|| {
378 best_effort_fields.push("n_kv_heads (no attention.head_count_kv key; assumed equal to n_heads, i.e. plain MHA)");
379 n_heads
380 });
381 let head_dim = file
382 .metadata_u64(&key("attention.key_length"))
383 .map(|v| v as usize)
384 .unwrap_or_else(|| {
385 best_effort_fields.push(
386 "head_dim (no attention.key_length key; derived as hidden_dim / n_heads)",
387 );
388 hidden_dim / n_heads
389 });
390 let v_head_dim = file
391 .metadata_u64(&key("attention.value_length"))
392 .map(|v| v as usize)
393 .unwrap_or(head_dim);
394 if v_head_dim != head_dim {
395 return Err(LoadError::UnsupportedFeature(
396 arch.clone(),
397 format!(
398 "split K/V head dims (key_length={head_dim}, value_length={v_head_dim}); \
399 generic decoder requires equal head dims"
400 ),
401 ));
402 }
403 let vocab_size = file
404 .metadata("tokenizer.ggml.tokens")
405 .and_then(|v| match v {
406 GgufValue::Array(items) => Some(items.len()),
407 _ => None,
408 })
409 .or_else(|| file.metadata_u64(&key("vocab_size")).map(|v| v as usize))
410 .unwrap_or_else(|| {
411 best_effort_fields.push("vocab_size (no tokenizer.ggml.tokens array or {arch}.vocab_size key; fell back to output.weight's own row count)");
412 file.find_tensor("output.weight")
417 .and_then(|t| t.shape.last().copied())
418 .unwrap_or(0) as usize
419 });
420 let rope_theta = metadata_f32_any(file, &[key("rope.freq_base")]).unwrap_or_else(|| {
421 best_effort_fields.push("rope_theta (no rope.freq_base key; defaulted to 10000.0)");
422 10000.0
423 });
424 let rms_norm_eps = metadata_f32_any(
425 file,
426 &[
427 key("attention.layer_norm_rms_epsilon"),
428 key("attention.layer_norm_epsilon"),
429 ],
430 )
431 .unwrap_or_else(|| {
432 best_effort_fields
433 .push("rms_norm_eps (no layer_norm_rms_epsilon key; defaulted to 1e-5)");
434 1e-5
435 });
436
437 let n_experts = metadata_u64_any(file, &[key("expert_count")]).unwrap_or(0) as usize;
438 let is_moe = n_experts > 1;
439
440 let n_experts_active = if is_moe {
441 metadata_u64_any(file, &[key("expert_used_count")]).unwrap_or_else(|| {
442 best_effort_fields
443 .push("moe.n_experts_active (no expert_used_count key; defaulted to 2)");
444 2
445 }) as usize
446 } else {
447 1
448 };
449 let n_shared_experts = match metadata_u64_any(file, &[key("expert_shared_count")]) {
455 Some(n) => n as usize,
456 None if is_moe && file.find_tensor("blk.0.ffn_gate_shexp.weight").is_some() => {
457 best_effort_fields.push(
458 "moe.n_shared_experts (no expert_shared_count; inferred 1 from blk.0.ffn_gate_shexp.weight)",
459 );
460 1
461 }
462 None => 0,
463 };
464 let feed_forward_length = metadata_u64_any(file, &[key("feed_forward_length")]);
470 let expert_ffn_dim = metadata_u64_any(file, &[key("expert_feed_forward_length")])
471 .or_else(|| {
472 feed_forward_length.map(|ff| {
473 if is_moe && n_experts_active > 0 {
474 ff / n_experts_active as u64
475 } else {
476 ff
477 }
478 })
479 })
480 .unwrap_or_else(|| {
481 best_effort_fields.push(
482 "moe.expert_ffn_dim (no expert_feed_forward_length/feed_forward_length; defaulted to 4x hidden_dim)",
483 );
484 (hidden_dim * 4) as u64
485 }) as usize;
486 let n_dense_leading_layers = if LEADING_DENSE_KEY_IS_INERT.contains(&arch.as_str()) {
487 0
488 } else {
489 metadata_u64_any(file, &[key("leading_dense_block_count")]).unwrap_or(0) as usize
490 };
491
492 let gating = match metadata_u64_any(file, &[key("expert_gating_func")]) {
498 Some(2) => GatingFunction::Sigmoid,
499 Some(1) => GatingFunction::Softmax,
500 _ => {
501 if SIGMOID_GATING_ARCHITECTURES.contains(&arch.as_str()) {
502 GatingFunction::Sigmoid
503 } else {
504 if is_moe {
505 best_effort_fields.push(
506 "moe.gating (no expert_gating_func key and architecture not in the known-sigmoid list; defaulted to softmax)",
507 );
508 }
509 GatingFunction::Softmax
510 }
511 }
512 };
513
514 let norm_topk_prob = match file.metadata_bool(&key("expert_weights_norm")) {
521 Some(v) => v,
522 None => {
523 if is_moe && matches!(gating, GatingFunction::Softmax) {
527 best_effort_fields.push(
528 "moe.norm_topk_prob (no expert_weights_norm key; defaulted by architecture-name lookup against NO_TOPK_RENORMALIZE_ARCHITECTURES)",
529 );
530 }
531 !NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&arch.as_str())
532 }
533 };
534
535 let expert_weights_scale = metadata_f32_any(file, &[key("expert_weights_scale")])
539 .filter(|s| *s != 0.0)
540 .unwrap_or(1.0);
541
542 let sliding_window = metadata_u64_any(file, &[key("attention.sliding_window")])
552 .map(|v| v as usize)
553 .filter(|&w| w > 0)
554 .filter(|_| !crate::capability::swa_disabled_by_arch(&arch));
560
561 let swa_layout = crate::capability::default_swa_layout(&arch);
570 let swa_dense_first = swa_layout.is_some_and(|p| p.dense_first);
571 let swa_pattern = metadata_u64_any(file, &[key("attention.sliding_window_pattern")])
572 .map(|v| v as usize)
573 .or_else(|| {
574 sliding_window?;
575 swa_layout.map(|p| p.period).or(
580 match arch_profile.family {
583 crate::capability::DecoderFamily::GemmaFamily => Some(6),
584 _ => None,
585 },
586 )
587 });
588
589 let attn_logit_softcap = metadata_f32_any(
590 file,
591 &[
592 key("attention.logit_softcapping"),
593 key("attn_logit_softcapping"),
594 ],
595 )
596 .filter(|&v| v > 0.0);
597 let final_logit_softcap =
598 metadata_f32_any(file, &[key("final_logit_softcapping")]).filter(|&v| v > 0.0);
599
600 let embedding_scale = if matches!(
602 arch_profile.family,
603 crate::capability::DecoderFamily::GemmaFamily
604 ) {
605 Some((hidden_dim as f32).sqrt())
606 } else {
607 None
608 };
609
610 let attention_scale = crate::capability::attention_scale_override(
623 &arch, n_layers, hidden_dim, n_heads, head_dim,
624 );
625
626 let rope_theta_swa = if sliding_window.is_some() {
631 let fallback = if crate::capability::swa_rope_base_follows_model(&arch) {
632 rope_theta
633 } else {
634 10_000.0
635 };
636 Some(
637 metadata_f32_any(
638 file,
639 &[key("rope.freq_base_swa"), key("rope_freq_base_swa")],
640 )
641 .unwrap_or(fallback),
642 )
643 } else {
644 None
645 };
646
647 let ffn_activation = match arch_profile.family {
648 _ if crate::capability::uses_geglu(&arch) => crate::config::FfnActivation::Gelu,
652 crate::capability::DecoderFamily::GemmaFamily => crate::config::FfnActivation::Gelu,
653 crate::capability::DecoderFamily::PhiFamily => {
654 crate::config::FfnActivation::SwigluFused
655 }
656 _ => crate::config::FfnActivation::Swiglu,
657 };
658
659 let rope_freqs = load_f32_vec_optional(file, "rope_freqs.weight")?;
666
667 let rope_orig_ctx = metadata_u64_any(file, &[key("rope.scaling.original_context_length")])
680 .map(|v| v as usize);
681 let (rope_freqs_long, rope_freqs_short) = if rope_freqs.is_some() {
686 (None, None)
687 } else {
688 (
689 load_f32_vec_optional(file, "rope_factors_long.weight")?,
690 load_f32_vec_optional(file, "rope_factors_short.weight")?,
691 )
692 };
693 let rope_freqs = match (rope_freqs, rope_orig_ctx) {
697 (Some(f), _) => Some(f),
698 (None, Some(orig)) => {
699 let model_ctx = metadata_u64_any(file, &[key("context_length")])
700 .unwrap_or(orig as u64) as usize;
701 if model_ctx > orig {
702 rope_freqs_long.clone().or_else(|| rope_freqs_short.clone())
703 } else {
704 rope_freqs_short.clone().or_else(|| rope_freqs_long.clone())
705 }
706 }
707 (None, None) => None,
708 };
709
710 let rope_dim = metadata_u64_any(file, &[key("rope.dimension_count")])
715 .map(|d| d as usize)
716 .filter(|d| *d > 0 && *d < head_dim);
717
718 let rope_attn_factor = metadata_f32_any(file, &[key("rope.scaling.attn_factor")])
720 .filter(|f| f.is_finite() && *f > 0.0)
721 .unwrap_or(1.0);
722
723 let rope_freqs_unscaled = rope_freqs.clone();
764
765 let rope_freqs = match linear_scaling_from_gguf(file, &arch) {
766 None => rope_freqs,
767 Some(factor) => {
768 let rotary_dim = rope_dim.unwrap_or(head_dim);
769 if rotary_dim == 0 || !rotary_dim.is_multiple_of(2) {
770 best_effort_fields.push(
771 "rope_freqs (linear scaling declared but the rotary width is odd; \
772 scaling not applied)",
773 );
774 rope_freqs
775 } else {
776 let linear = vec![factor; rotary_dim / 2];
777 match rope_freqs {
778 None => Some(linear),
779 Some(own) if own.len() == linear.len() => {
783 Some(own.iter().zip(linear.iter()).map(|(a, b)| a * b).collect())
784 }
785 Some(own) => {
786 best_effort_fields.push(
787 "rope_freqs (linear scaling declared but the file's own \
788 rope_freqs tensor has a different width; scaling not applied)",
789 );
790 Some(own)
791 }
792 }
793 }
794 }
795 };
796 let rope_freqs = match yarn_scaling_from_gguf(file, &arch, rope_orig_ctx) {
797 None => rope_freqs,
798 Some(scaling) => {
799 let rotary_dim = rope_dim.unwrap_or(head_dim);
800 if rotary_dim == 0 || !rotary_dim.is_multiple_of(2) {
801 best_effort_fields.push(
802 "rope_freqs (YaRN declared but the rotary width is odd; scaling not applied)",
803 );
804 rope_freqs
805 } else {
806 let yarn =
807 ferrox_core::attention::yarn_freq_factors(scaling, rotary_dim, rope_theta);
808 match rope_freqs {
809 None => Some(yarn),
810 Some(own) if own.len() == yarn.len() => {
811 Some(own.iter().zip(yarn.iter()).map(|(a, b)| a * b).collect())
812 }
813 Some(own) => {
814 best_effort_fields.push(
815 "rope_freqs (YaRN declared alongside a per-band factor tensor of a \
816 different width; the file's own tensor is used unscaled)",
817 );
818 Some(own)
819 }
820 }
821 }
822 }
823 };
824
825 let rope_freqs = rope_freqs.map(|full| {
841 let swa = (sliding_window.is_some()
842 && !crate::capability::swa_rope_scale_follows_model(&arch))
843 .then(|| rope_freqs_unscaled.unwrap_or_else(|| vec![1.0; full.len()]))
844 .filter(|swa| *swa != full);
845 crate::config::RopeFreqs { full, swa }
846 });
847
848 if best_effort_fields.is_empty() {
853 best_effort_fields.push(
854 "none -- every field above was read directly from this file's own GGUF metadata",
855 );
856 }
857
858 if matches!(
869 arch_profile.path,
870 crate::capability::ArchPath::GenericGqa { .. }
871 ) && !crate::capability::is_audited_generic(&arch)
872 && !matches!(
873 std::env::var("FERROX_ALLOW_UNAUDITED_ARCH").ok().as_deref(),
874 Some("1") | Some("true") | Some("on")
875 )
876 {
877 return Err(LoadError::UnauditedArchitecture(
878 arch.clone(),
879 rope_layout,
880 crate::capability::unaudited_refusal_detail(&arch),
881 ));
882 }
883
884 Ok(ModelConfig {
885 name,
886 n_layers,
887 hidden_dim,
888 n_heads,
889 n_kv_heads,
890 head_dim,
891 vocab_size,
892 rope_theta,
893 rms_norm_eps,
894 attention: crate::config::AttentionKind::Gqa,
898 sliding_window,
899 swa_pattern,
900 swa_dense_first,
901 moe: MoeLayerConfig {
902 n_experts: n_experts.max(1),
903 n_experts_active,
904 n_shared_experts,
905 hidden_dim,
906 expert_ffn_dim,
907 gating,
908 norm_topk_prob,
909 expert_group_count: metadata_u64_any(file, &[key("expert_group_count")])
910 .map(|v| v as usize)
911 .filter(|&c| c > 1),
912 expert_group_used_count: metadata_u64_any(file, &[key("expert_group_used_count")])
913 .map(|v| v as usize)
914 .filter(|&c| c > 0),
915 expert_weights_scale,
916 },
917 n_dense_leading_layers,
918 rope_freqs,
919 rope_layout,
920 qk_norm_style,
921 attn_logit_softcap,
922 final_logit_softcap,
923 embedding_scale,
924 attention_scale,
925 rope_attn_factor,
926 rope_dim,
927 rope_freqs_long,
928 rope_freqs_short,
929 rope_orig_ctx,
930 rope_theta_swa,
931 ffn_activation,
932 best_effort_fields: Box::leak(best_effort_fields.into_boxed_slice()),
933 })
934 }
935}
936
937impl crate::sampling::RecommendedSampling {
938 pub fn from_gguf(file: &impl TensorSource) -> Self {
960 let number = |k: &str| -> Option<f32> {
961 file.metadata(k)
962 .and_then(|v| v.as_f32().or_else(|| v.as_u64().map(|u| u as f32)))
963 };
964 crate::sampling::RecommendedSampling {
965 temperature: number("general.sampling.temp"),
966 top_p: number("general.sampling.top_p"),
967 top_k: file
968 .metadata("general.sampling.top_k")
969 .and_then(|v| v.as_u64())
970 .map(|v| v as usize),
971 }
972 }
973}
974
975fn linear_scaling_from_gguf(file: &impl TensorSource, arch: &str) -> Option<f32> {
982 let key = |suffix: &str| format!("{arch}.{suffix}");
983 let scaling_type = file.metadata_str(&key("rope.scaling.type"))?;
984 if !scaling_type.eq_ignore_ascii_case("linear") {
985 return None;
986 }
987 metadata_f32_any(file, &[key("rope.scaling.factor")]).filter(|f| f.is_finite() && *f > 1.0)
988}
989
990fn yarn_scaling_from_gguf(
1020 file: &impl TensorSource,
1021 arch: &str,
1022 orig_ctx: Option<usize>,
1023) -> Option<ferrox_core::attention::YarnScaling> {
1024 let key = |suffix: &str| format!("{arch}.{suffix}");
1025 let scaling_type = file.metadata_str(&key("rope.scaling.type"))?;
1026 if !scaling_type.eq_ignore_ascii_case("yarn") {
1027 return None;
1028 }
1029 let factor = metadata_f32_any(file, &[key("rope.scaling.factor")])
1030 .filter(|f| f.is_finite() && *f > 1.0)?;
1031 let orig_max_pos = orig_ctx?;
1032 let beta = |suffix: &str, default: f32| -> f32 {
1033 metadata_f32_any(
1034 file,
1035 &[
1036 key(&format!("rope.scaling.{suffix}")),
1037 key(&format!("rope.scaling.yarn_{suffix}")),
1038 ],
1039 )
1040 .filter(|v| v.is_finite() && *v > 0.0)
1041 .unwrap_or(default)
1042 };
1043 Some(ferrox_core::attention::YarnScaling {
1044 factor,
1045 beta_fast: beta("beta_fast", 32.0),
1046 beta_slow: beta("beta_slow", 1.0),
1047 orig_max_pos,
1048 truncate: true,
1052 })
1053}
1054
1055pub(crate) fn find_info<'a>(
1056 file: &'a impl TensorSource,
1057 name: &str,
1058) -> Result<&'a TensorInfo, LoadError> {
1059 file.find_tensor(name)
1060 .ok_or_else(|| LoadError::Gguf(GgufError::TensorNotFound(name.to_string())))
1061}
1062
1063fn load_gpt_oss_layer(
1089 file: &impl TensorSource,
1090 l: usize,
1091 config: &ModelConfig,
1092) -> Result<crate::decoder::GptOssLayer, LoadError> {
1093 let n_experts = config.moe.n_experts;
1094 let ff = config.moe.expert_ffn_dim;
1095
1096 let want = |name: &str, got: usize, expect: usize| -> Result<(), LoadError> {
1097 if got == expect {
1098 Ok(())
1099 } else {
1100 Err(LoadError::UnsupportedFeature(
1101 config.name.to_string(),
1102 format!("{name} has {got} elements, expected {expect}"),
1103 ))
1104 }
1105 };
1106
1107 let attn_sinks = load_f32_vec(file, &format!("blk.{l}.attn_sinks.weight"))?;
1108 want(
1109 &format!("blk.{l}.attn_sinks.weight"),
1110 attn_sinks.len(),
1111 config.n_heads,
1112 )?;
1113 let o_bias = load_f32_vec(file, &format!("blk.{l}.attn_output.bias"))?;
1114 want(
1115 &format!("blk.{l}.attn_output.bias"),
1116 o_bias.len(),
1117 config.hidden_dim,
1118 )?;
1119 let router_bias = load_f32_vec(file, &format!("blk.{l}.ffn_gate_inp.bias"))?;
1120 want(
1121 &format!("blk.{l}.ffn_gate_inp.bias"),
1122 router_bias.len(),
1123 n_experts,
1124 )?;
1125
1126 let gate_b = load_f32_vec(file, &format!("blk.{l}.ffn_gate_exps.bias"))?;
1127 want(
1128 &format!("blk.{l}.ffn_gate_exps.bias"),
1129 gate_b.len(),
1130 n_experts * ff,
1131 )?;
1132 let up_b = load_f32_vec(file, &format!("blk.{l}.ffn_up_exps.bias"))?;
1133 want(
1134 &format!("blk.{l}.ffn_up_exps.bias"),
1135 up_b.len(),
1136 n_experts * ff,
1137 )?;
1138 let down_b = load_f32_vec(file, &format!("blk.{l}.ffn_down_exps.bias"))?;
1139 want(
1140 &format!("blk.{l}.ffn_down_exps.bias"),
1141 down_b.len(),
1142 n_experts * config.hidden_dim,
1143 )?;
1144
1145 let expert_bias = (0..n_experts)
1146 .map(|e| ferrox_moe::ExpertBias {
1147 gate: gate_b[e * ff..(e + 1) * ff].to_vec(),
1148 up: up_b[e * ff..(e + 1) * ff].to_vec(),
1149 down: down_b[e * config.hidden_dim..(e + 1) * config.hidden_dim].to_vec(),
1150 })
1151 .collect();
1152
1153 Ok(crate::decoder::GptOssLayer {
1154 attn_sinks,
1155 o_bias,
1156 router_bias,
1157 expert_bias,
1158 })
1159}
1160
1161pub(crate) fn load_f32_vec_optional(
1162 file: &impl TensorSource,
1163 name: &str,
1164) -> Result<Option<Vec<f32>>, LoadError> {
1165 if file.find_tensor(name).is_none() {
1166 return Ok(None);
1167 }
1168 Ok(Some(load_f32_vec(file, name)?))
1169}
1170
1171fn slice_quantized_rows(m: &WeightMatrix, start: usize, n: usize) -> Option<WeightMatrix> {
1178 let WeightMatrix::Quantized {
1179 data,
1180 rows,
1181 cols,
1182 kind,
1183 } = m
1184 else {
1185 return None;
1186 };
1187 let total = data.len();
1188 if *rows == 0 || total % *rows != 0 || start + n > *rows {
1189 return None;
1190 }
1191 let row_bytes = total / *rows;
1192 let (b0, b1) = (start * row_bytes, (start + n) * row_bytes);
1193 let bytes = match data {
1194 WeightBytes::Mapped { mmap, range } => WeightBytes::Mapped {
1195 mmap: mmap.clone(),
1196 range: range.start + b0..range.start + b1,
1197 },
1198 other => WeightBytes::Owned(other.as_slice()[b0..b1].to_vec()),
1199 };
1200 Some(WeightMatrix::Quantized {
1201 data: bytes,
1202 rows: n,
1203 cols: *cols,
1204 kind: *kind,
1205 })
1206}
1207
1208fn load_qkv_projections(
1213 file: &impl TensorSource,
1214 layer: usize,
1215 config: &ModelConfig,
1216) -> Result<(WeightMatrix, WeightMatrix, WeightMatrix), LoadError> {
1217 let q_name = format!("blk.{layer}.attn_q.weight");
1218 let k_name = format!("blk.{layer}.attn_k.weight");
1219 let v_name = format!("blk.{layer}.attn_v.weight");
1220 let fused_name = format!("blk.{layer}.attn_qkv.weight");
1221
1222 if file.find_tensor(&q_name).is_some() {
1223 return Ok((
1224 load_weight_matrix(file, &q_name)?,
1225 load_weight_matrix(file, &k_name)?,
1226 load_weight_matrix(file, &v_name)?,
1227 ));
1228 }
1229 if file.find_tensor(&fused_name).is_none() {
1230 return Err(LoadError::Gguf(GgufError::TensorNotFound(q_name)));
1231 }
1232
1233 let fused = load_weight_matrix(file, &fused_name)?;
1234 let q_rows = config.n_heads * config.head_dim;
1235 let kv_rows = config.n_kv_heads * config.head_dim;
1236 let expected = q_rows + 2 * kv_rows;
1237 if fused.rows() != expected {
1238 return Err(LoadError::UnsupportedFeature(
1240 config.name.to_string(),
1241 format!(
1242 "{fused_name} has {} rows; expected q+k+v = {} \
1243 (n_heads*head_dim + 2*n_kv_heads*head_dim)",
1244 fused.rows(),
1245 expected
1246 ),
1247 ));
1248 }
1249 let cols = fused.cols();
1250 if let (Some(q), Some(k), Some(v)) = (
1253 slice_quantized_rows(&fused, 0, q_rows),
1254 slice_quantized_rows(&fused, q_rows, kv_rows),
1255 slice_quantized_rows(&fused, q_rows + kv_rows, kv_rows),
1256 ) {
1257 return Ok((q, k, v));
1258 }
1259 let mut full = Vec::with_capacity(fused.rows() * cols);
1261 for r in 0..fused.rows() {
1262 full.extend_from_slice(&fused.dequant_row(r));
1263 }
1264 let q = WeightMatrix::F32(Tensor::new(
1265 full[..q_rows * cols].to_vec(),
1266 vec![q_rows, cols],
1267 ));
1268 let k = WeightMatrix::F32(Tensor::new(
1269 full[q_rows * cols..(q_rows + kv_rows) * cols].to_vec(),
1270 vec![kv_rows, cols],
1271 ));
1272 let v = WeightMatrix::F32(Tensor::new(
1273 full[(q_rows + kv_rows) * cols..].to_vec(),
1274 vec![kv_rows, cols],
1275 ));
1276 Ok((q, k, v))
1277}
1278
1279fn load_dense_expert(
1282 file: &impl TensorSource,
1283 layer: usize,
1284 config: &ModelConfig,
1285) -> Result<ExpertWeights, LoadError> {
1286 let gate_name = format!("blk.{layer}.ffn_gate.weight");
1287 let up_name = format!("blk.{layer}.ffn_up.weight");
1288 let down_name = format!("blk.{layer}.ffn_down.weight");
1289 if file.find_tensor(&gate_name).is_some() {
1290 return Ok(ExpertWeights {
1291 gate: load_weight_matrix(file, &gate_name)?,
1292 up: load_weight_matrix(file, &up_name)?,
1293 down: load_weight_matrix(file, &down_name)?,
1294 });
1295 }
1296 let fused = load_weight_matrix(file, &up_name)?;
1298 let ff = config.moe.expert_ffn_dim;
1299 if fused.rows() != 2 * ff {
1300 return Err(LoadError::UnsupportedFeature(
1301 config.name.to_string(),
1302 format!(
1303 "{up_name} has {} rows without a companion ffn_gate; \
1304 expected fused SwiGLU with 2*ffn_dim = {} rows",
1305 fused.rows(),
1306 2 * ff
1307 ),
1308 ));
1309 }
1310 let cols = fused.cols();
1311 if let (Some(gate), Some(up)) = (
1313 slice_quantized_rows(&fused, 0, ff),
1314 slice_quantized_rows(&fused, ff, ff),
1315 ) {
1316 return Ok(ExpertWeights {
1317 gate,
1318 up,
1319 down: load_weight_matrix(file, &down_name)?,
1320 });
1321 }
1322 let mut full = Vec::with_capacity(fused.rows() * cols);
1323 for r in 0..fused.rows() {
1324 full.extend_from_slice(&fused.dequant_row(r));
1325 }
1326 let gate = WeightMatrix::F32(Tensor::new(full[..ff * cols].to_vec(), vec![ff, cols]));
1327 let up = WeightMatrix::F32(Tensor::new(full[ff * cols..].to_vec(), vec![ff, cols]));
1328 Ok(ExpertWeights {
1329 gate,
1330 up,
1331 down: load_weight_matrix(file, &down_name)?,
1332 })
1333}
1334
1335pub(crate) fn widen_plain_float(
1344 dtype: GgmlType,
1345 raw: &[u8],
1346 name: &str,
1347) -> Result<Vec<f32>, LoadError> {
1348 match dtype {
1349 GgmlType::F32 => {
1350 let mut out = Vec::with_capacity(raw.len() / 4);
1351 for chunk in raw.as_chunks::<4>().0 {
1352 out.push(f32::from_le_bytes(*chunk));
1353 }
1354 Ok(out)
1355 }
1356 GgmlType::F16 => ferrox_quant::dequant_f16(raw)
1357 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::F16)),
1358 GgmlType::BF16 => ferrox_quant::dequant_bf16(raw)
1359 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::BF16)),
1360 GgmlType::MXFP4 => ferrox_quant::dequant_mxfp4_gguf(raw)
1367 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::MXFP4)),
1368 other => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1369 }
1370}
1371
1372pub(crate) fn load_f32_vec(file: &impl TensorSource, name: &str) -> Result<Vec<f32>, LoadError> {
1373 let info = find_info(file, name)?;
1374 let raw = file.tensor_bytes(name)?;
1375 match info.dtype {
1376 GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 | GgmlType::MXFP4 => {
1388 widen_plain_float(info.dtype, raw, name)
1389 }
1390 GgmlType::Q8_0 => ferrox_quant::dequant_q8_0(raw)
1391 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q8_0)),
1392 GgmlType::Q4_0 => ferrox_quant::dequant_q4_0(raw)
1393 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4_0)),
1394 GgmlType::Q4K => ferrox_quant::dequant_q4_k(raw)
1395 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4K)),
1396 GgmlType::Q5K => ferrox_quant::dequant_q5_k(raw)
1397 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5K)),
1398 GgmlType::Q6K => ferrox_quant::dequant_q6_k(raw)
1399 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q6K)),
1400 GgmlType::Q2K => ferrox_quant::dequant_q2_k(raw)
1401 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q2K)),
1402 GgmlType::Q3K => ferrox_quant::dequant_q3_k(raw)
1403 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q3K)),
1404 GgmlType::Q4_1 => ferrox_quant::dequant_q4_1(raw)
1405 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4_1)),
1406 GgmlType::Q5_0 => ferrox_quant::dequant_q5_0(raw)
1407 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5_0)),
1408 GgmlType::Q5_1 => ferrox_quant::dequant_q5_1(raw)
1409 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5_1)),
1410 GgmlType::Q8_1 => ferrox_quant::dequant_q8_1(raw)
1411 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q8_1)),
1412 GgmlType::IQ4NL => ferrox_quant::dequant_iq4_nl(raw)
1413 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ4NL)),
1414 GgmlType::IQ4XS => ferrox_quant::dequant_iq4_xs(raw)
1415 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ4XS)),
1416 GgmlType::IQ1S => ferrox_quant::dequant_iq1_s(raw)
1423 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ1S)),
1424 GgmlType::IQ1M => ferrox_quant::dequant_iq1_m(raw)
1425 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ1M)),
1426 GgmlType::IQ2XXS => ferrox_quant::dequant_iq2_xxs(raw)
1427 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2XXS)),
1428 GgmlType::IQ2XS => ferrox_quant::dequant_iq2_xs(raw)
1429 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2XS)),
1430 GgmlType::IQ2S => ferrox_quant::dequant_iq2_s(raw)
1431 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2S)),
1432 GgmlType::IQ3XXS => ferrox_quant::dequant_iq3_xxs(raw)
1433 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ3XXS)),
1434 GgmlType::IQ3S => ferrox_quant::dequant_iq3_s(raw)
1435 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ3S)),
1436 other => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1437 }
1438}
1439
1440pub(crate) fn load_weight_matrix(
1447 file: &impl TensorSource,
1448 name: &str,
1449) -> Result<WeightMatrix, LoadError> {
1450 let info = find_info(file, name)?;
1451 let shape: Vec<usize> = info.shape.iter().rev().map(|&d| d as usize).collect();
1464 let (rows, cols) = match shape.as_slice() {
1476 [r, c] => (*r, *c),
1477 [c] => (1, *c),
1478 other => {
1479 return Err(LoadError::UnsupportedDtype(
1480 format!("{name} (expected 2D, got shape {other:?})"),
1481 info.dtype,
1482 ))
1483 }
1484 };
1485 let shape = vec![rows, cols];
1489
1490 match info.dtype {
1491 GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => {
1497 let data = load_f32_vec(file, name)?;
1498 Ok(WeightMatrix::F32(Tensor::new(data, shape)))
1499 }
1500 other => match quant_kind_for(other) {
1501 Some(kind) => {
1502 let (mmap, range) = file.tensor_mapped_range(name)?;
1503 #[cfg(feature = "metal")]
1504 ferrox_metal::gpu::register_weight_mmap(Arc::clone(&mmap));
1505 Ok(WeightMatrix::Quantized {
1506 data: WeightBytes::Mapped { mmap, range },
1507 rows,
1508 cols,
1509 kind,
1510 })
1511 }
1512 None => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1513 },
1514 }
1515}
1516
1517pub(crate) fn split_expert_tensor(
1524 file: &impl TensorSource,
1525 name: &str,
1526 n_experts: usize,
1527) -> Result<Vec<WeightMatrix>, LoadError> {
1528 let info = find_info(file, name)?;
1529 if info.shape.len() != 3 || info.shape[2] as usize != n_experts {
1536 let file_experts = info.shape.last().map(|&d| d as usize).unwrap_or(0);
1537 return Err(LoadError::ExpertCountMismatch(
1538 name.to_string(),
1539 file_experts,
1540 n_experts,
1541 ));
1542 }
1543 let out_dim = info.shape[1] as usize;
1544 let in_dim = info.shape[0] as usize;
1545 let raw = file.tensor_bytes(name)?;
1546
1547 match info.dtype {
1548 GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => {
1549 let all = crate::loader::widen_plain_float(info.dtype, raw, name)?;
1550 let per_expert = out_dim * in_dim;
1551 Ok((0..n_experts)
1552 .map(|e| {
1553 WeightMatrix::F32(Tensor::new(
1554 all[e * per_expert..(e + 1) * per_expert].to_vec(),
1555 vec![out_dim, in_dim],
1556 ))
1557 })
1558 .collect())
1559 }
1560 other => match quant_kind_for(other) {
1561 Some(kind) => {
1562 let (mmap, full_range) = file.tensor_mapped_range(name)?;
1563 #[cfg(feature = "metal")]
1564 ferrox_metal::gpu::register_weight_mmap(Arc::clone(&mmap));
1565 let bytes_per_expert = raw.len() / n_experts;
1566 Ok((0..n_experts)
1567 .map(|e| WeightMatrix::Quantized {
1568 data: WeightBytes::Mapped {
1569 mmap: Arc::clone(&mmap),
1570 range: (full_range.start + e * bytes_per_expert)
1571 ..(full_range.start + (e + 1) * bytes_per_expert),
1572 },
1573 rows: out_dim,
1574 cols: in_dim,
1575 kind,
1576 })
1577 .collect())
1578 }
1579 None => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1580 },
1581 }
1582}
1583
1584#[cfg(feature = "metal")]
1589fn try_build_moe_packed_q4_planes(experts: &[ExpertWeights]) -> Option<MoePackedQ4Planes> {
1590 use ferrox_core::weight_matrix::{QuantKind, WeightBytes};
1591 use std::sync::Arc;
1592
1593 if experts.is_empty() {
1594 return None;
1595 }
1596
1597 fn mapped_sg(m: &WeightMatrix) -> Option<(WeightBytes, usize, &'static str)> {
1598 match m {
1599 WeightMatrix::Quantized {
1600 data: WeightBytes::Mapped { mmap, range },
1601 rows,
1602 kind,
1603 ..
1604 } => {
1605 let kind_str = match kind {
1606 QuantKind::Q4_0 => "Q4_0",
1607 QuantKind::Q5_0 => "Q5_0",
1608 QuantKind::Q4K => "Q4_K",
1609 QuantKind::Q5K => "Q5_K",
1610 QuantKind::Q6K => "Q6_K",
1611 QuantKind::Q8_0 => "Q8_0",
1612 QuantKind::IQ4XS => "IQ4_XS",
1613 _ => return None,
1614 };
1615 let _ = ferrox_metal::gpu::mul_mm_sg_meta(kind_str)?;
1616 Some((
1617 WeightBytes::Mapped {
1618 mmap: Arc::clone(mmap),
1619 range: range.clone(),
1620 },
1621 *rows,
1622 kind_str,
1623 ))
1624 }
1625 _ => None,
1626 }
1627 }
1628
1629 let (gate0, ffn_rows, gate_kind) = mapped_sg(&experts[0].gate)?;
1630 let (up0, up_rows, up_kind) = mapped_sg(&experts[0].up)?;
1631 let (down0, hidden_rows, down_kind) = mapped_sg(&experts[0].down)?;
1632 if up_rows != ffn_rows {
1633 return None;
1634 }
1635 let WeightBytes::Mapped {
1636 mmap: gate_mmap,
1637 range: gate0_range,
1638 } = &gate0
1639 else {
1640 return None;
1641 };
1642 let WeightBytes::Mapped {
1643 mmap: up_mmap,
1644 range: up0_range,
1645 } = &up0
1646 else {
1647 return None;
1648 };
1649 let WeightBytes::Mapped {
1650 mmap: down_mmap,
1651 range: down0_range,
1652 } = &down0
1653 else {
1654 return None;
1655 };
1656
1657 let gate_stride = gate0_range.len();
1658 let up_stride = up0_range.len();
1659 let down_stride = down0_range.len();
1660 if gate_stride == 0 || up_stride == 0 || down_stride == 0 {
1661 return None;
1662 }
1663
1664 let n = experts.len();
1665 for (i, ex) in experts.iter().enumerate().skip(1) {
1666 let (g, fr, gk) = mapped_sg(&ex.gate)?;
1667 let (u, ur, uk) = mapped_sg(&ex.up)?;
1668 let (d, hr, dk) = mapped_sg(&ex.down)?;
1669 if gk != gate_kind || uk != up_kind || dk != down_kind {
1670 return None;
1671 }
1672 let WeightBytes::Mapped { mmap, range } = &g else {
1673 return None;
1674 };
1675 if fr != ffn_rows {
1676 return None;
1677 }
1678 if !Arc::ptr_eq(mmap, gate_mmap)
1679 || range.len() != gate_stride
1680 || range.start != gate0_range.start + i * gate_stride
1681 {
1682 return None;
1683 }
1684 let WeightBytes::Mapped { mmap, range } = &u else {
1685 return None;
1686 };
1687 if ur != ffn_rows
1688 || !Arc::ptr_eq(mmap, up_mmap)
1689 || range.len() != up_stride
1690 || range.start != up0_range.start + i * up_stride
1691 {
1692 return None;
1693 }
1694 let WeightBytes::Mapped { mmap, range } = &d else {
1695 return None;
1696 };
1697 if hr != hidden_rows
1698 || !Arc::ptr_eq(mmap, down_mmap)
1699 || range.len() != down_stride
1700 || range.start != down0_range.start + i * down_stride
1701 {
1702 return None;
1703 }
1704 }
1705
1706 Some(MoePackedQ4Planes::new(
1707 WeightBytes::Mapped {
1708 mmap: Arc::clone(gate_mmap),
1709 range: gate0_range.start..gate0_range.start + n * gate_stride,
1710 },
1711 WeightBytes::Mapped {
1712 mmap: Arc::clone(up_mmap),
1713 range: up0_range.start..up0_range.start + n * up_stride,
1714 },
1715 WeightBytes::Mapped {
1716 mmap: Arc::clone(down_mmap),
1717 range: down0_range.start..down0_range.start + n * down_stride,
1718 },
1719 gate_stride,
1720 up_stride,
1721 down_stride,
1722 n,
1723 ffn_rows,
1724 hidden_rows,
1725 gate_kind,
1726 up_kind,
1727 down_kind,
1728 ))
1729}
1730
1731#[derive(Debug, Clone, Copy)]
1735pub struct StoredMatrixSpec {
1736 pub offset: usize,
1737 pub len: usize,
1738 pub rows: usize,
1739 pub cols: usize,
1740 pub kind: QuantKind,
1741}
1742
1743#[derive(Debug, Clone, Copy)]
1745pub struct StoredExpertLayout {
1746 pub gate: StoredMatrixSpec,
1747 pub up: StoredMatrixSpec,
1748 pub down: StoredMatrixSpec,
1749}
1750
1751impl StoredExpertLayout {
1752 pub fn total_bytes(&self) -> usize {
1753 self.gate.len + self.up.len + self.down.len
1754 }
1755
1756 pub fn materialize(&self, lease: &ferrox_core::expert_store::ExpertLease) -> ExpertWeights {
1760 let mk = |spec: &StoredMatrixSpec| WeightMatrix::Quantized {
1761 data: WeightBytes::Shared {
1762 buf: lease.shared_buf(),
1763 range: spec.offset..spec.offset + spec.len,
1764 },
1765 rows: spec.rows,
1766 cols: spec.cols,
1767 kind: spec.kind,
1768 };
1769 ExpertWeights {
1770 gate: mk(&self.gate),
1771 up: mk(&self.up),
1772 down: mk(&self.down),
1773 }
1774 }
1775}
1776
1777pub struct GgufExpertSource {
1783 files: Vec<std::fs::File>,
1784 segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]>,
1787}
1788
1789impl ExpertSource for GgufExpertSource {
1790 fn expert_len(&self, key: ExpertKey) -> Option<usize> {
1791 self.segments
1792 .get(&key)
1793 .map(|segs| segs.iter().map(|&(_, _, len)| len).sum())
1794 }
1795
1796 fn read_expert(&self, key: ExpertKey) -> std::io::Result<Vec<u8>> {
1797 let segs = self
1798 .segments
1799 .get(&key)
1800 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, format!("{key:?}")))?;
1801 let total: usize = segs.iter().map(|&(_, _, len)| len).sum();
1802 let mut buf = vec![0u8; total];
1803 let mut written = 0;
1804 for &(fi, offset, len) in segs {
1805 let dst = &mut buf[written..written + len];
1806 #[cfg(unix)]
1807 {
1808 use std::os::unix::fs::FileExt;
1809 self.files[fi].read_exact_at(dst, offset)?;
1810 }
1811 #[cfg(not(unix))]
1812 {
1813 use std::io::{Read, Seek, SeekFrom};
1814 let mut f = &self.files[fi];
1815 f.seek(SeekFrom::Start(offset))?;
1816 f.read_exact(dst)?;
1817 }
1818 written += len;
1819 }
1820 Ok(buf)
1821 }
1822}
1823
1824struct StoredTensorSpecs {
1834 shard: usize,
1835 per_expert: Vec<(u64, usize)>,
1836 spec: StoredMatrixSpec,
1837}
1838
1839fn stored_expert_specs(
1840 file: &ShardedGguf,
1841 name: &str,
1842 n_experts: usize,
1843) -> Result<Option<StoredTensorSpecs>, LoadError> {
1844 let info = find_info(file, name)?;
1845 if info.shape.len() != 3 || info.shape[2] as usize != n_experts {
1846 let file_experts = info.shape.last().map(|&d| d as usize).unwrap_or(0);
1847 return Err(LoadError::ExpertCountMismatch(
1848 name.to_string(),
1849 file_experts,
1850 n_experts,
1851 ));
1852 }
1853 let out_dim = info.shape[1] as usize;
1854 let in_dim = info.shape[0] as usize;
1855 let Some(kind) = quant_kind_for(info.dtype) else {
1856 return Ok(None); };
1858 let shard = file
1859 .tensor_shard_index(name)
1860 .expect("find_info succeeded, shard index must exist");
1861 let (_, full_range) = file.tensor_mapped_range(name)?;
1864 let total_len = full_range.end - full_range.start;
1865 let bytes_per_expert = total_len / n_experts;
1866 let per_expert: Vec<(u64, usize)> = (0..n_experts)
1867 .map(|e| {
1868 (
1869 (full_range.start + e * bytes_per_expert) as u64,
1870 bytes_per_expert,
1871 )
1872 })
1873 .collect();
1874 let spec = StoredMatrixSpec {
1875 offset: 0, len: bytes_per_expert,
1877 rows: out_dim,
1878 cols: in_dim,
1879 kind,
1880 };
1881 Ok(Some(StoredTensorSpecs {
1882 shard,
1883 per_expert,
1884 spec,
1885 }))
1886}
1887
1888impl Decoder {
1889 pub fn from_gguf(
1898 path: impl AsRef<std::path::Path>,
1899 config: ModelConfig,
1900 ) -> Result<Self, LoadError> {
1901 Self::from_gguf_with_expert_cache(path, config, None)
1902 }
1903
1904 pub fn from_gguf_with_expert_cache(
1917 path: impl AsRef<std::path::Path>,
1918 mut config: ModelConfig,
1919 expert_cache_bytes: Option<u64>,
1920 ) -> Result<Self, LoadError> {
1921 let path = path.as_ref();
1922 let file = ShardedGguf::open(path)?;
1923
1924 let arch = file
1936 .metadata_str("general.architecture")
1937 .unwrap_or_default()
1938 .to_string();
1939 let is_gpt_oss = arch == "gpt-oss";
1940 let post_attn_norm_is_pre_ffn_norm = pre_ffn_norm_is_post_attention_norm(&arch);
1941 let mut gpt_oss_layers: Vec<crate::decoder::GptOssLayer> = Vec::new();
1942
1943 let mut store_segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]> =
1947 std::collections::HashMap::new();
1948 let mut stored_layouts: Vec<Option<Vec<StoredExpertLayout>>> = Vec::new();
1949
1950 let embedding = load_weight_matrix(&file, "token_embd.weight")?;
1955
1956 let mut layers = Vec::with_capacity(config.n_layers);
1957 let mut refined_qk_norm = config.qk_norm_style;
1958 for l in 0..config.n_layers {
1959 let (q_proj, k_proj, v_proj) = load_qkv_projections(&file, l, &config)?;
1960 let q_norm = load_f32_vec_optional(&file, &format!("blk.{l}.attn_q_norm.weight"))?;
1961 let k_norm = load_f32_vec_optional(&file, &format!("blk.{l}.attn_k_norm.weight"))?;
1962 if let Some(ref w) = q_norm {
1964 if w.len() == config.head_dim {
1965 refined_qk_norm = crate::capability::QkNormStyle::PerHead;
1966 } else if w.len() == config.n_heads * config.head_dim {
1967 refined_qk_norm = crate::capability::QkNormStyle::WholeVector;
1968 } else {
1969 return Err(LoadError::UnsupportedFeature(
1970 config.name.to_string(),
1971 format!(
1972 "blk.{l}.attn_q_norm.weight length {} matches neither head_dim={} \
1973 nor n_heads*head_dim={}",
1974 w.len(),
1975 config.head_dim,
1976 config.n_heads * config.head_dim
1977 ),
1978 ));
1979 }
1980 }
1981 let attn = AttnWeights {
1982 q_proj,
1983 k_proj,
1984 v_proj,
1985 o_proj: load_weight_matrix(&file, &format!("blk.{l}.attn_output.weight"))?,
1986 norm_weight: load_f32_vec(&file, &format!("blk.{l}.attn_norm.weight"))?,
1987 q_norm,
1988 k_norm,
1989 q_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_q.bias"))?,
1993 k_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_k.bias"))?,
1994 v_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_v.bias"))?,
1995 post_attn_norm: if post_attn_norm_is_pre_ffn_norm {
2001 None
2002 } else {
2003 load_f32_vec_optional(&file, &format!("blk.{l}.post_attention_norm.weight"))?
2004 },
2005 post_ffn_norm: load_f32_vec_optional(
2006 &file,
2007 &format!("blk.{l}.post_ffw_norm.weight"),
2008 )?,
2009 };
2010
2011 let is_dense_layer = config.layer_is_dense(l) || config.moe.n_experts <= 1;
2019 let n_experts = if is_dense_layer {
2020 1
2021 } else {
2022 config.moe.n_experts
2023 };
2024 let experts: ExpertBacking = if is_dense_layer {
2025 ExpertBacking::Resident(vec![load_dense_expert(&file, l, &config)?])
2026 } else {
2027 let stored = if expert_cache_bytes.is_some() {
2031 let g = stored_expert_specs(
2032 &file,
2033 &format!("blk.{l}.ffn_gate_exps.weight"),
2034 n_experts,
2035 )?;
2036 let u = stored_expert_specs(
2037 &file,
2038 &format!("blk.{l}.ffn_up_exps.weight"),
2039 n_experts,
2040 )?;
2041 let d = stored_expert_specs(
2042 &file,
2043 &format!("blk.{l}.ffn_down_exps.weight"),
2044 n_experts,
2045 )?;
2046 match (g, u, d) {
2047 (Some(gt), Some(ut), Some(dt)) => {
2048 let mut layouts = Vec::with_capacity(n_experts);
2049 for e in 0..n_experts {
2050 let key = ExpertKey {
2051 layer: l as u32,
2052 expert: e as u32,
2053 };
2054 store_segments.insert(
2055 key,
2056 [
2057 (gt.shard, gt.per_expert[e].0, gt.per_expert[e].1),
2058 (ut.shard, ut.per_expert[e].0, ut.per_expert[e].1),
2059 (dt.shard, dt.per_expert[e].0, dt.per_expert[e].1),
2060 ],
2061 );
2062 let mut gate = gt.spec;
2063 let mut up = ut.spec;
2064 let mut down = dt.spec;
2065 gate.offset = 0;
2066 up.offset = gate.len;
2067 down.offset = gate.len + up.len;
2068 layouts.push(StoredExpertLayout { gate, up, down });
2069 }
2070 Some(layouts)
2071 }
2072 _ => None,
2073 }
2074 } else {
2075 None
2076 };
2077 match stored {
2078 Some(layouts) => {
2079 stored_layouts.push(Some(layouts));
2083 ExpertBacking::Resident(Vec::new())
2084 }
2085 None => {
2086 let gates = split_expert_tensor(
2087 &file,
2088 &format!("blk.{l}.ffn_gate_exps.weight"),
2089 n_experts,
2090 )?;
2091 let ups = split_expert_tensor(
2092 &file,
2093 &format!("blk.{l}.ffn_up_exps.weight"),
2094 n_experts,
2095 )?;
2096 let downs = split_expert_tensor(
2097 &file,
2098 &format!("blk.{l}.ffn_down_exps.weight"),
2099 n_experts,
2100 )?;
2101 ExpertBacking::Resident(
2102 gates
2103 .into_iter()
2104 .zip(ups)
2105 .zip(downs)
2106 .map(|((gate, up), down)| ExpertWeights { gate, up, down })
2107 .collect(),
2108 )
2109 }
2110 }
2111 };
2112 if stored_layouts.len() < layers.len() + 1 {
2113 stored_layouts.push(None);
2114 }
2115
2116 let shared_experts: Vec<ExpertWeights> =
2117 if config.moe.n_shared_experts > 0 && !is_dense_layer {
2118 vec![ExpertWeights {
2119 gate: load_weight_matrix(&file, &format!("blk.{l}.ffn_gate_shexp.weight"))?,
2120 up: load_weight_matrix(&file, &format!("blk.{l}.ffn_up_shexp.weight"))?,
2121 down: load_weight_matrix(&file, &format!("blk.{l}.ffn_down_shexp.weight"))?,
2122 }]
2123 } else {
2124 Vec::new()
2125 };
2126
2127 let router = if !is_dense_layer {
2128 load_weight_matrix(&file, &format!("blk.{l}.ffn_gate_inp.weight"))?
2129 } else {
2130 WeightMatrix::F32(Tensor::zeros(vec![1, config.hidden_dim]))
2133 };
2134
2135 let n_for_counts = match &experts {
2136 ExpertBacking::Resident(v) if v.is_empty() => n_experts,
2137 other => other.n_experts(),
2138 };
2139 let activation_counts = (0..n_for_counts)
2140 .map(|_| std::sync::atomic::AtomicU64::new(0))
2141 .collect();
2142 let shared_expert_gate = if is_dense_layer {
2151 None
2152 } else {
2153 load_f32_vec_optional(&file, &format!("blk.{l}.ffn_gate_inp_shexp.weight"))?
2154 };
2155 #[cfg(feature = "metal")]
2156 let packed_q4 = match &experts {
2157 ExpertBacking::Resident(v) if !v.is_empty() => try_build_moe_packed_q4_planes(v),
2158 _ => None,
2159 };
2160 let exp_probs_bias = if is_dense_layer {
2168 None
2169 } else {
2170 load_f32_vec_optional(&file, &format!("blk.{l}.exp_probs_b.bias"))?
2171 };
2172 if let Some(bias) = &exp_probs_bias {
2173 if bias.len() != config.moe.n_experts {
2174 return Err(LoadError::UnsupportedFeature(
2175 arch.clone(),
2176 format!(
2177 "blk.{l}.exp_probs_b.bias has {} entries but the model has {} experts",
2178 bias.len(),
2179 config.moe.n_experts
2180 ),
2181 ));
2182 }
2183 if config.moe.expert_group_count.is_some() {
2190 return Err(LoadError::UnsupportedFeature(
2191 arch.clone(),
2192 format!(
2193 "blk.{l}.exp_probs_b.bias together with expert groups \
2194 ({:?}): llama.cpp masks the biased scores per group \
2195 before a global top-k, which is not the per-group \
2196 top-k ferrox implements",
2197 config.moe.expert_group_count
2198 ),
2199 ));
2200 }
2201 }
2202 let moe = MoeWeights {
2203 router,
2204 experts,
2205 shared_experts,
2206 shared_expert_gate,
2207 exp_probs_bias,
2208 norm_weight: if post_attn_norm_is_pre_ffn_norm {
2209 load_f32_vec(&file, &format!("blk.{l}.post_attention_norm.weight"))?
2210 } else {
2211 load_f32_vec(&file, &format!("blk.{l}.ffn_norm.weight"))?
2212 },
2213 activation_counts,
2214 #[cfg(feature = "metal")]
2215 packed_q4,
2216 };
2217
2218 if is_gpt_oss {
2219 gpt_oss_layers.push(load_gpt_oss_layer(&file, l, &config)?);
2220 }
2221
2222 layers.push(LayerWeights { attn, moe });
2223 }
2224
2225 let final_norm = load_f32_vec(&file, "output_norm.weight")?;
2226 let output_head = match load_weight_matrix(&file, "output.weight") {
2231 Ok(w) => w,
2232 Err(_) => load_weight_matrix(&file, "token_embd.weight")?,
2233 };
2234
2235 if !store_segments.is_empty() {
2241 let budget = expert_cache_bytes
2242 .expect("store_segments only populated when a cache budget is set")
2243 as usize;
2244 let files: Result<Vec<std::fs::File>, std::io::Error> =
2245 file.shard_paths().iter().map(std::fs::File::open).collect();
2246 let files = files.map_err(GgufError::from)?;
2247 let store = std::sync::Arc::new(ExpertStore::new(
2248 GgufExpertSource {
2249 files,
2250 segments: store_segments,
2251 },
2252 budget,
2253 ));
2254 for (l, layer) in layers.iter_mut().enumerate() {
2255 if let Some(layouts) = stored_layouts.get_mut(l).and_then(Option::take) {
2256 layer.moe.experts = ExpertBacking::Stored {
2257 store: std::sync::Arc::clone(&store),
2258 layouts,
2259 layer: l as u32,
2260 };
2261 }
2262 }
2263 }
2264
2265 config.qk_norm_style = refined_qk_norm;
2266
2267 let family = crate::capability::resolve_profile(
2268 file.metadata_str("general.architecture").unwrap_or("llama"),
2269 )
2270 .map(|p| p.family)
2271 .unwrap_or(crate::capability::DecoderFamily::StandardGqa);
2272 let memory_kind = crate::capability::resolve_profile(
2273 file.metadata_str("general.architecture").unwrap_or("llama"),
2274 )
2275 .map(|p| p.memory)
2276 .unwrap_or(crate::capability::MemoryKind::KvGqa);
2277 let execution_plan = crate::execution_plan::ExecutionPlan::from_config(
2278 &config,
2279 family,
2280 memory_kind,
2281 crate::execution_plan::ExecutionPlan::probe_metal_caps(),
2282 );
2283
2284 let decoder = Decoder {
2285 config,
2286 embedding,
2287 layers,
2288 final_norm,
2289 output_head,
2290 gpu_vram_budget_bytes: None,
2291 gpt_oss: if is_gpt_oss {
2292 Some(crate::decoder::GptOssWeights {
2293 layers: gpt_oss_layers,
2294 })
2295 } else {
2296 None
2297 },
2298 qk_norm_after_rope: QK_NORM_AFTER_ROPE_ARCHITECTURES.contains(&arch.as_str()),
2299 #[cfg(feature = "metal")]
2300 metal_attn_kv: std::sync::Mutex::new(None),
2301 execution_plan,
2302 plan_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
2303 };
2304 decoder.probe_kernels();
2308 ferrox_core::kernel_registry::seal_or_error()
2309 .map_err(|e| LoadError::StrictKernels(e.to_string()))?;
2310 for name in crate::config::MODEL_LEVEL_TENSORS_READ_BY_CONFIG {
2317 file.note_consumed(name);
2318 }
2319 assert_every_tensor_consumed(&file)?;
2320 Ok(decoder)
2321 }
2322}
2323
2324const IGNORED_TENSOR_PREFIXES: &[&str] = &["mm.", "v.", "mmproj.", "resampler.", "audio."];
2329
2330pub fn assert_every_tensor_consumed(file: &ShardedGguf) -> Result<(), LoadError> {
2352 let mut left: Vec<String> = file
2353 .unconsumed_tensors()
2354 .into_iter()
2355 .filter(|n| !IGNORED_TENSOR_PREFIXES.iter().any(|p| n.starts_with(p)))
2356 .collect();
2357 if left.is_empty() {
2358 return Ok(());
2359 }
2360 left.sort();
2361 let shown = left.iter().take(8).cloned().collect::<Vec<_>>().join(", ");
2362 let listing = if left.len() > 8 {
2363 format!("{shown}, … (+{} more)", left.len() - 8)
2364 } else {
2365 shown
2366 };
2367 if matches!(
2368 std::env::var("FERROX_ALLOW_UNKNOWN_TENSORS")
2369 .ok()
2370 .as_deref(),
2371 Some("1") | Some("true") | Some("on")
2372 ) {
2373 eprintln!(
2374 "ferrox: WARNING -- {} tensor(s) in this checkpoint are never read \
2375 ({listing}); output may be wrong (FERROX_ALLOW_UNKNOWN_TENSORS=1)",
2376 left.len()
2377 );
2378 return Ok(());
2379 }
2380 Err(LoadError::UnconsumedTensors(left.len(), listing))
2381}
2382
2383#[cfg(test)]
2384mod tests {
2385
2386 #[test]
2402 fn a_quantized_one_dimensional_tensor_widens_through_the_shared_helper() {
2403 let values: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) * 0.25).collect();
2404 let quantized = ferrox_quant::quantize_q8_0(&values);
2405
2406 struct OneTensor {
2407 info: TensorInfo,
2408 bytes: Vec<u8>,
2409 }
2410 impl TensorSource for OneTensor {
2411 fn metadata(&self, _key: &str) -> Option<&ferrox_gguf::GgufValue> {
2412 None
2413 }
2414 fn find_tensor(&self, name: &str) -> Option<&TensorInfo> {
2415 (name == self.info.name).then_some(&self.info)
2416 }
2417 fn tensor_bytes(&self, _name: &str) -> Result<&[u8], GgufError> {
2418 Ok(&self.bytes)
2419 }
2420 fn tensor_mapped_range(
2421 &self,
2422 name: &str,
2423 ) -> Result<
2424 (
2425 std::sync::Arc<ferrox_gguf::MmapHandle>,
2426 std::ops::Range<usize>,
2427 ),
2428 GgufError,
2429 > {
2430 Err(GgufError::TensorNotFound(name.to_string()))
2432 }
2433 }
2434
2435 let source = OneTensor {
2436 info: TensorInfo {
2437 name: "blk.0.attn_norm.weight".to_string(),
2438 shape: vec![64],
2439 dtype: GgmlType::Q8_0,
2440 offset: 0,
2441 },
2442 bytes: quantized,
2443 };
2444
2445 let widened = load_f32_vec(&source, "blk.0.attn_norm.weight")
2446 .expect("a Q8_0 norm must load, not report an unsupported dtype");
2447 assert_eq!(widened.len(), values.len());
2448 for (got, want) in widened.iter().zip(values.iter()) {
2449 assert!(
2450 (got - want).abs() < 0.05,
2451 "q8_0 round trip: got {got}, want {want}"
2452 );
2453 }
2454 }
2455 use super::*;
2456 use byteorder::{LittleEndian, WriteBytesExt};
2457 use std::io::Write;
2458
2459 fn write_string(buf: &mut Vec<u8>, s: &str) {
2460 buf.write_u64::<LittleEndian>(s.len() as u64).unwrap();
2461 buf.write_all(s.as_bytes()).unwrap();
2462 }
2463
2464 fn write_kv_str(buf: &mut Vec<u8>, key: &str, val: &str) {
2465 write_string(buf, key);
2466 buf.write_u32::<LittleEndian>(8).unwrap(); write_string(buf, val);
2468 }
2469
2470 fn build_arch_only_gguf(arch: &str) -> Vec<u8> {
2476 let mut buf = Vec::new();
2477 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2478 .unwrap();
2479 buf.write_u32::<LittleEndian>(3).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); write_kv_str(&mut buf, "general.architecture", arch);
2483 buf
2484 }
2485
2486 #[test]
2487 fn model_config_from_gguf_fails_loudly_when_required_hparams_are_missing() {
2488 let tmp =
2489 std::env::temp_dir().join(format!("ferrox_test_arch_only_{}.gguf", std::process::id()));
2490 std::fs::write(&tmp, build_arch_only_gguf("llama")).unwrap();
2493 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2494 std::fs::remove_file(&tmp).ok();
2495
2496 match ModelConfig::from_gguf(&file) {
2497 Err(LoadError::MissingHparam(key)) => {
2498 assert_eq!(key, "llama.block_count");
2499 }
2500 other => panic!(
2501 "expected LoadError::MissingHparam for a file with no hparam keys, got {other:?}"
2502 ),
2503 }
2504 }
2505
2506 #[test]
2507 fn model_config_from_gguf_fails_closed_on_unknown_architecture() {
2508 let tmp = std::env::temp_dir().join(format!(
2509 "ferrox_test_unknown_arch_{}.gguf",
2510 std::process::id()
2511 ));
2512 std::fs::write(&tmp, build_arch_only_gguf("bogus-arch-with-no-hparams")).unwrap();
2513 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2514 std::fs::remove_file(&tmp).ok();
2515
2516 match ModelConfig::from_gguf(&file) {
2517 Err(LoadError::UnsupportedArchitecture(arch)) => {
2518 assert_eq!(arch, "bogus-arch-with-no-hparams");
2519 }
2520 other => panic!(
2521 "expected LoadError::UnsupportedArchitecture for an unregistered arch, got {other:?}"
2522 ),
2523 }
2524 }
2525
2526 fn write_kv_f32(buf: &mut Vec<u8>, key: &str, val: f32) {
2527 write_string(buf, key);
2528 buf.write_u32::<LittleEndian>(6).unwrap(); buf.write_f32::<LittleEndian>(val).unwrap();
2530 }
2531
2532 fn build_arch_plus_f32_gguf(arch: &str, key: &str, val: f32) -> Vec<u8> {
2535 let mut buf = Vec::new();
2536 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2537 .unwrap();
2538 buf.write_u32::<LittleEndian>(3).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); buf.write_u64::<LittleEndian>(2).unwrap(); write_kv_str(&mut buf, "general.architecture", arch);
2542 write_kv_f32(&mut buf, key, val);
2543 buf
2544 }
2545
2546 fn config_error_for(arch: &str, key: &str, val: f32, tag: &str) -> LoadError {
2547 let tmp = std::env::temp_dir().join(format!("ferrox_test_scale_{tag}.gguf"));
2548 std::fs::write(&tmp, build_arch_plus_f32_gguf(arch, key, val)).unwrap();
2549 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2550 std::fs::remove_file(&tmp).ok();
2551 ModelConfig::from_gguf(&file).expect_err("must not succeed")
2552 }
2553
2554 #[test]
2560 fn a_declared_multiplier_this_decoder_does_not_apply_is_refused_by_name() {
2561 for (key, val) in [
2562 ("granite.logit_scale", 6.0f32),
2563 ("granite.residual_scale", 0.22),
2564 ("granite.embedding_scale", 12.0),
2565 ("granite.attention.scale", 0.015_625),
2566 ] {
2567 let tag = key.replace('.', "_");
2568 match config_error_for("granite", key, val, &tag) {
2569 LoadError::UnsupportedFeature(arch, msg) => {
2570 assert_eq!(arch, "granite");
2571 assert!(msg.contains(key), "error must name the key: {msg}");
2572 }
2573 other => panic!("expected UnsupportedFeature for {key}, got {other:?}"),
2574 }
2575 }
2576 }
2577
2578 #[test]
2584 fn a_multiplier_that_is_a_no_op_is_not_refused() {
2585 for (key, val) in [
2586 ("granite.logit_scale", 1.0f32),
2587 ("granite.residual_scale", 1.0),
2588 ("granite.embedding_scale", 1.0),
2589 ("granite.attention.scale", 0.0),
2590 ] {
2591 let tag = format!("noop_{}", key.replace('.', "_"));
2592 match config_error_for("granite", key, val, &tag) {
2595 LoadError::MissingHparam(k) => assert_eq!(k, "granite.block_count"),
2596 other => panic!("no-op {key}={val} must pass the scaling gate, got {other:?}"),
2597 }
2598 }
2599 }
2600
2601 enum Kv<'a> {
2604 Str(&'a str),
2605 U32(u32),
2606 F32(f32),
2607 }
2608
2609 fn build_metadata_gguf(kvs: &[(&str, Kv)]) -> Vec<u8> {
2612 let mut buf = Vec::new();
2613 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2614 .unwrap();
2615 buf.write_u32::<LittleEndian>(3).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); buf.write_u64::<LittleEndian>(kvs.len() as u64).unwrap();
2618 for (k, v) in kvs {
2619 match v {
2620 Kv::Str(s) => write_kv_str(&mut buf, k, s),
2621 Kv::U32(n) => {
2622 write_string(&mut buf, k);
2623 buf.write_u32::<LittleEndian>(4).unwrap(); buf.write_u32::<LittleEndian>(*n).unwrap();
2625 }
2626 Kv::F32(f) => write_kv_f32(&mut buf, k, *f),
2627 }
2628 }
2629 buf
2630 }
2631
2632 fn open_metadata_gguf(tag: &str, kvs: &[(&str, Kv)]) -> ferrox_gguf::GgufFile {
2633 let tmp = std::env::temp_dir().join(format!("ferrox_test_meta_{tag}.gguf"));
2634 std::fs::write(&tmp, build_metadata_gguf(kvs)).unwrap();
2635 let file = ferrox_gguf::GgufFile::open(&tmp).expect("header-only file must parse");
2636 std::fs::remove_file(&tmp).ok();
2637 file
2638 }
2639
2640 fn llama_config_with(tag: &str, extra: &[(&str, Kv)]) -> ModelConfig {
2643 let mut kvs: Vec<(&str, Kv)> = vec![
2644 ("general.architecture", Kv::Str("llama")),
2645 ("llama.block_count", Kv::U32(1)),
2646 ("llama.embedding_length", Kv::U32(64)),
2647 ("llama.attention.head_count", Kv::U32(1)),
2648 ("llama.attention.head_count_kv", Kv::U32(1)),
2649 ("llama.attention.key_length", Kv::U32(64)),
2650 ("llama.rope.freq_base", Kv::F32(10_000.0)),
2651 ];
2652 for (k, v) in extra {
2653 kvs.push((
2654 k,
2655 match v {
2656 Kv::Str(s) => Kv::Str(s),
2657 Kv::U32(n) => Kv::U32(*n),
2658 Kv::F32(f) => Kv::F32(*f),
2659 },
2660 ));
2661 }
2662 ModelConfig::from_gguf(&open_metadata_gguf(tag, &kvs)).expect("fixture must load")
2663 }
2664
2665 #[test]
2679 fn the_ffn_activation_follows_the_architecture_not_the_family() {
2680 use crate::capability::uses_geglu;
2681 use crate::config::FfnActivation;
2682
2683 assert!(uses_geglu("grok"), "grok's MoE FFN gate is GELU upstream");
2684 assert!(!uses_geglu("dbrx"));
2687 assert!(!uses_geglu("llama"));
2688
2689 for gemma in ["gemma2", "gemma3"] {
2694 assert!(
2695 !uses_geglu(gemma),
2696 "{gemma} is GELU via GemmaFamily; listing it here too \
2697 would hide a later regression in the family rule"
2698 );
2699 assert_eq!(
2700 config_for_arch(gemma).expect("gemma loads").ffn_activation,
2701 FfnActivation::Gelu,
2702 "{gemma}"
2703 );
2704 }
2705
2706 assert_eq!(
2708 config_for_arch("llama")
2709 .expect("llama loads")
2710 .ffn_activation,
2711 FfnActivation::Swiglu
2712 );
2713 }
2714
2715 #[test]
2731 fn the_architectures_llama_cpp_does_not_renormalise_are_pinned() {
2732 for arch in ["deepseek", "olmoe", "qwen2moe"] {
2733 assert!(
2734 NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&arch),
2735 "{arch} passes norm_w=false in llama.cpp and must not be renormalised"
2736 );
2737 }
2738 assert!(!NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"deepseek2"));
2742 assert!(!NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"qwen3moe"));
2743 }
2744
2745 #[test]
2755 fn the_architectures_llama_cpp_defaults_to_sigmoid_gating_are_pinned() {
2756 for arch in ["afmoe", "deepseek2", "glm4moe", "laguna", "step35"] {
2757 assert!(
2758 SIGMOID_GATING_ARCHITECTURES.contains(&arch),
2759 "{arch} sets SIGMOID when the gating key is absent"
2760 );
2761 }
2762 for softmax in ["ernie4_5-moe", "qwen3moe", "olmoe", "llama"] {
2766 assert!(
2767 !SIGMOID_GATING_ARCHITECTURES.contains(&softmax),
2768 "{softmax} does not default to sigmoid"
2769 );
2770 }
2771 }
2772
2773 #[test]
2792 fn every_architecture_keyed_behaviour_table_names_a_real_generic_row() {
2793 let tables: &[(&str, &[&str])] = &[
2794 ("SIGMOID_GATING_ARCHITECTURES", SIGMOID_GATING_ARCHITECTURES),
2795 (
2796 "NO_TOPK_RENORMALIZE_ARCHITECTURES",
2797 NO_TOPK_RENORMALIZE_ARCHITECTURES,
2798 ),
2799 (
2800 "PRE_FFN_NORM_IS_POST_ATTENTION_NORM",
2801 PRE_FFN_NORM_IS_POST_ATTENTION_NORM,
2802 ),
2803 ("LEADING_DENSE_KEY_IS_INERT", LEADING_DENSE_KEY_IS_INERT),
2804 (
2805 "QK_NORM_AFTER_ROPE_ARCHITECTURES",
2806 QK_NORM_AFTER_ROPE_ARCHITECTURES,
2807 ),
2808 ];
2809 for (table, names) in tables {
2810 for arch in *names {
2811 let profile = crate::capability::resolve_profile(arch).unwrap_or_else(|| {
2812 panic!("{table} names `{arch}`, which the catalog does not have")
2813 });
2814 if matches!(profile.path, crate::capability::ArchPath::GenericGqa { .. }) {
2815 continue;
2816 }
2817 let owner = DEDICATED_OWNS_ITS_BEHAVIOUR
2822 .iter()
2823 .find(|(name, _)| name == arch)
2824 .map(|(_, owner)| *owner);
2825 assert!(
2826 owner.is_some(),
2827 "{table} names `{arch}`, which resolves to {:?} and never reaches this \
2828 loader, so the entry cannot fire. Either drop it, or add it to \
2829 DEDICATED_OWNS_ITS_BEHAVIOUR naming what applies the behaviour instead",
2830 profile.path
2831 );
2832 }
2833 }
2834 }
2835
2836 #[test]
2850 fn the_layer_shape_tables_only_name_audited_architectures() {
2851 for (table, names) in [
2852 (
2853 "PRE_FFN_NORM_IS_POST_ATTENTION_NORM",
2854 PRE_FFN_NORM_IS_POST_ATTENTION_NORM,
2855 ),
2856 ("LEADING_DENSE_KEY_IS_INERT", LEADING_DENSE_KEY_IS_INERT),
2857 (
2858 "QK_NORM_AFTER_ROPE_ARCHITECTURES",
2859 QK_NORM_AFTER_ROPE_ARCHITECTURES,
2860 ),
2861 ] {
2862 for arch in names {
2863 assert!(
2864 crate::capability::is_audited_generic(arch),
2865 "{table} names `{arch}`, which is not in AUDITED_GENERIC_GQA. Either it \
2866 has a fixture proving the change is right -- audit it -- or the entry \
2867 is a guess about a graph"
2868 );
2869 }
2870 }
2871 }
2872
2873 fn config_for_arch(arch: &'static str) -> Result<ModelConfig, LoadError> {
2874 let keys: Vec<String> = [
2877 "block_count",
2878 "embedding_length",
2879 "attention.head_count",
2880 "attention.head_count_kv",
2881 "attention.key_length",
2882 ]
2883 .iter()
2884 .map(|k| format!("{arch}.{k}"))
2885 .collect();
2886 let theta = format!("{arch}.rope.freq_base");
2887 let kvs: Vec<(&str, Kv)> = vec![
2888 ("general.architecture", Kv::Str(arch)),
2889 (keys[0].as_str(), Kv::U32(1)),
2890 (keys[1].as_str(), Kv::U32(64)),
2891 (keys[2].as_str(), Kv::U32(1)),
2892 (keys[3].as_str(), Kv::U32(1)),
2893 (keys[4].as_str(), Kv::U32(64)),
2894 (theta.as_str(), Kv::F32(10_000.0)),
2895 ];
2896 ModelConfig::from_gguf(&open_metadata_gguf(arch, &kvs))
2897 }
2898
2899 #[test]
2907 fn an_unaudited_generic_architecture_refuses_rather_than_guessing() {
2908 assert!(
2916 !crate::capability::is_audited_generic("xverse"),
2917 "this test needs an arch that is generic AND unaudited"
2918 );
2919 match config_for_arch("xverse") {
2920 Err(LoadError::UnauditedArchitecture(name, ..)) => assert_eq!(name, "xverse"),
2921 other => panic!("expected an unaudited refusal, got {other:?}"),
2922 }
2923 }
2924
2925 #[test]
2928 fn an_audited_architecture_still_loads() {
2929 assert!(crate::capability::is_audited_generic("llama"));
2930 assert!(config_for_arch("llama").is_ok());
2931 }
2932
2933 #[test]
2940 fn a_named_refusal_outranks_the_unaudited_one() {
2941 let err = config_for_arch("gpt2").expect_err("gpt2 must refuse");
2942 assert!(
2943 !matches!(err, LoadError::UnauditedArchitecture(..)),
2944 "gpt2 should report its own reason, not that nobody audited it: {err:?}"
2945 );
2946 }
2947
2948 #[test]
2959 fn a_gguf_declaring_yarn_gets_its_rope_frequencies_rewritten() {
2960 let cfg = llama_config_with(
2961 "yarn",
2962 &[
2963 ("llama.rope.scaling.type", Kv::Str("yarn")),
2964 ("llama.rope.scaling.factor", Kv::F32(8.0)),
2965 (
2966 "llama.rope.scaling.original_context_length",
2967 Kv::U32(131_072),
2968 ),
2969 ],
2970 );
2971 let factors = cfg
2972 .rope_freqs
2973 .expect("a YaRN checkpoint must carry rewritten per-band frequencies")
2974 .full;
2975 assert_eq!(factors.len(), 32, "one divisor per rotation band");
2976 assert!(
2977 (factors[0] - 1.0).abs() < 1e-6,
2978 "the fastest band is left extrapolated, got {}",
2979 factors[0]
2980 );
2981 let ramp = (31.0 - 22.0) / (35.0 - 22.0);
2982 let want = 1.0 / (ramp / 8.0 + (1.0 - ramp));
2983 assert!(
2984 (factors[31] - want).abs() < 1e-4,
2985 "slowest band: got {}, reference {want}",
2986 factors[31]
2987 );
2988 }
2989
2990 #[test]
3005 fn linear_scaling_is_applied_as_a_uniform_frequency_divisor() {
3006 let cfg = llama_config_with(
3007 "linear",
3008 &[
3009 ("llama.rope.scaling.type", Kv::Str("linear")),
3010 ("llama.rope.scaling.factor", Kv::F32(4.0)),
3011 ],
3012 );
3013 let freqs = &cfg
3014 .rope_freqs
3015 .as_ref()
3016 .expect("linear scaling must produce frequency factors")
3017 .full;
3018 assert_eq!(freqs.len(), cfg.head_dim / 2, "one factor per rotated pair");
3019 assert!(
3020 freqs.iter().all(|f| (*f - 4.0).abs() < 1e-6),
3021 "linear scaling is uniform across bands, unlike YaRN: got {freqs:?}"
3022 );
3023 }
3024
3025 #[test]
3027 fn a_linear_factor_of_one_is_treated_as_absent() {
3028 assert!(llama_config_with(
3029 "linear_one",
3030 &[
3031 ("llama.rope.scaling.type", Kv::Str("linear")),
3032 ("llama.rope.scaling.factor", Kv::F32(1.0)),
3033 ],
3034 )
3035 .rope_freqs
3036 .is_none());
3037 }
3038
3039 #[test]
3040 fn a_gguf_without_yarn_scaling_keeps_its_rope_frequencies_untouched() {
3041 assert!(llama_config_with("noscale", &[]).rope_freqs.is_none());
3042 assert!(llama_config_with(
3049 "yarn_factor_one",
3050 &[
3051 ("llama.rope.scaling.type", Kv::Str("yarn")),
3052 ("llama.rope.scaling.factor", Kv::F32(1.0)),
3053 (
3054 "llama.rope.scaling.original_context_length",
3055 Kv::U32(131_072),
3056 ),
3057 ],
3058 )
3059 .rope_freqs
3060 .is_none());
3061 }
3062
3063 #[test]
3070 fn yarn_without_an_original_context_length_is_not_guessed_at() {
3071 let cfg = llama_config_with(
3072 "yarn_noctx",
3073 &[
3074 ("llama.rope.scaling.type", Kv::Str("yarn")),
3075 ("llama.rope.scaling.factor", Kv::F32(8.0)),
3076 ],
3077 );
3078 assert!(cfg.rope_freqs.is_none());
3079 }
3080
3081 #[test]
3086 fn gguf_sampling_metadata_is_read_as_the_checkpoints_recommendation() {
3087 use crate::sampling::RecommendedSampling;
3088 let full = RecommendedSampling::from_gguf(&open_metadata_gguf(
3089 "sampling_full",
3090 &[
3091 ("general.architecture", Kv::Str("llama")),
3092 ("general.sampling.temp", Kv::F32(1.0)),
3093 ("general.sampling.top_k", Kv::U32(20)),
3094 ("general.sampling.top_p", Kv::F32(0.95)),
3095 ],
3096 ));
3097 assert_eq!(
3098 full,
3099 RecommendedSampling {
3100 temperature: Some(1.0),
3101 top_p: Some(0.95),
3102 top_k: Some(20),
3103 }
3104 );
3105
3106 let partial = RecommendedSampling::from_gguf(&open_metadata_gguf(
3107 "sampling_partial",
3108 &[
3109 ("general.architecture", Kv::Str("llama")),
3110 ("general.sampling.top_k", Kv::U32(40)),
3111 ],
3112 ));
3113 assert_eq!(partial.top_k, Some(40));
3114 assert_eq!(partial.temperature, None);
3115 assert_eq!(partial.top_p, None);
3116 }
3117
3118 #[test]
3123 fn an_integer_valued_sampling_temp_is_still_a_recommendation() {
3124 let recommended = crate::sampling::RecommendedSampling::from_gguf(&open_metadata_gguf(
3125 "sampling_int_temp",
3126 &[
3127 ("general.architecture", Kv::Str("llama")),
3128 ("general.sampling.temp", Kv::U32(1)),
3129 ],
3130 ));
3131 assert_eq!(recommended.temperature, Some(1.0));
3132 }
3133
3134 #[test]
3137 fn a_gguf_without_sampling_metadata_recommends_nothing() {
3138 let recommended = crate::sampling::RecommendedSampling::from_gguf(&open_metadata_gguf(
3139 "sampling_absent",
3140 &[("general.architecture", Kv::Str("llama"))],
3141 ));
3142 assert!(recommended.is_empty());
3143 }
3144
3145 #[test]
3146 fn model_config_from_gguf_rejects_dedicated_architectures() {
3147 let tmp = std::env::temp_dir().join(format!(
3148 "ferrox_test_dedicated_arch_{}.gguf",
3149 std::process::id()
3150 ));
3151 std::fs::write(&tmp, build_arch_only_gguf("deepseek4")).unwrap();
3152 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
3153 std::fs::remove_file(&tmp).ok();
3154
3155 match ModelConfig::from_gguf(&file) {
3156 Err(LoadError::DedicatedArchitectureRequired(arch, _)) => {
3157 assert_eq!(arch, "deepseek4");
3158 }
3159 other => panic!(
3160 "expected LoadError::DedicatedArchitectureRequired for deepseek4, got {other:?}"
3161 ),
3162 }
3163 }
3164
3165 #[rustfmt::skip]
3169 const Q5_K_TEST_BLOCK: [u8; 176] = [
3170 0x66, 0x2a, 0x66, 0x2a, 0x01, 0x01, 0x01, 0x01, 0x4f, 0x4b, 0x10, 0x12, 0x41, 0xe2, 0xc1,
3171 0xb1, 0x72, 0x2f, 0x20, 0x07, 0x31, 0x0c, 0x38, 0xb3, 0x9c, 0xb8, 0xad, 0x2f, 0x9a, 0xea,
3172 0x17, 0xd0, 0xee, 0x93, 0x9e, 0x3e, 0x74, 0xbb, 0x28, 0x18, 0x39, 0x25, 0xb6, 0x09, 0x18,
3173 0x29, 0x1c, 0x1d, 0x29, 0x41, 0x40, 0x0a, 0x74, 0x7d, 0xfd, 0x21, 0xdd, 0x6d, 0x45, 0x73,
3174 0x0e, 0x1e, 0xc0, 0x4a, 0xfc, 0xf3, 0x8e, 0x24, 0x6b, 0x34, 0x7d, 0xbe, 0x94, 0xde, 0x59,
3175 0x7a, 0x35, 0x30, 0x36, 0x0a, 0xf9, 0x4a, 0x9b, 0xa2, 0x26, 0x21, 0xa2, 0xfa, 0xdf, 0x4b,
3176 0x29, 0x64, 0x6f, 0xbb, 0xca, 0x0f, 0x3c, 0xda, 0x20, 0xf4, 0x93, 0x86, 0xab, 0x6e, 0xb9,
3177 0xe5, 0xd5, 0xa0, 0x82, 0xd6, 0x41, 0xff, 0x12, 0xbc, 0x34, 0xbb, 0xab, 0xb8, 0x20, 0x2f,
3178 0xbb, 0x5f, 0x0c, 0x10, 0xcf, 0x49, 0xc5, 0x86, 0x5c, 0xdf, 0xff, 0x78, 0x44, 0x26, 0x3b,
3179 0xc2, 0x23, 0x3d, 0x2b, 0xe9, 0x00, 0x12, 0xf8, 0xea, 0xe2, 0x9e, 0x5e, 0x50, 0x20, 0x9f,
3180 0x9d, 0x8d, 0x7d, 0x7f, 0xcc, 0x1d, 0x0e, 0x13, 0xf8, 0xc2, 0xf1, 0x3d, 0x08, 0x2f, 0x23,
3181 0x13, 0xac, 0x0d, 0xa7, 0xe7, 0x20, 0xa3, 0x90, 0xb7, 0xc8, 0x28,
3182 ];
3183
3184 fn build_single_q5_k_tensor_gguf() -> Vec<u8> {
3185 let mut buf = Vec::new();
3186 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3187 .unwrap();
3188 buf.write_u32::<LittleEndian>(3).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); write_kv_str(&mut buf, "general.architecture", "ferrox-q5k-test");
3193
3194 write_string(&mut buf, "test.weight");
3195 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(256).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u32::<LittleEndian>(13).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
3205 buf.push(0);
3206 }
3207 buf.extend_from_slice(&Q5_K_TEST_BLOCK);
3208 buf
3209 }
3210
3211 fn fused_dot_tolerance(weights: &[f32], x: &[f32], exact_bound: f32) -> f32 {
3231 if !ferrox_core::weight_matrix::cpu_int_dot_enabled() {
3232 return exact_bound;
3233 }
3234 let amax = x.iter().fold(0.0f32, |a, v| a.max(v.abs()));
3235 let l2 = weights.iter().map(|w| w * w).sum::<f32>().sqrt();
3236 4.0 * (amax / 127.0) / 12f32.sqrt() * l2 + exact_bound
3237 }
3238
3239 #[test]
3240 fn load_weight_matrix_handles_a_real_on_disk_q5_k_tensor_end_to_end() {
3241 let tmp = std::env::temp_dir().join(format!(
3242 "ferrox_test_q5k_tensor_{}.gguf",
3243 std::process::id()
3244 ));
3245 std::fs::write(&tmp, build_single_q5_k_tensor_gguf()).unwrap();
3246 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q5_K GGUF file must parse");
3247 std::fs::remove_file(&tmp).ok();
3248
3249 let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_K tensor must load");
3250 assert_eq!(matrix.rows(), 1);
3251 assert_eq!(matrix.cols(), 256);
3252 match &matrix {
3253 WeightMatrix::Quantized { kind, data, .. } => {
3254 assert_eq!(*kind, QuantKind::Q5K);
3255 assert!(
3256 data.is_mapped(),
3257 "Q5_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
3258 );
3259 }
3260 _ => panic!("expected a Quantized matrix for a Q5_K tensor"),
3261 }
3262
3263 let expected = ferrox_quant::dequant_q5_k(&Q5_K_TEST_BLOCK).unwrap();
3264 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3265 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3266
3267 let got = matrix.apply(&x);
3268 assert_eq!(got.len(), 1);
3269 assert!(
3270 (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-2),
3271 "end-to-end loaded+applied Q5_K matrix diverged from direct dequant: got={} expected={}",
3272 got[0],
3273 expected_dot
3274 );
3275 }
3276
3277 #[rustfmt::skip]
3286 const Q6_K_TEST_BLOCK: [u8; 210] = [
3287 0xe0, 0xa5, 0x40, 0x5c, 0x8d, 0x3a, 0x0a, 0x26, 0xfb, 0x4b, 0x6e, 0x9a, 0xdf, 0x3e, 0xa3,
3288 0xc4, 0xf8, 0x2b, 0x1d, 0x95, 0x76, 0x7d, 0x3b, 0xcd, 0xfd, 0xef, 0xc2, 0x0b, 0x07, 0x63,
3289 0x29, 0xfb, 0x81, 0x57, 0xbe, 0xbe, 0x06, 0xf7, 0x3a, 0x92, 0xc4, 0x43, 0xff, 0xad, 0xac,
3290 0x7e, 0x0f, 0x00, 0x2a, 0x4f, 0xf0, 0xf8, 0xa9, 0xfa, 0x3c, 0x90, 0x6d, 0x73, 0x2d, 0x5a,
3291 0xe6, 0xc6, 0x46, 0xf2, 0x0d, 0x55, 0x4c, 0x25, 0x38, 0x71, 0x2b, 0x35, 0x38, 0x82, 0x16,
3292 0x37, 0x5f, 0x32, 0x61, 0x02, 0xdd, 0x2f, 0x6f, 0x7b, 0x1f, 0xb4, 0x1a, 0x1b, 0x3e, 0x4f,
3293 0x11, 0xa3, 0x17, 0x40, 0x5a, 0x5f, 0x76, 0xcd, 0x19, 0x27, 0x9b, 0xc7, 0xc8, 0xf7, 0xf7,
3294 0xee, 0xf4, 0x86, 0xd9, 0xfd, 0xa7, 0xfe, 0x9e, 0xac, 0x70, 0x53, 0x5b, 0x76, 0xfb, 0x39,
3295 0xf8, 0x4b, 0x98, 0xfe, 0xd0, 0x06, 0x21, 0x4c, 0x4d, 0xbe, 0x10, 0x2b, 0x06, 0x65, 0xc9,
3296 0x5e, 0xf9, 0x95, 0x72, 0xae, 0x99, 0xd9, 0x7e, 0x15, 0xbd, 0x5e, 0x6d, 0xe8, 0x25, 0x8a,
3297 0xd5, 0x99, 0xc6, 0x6b, 0x69, 0xc7, 0x84, 0xc6, 0xa4, 0xf7, 0xb9, 0x6d, 0x68, 0x45, 0x0e,
3298 0x65, 0x69, 0xeb, 0xe6, 0xeb, 0xe9, 0x28, 0xa6, 0xb9, 0x96, 0xf2, 0xe8, 0xa7, 0x9b, 0x6e,
3299 0x79, 0x8a, 0x68, 0x65, 0x59, 0x98, 0x8b, 0x44, 0x41, 0x98, 0x9a, 0x56, 0x01, 0x01, 0x01,
3300 0x02, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, 0x01, 0x01, 0x01, 0x02, 0x1f, 0x25,
3301 ];
3302
3303 fn build_single_q6_k_tensor_gguf() -> Vec<u8> {
3304 let mut buf = Vec::new();
3305 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3306 .unwrap();
3307 buf.write_u32::<LittleEndian>(3).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); write_kv_str(&mut buf, "general.architecture", "ferrox-q6k-test");
3312
3313 write_string(&mut buf, "test.weight");
3314 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(256).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u32::<LittleEndian>(14).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
3322 buf.push(0);
3323 }
3324 buf.extend_from_slice(&Q6_K_TEST_BLOCK);
3325 buf
3326 }
3327
3328 #[test]
3329 fn load_weight_matrix_handles_a_real_on_disk_q6_k_tensor_end_to_end() {
3330 let tmp = std::env::temp_dir().join(format!(
3331 "ferrox_test_q6k_tensor_{}.gguf",
3332 std::process::id()
3333 ));
3334 std::fs::write(&tmp, build_single_q6_k_tensor_gguf()).unwrap();
3335 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q6_K GGUF file must parse");
3336 std::fs::remove_file(&tmp).ok();
3337
3338 let matrix = load_weight_matrix(&file, "test.weight").expect("Q6_K tensor must load");
3339 assert_eq!(matrix.rows(), 1);
3340 assert_eq!(matrix.cols(), 256);
3341 match &matrix {
3342 WeightMatrix::Quantized { kind, data, .. } => {
3343 assert_eq!(*kind, QuantKind::Q6K);
3344 assert!(
3345 data.is_mapped(),
3346 "Q6_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
3347 );
3348 }
3349 _ => panic!("expected a Quantized matrix for a Q6_K tensor"),
3350 }
3351
3352 let expected = ferrox_quant::dequant_q6_k(&Q6_K_TEST_BLOCK).unwrap();
3353 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3354 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3355
3356 let got = matrix.apply(&x);
3357 assert_eq!(got.len(), 1);
3358 assert!(
3359 (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-2),
3360 "end-to-end loaded+applied Q6_K matrix diverged from direct dequant: got={} expected={}",
3361 got[0],
3362 expected_dot
3363 );
3364 }
3365
3366 fn build_single_bf16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
3367 let mut buf = Vec::new();
3368 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3369 .unwrap();
3370 buf.write_u32::<LittleEndian>(3).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); write_kv_str(&mut buf, "general.architecture", "ferrox-bf16-test");
3375
3376 write_string(&mut buf, "test.weight");
3377 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(cols).unwrap();
3380 buf.write_u64::<LittleEndian>(rows).unwrap();
3381 buf.write_u32::<LittleEndian>(30).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
3385 buf.push(0);
3386 }
3387 for &v in values {
3388 let bf16_bits = (v.to_bits() >> 16) as u16;
3392 buf.extend_from_slice(&bf16_bits.to_le_bytes());
3393 }
3394 buf
3395 }
3396
3397 #[test]
3398 fn load_weight_matrix_handles_a_real_on_disk_bf16_tensor_end_to_end() {
3399 let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
3402 let tmp = std::env::temp_dir().join(format!(
3403 "ferrox_test_bf16_tensor_{}.gguf",
3404 std::process::id()
3405 ));
3406 std::fs::write(&tmp, build_single_bf16_tensor_gguf(2, 3, &values)).unwrap();
3407 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real BF16 GGUF file must parse");
3408 std::fs::remove_file(&tmp).ok();
3409
3410 let matrix = load_weight_matrix(&file, "test.weight").expect("BF16 tensor must load");
3411 assert_eq!(matrix.rows(), 2);
3412 assert_eq!(matrix.cols(), 3);
3413 match &matrix {
3414 WeightMatrix::F32(tensor) => {
3415 assert_eq!(tensor.data, values, "BF16 must widen to f32 exactly");
3416 }
3417 _ => panic!("expected an F32 matrix for a BF16 tensor (no fused dot kernel for it)"),
3418 }
3419 }
3420
3421 fn build_single_f16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
3422 let mut buf = Vec::new();
3423 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3424 .unwrap();
3425 buf.write_u32::<LittleEndian>(3).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); write_kv_str(&mut buf, "general.architecture", "ferrox-f16-test");
3430
3431 write_string(&mut buf, "test.weight");
3432 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(cols).unwrap();
3434 buf.write_u64::<LittleEndian>(rows).unwrap();
3435 buf.write_u32::<LittleEndian>(1).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
3439 buf.push(0);
3440 }
3441 for &v in values {
3442 buf.extend_from_slice(&half::f16::from_f32(v).to_le_bytes());
3443 }
3444 buf
3445 }
3446
3447 #[test]
3452 fn load_weight_matrix_handles_a_real_on_disk_f16_tensor_end_to_end() {
3453 let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
3454 let tmp = std::env::temp_dir().join(format!(
3455 "ferrox_test_f16_tensor_{}.gguf",
3456 std::process::id()
3457 ));
3458 std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
3459 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
3460 std::fs::remove_file(&tmp).ok();
3461
3462 let matrix = load_weight_matrix(&file, "test.weight").expect("F16 tensor must load");
3463 assert_eq!(matrix.rows(), 2);
3464 assert_eq!(matrix.cols(), 3);
3465 match &matrix {
3466 WeightMatrix::F32(tensor) => {
3467 assert_eq!(tensor.data, values, "F16 must widen to f32 exactly");
3468 }
3469 _ => panic!("expected an F32 matrix for an F16 tensor (no fused dot kernel for it)"),
3470 }
3471
3472 let tmp =
3475 std::env::temp_dir().join(format!("ferrox_test_f16_vec_{}.gguf", std::process::id()));
3476 std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
3477 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
3478 std::fs::remove_file(&tmp).ok();
3479 assert_eq!(load_f32_vec(&file, "test.weight").unwrap(), values);
3480 }
3481
3482 fn build_single_q5_1_tensor_gguf() -> Vec<u8> {
3483 let mut buf = Vec::new();
3484 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3485 .unwrap();
3486 buf.write_u32::<LittleEndian>(3).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); write_kv_str(&mut buf, "general.architecture", "ferrox-q5-1-test");
3491
3492 write_string(&mut buf, "test.weight");
3493 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(32).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u32::<LittleEndian>(7).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
3501 buf.push(0);
3502 }
3503 buf.extend_from_slice(&0x3400u16.to_le_bytes());
3508 buf.extend_from_slice(&0x3E00u16.to_le_bytes());
3509 buf.extend_from_slice(&[0x9au8, 0x3c, 0xf0, 0x0f]);
3510 buf.extend_from_slice(&(0..16u8).map(|i| i | ((15 - i) << 4)).collect::<Vec<u8>>());
3511 buf
3512 }
3513
3514 #[test]
3515 fn load_weight_matrix_handles_a_real_on_disk_q5_1_tensor_end_to_end() {
3516 let tmp = std::env::temp_dir().join(format!(
3517 "ferrox_test_q5_1_tensor_{}.gguf",
3518 std::process::id()
3519 ));
3520 std::fs::write(&tmp, build_single_q5_1_tensor_gguf()).unwrap();
3521 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q5_1 GGUF file must parse");
3522 std::fs::remove_file(&tmp).ok();
3523
3524 let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_1 tensor must load");
3525 assert_eq!(matrix.rows(), 1);
3526 assert_eq!(matrix.cols(), 32);
3527 let raw = file.tensor_bytes("test.weight").unwrap();
3528 let expected = ferrox_quant::dequant_q5_1(raw).unwrap();
3529 match &matrix {
3530 WeightMatrix::Quantized { kind, data, .. } => {
3531 assert_eq!(*kind, QuantKind::Q5_1);
3532 assert!(data.is_mapped());
3533 }
3534 _ => panic!("expected a Quantized matrix for a Q5_1 tensor"),
3535 }
3536
3537 let x: Vec<f32> = (0..32).map(|i| ((i as f32) * 0.017).cos()).collect();
3538 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3539 let got = matrix.apply(&x);
3540 assert_eq!(got.len(), 1);
3541 assert!(
3542 (got[0] - expected_dot).abs() < 1e-2,
3543 "end-to-end loaded+applied Q5_1 matrix diverged from direct dequant: got={} expected={}",
3544 got[0],
3545 expected_dot
3546 );
3547 }
3548
3549 const Q3_K_TEST_BLOCK: [u8; 110] = [
3554 0x56, 0xf2, 0xb4, 0x2b, 0xd5, 0x6f, 0x51, 0x71, 0x3c, 0x0a, 0xb9, 0x1d, 0xd0, 0xb9, 0x3b,
3555 0xb3, 0x0f, 0xff, 0x8c, 0xb2, 0x83, 0x3a, 0x3d, 0x24, 0xb1, 0x12, 0x56, 0xe3, 0x23, 0x54,
3556 0xf2, 0xfa, 0x7f, 0xdf, 0x31, 0xe1, 0x18, 0x26, 0x6e, 0xcd, 0x5b, 0x38, 0xee, 0xbd, 0x9f,
3557 0x8c, 0x57, 0x47, 0x0b, 0x11, 0xcb, 0xfb, 0xb4, 0x83, 0xa0, 0x4e, 0x0b, 0xd4, 0xa7, 0x85,
3558 0xe0, 0x60, 0xf3, 0xb3, 0xe3, 0x95, 0x43, 0xc6, 0x05, 0x05, 0x77, 0x53, 0xed, 0x23, 0xcc,
3559 0x6a, 0x0e, 0x89, 0xa1, 0x79, 0x85, 0xf6, 0x6e, 0x5a, 0x23, 0x63, 0xbe, 0x53, 0xfa, 0xa2,
3560 0x2b, 0xe9, 0xcd, 0xce, 0xf8, 0x3d, 0x6f, 0xd0, 0x42, 0x6e, 0x3b, 0x7f, 0x23, 0x26, 0xd3,
3561 0xb9, 0x18, 0xbf, 0xa4, 0x34,
3562 ];
3563
3564 fn build_single_q3_k_tensor_gguf() -> Vec<u8> {
3565 let mut buf = Vec::new();
3566 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3567 .unwrap();
3568 buf.write_u32::<LittleEndian>(3).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); write_kv_str(&mut buf, "general.architecture", "ferrox-q3k-test");
3573
3574 write_string(&mut buf, "test.weight");
3575 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(256).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u32::<LittleEndian>(11).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
3583 buf.push(0);
3584 }
3585 buf.extend_from_slice(&Q3_K_TEST_BLOCK);
3586 buf
3587 }
3588
3589 #[test]
3590 fn load_weight_matrix_handles_a_real_on_disk_q3_k_tensor_end_to_end() {
3591 let tmp = std::env::temp_dir().join(format!(
3592 "ferrox_test_q3k_tensor_{}.gguf",
3593 std::process::id()
3594 ));
3595 std::fs::write(&tmp, build_single_q3_k_tensor_gguf()).unwrap();
3596 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q3_K GGUF file must parse");
3597 std::fs::remove_file(&tmp).ok();
3598
3599 let matrix = load_weight_matrix(&file, "test.weight").expect("Q3_K tensor must load");
3600 assert_eq!(matrix.rows(), 1);
3601 assert_eq!(matrix.cols(), 256);
3602 match &matrix {
3603 WeightMatrix::Quantized { kind, data, .. } => {
3604 assert_eq!(*kind, QuantKind::Q3K);
3605 assert!(data.is_mapped());
3606 }
3607 _ => panic!("expected a Quantized matrix for a Q3_K tensor"),
3608 }
3609
3610 let expected = ferrox_quant::dequant_q3_k(&Q3_K_TEST_BLOCK).unwrap();
3611 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3612 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3613
3614 let got = matrix.apply(&x);
3615 assert_eq!(got.len(), 1);
3616 assert!(
3617 (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-1),
3618 "end-to-end loaded+applied Q3_K matrix diverged from direct dequant: got={} expected={}",
3619 got[0],
3620 expected_dot
3621 );
3622 }
3623
3624 const IQ4_XS_TEST_BLOCK: [u8; 136] = [
3628 0x5c, 0x33, 0xb4, 0x39, 0xd1, 0x64, 0x97, 0x82, 0xcb, 0xbd, 0x88, 0x95, 0xf3, 0x60, 0x2a,
3629 0xb5, 0xe7, 0x24, 0xd3, 0xee, 0xfe, 0x71, 0x13, 0xbe, 0x70, 0x84, 0x48, 0x79, 0x7b, 0x3e,
3630 0xf0, 0x55, 0xdc, 0xb2, 0xb2, 0xde, 0x32, 0xa1, 0x5b, 0x02, 0x01, 0xdc, 0x2a, 0xbb, 0xf7,
3631 0x0b, 0x8a, 0x88, 0xdd, 0x0b, 0x02, 0x7e, 0x5e, 0x76, 0x87, 0x30, 0x1e, 0x1c, 0xcf, 0x48,
3632 0xd7, 0x61, 0xf3, 0x51, 0x52, 0x17, 0x98, 0x0a, 0x87, 0xcf, 0x02, 0x91, 0xc8, 0xee, 0xc0,
3633 0x91, 0x69, 0x2a, 0x4f, 0x64, 0x68, 0xa7, 0xb2, 0xe6, 0x98, 0x21, 0x81, 0x75, 0x53, 0x2a,
3634 0x8d, 0x12, 0xae, 0xe0, 0xea, 0x0c, 0x75, 0xff, 0x22, 0x5e, 0x25, 0x19, 0xda, 0x2e, 0x51,
3635 0x4e, 0x81, 0xdc, 0x0e, 0x78, 0x86, 0xd7, 0x58, 0xb5, 0xb7, 0xf6, 0x45, 0xa9, 0x0a, 0x83,
3636 0xfd, 0x2a, 0x12, 0x7d, 0xf0, 0x12, 0x97, 0xe2, 0xfe, 0xf4, 0xd0, 0xa2, 0x11, 0x14, 0x78,
3637 0xdb,
3638 ];
3639
3640 fn build_single_iq4_xs_tensor_gguf() -> Vec<u8> {
3641 let mut buf = Vec::new();
3642 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3643 .unwrap();
3644 buf.write_u32::<LittleEndian>(3).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); write_kv_str(&mut buf, "general.architecture", "ferrox-iq4xs-test");
3649
3650 write_string(&mut buf, "test.weight");
3651 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(256).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u32::<LittleEndian>(23).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
3659 buf.push(0);
3660 }
3661 buf.extend_from_slice(&IQ4_XS_TEST_BLOCK);
3662 buf
3663 }
3664
3665 #[test]
3666 fn load_weight_matrix_handles_a_real_on_disk_iq4_xs_tensor_end_to_end() {
3667 let tmp = std::env::temp_dir().join(format!(
3668 "ferrox_test_iq4xs_tensor_{}.gguf",
3669 std::process::id()
3670 ));
3671 std::fs::write(&tmp, build_single_iq4_xs_tensor_gguf()).unwrap();
3672 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real IQ4_XS GGUF file must parse");
3673 std::fs::remove_file(&tmp).ok();
3674
3675 let matrix = load_weight_matrix(&file, "test.weight").expect("IQ4_XS tensor must load");
3676 assert_eq!(matrix.rows(), 1);
3677 assert_eq!(matrix.cols(), 256);
3678 match &matrix {
3679 WeightMatrix::Quantized { kind, data, .. } => {
3680 assert_eq!(*kind, QuantKind::IQ4XS);
3681 assert!(data.is_mapped());
3682 }
3683 _ => panic!("expected a Quantized matrix for an IQ4_XS tensor"),
3684 }
3685
3686 let expected = ferrox_quant::dequant_iq4_xs(&IQ4_XS_TEST_BLOCK).unwrap();
3687 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3688 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3689
3690 let got = matrix.apply(&x);
3691 assert_eq!(got.len(), 1);
3692 assert!(
3693 (got[0] - expected_dot).abs() < 1e-1,
3694 "end-to-end loaded+applied IQ4_XS matrix diverged from direct dequant: got={} expected={}",
3695 got[0],
3696 expected_dot
3697 );
3698 }
3699
3700 const IQ1_S_TEST_BLOCK: [u8; 50] = [
3705 0x0a, 0x2f, 0xfa, 0x06, 0x1e, 0x37, 0x6f, 0xe3, 0x62, 0xd0, 0xb6, 0xa4, 0x25, 0xae, 0x76,
3706 0x14, 0x72, 0x5b, 0xfa, 0x05, 0xd1, 0xf1, 0x2a, 0x4c, 0xad, 0x29, 0xae, 0xf4, 0xcf, 0x0c,
3707 0x96, 0x51, 0x58, 0x03, 0x6d, 0xd3, 0x10, 0x92, 0x70, 0xff, 0x61, 0x58, 0xc8, 0x30, 0x25,
3708 0x64, 0x49, 0x85, 0xc0, 0x24,
3709 ];
3710 const IQ2_XXS_TEST_BLOCK: [u8; 66] = [
3711 0x29, 0x30, 0xd9, 0x33, 0x95, 0x4c, 0x08, 0x1e, 0xad, 0x79, 0x49, 0xf2, 0x8d, 0x5f, 0x93,
3712 0xea, 0x78, 0x18, 0x98, 0xb9, 0x94, 0x14, 0xad, 0xce, 0xca, 0x1d, 0xab, 0x81, 0x53, 0x4a,
3713 0x68, 0xd0, 0x59, 0x96, 0x36, 0x5d, 0xbe, 0x20, 0xc4, 0xff, 0xe4, 0x2c, 0xcd, 0x2f, 0x4f,
3714 0x4f, 0x67, 0x53, 0xc6, 0xd5, 0xa2, 0xfb, 0xc7, 0xf3, 0xe2, 0x6b, 0xf1, 0x99, 0x23, 0x1e,
3715 0x2d, 0x5e, 0x8c, 0x78, 0xc2, 0x31,
3716 ];
3717 const IQ3_XXS_TEST_BLOCK: [u8; 98] = [
3718 0x71, 0x31, 0x16, 0x0a, 0x79, 0x04, 0x5d, 0x87, 0xae, 0x2a, 0x4a, 0x43, 0xfd, 0x02, 0xba,
3719 0x6c, 0x10, 0x42, 0x80, 0xe5, 0x1d, 0x08, 0x22, 0xcb, 0x21, 0x54, 0xf9, 0xaa, 0x8e, 0xc2,
3720 0xf2, 0x34, 0x66, 0x1e, 0x2a, 0xef, 0x19, 0xae, 0x48, 0x47, 0x29, 0xa0, 0x72, 0xd1, 0x31,
3721 0xc0, 0x65, 0x49, 0xde, 0x79, 0x32, 0xe6, 0x4d, 0xb6, 0x55, 0x3f, 0x4d, 0xf1, 0x18, 0xbb,
3722 0x18, 0x59, 0x4c, 0x31, 0xa3, 0xb2, 0x34, 0xdd, 0xf6, 0x4a, 0x91, 0x51, 0x3f, 0x3e, 0x40,
3723 0x69, 0xad, 0xbf, 0x1a, 0xd0, 0x05, 0xfb, 0xbe, 0x8b, 0x0b, 0xdd, 0xdf, 0x7d, 0x94, 0x74,
3724 0x92, 0x3e, 0xff, 0x04, 0x2a, 0xc4, 0xea, 0xc9,
3725 ];
3726
3727 #[rustfmt::skip]
3728 const MXFP4_GGUF_TEST_BLOCKS: [u8; 68] = [0x79, 0xb4, 0x8d, 0xe2, 0x62, 0x5d, 0xbb, 0x9d, 0x54, 0xe6, 0xdb, 0x94, 0x59, 0x7d, 0x28, 0xf9, 0x79, 0x7a, 0xfc, 0xc1, 0xfa, 0x1e, 0x53, 0x5b, 0x0e, 0xc2, 0x5a, 0x2f, 0x0c, 0x82, 0x4d, 0xcb, 0x11, 0x28, 0x7b, 0x7c, 0xb6, 0x45, 0xe0, 0xb0, 0x52, 0x40, 0x51, 0xec, 0x30, 0x1a, 0xd2, 0x17, 0xf3, 0xbb, 0xfc, 0x7c, 0x8f, 0xf0, 0x67, 0x83, 0x88, 0x9d, 0x79, 0xdb, 0xf4, 0x45, 0x29, 0x78, 0xe6, 0xf4, 0x99, 0xea];
3729
3730 #[test]
3740 fn a_recognized_but_unimplemented_ggml_type_refuses_by_name_after_sizing_correctly() {
3741 let block = pseudo_iq_block(66, 0x0720_5eed);
3743 let tmp =
3744 std::env::temp_dir().join(format!("ferrox_test_tq2_0_{}.gguf", std::process::id()));
3745 std::fs::write(
3746 &tmp,
3747 build_single_iq_lowbit_tensor_gguf("tq2test", 35, 256, &block),
3748 )
3749 .unwrap();
3750 let file = ferrox_gguf::GgufFile::open(&tmp).expect("a TQ2_0 file must still parse");
3751 std::fs::remove_file(&tmp).ok();
3752
3753 let info = file.find_tensor("test.weight").expect("tensor present");
3756 assert_eq!(info.dtype, GgmlType::TQ2_0);
3757 assert_eq!(info.byte_len(), Some(66));
3758 assert_eq!(
3759 file.tensor_bytes("test.weight").map(<[u8]>::len).ok(),
3760 Some(66)
3761 );
3762
3763 match load_weight_matrix(&file, "test.weight") {
3764 Err(LoadError::UnsupportedDtype(name, GgmlType::TQ2_0)) => {
3765 assert_eq!(name, "test.weight");
3766 }
3767 Err(other) => panic!("TQ2_0 must be refused by name, got {other:?}"),
3768 Ok(_) => panic!("TQ2_0 must be refused, not loaded as some other kind"),
3769 }
3770 }
3771
3772 #[test]
3782 fn an_mxfp4_one_dimensional_tensor_widens_instead_of_being_refused() {
3783 let expected = ferrox_quant::dequant_mxfp4_gguf(&MXFP4_GGUF_TEST_BLOCKS)
3784 .expect("the fixture blocks must dequantize");
3785 let cols = expected.len();
3786 let tmp = std::env::temp_dir().join(format!(
3787 "ferrox_test_mxfp4_norm_{}.gguf",
3788 std::process::id()
3789 ));
3790 std::fs::write(
3791 &tmp,
3792 build_single_iq_lowbit_tensor_gguf(
3793 "mxfp4norm",
3794 39,
3795 cols as u64,
3796 &MXFP4_GGUF_TEST_BLOCKS,
3797 ),
3798 )
3799 .unwrap();
3800 let file = ferrox_gguf::GgufFile::open(&tmp).expect("file must parse");
3801 std::fs::remove_file(&tmp).ok();
3802
3803 let got = load_f32_vec(&file, "test.weight")
3804 .expect("an MXFP4 norm must load, not report an unsupported dtype");
3805 assert_eq!(got, expected);
3806
3807 let direct = widen_plain_float(GgmlType::MXFP4, &MXFP4_GGUF_TEST_BLOCKS, "test.weight")
3811 .expect("widen_plain_float must widen MXFP4");
3812 assert_eq!(direct, expected);
3813
3814 match widen_plain_float(GgmlType::TQ2_0, &MXFP4_GGUF_TEST_BLOCKS, "test.weight") {
3818 Err(LoadError::UnsupportedDtype(name, GgmlType::TQ2_0)) => {
3819 assert_eq!(name, "test.weight");
3820 }
3821 other => panic!("TQ2_0 must be refused by name, got {other:?}"),
3822 }
3823 }
3824
3825 fn build_single_iq_lowbit_tensor_gguf(
3826 arch: &str,
3827 tag: u32,
3828 cols: u64,
3829 block: &[u8],
3830 ) -> Vec<u8> {
3831 let mut buf = Vec::new();
3832 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3833 .unwrap();
3834 buf.write_u32::<LittleEndian>(3).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u64::<LittleEndian>(1).unwrap(); write_kv_str(&mut buf, "general.architecture", arch);
3838 write_string(&mut buf, "test.weight");
3839 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(cols).unwrap();
3841 buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u32::<LittleEndian>(tag).unwrap();
3843 buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
3845 buf.push(0);
3846 }
3847 buf.extend_from_slice(block);
3848 buf
3849 }
3850
3851 fn pseudo_iq_block(len: usize, seed: u32) -> Vec<u8> {
3858 let mut s = seed;
3859 let mut out = Vec::with_capacity(len);
3860 for _ in 0..len {
3861 s ^= s << 13;
3862 s ^= s >> 17;
3863 s ^= s << 5;
3864 out.push((s >> 24) as u8);
3865 }
3866 out
3867 }
3868
3869 #[test]
3880 fn load_weight_matrix_handles_real_on_disk_iq_lowbit_tensors_end_to_end() {
3881 type DequantFn = fn(&[u8]) -> Result<Vec<f32>, ferrox_quant::QuantError>;
3882 let mut iq1m = pseudo_iq_block(ferrox_quant::IQ1_M_BLOCK_BYTES, 0x2907_31A0);
3889 iq1m[55] = (iq1m[55] & 0x0F) | 0x20;
3890 let mut iq2xs = pseudo_iq_block(ferrox_quant::IQ2_XS_BLOCK_BYTES, 0x2107_31A1);
3891 let mut iq2s = pseudo_iq_block(ferrox_quant::IQ2_S_BLOCK_BYTES, 0x2207_31A2);
3892 let mut iq3s = pseudo_iq_block(ferrox_quant::IQ3_S_BLOCK_BYTES, 0x2307_31A3);
3893 for blk in [&mut iq2xs, &mut iq2s, &mut iq3s] {
3894 blk[0..2].copy_from_slice(&half::f16::from_f32(0.115).to_le_bytes());
3895 }
3896 let cases: [(&str, u32, &[u8], QuantKind, DequantFn); 8] = [
3897 (
3898 "iq1s",
3899 19,
3900 &IQ1_S_TEST_BLOCK,
3901 QuantKind::IQ1S,
3902 ferrox_quant::dequant_iq1_s,
3903 ),
3904 (
3905 "iq1m",
3906 29,
3907 &iq1m,
3908 QuantKind::IQ1M,
3909 ferrox_quant::dequant_iq1_m,
3910 ),
3911 (
3912 "iq2xxs",
3913 16,
3914 &IQ2_XXS_TEST_BLOCK,
3915 QuantKind::IQ2XXS,
3916 ferrox_quant::dequant_iq2_xxs,
3917 ),
3918 (
3919 "iq2xs",
3920 17,
3921 &iq2xs,
3922 QuantKind::IQ2XS,
3923 ferrox_quant::dequant_iq2_xs,
3924 ),
3925 (
3926 "iq2s",
3927 22,
3928 &iq2s,
3929 QuantKind::IQ2S,
3930 ferrox_quant::dequant_iq2_s,
3931 ),
3932 (
3933 "iq3xxs",
3934 18,
3935 &IQ3_XXS_TEST_BLOCK,
3936 QuantKind::IQ3XXS,
3937 ferrox_quant::dequant_iq3_xxs,
3938 ),
3939 (
3940 "iq3s",
3941 21,
3942 &iq3s,
3943 QuantKind::IQ3S,
3944 ferrox_quant::dequant_iq3_s,
3945 ),
3946 (
3947 "mxfp4_gguf",
3948 39,
3949 &MXFP4_GGUF_TEST_BLOCKS,
3950 QuantKind::Mxfp4Gguf,
3951 ferrox_quant::dequant_mxfp4_gguf,
3952 ),
3953 ];
3954 for (name, tag, block, kind, dequant) in cases {
3955 let expected = dequant(block).unwrap();
3956 let cols = expected.len();
3957 let tmp = std::env::temp_dir().join(format!("ferrox_test_{name}_tensor.gguf"));
3958 std::fs::write(
3959 &tmp,
3960 build_single_iq_lowbit_tensor_gguf(name, tag, cols as u64, block),
3961 )
3962 .unwrap();
3963 let file = ferrox_gguf::GgufFile::open(&tmp).expect("file must parse");
3964 std::fs::remove_file(&tmp).ok();
3965
3966 let matrix =
3967 load_weight_matrix(&file, "test.weight").expect("low-bit tensor must load");
3968 assert_eq!((matrix.rows(), matrix.cols()), (1, cols), "{name}");
3969 match &matrix {
3970 WeightMatrix::Quantized { kind: k, data, .. } => {
3971 assert_eq!(*k, kind, "{name}");
3972 assert!(data.is_mapped(), "{name} must load zero-copy");
3973 }
3974 _ => panic!("expected a Quantized matrix for {name}"),
3975 }
3976
3977 let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.013).sin()).collect();
3978 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3979 let got = matrix.apply(&x);
3980 assert!(
3981 (got[0] - expected_dot).abs() < 1e-1,
3982 "{name}: loaded+applied diverged from direct dequant: got={} expected={}",
3983 got[0],
3984 expected_dot
3985 );
3986 }
3987 }
3988
3989 #[test]
3990 fn qwen2moe_disables_topk_renorm() {
3991 assert!(
3992 NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"qwen2moe"),
3993 "qwen2moe must have norm_topk_prob=false (llama.cpp build_moe_ffn norm_w=false)"
3994 );
3995 }
3996
3997 #[test]
4008 fn an_architecture_with_no_rope_is_refused_by_name() {
4009 for arch in ["gpt2", "mpt", "refact", "bloom", "jais"] {
4010 let file = open_metadata_gguf(
4011 &format!("norope_{arch}"),
4012 &[("general.architecture", Kv::Str(arch))],
4013 );
4014 match ModelConfig::from_gguf(&file) {
4015 Err(LoadError::DedicatedArchitectureRequired(got, reason)) => {
4016 assert_eq!(got, arch);
4017 assert!(
4018 reason.contains("ALiBi") || reason.contains("position embeddings"),
4019 "{arch}: the refusal must name what is missing, got {reason:?}"
4020 );
4021 }
4022 other => panic!("{arch} must be refused, got {other:?}"),
4023 }
4024 }
4025 }
4026
4027 #[test]
4033 fn baichuan_13b_is_refused_because_it_uses_alibi_and_the_7b_is_not() {
4034 let thirteen_b = open_metadata_gguf(
4035 "baichuan13b",
4036 &[
4037 ("general.architecture", Kv::Str("baichuan")),
4038 ("baichuan.block_count", Kv::U32(40)),
4039 ],
4040 );
4041 match ModelConfig::from_gguf(&thirteen_b) {
4042 Err(LoadError::UnsupportedFeature(arch, msg)) => {
4043 assert_eq!(arch, "baichuan");
4044 assert!(msg.contains("ALiBi"), "{msg}");
4045 assert!(
4046 msg.contains("40"),
4047 "the refusal must name the layer count: {msg}"
4048 );
4049 }
4050 other => panic!("Baichuan-13B must be refused, got {other:?}"),
4051 }
4052
4053 let seven_b = open_metadata_gguf(
4057 "baichuan7b",
4058 &[
4059 ("general.architecture", Kv::Str("baichuan")),
4060 ("baichuan.block_count", Kv::U32(32)),
4061 ],
4062 );
4063 match ModelConfig::from_gguf(&seven_b) {
4064 Err(LoadError::MissingHparam(key)) => assert_eq!(key, "baichuan.embedding_length"),
4065 other => panic!("Baichuan-7B must pass the ALiBi gate, got {other:?}"),
4066 }
4067 }
4068
4069 fn gemma3_config(
4080 tag: &str,
4081 n_layers: u32,
4082 hidden_dim: u32,
4083 n_heads: u32,
4084 head_dim: u32,
4085 linear_factor: Option<f32>,
4086 ) -> ModelConfig {
4087 let mut kvs: Vec<(&str, Kv)> = vec![
4088 ("general.architecture", Kv::Str("gemma3")),
4089 ("gemma3.block_count", Kv::U32(n_layers)),
4090 ("gemma3.embedding_length", Kv::U32(hidden_dim)),
4091 ("gemma3.attention.head_count", Kv::U32(n_heads)),
4092 ("gemma3.attention.head_count_kv", Kv::U32(n_heads)),
4093 ("gemma3.attention.key_length", Kv::U32(head_dim)),
4094 ("gemma3.attention.value_length", Kv::U32(head_dim)),
4095 ("gemma3.rope.freq_base", Kv::F32(1_000_000.0)),
4099 ("gemma3.attention.sliding_window", Kv::U32(1024)),
4100 ("gemma3.attention.sliding_window_pattern", Kv::U32(6)),
4101 ];
4102 if let Some(factor) = linear_factor {
4103 kvs.push(("gemma3.rope.scaling.type", Kv::Str("linear")));
4104 kvs.push(("gemma3.rope.scaling.factor", Kv::F32(factor)));
4105 }
4106 ModelConfig::from_gguf(&open_metadata_gguf(tag, &kvs)).expect("gemma3 fixture must load")
4107 }
4108
4109 #[test]
4119 fn a_gemma3_27b_header_sets_the_attention_scale_and_no_smaller_size_does() {
4120 let big = gemma3_config("g3_27b_scale", 62, 5376, 32, 128, Some(8.0));
4122 let want = 1.0f32 / (5376.0f32 / 32.0).sqrt();
4123 let got = big
4124 .attention_scale
4125 .expect("Gemma-3-27B is llama.cpp's LLM_TYPE_27B");
4126 assert!(
4127 (got - want).abs() < 1e-7,
4128 "want 1/sqrt(168) = {want}, got {got}"
4129 );
4130 let kernel = 1.0f32 / 128.0f32.sqrt();
4133 assert!((kernel / got - (168.0f32 / 128.0).sqrt()).abs() < 1e-5);
4134
4135 for (tag, n_layers, hidden, heads) in
4139 [("g3_1b_scale", 26, 1152, 4), ("g3_4b_scale", 34, 2560, 8)]
4140 {
4141 let cfg = gemma3_config(tag, n_layers, hidden, heads, 256, None);
4142 assert_eq!(
4143 cfg.attention_scale, None,
4144 "{tag} must keep the kernels' own 1/sqrt(head_dim)"
4145 );
4146 }
4147 }
4148
4149 #[test]
4163 fn gemma3_linear_scaling_reaches_the_full_layers_and_not_the_sliding_ones() {
4164 let cfg = gemma3_config("g3_4b_rope", 34, 2560, 8, 256, Some(8.0));
4166 let freqs = cfg
4167 .rope_freqs
4168 .as_ref()
4169 .expect("declared linear scaling must produce per-band divisors");
4170 assert!(
4171 freqs.full.iter().all(|f| (*f - 8.0).abs() < 1e-6),
4172 "full-attention layers divide every band by the trained factor: {:?}",
4173 freqs.full
4174 );
4175 let swa = freqs
4176 .swa
4177 .as_ref()
4178 .expect("gemma3 does not assign rope_freq_scale_train_swa, so 1.0 applies");
4179 assert!(
4180 swa.iter().all(|f| (*f - 1.0).abs() < 1e-6),
4181 "sliding layers rope at the raw position: {swa:?}"
4182 );
4183 assert_eq!(swa.len(), freqs.full.len(), "one divisor per rotated pair");
4184
4185 assert!(cfg.layer_sliding_window(0).is_some());
4189 assert!(cfg.layer_sliding_window(5).is_none());
4190 assert_eq!(cfg.layer_rope_freqs(0), Some(&[1.0f32; 128][..]));
4191 assert_eq!(cfg.layer_rope_freqs(5), Some(&[8.0f32; 128][..]));
4192 assert_eq!(cfg.layer_rope_theta(0), 10_000.0);
4193 assert_eq!(cfg.layer_rope_theta(5), 1_000_000.0);
4194 assert!(
4195 cfg.rope_freqs_vary_by_layer(),
4196 "the fused Metal stacks take one divisor slice for a whole run \
4197 and so must refuse this model"
4198 );
4199
4200 let plain = gemma3_config("g3_1b_rope", 26, 1152, 4, 256, None);
4204 assert!(plain.rope_freqs.is_none());
4205 assert!(!plain.rope_freqs_vary_by_layer());
4206 }
4207
4208 #[test]
4216 fn gemma2_sliding_layers_inherit_the_trained_rope_scale() {
4217 let cfg = ModelConfig::from_gguf(&open_metadata_gguf(
4218 "g2_rope",
4219 &[
4220 ("general.architecture", Kv::Str("gemma2")),
4221 ("gemma2.block_count", Kv::U32(26)),
4222 ("gemma2.embedding_length", Kv::U32(2304)),
4223 ("gemma2.attention.head_count", Kv::U32(8)),
4224 ("gemma2.attention.head_count_kv", Kv::U32(4)),
4225 ("gemma2.attention.key_length", Kv::U32(256)),
4226 ("gemma2.attention.value_length", Kv::U32(256)),
4227 ("gemma2.rope.freq_base", Kv::F32(10_000.0)),
4228 ("gemma2.attention.sliding_window", Kv::U32(4096)),
4229 ("gemma2.rope.scaling.type", Kv::Str("linear")),
4230 ("gemma2.rope.scaling.factor", Kv::F32(8.0)),
4231 ],
4232 ))
4233 .expect("gemma2 fixture must load");
4234
4235 let freqs = cfg.rope_freqs.as_ref().expect("linear scaling declared");
4236 assert_eq!(
4237 freqs.swa, None,
4238 "gemma2.cpp:11 assigns rope_freq_scale_train_swa from the trained scale"
4239 );
4240 assert!(!cfg.rope_freqs_vary_by_layer());
4241 assert!(cfg.layer_sliding_window(0).is_some());
4244 assert!(cfg.layer_sliding_window(1).is_none());
4245 assert_eq!(cfg.layer_rope_freqs(0), cfg.layer_rope_freqs(1));
4246
4247 assert!(crate::capability::swa_rope_scale_follows_model("gemma2"));
4250 assert!(!crate::capability::swa_rope_scale_follows_model("gemma3"));
4251 for arch in ["olmo2", "laguna"] {
4252 assert!(
4253 crate::capability::swa_rope_base_follows_model(arch),
4254 "{arch} seeds the SWA base from the model"
4255 );
4256 assert!(
4257 !crate::capability::swa_rope_scale_follows_model(arch),
4258 "{arch} pins the SWA scale to 1.0 (olmo2.cpp:14, laguna.cpp:48)"
4259 );
4260 }
4261 }
4262}