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
116const NO_TOPK_RENORMALIZE_ARCHITECTURES: &[&str] = &["deepseek", "olmoe", "qwen2moe"];
143
144fn metadata_u64_any(file: &impl TensorSource, keys: &[String]) -> Option<u64> {
145 keys.iter().find_map(|k| file.metadata_u64(k))
146}
147
148fn metadata_f32_any(file: &impl TensorSource, keys: &[String]) -> Option<f32> {
149 keys.iter()
150 .find_map(|k| file.metadata(k).and_then(GgufValue::as_f32))
151}
152
153impl ModelConfig {
154 pub fn from_gguf(file: &impl TensorSource) -> Result<Self, LoadError> {
168 let arch = file
169 .metadata_str("general.architecture")
170 .ok_or_else(|| LoadError::MissingHparam("general.architecture".to_string()))?
171 .to_string();
172 let arch_profile = crate::capability::resolve_profile(&arch)
173 .ok_or_else(|| LoadError::UnsupportedArchitecture(arch.clone()))?;
174 let rope_layout = match arch_profile.path {
175 crate::capability::ArchPath::GenericGqa { rope }
176 | crate::capability::ArchPath::TestFixture { rope } => rope,
177 crate::capability::ArchPath::DedicatedOnly { reason } => {
178 return Err(LoadError::DedicatedArchitectureRequired(
179 arch.clone(),
180 reason,
181 ));
182 }
183 crate::capability::ArchPath::Deferred { reason } => {
184 return Err(LoadError::UnsupportedFeature(
185 arch.clone(),
186 format!("architecture deferred from Ferrox text-generation scope: {reason}"),
187 ));
188 }
189 };
190 let qk_norm_style = arch_profile.qk_norm;
191 for (meta_key, feature) in crate::capability::unsupported_feature_keys(&arch) {
192 if let Some(v) = metadata_f32_any(file, std::slice::from_ref(&meta_key)) {
193 if v > 0.0 {
194 return Err(LoadError::UnsupportedFeature(
195 arch.clone(),
196 format!("{feature} (metadata {meta_key}={v})"),
197 ));
198 }
199 }
200 if let Some(v) = metadata_u64_any(file, std::slice::from_ref(&meta_key)) {
201 if v > 0 {
202 return Err(LoadError::UnsupportedFeature(
203 arch.clone(),
204 feature.to_string(),
205 ));
206 }
207 }
208 }
209 for (meta_key, feature, no_op) in crate::capability::unsupported_scaling_keys(&arch) {
215 if let Some(v) = metadata_f32_any(file, std::slice::from_ref(&meta_key)) {
216 if (v - no_op).abs() > 1e-6 {
217 return Err(LoadError::UnsupportedFeature(
218 arch.clone(),
219 format!("{feature} (metadata {meta_key}={v})"),
220 ));
221 }
222 }
223 }
224 let key = |suffix: &str| format!("{arch}.{suffix}");
225
226 let name: &'static str = Box::leak(
227 file.metadata_str("general.name")
228 .unwrap_or(&arch)
229 .to_string()
230 .into_boxed_str(),
231 );
232
233 let n_layers =
234 file.metadata_u64(&key("block_count"))
235 .ok_or_else(|| LoadError::MissingHparam(key("block_count")))? as usize;
236 if arch == "baichuan" && n_layers == 40 {
247 return Err(LoadError::UnsupportedFeature(
248 arch.clone(),
249 "Baichuan-13B (block_count=40) uses ALiBi and no RoPE, decided by layer \
250 count with no GGUF key to declare it; the generic decoder would rotate \
251 every Q/K head instead. Baichuan-7B (block_count=32) is unaffected"
252 .to_string(),
253 ));
254 }
255 let hidden_dim = file
256 .metadata_u64(&key("embedding_length"))
257 .ok_or_else(|| LoadError::MissingHparam(key("embedding_length")))?
258 as usize;
259 let n_heads = file
260 .metadata_u64(&key("attention.head_count"))
261 .ok_or_else(|| LoadError::MissingHparam(key("attention.head_count")))?
262 as usize;
263
264 let mut best_effort_fields: Vec<&'static str> = Vec::new();
265
266 let n_kv_heads = file
267 .metadata_u64(&key("attention.head_count_kv"))
268 .map(|v| v as usize)
269 .unwrap_or_else(|| {
270 best_effort_fields.push("n_kv_heads (no attention.head_count_kv key; assumed equal to n_heads, i.e. plain MHA)");
271 n_heads
272 });
273 let head_dim = file
274 .metadata_u64(&key("attention.key_length"))
275 .map(|v| v as usize)
276 .unwrap_or_else(|| {
277 best_effort_fields.push(
278 "head_dim (no attention.key_length key; derived as hidden_dim / n_heads)",
279 );
280 hidden_dim / n_heads
281 });
282 let v_head_dim = file
283 .metadata_u64(&key("attention.value_length"))
284 .map(|v| v as usize)
285 .unwrap_or(head_dim);
286 if v_head_dim != head_dim {
287 return Err(LoadError::UnsupportedFeature(
288 arch.clone(),
289 format!(
290 "split K/V head dims (key_length={head_dim}, value_length={v_head_dim}); \
291 generic decoder requires equal head dims"
292 ),
293 ));
294 }
295 let vocab_size = file
296 .metadata("tokenizer.ggml.tokens")
297 .and_then(|v| match v {
298 GgufValue::Array(items) => Some(items.len()),
299 _ => None,
300 })
301 .or_else(|| file.metadata_u64(&key("vocab_size")).map(|v| v as usize))
302 .unwrap_or_else(|| {
303 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)");
304 file.find_tensor("output.weight")
309 .and_then(|t| t.shape.last().copied())
310 .unwrap_or(0) as usize
311 });
312 let rope_theta = metadata_f32_any(file, &[key("rope.freq_base")]).unwrap_or_else(|| {
313 best_effort_fields.push("rope_theta (no rope.freq_base key; defaulted to 10000.0)");
314 10000.0
315 });
316 let rms_norm_eps = metadata_f32_any(
317 file,
318 &[
319 key("attention.layer_norm_rms_epsilon"),
320 key("attention.layer_norm_epsilon"),
321 ],
322 )
323 .unwrap_or_else(|| {
324 best_effort_fields
325 .push("rms_norm_eps (no layer_norm_rms_epsilon key; defaulted to 1e-5)");
326 1e-5
327 });
328
329 let n_experts = metadata_u64_any(file, &[key("expert_count")]).unwrap_or(0) as usize;
330 let is_moe = n_experts > 1;
331
332 let n_experts_active = if is_moe {
333 metadata_u64_any(file, &[key("expert_used_count")]).unwrap_or_else(|| {
334 best_effort_fields
335 .push("moe.n_experts_active (no expert_used_count key; defaulted to 2)");
336 2
337 }) as usize
338 } else {
339 1
340 };
341 let n_shared_experts = match metadata_u64_any(file, &[key("expert_shared_count")]) {
347 Some(n) => n as usize,
348 None if is_moe && file.find_tensor("blk.0.ffn_gate_shexp.weight").is_some() => {
349 best_effort_fields.push(
350 "moe.n_shared_experts (no expert_shared_count; inferred 1 from blk.0.ffn_gate_shexp.weight)",
351 );
352 1
353 }
354 None => 0,
355 };
356 let feed_forward_length = metadata_u64_any(file, &[key("feed_forward_length")]);
362 let expert_ffn_dim = metadata_u64_any(file, &[key("expert_feed_forward_length")])
363 .or_else(|| {
364 feed_forward_length.map(|ff| {
365 if is_moe && n_experts_active > 0 {
366 ff / n_experts_active as u64
367 } else {
368 ff
369 }
370 })
371 })
372 .unwrap_or_else(|| {
373 best_effort_fields.push(
374 "moe.expert_ffn_dim (no expert_feed_forward_length/feed_forward_length; defaulted to 4x hidden_dim)",
375 );
376 (hidden_dim * 4) as u64
377 }) as usize;
378 let n_dense_leading_layers =
379 metadata_u64_any(file, &[key("leading_dense_block_count")]).unwrap_or(0) as usize;
380
381 let gating = match metadata_u64_any(file, &[key("expert_gating_func")]) {
387 Some(2) => GatingFunction::Sigmoid,
388 Some(1) => GatingFunction::Softmax,
389 _ => {
390 if SIGMOID_GATING_ARCHITECTURES.contains(&arch.as_str()) {
391 GatingFunction::Sigmoid
392 } else {
393 if is_moe {
394 best_effort_fields.push(
395 "moe.gating (no expert_gating_func key and architecture not in the known-sigmoid list; defaulted to softmax)",
396 );
397 }
398 GatingFunction::Softmax
399 }
400 }
401 };
402
403 let norm_topk_prob = match file.metadata_bool(&key("expert_weights_norm")) {
410 Some(v) => v,
411 None => {
412 if is_moe && matches!(gating, GatingFunction::Softmax) {
416 best_effort_fields.push(
417 "moe.norm_topk_prob (no expert_weights_norm key; defaulted by architecture-name lookup against NO_TOPK_RENORMALIZE_ARCHITECTURES)",
418 );
419 }
420 !NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&arch.as_str())
421 }
422 };
423
424 let expert_weights_scale = metadata_f32_any(file, &[key("expert_weights_scale")])
428 .filter(|s| *s != 0.0)
429 .unwrap_or(1.0);
430
431 let sliding_window = metadata_u64_any(file, &[key("attention.sliding_window")])
441 .map(|v| v as usize)
442 .filter(|&w| w > 0)
443 .filter(|_| !crate::capability::swa_disabled_by_arch(&arch));
449
450 let swa_layout = crate::capability::default_swa_layout(&arch);
459 let swa_dense_first = swa_layout.is_some_and(|p| p.dense_first);
460 let swa_pattern = metadata_u64_any(file, &[key("attention.sliding_window_pattern")])
461 .map(|v| v as usize)
462 .or_else(|| {
463 sliding_window?;
464 swa_layout.map(|p| p.period).or(
469 match arch_profile.family {
472 crate::capability::DecoderFamily::GemmaFamily => Some(6),
473 _ => None,
474 },
475 )
476 });
477
478 let attn_logit_softcap = metadata_f32_any(
479 file,
480 &[
481 key("attention.logit_softcapping"),
482 key("attn_logit_softcapping"),
483 ],
484 )
485 .filter(|&v| v > 0.0);
486 let final_logit_softcap =
487 metadata_f32_any(file, &[key("final_logit_softcapping")]).filter(|&v| v > 0.0);
488
489 let embedding_scale = if matches!(
491 arch_profile.family,
492 crate::capability::DecoderFamily::GemmaFamily
493 ) {
494 Some((hidden_dim as f32).sqrt())
495 } else {
496 None
497 };
498
499 let attention_scale = None;
504
505 let rope_theta_swa = if sliding_window.is_some() {
510 let fallback = if crate::capability::swa_rope_base_follows_model(&arch) {
511 rope_theta
512 } else {
513 10_000.0
514 };
515 Some(
516 metadata_f32_any(
517 file,
518 &[key("rope.freq_base_swa"), key("rope_freq_base_swa")],
519 )
520 .unwrap_or(fallback),
521 )
522 } else {
523 None
524 };
525
526 let ffn_activation = match arch_profile.family {
527 _ if crate::capability::uses_geglu(&arch) => crate::config::FfnActivation::Gelu,
531 crate::capability::DecoderFamily::GemmaFamily => crate::config::FfnActivation::Gelu,
532 crate::capability::DecoderFamily::PhiFamily => {
533 crate::config::FfnActivation::SwigluFused
534 }
535 _ => crate::config::FfnActivation::Swiglu,
536 };
537
538 let rope_freqs = load_f32_vec_optional(file, "rope_freqs.weight")?;
545
546 let rope_orig_ctx = metadata_u64_any(file, &[key("rope.scaling.original_context_length")])
559 .map(|v| v as usize);
560 let (rope_freqs_long, rope_freqs_short) = if rope_freqs.is_some() {
565 (None, None)
566 } else {
567 (
568 load_f32_vec_optional(file, "rope_factors_long.weight")?,
569 load_f32_vec_optional(file, "rope_factors_short.weight")?,
570 )
571 };
572 let rope_freqs = match (rope_freqs, rope_orig_ctx) {
576 (Some(f), _) => Some(f),
577 (None, Some(orig)) => {
578 let model_ctx = metadata_u64_any(file, &[key("context_length")])
579 .unwrap_or(orig as u64) as usize;
580 if model_ctx > orig {
581 rope_freqs_long.clone().or_else(|| rope_freqs_short.clone())
582 } else {
583 rope_freqs_short.clone().or_else(|| rope_freqs_long.clone())
584 }
585 }
586 (None, None) => None,
587 };
588
589 let rope_dim = metadata_u64_any(file, &[key("rope.dimension_count")])
594 .map(|d| d as usize)
595 .filter(|d| *d > 0 && *d < head_dim);
596
597 let rope_attn_factor = metadata_f32_any(file, &[key("rope.scaling.attn_factor")])
599 .filter(|f| f.is_finite() && *f > 0.0)
600 .unwrap_or(1.0);
601
602 let rope_freqs = match linear_scaling_from_gguf(file, &arch) {
635 None => rope_freqs,
636 Some(factor) => {
637 let rotary_dim = rope_dim.unwrap_or(head_dim);
638 if rotary_dim == 0 || !rotary_dim.is_multiple_of(2) {
639 best_effort_fields.push(
640 "rope_freqs (linear scaling declared but the rotary width is odd; \
641 scaling not applied)",
642 );
643 rope_freqs
644 } else {
645 let linear = vec![factor; rotary_dim / 2];
646 match rope_freqs {
647 None => Some(linear),
648 Some(own) if own.len() == linear.len() => {
652 Some(own.iter().zip(linear.iter()).map(|(a, b)| a * b).collect())
653 }
654 Some(own) => {
655 best_effort_fields.push(
656 "rope_freqs (linear scaling declared but the file's own \
657 rope_freqs tensor has a different width; scaling not applied)",
658 );
659 Some(own)
660 }
661 }
662 }
663 }
664 };
665 let rope_freqs = match yarn_scaling_from_gguf(file, &arch, rope_orig_ctx) {
666 None => rope_freqs,
667 Some(scaling) => {
668 let rotary_dim = rope_dim.unwrap_or(head_dim);
669 if rotary_dim == 0 || !rotary_dim.is_multiple_of(2) {
670 best_effort_fields.push(
671 "rope_freqs (YaRN declared but the rotary width is odd; scaling not applied)",
672 );
673 rope_freqs
674 } else {
675 let yarn =
676 ferrox_core::attention::yarn_freq_factors(scaling, rotary_dim, rope_theta);
677 match rope_freqs {
678 None => Some(yarn),
679 Some(own) if own.len() == yarn.len() => {
680 Some(own.iter().zip(yarn.iter()).map(|(a, b)| a * b).collect())
681 }
682 Some(own) => {
683 best_effort_fields.push(
684 "rope_freqs (YaRN declared alongside a per-band factor tensor of a \
685 different width; the file's own tensor is used unscaled)",
686 );
687 Some(own)
688 }
689 }
690 }
691 }
692 };
693
694 if best_effort_fields.is_empty() {
699 best_effort_fields.push(
700 "none -- every field above was read directly from this file's own GGUF metadata",
701 );
702 }
703
704 if matches!(
715 arch_profile.path,
716 crate::capability::ArchPath::GenericGqa { .. }
717 ) && !crate::capability::is_audited_generic(&arch)
718 && !matches!(
719 std::env::var("FERROX_ALLOW_UNAUDITED_ARCH").ok().as_deref(),
720 Some("1") | Some("true") | Some("on")
721 )
722 {
723 return Err(LoadError::UnauditedArchitecture(
724 arch.clone(),
725 rope_layout,
726 crate::capability::unaudited_refusal_detail(&arch),
727 ));
728 }
729
730 Ok(ModelConfig {
731 name,
732 n_layers,
733 hidden_dim,
734 n_heads,
735 n_kv_heads,
736 head_dim,
737 vocab_size,
738 rope_theta,
739 rms_norm_eps,
740 attention: crate::config::AttentionKind::Gqa,
744 sliding_window,
745 swa_pattern,
746 swa_dense_first,
747 moe: MoeLayerConfig {
748 n_experts: n_experts.max(1),
749 n_experts_active,
750 n_shared_experts,
751 hidden_dim,
752 expert_ffn_dim,
753 gating,
754 norm_topk_prob,
755 expert_group_count: metadata_u64_any(file, &[key("expert_group_count")])
756 .map(|v| v as usize)
757 .filter(|&c| c > 1),
758 expert_group_used_count: metadata_u64_any(file, &[key("expert_group_used_count")])
759 .map(|v| v as usize)
760 .filter(|&c| c > 0),
761 expert_weights_scale,
762 },
763 n_dense_leading_layers,
764 rope_freqs,
765 rope_layout,
766 qk_norm_style,
767 attn_logit_softcap,
768 final_logit_softcap,
769 embedding_scale,
770 attention_scale,
771 rope_attn_factor,
772 rope_dim,
773 rope_freqs_long,
774 rope_freqs_short,
775 rope_orig_ctx,
776 rope_theta_swa,
777 ffn_activation,
778 best_effort_fields: Box::leak(best_effort_fields.into_boxed_slice()),
779 })
780 }
781}
782
783impl crate::sampling::RecommendedSampling {
784 pub fn from_gguf(file: &impl TensorSource) -> Self {
806 let number = |k: &str| -> Option<f32> {
807 file.metadata(k)
808 .and_then(|v| v.as_f32().or_else(|| v.as_u64().map(|u| u as f32)))
809 };
810 crate::sampling::RecommendedSampling {
811 temperature: number("general.sampling.temp"),
812 top_p: number("general.sampling.top_p"),
813 top_k: file
814 .metadata("general.sampling.top_k")
815 .and_then(|v| v.as_u64())
816 .map(|v| v as usize),
817 }
818 }
819}
820
821fn linear_scaling_from_gguf(file: &impl TensorSource, arch: &str) -> Option<f32> {
828 let key = |suffix: &str| format!("{arch}.{suffix}");
829 let scaling_type = file.metadata_str(&key("rope.scaling.type"))?;
830 if !scaling_type.eq_ignore_ascii_case("linear") {
831 return None;
832 }
833 metadata_f32_any(file, &[key("rope.scaling.factor")]).filter(|f| f.is_finite() && *f > 1.0)
834}
835
836fn yarn_scaling_from_gguf(
866 file: &impl TensorSource,
867 arch: &str,
868 orig_ctx: Option<usize>,
869) -> Option<ferrox_core::attention::YarnScaling> {
870 let key = |suffix: &str| format!("{arch}.{suffix}");
871 let scaling_type = file.metadata_str(&key("rope.scaling.type"))?;
872 if !scaling_type.eq_ignore_ascii_case("yarn") {
873 return None;
874 }
875 let factor = metadata_f32_any(file, &[key("rope.scaling.factor")])
876 .filter(|f| f.is_finite() && *f > 1.0)?;
877 let orig_max_pos = orig_ctx?;
878 let beta = |suffix: &str, default: f32| -> f32 {
879 metadata_f32_any(
880 file,
881 &[
882 key(&format!("rope.scaling.{suffix}")),
883 key(&format!("rope.scaling.yarn_{suffix}")),
884 ],
885 )
886 .filter(|v| v.is_finite() && *v > 0.0)
887 .unwrap_or(default)
888 };
889 Some(ferrox_core::attention::YarnScaling {
890 factor,
891 beta_fast: beta("beta_fast", 32.0),
892 beta_slow: beta("beta_slow", 1.0),
893 orig_max_pos,
894 truncate: true,
898 })
899}
900
901pub(crate) fn find_info<'a>(
902 file: &'a impl TensorSource,
903 name: &str,
904) -> Result<&'a TensorInfo, LoadError> {
905 file.find_tensor(name)
906 .ok_or_else(|| LoadError::Gguf(GgufError::TensorNotFound(name.to_string())))
907}
908
909fn load_gpt_oss_layer(
935 file: &impl TensorSource,
936 l: usize,
937 config: &ModelConfig,
938) -> Result<crate::decoder::GptOssLayer, LoadError> {
939 let n_experts = config.moe.n_experts;
940 let ff = config.moe.expert_ffn_dim;
941
942 let want = |name: &str, got: usize, expect: usize| -> Result<(), LoadError> {
943 if got == expect {
944 Ok(())
945 } else {
946 Err(LoadError::UnsupportedFeature(
947 config.name.to_string(),
948 format!("{name} has {got} elements, expected {expect}"),
949 ))
950 }
951 };
952
953 let attn_sinks = load_f32_vec(file, &format!("blk.{l}.attn_sinks.weight"))?;
954 want(
955 &format!("blk.{l}.attn_sinks.weight"),
956 attn_sinks.len(),
957 config.n_heads,
958 )?;
959 let o_bias = load_f32_vec(file, &format!("blk.{l}.attn_output.bias"))?;
960 want(
961 &format!("blk.{l}.attn_output.bias"),
962 o_bias.len(),
963 config.hidden_dim,
964 )?;
965 let router_bias = load_f32_vec(file, &format!("blk.{l}.ffn_gate_inp.bias"))?;
966 want(
967 &format!("blk.{l}.ffn_gate_inp.bias"),
968 router_bias.len(),
969 n_experts,
970 )?;
971
972 let gate_b = load_f32_vec(file, &format!("blk.{l}.ffn_gate_exps.bias"))?;
973 want(
974 &format!("blk.{l}.ffn_gate_exps.bias"),
975 gate_b.len(),
976 n_experts * ff,
977 )?;
978 let up_b = load_f32_vec(file, &format!("blk.{l}.ffn_up_exps.bias"))?;
979 want(
980 &format!("blk.{l}.ffn_up_exps.bias"),
981 up_b.len(),
982 n_experts * ff,
983 )?;
984 let down_b = load_f32_vec(file, &format!("blk.{l}.ffn_down_exps.bias"))?;
985 want(
986 &format!("blk.{l}.ffn_down_exps.bias"),
987 down_b.len(),
988 n_experts * config.hidden_dim,
989 )?;
990
991 let expert_bias = (0..n_experts)
992 .map(|e| ferrox_moe::ExpertBias {
993 gate: gate_b[e * ff..(e + 1) * ff].to_vec(),
994 up: up_b[e * ff..(e + 1) * ff].to_vec(),
995 down: down_b[e * config.hidden_dim..(e + 1) * config.hidden_dim].to_vec(),
996 })
997 .collect();
998
999 Ok(crate::decoder::GptOssLayer {
1000 attn_sinks,
1001 o_bias,
1002 router_bias,
1003 expert_bias,
1004 })
1005}
1006
1007pub(crate) fn load_f32_vec_optional(
1008 file: &impl TensorSource,
1009 name: &str,
1010) -> Result<Option<Vec<f32>>, LoadError> {
1011 if file.find_tensor(name).is_none() {
1012 return Ok(None);
1013 }
1014 Ok(Some(load_f32_vec(file, name)?))
1015}
1016
1017fn slice_quantized_rows(m: &WeightMatrix, start: usize, n: usize) -> Option<WeightMatrix> {
1024 let WeightMatrix::Quantized {
1025 data,
1026 rows,
1027 cols,
1028 kind,
1029 } = m
1030 else {
1031 return None;
1032 };
1033 let total = data.len();
1034 if *rows == 0 || total % *rows != 0 || start + n > *rows {
1035 return None;
1036 }
1037 let row_bytes = total / *rows;
1038 let (b0, b1) = (start * row_bytes, (start + n) * row_bytes);
1039 let bytes = match data {
1040 WeightBytes::Mapped { mmap, range } => WeightBytes::Mapped {
1041 mmap: mmap.clone(),
1042 range: range.start + b0..range.start + b1,
1043 },
1044 other => WeightBytes::Owned(other.as_slice()[b0..b1].to_vec()),
1045 };
1046 Some(WeightMatrix::Quantized {
1047 data: bytes,
1048 rows: n,
1049 cols: *cols,
1050 kind: *kind,
1051 })
1052}
1053
1054fn load_qkv_projections(
1059 file: &impl TensorSource,
1060 layer: usize,
1061 config: &ModelConfig,
1062) -> Result<(WeightMatrix, WeightMatrix, WeightMatrix), LoadError> {
1063 let q_name = format!("blk.{layer}.attn_q.weight");
1064 let k_name = format!("blk.{layer}.attn_k.weight");
1065 let v_name = format!("blk.{layer}.attn_v.weight");
1066 let fused_name = format!("blk.{layer}.attn_qkv.weight");
1067
1068 if file.find_tensor(&q_name).is_some() {
1069 return Ok((
1070 load_weight_matrix(file, &q_name)?,
1071 load_weight_matrix(file, &k_name)?,
1072 load_weight_matrix(file, &v_name)?,
1073 ));
1074 }
1075 if file.find_tensor(&fused_name).is_none() {
1076 return Err(LoadError::Gguf(GgufError::TensorNotFound(q_name)));
1077 }
1078
1079 let fused = load_weight_matrix(file, &fused_name)?;
1080 let q_rows = config.n_heads * config.head_dim;
1081 let kv_rows = config.n_kv_heads * config.head_dim;
1082 let expected = q_rows + 2 * kv_rows;
1083 if fused.rows() != expected {
1084 return Err(LoadError::UnsupportedFeature(
1086 config.name.to_string(),
1087 format!(
1088 "{fused_name} has {} rows; expected q+k+v = {} \
1089 (n_heads*head_dim + 2*n_kv_heads*head_dim)",
1090 fused.rows(),
1091 expected
1092 ),
1093 ));
1094 }
1095 let cols = fused.cols();
1096 if let (Some(q), Some(k), Some(v)) = (
1099 slice_quantized_rows(&fused, 0, q_rows),
1100 slice_quantized_rows(&fused, q_rows, kv_rows),
1101 slice_quantized_rows(&fused, q_rows + kv_rows, kv_rows),
1102 ) {
1103 return Ok((q, k, v));
1104 }
1105 let mut full = Vec::with_capacity(fused.rows() * cols);
1107 for r in 0..fused.rows() {
1108 full.extend_from_slice(&fused.dequant_row(r));
1109 }
1110 let q = WeightMatrix::F32(Tensor::new(
1111 full[..q_rows * cols].to_vec(),
1112 vec![q_rows, cols],
1113 ));
1114 let k = WeightMatrix::F32(Tensor::new(
1115 full[q_rows * cols..(q_rows + kv_rows) * cols].to_vec(),
1116 vec![kv_rows, cols],
1117 ));
1118 let v = WeightMatrix::F32(Tensor::new(
1119 full[(q_rows + kv_rows) * cols..].to_vec(),
1120 vec![kv_rows, cols],
1121 ));
1122 Ok((q, k, v))
1123}
1124
1125fn load_dense_expert(
1128 file: &impl TensorSource,
1129 layer: usize,
1130 config: &ModelConfig,
1131) -> Result<ExpertWeights, LoadError> {
1132 let gate_name = format!("blk.{layer}.ffn_gate.weight");
1133 let up_name = format!("blk.{layer}.ffn_up.weight");
1134 let down_name = format!("blk.{layer}.ffn_down.weight");
1135 if file.find_tensor(&gate_name).is_some() {
1136 return Ok(ExpertWeights {
1137 gate: load_weight_matrix(file, &gate_name)?,
1138 up: load_weight_matrix(file, &up_name)?,
1139 down: load_weight_matrix(file, &down_name)?,
1140 });
1141 }
1142 let fused = load_weight_matrix(file, &up_name)?;
1144 let ff = config.moe.expert_ffn_dim;
1145 if fused.rows() != 2 * ff {
1146 return Err(LoadError::UnsupportedFeature(
1147 config.name.to_string(),
1148 format!(
1149 "{up_name} has {} rows without a companion ffn_gate; \
1150 expected fused SwiGLU with 2*ffn_dim = {} rows",
1151 fused.rows(),
1152 2 * ff
1153 ),
1154 ));
1155 }
1156 let cols = fused.cols();
1157 if let (Some(gate), Some(up)) = (
1159 slice_quantized_rows(&fused, 0, ff),
1160 slice_quantized_rows(&fused, ff, ff),
1161 ) {
1162 return Ok(ExpertWeights {
1163 gate,
1164 up,
1165 down: load_weight_matrix(file, &down_name)?,
1166 });
1167 }
1168 let mut full = Vec::with_capacity(fused.rows() * cols);
1169 for r in 0..fused.rows() {
1170 full.extend_from_slice(&fused.dequant_row(r));
1171 }
1172 let gate = WeightMatrix::F32(Tensor::new(full[..ff * cols].to_vec(), vec![ff, cols]));
1173 let up = WeightMatrix::F32(Tensor::new(full[ff * cols..].to_vec(), vec![ff, cols]));
1174 Ok(ExpertWeights {
1175 gate,
1176 up,
1177 down: load_weight_matrix(file, &down_name)?,
1178 })
1179}
1180
1181pub(crate) fn widen_plain_float(
1190 dtype: GgmlType,
1191 raw: &[u8],
1192 name: &str,
1193) -> Result<Vec<f32>, LoadError> {
1194 match dtype {
1195 GgmlType::F32 => {
1196 let mut out = Vec::with_capacity(raw.len() / 4);
1197 for chunk in raw.as_chunks::<4>().0 {
1198 out.push(f32::from_le_bytes(*chunk));
1199 }
1200 Ok(out)
1201 }
1202 GgmlType::F16 => ferrox_quant::dequant_f16(raw)
1203 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::F16)),
1204 GgmlType::BF16 => ferrox_quant::dequant_bf16(raw)
1205 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::BF16)),
1206 GgmlType::MXFP4 => ferrox_quant::dequant_mxfp4_gguf(raw)
1213 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::MXFP4)),
1214 other => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1215 }
1216}
1217
1218pub(crate) fn load_f32_vec(file: &impl TensorSource, name: &str) -> Result<Vec<f32>, LoadError> {
1219 let info = find_info(file, name)?;
1220 let raw = file.tensor_bytes(name)?;
1221 match info.dtype {
1222 GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 | GgmlType::MXFP4 => {
1234 widen_plain_float(info.dtype, raw, name)
1235 }
1236 GgmlType::Q8_0 => ferrox_quant::dequant_q8_0(raw)
1237 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q8_0)),
1238 GgmlType::Q4_0 => ferrox_quant::dequant_q4_0(raw)
1239 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4_0)),
1240 GgmlType::Q4K => ferrox_quant::dequant_q4_k(raw)
1241 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4K)),
1242 GgmlType::Q5K => ferrox_quant::dequant_q5_k(raw)
1243 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5K)),
1244 GgmlType::Q6K => ferrox_quant::dequant_q6_k(raw)
1245 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q6K)),
1246 GgmlType::Q2K => ferrox_quant::dequant_q2_k(raw)
1247 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q2K)),
1248 GgmlType::Q3K => ferrox_quant::dequant_q3_k(raw)
1249 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q3K)),
1250 GgmlType::Q4_1 => ferrox_quant::dequant_q4_1(raw)
1251 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4_1)),
1252 GgmlType::Q5_0 => ferrox_quant::dequant_q5_0(raw)
1253 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5_0)),
1254 GgmlType::Q5_1 => ferrox_quant::dequant_q5_1(raw)
1255 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5_1)),
1256 GgmlType::Q8_1 => ferrox_quant::dequant_q8_1(raw)
1257 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q8_1)),
1258 GgmlType::IQ4NL => ferrox_quant::dequant_iq4_nl(raw)
1259 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ4NL)),
1260 GgmlType::IQ4XS => ferrox_quant::dequant_iq4_xs(raw)
1261 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ4XS)),
1262 GgmlType::IQ1S => ferrox_quant::dequant_iq1_s(raw)
1269 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ1S)),
1270 GgmlType::IQ1M => ferrox_quant::dequant_iq1_m(raw)
1271 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ1M)),
1272 GgmlType::IQ2XXS => ferrox_quant::dequant_iq2_xxs(raw)
1273 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2XXS)),
1274 GgmlType::IQ2XS => ferrox_quant::dequant_iq2_xs(raw)
1275 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2XS)),
1276 GgmlType::IQ2S => ferrox_quant::dequant_iq2_s(raw)
1277 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2S)),
1278 GgmlType::IQ3XXS => ferrox_quant::dequant_iq3_xxs(raw)
1279 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ3XXS)),
1280 GgmlType::IQ3S => ferrox_quant::dequant_iq3_s(raw)
1281 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ3S)),
1282 other => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1283 }
1284}
1285
1286pub(crate) fn load_weight_matrix(
1293 file: &impl TensorSource,
1294 name: &str,
1295) -> Result<WeightMatrix, LoadError> {
1296 let info = find_info(file, name)?;
1297 let shape: Vec<usize> = info.shape.iter().rev().map(|&d| d as usize).collect();
1310 let (rows, cols) = match shape.as_slice() {
1311 [r, c] => (*r, *c),
1312 other => {
1313 return Err(LoadError::UnsupportedDtype(
1314 format!("{name} (expected 2D, got shape {other:?})"),
1315 info.dtype,
1316 ))
1317 }
1318 };
1319
1320 match info.dtype {
1321 GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => {
1327 let data = load_f32_vec(file, name)?;
1328 Ok(WeightMatrix::F32(Tensor::new(data, shape)))
1329 }
1330 other => match quant_kind_for(other) {
1331 Some(kind) => {
1332 let (mmap, range) = file.tensor_mapped_range(name)?;
1333 #[cfg(feature = "metal")]
1334 ferrox_metal::gpu::register_weight_mmap(Arc::clone(&mmap));
1335 Ok(WeightMatrix::Quantized {
1336 data: WeightBytes::Mapped { mmap, range },
1337 rows,
1338 cols,
1339 kind,
1340 })
1341 }
1342 None => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1343 },
1344 }
1345}
1346
1347pub(crate) fn split_expert_tensor(
1354 file: &impl TensorSource,
1355 name: &str,
1356 n_experts: usize,
1357) -> Result<Vec<WeightMatrix>, LoadError> {
1358 let info = find_info(file, name)?;
1359 if info.shape.len() != 3 || info.shape[2] as usize != n_experts {
1366 let file_experts = info.shape.last().map(|&d| d as usize).unwrap_or(0);
1367 return Err(LoadError::ExpertCountMismatch(
1368 name.to_string(),
1369 file_experts,
1370 n_experts,
1371 ));
1372 }
1373 let out_dim = info.shape[1] as usize;
1374 let in_dim = info.shape[0] as usize;
1375 let raw = file.tensor_bytes(name)?;
1376
1377 match info.dtype {
1378 GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => {
1379 let all = crate::loader::widen_plain_float(info.dtype, raw, name)?;
1380 let per_expert = out_dim * in_dim;
1381 Ok((0..n_experts)
1382 .map(|e| {
1383 WeightMatrix::F32(Tensor::new(
1384 all[e * per_expert..(e + 1) * per_expert].to_vec(),
1385 vec![out_dim, in_dim],
1386 ))
1387 })
1388 .collect())
1389 }
1390 other => match quant_kind_for(other) {
1391 Some(kind) => {
1392 let (mmap, full_range) = file.tensor_mapped_range(name)?;
1393 #[cfg(feature = "metal")]
1394 ferrox_metal::gpu::register_weight_mmap(Arc::clone(&mmap));
1395 let bytes_per_expert = raw.len() / n_experts;
1396 Ok((0..n_experts)
1397 .map(|e| WeightMatrix::Quantized {
1398 data: WeightBytes::Mapped {
1399 mmap: Arc::clone(&mmap),
1400 range: (full_range.start + e * bytes_per_expert)
1401 ..(full_range.start + (e + 1) * bytes_per_expert),
1402 },
1403 rows: out_dim,
1404 cols: in_dim,
1405 kind,
1406 })
1407 .collect())
1408 }
1409 None => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1410 },
1411 }
1412}
1413
1414#[cfg(feature = "metal")]
1419fn try_build_moe_packed_q4_planes(experts: &[ExpertWeights]) -> Option<MoePackedQ4Planes> {
1420 use ferrox_core::weight_matrix::{QuantKind, WeightBytes};
1421 use std::sync::Arc;
1422
1423 if experts.is_empty() {
1424 return None;
1425 }
1426
1427 fn mapped_sg(m: &WeightMatrix) -> Option<(WeightBytes, usize, &'static str)> {
1428 match m {
1429 WeightMatrix::Quantized {
1430 data: WeightBytes::Mapped { mmap, range },
1431 rows,
1432 kind,
1433 ..
1434 } => {
1435 let kind_str = match kind {
1436 QuantKind::Q4_0 => "Q4_0",
1437 QuantKind::Q5_0 => "Q5_0",
1438 QuantKind::Q4K => "Q4_K",
1439 QuantKind::Q5K => "Q5_K",
1440 QuantKind::Q6K => "Q6_K",
1441 QuantKind::Q8_0 => "Q8_0",
1442 QuantKind::IQ4XS => "IQ4_XS",
1443 _ => return None,
1444 };
1445 let _ = ferrox_metal::gpu::mul_mm_sg_meta(kind_str)?;
1446 Some((
1447 WeightBytes::Mapped {
1448 mmap: Arc::clone(mmap),
1449 range: range.clone(),
1450 },
1451 *rows,
1452 kind_str,
1453 ))
1454 }
1455 _ => None,
1456 }
1457 }
1458
1459 let (gate0, ffn_rows, gate_kind) = mapped_sg(&experts[0].gate)?;
1460 let (up0, up_rows, up_kind) = mapped_sg(&experts[0].up)?;
1461 let (down0, hidden_rows, down_kind) = mapped_sg(&experts[0].down)?;
1462 if up_rows != ffn_rows {
1463 return None;
1464 }
1465 let WeightBytes::Mapped {
1466 mmap: gate_mmap,
1467 range: gate0_range,
1468 } = &gate0
1469 else {
1470 return None;
1471 };
1472 let WeightBytes::Mapped {
1473 mmap: up_mmap,
1474 range: up0_range,
1475 } = &up0
1476 else {
1477 return None;
1478 };
1479 let WeightBytes::Mapped {
1480 mmap: down_mmap,
1481 range: down0_range,
1482 } = &down0
1483 else {
1484 return None;
1485 };
1486
1487 let gate_stride = gate0_range.len();
1488 let up_stride = up0_range.len();
1489 let down_stride = down0_range.len();
1490 if gate_stride == 0 || up_stride == 0 || down_stride == 0 {
1491 return None;
1492 }
1493
1494 let n = experts.len();
1495 for (i, ex) in experts.iter().enumerate().skip(1) {
1496 let (g, fr, gk) = mapped_sg(&ex.gate)?;
1497 let (u, ur, uk) = mapped_sg(&ex.up)?;
1498 let (d, hr, dk) = mapped_sg(&ex.down)?;
1499 if gk != gate_kind || uk != up_kind || dk != down_kind {
1500 return None;
1501 }
1502 let WeightBytes::Mapped { mmap, range } = &g else {
1503 return None;
1504 };
1505 if fr != ffn_rows {
1506 return None;
1507 }
1508 if !Arc::ptr_eq(mmap, gate_mmap)
1509 || range.len() != gate_stride
1510 || range.start != gate0_range.start + i * gate_stride
1511 {
1512 return None;
1513 }
1514 let WeightBytes::Mapped { mmap, range } = &u else {
1515 return None;
1516 };
1517 if ur != ffn_rows
1518 || !Arc::ptr_eq(mmap, up_mmap)
1519 || range.len() != up_stride
1520 || range.start != up0_range.start + i * up_stride
1521 {
1522 return None;
1523 }
1524 let WeightBytes::Mapped { mmap, range } = &d else {
1525 return None;
1526 };
1527 if hr != hidden_rows
1528 || !Arc::ptr_eq(mmap, down_mmap)
1529 || range.len() != down_stride
1530 || range.start != down0_range.start + i * down_stride
1531 {
1532 return None;
1533 }
1534 }
1535
1536 Some(MoePackedQ4Planes::new(
1537 WeightBytes::Mapped {
1538 mmap: Arc::clone(gate_mmap),
1539 range: gate0_range.start..gate0_range.start + n * gate_stride,
1540 },
1541 WeightBytes::Mapped {
1542 mmap: Arc::clone(up_mmap),
1543 range: up0_range.start..up0_range.start + n * up_stride,
1544 },
1545 WeightBytes::Mapped {
1546 mmap: Arc::clone(down_mmap),
1547 range: down0_range.start..down0_range.start + n * down_stride,
1548 },
1549 gate_stride,
1550 up_stride,
1551 down_stride,
1552 n,
1553 ffn_rows,
1554 hidden_rows,
1555 gate_kind,
1556 up_kind,
1557 down_kind,
1558 ))
1559}
1560
1561#[derive(Debug, Clone, Copy)]
1565pub struct StoredMatrixSpec {
1566 pub offset: usize,
1567 pub len: usize,
1568 pub rows: usize,
1569 pub cols: usize,
1570 pub kind: QuantKind,
1571}
1572
1573#[derive(Debug, Clone, Copy)]
1575pub struct StoredExpertLayout {
1576 pub gate: StoredMatrixSpec,
1577 pub up: StoredMatrixSpec,
1578 pub down: StoredMatrixSpec,
1579}
1580
1581impl StoredExpertLayout {
1582 pub fn total_bytes(&self) -> usize {
1583 self.gate.len + self.up.len + self.down.len
1584 }
1585
1586 pub fn materialize(&self, lease: &ferrox_core::expert_store::ExpertLease) -> ExpertWeights {
1590 let mk = |spec: &StoredMatrixSpec| WeightMatrix::Quantized {
1591 data: WeightBytes::Shared {
1592 buf: lease.shared_buf(),
1593 range: spec.offset..spec.offset + spec.len,
1594 },
1595 rows: spec.rows,
1596 cols: spec.cols,
1597 kind: spec.kind,
1598 };
1599 ExpertWeights {
1600 gate: mk(&self.gate),
1601 up: mk(&self.up),
1602 down: mk(&self.down),
1603 }
1604 }
1605}
1606
1607pub struct GgufExpertSource {
1613 files: Vec<std::fs::File>,
1614 segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]>,
1617}
1618
1619impl ExpertSource for GgufExpertSource {
1620 fn expert_len(&self, key: ExpertKey) -> Option<usize> {
1621 self.segments
1622 .get(&key)
1623 .map(|segs| segs.iter().map(|&(_, _, len)| len).sum())
1624 }
1625
1626 fn read_expert(&self, key: ExpertKey) -> std::io::Result<Vec<u8>> {
1627 let segs = self
1628 .segments
1629 .get(&key)
1630 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, format!("{key:?}")))?;
1631 let total: usize = segs.iter().map(|&(_, _, len)| len).sum();
1632 let mut buf = vec![0u8; total];
1633 let mut written = 0;
1634 for &(fi, offset, len) in segs {
1635 let dst = &mut buf[written..written + len];
1636 #[cfg(unix)]
1637 {
1638 use std::os::unix::fs::FileExt;
1639 self.files[fi].read_exact_at(dst, offset)?;
1640 }
1641 #[cfg(not(unix))]
1642 {
1643 use std::io::{Read, Seek, SeekFrom};
1644 let mut f = &self.files[fi];
1645 f.seek(SeekFrom::Start(offset))?;
1646 f.read_exact(dst)?;
1647 }
1648 written += len;
1649 }
1650 Ok(buf)
1651 }
1652}
1653
1654struct StoredTensorSpecs {
1664 shard: usize,
1665 per_expert: Vec<(u64, usize)>,
1666 spec: StoredMatrixSpec,
1667}
1668
1669fn stored_expert_specs(
1670 file: &ShardedGguf,
1671 name: &str,
1672 n_experts: usize,
1673) -> Result<Option<StoredTensorSpecs>, LoadError> {
1674 let info = find_info(file, name)?;
1675 if info.shape.len() != 3 || info.shape[2] as usize != n_experts {
1676 let file_experts = info.shape.last().map(|&d| d as usize).unwrap_or(0);
1677 return Err(LoadError::ExpertCountMismatch(
1678 name.to_string(),
1679 file_experts,
1680 n_experts,
1681 ));
1682 }
1683 let out_dim = info.shape[1] as usize;
1684 let in_dim = info.shape[0] as usize;
1685 let Some(kind) = quant_kind_for(info.dtype) else {
1686 return Ok(None); };
1688 let shard = file
1689 .tensor_shard_index(name)
1690 .expect("find_info succeeded, shard index must exist");
1691 let (_, full_range) = file.tensor_mapped_range(name)?;
1694 let total_len = full_range.end - full_range.start;
1695 let bytes_per_expert = total_len / n_experts;
1696 let per_expert: Vec<(u64, usize)> = (0..n_experts)
1697 .map(|e| {
1698 (
1699 (full_range.start + e * bytes_per_expert) as u64,
1700 bytes_per_expert,
1701 )
1702 })
1703 .collect();
1704 let spec = StoredMatrixSpec {
1705 offset: 0, len: bytes_per_expert,
1707 rows: out_dim,
1708 cols: in_dim,
1709 kind,
1710 };
1711 Ok(Some(StoredTensorSpecs {
1712 shard,
1713 per_expert,
1714 spec,
1715 }))
1716}
1717
1718impl Decoder {
1719 pub fn from_gguf(
1728 path: impl AsRef<std::path::Path>,
1729 config: ModelConfig,
1730 ) -> Result<Self, LoadError> {
1731 Self::from_gguf_with_expert_cache(path, config, None)
1732 }
1733
1734 pub fn from_gguf_with_expert_cache(
1747 path: impl AsRef<std::path::Path>,
1748 mut config: ModelConfig,
1749 expert_cache_bytes: Option<u64>,
1750 ) -> Result<Self, LoadError> {
1751 let path = path.as_ref();
1752 let file = ShardedGguf::open(path)?;
1753
1754 let arch = file
1760 .metadata_str("general.architecture")
1761 .unwrap_or_default()
1762 .to_string();
1763 let is_gpt_oss = arch == "gpt-oss";
1764 let mut gpt_oss_layers: Vec<crate::decoder::GptOssLayer> = Vec::new();
1765
1766 let mut store_segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]> =
1770 std::collections::HashMap::new();
1771 let mut stored_layouts: Vec<Option<Vec<StoredExpertLayout>>> = Vec::new();
1772
1773 let embedding = load_weight_matrix(&file, "token_embd.weight")?;
1778
1779 let mut layers = Vec::with_capacity(config.n_layers);
1780 let mut refined_qk_norm = config.qk_norm_style;
1781 for l in 0..config.n_layers {
1782 let (q_proj, k_proj, v_proj) = load_qkv_projections(&file, l, &config)?;
1783 let q_norm = load_f32_vec_optional(&file, &format!("blk.{l}.attn_q_norm.weight"))?;
1784 let k_norm = load_f32_vec_optional(&file, &format!("blk.{l}.attn_k_norm.weight"))?;
1785 if let Some(ref w) = q_norm {
1787 if w.len() == config.head_dim {
1788 refined_qk_norm = crate::capability::QkNormStyle::PerHead;
1789 } else if w.len() == config.n_heads * config.head_dim {
1790 refined_qk_norm = crate::capability::QkNormStyle::WholeVector;
1791 } else {
1792 return Err(LoadError::UnsupportedFeature(
1793 config.name.to_string(),
1794 format!(
1795 "blk.{l}.attn_q_norm.weight length {} matches neither head_dim={} \
1796 nor n_heads*head_dim={}",
1797 w.len(),
1798 config.head_dim,
1799 config.n_heads * config.head_dim
1800 ),
1801 ));
1802 }
1803 }
1804 let attn = AttnWeights {
1805 q_proj,
1806 k_proj,
1807 v_proj,
1808 o_proj: load_weight_matrix(&file, &format!("blk.{l}.attn_output.weight"))?,
1809 norm_weight: load_f32_vec(&file, &format!("blk.{l}.attn_norm.weight"))?,
1810 q_norm,
1811 k_norm,
1812 q_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_q.bias"))?,
1816 k_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_k.bias"))?,
1817 v_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_v.bias"))?,
1818 post_attn_norm: if is_gpt_oss {
1824 None
1825 } else {
1826 load_f32_vec_optional(&file, &format!("blk.{l}.post_attention_norm.weight"))?
1827 },
1828 post_ffn_norm: load_f32_vec_optional(
1829 &file,
1830 &format!("blk.{l}.post_ffw_norm.weight"),
1831 )?,
1832 };
1833
1834 let is_dense_layer = config.layer_is_dense(l) || config.moe.n_experts <= 1;
1842 let n_experts = if is_dense_layer {
1843 1
1844 } else {
1845 config.moe.n_experts
1846 };
1847 let experts: ExpertBacking = if is_dense_layer {
1848 ExpertBacking::Resident(vec![load_dense_expert(&file, l, &config)?])
1849 } else {
1850 let stored = if expert_cache_bytes.is_some() {
1854 let g = stored_expert_specs(
1855 &file,
1856 &format!("blk.{l}.ffn_gate_exps.weight"),
1857 n_experts,
1858 )?;
1859 let u = stored_expert_specs(
1860 &file,
1861 &format!("blk.{l}.ffn_up_exps.weight"),
1862 n_experts,
1863 )?;
1864 let d = stored_expert_specs(
1865 &file,
1866 &format!("blk.{l}.ffn_down_exps.weight"),
1867 n_experts,
1868 )?;
1869 match (g, u, d) {
1870 (Some(gt), Some(ut), Some(dt)) => {
1871 let mut layouts = Vec::with_capacity(n_experts);
1872 for e in 0..n_experts {
1873 let key = ExpertKey {
1874 layer: l as u32,
1875 expert: e as u32,
1876 };
1877 store_segments.insert(
1878 key,
1879 [
1880 (gt.shard, gt.per_expert[e].0, gt.per_expert[e].1),
1881 (ut.shard, ut.per_expert[e].0, ut.per_expert[e].1),
1882 (dt.shard, dt.per_expert[e].0, dt.per_expert[e].1),
1883 ],
1884 );
1885 let mut gate = gt.spec;
1886 let mut up = ut.spec;
1887 let mut down = dt.spec;
1888 gate.offset = 0;
1889 up.offset = gate.len;
1890 down.offset = gate.len + up.len;
1891 layouts.push(StoredExpertLayout { gate, up, down });
1892 }
1893 Some(layouts)
1894 }
1895 _ => None,
1896 }
1897 } else {
1898 None
1899 };
1900 match stored {
1901 Some(layouts) => {
1902 stored_layouts.push(Some(layouts));
1906 ExpertBacking::Resident(Vec::new())
1907 }
1908 None => {
1909 let gates = split_expert_tensor(
1910 &file,
1911 &format!("blk.{l}.ffn_gate_exps.weight"),
1912 n_experts,
1913 )?;
1914 let ups = split_expert_tensor(
1915 &file,
1916 &format!("blk.{l}.ffn_up_exps.weight"),
1917 n_experts,
1918 )?;
1919 let downs = split_expert_tensor(
1920 &file,
1921 &format!("blk.{l}.ffn_down_exps.weight"),
1922 n_experts,
1923 )?;
1924 ExpertBacking::Resident(
1925 gates
1926 .into_iter()
1927 .zip(ups)
1928 .zip(downs)
1929 .map(|((gate, up), down)| ExpertWeights { gate, up, down })
1930 .collect(),
1931 )
1932 }
1933 }
1934 };
1935 if stored_layouts.len() < layers.len() + 1 {
1936 stored_layouts.push(None);
1937 }
1938
1939 let shared_experts: Vec<ExpertWeights> =
1940 if config.moe.n_shared_experts > 0 && !is_dense_layer {
1941 vec![ExpertWeights {
1942 gate: load_weight_matrix(&file, &format!("blk.{l}.ffn_gate_shexp.weight"))?,
1943 up: load_weight_matrix(&file, &format!("blk.{l}.ffn_up_shexp.weight"))?,
1944 down: load_weight_matrix(&file, &format!("blk.{l}.ffn_down_shexp.weight"))?,
1945 }]
1946 } else {
1947 Vec::new()
1948 };
1949
1950 let router = if !is_dense_layer {
1951 load_weight_matrix(&file, &format!("blk.{l}.ffn_gate_inp.weight"))?
1952 } else {
1953 WeightMatrix::F32(Tensor::zeros(vec![1, config.hidden_dim]))
1956 };
1957
1958 let n_for_counts = match &experts {
1959 ExpertBacking::Resident(v) if v.is_empty() => n_experts,
1960 other => other.n_experts(),
1961 };
1962 let activation_counts = (0..n_for_counts)
1963 .map(|_| std::sync::atomic::AtomicU64::new(0))
1964 .collect();
1965 let shared_expert_gate = if is_dense_layer {
1974 None
1975 } else {
1976 load_f32_vec_optional(&file, &format!("blk.{l}.ffn_gate_inp_shexp.weight"))?
1977 };
1978 #[cfg(feature = "metal")]
1979 let packed_q4 = match &experts {
1980 ExpertBacking::Resident(v) if !v.is_empty() => try_build_moe_packed_q4_planes(v),
1981 _ => None,
1982 };
1983 let exp_probs_bias = if is_dense_layer {
1991 None
1992 } else {
1993 load_f32_vec_optional(&file, &format!("blk.{l}.exp_probs_b.bias"))?
1994 };
1995 if let Some(bias) = &exp_probs_bias {
1996 if bias.len() != config.moe.n_experts {
1997 return Err(LoadError::UnsupportedFeature(
1998 arch.clone(),
1999 format!(
2000 "blk.{l}.exp_probs_b.bias has {} entries but the model has {} experts",
2001 bias.len(),
2002 config.moe.n_experts
2003 ),
2004 ));
2005 }
2006 if config.moe.expert_group_count.is_some() {
2013 return Err(LoadError::UnsupportedFeature(
2014 arch.clone(),
2015 format!(
2016 "blk.{l}.exp_probs_b.bias together with expert groups \
2017 ({:?}): llama.cpp masks the biased scores per group \
2018 before a global top-k, which is not the per-group \
2019 top-k ferrox implements",
2020 config.moe.expert_group_count
2021 ),
2022 ));
2023 }
2024 }
2025 let moe = MoeWeights {
2026 router,
2027 experts,
2028 shared_experts,
2029 shared_expert_gate,
2030 exp_probs_bias,
2031 norm_weight: if is_gpt_oss {
2032 load_f32_vec(&file, &format!("blk.{l}.post_attention_norm.weight"))?
2033 } else {
2034 load_f32_vec(&file, &format!("blk.{l}.ffn_norm.weight"))?
2035 },
2036 activation_counts,
2037 #[cfg(feature = "metal")]
2038 packed_q4,
2039 };
2040
2041 if is_gpt_oss {
2042 gpt_oss_layers.push(load_gpt_oss_layer(&file, l, &config)?);
2043 }
2044
2045 layers.push(LayerWeights { attn, moe });
2046 }
2047
2048 let final_norm = load_f32_vec(&file, "output_norm.weight")?;
2049 let output_head = match load_weight_matrix(&file, "output.weight") {
2054 Ok(w) => w,
2055 Err(_) => load_weight_matrix(&file, "token_embd.weight")?,
2056 };
2057
2058 if !store_segments.is_empty() {
2064 let budget = expert_cache_bytes
2065 .expect("store_segments only populated when a cache budget is set")
2066 as usize;
2067 let files: Result<Vec<std::fs::File>, std::io::Error> =
2068 file.shard_paths().iter().map(std::fs::File::open).collect();
2069 let files = files.map_err(GgufError::from)?;
2070 let store = std::sync::Arc::new(ExpertStore::new(
2071 GgufExpertSource {
2072 files,
2073 segments: store_segments,
2074 },
2075 budget,
2076 ));
2077 for (l, layer) in layers.iter_mut().enumerate() {
2078 if let Some(layouts) = stored_layouts.get_mut(l).and_then(Option::take) {
2079 layer.moe.experts = ExpertBacking::Stored {
2080 store: std::sync::Arc::clone(&store),
2081 layouts,
2082 layer: l as u32,
2083 };
2084 }
2085 }
2086 }
2087
2088 config.qk_norm_style = refined_qk_norm;
2089
2090 let family = crate::capability::resolve_profile(
2091 file.metadata_str("general.architecture").unwrap_or("llama"),
2092 )
2093 .map(|p| p.family)
2094 .unwrap_or(crate::capability::DecoderFamily::StandardGqa);
2095 let memory_kind = crate::capability::resolve_profile(
2096 file.metadata_str("general.architecture").unwrap_or("llama"),
2097 )
2098 .map(|p| p.memory)
2099 .unwrap_or(crate::capability::MemoryKind::KvGqa);
2100 let execution_plan = crate::execution_plan::ExecutionPlan::from_config(
2101 &config,
2102 family,
2103 memory_kind,
2104 crate::execution_plan::ExecutionPlan::probe_metal_caps(),
2105 );
2106
2107 let decoder = Decoder {
2108 config,
2109 embedding,
2110 layers,
2111 final_norm,
2112 output_head,
2113 gpu_vram_budget_bytes: None,
2114 gpt_oss: if is_gpt_oss {
2115 Some(crate::decoder::GptOssWeights {
2116 layers: gpt_oss_layers,
2117 })
2118 } else {
2119 None
2120 },
2121 #[cfg(feature = "metal")]
2122 metal_attn_kv: std::sync::Mutex::new(None),
2123 execution_plan,
2124 plan_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
2125 };
2126 decoder.probe_kernels();
2130 ferrox_core::kernel_registry::seal_or_error()
2131 .map_err(|e| LoadError::StrictKernels(e.to_string()))?;
2132 for name in crate::config::MODEL_LEVEL_TENSORS_READ_BY_CONFIG {
2139 file.note_consumed(name);
2140 }
2141 assert_every_tensor_consumed(&file)?;
2142 Ok(decoder)
2143 }
2144}
2145
2146const IGNORED_TENSOR_PREFIXES: &[&str] = &["mm.", "v.", "mmproj.", "resampler.", "audio."];
2151
2152pub fn assert_every_tensor_consumed(file: &ShardedGguf) -> Result<(), LoadError> {
2174 let mut left: Vec<String> = file
2175 .unconsumed_tensors()
2176 .into_iter()
2177 .filter(|n| !IGNORED_TENSOR_PREFIXES.iter().any(|p| n.starts_with(p)))
2178 .collect();
2179 if left.is_empty() {
2180 return Ok(());
2181 }
2182 left.sort();
2183 let shown = left.iter().take(8).cloned().collect::<Vec<_>>().join(", ");
2184 let listing = if left.len() > 8 {
2185 format!("{shown}, … (+{} more)", left.len() - 8)
2186 } else {
2187 shown
2188 };
2189 if matches!(
2190 std::env::var("FERROX_ALLOW_UNKNOWN_TENSORS")
2191 .ok()
2192 .as_deref(),
2193 Some("1") | Some("true") | Some("on")
2194 ) {
2195 eprintln!(
2196 "ferrox: WARNING — {} tensor(s) in this checkpoint are never read \
2197 ({listing}); output may be wrong (FERROX_ALLOW_UNKNOWN_TENSORS=1)",
2198 left.len()
2199 );
2200 return Ok(());
2201 }
2202 Err(LoadError::UnconsumedTensors(left.len(), listing))
2203}
2204
2205#[cfg(test)]
2206mod tests {
2207
2208 #[test]
2224 fn a_quantized_one_dimensional_tensor_widens_through_the_shared_helper() {
2225 let values: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) * 0.25).collect();
2226 let quantized = ferrox_quant::quantize_q8_0(&values);
2227
2228 struct OneTensor {
2229 info: TensorInfo,
2230 bytes: Vec<u8>,
2231 }
2232 impl TensorSource for OneTensor {
2233 fn metadata(&self, _key: &str) -> Option<&ferrox_gguf::GgufValue> {
2234 None
2235 }
2236 fn find_tensor(&self, name: &str) -> Option<&TensorInfo> {
2237 (name == self.info.name).then_some(&self.info)
2238 }
2239 fn tensor_bytes(&self, _name: &str) -> Result<&[u8], GgufError> {
2240 Ok(&self.bytes)
2241 }
2242 fn tensor_mapped_range(
2243 &self,
2244 name: &str,
2245 ) -> Result<
2246 (
2247 std::sync::Arc<ferrox_gguf::MmapHandle>,
2248 std::ops::Range<usize>,
2249 ),
2250 GgufError,
2251 > {
2252 Err(GgufError::TensorNotFound(name.to_string()))
2254 }
2255 }
2256
2257 let source = OneTensor {
2258 info: TensorInfo {
2259 name: "blk.0.attn_norm.weight".to_string(),
2260 shape: vec![64],
2261 dtype: GgmlType::Q8_0,
2262 offset: 0,
2263 },
2264 bytes: quantized,
2265 };
2266
2267 let widened = load_f32_vec(&source, "blk.0.attn_norm.weight")
2268 .expect("a Q8_0 norm must load, not report an unsupported dtype");
2269 assert_eq!(widened.len(), values.len());
2270 for (got, want) in widened.iter().zip(values.iter()) {
2271 assert!(
2272 (got - want).abs() < 0.05,
2273 "q8_0 round trip: got {got}, want {want}"
2274 );
2275 }
2276 }
2277 use super::*;
2278 use byteorder::{LittleEndian, WriteBytesExt};
2279 use std::io::Write;
2280
2281 fn write_string(buf: &mut Vec<u8>, s: &str) {
2282 buf.write_u64::<LittleEndian>(s.len() as u64).unwrap();
2283 buf.write_all(s.as_bytes()).unwrap();
2284 }
2285
2286 fn write_kv_str(buf: &mut Vec<u8>, key: &str, val: &str) {
2287 write_string(buf, key);
2288 buf.write_u32::<LittleEndian>(8).unwrap(); write_string(buf, val);
2290 }
2291
2292 fn build_arch_only_gguf(arch: &str) -> Vec<u8> {
2298 let mut buf = Vec::new();
2299 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2300 .unwrap();
2301 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);
2305 buf
2306 }
2307
2308 #[test]
2309 fn model_config_from_gguf_fails_loudly_when_required_hparams_are_missing() {
2310 let tmp =
2311 std::env::temp_dir().join(format!("ferrox_test_arch_only_{}.gguf", std::process::id()));
2312 std::fs::write(&tmp, build_arch_only_gguf("llama")).unwrap();
2315 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2316 std::fs::remove_file(&tmp).ok();
2317
2318 match ModelConfig::from_gguf(&file) {
2319 Err(LoadError::MissingHparam(key)) => {
2320 assert_eq!(key, "llama.block_count");
2321 }
2322 other => panic!(
2323 "expected LoadError::MissingHparam for a file with no hparam keys, got {other:?}"
2324 ),
2325 }
2326 }
2327
2328 #[test]
2329 fn model_config_from_gguf_fails_closed_on_unknown_architecture() {
2330 let tmp = std::env::temp_dir().join(format!(
2331 "ferrox_test_unknown_arch_{}.gguf",
2332 std::process::id()
2333 ));
2334 std::fs::write(&tmp, build_arch_only_gguf("bogus-arch-with-no-hparams")).unwrap();
2335 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2336 std::fs::remove_file(&tmp).ok();
2337
2338 match ModelConfig::from_gguf(&file) {
2339 Err(LoadError::UnsupportedArchitecture(arch)) => {
2340 assert_eq!(arch, "bogus-arch-with-no-hparams");
2341 }
2342 other => panic!(
2343 "expected LoadError::UnsupportedArchitecture for an unregistered arch, got {other:?}"
2344 ),
2345 }
2346 }
2347
2348 fn write_kv_f32(buf: &mut Vec<u8>, key: &str, val: f32) {
2349 write_string(buf, key);
2350 buf.write_u32::<LittleEndian>(6).unwrap(); buf.write_f32::<LittleEndian>(val).unwrap();
2352 }
2353
2354 fn build_arch_plus_f32_gguf(arch: &str, key: &str, val: f32) -> Vec<u8> {
2357 let mut buf = Vec::new();
2358 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2359 .unwrap();
2360 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);
2364 write_kv_f32(&mut buf, key, val);
2365 buf
2366 }
2367
2368 fn config_error_for(arch: &str, key: &str, val: f32, tag: &str) -> LoadError {
2369 let tmp = std::env::temp_dir().join(format!("ferrox_test_scale_{tag}.gguf"));
2370 std::fs::write(&tmp, build_arch_plus_f32_gguf(arch, key, val)).unwrap();
2371 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2372 std::fs::remove_file(&tmp).ok();
2373 ModelConfig::from_gguf(&file).expect_err("must not succeed")
2374 }
2375
2376 #[test]
2382 fn a_declared_multiplier_this_decoder_does_not_apply_is_refused_by_name() {
2383 for (key, val) in [
2384 ("granite.logit_scale", 6.0f32),
2385 ("granite.residual_scale", 0.22),
2386 ("granite.embedding_scale", 12.0),
2387 ("granite.attention.scale", 0.015_625),
2388 ] {
2389 let tag = key.replace('.', "_");
2390 match config_error_for("granite", key, val, &tag) {
2391 LoadError::UnsupportedFeature(arch, msg) => {
2392 assert_eq!(arch, "granite");
2393 assert!(msg.contains(key), "error must name the key: {msg}");
2394 }
2395 other => panic!("expected UnsupportedFeature for {key}, got {other:?}"),
2396 }
2397 }
2398 }
2399
2400 #[test]
2406 fn a_multiplier_that_is_a_no_op_is_not_refused() {
2407 for (key, val) in [
2408 ("granite.logit_scale", 1.0f32),
2409 ("granite.residual_scale", 1.0),
2410 ("granite.embedding_scale", 1.0),
2411 ("granite.attention.scale", 0.0),
2412 ] {
2413 let tag = format!("noop_{}", key.replace('.', "_"));
2414 match config_error_for("granite", key, val, &tag) {
2417 LoadError::MissingHparam(k) => assert_eq!(k, "granite.block_count"),
2418 other => panic!("no-op {key}={val} must pass the scaling gate, got {other:?}"),
2419 }
2420 }
2421 }
2422
2423 enum Kv<'a> {
2426 Str(&'a str),
2427 U32(u32),
2428 F32(f32),
2429 }
2430
2431 fn build_metadata_gguf(kvs: &[(&str, Kv)]) -> Vec<u8> {
2434 let mut buf = Vec::new();
2435 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2436 .unwrap();
2437 buf.write_u32::<LittleEndian>(3).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); buf.write_u64::<LittleEndian>(kvs.len() as u64).unwrap();
2440 for (k, v) in kvs {
2441 match v {
2442 Kv::Str(s) => write_kv_str(&mut buf, k, s),
2443 Kv::U32(n) => {
2444 write_string(&mut buf, k);
2445 buf.write_u32::<LittleEndian>(4).unwrap(); buf.write_u32::<LittleEndian>(*n).unwrap();
2447 }
2448 Kv::F32(f) => write_kv_f32(&mut buf, k, *f),
2449 }
2450 }
2451 buf
2452 }
2453
2454 fn open_metadata_gguf(tag: &str, kvs: &[(&str, Kv)]) -> ferrox_gguf::GgufFile {
2455 let tmp = std::env::temp_dir().join(format!("ferrox_test_meta_{tag}.gguf"));
2456 std::fs::write(&tmp, build_metadata_gguf(kvs)).unwrap();
2457 let file = ferrox_gguf::GgufFile::open(&tmp).expect("header-only file must parse");
2458 std::fs::remove_file(&tmp).ok();
2459 file
2460 }
2461
2462 fn llama_config_with(tag: &str, extra: &[(&str, Kv)]) -> ModelConfig {
2465 let mut kvs: Vec<(&str, Kv)> = vec![
2466 ("general.architecture", Kv::Str("llama")),
2467 ("llama.block_count", Kv::U32(1)),
2468 ("llama.embedding_length", Kv::U32(64)),
2469 ("llama.attention.head_count", Kv::U32(1)),
2470 ("llama.attention.head_count_kv", Kv::U32(1)),
2471 ("llama.attention.key_length", Kv::U32(64)),
2472 ("llama.rope.freq_base", Kv::F32(10_000.0)),
2473 ];
2474 for (k, v) in extra {
2475 kvs.push((
2476 k,
2477 match v {
2478 Kv::Str(s) => Kv::Str(s),
2479 Kv::U32(n) => Kv::U32(*n),
2480 Kv::F32(f) => Kv::F32(*f),
2481 },
2482 ));
2483 }
2484 ModelConfig::from_gguf(&open_metadata_gguf(tag, &kvs)).expect("fixture must load")
2485 }
2486
2487 #[test]
2501 fn the_ffn_activation_follows_the_architecture_not_the_family() {
2502 use crate::capability::uses_geglu;
2503 use crate::config::FfnActivation;
2504
2505 assert!(uses_geglu("grok"), "grok's MoE FFN gate is GELU upstream");
2506 assert!(!uses_geglu("dbrx"));
2509 assert!(!uses_geglu("llama"));
2510
2511 for gemma in ["gemma2", "gemma3"] {
2516 assert!(
2517 !uses_geglu(gemma),
2518 "{gemma} is GELU via GemmaFamily; listing it here too \
2519 would hide a later regression in the family rule"
2520 );
2521 assert_eq!(
2522 config_for_arch(gemma).expect("gemma loads").ffn_activation,
2523 FfnActivation::Gelu,
2524 "{gemma}"
2525 );
2526 }
2527
2528 assert_eq!(
2530 config_for_arch("llama")
2531 .expect("llama loads")
2532 .ffn_activation,
2533 FfnActivation::Swiglu
2534 );
2535 }
2536
2537 #[test]
2553 fn the_architectures_llama_cpp_does_not_renormalise_are_pinned() {
2554 for arch in ["deepseek", "olmoe", "qwen2moe"] {
2555 assert!(
2556 NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&arch),
2557 "{arch} passes norm_w=false in llama.cpp and must not be renormalised"
2558 );
2559 }
2560 assert!(!NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"deepseek2"));
2564 assert!(!NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"qwen3moe"));
2565 }
2566
2567 #[test]
2577 fn the_architectures_llama_cpp_defaults_to_sigmoid_gating_are_pinned() {
2578 for arch in ["afmoe", "deepseek2", "glm4moe", "laguna", "step35"] {
2579 assert!(
2580 SIGMOID_GATING_ARCHITECTURES.contains(&arch),
2581 "{arch} sets SIGMOID when the gating key is absent"
2582 );
2583 }
2584 for softmax in ["ernie4_5-moe", "qwen3moe", "olmoe", "llama"] {
2588 assert!(
2589 !SIGMOID_GATING_ARCHITECTURES.contains(&softmax),
2590 "{softmax} does not default to sigmoid"
2591 );
2592 }
2593 }
2594
2595 fn config_for_arch(arch: &'static str) -> Result<ModelConfig, LoadError> {
2596 let keys: Vec<String> = [
2599 "block_count",
2600 "embedding_length",
2601 "attention.head_count",
2602 "attention.head_count_kv",
2603 "attention.key_length",
2604 ]
2605 .iter()
2606 .map(|k| format!("{arch}.{k}"))
2607 .collect();
2608 let theta = format!("{arch}.rope.freq_base");
2609 let kvs: Vec<(&str, Kv)> = vec![
2610 ("general.architecture", Kv::Str(arch)),
2611 (keys[0].as_str(), Kv::U32(1)),
2612 (keys[1].as_str(), Kv::U32(64)),
2613 (keys[2].as_str(), Kv::U32(1)),
2614 (keys[3].as_str(), Kv::U32(1)),
2615 (keys[4].as_str(), Kv::U32(64)),
2616 (theta.as_str(), Kv::F32(10_000.0)),
2617 ];
2618 ModelConfig::from_gguf(&open_metadata_gguf(arch, &kvs))
2619 }
2620
2621 #[test]
2629 fn an_unaudited_generic_architecture_refuses_rather_than_guessing() {
2630 assert!(
2638 !crate::capability::is_audited_generic("xverse"),
2639 "this test needs an arch that is generic AND unaudited"
2640 );
2641 match config_for_arch("xverse") {
2642 Err(LoadError::UnauditedArchitecture(name, ..)) => assert_eq!(name, "xverse"),
2643 other => panic!("expected an unaudited refusal, got {other:?}"),
2644 }
2645 }
2646
2647 #[test]
2650 fn an_audited_architecture_still_loads() {
2651 assert!(crate::capability::is_audited_generic("llama"));
2652 assert!(config_for_arch("llama").is_ok());
2653 }
2654
2655 #[test]
2662 fn a_named_refusal_outranks_the_unaudited_one() {
2663 let err = config_for_arch("gpt2").expect_err("gpt2 must refuse");
2664 assert!(
2665 !matches!(err, LoadError::UnauditedArchitecture(..)),
2666 "gpt2 should report its own reason, not that nobody audited it: {err:?}"
2667 );
2668 }
2669
2670 #[test]
2681 fn a_gguf_declaring_yarn_gets_its_rope_frequencies_rewritten() {
2682 let cfg = llama_config_with(
2683 "yarn",
2684 &[
2685 ("llama.rope.scaling.type", Kv::Str("yarn")),
2686 ("llama.rope.scaling.factor", Kv::F32(8.0)),
2687 (
2688 "llama.rope.scaling.original_context_length",
2689 Kv::U32(131_072),
2690 ),
2691 ],
2692 );
2693 let factors = cfg
2694 .rope_freqs
2695 .expect("a YaRN checkpoint must carry rewritten per-band frequencies");
2696 assert_eq!(factors.len(), 32, "one divisor per rotation band");
2697 assert!(
2698 (factors[0] - 1.0).abs() < 1e-6,
2699 "the fastest band is left extrapolated, got {}",
2700 factors[0]
2701 );
2702 let ramp = (31.0 - 22.0) / (35.0 - 22.0);
2703 let want = 1.0 / (ramp / 8.0 + (1.0 - ramp));
2704 assert!(
2705 (factors[31] - want).abs() < 1e-4,
2706 "slowest band: got {}, reference {want}",
2707 factors[31]
2708 );
2709 }
2710
2711 #[test]
2726 fn linear_scaling_is_applied_as_a_uniform_frequency_divisor() {
2727 let cfg = llama_config_with(
2728 "linear",
2729 &[
2730 ("llama.rope.scaling.type", Kv::Str("linear")),
2731 ("llama.rope.scaling.factor", Kv::F32(4.0)),
2732 ],
2733 );
2734 let freqs = cfg
2735 .rope_freqs
2736 .as_ref()
2737 .expect("linear scaling must produce frequency factors");
2738 assert_eq!(freqs.len(), cfg.head_dim / 2, "one factor per rotated pair");
2739 assert!(
2740 freqs.iter().all(|f| (*f - 4.0).abs() < 1e-6),
2741 "linear scaling is uniform across bands, unlike YaRN: got {freqs:?}"
2742 );
2743 }
2744
2745 #[test]
2747 fn a_linear_factor_of_one_is_treated_as_absent() {
2748 assert!(llama_config_with(
2749 "linear_one",
2750 &[
2751 ("llama.rope.scaling.type", Kv::Str("linear")),
2752 ("llama.rope.scaling.factor", Kv::F32(1.0)),
2753 ],
2754 )
2755 .rope_freqs
2756 .is_none());
2757 }
2758
2759 #[test]
2760 fn a_gguf_without_yarn_scaling_keeps_its_rope_frequencies_untouched() {
2761 assert!(llama_config_with("noscale", &[]).rope_freqs.is_none());
2762 assert!(llama_config_with(
2769 "yarn_factor_one",
2770 &[
2771 ("llama.rope.scaling.type", Kv::Str("yarn")),
2772 ("llama.rope.scaling.factor", Kv::F32(1.0)),
2773 (
2774 "llama.rope.scaling.original_context_length",
2775 Kv::U32(131_072),
2776 ),
2777 ],
2778 )
2779 .rope_freqs
2780 .is_none());
2781 }
2782
2783 #[test]
2790 fn yarn_without_an_original_context_length_is_not_guessed_at() {
2791 let cfg = llama_config_with(
2792 "yarn_noctx",
2793 &[
2794 ("llama.rope.scaling.type", Kv::Str("yarn")),
2795 ("llama.rope.scaling.factor", Kv::F32(8.0)),
2796 ],
2797 );
2798 assert!(cfg.rope_freqs.is_none());
2799 }
2800
2801 #[test]
2806 fn gguf_sampling_metadata_is_read_as_the_checkpoints_recommendation() {
2807 use crate::sampling::RecommendedSampling;
2808 let full = RecommendedSampling::from_gguf(&open_metadata_gguf(
2809 "sampling_full",
2810 &[
2811 ("general.architecture", Kv::Str("llama")),
2812 ("general.sampling.temp", Kv::F32(1.0)),
2813 ("general.sampling.top_k", Kv::U32(20)),
2814 ("general.sampling.top_p", Kv::F32(0.95)),
2815 ],
2816 ));
2817 assert_eq!(
2818 full,
2819 RecommendedSampling {
2820 temperature: Some(1.0),
2821 top_p: Some(0.95),
2822 top_k: Some(20),
2823 }
2824 );
2825
2826 let partial = RecommendedSampling::from_gguf(&open_metadata_gguf(
2827 "sampling_partial",
2828 &[
2829 ("general.architecture", Kv::Str("llama")),
2830 ("general.sampling.top_k", Kv::U32(40)),
2831 ],
2832 ));
2833 assert_eq!(partial.top_k, Some(40));
2834 assert_eq!(partial.temperature, None);
2835 assert_eq!(partial.top_p, None);
2836 }
2837
2838 #[test]
2843 fn an_integer_valued_sampling_temp_is_still_a_recommendation() {
2844 let recommended = crate::sampling::RecommendedSampling::from_gguf(&open_metadata_gguf(
2845 "sampling_int_temp",
2846 &[
2847 ("general.architecture", Kv::Str("llama")),
2848 ("general.sampling.temp", Kv::U32(1)),
2849 ],
2850 ));
2851 assert_eq!(recommended.temperature, Some(1.0));
2852 }
2853
2854 #[test]
2857 fn a_gguf_without_sampling_metadata_recommends_nothing() {
2858 let recommended = crate::sampling::RecommendedSampling::from_gguf(&open_metadata_gguf(
2859 "sampling_absent",
2860 &[("general.architecture", Kv::Str("llama"))],
2861 ));
2862 assert!(recommended.is_empty());
2863 }
2864
2865 #[test]
2866 fn model_config_from_gguf_rejects_dedicated_architectures() {
2867 let tmp = std::env::temp_dir().join(format!(
2868 "ferrox_test_dedicated_arch_{}.gguf",
2869 std::process::id()
2870 ));
2871 std::fs::write(&tmp, build_arch_only_gguf("deepseek4")).unwrap();
2872 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2873 std::fs::remove_file(&tmp).ok();
2874
2875 match ModelConfig::from_gguf(&file) {
2876 Err(LoadError::DedicatedArchitectureRequired(arch, _)) => {
2877 assert_eq!(arch, "deepseek4");
2878 }
2879 other => panic!(
2880 "expected LoadError::DedicatedArchitectureRequired for deepseek4, got {other:?}"
2881 ),
2882 }
2883 }
2884
2885 #[rustfmt::skip]
2889 const Q5_K_TEST_BLOCK: [u8; 176] = [
2890 0x66, 0x2a, 0x66, 0x2a, 0x01, 0x01, 0x01, 0x01, 0x4f, 0x4b, 0x10, 0x12, 0x41, 0xe2, 0xc1,
2891 0xb1, 0x72, 0x2f, 0x20, 0x07, 0x31, 0x0c, 0x38, 0xb3, 0x9c, 0xb8, 0xad, 0x2f, 0x9a, 0xea,
2892 0x17, 0xd0, 0xee, 0x93, 0x9e, 0x3e, 0x74, 0xbb, 0x28, 0x18, 0x39, 0x25, 0xb6, 0x09, 0x18,
2893 0x29, 0x1c, 0x1d, 0x29, 0x41, 0x40, 0x0a, 0x74, 0x7d, 0xfd, 0x21, 0xdd, 0x6d, 0x45, 0x73,
2894 0x0e, 0x1e, 0xc0, 0x4a, 0xfc, 0xf3, 0x8e, 0x24, 0x6b, 0x34, 0x7d, 0xbe, 0x94, 0xde, 0x59,
2895 0x7a, 0x35, 0x30, 0x36, 0x0a, 0xf9, 0x4a, 0x9b, 0xa2, 0x26, 0x21, 0xa2, 0xfa, 0xdf, 0x4b,
2896 0x29, 0x64, 0x6f, 0xbb, 0xca, 0x0f, 0x3c, 0xda, 0x20, 0xf4, 0x93, 0x86, 0xab, 0x6e, 0xb9,
2897 0xe5, 0xd5, 0xa0, 0x82, 0xd6, 0x41, 0xff, 0x12, 0xbc, 0x34, 0xbb, 0xab, 0xb8, 0x20, 0x2f,
2898 0xbb, 0x5f, 0x0c, 0x10, 0xcf, 0x49, 0xc5, 0x86, 0x5c, 0xdf, 0xff, 0x78, 0x44, 0x26, 0x3b,
2899 0xc2, 0x23, 0x3d, 0x2b, 0xe9, 0x00, 0x12, 0xf8, 0xea, 0xe2, 0x9e, 0x5e, 0x50, 0x20, 0x9f,
2900 0x9d, 0x8d, 0x7d, 0x7f, 0xcc, 0x1d, 0x0e, 0x13, 0xf8, 0xc2, 0xf1, 0x3d, 0x08, 0x2f, 0x23,
2901 0x13, 0xac, 0x0d, 0xa7, 0xe7, 0x20, 0xa3, 0x90, 0xb7, 0xc8, 0x28,
2902 ];
2903
2904 fn build_single_q5_k_tensor_gguf() -> Vec<u8> {
2905 let mut buf = Vec::new();
2906 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2907 .unwrap();
2908 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");
2913
2914 write_string(&mut buf, "test.weight");
2915 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 {
2925 buf.push(0);
2926 }
2927 buf.extend_from_slice(&Q5_K_TEST_BLOCK);
2928 buf
2929 }
2930
2931 fn fused_dot_tolerance(weights: &[f32], x: &[f32], exact_bound: f32) -> f32 {
2951 if !ferrox_core::weight_matrix::cpu_int_dot_enabled() {
2952 return exact_bound;
2953 }
2954 let amax = x.iter().fold(0.0f32, |a, v| a.max(v.abs()));
2955 let l2 = weights.iter().map(|w| w * w).sum::<f32>().sqrt();
2956 4.0 * (amax / 127.0) / 12f32.sqrt() * l2 + exact_bound
2957 }
2958
2959 #[test]
2960 fn load_weight_matrix_handles_a_real_on_disk_q5_k_tensor_end_to_end() {
2961 let tmp = std::env::temp_dir().join(format!(
2962 "ferrox_test_q5k_tensor_{}.gguf",
2963 std::process::id()
2964 ));
2965 std::fs::write(&tmp, build_single_q5_k_tensor_gguf()).unwrap();
2966 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q5_K GGUF file must parse");
2967 std::fs::remove_file(&tmp).ok();
2968
2969 let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_K tensor must load");
2970 assert_eq!(matrix.rows(), 1);
2971 assert_eq!(matrix.cols(), 256);
2972 match &matrix {
2973 WeightMatrix::Quantized { kind, data, .. } => {
2974 assert_eq!(*kind, QuantKind::Q5K);
2975 assert!(
2976 data.is_mapped(),
2977 "Q5_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
2978 );
2979 }
2980 _ => panic!("expected a Quantized matrix for a Q5_K tensor"),
2981 }
2982
2983 let expected = ferrox_quant::dequant_q5_k(&Q5_K_TEST_BLOCK).unwrap();
2984 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
2985 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
2986
2987 let got = matrix.apply(&x);
2988 assert_eq!(got.len(), 1);
2989 assert!(
2990 (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-2),
2991 "end-to-end loaded+applied Q5_K matrix diverged from direct dequant: got={} expected={}",
2992 got[0],
2993 expected_dot
2994 );
2995 }
2996
2997 #[rustfmt::skip]
3006 const Q6_K_TEST_BLOCK: [u8; 210] = [
3007 0xe0, 0xa5, 0x40, 0x5c, 0x8d, 0x3a, 0x0a, 0x26, 0xfb, 0x4b, 0x6e, 0x9a, 0xdf, 0x3e, 0xa3,
3008 0xc4, 0xf8, 0x2b, 0x1d, 0x95, 0x76, 0x7d, 0x3b, 0xcd, 0xfd, 0xef, 0xc2, 0x0b, 0x07, 0x63,
3009 0x29, 0xfb, 0x81, 0x57, 0xbe, 0xbe, 0x06, 0xf7, 0x3a, 0x92, 0xc4, 0x43, 0xff, 0xad, 0xac,
3010 0x7e, 0x0f, 0x00, 0x2a, 0x4f, 0xf0, 0xf8, 0xa9, 0xfa, 0x3c, 0x90, 0x6d, 0x73, 0x2d, 0x5a,
3011 0xe6, 0xc6, 0x46, 0xf2, 0x0d, 0x55, 0x4c, 0x25, 0x38, 0x71, 0x2b, 0x35, 0x38, 0x82, 0x16,
3012 0x37, 0x5f, 0x32, 0x61, 0x02, 0xdd, 0x2f, 0x6f, 0x7b, 0x1f, 0xb4, 0x1a, 0x1b, 0x3e, 0x4f,
3013 0x11, 0xa3, 0x17, 0x40, 0x5a, 0x5f, 0x76, 0xcd, 0x19, 0x27, 0x9b, 0xc7, 0xc8, 0xf7, 0xf7,
3014 0xee, 0xf4, 0x86, 0xd9, 0xfd, 0xa7, 0xfe, 0x9e, 0xac, 0x70, 0x53, 0x5b, 0x76, 0xfb, 0x39,
3015 0xf8, 0x4b, 0x98, 0xfe, 0xd0, 0x06, 0x21, 0x4c, 0x4d, 0xbe, 0x10, 0x2b, 0x06, 0x65, 0xc9,
3016 0x5e, 0xf9, 0x95, 0x72, 0xae, 0x99, 0xd9, 0x7e, 0x15, 0xbd, 0x5e, 0x6d, 0xe8, 0x25, 0x8a,
3017 0xd5, 0x99, 0xc6, 0x6b, 0x69, 0xc7, 0x84, 0xc6, 0xa4, 0xf7, 0xb9, 0x6d, 0x68, 0x45, 0x0e,
3018 0x65, 0x69, 0xeb, 0xe6, 0xeb, 0xe9, 0x28, 0xa6, 0xb9, 0x96, 0xf2, 0xe8, 0xa7, 0x9b, 0x6e,
3019 0x79, 0x8a, 0x68, 0x65, 0x59, 0x98, 0x8b, 0x44, 0x41, 0x98, 0x9a, 0x56, 0x01, 0x01, 0x01,
3020 0x02, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, 0x01, 0x01, 0x01, 0x02, 0x1f, 0x25,
3021 ];
3022
3023 fn build_single_q6_k_tensor_gguf() -> Vec<u8> {
3024 let mut buf = Vec::new();
3025 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3026 .unwrap();
3027 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");
3032
3033 write_string(&mut buf, "test.weight");
3034 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 {
3042 buf.push(0);
3043 }
3044 buf.extend_from_slice(&Q6_K_TEST_BLOCK);
3045 buf
3046 }
3047
3048 #[test]
3049 fn load_weight_matrix_handles_a_real_on_disk_q6_k_tensor_end_to_end() {
3050 let tmp = std::env::temp_dir().join(format!(
3051 "ferrox_test_q6k_tensor_{}.gguf",
3052 std::process::id()
3053 ));
3054 std::fs::write(&tmp, build_single_q6_k_tensor_gguf()).unwrap();
3055 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q6_K GGUF file must parse");
3056 std::fs::remove_file(&tmp).ok();
3057
3058 let matrix = load_weight_matrix(&file, "test.weight").expect("Q6_K tensor must load");
3059 assert_eq!(matrix.rows(), 1);
3060 assert_eq!(matrix.cols(), 256);
3061 match &matrix {
3062 WeightMatrix::Quantized { kind, data, .. } => {
3063 assert_eq!(*kind, QuantKind::Q6K);
3064 assert!(
3065 data.is_mapped(),
3066 "Q6_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
3067 );
3068 }
3069 _ => panic!("expected a Quantized matrix for a Q6_K tensor"),
3070 }
3071
3072 let expected = ferrox_quant::dequant_q6_k(&Q6_K_TEST_BLOCK).unwrap();
3073 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3074 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3075
3076 let got = matrix.apply(&x);
3077 assert_eq!(got.len(), 1);
3078 assert!(
3079 (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-2),
3080 "end-to-end loaded+applied Q6_K matrix diverged from direct dequant: got={} expected={}",
3081 got[0],
3082 expected_dot
3083 );
3084 }
3085
3086 fn build_single_bf16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
3087 let mut buf = Vec::new();
3088 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3089 .unwrap();
3090 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");
3095
3096 write_string(&mut buf, "test.weight");
3097 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(cols).unwrap();
3100 buf.write_u64::<LittleEndian>(rows).unwrap();
3101 buf.write_u32::<LittleEndian>(30).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
3105 buf.push(0);
3106 }
3107 for &v in values {
3108 let bf16_bits = (v.to_bits() >> 16) as u16;
3112 buf.extend_from_slice(&bf16_bits.to_le_bytes());
3113 }
3114 buf
3115 }
3116
3117 #[test]
3118 fn load_weight_matrix_handles_a_real_on_disk_bf16_tensor_end_to_end() {
3119 let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
3122 let tmp = std::env::temp_dir().join(format!(
3123 "ferrox_test_bf16_tensor_{}.gguf",
3124 std::process::id()
3125 ));
3126 std::fs::write(&tmp, build_single_bf16_tensor_gguf(2, 3, &values)).unwrap();
3127 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real BF16 GGUF file must parse");
3128 std::fs::remove_file(&tmp).ok();
3129
3130 let matrix = load_weight_matrix(&file, "test.weight").expect("BF16 tensor must load");
3131 assert_eq!(matrix.rows(), 2);
3132 assert_eq!(matrix.cols(), 3);
3133 match &matrix {
3134 WeightMatrix::F32(tensor) => {
3135 assert_eq!(tensor.data, values, "BF16 must widen to f32 exactly");
3136 }
3137 _ => panic!("expected an F32 matrix for a BF16 tensor (no fused dot kernel for it)"),
3138 }
3139 }
3140
3141 fn build_single_f16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
3142 let mut buf = Vec::new();
3143 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3144 .unwrap();
3145 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");
3150
3151 write_string(&mut buf, "test.weight");
3152 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(cols).unwrap();
3154 buf.write_u64::<LittleEndian>(rows).unwrap();
3155 buf.write_u32::<LittleEndian>(1).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
3159 buf.push(0);
3160 }
3161 for &v in values {
3162 buf.extend_from_slice(&half::f16::from_f32(v).to_le_bytes());
3163 }
3164 buf
3165 }
3166
3167 #[test]
3172 fn load_weight_matrix_handles_a_real_on_disk_f16_tensor_end_to_end() {
3173 let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
3174 let tmp = std::env::temp_dir().join(format!(
3175 "ferrox_test_f16_tensor_{}.gguf",
3176 std::process::id()
3177 ));
3178 std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
3179 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
3180 std::fs::remove_file(&tmp).ok();
3181
3182 let matrix = load_weight_matrix(&file, "test.weight").expect("F16 tensor must load");
3183 assert_eq!(matrix.rows(), 2);
3184 assert_eq!(matrix.cols(), 3);
3185 match &matrix {
3186 WeightMatrix::F32(tensor) => {
3187 assert_eq!(tensor.data, values, "F16 must widen to f32 exactly");
3188 }
3189 _ => panic!("expected an F32 matrix for an F16 tensor (no fused dot kernel for it)"),
3190 }
3191
3192 let tmp =
3195 std::env::temp_dir().join(format!("ferrox_test_f16_vec_{}.gguf", std::process::id()));
3196 std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
3197 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
3198 std::fs::remove_file(&tmp).ok();
3199 assert_eq!(load_f32_vec(&file, "test.weight").unwrap(), values);
3200 }
3201
3202 fn build_single_q5_1_tensor_gguf() -> Vec<u8> {
3203 let mut buf = Vec::new();
3204 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3205 .unwrap();
3206 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");
3211
3212 write_string(&mut buf, "test.weight");
3213 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 {
3221 buf.push(0);
3222 }
3223 buf.extend_from_slice(&0x3400u16.to_le_bytes());
3228 buf.extend_from_slice(&0x3E00u16.to_le_bytes());
3229 buf.extend_from_slice(&[0x9au8, 0x3c, 0xf0, 0x0f]);
3230 buf.extend_from_slice(&(0..16u8).map(|i| i | ((15 - i) << 4)).collect::<Vec<u8>>());
3231 buf
3232 }
3233
3234 #[test]
3235 fn load_weight_matrix_handles_a_real_on_disk_q5_1_tensor_end_to_end() {
3236 let tmp = std::env::temp_dir().join(format!(
3237 "ferrox_test_q5_1_tensor_{}.gguf",
3238 std::process::id()
3239 ));
3240 std::fs::write(&tmp, build_single_q5_1_tensor_gguf()).unwrap();
3241 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q5_1 GGUF file must parse");
3242 std::fs::remove_file(&tmp).ok();
3243
3244 let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_1 tensor must load");
3245 assert_eq!(matrix.rows(), 1);
3246 assert_eq!(matrix.cols(), 32);
3247 let raw = file.tensor_bytes("test.weight").unwrap();
3248 let expected = ferrox_quant::dequant_q5_1(raw).unwrap();
3249 match &matrix {
3250 WeightMatrix::Quantized { kind, data, .. } => {
3251 assert_eq!(*kind, QuantKind::Q5_1);
3252 assert!(data.is_mapped());
3253 }
3254 _ => panic!("expected a Quantized matrix for a Q5_1 tensor"),
3255 }
3256
3257 let x: Vec<f32> = (0..32).map(|i| ((i as f32) * 0.017).cos()).collect();
3258 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3259 let got = matrix.apply(&x);
3260 assert_eq!(got.len(), 1);
3261 assert!(
3262 (got[0] - expected_dot).abs() < 1e-2,
3263 "end-to-end loaded+applied Q5_1 matrix diverged from direct dequant: got={} expected={}",
3264 got[0],
3265 expected_dot
3266 );
3267 }
3268
3269 const Q3_K_TEST_BLOCK: [u8; 110] = [
3274 0x56, 0xf2, 0xb4, 0x2b, 0xd5, 0x6f, 0x51, 0x71, 0x3c, 0x0a, 0xb9, 0x1d, 0xd0, 0xb9, 0x3b,
3275 0xb3, 0x0f, 0xff, 0x8c, 0xb2, 0x83, 0x3a, 0x3d, 0x24, 0xb1, 0x12, 0x56, 0xe3, 0x23, 0x54,
3276 0xf2, 0xfa, 0x7f, 0xdf, 0x31, 0xe1, 0x18, 0x26, 0x6e, 0xcd, 0x5b, 0x38, 0xee, 0xbd, 0x9f,
3277 0x8c, 0x57, 0x47, 0x0b, 0x11, 0xcb, 0xfb, 0xb4, 0x83, 0xa0, 0x4e, 0x0b, 0xd4, 0xa7, 0x85,
3278 0xe0, 0x60, 0xf3, 0xb3, 0xe3, 0x95, 0x43, 0xc6, 0x05, 0x05, 0x77, 0x53, 0xed, 0x23, 0xcc,
3279 0x6a, 0x0e, 0x89, 0xa1, 0x79, 0x85, 0xf6, 0x6e, 0x5a, 0x23, 0x63, 0xbe, 0x53, 0xfa, 0xa2,
3280 0x2b, 0xe9, 0xcd, 0xce, 0xf8, 0x3d, 0x6f, 0xd0, 0x42, 0x6e, 0x3b, 0x7f, 0x23, 0x26, 0xd3,
3281 0xb9, 0x18, 0xbf, 0xa4, 0x34,
3282 ];
3283
3284 fn build_single_q3_k_tensor_gguf() -> Vec<u8> {
3285 let mut buf = Vec::new();
3286 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3287 .unwrap();
3288 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");
3293
3294 write_string(&mut buf, "test.weight");
3295 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 {
3303 buf.push(0);
3304 }
3305 buf.extend_from_slice(&Q3_K_TEST_BLOCK);
3306 buf
3307 }
3308
3309 #[test]
3310 fn load_weight_matrix_handles_a_real_on_disk_q3_k_tensor_end_to_end() {
3311 let tmp = std::env::temp_dir().join(format!(
3312 "ferrox_test_q3k_tensor_{}.gguf",
3313 std::process::id()
3314 ));
3315 std::fs::write(&tmp, build_single_q3_k_tensor_gguf()).unwrap();
3316 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q3_K GGUF file must parse");
3317 std::fs::remove_file(&tmp).ok();
3318
3319 let matrix = load_weight_matrix(&file, "test.weight").expect("Q3_K tensor must load");
3320 assert_eq!(matrix.rows(), 1);
3321 assert_eq!(matrix.cols(), 256);
3322 match &matrix {
3323 WeightMatrix::Quantized { kind, data, .. } => {
3324 assert_eq!(*kind, QuantKind::Q3K);
3325 assert!(data.is_mapped());
3326 }
3327 _ => panic!("expected a Quantized matrix for a Q3_K tensor"),
3328 }
3329
3330 let expected = ferrox_quant::dequant_q3_k(&Q3_K_TEST_BLOCK).unwrap();
3331 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3332 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3333
3334 let got = matrix.apply(&x);
3335 assert_eq!(got.len(), 1);
3336 assert!(
3337 (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-1),
3338 "end-to-end loaded+applied Q3_K matrix diverged from direct dequant: got={} expected={}",
3339 got[0],
3340 expected_dot
3341 );
3342 }
3343
3344 const IQ4_XS_TEST_BLOCK: [u8; 136] = [
3348 0x5c, 0x33, 0xb4, 0x39, 0xd1, 0x64, 0x97, 0x82, 0xcb, 0xbd, 0x88, 0x95, 0xf3, 0x60, 0x2a,
3349 0xb5, 0xe7, 0x24, 0xd3, 0xee, 0xfe, 0x71, 0x13, 0xbe, 0x70, 0x84, 0x48, 0x79, 0x7b, 0x3e,
3350 0xf0, 0x55, 0xdc, 0xb2, 0xb2, 0xde, 0x32, 0xa1, 0x5b, 0x02, 0x01, 0xdc, 0x2a, 0xbb, 0xf7,
3351 0x0b, 0x8a, 0x88, 0xdd, 0x0b, 0x02, 0x7e, 0x5e, 0x76, 0x87, 0x30, 0x1e, 0x1c, 0xcf, 0x48,
3352 0xd7, 0x61, 0xf3, 0x51, 0x52, 0x17, 0x98, 0x0a, 0x87, 0xcf, 0x02, 0x91, 0xc8, 0xee, 0xc0,
3353 0x91, 0x69, 0x2a, 0x4f, 0x64, 0x68, 0xa7, 0xb2, 0xe6, 0x98, 0x21, 0x81, 0x75, 0x53, 0x2a,
3354 0x8d, 0x12, 0xae, 0xe0, 0xea, 0x0c, 0x75, 0xff, 0x22, 0x5e, 0x25, 0x19, 0xda, 0x2e, 0x51,
3355 0x4e, 0x81, 0xdc, 0x0e, 0x78, 0x86, 0xd7, 0x58, 0xb5, 0xb7, 0xf6, 0x45, 0xa9, 0x0a, 0x83,
3356 0xfd, 0x2a, 0x12, 0x7d, 0xf0, 0x12, 0x97, 0xe2, 0xfe, 0xf4, 0xd0, 0xa2, 0x11, 0x14, 0x78,
3357 0xdb,
3358 ];
3359
3360 fn build_single_iq4_xs_tensor_gguf() -> 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-iq4xs-test");
3369
3370 write_string(&mut buf, "test.weight");
3371 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 {
3379 buf.push(0);
3380 }
3381 buf.extend_from_slice(&IQ4_XS_TEST_BLOCK);
3382 buf
3383 }
3384
3385 #[test]
3386 fn load_weight_matrix_handles_a_real_on_disk_iq4_xs_tensor_end_to_end() {
3387 let tmp = std::env::temp_dir().join(format!(
3388 "ferrox_test_iq4xs_tensor_{}.gguf",
3389 std::process::id()
3390 ));
3391 std::fs::write(&tmp, build_single_iq4_xs_tensor_gguf()).unwrap();
3392 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real IQ4_XS GGUF file must parse");
3393 std::fs::remove_file(&tmp).ok();
3394
3395 let matrix = load_weight_matrix(&file, "test.weight").expect("IQ4_XS tensor must load");
3396 assert_eq!(matrix.rows(), 1);
3397 assert_eq!(matrix.cols(), 256);
3398 match &matrix {
3399 WeightMatrix::Quantized { kind, data, .. } => {
3400 assert_eq!(*kind, QuantKind::IQ4XS);
3401 assert!(data.is_mapped());
3402 }
3403 _ => panic!("expected a Quantized matrix for an IQ4_XS tensor"),
3404 }
3405
3406 let expected = ferrox_quant::dequant_iq4_xs(&IQ4_XS_TEST_BLOCK).unwrap();
3407 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3408 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3409
3410 let got = matrix.apply(&x);
3411 assert_eq!(got.len(), 1);
3412 assert!(
3413 (got[0] - expected_dot).abs() < 1e-1,
3414 "end-to-end loaded+applied IQ4_XS matrix diverged from direct dequant: got={} expected={}",
3415 got[0],
3416 expected_dot
3417 );
3418 }
3419
3420 const IQ1_S_TEST_BLOCK: [u8; 50] = [
3425 0x0a, 0x2f, 0xfa, 0x06, 0x1e, 0x37, 0x6f, 0xe3, 0x62, 0xd0, 0xb6, 0xa4, 0x25, 0xae, 0x76,
3426 0x14, 0x72, 0x5b, 0xfa, 0x05, 0xd1, 0xf1, 0x2a, 0x4c, 0xad, 0x29, 0xae, 0xf4, 0xcf, 0x0c,
3427 0x96, 0x51, 0x58, 0x03, 0x6d, 0xd3, 0x10, 0x92, 0x70, 0xff, 0x61, 0x58, 0xc8, 0x30, 0x25,
3428 0x64, 0x49, 0x85, 0xc0, 0x24,
3429 ];
3430 const IQ2_XXS_TEST_BLOCK: [u8; 66] = [
3431 0x29, 0x30, 0xd9, 0x33, 0x95, 0x4c, 0x08, 0x1e, 0xad, 0x79, 0x49, 0xf2, 0x8d, 0x5f, 0x93,
3432 0xea, 0x78, 0x18, 0x98, 0xb9, 0x94, 0x14, 0xad, 0xce, 0xca, 0x1d, 0xab, 0x81, 0x53, 0x4a,
3433 0x68, 0xd0, 0x59, 0x96, 0x36, 0x5d, 0xbe, 0x20, 0xc4, 0xff, 0xe4, 0x2c, 0xcd, 0x2f, 0x4f,
3434 0x4f, 0x67, 0x53, 0xc6, 0xd5, 0xa2, 0xfb, 0xc7, 0xf3, 0xe2, 0x6b, 0xf1, 0x99, 0x23, 0x1e,
3435 0x2d, 0x5e, 0x8c, 0x78, 0xc2, 0x31,
3436 ];
3437 const IQ3_XXS_TEST_BLOCK: [u8; 98] = [
3438 0x71, 0x31, 0x16, 0x0a, 0x79, 0x04, 0x5d, 0x87, 0xae, 0x2a, 0x4a, 0x43, 0xfd, 0x02, 0xba,
3439 0x6c, 0x10, 0x42, 0x80, 0xe5, 0x1d, 0x08, 0x22, 0xcb, 0x21, 0x54, 0xf9, 0xaa, 0x8e, 0xc2,
3440 0xf2, 0x34, 0x66, 0x1e, 0x2a, 0xef, 0x19, 0xae, 0x48, 0x47, 0x29, 0xa0, 0x72, 0xd1, 0x31,
3441 0xc0, 0x65, 0x49, 0xde, 0x79, 0x32, 0xe6, 0x4d, 0xb6, 0x55, 0x3f, 0x4d, 0xf1, 0x18, 0xbb,
3442 0x18, 0x59, 0x4c, 0x31, 0xa3, 0xb2, 0x34, 0xdd, 0xf6, 0x4a, 0x91, 0x51, 0x3f, 0x3e, 0x40,
3443 0x69, 0xad, 0xbf, 0x1a, 0xd0, 0x05, 0xfb, 0xbe, 0x8b, 0x0b, 0xdd, 0xdf, 0x7d, 0x94, 0x74,
3444 0x92, 0x3e, 0xff, 0x04, 0x2a, 0xc4, 0xea, 0xc9,
3445 ];
3446
3447 #[rustfmt::skip]
3448 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];
3449
3450 #[test]
3460 fn a_recognized_but_unimplemented_ggml_type_refuses_by_name_after_sizing_correctly() {
3461 let block = pseudo_iq_block(66, 0x0720_5eed);
3463 let tmp =
3464 std::env::temp_dir().join(format!("ferrox_test_tq2_0_{}.gguf", std::process::id()));
3465 std::fs::write(
3466 &tmp,
3467 build_single_iq_lowbit_tensor_gguf("tq2test", 35, 256, &block),
3468 )
3469 .unwrap();
3470 let file = ferrox_gguf::GgufFile::open(&tmp).expect("a TQ2_0 file must still parse");
3471 std::fs::remove_file(&tmp).ok();
3472
3473 let info = file.find_tensor("test.weight").expect("tensor present");
3476 assert_eq!(info.dtype, GgmlType::TQ2_0);
3477 assert_eq!(info.byte_len(), Some(66));
3478 assert_eq!(
3479 file.tensor_bytes("test.weight").map(<[u8]>::len).ok(),
3480 Some(66)
3481 );
3482
3483 match load_weight_matrix(&file, "test.weight") {
3484 Err(LoadError::UnsupportedDtype(name, GgmlType::TQ2_0)) => {
3485 assert_eq!(name, "test.weight");
3486 }
3487 Err(other) => panic!("TQ2_0 must be refused by name, got {other:?}"),
3488 Ok(_) => panic!("TQ2_0 must be refused, not loaded as some other kind"),
3489 }
3490 }
3491
3492 #[test]
3502 fn an_mxfp4_one_dimensional_tensor_widens_instead_of_being_refused() {
3503 let expected = ferrox_quant::dequant_mxfp4_gguf(&MXFP4_GGUF_TEST_BLOCKS)
3504 .expect("the fixture blocks must dequantize");
3505 let cols = expected.len();
3506 let tmp = std::env::temp_dir().join(format!(
3507 "ferrox_test_mxfp4_norm_{}.gguf",
3508 std::process::id()
3509 ));
3510 std::fs::write(
3511 &tmp,
3512 build_single_iq_lowbit_tensor_gguf(
3513 "mxfp4norm",
3514 39,
3515 cols as u64,
3516 &MXFP4_GGUF_TEST_BLOCKS,
3517 ),
3518 )
3519 .unwrap();
3520 let file = ferrox_gguf::GgufFile::open(&tmp).expect("file must parse");
3521 std::fs::remove_file(&tmp).ok();
3522
3523 let got = load_f32_vec(&file, "test.weight")
3524 .expect("an MXFP4 norm must load, not report an unsupported dtype");
3525 assert_eq!(got, expected);
3526
3527 let direct = widen_plain_float(GgmlType::MXFP4, &MXFP4_GGUF_TEST_BLOCKS, "test.weight")
3531 .expect("widen_plain_float must widen MXFP4");
3532 assert_eq!(direct, expected);
3533
3534 match widen_plain_float(GgmlType::TQ2_0, &MXFP4_GGUF_TEST_BLOCKS, "test.weight") {
3538 Err(LoadError::UnsupportedDtype(name, GgmlType::TQ2_0)) => {
3539 assert_eq!(name, "test.weight");
3540 }
3541 other => panic!("TQ2_0 must be refused by name, got {other:?}"),
3542 }
3543 }
3544
3545 fn build_single_iq_lowbit_tensor_gguf(
3546 arch: &str,
3547 tag: u32,
3548 cols: u64,
3549 block: &[u8],
3550 ) -> Vec<u8> {
3551 let mut buf = Vec::new();
3552 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3553 .unwrap();
3554 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);
3558 write_string(&mut buf, "test.weight");
3559 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(cols).unwrap();
3561 buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u32::<LittleEndian>(tag).unwrap();
3563 buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
3565 buf.push(0);
3566 }
3567 buf.extend_from_slice(block);
3568 buf
3569 }
3570
3571 fn pseudo_iq_block(len: usize, seed: u32) -> Vec<u8> {
3578 let mut s = seed;
3579 let mut out = Vec::with_capacity(len);
3580 for _ in 0..len {
3581 s ^= s << 13;
3582 s ^= s >> 17;
3583 s ^= s << 5;
3584 out.push((s >> 24) as u8);
3585 }
3586 out
3587 }
3588
3589 #[test]
3600 fn load_weight_matrix_handles_real_on_disk_iq_lowbit_tensors_end_to_end() {
3601 type DequantFn = fn(&[u8]) -> Result<Vec<f32>, ferrox_quant::QuantError>;
3602 let mut iq1m = pseudo_iq_block(ferrox_quant::IQ1_M_BLOCK_BYTES, 0x2907_31A0);
3609 iq1m[55] = (iq1m[55] & 0x0F) | 0x20;
3610 let mut iq2xs = pseudo_iq_block(ferrox_quant::IQ2_XS_BLOCK_BYTES, 0x2107_31A1);
3611 let mut iq2s = pseudo_iq_block(ferrox_quant::IQ2_S_BLOCK_BYTES, 0x2207_31A2);
3612 let mut iq3s = pseudo_iq_block(ferrox_quant::IQ3_S_BLOCK_BYTES, 0x2307_31A3);
3613 for blk in [&mut iq2xs, &mut iq2s, &mut iq3s] {
3614 blk[0..2].copy_from_slice(&half::f16::from_f32(0.115).to_le_bytes());
3615 }
3616 let cases: [(&str, u32, &[u8], QuantKind, DequantFn); 8] = [
3617 (
3618 "iq1s",
3619 19,
3620 &IQ1_S_TEST_BLOCK,
3621 QuantKind::IQ1S,
3622 ferrox_quant::dequant_iq1_s,
3623 ),
3624 (
3625 "iq1m",
3626 29,
3627 &iq1m,
3628 QuantKind::IQ1M,
3629 ferrox_quant::dequant_iq1_m,
3630 ),
3631 (
3632 "iq2xxs",
3633 16,
3634 &IQ2_XXS_TEST_BLOCK,
3635 QuantKind::IQ2XXS,
3636 ferrox_quant::dequant_iq2_xxs,
3637 ),
3638 (
3639 "iq2xs",
3640 17,
3641 &iq2xs,
3642 QuantKind::IQ2XS,
3643 ferrox_quant::dequant_iq2_xs,
3644 ),
3645 (
3646 "iq2s",
3647 22,
3648 &iq2s,
3649 QuantKind::IQ2S,
3650 ferrox_quant::dequant_iq2_s,
3651 ),
3652 (
3653 "iq3xxs",
3654 18,
3655 &IQ3_XXS_TEST_BLOCK,
3656 QuantKind::IQ3XXS,
3657 ferrox_quant::dequant_iq3_xxs,
3658 ),
3659 (
3660 "iq3s",
3661 21,
3662 &iq3s,
3663 QuantKind::IQ3S,
3664 ferrox_quant::dequant_iq3_s,
3665 ),
3666 (
3667 "mxfp4_gguf",
3668 39,
3669 &MXFP4_GGUF_TEST_BLOCKS,
3670 QuantKind::Mxfp4Gguf,
3671 ferrox_quant::dequant_mxfp4_gguf,
3672 ),
3673 ];
3674 for (name, tag, block, kind, dequant) in cases {
3675 let expected = dequant(block).unwrap();
3676 let cols = expected.len();
3677 let tmp = std::env::temp_dir().join(format!("ferrox_test_{name}_tensor.gguf"));
3678 std::fs::write(
3679 &tmp,
3680 build_single_iq_lowbit_tensor_gguf(name, tag, cols as u64, block),
3681 )
3682 .unwrap();
3683 let file = ferrox_gguf::GgufFile::open(&tmp).expect("file must parse");
3684 std::fs::remove_file(&tmp).ok();
3685
3686 let matrix =
3687 load_weight_matrix(&file, "test.weight").expect("low-bit tensor must load");
3688 assert_eq!((matrix.rows(), matrix.cols()), (1, cols), "{name}");
3689 match &matrix {
3690 WeightMatrix::Quantized { kind: k, data, .. } => {
3691 assert_eq!(*k, kind, "{name}");
3692 assert!(data.is_mapped(), "{name} must load zero-copy");
3693 }
3694 _ => panic!("expected a Quantized matrix for {name}"),
3695 }
3696
3697 let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.013).sin()).collect();
3698 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3699 let got = matrix.apply(&x);
3700 assert!(
3701 (got[0] - expected_dot).abs() < 1e-1,
3702 "{name}: loaded+applied diverged from direct dequant: got={} expected={}",
3703 got[0],
3704 expected_dot
3705 );
3706 }
3707 }
3708
3709 #[test]
3710 fn qwen2moe_disables_topk_renorm() {
3711 assert!(
3712 NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"qwen2moe"),
3713 "qwen2moe must have norm_topk_prob=false (llama.cpp build_moe_ffn norm_w=false)"
3714 );
3715 }
3716
3717 #[test]
3728 fn an_architecture_with_no_rope_is_refused_by_name() {
3729 for arch in ["gpt2", "mpt", "refact", "bloom", "jais"] {
3730 let file = open_metadata_gguf(
3731 &format!("norope_{arch}"),
3732 &[("general.architecture", Kv::Str(arch))],
3733 );
3734 match ModelConfig::from_gguf(&file) {
3735 Err(LoadError::DedicatedArchitectureRequired(got, reason)) => {
3736 assert_eq!(got, arch);
3737 assert!(
3738 reason.contains("ALiBi") || reason.contains("position embeddings"),
3739 "{arch}: the refusal must name what is missing, got {reason:?}"
3740 );
3741 }
3742 other => panic!("{arch} must be refused, got {other:?}"),
3743 }
3744 }
3745 }
3746
3747 #[test]
3753 fn baichuan_13b_is_refused_because_it_uses_alibi_and_the_7b_is_not() {
3754 let thirteen_b = open_metadata_gguf(
3755 "baichuan13b",
3756 &[
3757 ("general.architecture", Kv::Str("baichuan")),
3758 ("baichuan.block_count", Kv::U32(40)),
3759 ],
3760 );
3761 match ModelConfig::from_gguf(&thirteen_b) {
3762 Err(LoadError::UnsupportedFeature(arch, msg)) => {
3763 assert_eq!(arch, "baichuan");
3764 assert!(msg.contains("ALiBi"), "{msg}");
3765 assert!(
3766 msg.contains("40"),
3767 "the refusal must name the layer count: {msg}"
3768 );
3769 }
3770 other => panic!("Baichuan-13B must be refused, got {other:?}"),
3771 }
3772
3773 let seven_b = open_metadata_gguf(
3777 "baichuan7b",
3778 &[
3779 ("general.architecture", Kv::Str("baichuan")),
3780 ("baichuan.block_count", Kv::U32(32)),
3781 ],
3782 );
3783 match ModelConfig::from_gguf(&seven_b) {
3784 Err(LoadError::MissingHparam(key)) => assert_eq!(key, "baichuan.embedding_length"),
3785 other => panic!("Baichuan-7B must pass the ALiBi gate, got {other:?}"),
3786 }
3787 }
3788}