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 = None;
615
616 let rope_theta_swa = if sliding_window.is_some() {
621 let fallback = if crate::capability::swa_rope_base_follows_model(&arch) {
622 rope_theta
623 } else {
624 10_000.0
625 };
626 Some(
627 metadata_f32_any(
628 file,
629 &[key("rope.freq_base_swa"), key("rope_freq_base_swa")],
630 )
631 .unwrap_or(fallback),
632 )
633 } else {
634 None
635 };
636
637 let ffn_activation = match arch_profile.family {
638 _ if crate::capability::uses_geglu(&arch) => crate::config::FfnActivation::Gelu,
642 crate::capability::DecoderFamily::GemmaFamily => crate::config::FfnActivation::Gelu,
643 crate::capability::DecoderFamily::PhiFamily => {
644 crate::config::FfnActivation::SwigluFused
645 }
646 _ => crate::config::FfnActivation::Swiglu,
647 };
648
649 let rope_freqs = load_f32_vec_optional(file, "rope_freqs.weight")?;
656
657 let rope_orig_ctx = metadata_u64_any(file, &[key("rope.scaling.original_context_length")])
670 .map(|v| v as usize);
671 let (rope_freqs_long, rope_freqs_short) = if rope_freqs.is_some() {
676 (None, None)
677 } else {
678 (
679 load_f32_vec_optional(file, "rope_factors_long.weight")?,
680 load_f32_vec_optional(file, "rope_factors_short.weight")?,
681 )
682 };
683 let rope_freqs = match (rope_freqs, rope_orig_ctx) {
687 (Some(f), _) => Some(f),
688 (None, Some(orig)) => {
689 let model_ctx = metadata_u64_any(file, &[key("context_length")])
690 .unwrap_or(orig as u64) as usize;
691 if model_ctx > orig {
692 rope_freqs_long.clone().or_else(|| rope_freqs_short.clone())
693 } else {
694 rope_freqs_short.clone().or_else(|| rope_freqs_long.clone())
695 }
696 }
697 (None, None) => None,
698 };
699
700 let rope_dim = metadata_u64_any(file, &[key("rope.dimension_count")])
705 .map(|d| d as usize)
706 .filter(|d| *d > 0 && *d < head_dim);
707
708 let rope_attn_factor = metadata_f32_any(file, &[key("rope.scaling.attn_factor")])
710 .filter(|f| f.is_finite() && *f > 0.0)
711 .unwrap_or(1.0);
712
713 let rope_freqs = match linear_scaling_from_gguf(file, &arch) {
746 None => rope_freqs,
747 Some(factor) => {
748 let rotary_dim = rope_dim.unwrap_or(head_dim);
749 if rotary_dim == 0 || !rotary_dim.is_multiple_of(2) {
750 best_effort_fields.push(
751 "rope_freqs (linear scaling declared but the rotary width is odd; \
752 scaling not applied)",
753 );
754 rope_freqs
755 } else {
756 let linear = vec![factor; rotary_dim / 2];
757 match rope_freqs {
758 None => Some(linear),
759 Some(own) if own.len() == linear.len() => {
763 Some(own.iter().zip(linear.iter()).map(|(a, b)| a * b).collect())
764 }
765 Some(own) => {
766 best_effort_fields.push(
767 "rope_freqs (linear scaling declared but the file's own \
768 rope_freqs tensor has a different width; scaling not applied)",
769 );
770 Some(own)
771 }
772 }
773 }
774 }
775 };
776 let rope_freqs = match yarn_scaling_from_gguf(file, &arch, rope_orig_ctx) {
777 None => rope_freqs,
778 Some(scaling) => {
779 let rotary_dim = rope_dim.unwrap_or(head_dim);
780 if rotary_dim == 0 || !rotary_dim.is_multiple_of(2) {
781 best_effort_fields.push(
782 "rope_freqs (YaRN declared but the rotary width is odd; scaling not applied)",
783 );
784 rope_freqs
785 } else {
786 let yarn =
787 ferrox_core::attention::yarn_freq_factors(scaling, rotary_dim, rope_theta);
788 match rope_freqs {
789 None => Some(yarn),
790 Some(own) if own.len() == yarn.len() => {
791 Some(own.iter().zip(yarn.iter()).map(|(a, b)| a * b).collect())
792 }
793 Some(own) => {
794 best_effort_fields.push(
795 "rope_freqs (YaRN declared alongside a per-band factor tensor of a \
796 different width; the file's own tensor is used unscaled)",
797 );
798 Some(own)
799 }
800 }
801 }
802 }
803 };
804
805 if best_effort_fields.is_empty() {
810 best_effort_fields.push(
811 "none -- every field above was read directly from this file's own GGUF metadata",
812 );
813 }
814
815 if matches!(
826 arch_profile.path,
827 crate::capability::ArchPath::GenericGqa { .. }
828 ) && !crate::capability::is_audited_generic(&arch)
829 && !matches!(
830 std::env::var("FERROX_ALLOW_UNAUDITED_ARCH").ok().as_deref(),
831 Some("1") | Some("true") | Some("on")
832 )
833 {
834 return Err(LoadError::UnauditedArchitecture(
835 arch.clone(),
836 rope_layout,
837 crate::capability::unaudited_refusal_detail(&arch),
838 ));
839 }
840
841 Ok(ModelConfig {
842 name,
843 n_layers,
844 hidden_dim,
845 n_heads,
846 n_kv_heads,
847 head_dim,
848 vocab_size,
849 rope_theta,
850 rms_norm_eps,
851 attention: crate::config::AttentionKind::Gqa,
855 sliding_window,
856 swa_pattern,
857 swa_dense_first,
858 moe: MoeLayerConfig {
859 n_experts: n_experts.max(1),
860 n_experts_active,
861 n_shared_experts,
862 hidden_dim,
863 expert_ffn_dim,
864 gating,
865 norm_topk_prob,
866 expert_group_count: metadata_u64_any(file, &[key("expert_group_count")])
867 .map(|v| v as usize)
868 .filter(|&c| c > 1),
869 expert_group_used_count: metadata_u64_any(file, &[key("expert_group_used_count")])
870 .map(|v| v as usize)
871 .filter(|&c| c > 0),
872 expert_weights_scale,
873 },
874 n_dense_leading_layers,
875 rope_freqs,
876 rope_layout,
877 qk_norm_style,
878 attn_logit_softcap,
879 final_logit_softcap,
880 embedding_scale,
881 attention_scale,
882 rope_attn_factor,
883 rope_dim,
884 rope_freqs_long,
885 rope_freqs_short,
886 rope_orig_ctx,
887 rope_theta_swa,
888 ffn_activation,
889 best_effort_fields: Box::leak(best_effort_fields.into_boxed_slice()),
890 })
891 }
892}
893
894impl crate::sampling::RecommendedSampling {
895 pub fn from_gguf(file: &impl TensorSource) -> Self {
917 let number = |k: &str| -> Option<f32> {
918 file.metadata(k)
919 .and_then(|v| v.as_f32().or_else(|| v.as_u64().map(|u| u as f32)))
920 };
921 crate::sampling::RecommendedSampling {
922 temperature: number("general.sampling.temp"),
923 top_p: number("general.sampling.top_p"),
924 top_k: file
925 .metadata("general.sampling.top_k")
926 .and_then(|v| v.as_u64())
927 .map(|v| v as usize),
928 }
929 }
930}
931
932fn linear_scaling_from_gguf(file: &impl TensorSource, arch: &str) -> Option<f32> {
939 let key = |suffix: &str| format!("{arch}.{suffix}");
940 let scaling_type = file.metadata_str(&key("rope.scaling.type"))?;
941 if !scaling_type.eq_ignore_ascii_case("linear") {
942 return None;
943 }
944 metadata_f32_any(file, &[key("rope.scaling.factor")]).filter(|f| f.is_finite() && *f > 1.0)
945}
946
947fn yarn_scaling_from_gguf(
977 file: &impl TensorSource,
978 arch: &str,
979 orig_ctx: Option<usize>,
980) -> Option<ferrox_core::attention::YarnScaling> {
981 let key = |suffix: &str| format!("{arch}.{suffix}");
982 let scaling_type = file.metadata_str(&key("rope.scaling.type"))?;
983 if !scaling_type.eq_ignore_ascii_case("yarn") {
984 return None;
985 }
986 let factor = metadata_f32_any(file, &[key("rope.scaling.factor")])
987 .filter(|f| f.is_finite() && *f > 1.0)?;
988 let orig_max_pos = orig_ctx?;
989 let beta = |suffix: &str, default: f32| -> f32 {
990 metadata_f32_any(
991 file,
992 &[
993 key(&format!("rope.scaling.{suffix}")),
994 key(&format!("rope.scaling.yarn_{suffix}")),
995 ],
996 )
997 .filter(|v| v.is_finite() && *v > 0.0)
998 .unwrap_or(default)
999 };
1000 Some(ferrox_core::attention::YarnScaling {
1001 factor,
1002 beta_fast: beta("beta_fast", 32.0),
1003 beta_slow: beta("beta_slow", 1.0),
1004 orig_max_pos,
1005 truncate: true,
1009 })
1010}
1011
1012pub(crate) fn find_info<'a>(
1013 file: &'a impl TensorSource,
1014 name: &str,
1015) -> Result<&'a TensorInfo, LoadError> {
1016 file.find_tensor(name)
1017 .ok_or_else(|| LoadError::Gguf(GgufError::TensorNotFound(name.to_string())))
1018}
1019
1020fn load_gpt_oss_layer(
1046 file: &impl TensorSource,
1047 l: usize,
1048 config: &ModelConfig,
1049) -> Result<crate::decoder::GptOssLayer, LoadError> {
1050 let n_experts = config.moe.n_experts;
1051 let ff = config.moe.expert_ffn_dim;
1052
1053 let want = |name: &str, got: usize, expect: usize| -> Result<(), LoadError> {
1054 if got == expect {
1055 Ok(())
1056 } else {
1057 Err(LoadError::UnsupportedFeature(
1058 config.name.to_string(),
1059 format!("{name} has {got} elements, expected {expect}"),
1060 ))
1061 }
1062 };
1063
1064 let attn_sinks = load_f32_vec(file, &format!("blk.{l}.attn_sinks.weight"))?;
1065 want(
1066 &format!("blk.{l}.attn_sinks.weight"),
1067 attn_sinks.len(),
1068 config.n_heads,
1069 )?;
1070 let o_bias = load_f32_vec(file, &format!("blk.{l}.attn_output.bias"))?;
1071 want(
1072 &format!("blk.{l}.attn_output.bias"),
1073 o_bias.len(),
1074 config.hidden_dim,
1075 )?;
1076 let router_bias = load_f32_vec(file, &format!("blk.{l}.ffn_gate_inp.bias"))?;
1077 want(
1078 &format!("blk.{l}.ffn_gate_inp.bias"),
1079 router_bias.len(),
1080 n_experts,
1081 )?;
1082
1083 let gate_b = load_f32_vec(file, &format!("blk.{l}.ffn_gate_exps.bias"))?;
1084 want(
1085 &format!("blk.{l}.ffn_gate_exps.bias"),
1086 gate_b.len(),
1087 n_experts * ff,
1088 )?;
1089 let up_b = load_f32_vec(file, &format!("blk.{l}.ffn_up_exps.bias"))?;
1090 want(
1091 &format!("blk.{l}.ffn_up_exps.bias"),
1092 up_b.len(),
1093 n_experts * ff,
1094 )?;
1095 let down_b = load_f32_vec(file, &format!("blk.{l}.ffn_down_exps.bias"))?;
1096 want(
1097 &format!("blk.{l}.ffn_down_exps.bias"),
1098 down_b.len(),
1099 n_experts * config.hidden_dim,
1100 )?;
1101
1102 let expert_bias = (0..n_experts)
1103 .map(|e| ferrox_moe::ExpertBias {
1104 gate: gate_b[e * ff..(e + 1) * ff].to_vec(),
1105 up: up_b[e * ff..(e + 1) * ff].to_vec(),
1106 down: down_b[e * config.hidden_dim..(e + 1) * config.hidden_dim].to_vec(),
1107 })
1108 .collect();
1109
1110 Ok(crate::decoder::GptOssLayer {
1111 attn_sinks,
1112 o_bias,
1113 router_bias,
1114 expert_bias,
1115 })
1116}
1117
1118pub(crate) fn load_f32_vec_optional(
1119 file: &impl TensorSource,
1120 name: &str,
1121) -> Result<Option<Vec<f32>>, LoadError> {
1122 if file.find_tensor(name).is_none() {
1123 return Ok(None);
1124 }
1125 Ok(Some(load_f32_vec(file, name)?))
1126}
1127
1128fn slice_quantized_rows(m: &WeightMatrix, start: usize, n: usize) -> Option<WeightMatrix> {
1135 let WeightMatrix::Quantized {
1136 data,
1137 rows,
1138 cols,
1139 kind,
1140 } = m
1141 else {
1142 return None;
1143 };
1144 let total = data.len();
1145 if *rows == 0 || total % *rows != 0 || start + n > *rows {
1146 return None;
1147 }
1148 let row_bytes = total / *rows;
1149 let (b0, b1) = (start * row_bytes, (start + n) * row_bytes);
1150 let bytes = match data {
1151 WeightBytes::Mapped { mmap, range } => WeightBytes::Mapped {
1152 mmap: mmap.clone(),
1153 range: range.start + b0..range.start + b1,
1154 },
1155 other => WeightBytes::Owned(other.as_slice()[b0..b1].to_vec()),
1156 };
1157 Some(WeightMatrix::Quantized {
1158 data: bytes,
1159 rows: n,
1160 cols: *cols,
1161 kind: *kind,
1162 })
1163}
1164
1165fn load_qkv_projections(
1170 file: &impl TensorSource,
1171 layer: usize,
1172 config: &ModelConfig,
1173) -> Result<(WeightMatrix, WeightMatrix, WeightMatrix), LoadError> {
1174 let q_name = format!("blk.{layer}.attn_q.weight");
1175 let k_name = format!("blk.{layer}.attn_k.weight");
1176 let v_name = format!("blk.{layer}.attn_v.weight");
1177 let fused_name = format!("blk.{layer}.attn_qkv.weight");
1178
1179 if file.find_tensor(&q_name).is_some() {
1180 return Ok((
1181 load_weight_matrix(file, &q_name)?,
1182 load_weight_matrix(file, &k_name)?,
1183 load_weight_matrix(file, &v_name)?,
1184 ));
1185 }
1186 if file.find_tensor(&fused_name).is_none() {
1187 return Err(LoadError::Gguf(GgufError::TensorNotFound(q_name)));
1188 }
1189
1190 let fused = load_weight_matrix(file, &fused_name)?;
1191 let q_rows = config.n_heads * config.head_dim;
1192 let kv_rows = config.n_kv_heads * config.head_dim;
1193 let expected = q_rows + 2 * kv_rows;
1194 if fused.rows() != expected {
1195 return Err(LoadError::UnsupportedFeature(
1197 config.name.to_string(),
1198 format!(
1199 "{fused_name} has {} rows; expected q+k+v = {} \
1200 (n_heads*head_dim + 2*n_kv_heads*head_dim)",
1201 fused.rows(),
1202 expected
1203 ),
1204 ));
1205 }
1206 let cols = fused.cols();
1207 if let (Some(q), Some(k), Some(v)) = (
1210 slice_quantized_rows(&fused, 0, q_rows),
1211 slice_quantized_rows(&fused, q_rows, kv_rows),
1212 slice_quantized_rows(&fused, q_rows + kv_rows, kv_rows),
1213 ) {
1214 return Ok((q, k, v));
1215 }
1216 let mut full = Vec::with_capacity(fused.rows() * cols);
1218 for r in 0..fused.rows() {
1219 full.extend_from_slice(&fused.dequant_row(r));
1220 }
1221 let q = WeightMatrix::F32(Tensor::new(
1222 full[..q_rows * cols].to_vec(),
1223 vec![q_rows, cols],
1224 ));
1225 let k = WeightMatrix::F32(Tensor::new(
1226 full[q_rows * cols..(q_rows + kv_rows) * cols].to_vec(),
1227 vec![kv_rows, cols],
1228 ));
1229 let v = WeightMatrix::F32(Tensor::new(
1230 full[(q_rows + kv_rows) * cols..].to_vec(),
1231 vec![kv_rows, cols],
1232 ));
1233 Ok((q, k, v))
1234}
1235
1236fn load_dense_expert(
1239 file: &impl TensorSource,
1240 layer: usize,
1241 config: &ModelConfig,
1242) -> Result<ExpertWeights, LoadError> {
1243 let gate_name = format!("blk.{layer}.ffn_gate.weight");
1244 let up_name = format!("blk.{layer}.ffn_up.weight");
1245 let down_name = format!("blk.{layer}.ffn_down.weight");
1246 if file.find_tensor(&gate_name).is_some() {
1247 return Ok(ExpertWeights {
1248 gate: load_weight_matrix(file, &gate_name)?,
1249 up: load_weight_matrix(file, &up_name)?,
1250 down: load_weight_matrix(file, &down_name)?,
1251 });
1252 }
1253 let fused = load_weight_matrix(file, &up_name)?;
1255 let ff = config.moe.expert_ffn_dim;
1256 if fused.rows() != 2 * ff {
1257 return Err(LoadError::UnsupportedFeature(
1258 config.name.to_string(),
1259 format!(
1260 "{up_name} has {} rows without a companion ffn_gate; \
1261 expected fused SwiGLU with 2*ffn_dim = {} rows",
1262 fused.rows(),
1263 2 * ff
1264 ),
1265 ));
1266 }
1267 let cols = fused.cols();
1268 if let (Some(gate), Some(up)) = (
1270 slice_quantized_rows(&fused, 0, ff),
1271 slice_quantized_rows(&fused, ff, ff),
1272 ) {
1273 return Ok(ExpertWeights {
1274 gate,
1275 up,
1276 down: load_weight_matrix(file, &down_name)?,
1277 });
1278 }
1279 let mut full = Vec::with_capacity(fused.rows() * cols);
1280 for r in 0..fused.rows() {
1281 full.extend_from_slice(&fused.dequant_row(r));
1282 }
1283 let gate = WeightMatrix::F32(Tensor::new(full[..ff * cols].to_vec(), vec![ff, cols]));
1284 let up = WeightMatrix::F32(Tensor::new(full[ff * cols..].to_vec(), vec![ff, cols]));
1285 Ok(ExpertWeights {
1286 gate,
1287 up,
1288 down: load_weight_matrix(file, &down_name)?,
1289 })
1290}
1291
1292pub(crate) fn widen_plain_float(
1301 dtype: GgmlType,
1302 raw: &[u8],
1303 name: &str,
1304) -> Result<Vec<f32>, LoadError> {
1305 match dtype {
1306 GgmlType::F32 => {
1307 let mut out = Vec::with_capacity(raw.len() / 4);
1308 for chunk in raw.as_chunks::<4>().0 {
1309 out.push(f32::from_le_bytes(*chunk));
1310 }
1311 Ok(out)
1312 }
1313 GgmlType::F16 => ferrox_quant::dequant_f16(raw)
1314 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::F16)),
1315 GgmlType::BF16 => ferrox_quant::dequant_bf16(raw)
1316 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::BF16)),
1317 GgmlType::MXFP4 => ferrox_quant::dequant_mxfp4_gguf(raw)
1324 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::MXFP4)),
1325 other => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1326 }
1327}
1328
1329pub(crate) fn load_f32_vec(file: &impl TensorSource, name: &str) -> Result<Vec<f32>, LoadError> {
1330 let info = find_info(file, name)?;
1331 let raw = file.tensor_bytes(name)?;
1332 match info.dtype {
1333 GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 | GgmlType::MXFP4 => {
1345 widen_plain_float(info.dtype, raw, name)
1346 }
1347 GgmlType::Q8_0 => ferrox_quant::dequant_q8_0(raw)
1348 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q8_0)),
1349 GgmlType::Q4_0 => ferrox_quant::dequant_q4_0(raw)
1350 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4_0)),
1351 GgmlType::Q4K => ferrox_quant::dequant_q4_k(raw)
1352 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4K)),
1353 GgmlType::Q5K => ferrox_quant::dequant_q5_k(raw)
1354 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5K)),
1355 GgmlType::Q6K => ferrox_quant::dequant_q6_k(raw)
1356 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q6K)),
1357 GgmlType::Q2K => ferrox_quant::dequant_q2_k(raw)
1358 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q2K)),
1359 GgmlType::Q3K => ferrox_quant::dequant_q3_k(raw)
1360 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q3K)),
1361 GgmlType::Q4_1 => ferrox_quant::dequant_q4_1(raw)
1362 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4_1)),
1363 GgmlType::Q5_0 => ferrox_quant::dequant_q5_0(raw)
1364 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5_0)),
1365 GgmlType::Q5_1 => ferrox_quant::dequant_q5_1(raw)
1366 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5_1)),
1367 GgmlType::Q8_1 => ferrox_quant::dequant_q8_1(raw)
1368 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q8_1)),
1369 GgmlType::IQ4NL => ferrox_quant::dequant_iq4_nl(raw)
1370 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ4NL)),
1371 GgmlType::IQ4XS => ferrox_quant::dequant_iq4_xs(raw)
1372 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ4XS)),
1373 GgmlType::IQ1S => ferrox_quant::dequant_iq1_s(raw)
1380 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ1S)),
1381 GgmlType::IQ1M => ferrox_quant::dequant_iq1_m(raw)
1382 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ1M)),
1383 GgmlType::IQ2XXS => ferrox_quant::dequant_iq2_xxs(raw)
1384 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2XXS)),
1385 GgmlType::IQ2XS => ferrox_quant::dequant_iq2_xs(raw)
1386 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2XS)),
1387 GgmlType::IQ2S => ferrox_quant::dequant_iq2_s(raw)
1388 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2S)),
1389 GgmlType::IQ3XXS => ferrox_quant::dequant_iq3_xxs(raw)
1390 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ3XXS)),
1391 GgmlType::IQ3S => ferrox_quant::dequant_iq3_s(raw)
1392 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ3S)),
1393 other => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1394 }
1395}
1396
1397pub(crate) fn load_weight_matrix(
1404 file: &impl TensorSource,
1405 name: &str,
1406) -> Result<WeightMatrix, LoadError> {
1407 let info = find_info(file, name)?;
1408 let shape: Vec<usize> = info.shape.iter().rev().map(|&d| d as usize).collect();
1421 let (rows, cols) = match shape.as_slice() {
1422 [r, c] => (*r, *c),
1423 other => {
1424 return Err(LoadError::UnsupportedDtype(
1425 format!("{name} (expected 2D, got shape {other:?})"),
1426 info.dtype,
1427 ))
1428 }
1429 };
1430
1431 match info.dtype {
1432 GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => {
1438 let data = load_f32_vec(file, name)?;
1439 Ok(WeightMatrix::F32(Tensor::new(data, shape)))
1440 }
1441 other => match quant_kind_for(other) {
1442 Some(kind) => {
1443 let (mmap, range) = file.tensor_mapped_range(name)?;
1444 #[cfg(feature = "metal")]
1445 ferrox_metal::gpu::register_weight_mmap(Arc::clone(&mmap));
1446 Ok(WeightMatrix::Quantized {
1447 data: WeightBytes::Mapped { mmap, range },
1448 rows,
1449 cols,
1450 kind,
1451 })
1452 }
1453 None => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1454 },
1455 }
1456}
1457
1458pub(crate) fn split_expert_tensor(
1465 file: &impl TensorSource,
1466 name: &str,
1467 n_experts: usize,
1468) -> Result<Vec<WeightMatrix>, LoadError> {
1469 let info = find_info(file, name)?;
1470 if info.shape.len() != 3 || info.shape[2] as usize != n_experts {
1477 let file_experts = info.shape.last().map(|&d| d as usize).unwrap_or(0);
1478 return Err(LoadError::ExpertCountMismatch(
1479 name.to_string(),
1480 file_experts,
1481 n_experts,
1482 ));
1483 }
1484 let out_dim = info.shape[1] as usize;
1485 let in_dim = info.shape[0] as usize;
1486 let raw = file.tensor_bytes(name)?;
1487
1488 match info.dtype {
1489 GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => {
1490 let all = crate::loader::widen_plain_float(info.dtype, raw, name)?;
1491 let per_expert = out_dim * in_dim;
1492 Ok((0..n_experts)
1493 .map(|e| {
1494 WeightMatrix::F32(Tensor::new(
1495 all[e * per_expert..(e + 1) * per_expert].to_vec(),
1496 vec![out_dim, in_dim],
1497 ))
1498 })
1499 .collect())
1500 }
1501 other => match quant_kind_for(other) {
1502 Some(kind) => {
1503 let (mmap, full_range) = file.tensor_mapped_range(name)?;
1504 #[cfg(feature = "metal")]
1505 ferrox_metal::gpu::register_weight_mmap(Arc::clone(&mmap));
1506 let bytes_per_expert = raw.len() / n_experts;
1507 Ok((0..n_experts)
1508 .map(|e| WeightMatrix::Quantized {
1509 data: WeightBytes::Mapped {
1510 mmap: Arc::clone(&mmap),
1511 range: (full_range.start + e * bytes_per_expert)
1512 ..(full_range.start + (e + 1) * bytes_per_expert),
1513 },
1514 rows: out_dim,
1515 cols: in_dim,
1516 kind,
1517 })
1518 .collect())
1519 }
1520 None => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1521 },
1522 }
1523}
1524
1525#[cfg(feature = "metal")]
1530fn try_build_moe_packed_q4_planes(experts: &[ExpertWeights]) -> Option<MoePackedQ4Planes> {
1531 use ferrox_core::weight_matrix::{QuantKind, WeightBytes};
1532 use std::sync::Arc;
1533
1534 if experts.is_empty() {
1535 return None;
1536 }
1537
1538 fn mapped_sg(m: &WeightMatrix) -> Option<(WeightBytes, usize, &'static str)> {
1539 match m {
1540 WeightMatrix::Quantized {
1541 data: WeightBytes::Mapped { mmap, range },
1542 rows,
1543 kind,
1544 ..
1545 } => {
1546 let kind_str = match kind {
1547 QuantKind::Q4_0 => "Q4_0",
1548 QuantKind::Q5_0 => "Q5_0",
1549 QuantKind::Q4K => "Q4_K",
1550 QuantKind::Q5K => "Q5_K",
1551 QuantKind::Q6K => "Q6_K",
1552 QuantKind::Q8_0 => "Q8_0",
1553 QuantKind::IQ4XS => "IQ4_XS",
1554 _ => return None,
1555 };
1556 let _ = ferrox_metal::gpu::mul_mm_sg_meta(kind_str)?;
1557 Some((
1558 WeightBytes::Mapped {
1559 mmap: Arc::clone(mmap),
1560 range: range.clone(),
1561 },
1562 *rows,
1563 kind_str,
1564 ))
1565 }
1566 _ => None,
1567 }
1568 }
1569
1570 let (gate0, ffn_rows, gate_kind) = mapped_sg(&experts[0].gate)?;
1571 let (up0, up_rows, up_kind) = mapped_sg(&experts[0].up)?;
1572 let (down0, hidden_rows, down_kind) = mapped_sg(&experts[0].down)?;
1573 if up_rows != ffn_rows {
1574 return None;
1575 }
1576 let WeightBytes::Mapped {
1577 mmap: gate_mmap,
1578 range: gate0_range,
1579 } = &gate0
1580 else {
1581 return None;
1582 };
1583 let WeightBytes::Mapped {
1584 mmap: up_mmap,
1585 range: up0_range,
1586 } = &up0
1587 else {
1588 return None;
1589 };
1590 let WeightBytes::Mapped {
1591 mmap: down_mmap,
1592 range: down0_range,
1593 } = &down0
1594 else {
1595 return None;
1596 };
1597
1598 let gate_stride = gate0_range.len();
1599 let up_stride = up0_range.len();
1600 let down_stride = down0_range.len();
1601 if gate_stride == 0 || up_stride == 0 || down_stride == 0 {
1602 return None;
1603 }
1604
1605 let n = experts.len();
1606 for (i, ex) in experts.iter().enumerate().skip(1) {
1607 let (g, fr, gk) = mapped_sg(&ex.gate)?;
1608 let (u, ur, uk) = mapped_sg(&ex.up)?;
1609 let (d, hr, dk) = mapped_sg(&ex.down)?;
1610 if gk != gate_kind || uk != up_kind || dk != down_kind {
1611 return None;
1612 }
1613 let WeightBytes::Mapped { mmap, range } = &g else {
1614 return None;
1615 };
1616 if fr != ffn_rows {
1617 return None;
1618 }
1619 if !Arc::ptr_eq(mmap, gate_mmap)
1620 || range.len() != gate_stride
1621 || range.start != gate0_range.start + i * gate_stride
1622 {
1623 return None;
1624 }
1625 let WeightBytes::Mapped { mmap, range } = &u else {
1626 return None;
1627 };
1628 if ur != ffn_rows
1629 || !Arc::ptr_eq(mmap, up_mmap)
1630 || range.len() != up_stride
1631 || range.start != up0_range.start + i * up_stride
1632 {
1633 return None;
1634 }
1635 let WeightBytes::Mapped { mmap, range } = &d else {
1636 return None;
1637 };
1638 if hr != hidden_rows
1639 || !Arc::ptr_eq(mmap, down_mmap)
1640 || range.len() != down_stride
1641 || range.start != down0_range.start + i * down_stride
1642 {
1643 return None;
1644 }
1645 }
1646
1647 Some(MoePackedQ4Planes::new(
1648 WeightBytes::Mapped {
1649 mmap: Arc::clone(gate_mmap),
1650 range: gate0_range.start..gate0_range.start + n * gate_stride,
1651 },
1652 WeightBytes::Mapped {
1653 mmap: Arc::clone(up_mmap),
1654 range: up0_range.start..up0_range.start + n * up_stride,
1655 },
1656 WeightBytes::Mapped {
1657 mmap: Arc::clone(down_mmap),
1658 range: down0_range.start..down0_range.start + n * down_stride,
1659 },
1660 gate_stride,
1661 up_stride,
1662 down_stride,
1663 n,
1664 ffn_rows,
1665 hidden_rows,
1666 gate_kind,
1667 up_kind,
1668 down_kind,
1669 ))
1670}
1671
1672#[derive(Debug, Clone, Copy)]
1676pub struct StoredMatrixSpec {
1677 pub offset: usize,
1678 pub len: usize,
1679 pub rows: usize,
1680 pub cols: usize,
1681 pub kind: QuantKind,
1682}
1683
1684#[derive(Debug, Clone, Copy)]
1686pub struct StoredExpertLayout {
1687 pub gate: StoredMatrixSpec,
1688 pub up: StoredMatrixSpec,
1689 pub down: StoredMatrixSpec,
1690}
1691
1692impl StoredExpertLayout {
1693 pub fn total_bytes(&self) -> usize {
1694 self.gate.len + self.up.len + self.down.len
1695 }
1696
1697 pub fn materialize(&self, lease: &ferrox_core::expert_store::ExpertLease) -> ExpertWeights {
1701 let mk = |spec: &StoredMatrixSpec| WeightMatrix::Quantized {
1702 data: WeightBytes::Shared {
1703 buf: lease.shared_buf(),
1704 range: spec.offset..spec.offset + spec.len,
1705 },
1706 rows: spec.rows,
1707 cols: spec.cols,
1708 kind: spec.kind,
1709 };
1710 ExpertWeights {
1711 gate: mk(&self.gate),
1712 up: mk(&self.up),
1713 down: mk(&self.down),
1714 }
1715 }
1716}
1717
1718pub struct GgufExpertSource {
1724 files: Vec<std::fs::File>,
1725 segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]>,
1728}
1729
1730impl ExpertSource for GgufExpertSource {
1731 fn expert_len(&self, key: ExpertKey) -> Option<usize> {
1732 self.segments
1733 .get(&key)
1734 .map(|segs| segs.iter().map(|&(_, _, len)| len).sum())
1735 }
1736
1737 fn read_expert(&self, key: ExpertKey) -> std::io::Result<Vec<u8>> {
1738 let segs = self
1739 .segments
1740 .get(&key)
1741 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, format!("{key:?}")))?;
1742 let total: usize = segs.iter().map(|&(_, _, len)| len).sum();
1743 let mut buf = vec![0u8; total];
1744 let mut written = 0;
1745 for &(fi, offset, len) in segs {
1746 let dst = &mut buf[written..written + len];
1747 #[cfg(unix)]
1748 {
1749 use std::os::unix::fs::FileExt;
1750 self.files[fi].read_exact_at(dst, offset)?;
1751 }
1752 #[cfg(not(unix))]
1753 {
1754 use std::io::{Read, Seek, SeekFrom};
1755 let mut f = &self.files[fi];
1756 f.seek(SeekFrom::Start(offset))?;
1757 f.read_exact(dst)?;
1758 }
1759 written += len;
1760 }
1761 Ok(buf)
1762 }
1763}
1764
1765struct StoredTensorSpecs {
1775 shard: usize,
1776 per_expert: Vec<(u64, usize)>,
1777 spec: StoredMatrixSpec,
1778}
1779
1780fn stored_expert_specs(
1781 file: &ShardedGguf,
1782 name: &str,
1783 n_experts: usize,
1784) -> Result<Option<StoredTensorSpecs>, LoadError> {
1785 let info = find_info(file, name)?;
1786 if info.shape.len() != 3 || info.shape[2] as usize != n_experts {
1787 let file_experts = info.shape.last().map(|&d| d as usize).unwrap_or(0);
1788 return Err(LoadError::ExpertCountMismatch(
1789 name.to_string(),
1790 file_experts,
1791 n_experts,
1792 ));
1793 }
1794 let out_dim = info.shape[1] as usize;
1795 let in_dim = info.shape[0] as usize;
1796 let Some(kind) = quant_kind_for(info.dtype) else {
1797 return Ok(None); };
1799 let shard = file
1800 .tensor_shard_index(name)
1801 .expect("find_info succeeded, shard index must exist");
1802 let (_, full_range) = file.tensor_mapped_range(name)?;
1805 let total_len = full_range.end - full_range.start;
1806 let bytes_per_expert = total_len / n_experts;
1807 let per_expert: Vec<(u64, usize)> = (0..n_experts)
1808 .map(|e| {
1809 (
1810 (full_range.start + e * bytes_per_expert) as u64,
1811 bytes_per_expert,
1812 )
1813 })
1814 .collect();
1815 let spec = StoredMatrixSpec {
1816 offset: 0, len: bytes_per_expert,
1818 rows: out_dim,
1819 cols: in_dim,
1820 kind,
1821 };
1822 Ok(Some(StoredTensorSpecs {
1823 shard,
1824 per_expert,
1825 spec,
1826 }))
1827}
1828
1829impl Decoder {
1830 pub fn from_gguf(
1839 path: impl AsRef<std::path::Path>,
1840 config: ModelConfig,
1841 ) -> Result<Self, LoadError> {
1842 Self::from_gguf_with_expert_cache(path, config, None)
1843 }
1844
1845 pub fn from_gguf_with_expert_cache(
1858 path: impl AsRef<std::path::Path>,
1859 mut config: ModelConfig,
1860 expert_cache_bytes: Option<u64>,
1861 ) -> Result<Self, LoadError> {
1862 let path = path.as_ref();
1863 let file = ShardedGguf::open(path)?;
1864
1865 let arch = file
1877 .metadata_str("general.architecture")
1878 .unwrap_or_default()
1879 .to_string();
1880 let is_gpt_oss = arch == "gpt-oss";
1881 let post_attn_norm_is_pre_ffn_norm = pre_ffn_norm_is_post_attention_norm(&arch);
1882 let mut gpt_oss_layers: Vec<crate::decoder::GptOssLayer> = Vec::new();
1883
1884 let mut store_segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]> =
1888 std::collections::HashMap::new();
1889 let mut stored_layouts: Vec<Option<Vec<StoredExpertLayout>>> = Vec::new();
1890
1891 let embedding = load_weight_matrix(&file, "token_embd.weight")?;
1896
1897 let mut layers = Vec::with_capacity(config.n_layers);
1898 let mut refined_qk_norm = config.qk_norm_style;
1899 for l in 0..config.n_layers {
1900 let (q_proj, k_proj, v_proj) = load_qkv_projections(&file, l, &config)?;
1901 let q_norm = load_f32_vec_optional(&file, &format!("blk.{l}.attn_q_norm.weight"))?;
1902 let k_norm = load_f32_vec_optional(&file, &format!("blk.{l}.attn_k_norm.weight"))?;
1903 if let Some(ref w) = q_norm {
1905 if w.len() == config.head_dim {
1906 refined_qk_norm = crate::capability::QkNormStyle::PerHead;
1907 } else if w.len() == config.n_heads * config.head_dim {
1908 refined_qk_norm = crate::capability::QkNormStyle::WholeVector;
1909 } else {
1910 return Err(LoadError::UnsupportedFeature(
1911 config.name.to_string(),
1912 format!(
1913 "blk.{l}.attn_q_norm.weight length {} matches neither head_dim={} \
1914 nor n_heads*head_dim={}",
1915 w.len(),
1916 config.head_dim,
1917 config.n_heads * config.head_dim
1918 ),
1919 ));
1920 }
1921 }
1922 let attn = AttnWeights {
1923 q_proj,
1924 k_proj,
1925 v_proj,
1926 o_proj: load_weight_matrix(&file, &format!("blk.{l}.attn_output.weight"))?,
1927 norm_weight: load_f32_vec(&file, &format!("blk.{l}.attn_norm.weight"))?,
1928 q_norm,
1929 k_norm,
1930 q_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_q.bias"))?,
1934 k_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_k.bias"))?,
1935 v_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_v.bias"))?,
1936 post_attn_norm: if post_attn_norm_is_pre_ffn_norm {
1942 None
1943 } else {
1944 load_f32_vec_optional(&file, &format!("blk.{l}.post_attention_norm.weight"))?
1945 },
1946 post_ffn_norm: load_f32_vec_optional(
1947 &file,
1948 &format!("blk.{l}.post_ffw_norm.weight"),
1949 )?,
1950 };
1951
1952 let is_dense_layer = config.layer_is_dense(l) || config.moe.n_experts <= 1;
1960 let n_experts = if is_dense_layer {
1961 1
1962 } else {
1963 config.moe.n_experts
1964 };
1965 let experts: ExpertBacking = if is_dense_layer {
1966 ExpertBacking::Resident(vec![load_dense_expert(&file, l, &config)?])
1967 } else {
1968 let stored = if expert_cache_bytes.is_some() {
1972 let g = stored_expert_specs(
1973 &file,
1974 &format!("blk.{l}.ffn_gate_exps.weight"),
1975 n_experts,
1976 )?;
1977 let u = stored_expert_specs(
1978 &file,
1979 &format!("blk.{l}.ffn_up_exps.weight"),
1980 n_experts,
1981 )?;
1982 let d = stored_expert_specs(
1983 &file,
1984 &format!("blk.{l}.ffn_down_exps.weight"),
1985 n_experts,
1986 )?;
1987 match (g, u, d) {
1988 (Some(gt), Some(ut), Some(dt)) => {
1989 let mut layouts = Vec::with_capacity(n_experts);
1990 for e in 0..n_experts {
1991 let key = ExpertKey {
1992 layer: l as u32,
1993 expert: e as u32,
1994 };
1995 store_segments.insert(
1996 key,
1997 [
1998 (gt.shard, gt.per_expert[e].0, gt.per_expert[e].1),
1999 (ut.shard, ut.per_expert[e].0, ut.per_expert[e].1),
2000 (dt.shard, dt.per_expert[e].0, dt.per_expert[e].1),
2001 ],
2002 );
2003 let mut gate = gt.spec;
2004 let mut up = ut.spec;
2005 let mut down = dt.spec;
2006 gate.offset = 0;
2007 up.offset = gate.len;
2008 down.offset = gate.len + up.len;
2009 layouts.push(StoredExpertLayout { gate, up, down });
2010 }
2011 Some(layouts)
2012 }
2013 _ => None,
2014 }
2015 } else {
2016 None
2017 };
2018 match stored {
2019 Some(layouts) => {
2020 stored_layouts.push(Some(layouts));
2024 ExpertBacking::Resident(Vec::new())
2025 }
2026 None => {
2027 let gates = split_expert_tensor(
2028 &file,
2029 &format!("blk.{l}.ffn_gate_exps.weight"),
2030 n_experts,
2031 )?;
2032 let ups = split_expert_tensor(
2033 &file,
2034 &format!("blk.{l}.ffn_up_exps.weight"),
2035 n_experts,
2036 )?;
2037 let downs = split_expert_tensor(
2038 &file,
2039 &format!("blk.{l}.ffn_down_exps.weight"),
2040 n_experts,
2041 )?;
2042 ExpertBacking::Resident(
2043 gates
2044 .into_iter()
2045 .zip(ups)
2046 .zip(downs)
2047 .map(|((gate, up), down)| ExpertWeights { gate, up, down })
2048 .collect(),
2049 )
2050 }
2051 }
2052 };
2053 if stored_layouts.len() < layers.len() + 1 {
2054 stored_layouts.push(None);
2055 }
2056
2057 let shared_experts: Vec<ExpertWeights> =
2058 if config.moe.n_shared_experts > 0 && !is_dense_layer {
2059 vec![ExpertWeights {
2060 gate: load_weight_matrix(&file, &format!("blk.{l}.ffn_gate_shexp.weight"))?,
2061 up: load_weight_matrix(&file, &format!("blk.{l}.ffn_up_shexp.weight"))?,
2062 down: load_weight_matrix(&file, &format!("blk.{l}.ffn_down_shexp.weight"))?,
2063 }]
2064 } else {
2065 Vec::new()
2066 };
2067
2068 let router = if !is_dense_layer {
2069 load_weight_matrix(&file, &format!("blk.{l}.ffn_gate_inp.weight"))?
2070 } else {
2071 WeightMatrix::F32(Tensor::zeros(vec![1, config.hidden_dim]))
2074 };
2075
2076 let n_for_counts = match &experts {
2077 ExpertBacking::Resident(v) if v.is_empty() => n_experts,
2078 other => other.n_experts(),
2079 };
2080 let activation_counts = (0..n_for_counts)
2081 .map(|_| std::sync::atomic::AtomicU64::new(0))
2082 .collect();
2083 let shared_expert_gate = if is_dense_layer {
2092 None
2093 } else {
2094 load_f32_vec_optional(&file, &format!("blk.{l}.ffn_gate_inp_shexp.weight"))?
2095 };
2096 #[cfg(feature = "metal")]
2097 let packed_q4 = match &experts {
2098 ExpertBacking::Resident(v) if !v.is_empty() => try_build_moe_packed_q4_planes(v),
2099 _ => None,
2100 };
2101 let exp_probs_bias = if is_dense_layer {
2109 None
2110 } else {
2111 load_f32_vec_optional(&file, &format!("blk.{l}.exp_probs_b.bias"))?
2112 };
2113 if let Some(bias) = &exp_probs_bias {
2114 if bias.len() != config.moe.n_experts {
2115 return Err(LoadError::UnsupportedFeature(
2116 arch.clone(),
2117 format!(
2118 "blk.{l}.exp_probs_b.bias has {} entries but the model has {} experts",
2119 bias.len(),
2120 config.moe.n_experts
2121 ),
2122 ));
2123 }
2124 if config.moe.expert_group_count.is_some() {
2131 return Err(LoadError::UnsupportedFeature(
2132 arch.clone(),
2133 format!(
2134 "blk.{l}.exp_probs_b.bias together with expert groups \
2135 ({:?}): llama.cpp masks the biased scores per group \
2136 before a global top-k, which is not the per-group \
2137 top-k ferrox implements",
2138 config.moe.expert_group_count
2139 ),
2140 ));
2141 }
2142 }
2143 let moe = MoeWeights {
2144 router,
2145 experts,
2146 shared_experts,
2147 shared_expert_gate,
2148 exp_probs_bias,
2149 norm_weight: if post_attn_norm_is_pre_ffn_norm {
2150 load_f32_vec(&file, &format!("blk.{l}.post_attention_norm.weight"))?
2151 } else {
2152 load_f32_vec(&file, &format!("blk.{l}.ffn_norm.weight"))?
2153 },
2154 activation_counts,
2155 #[cfg(feature = "metal")]
2156 packed_q4,
2157 };
2158
2159 if is_gpt_oss {
2160 gpt_oss_layers.push(load_gpt_oss_layer(&file, l, &config)?);
2161 }
2162
2163 layers.push(LayerWeights { attn, moe });
2164 }
2165
2166 let final_norm = load_f32_vec(&file, "output_norm.weight")?;
2167 let output_head = match load_weight_matrix(&file, "output.weight") {
2172 Ok(w) => w,
2173 Err(_) => load_weight_matrix(&file, "token_embd.weight")?,
2174 };
2175
2176 if !store_segments.is_empty() {
2182 let budget = expert_cache_bytes
2183 .expect("store_segments only populated when a cache budget is set")
2184 as usize;
2185 let files: Result<Vec<std::fs::File>, std::io::Error> =
2186 file.shard_paths().iter().map(std::fs::File::open).collect();
2187 let files = files.map_err(GgufError::from)?;
2188 let store = std::sync::Arc::new(ExpertStore::new(
2189 GgufExpertSource {
2190 files,
2191 segments: store_segments,
2192 },
2193 budget,
2194 ));
2195 for (l, layer) in layers.iter_mut().enumerate() {
2196 if let Some(layouts) = stored_layouts.get_mut(l).and_then(Option::take) {
2197 layer.moe.experts = ExpertBacking::Stored {
2198 store: std::sync::Arc::clone(&store),
2199 layouts,
2200 layer: l as u32,
2201 };
2202 }
2203 }
2204 }
2205
2206 config.qk_norm_style = refined_qk_norm;
2207
2208 let family = crate::capability::resolve_profile(
2209 file.metadata_str("general.architecture").unwrap_or("llama"),
2210 )
2211 .map(|p| p.family)
2212 .unwrap_or(crate::capability::DecoderFamily::StandardGqa);
2213 let memory_kind = crate::capability::resolve_profile(
2214 file.metadata_str("general.architecture").unwrap_or("llama"),
2215 )
2216 .map(|p| p.memory)
2217 .unwrap_or(crate::capability::MemoryKind::KvGqa);
2218 let execution_plan = crate::execution_plan::ExecutionPlan::from_config(
2219 &config,
2220 family,
2221 memory_kind,
2222 crate::execution_plan::ExecutionPlan::probe_metal_caps(),
2223 );
2224
2225 let decoder = Decoder {
2226 config,
2227 embedding,
2228 layers,
2229 final_norm,
2230 output_head,
2231 gpu_vram_budget_bytes: None,
2232 gpt_oss: if is_gpt_oss {
2233 Some(crate::decoder::GptOssWeights {
2234 layers: gpt_oss_layers,
2235 })
2236 } else {
2237 None
2238 },
2239 qk_norm_after_rope: QK_NORM_AFTER_ROPE_ARCHITECTURES.contains(&arch.as_str()),
2240 #[cfg(feature = "metal")]
2241 metal_attn_kv: std::sync::Mutex::new(None),
2242 execution_plan,
2243 plan_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
2244 };
2245 decoder.probe_kernels();
2249 ferrox_core::kernel_registry::seal_or_error()
2250 .map_err(|e| LoadError::StrictKernels(e.to_string()))?;
2251 for name in crate::config::MODEL_LEVEL_TENSORS_READ_BY_CONFIG {
2258 file.note_consumed(name);
2259 }
2260 assert_every_tensor_consumed(&file)?;
2261 Ok(decoder)
2262 }
2263}
2264
2265const IGNORED_TENSOR_PREFIXES: &[&str] = &["mm.", "v.", "mmproj.", "resampler.", "audio."];
2270
2271pub fn assert_every_tensor_consumed(file: &ShardedGguf) -> Result<(), LoadError> {
2293 let mut left: Vec<String> = file
2294 .unconsumed_tensors()
2295 .into_iter()
2296 .filter(|n| !IGNORED_TENSOR_PREFIXES.iter().any(|p| n.starts_with(p)))
2297 .collect();
2298 if left.is_empty() {
2299 return Ok(());
2300 }
2301 left.sort();
2302 let shown = left.iter().take(8).cloned().collect::<Vec<_>>().join(", ");
2303 let listing = if left.len() > 8 {
2304 format!("{shown}, … (+{} more)", left.len() - 8)
2305 } else {
2306 shown
2307 };
2308 if matches!(
2309 std::env::var("FERROX_ALLOW_UNKNOWN_TENSORS")
2310 .ok()
2311 .as_deref(),
2312 Some("1") | Some("true") | Some("on")
2313 ) {
2314 eprintln!(
2315 "ferrox: WARNING -- {} tensor(s) in this checkpoint are never read \
2316 ({listing}); output may be wrong (FERROX_ALLOW_UNKNOWN_TENSORS=1)",
2317 left.len()
2318 );
2319 return Ok(());
2320 }
2321 Err(LoadError::UnconsumedTensors(left.len(), listing))
2322}
2323
2324#[cfg(test)]
2325mod tests {
2326
2327 #[test]
2343 fn a_quantized_one_dimensional_tensor_widens_through_the_shared_helper() {
2344 let values: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) * 0.25).collect();
2345 let quantized = ferrox_quant::quantize_q8_0(&values);
2346
2347 struct OneTensor {
2348 info: TensorInfo,
2349 bytes: Vec<u8>,
2350 }
2351 impl TensorSource for OneTensor {
2352 fn metadata(&self, _key: &str) -> Option<&ferrox_gguf::GgufValue> {
2353 None
2354 }
2355 fn find_tensor(&self, name: &str) -> Option<&TensorInfo> {
2356 (name == self.info.name).then_some(&self.info)
2357 }
2358 fn tensor_bytes(&self, _name: &str) -> Result<&[u8], GgufError> {
2359 Ok(&self.bytes)
2360 }
2361 fn tensor_mapped_range(
2362 &self,
2363 name: &str,
2364 ) -> Result<
2365 (
2366 std::sync::Arc<ferrox_gguf::MmapHandle>,
2367 std::ops::Range<usize>,
2368 ),
2369 GgufError,
2370 > {
2371 Err(GgufError::TensorNotFound(name.to_string()))
2373 }
2374 }
2375
2376 let source = OneTensor {
2377 info: TensorInfo {
2378 name: "blk.0.attn_norm.weight".to_string(),
2379 shape: vec![64],
2380 dtype: GgmlType::Q8_0,
2381 offset: 0,
2382 },
2383 bytes: quantized,
2384 };
2385
2386 let widened = load_f32_vec(&source, "blk.0.attn_norm.weight")
2387 .expect("a Q8_0 norm must load, not report an unsupported dtype");
2388 assert_eq!(widened.len(), values.len());
2389 for (got, want) in widened.iter().zip(values.iter()) {
2390 assert!(
2391 (got - want).abs() < 0.05,
2392 "q8_0 round trip: got {got}, want {want}"
2393 );
2394 }
2395 }
2396 use super::*;
2397 use byteorder::{LittleEndian, WriteBytesExt};
2398 use std::io::Write;
2399
2400 fn write_string(buf: &mut Vec<u8>, s: &str) {
2401 buf.write_u64::<LittleEndian>(s.len() as u64).unwrap();
2402 buf.write_all(s.as_bytes()).unwrap();
2403 }
2404
2405 fn write_kv_str(buf: &mut Vec<u8>, key: &str, val: &str) {
2406 write_string(buf, key);
2407 buf.write_u32::<LittleEndian>(8).unwrap(); write_string(buf, val);
2409 }
2410
2411 fn build_arch_only_gguf(arch: &str) -> Vec<u8> {
2417 let mut buf = Vec::new();
2418 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2419 .unwrap();
2420 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);
2424 buf
2425 }
2426
2427 #[test]
2428 fn model_config_from_gguf_fails_loudly_when_required_hparams_are_missing() {
2429 let tmp =
2430 std::env::temp_dir().join(format!("ferrox_test_arch_only_{}.gguf", std::process::id()));
2431 std::fs::write(&tmp, build_arch_only_gguf("llama")).unwrap();
2434 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2435 std::fs::remove_file(&tmp).ok();
2436
2437 match ModelConfig::from_gguf(&file) {
2438 Err(LoadError::MissingHparam(key)) => {
2439 assert_eq!(key, "llama.block_count");
2440 }
2441 other => panic!(
2442 "expected LoadError::MissingHparam for a file with no hparam keys, got {other:?}"
2443 ),
2444 }
2445 }
2446
2447 #[test]
2448 fn model_config_from_gguf_fails_closed_on_unknown_architecture() {
2449 let tmp = std::env::temp_dir().join(format!(
2450 "ferrox_test_unknown_arch_{}.gguf",
2451 std::process::id()
2452 ));
2453 std::fs::write(&tmp, build_arch_only_gguf("bogus-arch-with-no-hparams")).unwrap();
2454 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2455 std::fs::remove_file(&tmp).ok();
2456
2457 match ModelConfig::from_gguf(&file) {
2458 Err(LoadError::UnsupportedArchitecture(arch)) => {
2459 assert_eq!(arch, "bogus-arch-with-no-hparams");
2460 }
2461 other => panic!(
2462 "expected LoadError::UnsupportedArchitecture for an unregistered arch, got {other:?}"
2463 ),
2464 }
2465 }
2466
2467 fn write_kv_f32(buf: &mut Vec<u8>, key: &str, val: f32) {
2468 write_string(buf, key);
2469 buf.write_u32::<LittleEndian>(6).unwrap(); buf.write_f32::<LittleEndian>(val).unwrap();
2471 }
2472
2473 fn build_arch_plus_f32_gguf(arch: &str, key: &str, val: f32) -> 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>(2).unwrap(); write_kv_str(&mut buf, "general.architecture", arch);
2483 write_kv_f32(&mut buf, key, val);
2484 buf
2485 }
2486
2487 fn config_error_for(arch: &str, key: &str, val: f32, tag: &str) -> LoadError {
2488 let tmp = std::env::temp_dir().join(format!("ferrox_test_scale_{tag}.gguf"));
2489 std::fs::write(&tmp, build_arch_plus_f32_gguf(arch, key, val)).unwrap();
2490 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2491 std::fs::remove_file(&tmp).ok();
2492 ModelConfig::from_gguf(&file).expect_err("must not succeed")
2493 }
2494
2495 #[test]
2501 fn a_declared_multiplier_this_decoder_does_not_apply_is_refused_by_name() {
2502 for (key, val) in [
2503 ("granite.logit_scale", 6.0f32),
2504 ("granite.residual_scale", 0.22),
2505 ("granite.embedding_scale", 12.0),
2506 ("granite.attention.scale", 0.015_625),
2507 ] {
2508 let tag = key.replace('.', "_");
2509 match config_error_for("granite", key, val, &tag) {
2510 LoadError::UnsupportedFeature(arch, msg) => {
2511 assert_eq!(arch, "granite");
2512 assert!(msg.contains(key), "error must name the key: {msg}");
2513 }
2514 other => panic!("expected UnsupportedFeature for {key}, got {other:?}"),
2515 }
2516 }
2517 }
2518
2519 #[test]
2525 fn a_multiplier_that_is_a_no_op_is_not_refused() {
2526 for (key, val) in [
2527 ("granite.logit_scale", 1.0f32),
2528 ("granite.residual_scale", 1.0),
2529 ("granite.embedding_scale", 1.0),
2530 ("granite.attention.scale", 0.0),
2531 ] {
2532 let tag = format!("noop_{}", key.replace('.', "_"));
2533 match config_error_for("granite", key, val, &tag) {
2536 LoadError::MissingHparam(k) => assert_eq!(k, "granite.block_count"),
2537 other => panic!("no-op {key}={val} must pass the scaling gate, got {other:?}"),
2538 }
2539 }
2540 }
2541
2542 enum Kv<'a> {
2545 Str(&'a str),
2546 U32(u32),
2547 F32(f32),
2548 }
2549
2550 fn build_metadata_gguf(kvs: &[(&str, Kv)]) -> Vec<u8> {
2553 let mut buf = Vec::new();
2554 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2555 .unwrap();
2556 buf.write_u32::<LittleEndian>(3).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); buf.write_u64::<LittleEndian>(kvs.len() as u64).unwrap();
2559 for (k, v) in kvs {
2560 match v {
2561 Kv::Str(s) => write_kv_str(&mut buf, k, s),
2562 Kv::U32(n) => {
2563 write_string(&mut buf, k);
2564 buf.write_u32::<LittleEndian>(4).unwrap(); buf.write_u32::<LittleEndian>(*n).unwrap();
2566 }
2567 Kv::F32(f) => write_kv_f32(&mut buf, k, *f),
2568 }
2569 }
2570 buf
2571 }
2572
2573 fn open_metadata_gguf(tag: &str, kvs: &[(&str, Kv)]) -> ferrox_gguf::GgufFile {
2574 let tmp = std::env::temp_dir().join(format!("ferrox_test_meta_{tag}.gguf"));
2575 std::fs::write(&tmp, build_metadata_gguf(kvs)).unwrap();
2576 let file = ferrox_gguf::GgufFile::open(&tmp).expect("header-only file must parse");
2577 std::fs::remove_file(&tmp).ok();
2578 file
2579 }
2580
2581 fn llama_config_with(tag: &str, extra: &[(&str, Kv)]) -> ModelConfig {
2584 let mut kvs: Vec<(&str, Kv)> = vec![
2585 ("general.architecture", Kv::Str("llama")),
2586 ("llama.block_count", Kv::U32(1)),
2587 ("llama.embedding_length", Kv::U32(64)),
2588 ("llama.attention.head_count", Kv::U32(1)),
2589 ("llama.attention.head_count_kv", Kv::U32(1)),
2590 ("llama.attention.key_length", Kv::U32(64)),
2591 ("llama.rope.freq_base", Kv::F32(10_000.0)),
2592 ];
2593 for (k, v) in extra {
2594 kvs.push((
2595 k,
2596 match v {
2597 Kv::Str(s) => Kv::Str(s),
2598 Kv::U32(n) => Kv::U32(*n),
2599 Kv::F32(f) => Kv::F32(*f),
2600 },
2601 ));
2602 }
2603 ModelConfig::from_gguf(&open_metadata_gguf(tag, &kvs)).expect("fixture must load")
2604 }
2605
2606 #[test]
2620 fn the_ffn_activation_follows_the_architecture_not_the_family() {
2621 use crate::capability::uses_geglu;
2622 use crate::config::FfnActivation;
2623
2624 assert!(uses_geglu("grok"), "grok's MoE FFN gate is GELU upstream");
2625 assert!(!uses_geglu("dbrx"));
2628 assert!(!uses_geglu("llama"));
2629
2630 for gemma in ["gemma2", "gemma3"] {
2635 assert!(
2636 !uses_geglu(gemma),
2637 "{gemma} is GELU via GemmaFamily; listing it here too \
2638 would hide a later regression in the family rule"
2639 );
2640 assert_eq!(
2641 config_for_arch(gemma).expect("gemma loads").ffn_activation,
2642 FfnActivation::Gelu,
2643 "{gemma}"
2644 );
2645 }
2646
2647 assert_eq!(
2649 config_for_arch("llama")
2650 .expect("llama loads")
2651 .ffn_activation,
2652 FfnActivation::Swiglu
2653 );
2654 }
2655
2656 #[test]
2672 fn the_architectures_llama_cpp_does_not_renormalise_are_pinned() {
2673 for arch in ["deepseek", "olmoe", "qwen2moe"] {
2674 assert!(
2675 NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&arch),
2676 "{arch} passes norm_w=false in llama.cpp and must not be renormalised"
2677 );
2678 }
2679 assert!(!NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"deepseek2"));
2683 assert!(!NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"qwen3moe"));
2684 }
2685
2686 #[test]
2696 fn the_architectures_llama_cpp_defaults_to_sigmoid_gating_are_pinned() {
2697 for arch in ["afmoe", "deepseek2", "glm4moe", "laguna", "step35"] {
2698 assert!(
2699 SIGMOID_GATING_ARCHITECTURES.contains(&arch),
2700 "{arch} sets SIGMOID when the gating key is absent"
2701 );
2702 }
2703 for softmax in ["ernie4_5-moe", "qwen3moe", "olmoe", "llama"] {
2707 assert!(
2708 !SIGMOID_GATING_ARCHITECTURES.contains(&softmax),
2709 "{softmax} does not default to sigmoid"
2710 );
2711 }
2712 }
2713
2714 #[test]
2733 fn every_architecture_keyed_behaviour_table_names_a_real_generic_row() {
2734 let tables: &[(&str, &[&str])] = &[
2735 ("SIGMOID_GATING_ARCHITECTURES", SIGMOID_GATING_ARCHITECTURES),
2736 (
2737 "NO_TOPK_RENORMALIZE_ARCHITECTURES",
2738 NO_TOPK_RENORMALIZE_ARCHITECTURES,
2739 ),
2740 (
2741 "PRE_FFN_NORM_IS_POST_ATTENTION_NORM",
2742 PRE_FFN_NORM_IS_POST_ATTENTION_NORM,
2743 ),
2744 ("LEADING_DENSE_KEY_IS_INERT", LEADING_DENSE_KEY_IS_INERT),
2745 (
2746 "QK_NORM_AFTER_ROPE_ARCHITECTURES",
2747 QK_NORM_AFTER_ROPE_ARCHITECTURES,
2748 ),
2749 ];
2750 for (table, names) in tables {
2751 for arch in *names {
2752 let profile = crate::capability::resolve_profile(arch).unwrap_or_else(|| {
2753 panic!("{table} names `{arch}`, which the catalog does not have")
2754 });
2755 if matches!(profile.path, crate::capability::ArchPath::GenericGqa { .. }) {
2756 continue;
2757 }
2758 let owner = DEDICATED_OWNS_ITS_BEHAVIOUR
2763 .iter()
2764 .find(|(name, _)| name == arch)
2765 .map(|(_, owner)| *owner);
2766 assert!(
2767 owner.is_some(),
2768 "{table} names `{arch}`, which resolves to {:?} and never reaches this \
2769 loader, so the entry cannot fire. Either drop it, or add it to \
2770 DEDICATED_OWNS_ITS_BEHAVIOUR naming what applies the behaviour instead",
2771 profile.path
2772 );
2773 }
2774 }
2775 }
2776
2777 #[test]
2791 fn the_layer_shape_tables_only_name_audited_architectures() {
2792 for (table, names) in [
2793 (
2794 "PRE_FFN_NORM_IS_POST_ATTENTION_NORM",
2795 PRE_FFN_NORM_IS_POST_ATTENTION_NORM,
2796 ),
2797 ("LEADING_DENSE_KEY_IS_INERT", LEADING_DENSE_KEY_IS_INERT),
2798 (
2799 "QK_NORM_AFTER_ROPE_ARCHITECTURES",
2800 QK_NORM_AFTER_ROPE_ARCHITECTURES,
2801 ),
2802 ] {
2803 for arch in names {
2804 assert!(
2805 crate::capability::is_audited_generic(arch),
2806 "{table} names `{arch}`, which is not in AUDITED_GENERIC_GQA. Either it \
2807 has a fixture proving the change is right -- audit it -- or the entry \
2808 is a guess about a graph"
2809 );
2810 }
2811 }
2812 }
2813
2814 fn config_for_arch(arch: &'static str) -> Result<ModelConfig, LoadError> {
2815 let keys: Vec<String> = [
2818 "block_count",
2819 "embedding_length",
2820 "attention.head_count",
2821 "attention.head_count_kv",
2822 "attention.key_length",
2823 ]
2824 .iter()
2825 .map(|k| format!("{arch}.{k}"))
2826 .collect();
2827 let theta = format!("{arch}.rope.freq_base");
2828 let kvs: Vec<(&str, Kv)> = vec![
2829 ("general.architecture", Kv::Str(arch)),
2830 (keys[0].as_str(), Kv::U32(1)),
2831 (keys[1].as_str(), Kv::U32(64)),
2832 (keys[2].as_str(), Kv::U32(1)),
2833 (keys[3].as_str(), Kv::U32(1)),
2834 (keys[4].as_str(), Kv::U32(64)),
2835 (theta.as_str(), Kv::F32(10_000.0)),
2836 ];
2837 ModelConfig::from_gguf(&open_metadata_gguf(arch, &kvs))
2838 }
2839
2840 #[test]
2848 fn an_unaudited_generic_architecture_refuses_rather_than_guessing() {
2849 assert!(
2857 !crate::capability::is_audited_generic("xverse"),
2858 "this test needs an arch that is generic AND unaudited"
2859 );
2860 match config_for_arch("xverse") {
2861 Err(LoadError::UnauditedArchitecture(name, ..)) => assert_eq!(name, "xverse"),
2862 other => panic!("expected an unaudited refusal, got {other:?}"),
2863 }
2864 }
2865
2866 #[test]
2869 fn an_audited_architecture_still_loads() {
2870 assert!(crate::capability::is_audited_generic("llama"));
2871 assert!(config_for_arch("llama").is_ok());
2872 }
2873
2874 #[test]
2881 fn a_named_refusal_outranks_the_unaudited_one() {
2882 let err = config_for_arch("gpt2").expect_err("gpt2 must refuse");
2883 assert!(
2884 !matches!(err, LoadError::UnauditedArchitecture(..)),
2885 "gpt2 should report its own reason, not that nobody audited it: {err:?}"
2886 );
2887 }
2888
2889 #[test]
2900 fn a_gguf_declaring_yarn_gets_its_rope_frequencies_rewritten() {
2901 let cfg = llama_config_with(
2902 "yarn",
2903 &[
2904 ("llama.rope.scaling.type", Kv::Str("yarn")),
2905 ("llama.rope.scaling.factor", Kv::F32(8.0)),
2906 (
2907 "llama.rope.scaling.original_context_length",
2908 Kv::U32(131_072),
2909 ),
2910 ],
2911 );
2912 let factors = cfg
2913 .rope_freqs
2914 .expect("a YaRN checkpoint must carry rewritten per-band frequencies");
2915 assert_eq!(factors.len(), 32, "one divisor per rotation band");
2916 assert!(
2917 (factors[0] - 1.0).abs() < 1e-6,
2918 "the fastest band is left extrapolated, got {}",
2919 factors[0]
2920 );
2921 let ramp = (31.0 - 22.0) / (35.0 - 22.0);
2922 let want = 1.0 / (ramp / 8.0 + (1.0 - ramp));
2923 assert!(
2924 (factors[31] - want).abs() < 1e-4,
2925 "slowest band: got {}, reference {want}",
2926 factors[31]
2927 );
2928 }
2929
2930 #[test]
2945 fn linear_scaling_is_applied_as_a_uniform_frequency_divisor() {
2946 let cfg = llama_config_with(
2947 "linear",
2948 &[
2949 ("llama.rope.scaling.type", Kv::Str("linear")),
2950 ("llama.rope.scaling.factor", Kv::F32(4.0)),
2951 ],
2952 );
2953 let freqs = cfg
2954 .rope_freqs
2955 .as_ref()
2956 .expect("linear scaling must produce frequency factors");
2957 assert_eq!(freqs.len(), cfg.head_dim / 2, "one factor per rotated pair");
2958 assert!(
2959 freqs.iter().all(|f| (*f - 4.0).abs() < 1e-6),
2960 "linear scaling is uniform across bands, unlike YaRN: got {freqs:?}"
2961 );
2962 }
2963
2964 #[test]
2966 fn a_linear_factor_of_one_is_treated_as_absent() {
2967 assert!(llama_config_with(
2968 "linear_one",
2969 &[
2970 ("llama.rope.scaling.type", Kv::Str("linear")),
2971 ("llama.rope.scaling.factor", Kv::F32(1.0)),
2972 ],
2973 )
2974 .rope_freqs
2975 .is_none());
2976 }
2977
2978 #[test]
2979 fn a_gguf_without_yarn_scaling_keeps_its_rope_frequencies_untouched() {
2980 assert!(llama_config_with("noscale", &[]).rope_freqs.is_none());
2981 assert!(llama_config_with(
2988 "yarn_factor_one",
2989 &[
2990 ("llama.rope.scaling.type", Kv::Str("yarn")),
2991 ("llama.rope.scaling.factor", Kv::F32(1.0)),
2992 (
2993 "llama.rope.scaling.original_context_length",
2994 Kv::U32(131_072),
2995 ),
2996 ],
2997 )
2998 .rope_freqs
2999 .is_none());
3000 }
3001
3002 #[test]
3009 fn yarn_without_an_original_context_length_is_not_guessed_at() {
3010 let cfg = llama_config_with(
3011 "yarn_noctx",
3012 &[
3013 ("llama.rope.scaling.type", Kv::Str("yarn")),
3014 ("llama.rope.scaling.factor", Kv::F32(8.0)),
3015 ],
3016 );
3017 assert!(cfg.rope_freqs.is_none());
3018 }
3019
3020 #[test]
3025 fn gguf_sampling_metadata_is_read_as_the_checkpoints_recommendation() {
3026 use crate::sampling::RecommendedSampling;
3027 let full = RecommendedSampling::from_gguf(&open_metadata_gguf(
3028 "sampling_full",
3029 &[
3030 ("general.architecture", Kv::Str("llama")),
3031 ("general.sampling.temp", Kv::F32(1.0)),
3032 ("general.sampling.top_k", Kv::U32(20)),
3033 ("general.sampling.top_p", Kv::F32(0.95)),
3034 ],
3035 ));
3036 assert_eq!(
3037 full,
3038 RecommendedSampling {
3039 temperature: Some(1.0),
3040 top_p: Some(0.95),
3041 top_k: Some(20),
3042 }
3043 );
3044
3045 let partial = RecommendedSampling::from_gguf(&open_metadata_gguf(
3046 "sampling_partial",
3047 &[
3048 ("general.architecture", Kv::Str("llama")),
3049 ("general.sampling.top_k", Kv::U32(40)),
3050 ],
3051 ));
3052 assert_eq!(partial.top_k, Some(40));
3053 assert_eq!(partial.temperature, None);
3054 assert_eq!(partial.top_p, None);
3055 }
3056
3057 #[test]
3062 fn an_integer_valued_sampling_temp_is_still_a_recommendation() {
3063 let recommended = crate::sampling::RecommendedSampling::from_gguf(&open_metadata_gguf(
3064 "sampling_int_temp",
3065 &[
3066 ("general.architecture", Kv::Str("llama")),
3067 ("general.sampling.temp", Kv::U32(1)),
3068 ],
3069 ));
3070 assert_eq!(recommended.temperature, Some(1.0));
3071 }
3072
3073 #[test]
3076 fn a_gguf_without_sampling_metadata_recommends_nothing() {
3077 let recommended = crate::sampling::RecommendedSampling::from_gguf(&open_metadata_gguf(
3078 "sampling_absent",
3079 &[("general.architecture", Kv::Str("llama"))],
3080 ));
3081 assert!(recommended.is_empty());
3082 }
3083
3084 #[test]
3085 fn model_config_from_gguf_rejects_dedicated_architectures() {
3086 let tmp = std::env::temp_dir().join(format!(
3087 "ferrox_test_dedicated_arch_{}.gguf",
3088 std::process::id()
3089 ));
3090 std::fs::write(&tmp, build_arch_only_gguf("deepseek4")).unwrap();
3091 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
3092 std::fs::remove_file(&tmp).ok();
3093
3094 match ModelConfig::from_gguf(&file) {
3095 Err(LoadError::DedicatedArchitectureRequired(arch, _)) => {
3096 assert_eq!(arch, "deepseek4");
3097 }
3098 other => panic!(
3099 "expected LoadError::DedicatedArchitectureRequired for deepseek4, got {other:?}"
3100 ),
3101 }
3102 }
3103
3104 #[rustfmt::skip]
3108 const Q5_K_TEST_BLOCK: [u8; 176] = [
3109 0x66, 0x2a, 0x66, 0x2a, 0x01, 0x01, 0x01, 0x01, 0x4f, 0x4b, 0x10, 0x12, 0x41, 0xe2, 0xc1,
3110 0xb1, 0x72, 0x2f, 0x20, 0x07, 0x31, 0x0c, 0x38, 0xb3, 0x9c, 0xb8, 0xad, 0x2f, 0x9a, 0xea,
3111 0x17, 0xd0, 0xee, 0x93, 0x9e, 0x3e, 0x74, 0xbb, 0x28, 0x18, 0x39, 0x25, 0xb6, 0x09, 0x18,
3112 0x29, 0x1c, 0x1d, 0x29, 0x41, 0x40, 0x0a, 0x74, 0x7d, 0xfd, 0x21, 0xdd, 0x6d, 0x45, 0x73,
3113 0x0e, 0x1e, 0xc0, 0x4a, 0xfc, 0xf3, 0x8e, 0x24, 0x6b, 0x34, 0x7d, 0xbe, 0x94, 0xde, 0x59,
3114 0x7a, 0x35, 0x30, 0x36, 0x0a, 0xf9, 0x4a, 0x9b, 0xa2, 0x26, 0x21, 0xa2, 0xfa, 0xdf, 0x4b,
3115 0x29, 0x64, 0x6f, 0xbb, 0xca, 0x0f, 0x3c, 0xda, 0x20, 0xf4, 0x93, 0x86, 0xab, 0x6e, 0xb9,
3116 0xe5, 0xd5, 0xa0, 0x82, 0xd6, 0x41, 0xff, 0x12, 0xbc, 0x34, 0xbb, 0xab, 0xb8, 0x20, 0x2f,
3117 0xbb, 0x5f, 0x0c, 0x10, 0xcf, 0x49, 0xc5, 0x86, 0x5c, 0xdf, 0xff, 0x78, 0x44, 0x26, 0x3b,
3118 0xc2, 0x23, 0x3d, 0x2b, 0xe9, 0x00, 0x12, 0xf8, 0xea, 0xe2, 0x9e, 0x5e, 0x50, 0x20, 0x9f,
3119 0x9d, 0x8d, 0x7d, 0x7f, 0xcc, 0x1d, 0x0e, 0x13, 0xf8, 0xc2, 0xf1, 0x3d, 0x08, 0x2f, 0x23,
3120 0x13, 0xac, 0x0d, 0xa7, 0xe7, 0x20, 0xa3, 0x90, 0xb7, 0xc8, 0x28,
3121 ];
3122
3123 fn build_single_q5_k_tensor_gguf() -> Vec<u8> {
3124 let mut buf = Vec::new();
3125 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3126 .unwrap();
3127 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");
3132
3133 write_string(&mut buf, "test.weight");
3134 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 {
3144 buf.push(0);
3145 }
3146 buf.extend_from_slice(&Q5_K_TEST_BLOCK);
3147 buf
3148 }
3149
3150 fn fused_dot_tolerance(weights: &[f32], x: &[f32], exact_bound: f32) -> f32 {
3170 if !ferrox_core::weight_matrix::cpu_int_dot_enabled() {
3171 return exact_bound;
3172 }
3173 let amax = x.iter().fold(0.0f32, |a, v| a.max(v.abs()));
3174 let l2 = weights.iter().map(|w| w * w).sum::<f32>().sqrt();
3175 4.0 * (amax / 127.0) / 12f32.sqrt() * l2 + exact_bound
3176 }
3177
3178 #[test]
3179 fn load_weight_matrix_handles_a_real_on_disk_q5_k_tensor_end_to_end() {
3180 let tmp = std::env::temp_dir().join(format!(
3181 "ferrox_test_q5k_tensor_{}.gguf",
3182 std::process::id()
3183 ));
3184 std::fs::write(&tmp, build_single_q5_k_tensor_gguf()).unwrap();
3185 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q5_K GGUF file must parse");
3186 std::fs::remove_file(&tmp).ok();
3187
3188 let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_K tensor must load");
3189 assert_eq!(matrix.rows(), 1);
3190 assert_eq!(matrix.cols(), 256);
3191 match &matrix {
3192 WeightMatrix::Quantized { kind, data, .. } => {
3193 assert_eq!(*kind, QuantKind::Q5K);
3194 assert!(
3195 data.is_mapped(),
3196 "Q5_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
3197 );
3198 }
3199 _ => panic!("expected a Quantized matrix for a Q5_K tensor"),
3200 }
3201
3202 let expected = ferrox_quant::dequant_q5_k(&Q5_K_TEST_BLOCK).unwrap();
3203 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3204 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3205
3206 let got = matrix.apply(&x);
3207 assert_eq!(got.len(), 1);
3208 assert!(
3209 (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-2),
3210 "end-to-end loaded+applied Q5_K matrix diverged from direct dequant: got={} expected={}",
3211 got[0],
3212 expected_dot
3213 );
3214 }
3215
3216 #[rustfmt::skip]
3225 const Q6_K_TEST_BLOCK: [u8; 210] = [
3226 0xe0, 0xa5, 0x40, 0x5c, 0x8d, 0x3a, 0x0a, 0x26, 0xfb, 0x4b, 0x6e, 0x9a, 0xdf, 0x3e, 0xa3,
3227 0xc4, 0xf8, 0x2b, 0x1d, 0x95, 0x76, 0x7d, 0x3b, 0xcd, 0xfd, 0xef, 0xc2, 0x0b, 0x07, 0x63,
3228 0x29, 0xfb, 0x81, 0x57, 0xbe, 0xbe, 0x06, 0xf7, 0x3a, 0x92, 0xc4, 0x43, 0xff, 0xad, 0xac,
3229 0x7e, 0x0f, 0x00, 0x2a, 0x4f, 0xf0, 0xf8, 0xa9, 0xfa, 0x3c, 0x90, 0x6d, 0x73, 0x2d, 0x5a,
3230 0xe6, 0xc6, 0x46, 0xf2, 0x0d, 0x55, 0x4c, 0x25, 0x38, 0x71, 0x2b, 0x35, 0x38, 0x82, 0x16,
3231 0x37, 0x5f, 0x32, 0x61, 0x02, 0xdd, 0x2f, 0x6f, 0x7b, 0x1f, 0xb4, 0x1a, 0x1b, 0x3e, 0x4f,
3232 0x11, 0xa3, 0x17, 0x40, 0x5a, 0x5f, 0x76, 0xcd, 0x19, 0x27, 0x9b, 0xc7, 0xc8, 0xf7, 0xf7,
3233 0xee, 0xf4, 0x86, 0xd9, 0xfd, 0xa7, 0xfe, 0x9e, 0xac, 0x70, 0x53, 0x5b, 0x76, 0xfb, 0x39,
3234 0xf8, 0x4b, 0x98, 0xfe, 0xd0, 0x06, 0x21, 0x4c, 0x4d, 0xbe, 0x10, 0x2b, 0x06, 0x65, 0xc9,
3235 0x5e, 0xf9, 0x95, 0x72, 0xae, 0x99, 0xd9, 0x7e, 0x15, 0xbd, 0x5e, 0x6d, 0xe8, 0x25, 0x8a,
3236 0xd5, 0x99, 0xc6, 0x6b, 0x69, 0xc7, 0x84, 0xc6, 0xa4, 0xf7, 0xb9, 0x6d, 0x68, 0x45, 0x0e,
3237 0x65, 0x69, 0xeb, 0xe6, 0xeb, 0xe9, 0x28, 0xa6, 0xb9, 0x96, 0xf2, 0xe8, 0xa7, 0x9b, 0x6e,
3238 0x79, 0x8a, 0x68, 0x65, 0x59, 0x98, 0x8b, 0x44, 0x41, 0x98, 0x9a, 0x56, 0x01, 0x01, 0x01,
3239 0x02, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, 0x01, 0x01, 0x01, 0x02, 0x1f, 0x25,
3240 ];
3241
3242 fn build_single_q6_k_tensor_gguf() -> Vec<u8> {
3243 let mut buf = Vec::new();
3244 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3245 .unwrap();
3246 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");
3251
3252 write_string(&mut buf, "test.weight");
3253 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 {
3261 buf.push(0);
3262 }
3263 buf.extend_from_slice(&Q6_K_TEST_BLOCK);
3264 buf
3265 }
3266
3267 #[test]
3268 fn load_weight_matrix_handles_a_real_on_disk_q6_k_tensor_end_to_end() {
3269 let tmp = std::env::temp_dir().join(format!(
3270 "ferrox_test_q6k_tensor_{}.gguf",
3271 std::process::id()
3272 ));
3273 std::fs::write(&tmp, build_single_q6_k_tensor_gguf()).unwrap();
3274 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q6_K GGUF file must parse");
3275 std::fs::remove_file(&tmp).ok();
3276
3277 let matrix = load_weight_matrix(&file, "test.weight").expect("Q6_K tensor must load");
3278 assert_eq!(matrix.rows(), 1);
3279 assert_eq!(matrix.cols(), 256);
3280 match &matrix {
3281 WeightMatrix::Quantized { kind, data, .. } => {
3282 assert_eq!(*kind, QuantKind::Q6K);
3283 assert!(
3284 data.is_mapped(),
3285 "Q6_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
3286 );
3287 }
3288 _ => panic!("expected a Quantized matrix for a Q6_K tensor"),
3289 }
3290
3291 let expected = ferrox_quant::dequant_q6_k(&Q6_K_TEST_BLOCK).unwrap();
3292 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3293 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3294
3295 let got = matrix.apply(&x);
3296 assert_eq!(got.len(), 1);
3297 assert!(
3298 (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-2),
3299 "end-to-end loaded+applied Q6_K matrix diverged from direct dequant: got={} expected={}",
3300 got[0],
3301 expected_dot
3302 );
3303 }
3304
3305 fn build_single_bf16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
3306 let mut buf = Vec::new();
3307 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3308 .unwrap();
3309 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");
3314
3315 write_string(&mut buf, "test.weight");
3316 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(cols).unwrap();
3319 buf.write_u64::<LittleEndian>(rows).unwrap();
3320 buf.write_u32::<LittleEndian>(30).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
3324 buf.push(0);
3325 }
3326 for &v in values {
3327 let bf16_bits = (v.to_bits() >> 16) as u16;
3331 buf.extend_from_slice(&bf16_bits.to_le_bytes());
3332 }
3333 buf
3334 }
3335
3336 #[test]
3337 fn load_weight_matrix_handles_a_real_on_disk_bf16_tensor_end_to_end() {
3338 let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
3341 let tmp = std::env::temp_dir().join(format!(
3342 "ferrox_test_bf16_tensor_{}.gguf",
3343 std::process::id()
3344 ));
3345 std::fs::write(&tmp, build_single_bf16_tensor_gguf(2, 3, &values)).unwrap();
3346 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real BF16 GGUF file must parse");
3347 std::fs::remove_file(&tmp).ok();
3348
3349 let matrix = load_weight_matrix(&file, "test.weight").expect("BF16 tensor must load");
3350 assert_eq!(matrix.rows(), 2);
3351 assert_eq!(matrix.cols(), 3);
3352 match &matrix {
3353 WeightMatrix::F32(tensor) => {
3354 assert_eq!(tensor.data, values, "BF16 must widen to f32 exactly");
3355 }
3356 _ => panic!("expected an F32 matrix for a BF16 tensor (no fused dot kernel for it)"),
3357 }
3358 }
3359
3360 fn build_single_f16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
3361 let mut buf = Vec::new();
3362 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3363 .unwrap();
3364 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");
3369
3370 write_string(&mut buf, "test.weight");
3371 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(cols).unwrap();
3373 buf.write_u64::<LittleEndian>(rows).unwrap();
3374 buf.write_u32::<LittleEndian>(1).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
3378 buf.push(0);
3379 }
3380 for &v in values {
3381 buf.extend_from_slice(&half::f16::from_f32(v).to_le_bytes());
3382 }
3383 buf
3384 }
3385
3386 #[test]
3391 fn load_weight_matrix_handles_a_real_on_disk_f16_tensor_end_to_end() {
3392 let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
3393 let tmp = std::env::temp_dir().join(format!(
3394 "ferrox_test_f16_tensor_{}.gguf",
3395 std::process::id()
3396 ));
3397 std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
3398 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
3399 std::fs::remove_file(&tmp).ok();
3400
3401 let matrix = load_weight_matrix(&file, "test.weight").expect("F16 tensor must load");
3402 assert_eq!(matrix.rows(), 2);
3403 assert_eq!(matrix.cols(), 3);
3404 match &matrix {
3405 WeightMatrix::F32(tensor) => {
3406 assert_eq!(tensor.data, values, "F16 must widen to f32 exactly");
3407 }
3408 _ => panic!("expected an F32 matrix for an F16 tensor (no fused dot kernel for it)"),
3409 }
3410
3411 let tmp =
3414 std::env::temp_dir().join(format!("ferrox_test_f16_vec_{}.gguf", std::process::id()));
3415 std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
3416 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
3417 std::fs::remove_file(&tmp).ok();
3418 assert_eq!(load_f32_vec(&file, "test.weight").unwrap(), values);
3419 }
3420
3421 fn build_single_q5_1_tensor_gguf() -> 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-q5-1-test");
3430
3431 write_string(&mut buf, "test.weight");
3432 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 {
3440 buf.push(0);
3441 }
3442 buf.extend_from_slice(&0x3400u16.to_le_bytes());
3447 buf.extend_from_slice(&0x3E00u16.to_le_bytes());
3448 buf.extend_from_slice(&[0x9au8, 0x3c, 0xf0, 0x0f]);
3449 buf.extend_from_slice(&(0..16u8).map(|i| i | ((15 - i) << 4)).collect::<Vec<u8>>());
3450 buf
3451 }
3452
3453 #[test]
3454 fn load_weight_matrix_handles_a_real_on_disk_q5_1_tensor_end_to_end() {
3455 let tmp = std::env::temp_dir().join(format!(
3456 "ferrox_test_q5_1_tensor_{}.gguf",
3457 std::process::id()
3458 ));
3459 std::fs::write(&tmp, build_single_q5_1_tensor_gguf()).unwrap();
3460 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q5_1 GGUF file must parse");
3461 std::fs::remove_file(&tmp).ok();
3462
3463 let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_1 tensor must load");
3464 assert_eq!(matrix.rows(), 1);
3465 assert_eq!(matrix.cols(), 32);
3466 let raw = file.tensor_bytes("test.weight").unwrap();
3467 let expected = ferrox_quant::dequant_q5_1(raw).unwrap();
3468 match &matrix {
3469 WeightMatrix::Quantized { kind, data, .. } => {
3470 assert_eq!(*kind, QuantKind::Q5_1);
3471 assert!(data.is_mapped());
3472 }
3473 _ => panic!("expected a Quantized matrix for a Q5_1 tensor"),
3474 }
3475
3476 let x: Vec<f32> = (0..32).map(|i| ((i as f32) * 0.017).cos()).collect();
3477 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3478 let got = matrix.apply(&x);
3479 assert_eq!(got.len(), 1);
3480 assert!(
3481 (got[0] - expected_dot).abs() < 1e-2,
3482 "end-to-end loaded+applied Q5_1 matrix diverged from direct dequant: got={} expected={}",
3483 got[0],
3484 expected_dot
3485 );
3486 }
3487
3488 const Q3_K_TEST_BLOCK: [u8; 110] = [
3493 0x56, 0xf2, 0xb4, 0x2b, 0xd5, 0x6f, 0x51, 0x71, 0x3c, 0x0a, 0xb9, 0x1d, 0xd0, 0xb9, 0x3b,
3494 0xb3, 0x0f, 0xff, 0x8c, 0xb2, 0x83, 0x3a, 0x3d, 0x24, 0xb1, 0x12, 0x56, 0xe3, 0x23, 0x54,
3495 0xf2, 0xfa, 0x7f, 0xdf, 0x31, 0xe1, 0x18, 0x26, 0x6e, 0xcd, 0x5b, 0x38, 0xee, 0xbd, 0x9f,
3496 0x8c, 0x57, 0x47, 0x0b, 0x11, 0xcb, 0xfb, 0xb4, 0x83, 0xa0, 0x4e, 0x0b, 0xd4, 0xa7, 0x85,
3497 0xe0, 0x60, 0xf3, 0xb3, 0xe3, 0x95, 0x43, 0xc6, 0x05, 0x05, 0x77, 0x53, 0xed, 0x23, 0xcc,
3498 0x6a, 0x0e, 0x89, 0xa1, 0x79, 0x85, 0xf6, 0x6e, 0x5a, 0x23, 0x63, 0xbe, 0x53, 0xfa, 0xa2,
3499 0x2b, 0xe9, 0xcd, 0xce, 0xf8, 0x3d, 0x6f, 0xd0, 0x42, 0x6e, 0x3b, 0x7f, 0x23, 0x26, 0xd3,
3500 0xb9, 0x18, 0xbf, 0xa4, 0x34,
3501 ];
3502
3503 fn build_single_q3_k_tensor_gguf() -> Vec<u8> {
3504 let mut buf = Vec::new();
3505 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3506 .unwrap();
3507 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");
3512
3513 write_string(&mut buf, "test.weight");
3514 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 {
3522 buf.push(0);
3523 }
3524 buf.extend_from_slice(&Q3_K_TEST_BLOCK);
3525 buf
3526 }
3527
3528 #[test]
3529 fn load_weight_matrix_handles_a_real_on_disk_q3_k_tensor_end_to_end() {
3530 let tmp = std::env::temp_dir().join(format!(
3531 "ferrox_test_q3k_tensor_{}.gguf",
3532 std::process::id()
3533 ));
3534 std::fs::write(&tmp, build_single_q3_k_tensor_gguf()).unwrap();
3535 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q3_K GGUF file must parse");
3536 std::fs::remove_file(&tmp).ok();
3537
3538 let matrix = load_weight_matrix(&file, "test.weight").expect("Q3_K tensor must load");
3539 assert_eq!(matrix.rows(), 1);
3540 assert_eq!(matrix.cols(), 256);
3541 match &matrix {
3542 WeightMatrix::Quantized { kind, data, .. } => {
3543 assert_eq!(*kind, QuantKind::Q3K);
3544 assert!(data.is_mapped());
3545 }
3546 _ => panic!("expected a Quantized matrix for a Q3_K tensor"),
3547 }
3548
3549 let expected = ferrox_quant::dequant_q3_k(&Q3_K_TEST_BLOCK).unwrap();
3550 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3551 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3552
3553 let got = matrix.apply(&x);
3554 assert_eq!(got.len(), 1);
3555 assert!(
3556 (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-1),
3557 "end-to-end loaded+applied Q3_K matrix diverged from direct dequant: got={} expected={}",
3558 got[0],
3559 expected_dot
3560 );
3561 }
3562
3563 const IQ4_XS_TEST_BLOCK: [u8; 136] = [
3567 0x5c, 0x33, 0xb4, 0x39, 0xd1, 0x64, 0x97, 0x82, 0xcb, 0xbd, 0x88, 0x95, 0xf3, 0x60, 0x2a,
3568 0xb5, 0xe7, 0x24, 0xd3, 0xee, 0xfe, 0x71, 0x13, 0xbe, 0x70, 0x84, 0x48, 0x79, 0x7b, 0x3e,
3569 0xf0, 0x55, 0xdc, 0xb2, 0xb2, 0xde, 0x32, 0xa1, 0x5b, 0x02, 0x01, 0xdc, 0x2a, 0xbb, 0xf7,
3570 0x0b, 0x8a, 0x88, 0xdd, 0x0b, 0x02, 0x7e, 0x5e, 0x76, 0x87, 0x30, 0x1e, 0x1c, 0xcf, 0x48,
3571 0xd7, 0x61, 0xf3, 0x51, 0x52, 0x17, 0x98, 0x0a, 0x87, 0xcf, 0x02, 0x91, 0xc8, 0xee, 0xc0,
3572 0x91, 0x69, 0x2a, 0x4f, 0x64, 0x68, 0xa7, 0xb2, 0xe6, 0x98, 0x21, 0x81, 0x75, 0x53, 0x2a,
3573 0x8d, 0x12, 0xae, 0xe0, 0xea, 0x0c, 0x75, 0xff, 0x22, 0x5e, 0x25, 0x19, 0xda, 0x2e, 0x51,
3574 0x4e, 0x81, 0xdc, 0x0e, 0x78, 0x86, 0xd7, 0x58, 0xb5, 0xb7, 0xf6, 0x45, 0xa9, 0x0a, 0x83,
3575 0xfd, 0x2a, 0x12, 0x7d, 0xf0, 0x12, 0x97, 0xe2, 0xfe, 0xf4, 0xd0, 0xa2, 0x11, 0x14, 0x78,
3576 0xdb,
3577 ];
3578
3579 fn build_single_iq4_xs_tensor_gguf() -> Vec<u8> {
3580 let mut buf = Vec::new();
3581 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3582 .unwrap();
3583 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");
3588
3589 write_string(&mut buf, "test.weight");
3590 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 {
3598 buf.push(0);
3599 }
3600 buf.extend_from_slice(&IQ4_XS_TEST_BLOCK);
3601 buf
3602 }
3603
3604 #[test]
3605 fn load_weight_matrix_handles_a_real_on_disk_iq4_xs_tensor_end_to_end() {
3606 let tmp = std::env::temp_dir().join(format!(
3607 "ferrox_test_iq4xs_tensor_{}.gguf",
3608 std::process::id()
3609 ));
3610 std::fs::write(&tmp, build_single_iq4_xs_tensor_gguf()).unwrap();
3611 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real IQ4_XS GGUF file must parse");
3612 std::fs::remove_file(&tmp).ok();
3613
3614 let matrix = load_weight_matrix(&file, "test.weight").expect("IQ4_XS tensor must load");
3615 assert_eq!(matrix.rows(), 1);
3616 assert_eq!(matrix.cols(), 256);
3617 match &matrix {
3618 WeightMatrix::Quantized { kind, data, .. } => {
3619 assert_eq!(*kind, QuantKind::IQ4XS);
3620 assert!(data.is_mapped());
3621 }
3622 _ => panic!("expected a Quantized matrix for an IQ4_XS tensor"),
3623 }
3624
3625 let expected = ferrox_quant::dequant_iq4_xs(&IQ4_XS_TEST_BLOCK).unwrap();
3626 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3627 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3628
3629 let got = matrix.apply(&x);
3630 assert_eq!(got.len(), 1);
3631 assert!(
3632 (got[0] - expected_dot).abs() < 1e-1,
3633 "end-to-end loaded+applied IQ4_XS matrix diverged from direct dequant: got={} expected={}",
3634 got[0],
3635 expected_dot
3636 );
3637 }
3638
3639 const IQ1_S_TEST_BLOCK: [u8; 50] = [
3644 0x0a, 0x2f, 0xfa, 0x06, 0x1e, 0x37, 0x6f, 0xe3, 0x62, 0xd0, 0xb6, 0xa4, 0x25, 0xae, 0x76,
3645 0x14, 0x72, 0x5b, 0xfa, 0x05, 0xd1, 0xf1, 0x2a, 0x4c, 0xad, 0x29, 0xae, 0xf4, 0xcf, 0x0c,
3646 0x96, 0x51, 0x58, 0x03, 0x6d, 0xd3, 0x10, 0x92, 0x70, 0xff, 0x61, 0x58, 0xc8, 0x30, 0x25,
3647 0x64, 0x49, 0x85, 0xc0, 0x24,
3648 ];
3649 const IQ2_XXS_TEST_BLOCK: [u8; 66] = [
3650 0x29, 0x30, 0xd9, 0x33, 0x95, 0x4c, 0x08, 0x1e, 0xad, 0x79, 0x49, 0xf2, 0x8d, 0x5f, 0x93,
3651 0xea, 0x78, 0x18, 0x98, 0xb9, 0x94, 0x14, 0xad, 0xce, 0xca, 0x1d, 0xab, 0x81, 0x53, 0x4a,
3652 0x68, 0xd0, 0x59, 0x96, 0x36, 0x5d, 0xbe, 0x20, 0xc4, 0xff, 0xe4, 0x2c, 0xcd, 0x2f, 0x4f,
3653 0x4f, 0x67, 0x53, 0xc6, 0xd5, 0xa2, 0xfb, 0xc7, 0xf3, 0xe2, 0x6b, 0xf1, 0x99, 0x23, 0x1e,
3654 0x2d, 0x5e, 0x8c, 0x78, 0xc2, 0x31,
3655 ];
3656 const IQ3_XXS_TEST_BLOCK: [u8; 98] = [
3657 0x71, 0x31, 0x16, 0x0a, 0x79, 0x04, 0x5d, 0x87, 0xae, 0x2a, 0x4a, 0x43, 0xfd, 0x02, 0xba,
3658 0x6c, 0x10, 0x42, 0x80, 0xe5, 0x1d, 0x08, 0x22, 0xcb, 0x21, 0x54, 0xf9, 0xaa, 0x8e, 0xc2,
3659 0xf2, 0x34, 0x66, 0x1e, 0x2a, 0xef, 0x19, 0xae, 0x48, 0x47, 0x29, 0xa0, 0x72, 0xd1, 0x31,
3660 0xc0, 0x65, 0x49, 0xde, 0x79, 0x32, 0xe6, 0x4d, 0xb6, 0x55, 0x3f, 0x4d, 0xf1, 0x18, 0xbb,
3661 0x18, 0x59, 0x4c, 0x31, 0xa3, 0xb2, 0x34, 0xdd, 0xf6, 0x4a, 0x91, 0x51, 0x3f, 0x3e, 0x40,
3662 0x69, 0xad, 0xbf, 0x1a, 0xd0, 0x05, 0xfb, 0xbe, 0x8b, 0x0b, 0xdd, 0xdf, 0x7d, 0x94, 0x74,
3663 0x92, 0x3e, 0xff, 0x04, 0x2a, 0xc4, 0xea, 0xc9,
3664 ];
3665
3666 #[rustfmt::skip]
3667 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];
3668
3669 #[test]
3679 fn a_recognized_but_unimplemented_ggml_type_refuses_by_name_after_sizing_correctly() {
3680 let block = pseudo_iq_block(66, 0x0720_5eed);
3682 let tmp =
3683 std::env::temp_dir().join(format!("ferrox_test_tq2_0_{}.gguf", std::process::id()));
3684 std::fs::write(
3685 &tmp,
3686 build_single_iq_lowbit_tensor_gguf("tq2test", 35, 256, &block),
3687 )
3688 .unwrap();
3689 let file = ferrox_gguf::GgufFile::open(&tmp).expect("a TQ2_0 file must still parse");
3690 std::fs::remove_file(&tmp).ok();
3691
3692 let info = file.find_tensor("test.weight").expect("tensor present");
3695 assert_eq!(info.dtype, GgmlType::TQ2_0);
3696 assert_eq!(info.byte_len(), Some(66));
3697 assert_eq!(
3698 file.tensor_bytes("test.weight").map(<[u8]>::len).ok(),
3699 Some(66)
3700 );
3701
3702 match load_weight_matrix(&file, "test.weight") {
3703 Err(LoadError::UnsupportedDtype(name, GgmlType::TQ2_0)) => {
3704 assert_eq!(name, "test.weight");
3705 }
3706 Err(other) => panic!("TQ2_0 must be refused by name, got {other:?}"),
3707 Ok(_) => panic!("TQ2_0 must be refused, not loaded as some other kind"),
3708 }
3709 }
3710
3711 #[test]
3721 fn an_mxfp4_one_dimensional_tensor_widens_instead_of_being_refused() {
3722 let expected = ferrox_quant::dequant_mxfp4_gguf(&MXFP4_GGUF_TEST_BLOCKS)
3723 .expect("the fixture blocks must dequantize");
3724 let cols = expected.len();
3725 let tmp = std::env::temp_dir().join(format!(
3726 "ferrox_test_mxfp4_norm_{}.gguf",
3727 std::process::id()
3728 ));
3729 std::fs::write(
3730 &tmp,
3731 build_single_iq_lowbit_tensor_gguf(
3732 "mxfp4norm",
3733 39,
3734 cols as u64,
3735 &MXFP4_GGUF_TEST_BLOCKS,
3736 ),
3737 )
3738 .unwrap();
3739 let file = ferrox_gguf::GgufFile::open(&tmp).expect("file must parse");
3740 std::fs::remove_file(&tmp).ok();
3741
3742 let got = load_f32_vec(&file, "test.weight")
3743 .expect("an MXFP4 norm must load, not report an unsupported dtype");
3744 assert_eq!(got, expected);
3745
3746 let direct = widen_plain_float(GgmlType::MXFP4, &MXFP4_GGUF_TEST_BLOCKS, "test.weight")
3750 .expect("widen_plain_float must widen MXFP4");
3751 assert_eq!(direct, expected);
3752
3753 match widen_plain_float(GgmlType::TQ2_0, &MXFP4_GGUF_TEST_BLOCKS, "test.weight") {
3757 Err(LoadError::UnsupportedDtype(name, GgmlType::TQ2_0)) => {
3758 assert_eq!(name, "test.weight");
3759 }
3760 other => panic!("TQ2_0 must be refused by name, got {other:?}"),
3761 }
3762 }
3763
3764 fn build_single_iq_lowbit_tensor_gguf(
3765 arch: &str,
3766 tag: u32,
3767 cols: u64,
3768 block: &[u8],
3769 ) -> Vec<u8> {
3770 let mut buf = Vec::new();
3771 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3772 .unwrap();
3773 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);
3777 write_string(&mut buf, "test.weight");
3778 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(cols).unwrap();
3780 buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u32::<LittleEndian>(tag).unwrap();
3782 buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
3784 buf.push(0);
3785 }
3786 buf.extend_from_slice(block);
3787 buf
3788 }
3789
3790 fn pseudo_iq_block(len: usize, seed: u32) -> Vec<u8> {
3797 let mut s = seed;
3798 let mut out = Vec::with_capacity(len);
3799 for _ in 0..len {
3800 s ^= s << 13;
3801 s ^= s >> 17;
3802 s ^= s << 5;
3803 out.push((s >> 24) as u8);
3804 }
3805 out
3806 }
3807
3808 #[test]
3819 fn load_weight_matrix_handles_real_on_disk_iq_lowbit_tensors_end_to_end() {
3820 type DequantFn = fn(&[u8]) -> Result<Vec<f32>, ferrox_quant::QuantError>;
3821 let mut iq1m = pseudo_iq_block(ferrox_quant::IQ1_M_BLOCK_BYTES, 0x2907_31A0);
3828 iq1m[55] = (iq1m[55] & 0x0F) | 0x20;
3829 let mut iq2xs = pseudo_iq_block(ferrox_quant::IQ2_XS_BLOCK_BYTES, 0x2107_31A1);
3830 let mut iq2s = pseudo_iq_block(ferrox_quant::IQ2_S_BLOCK_BYTES, 0x2207_31A2);
3831 let mut iq3s = pseudo_iq_block(ferrox_quant::IQ3_S_BLOCK_BYTES, 0x2307_31A3);
3832 for blk in [&mut iq2xs, &mut iq2s, &mut iq3s] {
3833 blk[0..2].copy_from_slice(&half::f16::from_f32(0.115).to_le_bytes());
3834 }
3835 let cases: [(&str, u32, &[u8], QuantKind, DequantFn); 8] = [
3836 (
3837 "iq1s",
3838 19,
3839 &IQ1_S_TEST_BLOCK,
3840 QuantKind::IQ1S,
3841 ferrox_quant::dequant_iq1_s,
3842 ),
3843 (
3844 "iq1m",
3845 29,
3846 &iq1m,
3847 QuantKind::IQ1M,
3848 ferrox_quant::dequant_iq1_m,
3849 ),
3850 (
3851 "iq2xxs",
3852 16,
3853 &IQ2_XXS_TEST_BLOCK,
3854 QuantKind::IQ2XXS,
3855 ferrox_quant::dequant_iq2_xxs,
3856 ),
3857 (
3858 "iq2xs",
3859 17,
3860 &iq2xs,
3861 QuantKind::IQ2XS,
3862 ferrox_quant::dequant_iq2_xs,
3863 ),
3864 (
3865 "iq2s",
3866 22,
3867 &iq2s,
3868 QuantKind::IQ2S,
3869 ferrox_quant::dequant_iq2_s,
3870 ),
3871 (
3872 "iq3xxs",
3873 18,
3874 &IQ3_XXS_TEST_BLOCK,
3875 QuantKind::IQ3XXS,
3876 ferrox_quant::dequant_iq3_xxs,
3877 ),
3878 (
3879 "iq3s",
3880 21,
3881 &iq3s,
3882 QuantKind::IQ3S,
3883 ferrox_quant::dequant_iq3_s,
3884 ),
3885 (
3886 "mxfp4_gguf",
3887 39,
3888 &MXFP4_GGUF_TEST_BLOCKS,
3889 QuantKind::Mxfp4Gguf,
3890 ferrox_quant::dequant_mxfp4_gguf,
3891 ),
3892 ];
3893 for (name, tag, block, kind, dequant) in cases {
3894 let expected = dequant(block).unwrap();
3895 let cols = expected.len();
3896 let tmp = std::env::temp_dir().join(format!("ferrox_test_{name}_tensor.gguf"));
3897 std::fs::write(
3898 &tmp,
3899 build_single_iq_lowbit_tensor_gguf(name, tag, cols as u64, block),
3900 )
3901 .unwrap();
3902 let file = ferrox_gguf::GgufFile::open(&tmp).expect("file must parse");
3903 std::fs::remove_file(&tmp).ok();
3904
3905 let matrix =
3906 load_weight_matrix(&file, "test.weight").expect("low-bit tensor must load");
3907 assert_eq!((matrix.rows(), matrix.cols()), (1, cols), "{name}");
3908 match &matrix {
3909 WeightMatrix::Quantized { kind: k, data, .. } => {
3910 assert_eq!(*k, kind, "{name}");
3911 assert!(data.is_mapped(), "{name} must load zero-copy");
3912 }
3913 _ => panic!("expected a Quantized matrix for {name}"),
3914 }
3915
3916 let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.013).sin()).collect();
3917 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3918 let got = matrix.apply(&x);
3919 assert!(
3920 (got[0] - expected_dot).abs() < 1e-1,
3921 "{name}: loaded+applied diverged from direct dequant: got={} expected={}",
3922 got[0],
3923 expected_dot
3924 );
3925 }
3926 }
3927
3928 #[test]
3929 fn qwen2moe_disables_topk_renorm() {
3930 assert!(
3931 NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"qwen2moe"),
3932 "qwen2moe must have norm_topk_prob=false (llama.cpp build_moe_ffn norm_w=false)"
3933 );
3934 }
3935
3936 #[test]
3947 fn an_architecture_with_no_rope_is_refused_by_name() {
3948 for arch in ["gpt2", "mpt", "refact", "bloom", "jais"] {
3949 let file = open_metadata_gguf(
3950 &format!("norope_{arch}"),
3951 &[("general.architecture", Kv::Str(arch))],
3952 );
3953 match ModelConfig::from_gguf(&file) {
3954 Err(LoadError::DedicatedArchitectureRequired(got, reason)) => {
3955 assert_eq!(got, arch);
3956 assert!(
3957 reason.contains("ALiBi") || reason.contains("position embeddings"),
3958 "{arch}: the refusal must name what is missing, got {reason:?}"
3959 );
3960 }
3961 other => panic!("{arch} must be refused, got {other:?}"),
3962 }
3963 }
3964 }
3965
3966 #[test]
3972 fn baichuan_13b_is_refused_because_it_uses_alibi_and_the_7b_is_not() {
3973 let thirteen_b = open_metadata_gguf(
3974 "baichuan13b",
3975 &[
3976 ("general.architecture", Kv::Str("baichuan")),
3977 ("baichuan.block_count", Kv::U32(40)),
3978 ],
3979 );
3980 match ModelConfig::from_gguf(&thirteen_b) {
3981 Err(LoadError::UnsupportedFeature(arch, msg)) => {
3982 assert_eq!(arch, "baichuan");
3983 assert!(msg.contains("ALiBi"), "{msg}");
3984 assert!(
3985 msg.contains("40"),
3986 "the refusal must name the layer count: {msg}"
3987 );
3988 }
3989 other => panic!("Baichuan-13B must be refused, got {other:?}"),
3990 }
3991
3992 let seven_b = open_metadata_gguf(
3996 "baichuan7b",
3997 &[
3998 ("general.architecture", Kv::Str("baichuan")),
3999 ("baichuan.block_count", Kv::U32(32)),
4000 ],
4001 );
4002 match ModelConfig::from_gguf(&seven_b) {
4003 Err(LoadError::MissingHparam(key)) => assert_eq!(key, "baichuan.embedding_length"),
4004 other => panic!("Baichuan-7B must pass the ALiBi gate, got {other:?}"),
4005 }
4006 }
4007}