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. 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),
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] = &["deepseek2", "glm4moe"];
100
101const NO_TOPK_RENORMALIZE_ARCHITECTURES: &[&str] = &["olmoe", "qwen2moe"];
120
121fn metadata_u64_any(file: &impl TensorSource, keys: &[String]) -> Option<u64> {
122 keys.iter().find_map(|k| file.metadata_u64(k))
123}
124
125fn metadata_f32_any(file: &impl TensorSource, keys: &[String]) -> Option<f32> {
126 keys.iter()
127 .find_map(|k| file.metadata(k).and_then(GgufValue::as_f32))
128}
129
130impl ModelConfig {
131 pub fn from_gguf(file: &impl TensorSource) -> Result<Self, LoadError> {
145 let arch = file
146 .metadata_str("general.architecture")
147 .ok_or_else(|| LoadError::MissingHparam("general.architecture".to_string()))?
148 .to_string();
149 let arch_profile = crate::capability::resolve_profile(&arch)
150 .ok_or_else(|| LoadError::UnsupportedArchitecture(arch.clone()))?;
151 let rope_layout = match arch_profile.path {
152 crate::capability::ArchPath::GenericGqa { rope }
153 | crate::capability::ArchPath::TestFixture { rope } => rope,
154 crate::capability::ArchPath::DedicatedOnly { reason } => {
155 return Err(LoadError::DedicatedArchitectureRequired(
156 arch.clone(),
157 reason,
158 ));
159 }
160 crate::capability::ArchPath::Deferred { reason } => {
161 return Err(LoadError::UnsupportedFeature(
162 arch.clone(),
163 format!("architecture deferred from Ferrox text-generation scope: {reason}"),
164 ));
165 }
166 };
167 let qk_norm_style = arch_profile.qk_norm;
168 for (meta_key, feature) in crate::capability::unsupported_feature_keys(&arch) {
169 if let Some(v) = metadata_f32_any(file, std::slice::from_ref(&meta_key)) {
170 if v > 0.0 {
171 return Err(LoadError::UnsupportedFeature(
172 arch.clone(),
173 format!("{feature} (metadata {meta_key}={v})"),
174 ));
175 }
176 }
177 if let Some(v) = metadata_u64_any(file, std::slice::from_ref(&meta_key)) {
178 if v > 0 {
179 return Err(LoadError::UnsupportedFeature(
180 arch.clone(),
181 feature.to_string(),
182 ));
183 }
184 }
185 }
186 for (meta_key, feature, no_op) in crate::capability::unsupported_scaling_keys(&arch) {
192 if let Some(v) = metadata_f32_any(file, std::slice::from_ref(&meta_key)) {
193 if (v - no_op).abs() > 1e-6 {
194 return Err(LoadError::UnsupportedFeature(
195 arch.clone(),
196 format!("{feature} (metadata {meta_key}={v})"),
197 ));
198 }
199 }
200 }
201 let key = |suffix: &str| format!("{arch}.{suffix}");
202
203 let name: &'static str = Box::leak(
204 file.metadata_str("general.name")
205 .unwrap_or(&arch)
206 .to_string()
207 .into_boxed_str(),
208 );
209
210 let n_layers =
211 file.metadata_u64(&key("block_count"))
212 .ok_or_else(|| LoadError::MissingHparam(key("block_count")))? as usize;
213 if arch == "baichuan" && n_layers == 40 {
224 return Err(LoadError::UnsupportedFeature(
225 arch.clone(),
226 "Baichuan-13B (block_count=40) uses ALiBi and no RoPE, decided by layer \
227 count with no GGUF key to declare it; the generic decoder would rotate \
228 every Q/K head instead. Baichuan-7B (block_count=32) is unaffected"
229 .to_string(),
230 ));
231 }
232 let hidden_dim = file
233 .metadata_u64(&key("embedding_length"))
234 .ok_or_else(|| LoadError::MissingHparam(key("embedding_length")))?
235 as usize;
236 let n_heads = file
237 .metadata_u64(&key("attention.head_count"))
238 .ok_or_else(|| LoadError::MissingHparam(key("attention.head_count")))?
239 as usize;
240
241 let mut best_effort_fields: Vec<&'static str> = Vec::new();
242
243 let n_kv_heads = file
244 .metadata_u64(&key("attention.head_count_kv"))
245 .map(|v| v as usize)
246 .unwrap_or_else(|| {
247 best_effort_fields.push("n_kv_heads (no attention.head_count_kv key; assumed equal to n_heads, i.e. plain MHA)");
248 n_heads
249 });
250 let head_dim = file
251 .metadata_u64(&key("attention.key_length"))
252 .map(|v| v as usize)
253 .unwrap_or_else(|| {
254 best_effort_fields.push(
255 "head_dim (no attention.key_length key; derived as hidden_dim / n_heads)",
256 );
257 hidden_dim / n_heads
258 });
259 let v_head_dim = file
260 .metadata_u64(&key("attention.value_length"))
261 .map(|v| v as usize)
262 .unwrap_or(head_dim);
263 if v_head_dim != head_dim {
264 return Err(LoadError::UnsupportedFeature(
265 arch.clone(),
266 format!(
267 "split K/V head dims (key_length={head_dim}, value_length={v_head_dim}); \
268 generic decoder requires equal head dims"
269 ),
270 ));
271 }
272 let vocab_size = file
273 .metadata("tokenizer.ggml.tokens")
274 .and_then(|v| match v {
275 GgufValue::Array(items) => Some(items.len()),
276 _ => None,
277 })
278 .or_else(|| file.metadata_u64(&key("vocab_size")).map(|v| v as usize))
279 .unwrap_or_else(|| {
280 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)");
281 file.find_tensor("output.weight")
286 .and_then(|t| t.shape.last().copied())
287 .unwrap_or(0) as usize
288 });
289 let rope_theta = metadata_f32_any(file, &[key("rope.freq_base")]).unwrap_or_else(|| {
290 best_effort_fields.push("rope_theta (no rope.freq_base key; defaulted to 10000.0)");
291 10000.0
292 });
293 let rms_norm_eps = metadata_f32_any(
294 file,
295 &[
296 key("attention.layer_norm_rms_epsilon"),
297 key("attention.layer_norm_epsilon"),
298 ],
299 )
300 .unwrap_or_else(|| {
301 best_effort_fields
302 .push("rms_norm_eps (no layer_norm_rms_epsilon key; defaulted to 1e-5)");
303 1e-5
304 });
305
306 let n_experts = metadata_u64_any(file, &[key("expert_count")]).unwrap_or(0) as usize;
307 let is_moe = n_experts > 1;
308
309 let n_experts_active = if is_moe {
310 metadata_u64_any(file, &[key("expert_used_count")]).unwrap_or_else(|| {
311 best_effort_fields
312 .push("moe.n_experts_active (no expert_used_count key; defaulted to 2)");
313 2
314 }) as usize
315 } else {
316 1
317 };
318 let n_shared_experts = match metadata_u64_any(file, &[key("expert_shared_count")]) {
324 Some(n) => n as usize,
325 None if is_moe && file.find_tensor("blk.0.ffn_gate_shexp.weight").is_some() => {
326 best_effort_fields.push(
327 "moe.n_shared_experts (no expert_shared_count; inferred 1 from blk.0.ffn_gate_shexp.weight)",
328 );
329 1
330 }
331 None => 0,
332 };
333 let feed_forward_length = metadata_u64_any(file, &[key("feed_forward_length")]);
339 let expert_ffn_dim = metadata_u64_any(file, &[key("expert_feed_forward_length")])
340 .or_else(|| {
341 feed_forward_length.map(|ff| {
342 if is_moe && n_experts_active > 0 {
343 ff / n_experts_active as u64
344 } else {
345 ff
346 }
347 })
348 })
349 .unwrap_or_else(|| {
350 best_effort_fields.push(
351 "moe.expert_ffn_dim (no expert_feed_forward_length/feed_forward_length; defaulted to 4x hidden_dim)",
352 );
353 (hidden_dim * 4) as u64
354 }) as usize;
355 let n_dense_leading_layers =
356 metadata_u64_any(file, &[key("leading_dense_block_count")]).unwrap_or(0) as usize;
357
358 let gating = match metadata_u64_any(file, &[key("expert_gating_func")]) {
364 Some(2) => GatingFunction::Sigmoid,
365 Some(1) => GatingFunction::Softmax,
366 _ => {
367 if SIGMOID_GATING_ARCHITECTURES.contains(&arch.as_str()) {
368 GatingFunction::Sigmoid
369 } else {
370 if is_moe {
371 best_effort_fields.push(
372 "moe.gating (no expert_gating_func key and architecture not in the known-sigmoid list; defaulted to softmax)",
373 );
374 }
375 GatingFunction::Softmax
376 }
377 }
378 };
379
380 let norm_topk_prob = match file.metadata_bool(&key("expert_weights_norm")) {
387 Some(v) => v,
388 None => {
389 if is_moe && matches!(gating, GatingFunction::Softmax) {
393 best_effort_fields.push(
394 "moe.norm_topk_prob (no expert_weights_norm key; defaulted by architecture-name lookup against NO_TOPK_RENORMALIZE_ARCHITECTURES)",
395 );
396 }
397 !NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&arch.as_str())
398 }
399 };
400
401 let expert_weights_scale = metadata_f32_any(file, &[key("expert_weights_scale")])
405 .filter(|s| *s != 0.0)
406 .unwrap_or(1.0);
407
408 let sliding_window = metadata_u64_any(file, &[key("attention.sliding_window")])
418 .map(|v| v as usize)
419 .filter(|&w| w > 0);
420
421 let swa_pattern = metadata_u64_any(file, &[key("attention.sliding_window_pattern")])
425 .map(|v| v as usize)
426 .filter(|&p| p > 1)
427 .or_else(|| {
428 sliding_window?;
429 crate::capability::default_swa_pattern(&arch).or(
434 match arch_profile.family {
437 crate::capability::DecoderFamily::GemmaFamily => Some(6),
438 _ => None,
439 },
440 )
441 });
442
443 let attn_logit_softcap = metadata_f32_any(
444 file,
445 &[
446 key("attention.logit_softcapping"),
447 key("attn_logit_softcapping"),
448 ],
449 )
450 .filter(|&v| v > 0.0);
451 let final_logit_softcap =
452 metadata_f32_any(file, &[key("final_logit_softcapping")]).filter(|&v| v > 0.0);
453
454 let embedding_scale = if matches!(
456 arch_profile.family,
457 crate::capability::DecoderFamily::GemmaFamily
458 ) {
459 Some((hidden_dim as f32).sqrt())
460 } else {
461 None
462 };
463
464 let attention_scale = None;
469
470 let rope_theta_swa = if sliding_window.is_some() {
475 let fallback = if crate::capability::swa_rope_base_follows_model(&arch) {
476 rope_theta
477 } else {
478 10_000.0
479 };
480 Some(
481 metadata_f32_any(
482 file,
483 &[key("rope.freq_base_swa"), key("rope_freq_base_swa")],
484 )
485 .unwrap_or(fallback),
486 )
487 } else {
488 None
489 };
490
491 let ffn_activation = match arch_profile.family {
492 crate::capability::DecoderFamily::GemmaFamily => crate::config::FfnActivation::Gelu,
493 crate::capability::DecoderFamily::PhiFamily => {
494 crate::config::FfnActivation::SwigluFused
495 }
496 _ => crate::config::FfnActivation::Swiglu,
497 };
498
499 let rope_freqs = load_f32_vec_optional(file, "rope_freqs.weight")?;
506
507 let rope_orig_ctx = metadata_u64_any(file, &[key("rope.scaling.original_context_length")])
520 .map(|v| v as usize);
521 let (rope_freqs_long, rope_freqs_short) = if rope_freqs.is_some() {
526 (None, None)
527 } else {
528 (
529 load_f32_vec_optional(file, "rope_factors_long.weight")?,
530 load_f32_vec_optional(file, "rope_factors_short.weight")?,
531 )
532 };
533 let rope_freqs = match (rope_freqs, rope_orig_ctx) {
537 (Some(f), _) => Some(f),
538 (None, Some(orig)) => {
539 let model_ctx = metadata_u64_any(file, &[key("context_length")])
540 .unwrap_or(orig as u64) as usize;
541 if model_ctx > orig {
542 rope_freqs_long.clone().or_else(|| rope_freqs_short.clone())
543 } else {
544 rope_freqs_short.clone().or_else(|| rope_freqs_long.clone())
545 }
546 }
547 (None, None) => None,
548 };
549
550 let rope_dim = metadata_u64_any(file, &[key("rope.dimension_count")])
555 .map(|d| d as usize)
556 .filter(|d| *d > 0 && *d < head_dim);
557
558 let rope_attn_factor = metadata_f32_any(file, &[key("rope.scaling.attn_factor")])
560 .filter(|f| f.is_finite() && *f > 0.0)
561 .unwrap_or(1.0);
562
563 let rope_freqs = match linear_scaling_from_gguf(file, &arch) {
596 None => rope_freqs,
597 Some(factor) => {
598 let rotary_dim = rope_dim.unwrap_or(head_dim);
599 if rotary_dim == 0 || !rotary_dim.is_multiple_of(2) {
600 best_effort_fields.push(
601 "rope_freqs (linear scaling declared but the rotary width is odd; \
602 scaling not applied)",
603 );
604 rope_freqs
605 } else {
606 let linear = vec![factor; rotary_dim / 2];
607 match rope_freqs {
608 None => Some(linear),
609 Some(own) if own.len() == linear.len() => {
613 Some(own.iter().zip(linear.iter()).map(|(a, b)| a * b).collect())
614 }
615 Some(own) => {
616 best_effort_fields.push(
617 "rope_freqs (linear scaling declared but the file's own \
618 rope_freqs tensor has a different width; scaling not applied)",
619 );
620 Some(own)
621 }
622 }
623 }
624 }
625 };
626 let rope_freqs = match yarn_scaling_from_gguf(file, &arch, rope_orig_ctx) {
627 None => rope_freqs,
628 Some(scaling) => {
629 let rotary_dim = rope_dim.unwrap_or(head_dim);
630 if rotary_dim == 0 || !rotary_dim.is_multiple_of(2) {
631 best_effort_fields.push(
632 "rope_freqs (YaRN declared but the rotary width is odd; scaling not applied)",
633 );
634 rope_freqs
635 } else {
636 let yarn =
637 ferrox_core::attention::yarn_freq_factors(scaling, rotary_dim, rope_theta);
638 match rope_freqs {
639 None => Some(yarn),
640 Some(own) if own.len() == yarn.len() => {
641 Some(own.iter().zip(yarn.iter()).map(|(a, b)| a * b).collect())
642 }
643 Some(own) => {
644 best_effort_fields.push(
645 "rope_freqs (YaRN declared alongside a per-band factor tensor of a \
646 different width; the file's own tensor is used unscaled)",
647 );
648 Some(own)
649 }
650 }
651 }
652 }
653 };
654
655 if best_effort_fields.is_empty() {
660 best_effort_fields.push(
661 "none -- every field above was read directly from this file's own GGUF metadata",
662 );
663 }
664
665 if matches!(
676 arch_profile.path,
677 crate::capability::ArchPath::GenericGqa { .. }
678 ) && !crate::capability::is_audited_generic(&arch)
679 && !matches!(
680 std::env::var("FERROX_ALLOW_UNAUDITED_ARCH").ok().as_deref(),
681 Some("1") | Some("true") | Some("on")
682 )
683 {
684 return Err(LoadError::UnauditedArchitecture(arch.clone(), rope_layout));
685 }
686
687 Ok(ModelConfig {
688 name,
689 n_layers,
690 hidden_dim,
691 n_heads,
692 n_kv_heads,
693 head_dim,
694 vocab_size,
695 rope_theta,
696 rms_norm_eps,
697 attention: crate::config::AttentionKind::Gqa,
701 sliding_window,
702 swa_pattern,
703 moe: MoeLayerConfig {
704 n_experts: n_experts.max(1),
705 n_experts_active,
706 n_shared_experts,
707 hidden_dim,
708 expert_ffn_dim,
709 gating,
710 norm_topk_prob,
711 expert_group_count: metadata_u64_any(file, &[key("expert_group_count")])
712 .map(|v| v as usize)
713 .filter(|&c| c > 1),
714 expert_group_used_count: metadata_u64_any(file, &[key("expert_group_used_count")])
715 .map(|v| v as usize)
716 .filter(|&c| c > 0),
717 expert_weights_scale,
718 },
719 n_dense_leading_layers,
720 rope_freqs,
721 rope_layout,
722 qk_norm_style,
723 attn_logit_softcap,
724 final_logit_softcap,
725 embedding_scale,
726 attention_scale,
727 rope_attn_factor,
728 rope_dim,
729 rope_freqs_long,
730 rope_freqs_short,
731 rope_orig_ctx,
732 rope_theta_swa,
733 ffn_activation,
734 best_effort_fields: Box::leak(best_effort_fields.into_boxed_slice()),
735 })
736 }
737}
738
739impl crate::sampling::RecommendedSampling {
740 pub fn from_gguf(file: &impl TensorSource) -> Self {
762 let number = |k: &str| -> Option<f32> {
763 file.metadata(k)
764 .and_then(|v| v.as_f32().or_else(|| v.as_u64().map(|u| u as f32)))
765 };
766 crate::sampling::RecommendedSampling {
767 temperature: number("general.sampling.temp"),
768 top_p: number("general.sampling.top_p"),
769 top_k: file
770 .metadata("general.sampling.top_k")
771 .and_then(|v| v.as_u64())
772 .map(|v| v as usize),
773 }
774 }
775}
776
777fn linear_scaling_from_gguf(file: &impl TensorSource, arch: &str) -> Option<f32> {
784 let key = |suffix: &str| format!("{arch}.{suffix}");
785 let scaling_type = file.metadata_str(&key("rope.scaling.type"))?;
786 if !scaling_type.eq_ignore_ascii_case("linear") {
787 return None;
788 }
789 metadata_f32_any(file, &[key("rope.scaling.factor")]).filter(|f| f.is_finite() && *f > 1.0)
790}
791
792fn yarn_scaling_from_gguf(
822 file: &impl TensorSource,
823 arch: &str,
824 orig_ctx: Option<usize>,
825) -> Option<ferrox_core::attention::YarnScaling> {
826 let key = |suffix: &str| format!("{arch}.{suffix}");
827 let scaling_type = file.metadata_str(&key("rope.scaling.type"))?;
828 if !scaling_type.eq_ignore_ascii_case("yarn") {
829 return None;
830 }
831 let factor = metadata_f32_any(file, &[key("rope.scaling.factor")])
832 .filter(|f| f.is_finite() && *f > 1.0)?;
833 let orig_max_pos = orig_ctx?;
834 let beta = |suffix: &str, default: f32| -> f32 {
835 metadata_f32_any(
836 file,
837 &[
838 key(&format!("rope.scaling.{suffix}")),
839 key(&format!("rope.scaling.yarn_{suffix}")),
840 ],
841 )
842 .filter(|v| v.is_finite() && *v > 0.0)
843 .unwrap_or(default)
844 };
845 Some(ferrox_core::attention::YarnScaling {
846 factor,
847 beta_fast: beta("beta_fast", 32.0),
848 beta_slow: beta("beta_slow", 1.0),
849 orig_max_pos,
850 truncate: true,
854 })
855}
856
857pub(crate) fn find_info<'a>(
858 file: &'a impl TensorSource,
859 name: &str,
860) -> Result<&'a TensorInfo, LoadError> {
861 file.find_tensor(name)
862 .ok_or_else(|| LoadError::Gguf(GgufError::TensorNotFound(name.to_string())))
863}
864
865fn load_gpt_oss_layer(
891 file: &impl TensorSource,
892 l: usize,
893 config: &ModelConfig,
894) -> Result<crate::decoder::GptOssLayer, LoadError> {
895 let n_experts = config.moe.n_experts;
896 let ff = config.moe.expert_ffn_dim;
897
898 let want = |name: &str, got: usize, expect: usize| -> Result<(), LoadError> {
899 if got == expect {
900 Ok(())
901 } else {
902 Err(LoadError::UnsupportedFeature(
903 config.name.to_string(),
904 format!("{name} has {got} elements, expected {expect}"),
905 ))
906 }
907 };
908
909 let attn_sinks = load_f32_vec(file, &format!("blk.{l}.attn_sinks.weight"))?;
910 want(
911 &format!("blk.{l}.attn_sinks.weight"),
912 attn_sinks.len(),
913 config.n_heads,
914 )?;
915 let o_bias = load_f32_vec(file, &format!("blk.{l}.attn_output.bias"))?;
916 want(
917 &format!("blk.{l}.attn_output.bias"),
918 o_bias.len(),
919 config.hidden_dim,
920 )?;
921 let router_bias = load_f32_vec(file, &format!("blk.{l}.ffn_gate_inp.bias"))?;
922 want(
923 &format!("blk.{l}.ffn_gate_inp.bias"),
924 router_bias.len(),
925 n_experts,
926 )?;
927
928 let gate_b = load_f32_vec(file, &format!("blk.{l}.ffn_gate_exps.bias"))?;
929 want(
930 &format!("blk.{l}.ffn_gate_exps.bias"),
931 gate_b.len(),
932 n_experts * ff,
933 )?;
934 let up_b = load_f32_vec(file, &format!("blk.{l}.ffn_up_exps.bias"))?;
935 want(
936 &format!("blk.{l}.ffn_up_exps.bias"),
937 up_b.len(),
938 n_experts * ff,
939 )?;
940 let down_b = load_f32_vec(file, &format!("blk.{l}.ffn_down_exps.bias"))?;
941 want(
942 &format!("blk.{l}.ffn_down_exps.bias"),
943 down_b.len(),
944 n_experts * config.hidden_dim,
945 )?;
946
947 let expert_bias = (0..n_experts)
948 .map(|e| ferrox_moe::ExpertBias {
949 gate: gate_b[e * ff..(e + 1) * ff].to_vec(),
950 up: up_b[e * ff..(e + 1) * ff].to_vec(),
951 down: down_b[e * config.hidden_dim..(e + 1) * config.hidden_dim].to_vec(),
952 })
953 .collect();
954
955 Ok(crate::decoder::GptOssLayer {
956 attn_sinks,
957 o_bias,
958 router_bias,
959 expert_bias,
960 })
961}
962
963pub(crate) fn load_f32_vec_optional(
964 file: &impl TensorSource,
965 name: &str,
966) -> Result<Option<Vec<f32>>, LoadError> {
967 if file.find_tensor(name).is_none() {
968 return Ok(None);
969 }
970 Ok(Some(load_f32_vec(file, name)?))
971}
972
973fn slice_quantized_rows(m: &WeightMatrix, start: usize, n: usize) -> Option<WeightMatrix> {
980 let WeightMatrix::Quantized {
981 data,
982 rows,
983 cols,
984 kind,
985 } = m
986 else {
987 return None;
988 };
989 let total = data.len();
990 if *rows == 0 || total % *rows != 0 || start + n > *rows {
991 return None;
992 }
993 let row_bytes = total / *rows;
994 let (b0, b1) = (start * row_bytes, (start + n) * row_bytes);
995 let bytes = match data {
996 WeightBytes::Mapped { mmap, range } => WeightBytes::Mapped {
997 mmap: mmap.clone(),
998 range: range.start + b0..range.start + b1,
999 },
1000 other => WeightBytes::Owned(other.as_slice()[b0..b1].to_vec()),
1001 };
1002 Some(WeightMatrix::Quantized {
1003 data: bytes,
1004 rows: n,
1005 cols: *cols,
1006 kind: *kind,
1007 })
1008}
1009
1010fn load_qkv_projections(
1015 file: &impl TensorSource,
1016 layer: usize,
1017 config: &ModelConfig,
1018) -> Result<(WeightMatrix, WeightMatrix, WeightMatrix), LoadError> {
1019 let q_name = format!("blk.{layer}.attn_q.weight");
1020 let k_name = format!("blk.{layer}.attn_k.weight");
1021 let v_name = format!("blk.{layer}.attn_v.weight");
1022 let fused_name = format!("blk.{layer}.attn_qkv.weight");
1023
1024 if file.find_tensor(&q_name).is_some() {
1025 return Ok((
1026 load_weight_matrix(file, &q_name)?,
1027 load_weight_matrix(file, &k_name)?,
1028 load_weight_matrix(file, &v_name)?,
1029 ));
1030 }
1031 if file.find_tensor(&fused_name).is_none() {
1032 return Err(LoadError::Gguf(GgufError::TensorNotFound(q_name)));
1033 }
1034
1035 let fused = load_weight_matrix(file, &fused_name)?;
1036 let q_rows = config.n_heads * config.head_dim;
1037 let kv_rows = config.n_kv_heads * config.head_dim;
1038 let expected = q_rows + 2 * kv_rows;
1039 if fused.rows() != expected {
1040 return Err(LoadError::UnsupportedFeature(
1042 config.name.to_string(),
1043 format!(
1044 "{fused_name} has {} rows; expected q+k+v = {} \
1045 (n_heads*head_dim + 2*n_kv_heads*head_dim)",
1046 fused.rows(),
1047 expected
1048 ),
1049 ));
1050 }
1051 let cols = fused.cols();
1052 if let (Some(q), Some(k), Some(v)) = (
1055 slice_quantized_rows(&fused, 0, q_rows),
1056 slice_quantized_rows(&fused, q_rows, kv_rows),
1057 slice_quantized_rows(&fused, q_rows + kv_rows, kv_rows),
1058 ) {
1059 return Ok((q, k, v));
1060 }
1061 let mut full = Vec::with_capacity(fused.rows() * cols);
1063 for r in 0..fused.rows() {
1064 full.extend_from_slice(&fused.dequant_row(r));
1065 }
1066 let q = WeightMatrix::F32(Tensor::new(
1067 full[..q_rows * cols].to_vec(),
1068 vec![q_rows, cols],
1069 ));
1070 let k = WeightMatrix::F32(Tensor::new(
1071 full[q_rows * cols..(q_rows + kv_rows) * cols].to_vec(),
1072 vec![kv_rows, cols],
1073 ));
1074 let v = WeightMatrix::F32(Tensor::new(
1075 full[(q_rows + kv_rows) * cols..].to_vec(),
1076 vec![kv_rows, cols],
1077 ));
1078 Ok((q, k, v))
1079}
1080
1081fn load_dense_expert(
1084 file: &impl TensorSource,
1085 layer: usize,
1086 config: &ModelConfig,
1087) -> Result<ExpertWeights, LoadError> {
1088 let gate_name = format!("blk.{layer}.ffn_gate.weight");
1089 let up_name = format!("blk.{layer}.ffn_up.weight");
1090 let down_name = format!("blk.{layer}.ffn_down.weight");
1091 if file.find_tensor(&gate_name).is_some() {
1092 return Ok(ExpertWeights {
1093 gate: load_weight_matrix(file, &gate_name)?,
1094 up: load_weight_matrix(file, &up_name)?,
1095 down: load_weight_matrix(file, &down_name)?,
1096 });
1097 }
1098 let fused = load_weight_matrix(file, &up_name)?;
1100 let ff = config.moe.expert_ffn_dim;
1101 if fused.rows() != 2 * ff {
1102 return Err(LoadError::UnsupportedFeature(
1103 config.name.to_string(),
1104 format!(
1105 "{up_name} has {} rows without a companion ffn_gate; \
1106 expected fused SwiGLU with 2*ffn_dim = {} rows",
1107 fused.rows(),
1108 2 * ff
1109 ),
1110 ));
1111 }
1112 let cols = fused.cols();
1113 if let (Some(gate), Some(up)) = (
1115 slice_quantized_rows(&fused, 0, ff),
1116 slice_quantized_rows(&fused, ff, ff),
1117 ) {
1118 return Ok(ExpertWeights {
1119 gate,
1120 up,
1121 down: load_weight_matrix(file, &down_name)?,
1122 });
1123 }
1124 let mut full = Vec::with_capacity(fused.rows() * cols);
1125 for r in 0..fused.rows() {
1126 full.extend_from_slice(&fused.dequant_row(r));
1127 }
1128 let gate = WeightMatrix::F32(Tensor::new(full[..ff * cols].to_vec(), vec![ff, cols]));
1129 let up = WeightMatrix::F32(Tensor::new(full[ff * cols..].to_vec(), vec![ff, cols]));
1130 Ok(ExpertWeights {
1131 gate,
1132 up,
1133 down: load_weight_matrix(file, &down_name)?,
1134 })
1135}
1136
1137pub(crate) fn widen_plain_float(
1146 dtype: GgmlType,
1147 raw: &[u8],
1148 name: &str,
1149) -> Result<Vec<f32>, LoadError> {
1150 match dtype {
1151 GgmlType::F32 => {
1152 let mut out = Vec::with_capacity(raw.len() / 4);
1153 for chunk in raw.as_chunks::<4>().0 {
1154 out.push(f32::from_le_bytes(*chunk));
1155 }
1156 Ok(out)
1157 }
1158 GgmlType::F16 => ferrox_quant::dequant_f16(raw)
1159 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::F16)),
1160 GgmlType::BF16 => ferrox_quant::dequant_bf16(raw)
1161 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::BF16)),
1162 GgmlType::MXFP4 => ferrox_quant::dequant_mxfp4_gguf(raw)
1169 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::MXFP4)),
1170 other => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1171 }
1172}
1173
1174pub(crate) fn load_f32_vec(file: &impl TensorSource, name: &str) -> Result<Vec<f32>, LoadError> {
1175 let info = find_info(file, name)?;
1176 let raw = file.tensor_bytes(name)?;
1177 match info.dtype {
1178 GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => widen_plain_float(info.dtype, raw, name),
1179 GgmlType::Q8_0 => ferrox_quant::dequant_q8_0(raw)
1180 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q8_0)),
1181 GgmlType::Q4_0 => ferrox_quant::dequant_q4_0(raw)
1182 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4_0)),
1183 GgmlType::Q4K => ferrox_quant::dequant_q4_k(raw)
1184 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4K)),
1185 GgmlType::Q5K => ferrox_quant::dequant_q5_k(raw)
1186 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5K)),
1187 GgmlType::Q6K => ferrox_quant::dequant_q6_k(raw)
1188 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q6K)),
1189 GgmlType::Q2K => ferrox_quant::dequant_q2_k(raw)
1190 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q2K)),
1191 GgmlType::Q3K => ferrox_quant::dequant_q3_k(raw)
1192 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q3K)),
1193 GgmlType::Q4_1 => ferrox_quant::dequant_q4_1(raw)
1194 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4_1)),
1195 GgmlType::Q5_0 => ferrox_quant::dequant_q5_0(raw)
1196 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5_0)),
1197 GgmlType::Q5_1 => ferrox_quant::dequant_q5_1(raw)
1198 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5_1)),
1199 GgmlType::Q8_1 => ferrox_quant::dequant_q8_1(raw)
1200 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q8_1)),
1201 GgmlType::IQ4NL => ferrox_quant::dequant_iq4_nl(raw)
1202 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ4NL)),
1203 GgmlType::IQ4XS => ferrox_quant::dequant_iq4_xs(raw)
1204 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ4XS)),
1205 GgmlType::IQ1S => ferrox_quant::dequant_iq1_s(raw)
1212 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ1S)),
1213 GgmlType::IQ1M => ferrox_quant::dequant_iq1_m(raw)
1214 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ1M)),
1215 GgmlType::IQ2XXS => ferrox_quant::dequant_iq2_xxs(raw)
1216 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2XXS)),
1217 GgmlType::IQ2XS => ferrox_quant::dequant_iq2_xs(raw)
1218 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2XS)),
1219 GgmlType::IQ2S => ferrox_quant::dequant_iq2_s(raw)
1220 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2S)),
1221 GgmlType::IQ3XXS => ferrox_quant::dequant_iq3_xxs(raw)
1222 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ3XXS)),
1223 GgmlType::IQ3S => ferrox_quant::dequant_iq3_s(raw)
1224 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ3S)),
1225 other => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1226 }
1227}
1228
1229pub(crate) fn load_weight_matrix(
1236 file: &impl TensorSource,
1237 name: &str,
1238) -> Result<WeightMatrix, LoadError> {
1239 let info = find_info(file, name)?;
1240 let shape: Vec<usize> = info.shape.iter().rev().map(|&d| d as usize).collect();
1253 let (rows, cols) = match shape.as_slice() {
1254 [r, c] => (*r, *c),
1255 other => {
1256 return Err(LoadError::UnsupportedDtype(
1257 format!("{name} (expected 2D, got shape {other:?})"),
1258 info.dtype,
1259 ))
1260 }
1261 };
1262
1263 match info.dtype {
1264 GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => {
1270 let data = load_f32_vec(file, name)?;
1271 Ok(WeightMatrix::F32(Tensor::new(data, shape)))
1272 }
1273 other => match quant_kind_for(other) {
1274 Some(kind) => {
1275 let (mmap, range) = file.tensor_mapped_range(name)?;
1276 #[cfg(feature = "metal")]
1277 ferrox_metal::gpu::register_weight_mmap(Arc::clone(&mmap));
1278 Ok(WeightMatrix::Quantized {
1279 data: WeightBytes::Mapped { mmap, range },
1280 rows,
1281 cols,
1282 kind,
1283 })
1284 }
1285 None => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1286 },
1287 }
1288}
1289
1290pub(crate) fn split_expert_tensor(
1297 file: &impl TensorSource,
1298 name: &str,
1299 n_experts: usize,
1300) -> Result<Vec<WeightMatrix>, LoadError> {
1301 let info = find_info(file, name)?;
1302 if info.shape.len() != 3 || info.shape[2] as usize != n_experts {
1309 let file_experts = info.shape.last().map(|&d| d as usize).unwrap_or(0);
1310 return Err(LoadError::ExpertCountMismatch(
1311 name.to_string(),
1312 file_experts,
1313 n_experts,
1314 ));
1315 }
1316 let out_dim = info.shape[1] as usize;
1317 let in_dim = info.shape[0] as usize;
1318 let raw = file.tensor_bytes(name)?;
1319
1320 match info.dtype {
1321 GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => {
1322 let all = crate::loader::widen_plain_float(info.dtype, raw, name)?;
1323 let per_expert = out_dim * in_dim;
1324 Ok((0..n_experts)
1325 .map(|e| {
1326 WeightMatrix::F32(Tensor::new(
1327 all[e * per_expert..(e + 1) * per_expert].to_vec(),
1328 vec![out_dim, in_dim],
1329 ))
1330 })
1331 .collect())
1332 }
1333 other => match quant_kind_for(other) {
1334 Some(kind) => {
1335 let (mmap, full_range) = file.tensor_mapped_range(name)?;
1336 #[cfg(feature = "metal")]
1337 ferrox_metal::gpu::register_weight_mmap(Arc::clone(&mmap));
1338 let bytes_per_expert = raw.len() / n_experts;
1339 Ok((0..n_experts)
1340 .map(|e| WeightMatrix::Quantized {
1341 data: WeightBytes::Mapped {
1342 mmap: Arc::clone(&mmap),
1343 range: (full_range.start + e * bytes_per_expert)
1344 ..(full_range.start + (e + 1) * bytes_per_expert),
1345 },
1346 rows: out_dim,
1347 cols: in_dim,
1348 kind,
1349 })
1350 .collect())
1351 }
1352 None => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1353 },
1354 }
1355}
1356
1357#[cfg(feature = "metal")]
1362fn try_build_moe_packed_q4_planes(experts: &[ExpertWeights]) -> Option<MoePackedQ4Planes> {
1363 use ferrox_core::weight_matrix::{QuantKind, WeightBytes};
1364 use std::sync::Arc;
1365
1366 if experts.is_empty() {
1367 return None;
1368 }
1369
1370 fn mapped_sg(m: &WeightMatrix) -> Option<(WeightBytes, usize, &'static str)> {
1371 match m {
1372 WeightMatrix::Quantized {
1373 data: WeightBytes::Mapped { mmap, range },
1374 rows,
1375 kind,
1376 ..
1377 } => {
1378 let kind_str = match kind {
1379 QuantKind::Q4_0 => "Q4_0",
1380 QuantKind::Q5_0 => "Q5_0",
1381 QuantKind::Q4K => "Q4_K",
1382 QuantKind::Q5K => "Q5_K",
1383 QuantKind::Q6K => "Q6_K",
1384 QuantKind::Q8_0 => "Q8_0",
1385 QuantKind::IQ4XS => "IQ4_XS",
1386 _ => return None,
1387 };
1388 let _ = ferrox_metal::gpu::mul_mm_sg_meta(kind_str)?;
1389 Some((
1390 WeightBytes::Mapped {
1391 mmap: Arc::clone(mmap),
1392 range: range.clone(),
1393 },
1394 *rows,
1395 kind_str,
1396 ))
1397 }
1398 _ => None,
1399 }
1400 }
1401
1402 let (gate0, ffn_rows, gate_kind) = mapped_sg(&experts[0].gate)?;
1403 let (up0, up_rows, up_kind) = mapped_sg(&experts[0].up)?;
1404 let (down0, hidden_rows, down_kind) = mapped_sg(&experts[0].down)?;
1405 if up_rows != ffn_rows {
1406 return None;
1407 }
1408 let WeightBytes::Mapped {
1409 mmap: gate_mmap,
1410 range: gate0_range,
1411 } = &gate0
1412 else {
1413 return None;
1414 };
1415 let WeightBytes::Mapped {
1416 mmap: up_mmap,
1417 range: up0_range,
1418 } = &up0
1419 else {
1420 return None;
1421 };
1422 let WeightBytes::Mapped {
1423 mmap: down_mmap,
1424 range: down0_range,
1425 } = &down0
1426 else {
1427 return None;
1428 };
1429
1430 let gate_stride = gate0_range.len();
1431 let up_stride = up0_range.len();
1432 let down_stride = down0_range.len();
1433 if gate_stride == 0 || up_stride == 0 || down_stride == 0 {
1434 return None;
1435 }
1436
1437 let n = experts.len();
1438 for (i, ex) in experts.iter().enumerate().skip(1) {
1439 let (g, fr, gk) = mapped_sg(&ex.gate)?;
1440 let (u, ur, uk) = mapped_sg(&ex.up)?;
1441 let (d, hr, dk) = mapped_sg(&ex.down)?;
1442 if gk != gate_kind || uk != up_kind || dk != down_kind {
1443 return None;
1444 }
1445 let WeightBytes::Mapped { mmap, range } = &g else {
1446 return None;
1447 };
1448 if fr != ffn_rows {
1449 return None;
1450 }
1451 if !Arc::ptr_eq(mmap, gate_mmap)
1452 || range.len() != gate_stride
1453 || range.start != gate0_range.start + i * gate_stride
1454 {
1455 return None;
1456 }
1457 let WeightBytes::Mapped { mmap, range } = &u else {
1458 return None;
1459 };
1460 if ur != ffn_rows
1461 || !Arc::ptr_eq(mmap, up_mmap)
1462 || range.len() != up_stride
1463 || range.start != up0_range.start + i * up_stride
1464 {
1465 return None;
1466 }
1467 let WeightBytes::Mapped { mmap, range } = &d else {
1468 return None;
1469 };
1470 if hr != hidden_rows
1471 || !Arc::ptr_eq(mmap, down_mmap)
1472 || range.len() != down_stride
1473 || range.start != down0_range.start + i * down_stride
1474 {
1475 return None;
1476 }
1477 }
1478
1479 Some(MoePackedQ4Planes::new(
1480 WeightBytes::Mapped {
1481 mmap: Arc::clone(gate_mmap),
1482 range: gate0_range.start..gate0_range.start + n * gate_stride,
1483 },
1484 WeightBytes::Mapped {
1485 mmap: Arc::clone(up_mmap),
1486 range: up0_range.start..up0_range.start + n * up_stride,
1487 },
1488 WeightBytes::Mapped {
1489 mmap: Arc::clone(down_mmap),
1490 range: down0_range.start..down0_range.start + n * down_stride,
1491 },
1492 gate_stride,
1493 up_stride,
1494 down_stride,
1495 n,
1496 ffn_rows,
1497 hidden_rows,
1498 gate_kind,
1499 up_kind,
1500 down_kind,
1501 ))
1502}
1503
1504#[derive(Debug, Clone, Copy)]
1508pub struct StoredMatrixSpec {
1509 pub offset: usize,
1510 pub len: usize,
1511 pub rows: usize,
1512 pub cols: usize,
1513 pub kind: QuantKind,
1514}
1515
1516#[derive(Debug, Clone, Copy)]
1518pub struct StoredExpertLayout {
1519 pub gate: StoredMatrixSpec,
1520 pub up: StoredMatrixSpec,
1521 pub down: StoredMatrixSpec,
1522}
1523
1524impl StoredExpertLayout {
1525 pub fn total_bytes(&self) -> usize {
1526 self.gate.len + self.up.len + self.down.len
1527 }
1528
1529 pub fn materialize(&self, lease: &ferrox_core::expert_store::ExpertLease) -> ExpertWeights {
1533 let mk = |spec: &StoredMatrixSpec| WeightMatrix::Quantized {
1534 data: WeightBytes::Shared {
1535 buf: lease.shared_buf(),
1536 range: spec.offset..spec.offset + spec.len,
1537 },
1538 rows: spec.rows,
1539 cols: spec.cols,
1540 kind: spec.kind,
1541 };
1542 ExpertWeights {
1543 gate: mk(&self.gate),
1544 up: mk(&self.up),
1545 down: mk(&self.down),
1546 }
1547 }
1548}
1549
1550pub struct GgufExpertSource {
1556 files: Vec<std::fs::File>,
1557 segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]>,
1560}
1561
1562impl ExpertSource for GgufExpertSource {
1563 fn expert_len(&self, key: ExpertKey) -> Option<usize> {
1564 self.segments
1565 .get(&key)
1566 .map(|segs| segs.iter().map(|&(_, _, len)| len).sum())
1567 }
1568
1569 fn read_expert(&self, key: ExpertKey) -> std::io::Result<Vec<u8>> {
1570 let segs = self
1571 .segments
1572 .get(&key)
1573 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, format!("{key:?}")))?;
1574 let total: usize = segs.iter().map(|&(_, _, len)| len).sum();
1575 let mut buf = vec![0u8; total];
1576 let mut written = 0;
1577 for &(fi, offset, len) in segs {
1578 let dst = &mut buf[written..written + len];
1579 #[cfg(unix)]
1580 {
1581 use std::os::unix::fs::FileExt;
1582 self.files[fi].read_exact_at(dst, offset)?;
1583 }
1584 #[cfg(not(unix))]
1585 {
1586 use std::io::{Read, Seek, SeekFrom};
1587 let mut f = &self.files[fi];
1588 f.seek(SeekFrom::Start(offset))?;
1589 f.read_exact(dst)?;
1590 }
1591 written += len;
1592 }
1593 Ok(buf)
1594 }
1595}
1596
1597struct StoredTensorSpecs {
1607 shard: usize,
1608 per_expert: Vec<(u64, usize)>,
1609 spec: StoredMatrixSpec,
1610}
1611
1612fn stored_expert_specs(
1613 file: &ShardedGguf,
1614 name: &str,
1615 n_experts: usize,
1616) -> Result<Option<StoredTensorSpecs>, LoadError> {
1617 let info = find_info(file, name)?;
1618 if info.shape.len() != 3 || info.shape[2] as usize != n_experts {
1619 let file_experts = info.shape.last().map(|&d| d as usize).unwrap_or(0);
1620 return Err(LoadError::ExpertCountMismatch(
1621 name.to_string(),
1622 file_experts,
1623 n_experts,
1624 ));
1625 }
1626 let out_dim = info.shape[1] as usize;
1627 let in_dim = info.shape[0] as usize;
1628 let Some(kind) = quant_kind_for(info.dtype) else {
1629 return Ok(None); };
1631 let shard = file
1632 .tensor_shard_index(name)
1633 .expect("find_info succeeded, shard index must exist");
1634 let (_, full_range) = file.tensor_mapped_range(name)?;
1637 let total_len = full_range.end - full_range.start;
1638 let bytes_per_expert = total_len / n_experts;
1639 let per_expert: Vec<(u64, usize)> = (0..n_experts)
1640 .map(|e| {
1641 (
1642 (full_range.start + e * bytes_per_expert) as u64,
1643 bytes_per_expert,
1644 )
1645 })
1646 .collect();
1647 let spec = StoredMatrixSpec {
1648 offset: 0, len: bytes_per_expert,
1650 rows: out_dim,
1651 cols: in_dim,
1652 kind,
1653 };
1654 Ok(Some(StoredTensorSpecs {
1655 shard,
1656 per_expert,
1657 spec,
1658 }))
1659}
1660
1661impl Decoder {
1662 pub fn from_gguf(
1671 path: impl AsRef<std::path::Path>,
1672 config: ModelConfig,
1673 ) -> Result<Self, LoadError> {
1674 Self::from_gguf_with_expert_cache(path, config, None)
1675 }
1676
1677 pub fn from_gguf_with_expert_cache(
1690 path: impl AsRef<std::path::Path>,
1691 mut config: ModelConfig,
1692 expert_cache_bytes: Option<u64>,
1693 ) -> Result<Self, LoadError> {
1694 let path = path.as_ref();
1695 let file = ShardedGguf::open(path)?;
1696
1697 let arch = file
1703 .metadata_str("general.architecture")
1704 .unwrap_or_default()
1705 .to_string();
1706 let is_gpt_oss = arch == "gpt-oss";
1707 let mut gpt_oss_layers: Vec<crate::decoder::GptOssLayer> = Vec::new();
1708
1709 let mut store_segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]> =
1713 std::collections::HashMap::new();
1714 let mut stored_layouts: Vec<Option<Vec<StoredExpertLayout>>> = Vec::new();
1715
1716 let embedding = load_weight_matrix(&file, "token_embd.weight")?;
1721
1722 let mut layers = Vec::with_capacity(config.n_layers);
1723 let mut refined_qk_norm = config.qk_norm_style;
1724 for l in 0..config.n_layers {
1725 let (q_proj, k_proj, v_proj) = load_qkv_projections(&file, l, &config)?;
1726 let q_norm = load_f32_vec_optional(&file, &format!("blk.{l}.attn_q_norm.weight"))?;
1727 let k_norm = load_f32_vec_optional(&file, &format!("blk.{l}.attn_k_norm.weight"))?;
1728 if let Some(ref w) = q_norm {
1730 if w.len() == config.head_dim {
1731 refined_qk_norm = crate::capability::QkNormStyle::PerHead;
1732 } else if w.len() == config.n_heads * config.head_dim {
1733 refined_qk_norm = crate::capability::QkNormStyle::WholeVector;
1734 } else {
1735 return Err(LoadError::UnsupportedFeature(
1736 config.name.to_string(),
1737 format!(
1738 "blk.{l}.attn_q_norm.weight length {} matches neither head_dim={} \
1739 nor n_heads*head_dim={}",
1740 w.len(),
1741 config.head_dim,
1742 config.n_heads * config.head_dim
1743 ),
1744 ));
1745 }
1746 }
1747 let attn = AttnWeights {
1748 q_proj,
1749 k_proj,
1750 v_proj,
1751 o_proj: load_weight_matrix(&file, &format!("blk.{l}.attn_output.weight"))?,
1752 norm_weight: load_f32_vec(&file, &format!("blk.{l}.attn_norm.weight"))?,
1753 q_norm,
1754 k_norm,
1755 q_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_q.bias"))?,
1759 k_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_k.bias"))?,
1760 v_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_v.bias"))?,
1761 post_attn_norm: if is_gpt_oss {
1767 None
1768 } else {
1769 load_f32_vec_optional(&file, &format!("blk.{l}.post_attention_norm.weight"))?
1770 },
1771 post_ffn_norm: load_f32_vec_optional(
1772 &file,
1773 &format!("blk.{l}.post_ffw_norm.weight"),
1774 )?,
1775 };
1776
1777 let is_dense_layer = config.layer_is_dense(l) || config.moe.n_experts <= 1;
1785 let n_experts = if is_dense_layer {
1786 1
1787 } else {
1788 config.moe.n_experts
1789 };
1790 let experts: ExpertBacking = if is_dense_layer {
1791 ExpertBacking::Resident(vec![load_dense_expert(&file, l, &config)?])
1792 } else {
1793 let stored = if expert_cache_bytes.is_some() {
1797 let g = stored_expert_specs(
1798 &file,
1799 &format!("blk.{l}.ffn_gate_exps.weight"),
1800 n_experts,
1801 )?;
1802 let u = stored_expert_specs(
1803 &file,
1804 &format!("blk.{l}.ffn_up_exps.weight"),
1805 n_experts,
1806 )?;
1807 let d = stored_expert_specs(
1808 &file,
1809 &format!("blk.{l}.ffn_down_exps.weight"),
1810 n_experts,
1811 )?;
1812 match (g, u, d) {
1813 (Some(gt), Some(ut), Some(dt)) => {
1814 let mut layouts = Vec::with_capacity(n_experts);
1815 for e in 0..n_experts {
1816 let key = ExpertKey {
1817 layer: l as u32,
1818 expert: e as u32,
1819 };
1820 store_segments.insert(
1821 key,
1822 [
1823 (gt.shard, gt.per_expert[e].0, gt.per_expert[e].1),
1824 (ut.shard, ut.per_expert[e].0, ut.per_expert[e].1),
1825 (dt.shard, dt.per_expert[e].0, dt.per_expert[e].1),
1826 ],
1827 );
1828 let mut gate = gt.spec;
1829 let mut up = ut.spec;
1830 let mut down = dt.spec;
1831 gate.offset = 0;
1832 up.offset = gate.len;
1833 down.offset = gate.len + up.len;
1834 layouts.push(StoredExpertLayout { gate, up, down });
1835 }
1836 Some(layouts)
1837 }
1838 _ => None,
1839 }
1840 } else {
1841 None
1842 };
1843 match stored {
1844 Some(layouts) => {
1845 stored_layouts.push(Some(layouts));
1849 ExpertBacking::Resident(Vec::new())
1850 }
1851 None => {
1852 let gates = split_expert_tensor(
1853 &file,
1854 &format!("blk.{l}.ffn_gate_exps.weight"),
1855 n_experts,
1856 )?;
1857 let ups = split_expert_tensor(
1858 &file,
1859 &format!("blk.{l}.ffn_up_exps.weight"),
1860 n_experts,
1861 )?;
1862 let downs = split_expert_tensor(
1863 &file,
1864 &format!("blk.{l}.ffn_down_exps.weight"),
1865 n_experts,
1866 )?;
1867 ExpertBacking::Resident(
1868 gates
1869 .into_iter()
1870 .zip(ups)
1871 .zip(downs)
1872 .map(|((gate, up), down)| ExpertWeights { gate, up, down })
1873 .collect(),
1874 )
1875 }
1876 }
1877 };
1878 if stored_layouts.len() < layers.len() + 1 {
1879 stored_layouts.push(None);
1880 }
1881
1882 let shared_experts: Vec<ExpertWeights> =
1883 if config.moe.n_shared_experts > 0 && !is_dense_layer {
1884 vec![ExpertWeights {
1885 gate: load_weight_matrix(&file, &format!("blk.{l}.ffn_gate_shexp.weight"))?,
1886 up: load_weight_matrix(&file, &format!("blk.{l}.ffn_up_shexp.weight"))?,
1887 down: load_weight_matrix(&file, &format!("blk.{l}.ffn_down_shexp.weight"))?,
1888 }]
1889 } else {
1890 Vec::new()
1891 };
1892
1893 let router = if !is_dense_layer {
1894 load_weight_matrix(&file, &format!("blk.{l}.ffn_gate_inp.weight"))?
1895 } else {
1896 WeightMatrix::F32(Tensor::zeros(vec![1, config.hidden_dim]))
1899 };
1900
1901 let n_for_counts = match &experts {
1902 ExpertBacking::Resident(v) if v.is_empty() => n_experts,
1903 other => other.n_experts(),
1904 };
1905 let activation_counts = (0..n_for_counts)
1906 .map(|_| std::sync::atomic::AtomicU64::new(0))
1907 .collect();
1908 let shared_expert_gate = if is_dense_layer {
1917 None
1918 } else {
1919 load_f32_vec_optional(&file, &format!("blk.{l}.ffn_gate_inp_shexp.weight"))?
1920 };
1921 #[cfg(feature = "metal")]
1922 let packed_q4 = match &experts {
1923 ExpertBacking::Resident(v) if !v.is_empty() => try_build_moe_packed_q4_planes(v),
1924 _ => None,
1925 };
1926 let exp_probs_bias = if is_dense_layer {
1934 None
1935 } else {
1936 load_f32_vec_optional(&file, &format!("blk.{l}.exp_probs_b.bias"))?
1937 };
1938 if let Some(bias) = &exp_probs_bias {
1939 if bias.len() != config.moe.n_experts {
1940 return Err(LoadError::UnsupportedFeature(
1941 arch.clone(),
1942 format!(
1943 "blk.{l}.exp_probs_b.bias has {} entries but the model has {} experts",
1944 bias.len(),
1945 config.moe.n_experts
1946 ),
1947 ));
1948 }
1949 if config.moe.expert_group_count.is_some() {
1956 return Err(LoadError::UnsupportedFeature(
1957 arch.clone(),
1958 format!(
1959 "blk.{l}.exp_probs_b.bias together with expert groups \
1960 ({:?}): llama.cpp masks the biased scores per group \
1961 before a global top-k, which is not the per-group \
1962 top-k ferrox implements",
1963 config.moe.expert_group_count
1964 ),
1965 ));
1966 }
1967 }
1968 let moe = MoeWeights {
1969 router,
1970 experts,
1971 shared_experts,
1972 shared_expert_gate,
1973 exp_probs_bias,
1974 norm_weight: if is_gpt_oss {
1975 load_f32_vec(&file, &format!("blk.{l}.post_attention_norm.weight"))?
1976 } else {
1977 load_f32_vec(&file, &format!("blk.{l}.ffn_norm.weight"))?
1978 },
1979 activation_counts,
1980 #[cfg(feature = "metal")]
1981 packed_q4,
1982 };
1983
1984 if is_gpt_oss {
1985 gpt_oss_layers.push(load_gpt_oss_layer(&file, l, &config)?);
1986 }
1987
1988 layers.push(LayerWeights { attn, moe });
1989 }
1990
1991 let final_norm = load_f32_vec(&file, "output_norm.weight")?;
1992 let output_head = match load_weight_matrix(&file, "output.weight") {
1997 Ok(w) => w,
1998 Err(_) => load_weight_matrix(&file, "token_embd.weight")?,
1999 };
2000
2001 if !store_segments.is_empty() {
2007 let budget = expert_cache_bytes
2008 .expect("store_segments only populated when a cache budget is set")
2009 as usize;
2010 let files: Result<Vec<std::fs::File>, std::io::Error> =
2011 file.shard_paths().iter().map(std::fs::File::open).collect();
2012 let files = files.map_err(GgufError::from)?;
2013 let store = std::sync::Arc::new(ExpertStore::new(
2014 GgufExpertSource {
2015 files,
2016 segments: store_segments,
2017 },
2018 budget,
2019 ));
2020 for (l, layer) in layers.iter_mut().enumerate() {
2021 if let Some(layouts) = stored_layouts.get_mut(l).and_then(Option::take) {
2022 layer.moe.experts = ExpertBacking::Stored {
2023 store: std::sync::Arc::clone(&store),
2024 layouts,
2025 layer: l as u32,
2026 };
2027 }
2028 }
2029 }
2030
2031 config.qk_norm_style = refined_qk_norm;
2032
2033 let family = crate::capability::resolve_profile(
2034 file.metadata_str("general.architecture").unwrap_or("llama"),
2035 )
2036 .map(|p| p.family)
2037 .unwrap_or(crate::capability::DecoderFamily::StandardGqa);
2038 let memory_kind = crate::capability::resolve_profile(
2039 file.metadata_str("general.architecture").unwrap_or("llama"),
2040 )
2041 .map(|p| p.memory)
2042 .unwrap_or(crate::capability::MemoryKind::KvGqa);
2043 let execution_plan = crate::execution_plan::ExecutionPlan::from_config(
2044 &config,
2045 family,
2046 memory_kind,
2047 crate::execution_plan::ExecutionPlan::probe_metal_caps(),
2048 );
2049
2050 let decoder = Decoder {
2051 config,
2052 embedding,
2053 layers,
2054 final_norm,
2055 output_head,
2056 gpu_vram_budget_bytes: None,
2057 gpt_oss: if is_gpt_oss {
2058 Some(crate::decoder::GptOssWeights {
2059 layers: gpt_oss_layers,
2060 })
2061 } else {
2062 None
2063 },
2064 #[cfg(feature = "metal")]
2065 metal_attn_kv: std::sync::Mutex::new(None),
2066 execution_plan,
2067 plan_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
2068 };
2069 decoder.probe_kernels();
2073 ferrox_core::kernel_registry::seal_or_error()
2074 .map_err(|e| LoadError::StrictKernels(e.to_string()))?;
2075 for name in crate::config::MODEL_LEVEL_TENSORS_READ_BY_CONFIG {
2082 file.note_consumed(name);
2083 }
2084 assert_every_tensor_consumed(&file)?;
2085 Ok(decoder)
2086 }
2087}
2088
2089const IGNORED_TENSOR_PREFIXES: &[&str] = &["mm.", "v.", "mmproj.", "resampler.", "audio."];
2094
2095pub fn assert_every_tensor_consumed(file: &ShardedGguf) -> Result<(), LoadError> {
2117 let mut left: Vec<String> = file
2118 .unconsumed_tensors()
2119 .into_iter()
2120 .filter(|n| !IGNORED_TENSOR_PREFIXES.iter().any(|p| n.starts_with(p)))
2121 .collect();
2122 if left.is_empty() {
2123 return Ok(());
2124 }
2125 left.sort();
2126 let shown = left.iter().take(8).cloned().collect::<Vec<_>>().join(", ");
2127 let listing = if left.len() > 8 {
2128 format!("{shown}, … (+{} more)", left.len() - 8)
2129 } else {
2130 shown
2131 };
2132 if matches!(
2133 std::env::var("FERROX_ALLOW_UNKNOWN_TENSORS")
2134 .ok()
2135 .as_deref(),
2136 Some("1") | Some("true") | Some("on")
2137 ) {
2138 eprintln!(
2139 "ferrox: WARNING — {} tensor(s) in this checkpoint are never read \
2140 ({listing}); output may be wrong (FERROX_ALLOW_UNKNOWN_TENSORS=1)",
2141 left.len()
2142 );
2143 return Ok(());
2144 }
2145 Err(LoadError::UnconsumedTensors(left.len(), listing))
2146}
2147
2148#[cfg(test)]
2149mod tests {
2150
2151 #[test]
2167 fn a_quantized_one_dimensional_tensor_widens_through_the_shared_helper() {
2168 let values: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) * 0.25).collect();
2169 let quantized = ferrox_quant::quantize_q8_0(&values);
2170
2171 struct OneTensor {
2172 info: TensorInfo,
2173 bytes: Vec<u8>,
2174 }
2175 impl TensorSource for OneTensor {
2176 fn metadata(&self, _key: &str) -> Option<&ferrox_gguf::GgufValue> {
2177 None
2178 }
2179 fn find_tensor(&self, name: &str) -> Option<&TensorInfo> {
2180 (name == self.info.name).then_some(&self.info)
2181 }
2182 fn tensor_bytes(&self, _name: &str) -> Result<&[u8], GgufError> {
2183 Ok(&self.bytes)
2184 }
2185 fn tensor_mapped_range(
2186 &self,
2187 name: &str,
2188 ) -> Result<
2189 (
2190 std::sync::Arc<ferrox_gguf::MmapHandle>,
2191 std::ops::Range<usize>,
2192 ),
2193 GgufError,
2194 > {
2195 Err(GgufError::TensorNotFound(name.to_string()))
2197 }
2198 }
2199
2200 let source = OneTensor {
2201 info: TensorInfo {
2202 name: "blk.0.attn_norm.weight".to_string(),
2203 shape: vec![64],
2204 dtype: GgmlType::Q8_0,
2205 offset: 0,
2206 },
2207 bytes: quantized,
2208 };
2209
2210 let widened = load_f32_vec(&source, "blk.0.attn_norm.weight")
2211 .expect("a Q8_0 norm must load, not report an unsupported dtype");
2212 assert_eq!(widened.len(), values.len());
2213 for (got, want) in widened.iter().zip(values.iter()) {
2214 assert!(
2215 (got - want).abs() < 0.05,
2216 "q8_0 round trip: got {got}, want {want}"
2217 );
2218 }
2219 }
2220 use super::*;
2221 use byteorder::{LittleEndian, WriteBytesExt};
2222 use std::io::Write;
2223
2224 fn write_string(buf: &mut Vec<u8>, s: &str) {
2225 buf.write_u64::<LittleEndian>(s.len() as u64).unwrap();
2226 buf.write_all(s.as_bytes()).unwrap();
2227 }
2228
2229 fn write_kv_str(buf: &mut Vec<u8>, key: &str, val: &str) {
2230 write_string(buf, key);
2231 buf.write_u32::<LittleEndian>(8).unwrap(); write_string(buf, val);
2233 }
2234
2235 fn build_arch_only_gguf(arch: &str) -> Vec<u8> {
2241 let mut buf = Vec::new();
2242 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2243 .unwrap();
2244 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);
2248 buf
2249 }
2250
2251 #[test]
2252 fn model_config_from_gguf_fails_loudly_when_required_hparams_are_missing() {
2253 let tmp =
2254 std::env::temp_dir().join(format!("ferrox_test_arch_only_{}.gguf", std::process::id()));
2255 std::fs::write(&tmp, build_arch_only_gguf("llama")).unwrap();
2258 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2259 std::fs::remove_file(&tmp).ok();
2260
2261 match ModelConfig::from_gguf(&file) {
2262 Err(LoadError::MissingHparam(key)) => {
2263 assert_eq!(key, "llama.block_count");
2264 }
2265 other => panic!(
2266 "expected LoadError::MissingHparam for a file with no hparam keys, got {other:?}"
2267 ),
2268 }
2269 }
2270
2271 #[test]
2272 fn model_config_from_gguf_fails_closed_on_unknown_architecture() {
2273 let tmp = std::env::temp_dir().join(format!(
2274 "ferrox_test_unknown_arch_{}.gguf",
2275 std::process::id()
2276 ));
2277 std::fs::write(&tmp, build_arch_only_gguf("bogus-arch-with-no-hparams")).unwrap();
2278 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2279 std::fs::remove_file(&tmp).ok();
2280
2281 match ModelConfig::from_gguf(&file) {
2282 Err(LoadError::UnsupportedArchitecture(arch)) => {
2283 assert_eq!(arch, "bogus-arch-with-no-hparams");
2284 }
2285 other => panic!(
2286 "expected LoadError::UnsupportedArchitecture for an unregistered arch, got {other:?}"
2287 ),
2288 }
2289 }
2290
2291 fn write_kv_f32(buf: &mut Vec<u8>, key: &str, val: f32) {
2292 write_string(buf, key);
2293 buf.write_u32::<LittleEndian>(6).unwrap(); buf.write_f32::<LittleEndian>(val).unwrap();
2295 }
2296
2297 fn build_arch_plus_f32_gguf(arch: &str, key: &str, val: f32) -> Vec<u8> {
2300 let mut buf = Vec::new();
2301 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2302 .unwrap();
2303 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);
2307 write_kv_f32(&mut buf, key, val);
2308 buf
2309 }
2310
2311 fn config_error_for(arch: &str, key: &str, val: f32, tag: &str) -> LoadError {
2312 let tmp = std::env::temp_dir().join(format!("ferrox_test_scale_{tag}.gguf"));
2313 std::fs::write(&tmp, build_arch_plus_f32_gguf(arch, key, val)).unwrap();
2314 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2315 std::fs::remove_file(&tmp).ok();
2316 ModelConfig::from_gguf(&file).expect_err("must not succeed")
2317 }
2318
2319 #[test]
2325 fn a_declared_multiplier_this_decoder_does_not_apply_is_refused_by_name() {
2326 for (key, val) in [
2327 ("granite.logit_scale", 6.0f32),
2328 ("granite.residual_scale", 0.22),
2329 ("granite.embedding_scale", 12.0),
2330 ("granite.attention.scale", 0.015_625),
2331 ] {
2332 let tag = key.replace('.', "_");
2333 match config_error_for("granite", key, val, &tag) {
2334 LoadError::UnsupportedFeature(arch, msg) => {
2335 assert_eq!(arch, "granite");
2336 assert!(msg.contains(key), "error must name the key: {msg}");
2337 }
2338 other => panic!("expected UnsupportedFeature for {key}, got {other:?}"),
2339 }
2340 }
2341 }
2342
2343 #[test]
2349 fn a_multiplier_that_is_a_no_op_is_not_refused() {
2350 for (key, val) in [
2351 ("granite.logit_scale", 1.0f32),
2352 ("granite.residual_scale", 1.0),
2353 ("granite.embedding_scale", 1.0),
2354 ("granite.attention.scale", 0.0),
2355 ] {
2356 let tag = format!("noop_{}", key.replace('.', "_"));
2357 match config_error_for("granite", key, val, &tag) {
2360 LoadError::MissingHparam(k) => assert_eq!(k, "granite.block_count"),
2361 other => panic!("no-op {key}={val} must pass the scaling gate, got {other:?}"),
2362 }
2363 }
2364 }
2365
2366 enum Kv<'a> {
2369 Str(&'a str),
2370 U32(u32),
2371 F32(f32),
2372 }
2373
2374 fn build_metadata_gguf(kvs: &[(&str, Kv)]) -> Vec<u8> {
2377 let mut buf = Vec::new();
2378 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2379 .unwrap();
2380 buf.write_u32::<LittleEndian>(3).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); buf.write_u64::<LittleEndian>(kvs.len() as u64).unwrap();
2383 for (k, v) in kvs {
2384 match v {
2385 Kv::Str(s) => write_kv_str(&mut buf, k, s),
2386 Kv::U32(n) => {
2387 write_string(&mut buf, k);
2388 buf.write_u32::<LittleEndian>(4).unwrap(); buf.write_u32::<LittleEndian>(*n).unwrap();
2390 }
2391 Kv::F32(f) => write_kv_f32(&mut buf, k, *f),
2392 }
2393 }
2394 buf
2395 }
2396
2397 fn open_metadata_gguf(tag: &str, kvs: &[(&str, Kv)]) -> ferrox_gguf::GgufFile {
2398 let tmp = std::env::temp_dir().join(format!("ferrox_test_meta_{tag}.gguf"));
2399 std::fs::write(&tmp, build_metadata_gguf(kvs)).unwrap();
2400 let file = ferrox_gguf::GgufFile::open(&tmp).expect("header-only file must parse");
2401 std::fs::remove_file(&tmp).ok();
2402 file
2403 }
2404
2405 fn llama_config_with(tag: &str, extra: &[(&str, Kv)]) -> ModelConfig {
2408 let mut kvs: Vec<(&str, Kv)> = vec![
2409 ("general.architecture", Kv::Str("llama")),
2410 ("llama.block_count", Kv::U32(1)),
2411 ("llama.embedding_length", Kv::U32(64)),
2412 ("llama.attention.head_count", Kv::U32(1)),
2413 ("llama.attention.head_count_kv", Kv::U32(1)),
2414 ("llama.attention.key_length", Kv::U32(64)),
2415 ("llama.rope.freq_base", Kv::F32(10_000.0)),
2416 ];
2417 for (k, v) in extra {
2418 kvs.push((
2419 k,
2420 match v {
2421 Kv::Str(s) => Kv::Str(s),
2422 Kv::U32(n) => Kv::U32(*n),
2423 Kv::F32(f) => Kv::F32(*f),
2424 },
2425 ));
2426 }
2427 ModelConfig::from_gguf(&open_metadata_gguf(tag, &kvs)).expect("fixture must load")
2428 }
2429
2430 fn config_for_arch(arch: &'static str) -> Result<ModelConfig, LoadError> {
2433 let keys: Vec<String> = [
2436 "block_count",
2437 "embedding_length",
2438 "attention.head_count",
2439 "attention.head_count_kv",
2440 "attention.key_length",
2441 ]
2442 .iter()
2443 .map(|k| format!("{arch}.{k}"))
2444 .collect();
2445 let theta = format!("{arch}.rope.freq_base");
2446 let kvs: Vec<(&str, Kv)> = vec![
2447 ("general.architecture", Kv::Str(arch)),
2448 (keys[0].as_str(), Kv::U32(1)),
2449 (keys[1].as_str(), Kv::U32(64)),
2450 (keys[2].as_str(), Kv::U32(1)),
2451 (keys[3].as_str(), Kv::U32(1)),
2452 (keys[4].as_str(), Kv::U32(64)),
2453 (theta.as_str(), Kv::F32(10_000.0)),
2454 ];
2455 ModelConfig::from_gguf(&open_metadata_gguf(arch, &kvs))
2456 }
2457
2458 #[test]
2466 fn an_unaudited_generic_architecture_refuses_rather_than_guessing() {
2467 assert!(
2470 !crate::capability::is_audited_generic("starcoder"),
2471 "this test needs an arch that is generic AND unaudited"
2472 );
2473 match config_for_arch("starcoder") {
2474 Err(LoadError::UnauditedArchitecture(name, _)) => assert_eq!(name, "starcoder"),
2475 other => panic!("expected an unaudited refusal, got {other:?}"),
2476 }
2477 }
2478
2479 #[test]
2482 fn an_audited_architecture_still_loads() {
2483 assert!(crate::capability::is_audited_generic("llama"));
2484 assert!(config_for_arch("llama").is_ok());
2485 }
2486
2487 #[test]
2494 fn a_named_refusal_outranks_the_unaudited_one() {
2495 let err = config_for_arch("gpt2").expect_err("gpt2 must refuse");
2496 assert!(
2497 !matches!(err, LoadError::UnauditedArchitecture(..)),
2498 "gpt2 should report its own reason, not that nobody audited it: {err:?}"
2499 );
2500 }
2501
2502 #[test]
2513 fn a_gguf_declaring_yarn_gets_its_rope_frequencies_rewritten() {
2514 let cfg = llama_config_with(
2515 "yarn",
2516 &[
2517 ("llama.rope.scaling.type", Kv::Str("yarn")),
2518 ("llama.rope.scaling.factor", Kv::F32(8.0)),
2519 (
2520 "llama.rope.scaling.original_context_length",
2521 Kv::U32(131_072),
2522 ),
2523 ],
2524 );
2525 let factors = cfg
2526 .rope_freqs
2527 .expect("a YaRN checkpoint must carry rewritten per-band frequencies");
2528 assert_eq!(factors.len(), 32, "one divisor per rotation band");
2529 assert!(
2530 (factors[0] - 1.0).abs() < 1e-6,
2531 "the fastest band is left extrapolated, got {}",
2532 factors[0]
2533 );
2534 let ramp = (31.0 - 22.0) / (35.0 - 22.0);
2535 let want = 1.0 / (ramp / 8.0 + (1.0 - ramp));
2536 assert!(
2537 (factors[31] - want).abs() < 1e-4,
2538 "slowest band: got {}, reference {want}",
2539 factors[31]
2540 );
2541 }
2542
2543 #[test]
2558 fn linear_scaling_is_applied_as_a_uniform_frequency_divisor() {
2559 let cfg = llama_config_with(
2560 "linear",
2561 &[
2562 ("llama.rope.scaling.type", Kv::Str("linear")),
2563 ("llama.rope.scaling.factor", Kv::F32(4.0)),
2564 ],
2565 );
2566 let freqs = cfg
2567 .rope_freqs
2568 .as_ref()
2569 .expect("linear scaling must produce frequency factors");
2570 assert_eq!(freqs.len(), cfg.head_dim / 2, "one factor per rotated pair");
2571 assert!(
2572 freqs.iter().all(|f| (*f - 4.0).abs() < 1e-6),
2573 "linear scaling is uniform across bands, unlike YaRN: got {freqs:?}"
2574 );
2575 }
2576
2577 #[test]
2579 fn a_linear_factor_of_one_is_treated_as_absent() {
2580 assert!(llama_config_with(
2581 "linear_one",
2582 &[
2583 ("llama.rope.scaling.type", Kv::Str("linear")),
2584 ("llama.rope.scaling.factor", Kv::F32(1.0)),
2585 ],
2586 )
2587 .rope_freqs
2588 .is_none());
2589 }
2590
2591 #[test]
2592 fn a_gguf_without_yarn_scaling_keeps_its_rope_frequencies_untouched() {
2593 assert!(llama_config_with("noscale", &[]).rope_freqs.is_none());
2594 assert!(llama_config_with(
2601 "yarn_factor_one",
2602 &[
2603 ("llama.rope.scaling.type", Kv::Str("yarn")),
2604 ("llama.rope.scaling.factor", Kv::F32(1.0)),
2605 (
2606 "llama.rope.scaling.original_context_length",
2607 Kv::U32(131_072),
2608 ),
2609 ],
2610 )
2611 .rope_freqs
2612 .is_none());
2613 }
2614
2615 #[test]
2622 fn yarn_without_an_original_context_length_is_not_guessed_at() {
2623 let cfg = llama_config_with(
2624 "yarn_noctx",
2625 &[
2626 ("llama.rope.scaling.type", Kv::Str("yarn")),
2627 ("llama.rope.scaling.factor", Kv::F32(8.0)),
2628 ],
2629 );
2630 assert!(cfg.rope_freqs.is_none());
2631 }
2632
2633 #[test]
2638 fn gguf_sampling_metadata_is_read_as_the_checkpoints_recommendation() {
2639 use crate::sampling::RecommendedSampling;
2640 let full = RecommendedSampling::from_gguf(&open_metadata_gguf(
2641 "sampling_full",
2642 &[
2643 ("general.architecture", Kv::Str("llama")),
2644 ("general.sampling.temp", Kv::F32(1.0)),
2645 ("general.sampling.top_k", Kv::U32(20)),
2646 ("general.sampling.top_p", Kv::F32(0.95)),
2647 ],
2648 ));
2649 assert_eq!(
2650 full,
2651 RecommendedSampling {
2652 temperature: Some(1.0),
2653 top_p: Some(0.95),
2654 top_k: Some(20),
2655 }
2656 );
2657
2658 let partial = RecommendedSampling::from_gguf(&open_metadata_gguf(
2659 "sampling_partial",
2660 &[
2661 ("general.architecture", Kv::Str("llama")),
2662 ("general.sampling.top_k", Kv::U32(40)),
2663 ],
2664 ));
2665 assert_eq!(partial.top_k, Some(40));
2666 assert_eq!(partial.temperature, None);
2667 assert_eq!(partial.top_p, None);
2668 }
2669
2670 #[test]
2675 fn an_integer_valued_sampling_temp_is_still_a_recommendation() {
2676 let recommended = crate::sampling::RecommendedSampling::from_gguf(&open_metadata_gguf(
2677 "sampling_int_temp",
2678 &[
2679 ("general.architecture", Kv::Str("llama")),
2680 ("general.sampling.temp", Kv::U32(1)),
2681 ],
2682 ));
2683 assert_eq!(recommended.temperature, Some(1.0));
2684 }
2685
2686 #[test]
2689 fn a_gguf_without_sampling_metadata_recommends_nothing() {
2690 let recommended = crate::sampling::RecommendedSampling::from_gguf(&open_metadata_gguf(
2691 "sampling_absent",
2692 &[("general.architecture", Kv::Str("llama"))],
2693 ));
2694 assert!(recommended.is_empty());
2695 }
2696
2697 #[test]
2698 fn model_config_from_gguf_rejects_dedicated_architectures() {
2699 let tmp = std::env::temp_dir().join(format!(
2700 "ferrox_test_dedicated_arch_{}.gguf",
2701 std::process::id()
2702 ));
2703 std::fs::write(&tmp, build_arch_only_gguf("deepseek4")).unwrap();
2704 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2705 std::fs::remove_file(&tmp).ok();
2706
2707 match ModelConfig::from_gguf(&file) {
2708 Err(LoadError::DedicatedArchitectureRequired(arch, _)) => {
2709 assert_eq!(arch, "deepseek4");
2710 }
2711 other => panic!(
2712 "expected LoadError::DedicatedArchitectureRequired for deepseek4, got {other:?}"
2713 ),
2714 }
2715 }
2716
2717 #[rustfmt::skip]
2721 const Q5_K_TEST_BLOCK: [u8; 176] = [
2722 0x66, 0x2a, 0x66, 0x2a, 0x01, 0x01, 0x01, 0x01, 0x4f, 0x4b, 0x10, 0x12, 0x41, 0xe2, 0xc1,
2723 0xb1, 0x72, 0x2f, 0x20, 0x07, 0x31, 0x0c, 0x38, 0xb3, 0x9c, 0xb8, 0xad, 0x2f, 0x9a, 0xea,
2724 0x17, 0xd0, 0xee, 0x93, 0x9e, 0x3e, 0x74, 0xbb, 0x28, 0x18, 0x39, 0x25, 0xb6, 0x09, 0x18,
2725 0x29, 0x1c, 0x1d, 0x29, 0x41, 0x40, 0x0a, 0x74, 0x7d, 0xfd, 0x21, 0xdd, 0x6d, 0x45, 0x73,
2726 0x0e, 0x1e, 0xc0, 0x4a, 0xfc, 0xf3, 0x8e, 0x24, 0x6b, 0x34, 0x7d, 0xbe, 0x94, 0xde, 0x59,
2727 0x7a, 0x35, 0x30, 0x36, 0x0a, 0xf9, 0x4a, 0x9b, 0xa2, 0x26, 0x21, 0xa2, 0xfa, 0xdf, 0x4b,
2728 0x29, 0x64, 0x6f, 0xbb, 0xca, 0x0f, 0x3c, 0xda, 0x20, 0xf4, 0x93, 0x86, 0xab, 0x6e, 0xb9,
2729 0xe5, 0xd5, 0xa0, 0x82, 0xd6, 0x41, 0xff, 0x12, 0xbc, 0x34, 0xbb, 0xab, 0xb8, 0x20, 0x2f,
2730 0xbb, 0x5f, 0x0c, 0x10, 0xcf, 0x49, 0xc5, 0x86, 0x5c, 0xdf, 0xff, 0x78, 0x44, 0x26, 0x3b,
2731 0xc2, 0x23, 0x3d, 0x2b, 0xe9, 0x00, 0x12, 0xf8, 0xea, 0xe2, 0x9e, 0x5e, 0x50, 0x20, 0x9f,
2732 0x9d, 0x8d, 0x7d, 0x7f, 0xcc, 0x1d, 0x0e, 0x13, 0xf8, 0xc2, 0xf1, 0x3d, 0x08, 0x2f, 0x23,
2733 0x13, 0xac, 0x0d, 0xa7, 0xe7, 0x20, 0xa3, 0x90, 0xb7, 0xc8, 0x28,
2734 ];
2735
2736 fn build_single_q5_k_tensor_gguf() -> Vec<u8> {
2737 let mut buf = Vec::new();
2738 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2739 .unwrap();
2740 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");
2745
2746 write_string(&mut buf, "test.weight");
2747 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 {
2757 buf.push(0);
2758 }
2759 buf.extend_from_slice(&Q5_K_TEST_BLOCK);
2760 buf
2761 }
2762
2763 fn fused_dot_tolerance(weights: &[f32], x: &[f32], exact_bound: f32) -> f32 {
2783 if !ferrox_core::weight_matrix::cpu_int_dot_enabled() {
2784 return exact_bound;
2785 }
2786 let amax = x.iter().fold(0.0f32, |a, v| a.max(v.abs()));
2787 let l2 = weights.iter().map(|w| w * w).sum::<f32>().sqrt();
2788 4.0 * (amax / 127.0) / 12f32.sqrt() * l2 + exact_bound
2789 }
2790
2791 #[test]
2792 fn load_weight_matrix_handles_a_real_on_disk_q5_k_tensor_end_to_end() {
2793 let tmp = std::env::temp_dir().join(format!(
2794 "ferrox_test_q5k_tensor_{}.gguf",
2795 std::process::id()
2796 ));
2797 std::fs::write(&tmp, build_single_q5_k_tensor_gguf()).unwrap();
2798 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q5_K GGUF file must parse");
2799 std::fs::remove_file(&tmp).ok();
2800
2801 let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_K tensor must load");
2802 assert_eq!(matrix.rows(), 1);
2803 assert_eq!(matrix.cols(), 256);
2804 match &matrix {
2805 WeightMatrix::Quantized { kind, data, .. } => {
2806 assert_eq!(*kind, QuantKind::Q5K);
2807 assert!(
2808 data.is_mapped(),
2809 "Q5_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
2810 );
2811 }
2812 _ => panic!("expected a Quantized matrix for a Q5_K tensor"),
2813 }
2814
2815 let expected = ferrox_quant::dequant_q5_k(&Q5_K_TEST_BLOCK).unwrap();
2816 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
2817 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
2818
2819 let got = matrix.apply(&x);
2820 assert_eq!(got.len(), 1);
2821 assert!(
2822 (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-2),
2823 "end-to-end loaded+applied Q5_K matrix diverged from direct dequant: got={} expected={}",
2824 got[0],
2825 expected_dot
2826 );
2827 }
2828
2829 #[rustfmt::skip]
2838 const Q6_K_TEST_BLOCK: [u8; 210] = [
2839 0xe0, 0xa5, 0x40, 0x5c, 0x8d, 0x3a, 0x0a, 0x26, 0xfb, 0x4b, 0x6e, 0x9a, 0xdf, 0x3e, 0xa3,
2840 0xc4, 0xf8, 0x2b, 0x1d, 0x95, 0x76, 0x7d, 0x3b, 0xcd, 0xfd, 0xef, 0xc2, 0x0b, 0x07, 0x63,
2841 0x29, 0xfb, 0x81, 0x57, 0xbe, 0xbe, 0x06, 0xf7, 0x3a, 0x92, 0xc4, 0x43, 0xff, 0xad, 0xac,
2842 0x7e, 0x0f, 0x00, 0x2a, 0x4f, 0xf0, 0xf8, 0xa9, 0xfa, 0x3c, 0x90, 0x6d, 0x73, 0x2d, 0x5a,
2843 0xe6, 0xc6, 0x46, 0xf2, 0x0d, 0x55, 0x4c, 0x25, 0x38, 0x71, 0x2b, 0x35, 0x38, 0x82, 0x16,
2844 0x37, 0x5f, 0x32, 0x61, 0x02, 0xdd, 0x2f, 0x6f, 0x7b, 0x1f, 0xb4, 0x1a, 0x1b, 0x3e, 0x4f,
2845 0x11, 0xa3, 0x17, 0x40, 0x5a, 0x5f, 0x76, 0xcd, 0x19, 0x27, 0x9b, 0xc7, 0xc8, 0xf7, 0xf7,
2846 0xee, 0xf4, 0x86, 0xd9, 0xfd, 0xa7, 0xfe, 0x9e, 0xac, 0x70, 0x53, 0x5b, 0x76, 0xfb, 0x39,
2847 0xf8, 0x4b, 0x98, 0xfe, 0xd0, 0x06, 0x21, 0x4c, 0x4d, 0xbe, 0x10, 0x2b, 0x06, 0x65, 0xc9,
2848 0x5e, 0xf9, 0x95, 0x72, 0xae, 0x99, 0xd9, 0x7e, 0x15, 0xbd, 0x5e, 0x6d, 0xe8, 0x25, 0x8a,
2849 0xd5, 0x99, 0xc6, 0x6b, 0x69, 0xc7, 0x84, 0xc6, 0xa4, 0xf7, 0xb9, 0x6d, 0x68, 0x45, 0x0e,
2850 0x65, 0x69, 0xeb, 0xe6, 0xeb, 0xe9, 0x28, 0xa6, 0xb9, 0x96, 0xf2, 0xe8, 0xa7, 0x9b, 0x6e,
2851 0x79, 0x8a, 0x68, 0x65, 0x59, 0x98, 0x8b, 0x44, 0x41, 0x98, 0x9a, 0x56, 0x01, 0x01, 0x01,
2852 0x02, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, 0x01, 0x01, 0x01, 0x02, 0x1f, 0x25,
2853 ];
2854
2855 fn build_single_q6_k_tensor_gguf() -> Vec<u8> {
2856 let mut buf = Vec::new();
2857 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2858 .unwrap();
2859 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");
2864
2865 write_string(&mut buf, "test.weight");
2866 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 {
2874 buf.push(0);
2875 }
2876 buf.extend_from_slice(&Q6_K_TEST_BLOCK);
2877 buf
2878 }
2879
2880 #[test]
2881 fn load_weight_matrix_handles_a_real_on_disk_q6_k_tensor_end_to_end() {
2882 let tmp = std::env::temp_dir().join(format!(
2883 "ferrox_test_q6k_tensor_{}.gguf",
2884 std::process::id()
2885 ));
2886 std::fs::write(&tmp, build_single_q6_k_tensor_gguf()).unwrap();
2887 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q6_K GGUF file must parse");
2888 std::fs::remove_file(&tmp).ok();
2889
2890 let matrix = load_weight_matrix(&file, "test.weight").expect("Q6_K tensor must load");
2891 assert_eq!(matrix.rows(), 1);
2892 assert_eq!(matrix.cols(), 256);
2893 match &matrix {
2894 WeightMatrix::Quantized { kind, data, .. } => {
2895 assert_eq!(*kind, QuantKind::Q6K);
2896 assert!(
2897 data.is_mapped(),
2898 "Q6_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
2899 );
2900 }
2901 _ => panic!("expected a Quantized matrix for a Q6_K tensor"),
2902 }
2903
2904 let expected = ferrox_quant::dequant_q6_k(&Q6_K_TEST_BLOCK).unwrap();
2905 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
2906 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
2907
2908 let got = matrix.apply(&x);
2909 assert_eq!(got.len(), 1);
2910 assert!(
2911 (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-2),
2912 "end-to-end loaded+applied Q6_K matrix diverged from direct dequant: got={} expected={}",
2913 got[0],
2914 expected_dot
2915 );
2916 }
2917
2918 fn build_single_bf16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
2919 let mut buf = Vec::new();
2920 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2921 .unwrap();
2922 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");
2927
2928 write_string(&mut buf, "test.weight");
2929 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(cols).unwrap();
2932 buf.write_u64::<LittleEndian>(rows).unwrap();
2933 buf.write_u32::<LittleEndian>(30).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
2937 buf.push(0);
2938 }
2939 for &v in values {
2940 let bf16_bits = (v.to_bits() >> 16) as u16;
2944 buf.extend_from_slice(&bf16_bits.to_le_bytes());
2945 }
2946 buf
2947 }
2948
2949 #[test]
2950 fn load_weight_matrix_handles_a_real_on_disk_bf16_tensor_end_to_end() {
2951 let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
2954 let tmp = std::env::temp_dir().join(format!(
2955 "ferrox_test_bf16_tensor_{}.gguf",
2956 std::process::id()
2957 ));
2958 std::fs::write(&tmp, build_single_bf16_tensor_gguf(2, 3, &values)).unwrap();
2959 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real BF16 GGUF file must parse");
2960 std::fs::remove_file(&tmp).ok();
2961
2962 let matrix = load_weight_matrix(&file, "test.weight").expect("BF16 tensor must load");
2963 assert_eq!(matrix.rows(), 2);
2964 assert_eq!(matrix.cols(), 3);
2965 match &matrix {
2966 WeightMatrix::F32(tensor) => {
2967 assert_eq!(tensor.data, values, "BF16 must widen to f32 exactly");
2968 }
2969 _ => panic!("expected an F32 matrix for a BF16 tensor (no fused dot kernel for it)"),
2970 }
2971 }
2972
2973 fn build_single_f16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
2974 let mut buf = Vec::new();
2975 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2976 .unwrap();
2977 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");
2982
2983 write_string(&mut buf, "test.weight");
2984 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(cols).unwrap();
2986 buf.write_u64::<LittleEndian>(rows).unwrap();
2987 buf.write_u32::<LittleEndian>(1).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
2991 buf.push(0);
2992 }
2993 for &v in values {
2994 buf.extend_from_slice(&half::f16::from_f32(v).to_le_bytes());
2995 }
2996 buf
2997 }
2998
2999 #[test]
3004 fn load_weight_matrix_handles_a_real_on_disk_f16_tensor_end_to_end() {
3005 let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
3006 let tmp = std::env::temp_dir().join(format!(
3007 "ferrox_test_f16_tensor_{}.gguf",
3008 std::process::id()
3009 ));
3010 std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
3011 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
3012 std::fs::remove_file(&tmp).ok();
3013
3014 let matrix = load_weight_matrix(&file, "test.weight").expect("F16 tensor must load");
3015 assert_eq!(matrix.rows(), 2);
3016 assert_eq!(matrix.cols(), 3);
3017 match &matrix {
3018 WeightMatrix::F32(tensor) => {
3019 assert_eq!(tensor.data, values, "F16 must widen to f32 exactly");
3020 }
3021 _ => panic!("expected an F32 matrix for an F16 tensor (no fused dot kernel for it)"),
3022 }
3023
3024 let tmp =
3027 std::env::temp_dir().join(format!("ferrox_test_f16_vec_{}.gguf", std::process::id()));
3028 std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
3029 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
3030 std::fs::remove_file(&tmp).ok();
3031 assert_eq!(load_f32_vec(&file, "test.weight").unwrap(), values);
3032 }
3033
3034 fn build_single_q5_1_tensor_gguf() -> Vec<u8> {
3035 let mut buf = Vec::new();
3036 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3037 .unwrap();
3038 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");
3043
3044 write_string(&mut buf, "test.weight");
3045 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 {
3053 buf.push(0);
3054 }
3055 buf.extend_from_slice(&0x3400u16.to_le_bytes());
3060 buf.extend_from_slice(&0x3E00u16.to_le_bytes());
3061 buf.extend_from_slice(&[0x9au8, 0x3c, 0xf0, 0x0f]);
3062 buf.extend_from_slice(&(0..16u8).map(|i| i | ((15 - i) << 4)).collect::<Vec<u8>>());
3063 buf
3064 }
3065
3066 #[test]
3067 fn load_weight_matrix_handles_a_real_on_disk_q5_1_tensor_end_to_end() {
3068 let tmp = std::env::temp_dir().join(format!(
3069 "ferrox_test_q5_1_tensor_{}.gguf",
3070 std::process::id()
3071 ));
3072 std::fs::write(&tmp, build_single_q5_1_tensor_gguf()).unwrap();
3073 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q5_1 GGUF file must parse");
3074 std::fs::remove_file(&tmp).ok();
3075
3076 let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_1 tensor must load");
3077 assert_eq!(matrix.rows(), 1);
3078 assert_eq!(matrix.cols(), 32);
3079 let raw = file.tensor_bytes("test.weight").unwrap();
3080 let expected = ferrox_quant::dequant_q5_1(raw).unwrap();
3081 match &matrix {
3082 WeightMatrix::Quantized { kind, data, .. } => {
3083 assert_eq!(*kind, QuantKind::Q5_1);
3084 assert!(data.is_mapped());
3085 }
3086 _ => panic!("expected a Quantized matrix for a Q5_1 tensor"),
3087 }
3088
3089 let x: Vec<f32> = (0..32).map(|i| ((i as f32) * 0.017).cos()).collect();
3090 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3091 let got = matrix.apply(&x);
3092 assert_eq!(got.len(), 1);
3093 assert!(
3094 (got[0] - expected_dot).abs() < 1e-2,
3095 "end-to-end loaded+applied Q5_1 matrix diverged from direct dequant: got={} expected={}",
3096 got[0],
3097 expected_dot
3098 );
3099 }
3100
3101 const Q3_K_TEST_BLOCK: [u8; 110] = [
3106 0x56, 0xf2, 0xb4, 0x2b, 0xd5, 0x6f, 0x51, 0x71, 0x3c, 0x0a, 0xb9, 0x1d, 0xd0, 0xb9, 0x3b,
3107 0xb3, 0x0f, 0xff, 0x8c, 0xb2, 0x83, 0x3a, 0x3d, 0x24, 0xb1, 0x12, 0x56, 0xe3, 0x23, 0x54,
3108 0xf2, 0xfa, 0x7f, 0xdf, 0x31, 0xe1, 0x18, 0x26, 0x6e, 0xcd, 0x5b, 0x38, 0xee, 0xbd, 0x9f,
3109 0x8c, 0x57, 0x47, 0x0b, 0x11, 0xcb, 0xfb, 0xb4, 0x83, 0xa0, 0x4e, 0x0b, 0xd4, 0xa7, 0x85,
3110 0xe0, 0x60, 0xf3, 0xb3, 0xe3, 0x95, 0x43, 0xc6, 0x05, 0x05, 0x77, 0x53, 0xed, 0x23, 0xcc,
3111 0x6a, 0x0e, 0x89, 0xa1, 0x79, 0x85, 0xf6, 0x6e, 0x5a, 0x23, 0x63, 0xbe, 0x53, 0xfa, 0xa2,
3112 0x2b, 0xe9, 0xcd, 0xce, 0xf8, 0x3d, 0x6f, 0xd0, 0x42, 0x6e, 0x3b, 0x7f, 0x23, 0x26, 0xd3,
3113 0xb9, 0x18, 0xbf, 0xa4, 0x34,
3114 ];
3115
3116 fn build_single_q3_k_tensor_gguf() -> Vec<u8> {
3117 let mut buf = Vec::new();
3118 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3119 .unwrap();
3120 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");
3125
3126 write_string(&mut buf, "test.weight");
3127 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 {
3135 buf.push(0);
3136 }
3137 buf.extend_from_slice(&Q3_K_TEST_BLOCK);
3138 buf
3139 }
3140
3141 #[test]
3142 fn load_weight_matrix_handles_a_real_on_disk_q3_k_tensor_end_to_end() {
3143 let tmp = std::env::temp_dir().join(format!(
3144 "ferrox_test_q3k_tensor_{}.gguf",
3145 std::process::id()
3146 ));
3147 std::fs::write(&tmp, build_single_q3_k_tensor_gguf()).unwrap();
3148 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q3_K GGUF file must parse");
3149 std::fs::remove_file(&tmp).ok();
3150
3151 let matrix = load_weight_matrix(&file, "test.weight").expect("Q3_K tensor must load");
3152 assert_eq!(matrix.rows(), 1);
3153 assert_eq!(matrix.cols(), 256);
3154 match &matrix {
3155 WeightMatrix::Quantized { kind, data, .. } => {
3156 assert_eq!(*kind, QuantKind::Q3K);
3157 assert!(data.is_mapped());
3158 }
3159 _ => panic!("expected a Quantized matrix for a Q3_K tensor"),
3160 }
3161
3162 let expected = ferrox_quant::dequant_q3_k(&Q3_K_TEST_BLOCK).unwrap();
3163 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3164 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3165
3166 let got = matrix.apply(&x);
3167 assert_eq!(got.len(), 1);
3168 assert!(
3169 (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-1),
3170 "end-to-end loaded+applied Q3_K matrix diverged from direct dequant: got={} expected={}",
3171 got[0],
3172 expected_dot
3173 );
3174 }
3175
3176 const IQ4_XS_TEST_BLOCK: [u8; 136] = [
3180 0x5c, 0x33, 0xb4, 0x39, 0xd1, 0x64, 0x97, 0x82, 0xcb, 0xbd, 0x88, 0x95, 0xf3, 0x60, 0x2a,
3181 0xb5, 0xe7, 0x24, 0xd3, 0xee, 0xfe, 0x71, 0x13, 0xbe, 0x70, 0x84, 0x48, 0x79, 0x7b, 0x3e,
3182 0xf0, 0x55, 0xdc, 0xb2, 0xb2, 0xde, 0x32, 0xa1, 0x5b, 0x02, 0x01, 0xdc, 0x2a, 0xbb, 0xf7,
3183 0x0b, 0x8a, 0x88, 0xdd, 0x0b, 0x02, 0x7e, 0x5e, 0x76, 0x87, 0x30, 0x1e, 0x1c, 0xcf, 0x48,
3184 0xd7, 0x61, 0xf3, 0x51, 0x52, 0x17, 0x98, 0x0a, 0x87, 0xcf, 0x02, 0x91, 0xc8, 0xee, 0xc0,
3185 0x91, 0x69, 0x2a, 0x4f, 0x64, 0x68, 0xa7, 0xb2, 0xe6, 0x98, 0x21, 0x81, 0x75, 0x53, 0x2a,
3186 0x8d, 0x12, 0xae, 0xe0, 0xea, 0x0c, 0x75, 0xff, 0x22, 0x5e, 0x25, 0x19, 0xda, 0x2e, 0x51,
3187 0x4e, 0x81, 0xdc, 0x0e, 0x78, 0x86, 0xd7, 0x58, 0xb5, 0xb7, 0xf6, 0x45, 0xa9, 0x0a, 0x83,
3188 0xfd, 0x2a, 0x12, 0x7d, 0xf0, 0x12, 0x97, 0xe2, 0xfe, 0xf4, 0xd0, 0xa2, 0x11, 0x14, 0x78,
3189 0xdb,
3190 ];
3191
3192 fn build_single_iq4_xs_tensor_gguf() -> Vec<u8> {
3193 let mut buf = Vec::new();
3194 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3195 .unwrap();
3196 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");
3201
3202 write_string(&mut buf, "test.weight");
3203 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 {
3211 buf.push(0);
3212 }
3213 buf.extend_from_slice(&IQ4_XS_TEST_BLOCK);
3214 buf
3215 }
3216
3217 #[test]
3218 fn load_weight_matrix_handles_a_real_on_disk_iq4_xs_tensor_end_to_end() {
3219 let tmp = std::env::temp_dir().join(format!(
3220 "ferrox_test_iq4xs_tensor_{}.gguf",
3221 std::process::id()
3222 ));
3223 std::fs::write(&tmp, build_single_iq4_xs_tensor_gguf()).unwrap();
3224 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real IQ4_XS GGUF file must parse");
3225 std::fs::remove_file(&tmp).ok();
3226
3227 let matrix = load_weight_matrix(&file, "test.weight").expect("IQ4_XS tensor must load");
3228 assert_eq!(matrix.rows(), 1);
3229 assert_eq!(matrix.cols(), 256);
3230 match &matrix {
3231 WeightMatrix::Quantized { kind, data, .. } => {
3232 assert_eq!(*kind, QuantKind::IQ4XS);
3233 assert!(data.is_mapped());
3234 }
3235 _ => panic!("expected a Quantized matrix for an IQ4_XS tensor"),
3236 }
3237
3238 let expected = ferrox_quant::dequant_iq4_xs(&IQ4_XS_TEST_BLOCK).unwrap();
3239 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3240 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3241
3242 let got = matrix.apply(&x);
3243 assert_eq!(got.len(), 1);
3244 assert!(
3245 (got[0] - expected_dot).abs() < 1e-1,
3246 "end-to-end loaded+applied IQ4_XS matrix diverged from direct dequant: got={} expected={}",
3247 got[0],
3248 expected_dot
3249 );
3250 }
3251
3252 const IQ1_S_TEST_BLOCK: [u8; 50] = [
3257 0x0a, 0x2f, 0xfa, 0x06, 0x1e, 0x37, 0x6f, 0xe3, 0x62, 0xd0, 0xb6, 0xa4, 0x25, 0xae, 0x76,
3258 0x14, 0x72, 0x5b, 0xfa, 0x05, 0xd1, 0xf1, 0x2a, 0x4c, 0xad, 0x29, 0xae, 0xf4, 0xcf, 0x0c,
3259 0x96, 0x51, 0x58, 0x03, 0x6d, 0xd3, 0x10, 0x92, 0x70, 0xff, 0x61, 0x58, 0xc8, 0x30, 0x25,
3260 0x64, 0x49, 0x85, 0xc0, 0x24,
3261 ];
3262 const IQ2_XXS_TEST_BLOCK: [u8; 66] = [
3263 0x29, 0x30, 0xd9, 0x33, 0x95, 0x4c, 0x08, 0x1e, 0xad, 0x79, 0x49, 0xf2, 0x8d, 0x5f, 0x93,
3264 0xea, 0x78, 0x18, 0x98, 0xb9, 0x94, 0x14, 0xad, 0xce, 0xca, 0x1d, 0xab, 0x81, 0x53, 0x4a,
3265 0x68, 0xd0, 0x59, 0x96, 0x36, 0x5d, 0xbe, 0x20, 0xc4, 0xff, 0xe4, 0x2c, 0xcd, 0x2f, 0x4f,
3266 0x4f, 0x67, 0x53, 0xc6, 0xd5, 0xa2, 0xfb, 0xc7, 0xf3, 0xe2, 0x6b, 0xf1, 0x99, 0x23, 0x1e,
3267 0x2d, 0x5e, 0x8c, 0x78, 0xc2, 0x31,
3268 ];
3269 const IQ3_XXS_TEST_BLOCK: [u8; 98] = [
3270 0x71, 0x31, 0x16, 0x0a, 0x79, 0x04, 0x5d, 0x87, 0xae, 0x2a, 0x4a, 0x43, 0xfd, 0x02, 0xba,
3271 0x6c, 0x10, 0x42, 0x80, 0xe5, 0x1d, 0x08, 0x22, 0xcb, 0x21, 0x54, 0xf9, 0xaa, 0x8e, 0xc2,
3272 0xf2, 0x34, 0x66, 0x1e, 0x2a, 0xef, 0x19, 0xae, 0x48, 0x47, 0x29, 0xa0, 0x72, 0xd1, 0x31,
3273 0xc0, 0x65, 0x49, 0xde, 0x79, 0x32, 0xe6, 0x4d, 0xb6, 0x55, 0x3f, 0x4d, 0xf1, 0x18, 0xbb,
3274 0x18, 0x59, 0x4c, 0x31, 0xa3, 0xb2, 0x34, 0xdd, 0xf6, 0x4a, 0x91, 0x51, 0x3f, 0x3e, 0x40,
3275 0x69, 0xad, 0xbf, 0x1a, 0xd0, 0x05, 0xfb, 0xbe, 0x8b, 0x0b, 0xdd, 0xdf, 0x7d, 0x94, 0x74,
3276 0x92, 0x3e, 0xff, 0x04, 0x2a, 0xc4, 0xea, 0xc9,
3277 ];
3278
3279 #[rustfmt::skip]
3280 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];
3281
3282 fn build_single_iq_lowbit_tensor_gguf(
3283 arch: &str,
3284 tag: u32,
3285 cols: u64,
3286 block: &[u8],
3287 ) -> Vec<u8> {
3288 let mut buf = Vec::new();
3289 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3290 .unwrap();
3291 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);
3295 write_string(&mut buf, "test.weight");
3296 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(cols).unwrap();
3298 buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u32::<LittleEndian>(tag).unwrap();
3300 buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
3302 buf.push(0);
3303 }
3304 buf.extend_from_slice(block);
3305 buf
3306 }
3307
3308 fn pseudo_iq_block(len: usize, seed: u32) -> Vec<u8> {
3315 let mut s = seed;
3316 let mut out = Vec::with_capacity(len);
3317 for _ in 0..len {
3318 s ^= s << 13;
3319 s ^= s >> 17;
3320 s ^= s << 5;
3321 out.push((s >> 24) as u8);
3322 }
3323 out
3324 }
3325
3326 #[test]
3337 fn load_weight_matrix_handles_real_on_disk_iq_lowbit_tensors_end_to_end() {
3338 type DequantFn = fn(&[u8]) -> Result<Vec<f32>, ferrox_quant::QuantError>;
3339 let mut iq1m = pseudo_iq_block(ferrox_quant::IQ1_M_BLOCK_BYTES, 0x2907_31A0);
3346 iq1m[55] = (iq1m[55] & 0x0F) | 0x20;
3347 let mut iq2xs = pseudo_iq_block(ferrox_quant::IQ2_XS_BLOCK_BYTES, 0x2107_31A1);
3348 let mut iq2s = pseudo_iq_block(ferrox_quant::IQ2_S_BLOCK_BYTES, 0x2207_31A2);
3349 let mut iq3s = pseudo_iq_block(ferrox_quant::IQ3_S_BLOCK_BYTES, 0x2307_31A3);
3350 for blk in [&mut iq2xs, &mut iq2s, &mut iq3s] {
3351 blk[0..2].copy_from_slice(&half::f16::from_f32(0.115).to_le_bytes());
3352 }
3353 let cases: [(&str, u32, &[u8], QuantKind, DequantFn); 8] = [
3354 (
3355 "iq1s",
3356 19,
3357 &IQ1_S_TEST_BLOCK,
3358 QuantKind::IQ1S,
3359 ferrox_quant::dequant_iq1_s,
3360 ),
3361 (
3362 "iq1m",
3363 29,
3364 &iq1m,
3365 QuantKind::IQ1M,
3366 ferrox_quant::dequant_iq1_m,
3367 ),
3368 (
3369 "iq2xxs",
3370 16,
3371 &IQ2_XXS_TEST_BLOCK,
3372 QuantKind::IQ2XXS,
3373 ferrox_quant::dequant_iq2_xxs,
3374 ),
3375 (
3376 "iq2xs",
3377 17,
3378 &iq2xs,
3379 QuantKind::IQ2XS,
3380 ferrox_quant::dequant_iq2_xs,
3381 ),
3382 (
3383 "iq2s",
3384 22,
3385 &iq2s,
3386 QuantKind::IQ2S,
3387 ferrox_quant::dequant_iq2_s,
3388 ),
3389 (
3390 "iq3xxs",
3391 18,
3392 &IQ3_XXS_TEST_BLOCK,
3393 QuantKind::IQ3XXS,
3394 ferrox_quant::dequant_iq3_xxs,
3395 ),
3396 (
3397 "iq3s",
3398 21,
3399 &iq3s,
3400 QuantKind::IQ3S,
3401 ferrox_quant::dequant_iq3_s,
3402 ),
3403 (
3404 "mxfp4_gguf",
3405 39,
3406 &MXFP4_GGUF_TEST_BLOCKS,
3407 QuantKind::Mxfp4Gguf,
3408 ferrox_quant::dequant_mxfp4_gguf,
3409 ),
3410 ];
3411 for (name, tag, block, kind, dequant) in cases {
3412 let expected = dequant(block).unwrap();
3413 let cols = expected.len();
3414 let tmp = std::env::temp_dir().join(format!("ferrox_test_{name}_tensor.gguf"));
3415 std::fs::write(
3416 &tmp,
3417 build_single_iq_lowbit_tensor_gguf(name, tag, cols as u64, block),
3418 )
3419 .unwrap();
3420 let file = ferrox_gguf::GgufFile::open(&tmp).expect("file must parse");
3421 std::fs::remove_file(&tmp).ok();
3422
3423 let matrix =
3424 load_weight_matrix(&file, "test.weight").expect("low-bit tensor must load");
3425 assert_eq!((matrix.rows(), matrix.cols()), (1, cols), "{name}");
3426 match &matrix {
3427 WeightMatrix::Quantized { kind: k, data, .. } => {
3428 assert_eq!(*k, kind, "{name}");
3429 assert!(data.is_mapped(), "{name} must load zero-copy");
3430 }
3431 _ => panic!("expected a Quantized matrix for {name}"),
3432 }
3433
3434 let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.013).sin()).collect();
3435 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3436 let got = matrix.apply(&x);
3437 assert!(
3438 (got[0] - expected_dot).abs() < 1e-1,
3439 "{name}: loaded+applied diverged from direct dequant: got={} expected={}",
3440 got[0],
3441 expected_dot
3442 );
3443 }
3444 }
3445
3446 #[test]
3447 fn qwen2moe_disables_topk_renorm() {
3448 assert!(
3449 NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"qwen2moe"),
3450 "qwen2moe must have norm_topk_prob=false (llama.cpp build_moe_ffn norm_w=false)"
3451 );
3452 }
3453
3454 #[test]
3465 fn an_architecture_with_no_rope_is_refused_by_name() {
3466 for arch in ["gpt2", "mpt", "refact", "bloom", "jais"] {
3467 let file = open_metadata_gguf(
3468 &format!("norope_{arch}"),
3469 &[("general.architecture", Kv::Str(arch))],
3470 );
3471 match ModelConfig::from_gguf(&file) {
3472 Err(LoadError::DedicatedArchitectureRequired(got, reason)) => {
3473 assert_eq!(got, arch);
3474 assert!(
3475 reason.contains("ALiBi") || reason.contains("position embeddings"),
3476 "{arch}: the refusal must name what is missing, got {reason:?}"
3477 );
3478 }
3479 other => panic!("{arch} must be refused, got {other:?}"),
3480 }
3481 }
3482 }
3483
3484 #[test]
3490 fn baichuan_13b_is_refused_because_it_uses_alibi_and_the_7b_is_not() {
3491 let thirteen_b = open_metadata_gguf(
3492 "baichuan13b",
3493 &[
3494 ("general.architecture", Kv::Str("baichuan")),
3495 ("baichuan.block_count", Kv::U32(40)),
3496 ],
3497 );
3498 match ModelConfig::from_gguf(&thirteen_b) {
3499 Err(LoadError::UnsupportedFeature(arch, msg)) => {
3500 assert_eq!(arch, "baichuan");
3501 assert!(msg.contains("ALiBi"), "{msg}");
3502 assert!(
3503 msg.contains("40"),
3504 "the refusal must name the layer count: {msg}"
3505 );
3506 }
3507 other => panic!("Baichuan-13B must be refused, got {other:?}"),
3508 }
3509
3510 let seven_b = open_metadata_gguf(
3514 "baichuan7b",
3515 &[
3516 ("general.architecture", Kv::Str("baichuan")),
3517 ("baichuan.block_count", Kv::U32(32)),
3518 ],
3519 );
3520 match ModelConfig::from_gguf(&seven_b) {
3521 Err(LoadError::MissingHparam(key)) => assert_eq!(key, "baichuan.embedding_length"),
3522 other => panic!("Baichuan-7B must pass the ALiBi gate, got {other:?}"),
3523 }
3524 }
3525}