1use ferrox_core::expert_store::{ExpertKey, ExpertSource, ExpertStore};
26use ferrox_core::tensor::Tensor;
27use ferrox_core::weight_matrix::{QuantKind, WeightBytes, WeightMatrix};
28use ferrox_gguf::{GgmlType, GgufError, GgufValue, ShardedGguf, TensorInfo, TensorSource};
29use ferrox_moe::{ExpertWeights, GatingFunction, MoeLayerConfig};
30use std::sync::Arc;
31use thiserror::Error;
32
33use crate::config::ModelConfig;
34#[cfg(feature = "metal")]
35use crate::decoder::MoePackedQ4Planes;
36use crate::decoder::{AttnWeights, Decoder, ExpertBacking, LayerWeights, MoeWeights};
37
38#[derive(Debug, Error)]
39pub enum LoadError {
40 #[error(transparent)]
41 Gguf(#[from] GgufError),
42 #[error(transparent)]
43 Shard(#[from] ferrox_gguf::ShardError),
44 #[error("tensor '{0}' has unsupported dtype {1:?}")]
45 UnsupportedDtype(String, GgmlType),
46 #[error(
47 "MoE tensor '{0}' is not 3D or its expert count {1} does not match config n_experts {2}"
48 )]
49 ExpertCountMismatch(String, usize, usize),
50 #[error("GGUF file is missing required hparam metadata key '{0}'")]
51 MissingHparam(String),
52 #[error(
55 "unsupported GGUF architecture '{0}': not in ferrox's capability registry \
56 (unknown required features fail closed; see ferrox_models::capability)"
57 )]
58 UnsupportedArchitecture(String),
59 #[error("architecture '{0}' cannot use the generic Decoder: {1}")]
61 DedicatedArchitectureRequired(String, &'static str),
62 #[error("architecture '{0}' requires unimplemented feature: {1}")]
64 UnsupportedFeature(String, String),
65 #[error(
69 "checkpoint carries {0} tensor(s) this build never reads, so its graph is not the one \
70 ferrox would run: {1}. This is a missing feature, not a corrupt file. Override with \
71 FERROX_ALLOW_UNKNOWN_TENSORS=1 to load anyway and accept wrong output."
72 )]
73 UnconsumedTensors(usize, String),
74 #[error("{0}")]
80 StrictKernels(String),
81}
82
83const SIGMOID_GATING_ARCHITECTURES: &[&str] = &["deepseek2", "glm4moe"];
89
90const NO_TOPK_RENORMALIZE_ARCHITECTURES: &[&str] = &["olmoe", "qwen2moe"];
109
110fn metadata_u64_any(file: &impl TensorSource, keys: &[String]) -> Option<u64> {
111 keys.iter().find_map(|k| file.metadata_u64(k))
112}
113
114fn metadata_f32_any(file: &impl TensorSource, keys: &[String]) -> Option<f32> {
115 keys.iter()
116 .find_map(|k| file.metadata(k).and_then(GgufValue::as_f32))
117}
118
119impl ModelConfig {
120 pub fn from_gguf(file: &impl TensorSource) -> Result<Self, LoadError> {
134 let arch = file
135 .metadata_str("general.architecture")
136 .ok_or_else(|| LoadError::MissingHparam("general.architecture".to_string()))?
137 .to_string();
138 let arch_profile = crate::capability::resolve_profile(&arch)
139 .ok_or_else(|| LoadError::UnsupportedArchitecture(arch.clone()))?;
140 let rope_layout = match arch_profile.path {
141 crate::capability::ArchPath::GenericGqa { rope }
142 | crate::capability::ArchPath::TestFixture { rope } => rope,
143 crate::capability::ArchPath::DedicatedOnly { reason } => {
144 return Err(LoadError::DedicatedArchitectureRequired(
145 arch.clone(),
146 reason,
147 ));
148 }
149 crate::capability::ArchPath::Deferred { reason } => {
150 return Err(LoadError::UnsupportedFeature(
151 arch.clone(),
152 format!("architecture deferred from Ferrox text-generation scope: {reason}"),
153 ));
154 }
155 };
156 let qk_norm_style = arch_profile.qk_norm;
157 for (meta_key, feature) in crate::capability::unsupported_feature_keys(&arch) {
158 if let Some(v) = metadata_f32_any(file, std::slice::from_ref(&meta_key)) {
159 if v > 0.0 {
160 return Err(LoadError::UnsupportedFeature(
161 arch.clone(),
162 format!("{feature} (metadata {meta_key}={v})"),
163 ));
164 }
165 }
166 if let Some(v) = metadata_u64_any(file, std::slice::from_ref(&meta_key)) {
167 if v > 0 {
168 return Err(LoadError::UnsupportedFeature(
169 arch.clone(),
170 feature.to_string(),
171 ));
172 }
173 }
174 }
175 let key = |suffix: &str| format!("{arch}.{suffix}");
176
177 let name: &'static str = Box::leak(
178 file.metadata_str("general.name")
179 .unwrap_or(&arch)
180 .to_string()
181 .into_boxed_str(),
182 );
183
184 let n_layers =
185 file.metadata_u64(&key("block_count"))
186 .ok_or_else(|| LoadError::MissingHparam(key("block_count")))? as usize;
187 let hidden_dim = file
188 .metadata_u64(&key("embedding_length"))
189 .ok_or_else(|| LoadError::MissingHparam(key("embedding_length")))?
190 as usize;
191 let n_heads = file
192 .metadata_u64(&key("attention.head_count"))
193 .ok_or_else(|| LoadError::MissingHparam(key("attention.head_count")))?
194 as usize;
195
196 let mut best_effort_fields: Vec<&'static str> = Vec::new();
197
198 let n_kv_heads = file
199 .metadata_u64(&key("attention.head_count_kv"))
200 .map(|v| v as usize)
201 .unwrap_or_else(|| {
202 best_effort_fields.push("n_kv_heads (no attention.head_count_kv key; assumed equal to n_heads, i.e. plain MHA)");
203 n_heads
204 });
205 let head_dim = file
206 .metadata_u64(&key("attention.key_length"))
207 .map(|v| v as usize)
208 .unwrap_or_else(|| {
209 best_effort_fields.push(
210 "head_dim (no attention.key_length key; derived as hidden_dim / n_heads)",
211 );
212 hidden_dim / n_heads
213 });
214 let v_head_dim = file
215 .metadata_u64(&key("attention.value_length"))
216 .map(|v| v as usize)
217 .unwrap_or(head_dim);
218 if v_head_dim != head_dim {
219 return Err(LoadError::UnsupportedFeature(
220 arch.clone(),
221 format!(
222 "split K/V head dims (key_length={head_dim}, value_length={v_head_dim}); \
223 generic decoder requires equal head dims"
224 ),
225 ));
226 }
227 let vocab_size = file
228 .metadata("tokenizer.ggml.tokens")
229 .and_then(|v| match v {
230 GgufValue::Array(items) => Some(items.len()),
231 _ => None,
232 })
233 .or_else(|| file.metadata_u64(&key("vocab_size")).map(|v| v as usize))
234 .unwrap_or_else(|| {
235 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)");
236 file.find_tensor("output.weight")
241 .and_then(|t| t.shape.last().copied())
242 .unwrap_or(0) as usize
243 });
244 let rope_theta = metadata_f32_any(file, &[key("rope.freq_base")]).unwrap_or_else(|| {
245 best_effort_fields.push("rope_theta (no rope.freq_base key; defaulted to 10000.0)");
246 10000.0
247 });
248 let rms_norm_eps = metadata_f32_any(
249 file,
250 &[
251 key("attention.layer_norm_rms_epsilon"),
252 key("attention.layer_norm_epsilon"),
253 ],
254 )
255 .unwrap_or_else(|| {
256 best_effort_fields
257 .push("rms_norm_eps (no layer_norm_rms_epsilon key; defaulted to 1e-5)");
258 1e-5
259 });
260
261 let n_experts = metadata_u64_any(file, &[key("expert_count")]).unwrap_or(0) as usize;
262 let is_moe = n_experts > 1;
263
264 let n_experts_active = if is_moe {
265 metadata_u64_any(file, &[key("expert_used_count")]).unwrap_or_else(|| {
266 best_effort_fields
267 .push("moe.n_experts_active (no expert_used_count key; defaulted to 2)");
268 2
269 }) as usize
270 } else {
271 1
272 };
273 let n_shared_experts = match metadata_u64_any(file, &[key("expert_shared_count")]) {
279 Some(n) => n as usize,
280 None if is_moe && file.find_tensor("blk.0.ffn_gate_shexp.weight").is_some() => {
281 best_effort_fields.push(
282 "moe.n_shared_experts (no expert_shared_count; inferred 1 from blk.0.ffn_gate_shexp.weight)",
283 );
284 1
285 }
286 None => 0,
287 };
288 let feed_forward_length = metadata_u64_any(file, &[key("feed_forward_length")]);
294 let expert_ffn_dim = metadata_u64_any(file, &[key("expert_feed_forward_length")])
295 .or_else(|| {
296 feed_forward_length.map(|ff| {
297 if is_moe && n_experts_active > 0 {
298 ff / n_experts_active as u64
299 } else {
300 ff
301 }
302 })
303 })
304 .unwrap_or_else(|| {
305 best_effort_fields.push(
306 "moe.expert_ffn_dim (no expert_feed_forward_length/feed_forward_length; defaulted to 4x hidden_dim)",
307 );
308 (hidden_dim * 4) as u64
309 }) as usize;
310 let n_dense_leading_layers =
311 metadata_u64_any(file, &[key("leading_dense_block_count")]).unwrap_or(0) as usize;
312
313 let gating = match metadata_u64_any(file, &[key("expert_gating_func")]) {
319 Some(2) => GatingFunction::Sigmoid,
320 Some(1) => GatingFunction::Softmax,
321 _ => {
322 if SIGMOID_GATING_ARCHITECTURES.contains(&arch.as_str()) {
323 GatingFunction::Sigmoid
324 } else {
325 if is_moe {
326 best_effort_fields.push(
327 "moe.gating (no expert_gating_func key and architecture not in the known-sigmoid list; defaulted to softmax)",
328 );
329 }
330 GatingFunction::Softmax
331 }
332 }
333 };
334
335 let norm_topk_prob = !NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&arch.as_str());
339 if is_moe && matches!(gating, GatingFunction::Softmax) {
340 best_effort_fields.push(
341 "moe.norm_topk_prob (no GGUF metadata key exists for this; defaulted by architecture-name lookup against NO_TOPK_RENORMALIZE_ARCHITECTURES)",
342 );
343 }
344
345 let sliding_window = metadata_u64_any(file, &[key("attention.sliding_window")])
355 .map(|v| v as usize)
356 .filter(|&w| w > 0);
357
358 let swa_pattern = metadata_u64_any(file, &[key("attention.sliding_window_pattern")])
362 .map(|v| v as usize)
363 .filter(|&p| p > 1)
364 .or_else(|| {
365 sliding_window?;
366 crate::capability::default_swa_pattern(&arch).or(
371 match arch_profile.family {
374 crate::capability::DecoderFamily::GemmaFamily => Some(6),
375 _ => None,
376 },
377 )
378 });
379
380 let attn_logit_softcap = metadata_f32_any(
381 file,
382 &[
383 key("attention.logit_softcapping"),
384 key("attn_logit_softcapping"),
385 ],
386 )
387 .filter(|&v| v > 0.0);
388 let final_logit_softcap =
389 metadata_f32_any(file, &[key("final_logit_softcapping")]).filter(|&v| v > 0.0);
390
391 let embedding_scale = if matches!(
393 arch_profile.family,
394 crate::capability::DecoderFamily::GemmaFamily
395 ) {
396 Some((hidden_dim as f32).sqrt())
397 } else {
398 None
399 };
400
401 let attention_scale = None;
406
407 let rope_theta_swa = if sliding_window.is_some() {
412 let fallback = if crate::capability::swa_rope_base_follows_model(&arch) {
413 rope_theta
414 } else {
415 10_000.0
416 };
417 Some(
418 metadata_f32_any(
419 file,
420 &[key("rope.freq_base_swa"), key("rope_freq_base_swa")],
421 )
422 .unwrap_or(fallback),
423 )
424 } else {
425 None
426 };
427
428 let ffn_activation = match arch_profile.family {
429 crate::capability::DecoderFamily::GemmaFamily => crate::config::FfnActivation::Gelu,
430 crate::capability::DecoderFamily::PhiFamily => {
431 crate::config::FfnActivation::SwigluFused
432 }
433 _ => crate::config::FfnActivation::Swiglu,
434 };
435
436 let rope_freqs = load_f32_vec_optional(file, "rope_freqs.weight")?;
443
444 let rope_orig_ctx = metadata_u64_any(file, &[key("rope.scaling.original_context_length")])
457 .map(|v| v as usize);
458 let (rope_freqs_long, rope_freqs_short) = if rope_freqs.is_some() {
463 (None, None)
464 } else {
465 (
466 load_f32_vec_optional(file, "rope_factors_long.weight")?,
467 load_f32_vec_optional(file, "rope_factors_short.weight")?,
468 )
469 };
470 let rope_freqs = match (rope_freqs, rope_orig_ctx) {
474 (Some(f), _) => Some(f),
475 (None, Some(orig)) => {
476 let model_ctx = metadata_u64_any(file, &[key("context_length")])
477 .unwrap_or(orig as u64) as usize;
478 if model_ctx > orig {
479 rope_freqs_long.clone().or_else(|| rope_freqs_short.clone())
480 } else {
481 rope_freqs_short.clone().or_else(|| rope_freqs_long.clone())
482 }
483 }
484 (None, None) => None,
485 };
486
487 let rope_dim = metadata_u64_any(file, &[key("rope.dimension_count")])
492 .map(|d| d as usize)
493 .filter(|d| *d > 0 && *d < head_dim);
494
495 let rope_attn_factor = metadata_f32_any(file, &[key("rope.scaling.attn_factor")])
497 .filter(|f| f.is_finite() && *f > 0.0)
498 .unwrap_or(1.0);
499
500 if best_effort_fields.is_empty() {
505 best_effort_fields.push(
506 "none -- every field above was read directly from this file's own GGUF metadata",
507 );
508 }
509
510 Ok(ModelConfig {
511 name,
512 n_layers,
513 hidden_dim,
514 n_heads,
515 n_kv_heads,
516 head_dim,
517 vocab_size,
518 rope_theta,
519 rms_norm_eps,
520 attention: crate::config::AttentionKind::Gqa,
524 sliding_window,
525 swa_pattern,
526 moe: MoeLayerConfig {
527 n_experts: n_experts.max(1),
528 n_experts_active,
529 n_shared_experts,
530 hidden_dim,
531 expert_ffn_dim,
532 gating,
533 norm_topk_prob,
534 expert_group_count: metadata_u64_any(file, &[key("expert_group_count")])
535 .map(|v| v as usize)
536 .filter(|&c| c > 1),
537 expert_group_used_count: metadata_u64_any(file, &[key("expert_group_used_count")])
538 .map(|v| v as usize)
539 .filter(|&c| c > 0),
540 },
541 n_dense_leading_layers,
542 rope_freqs,
543 rope_layout,
544 qk_norm_style,
545 attn_logit_softcap,
546 final_logit_softcap,
547 embedding_scale,
548 attention_scale,
549 rope_attn_factor,
550 rope_dim,
551 rope_freqs_long,
552 rope_freqs_short,
553 rope_orig_ctx,
554 rope_theta_swa,
555 ffn_activation,
556 best_effort_fields: Box::leak(best_effort_fields.into_boxed_slice()),
557 })
558 }
559}
560
561fn find_info<'a>(file: &'a impl TensorSource, name: &str) -> Result<&'a TensorInfo, LoadError> {
562 file.find_tensor(name)
563 .ok_or_else(|| LoadError::Gguf(GgufError::TensorNotFound(name.to_string())))
564}
565
566fn quant_kind_for(dtype: GgmlType) -> Option<QuantKind> {
570 match dtype {
571 GgmlType::Q8_0 => Some(QuantKind::Q8_0),
572 GgmlType::Q4_0 => Some(QuantKind::Q4_0),
573 GgmlType::Q4K => Some(QuantKind::Q4K),
574 GgmlType::Q5K => Some(QuantKind::Q5K),
575 GgmlType::Q6K => Some(QuantKind::Q6K),
576 GgmlType::Q2K => Some(QuantKind::Q2K),
577 GgmlType::Q3K => Some(QuantKind::Q3K),
578 GgmlType::Q4_1 => Some(QuantKind::Q4_1),
579 GgmlType::Q5_0 => Some(QuantKind::Q5_0),
580 GgmlType::Q5_1 => Some(QuantKind::Q5_1),
581 GgmlType::Q8_1 => Some(QuantKind::Q8_1),
582 GgmlType::IQ4NL => Some(QuantKind::IQ4NL),
583 GgmlType::IQ4XS => Some(QuantKind::IQ4XS),
584 GgmlType::IQ2XS => Some(QuantKind::IQ2XS),
585 GgmlType::IQ2S => Some(QuantKind::IQ2S),
586 GgmlType::IQ3S => Some(QuantKind::IQ3S),
587 GgmlType::IQ1M => Some(QuantKind::IQ1M),
588 GgmlType::IQ1S => Some(QuantKind::IQ1S),
589 GgmlType::IQ2XXS => Some(QuantKind::IQ2XXS),
590 GgmlType::IQ3XXS => Some(QuantKind::IQ3XXS),
591 GgmlType::MXFP4 => Some(QuantKind::Mxfp4Gguf),
592 _ => None,
593 }
594}
595
596fn load_gpt_oss_layer(
622 file: &impl TensorSource,
623 l: usize,
624 config: &ModelConfig,
625) -> Result<crate::decoder::GptOssLayer, LoadError> {
626 let n_experts = config.moe.n_experts;
627 let ff = config.moe.expert_ffn_dim;
628
629 let want = |name: &str, got: usize, expect: usize| -> Result<(), LoadError> {
630 if got == expect {
631 Ok(())
632 } else {
633 Err(LoadError::UnsupportedFeature(
634 config.name.to_string(),
635 format!("{name} has {got} elements, expected {expect}"),
636 ))
637 }
638 };
639
640 let attn_sinks = load_f32_vec(file, &format!("blk.{l}.attn_sinks.weight"))?;
641 want(
642 &format!("blk.{l}.attn_sinks.weight"),
643 attn_sinks.len(),
644 config.n_heads,
645 )?;
646 let o_bias = load_f32_vec(file, &format!("blk.{l}.attn_output.bias"))?;
647 want(
648 &format!("blk.{l}.attn_output.bias"),
649 o_bias.len(),
650 config.hidden_dim,
651 )?;
652 let router_bias = load_f32_vec(file, &format!("blk.{l}.ffn_gate_inp.bias"))?;
653 want(
654 &format!("blk.{l}.ffn_gate_inp.bias"),
655 router_bias.len(),
656 n_experts,
657 )?;
658
659 let gate_b = load_f32_vec(file, &format!("blk.{l}.ffn_gate_exps.bias"))?;
660 want(
661 &format!("blk.{l}.ffn_gate_exps.bias"),
662 gate_b.len(),
663 n_experts * ff,
664 )?;
665 let up_b = load_f32_vec(file, &format!("blk.{l}.ffn_up_exps.bias"))?;
666 want(
667 &format!("blk.{l}.ffn_up_exps.bias"),
668 up_b.len(),
669 n_experts * ff,
670 )?;
671 let down_b = load_f32_vec(file, &format!("blk.{l}.ffn_down_exps.bias"))?;
672 want(
673 &format!("blk.{l}.ffn_down_exps.bias"),
674 down_b.len(),
675 n_experts * config.hidden_dim,
676 )?;
677
678 let expert_bias = (0..n_experts)
679 .map(|e| ferrox_moe::ExpertBias {
680 gate: gate_b[e * ff..(e + 1) * ff].to_vec(),
681 up: up_b[e * ff..(e + 1) * ff].to_vec(),
682 down: down_b[e * config.hidden_dim..(e + 1) * config.hidden_dim].to_vec(),
683 })
684 .collect();
685
686 Ok(crate::decoder::GptOssLayer {
687 attn_sinks,
688 o_bias,
689 router_bias,
690 expert_bias,
691 })
692}
693
694fn load_f32_vec_optional(
695 file: &impl TensorSource,
696 name: &str,
697) -> Result<Option<Vec<f32>>, LoadError> {
698 if file.find_tensor(name).is_none() {
699 return Ok(None);
700 }
701 Ok(Some(load_f32_vec(file, name)?))
702}
703
704fn slice_quantized_rows(m: &WeightMatrix, start: usize, n: usize) -> Option<WeightMatrix> {
711 let WeightMatrix::Quantized {
712 data,
713 rows,
714 cols,
715 kind,
716 } = m
717 else {
718 return None;
719 };
720 let total = data.len();
721 if *rows == 0 || total % *rows != 0 || start + n > *rows {
722 return None;
723 }
724 let row_bytes = total / *rows;
725 let (b0, b1) = (start * row_bytes, (start + n) * row_bytes);
726 let bytes = match data {
727 WeightBytes::Mapped { mmap, range } => WeightBytes::Mapped {
728 mmap: mmap.clone(),
729 range: range.start + b0..range.start + b1,
730 },
731 other => WeightBytes::Owned(other.as_slice()[b0..b1].to_vec()),
732 };
733 Some(WeightMatrix::Quantized {
734 data: bytes,
735 rows: n,
736 cols: *cols,
737 kind: *kind,
738 })
739}
740
741fn load_qkv_projections(
746 file: &impl TensorSource,
747 layer: usize,
748 config: &ModelConfig,
749) -> Result<(WeightMatrix, WeightMatrix, WeightMatrix), LoadError> {
750 let q_name = format!("blk.{layer}.attn_q.weight");
751 let k_name = format!("blk.{layer}.attn_k.weight");
752 let v_name = format!("blk.{layer}.attn_v.weight");
753 let fused_name = format!("blk.{layer}.attn_qkv.weight");
754
755 if file.find_tensor(&q_name).is_some() {
756 return Ok((
757 load_weight_matrix(file, &q_name)?,
758 load_weight_matrix(file, &k_name)?,
759 load_weight_matrix(file, &v_name)?,
760 ));
761 }
762 if file.find_tensor(&fused_name).is_none() {
763 return Err(LoadError::Gguf(GgufError::TensorNotFound(q_name)));
764 }
765
766 let fused = load_weight_matrix(file, &fused_name)?;
767 let q_rows = config.n_heads * config.head_dim;
768 let kv_rows = config.n_kv_heads * config.head_dim;
769 let expected = q_rows + 2 * kv_rows;
770 if fused.rows() != expected {
771 return Err(LoadError::UnsupportedFeature(
773 config.name.to_string(),
774 format!(
775 "{fused_name} has {} rows; expected q+k+v = {} \
776 (n_heads*head_dim + 2*n_kv_heads*head_dim)",
777 fused.rows(),
778 expected
779 ),
780 ));
781 }
782 let cols = fused.cols();
783 if let (Some(q), Some(k), Some(v)) = (
786 slice_quantized_rows(&fused, 0, q_rows),
787 slice_quantized_rows(&fused, q_rows, kv_rows),
788 slice_quantized_rows(&fused, q_rows + kv_rows, kv_rows),
789 ) {
790 return Ok((q, k, v));
791 }
792 let mut full = Vec::with_capacity(fused.rows() * cols);
794 for r in 0..fused.rows() {
795 full.extend_from_slice(&fused.dequant_row(r));
796 }
797 let q = WeightMatrix::F32(Tensor::new(
798 full[..q_rows * cols].to_vec(),
799 vec![q_rows, cols],
800 ));
801 let k = WeightMatrix::F32(Tensor::new(
802 full[q_rows * cols..(q_rows + kv_rows) * cols].to_vec(),
803 vec![kv_rows, cols],
804 ));
805 let v = WeightMatrix::F32(Tensor::new(
806 full[(q_rows + kv_rows) * cols..].to_vec(),
807 vec![kv_rows, cols],
808 ));
809 Ok((q, k, v))
810}
811
812fn load_dense_expert(
815 file: &impl TensorSource,
816 layer: usize,
817 config: &ModelConfig,
818) -> Result<ExpertWeights, LoadError> {
819 let gate_name = format!("blk.{layer}.ffn_gate.weight");
820 let up_name = format!("blk.{layer}.ffn_up.weight");
821 let down_name = format!("blk.{layer}.ffn_down.weight");
822 if file.find_tensor(&gate_name).is_some() {
823 return Ok(ExpertWeights {
824 gate: load_weight_matrix(file, &gate_name)?,
825 up: load_weight_matrix(file, &up_name)?,
826 down: load_weight_matrix(file, &down_name)?,
827 });
828 }
829 let fused = load_weight_matrix(file, &up_name)?;
831 let ff = config.moe.expert_ffn_dim;
832 if fused.rows() != 2 * ff {
833 return Err(LoadError::UnsupportedFeature(
834 config.name.to_string(),
835 format!(
836 "{up_name} has {} rows without a companion ffn_gate; \
837 expected fused SwiGLU with 2*ffn_dim = {} rows",
838 fused.rows(),
839 2 * ff
840 ),
841 ));
842 }
843 let cols = fused.cols();
844 if let (Some(gate), Some(up)) = (
846 slice_quantized_rows(&fused, 0, ff),
847 slice_quantized_rows(&fused, ff, ff),
848 ) {
849 return Ok(ExpertWeights {
850 gate,
851 up,
852 down: load_weight_matrix(file, &down_name)?,
853 });
854 }
855 let mut full = Vec::with_capacity(fused.rows() * cols);
856 for r in 0..fused.rows() {
857 full.extend_from_slice(&fused.dequant_row(r));
858 }
859 let gate = WeightMatrix::F32(Tensor::new(full[..ff * cols].to_vec(), vec![ff, cols]));
860 let up = WeightMatrix::F32(Tensor::new(full[ff * cols..].to_vec(), vec![ff, cols]));
861 Ok(ExpertWeights {
862 gate,
863 up,
864 down: load_weight_matrix(file, &down_name)?,
865 })
866}
867
868pub(crate) fn widen_plain_float(
877 dtype: GgmlType,
878 raw: &[u8],
879 name: &str,
880) -> Result<Vec<f32>, LoadError> {
881 match dtype {
882 GgmlType::F32 => {
883 let mut out = Vec::with_capacity(raw.len() / 4);
884 for chunk in raw.chunks_exact(4) {
885 out.push(f32::from_le_bytes(chunk.try_into().unwrap()));
886 }
887 Ok(out)
888 }
889 GgmlType::F16 => ferrox_quant::dequant_f16(raw)
890 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::F16)),
891 GgmlType::BF16 => ferrox_quant::dequant_bf16(raw)
892 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::BF16)),
893 other => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
894 }
895}
896
897fn load_f32_vec(file: &impl TensorSource, name: &str) -> Result<Vec<f32>, LoadError> {
898 let info = find_info(file, name)?;
899 let raw = file.tensor_bytes(name)?;
900 match info.dtype {
901 GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => widen_plain_float(info.dtype, raw, name),
902 GgmlType::Q8_0 => ferrox_quant::dequant_q8_0(raw)
903 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q8_0)),
904 GgmlType::Q4_0 => ferrox_quant::dequant_q4_0(raw)
905 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4_0)),
906 GgmlType::Q4K => ferrox_quant::dequant_q4_k(raw)
907 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4K)),
908 GgmlType::Q5K => ferrox_quant::dequant_q5_k(raw)
909 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5K)),
910 GgmlType::Q6K => ferrox_quant::dequant_q6_k(raw)
911 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q6K)),
912 GgmlType::Q2K => ferrox_quant::dequant_q2_k(raw)
913 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q2K)),
914 GgmlType::Q3K => ferrox_quant::dequant_q3_k(raw)
915 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q3K)),
916 GgmlType::Q4_1 => ferrox_quant::dequant_q4_1(raw)
917 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4_1)),
918 GgmlType::Q5_0 => ferrox_quant::dequant_q5_0(raw)
919 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5_0)),
920 GgmlType::Q5_1 => ferrox_quant::dequant_q5_1(raw)
921 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5_1)),
922 GgmlType::Q8_1 => ferrox_quant::dequant_q8_1(raw)
923 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q8_1)),
924 GgmlType::IQ4NL => ferrox_quant::dequant_iq4_nl(raw)
925 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ4NL)),
926 GgmlType::IQ4XS => ferrox_quant::dequant_iq4_xs(raw)
927 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ4XS)),
928 GgmlType::IQ1S => ferrox_quant::dequant_iq1_s(raw)
935 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ1S)),
936 GgmlType::IQ1M => ferrox_quant::dequant_iq1_m(raw)
937 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ1M)),
938 GgmlType::IQ2XXS => ferrox_quant::dequant_iq2_xxs(raw)
939 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2XXS)),
940 GgmlType::IQ2XS => ferrox_quant::dequant_iq2_xs(raw)
941 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2XS)),
942 GgmlType::IQ2S => ferrox_quant::dequant_iq2_s(raw)
943 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2S)),
944 GgmlType::IQ3XXS => ferrox_quant::dequant_iq3_xxs(raw)
945 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ3XXS)),
946 GgmlType::IQ3S => ferrox_quant::dequant_iq3_s(raw)
947 .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ3S)),
948 other => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
949 }
950}
951
952fn load_weight_matrix(file: &impl TensorSource, name: &str) -> Result<WeightMatrix, LoadError> {
959 let info = find_info(file, name)?;
960 let shape: Vec<usize> = info.shape.iter().rev().map(|&d| d as usize).collect();
973 let (rows, cols) = match shape.as_slice() {
974 [r, c] => (*r, *c),
975 other => {
976 return Err(LoadError::UnsupportedDtype(
977 format!("{name} (expected 2D, got shape {other:?})"),
978 info.dtype,
979 ))
980 }
981 };
982
983 match info.dtype {
984 GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => {
990 let data = load_f32_vec(file, name)?;
991 Ok(WeightMatrix::F32(Tensor::new(data, shape)))
992 }
993 other => match quant_kind_for(other) {
994 Some(kind) => {
995 let (mmap, range) = file.tensor_mapped_range(name)?;
996 #[cfg(feature = "metal")]
997 ferrox_metal::gpu::register_weight_mmap(Arc::clone(&mmap));
998 Ok(WeightMatrix::Quantized {
999 data: WeightBytes::Mapped { mmap, range },
1000 rows,
1001 cols,
1002 kind,
1003 })
1004 }
1005 None => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1006 },
1007 }
1008}
1009
1010fn split_expert_tensor(
1017 file: &impl TensorSource,
1018 name: &str,
1019 n_experts: usize,
1020) -> Result<Vec<WeightMatrix>, LoadError> {
1021 let info = find_info(file, name)?;
1022 if info.shape.len() != 3 || info.shape[2] as usize != n_experts {
1029 let file_experts = info.shape.last().map(|&d| d as usize).unwrap_or(0);
1030 return Err(LoadError::ExpertCountMismatch(
1031 name.to_string(),
1032 file_experts,
1033 n_experts,
1034 ));
1035 }
1036 let out_dim = info.shape[1] as usize;
1037 let in_dim = info.shape[0] as usize;
1038 let raw = file.tensor_bytes(name)?;
1039
1040 match info.dtype {
1041 GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => {
1042 let all = crate::loader::widen_plain_float(info.dtype, raw, name)?;
1043 let per_expert = out_dim * in_dim;
1044 Ok((0..n_experts)
1045 .map(|e| {
1046 WeightMatrix::F32(Tensor::new(
1047 all[e * per_expert..(e + 1) * per_expert].to_vec(),
1048 vec![out_dim, in_dim],
1049 ))
1050 })
1051 .collect())
1052 }
1053 other => match quant_kind_for(other) {
1054 Some(kind) => {
1055 let (mmap, full_range) = file.tensor_mapped_range(name)?;
1056 #[cfg(feature = "metal")]
1057 ferrox_metal::gpu::register_weight_mmap(Arc::clone(&mmap));
1058 let bytes_per_expert = raw.len() / n_experts;
1059 Ok((0..n_experts)
1060 .map(|e| WeightMatrix::Quantized {
1061 data: WeightBytes::Mapped {
1062 mmap: Arc::clone(&mmap),
1063 range: (full_range.start + e * bytes_per_expert)
1064 ..(full_range.start + (e + 1) * bytes_per_expert),
1065 },
1066 rows: out_dim,
1067 cols: in_dim,
1068 kind,
1069 })
1070 .collect())
1071 }
1072 None => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1073 },
1074 }
1075}
1076
1077#[cfg(feature = "metal")]
1082fn try_build_moe_packed_q4_planes(experts: &[ExpertWeights]) -> Option<MoePackedQ4Planes> {
1083 use ferrox_core::weight_matrix::{QuantKind, WeightBytes};
1084 use std::sync::Arc;
1085
1086 if experts.is_empty() {
1087 return None;
1088 }
1089
1090 fn mapped_sg(m: &WeightMatrix) -> Option<(WeightBytes, usize, &'static str)> {
1091 match m {
1092 WeightMatrix::Quantized {
1093 data: WeightBytes::Mapped { mmap, range },
1094 rows,
1095 kind,
1096 ..
1097 } => {
1098 let kind_str = match kind {
1099 QuantKind::Q4_0 => "Q4_0",
1100 QuantKind::Q5_0 => "Q5_0",
1101 QuantKind::Q4K => "Q4_K",
1102 QuantKind::Q5K => "Q5_K",
1103 QuantKind::Q6K => "Q6_K",
1104 QuantKind::Q8_0 => "Q8_0",
1105 QuantKind::IQ4XS => "IQ4_XS",
1106 _ => return None,
1107 };
1108 let _ = ferrox_metal::gpu::mul_mm_sg_meta(kind_str)?;
1109 Some((
1110 WeightBytes::Mapped {
1111 mmap: Arc::clone(mmap),
1112 range: range.clone(),
1113 },
1114 *rows,
1115 kind_str,
1116 ))
1117 }
1118 _ => None,
1119 }
1120 }
1121
1122 let (gate0, ffn_rows, gate_kind) = mapped_sg(&experts[0].gate)?;
1123 let (up0, up_rows, up_kind) = mapped_sg(&experts[0].up)?;
1124 let (down0, hidden_rows, down_kind) = mapped_sg(&experts[0].down)?;
1125 if up_rows != ffn_rows {
1126 return None;
1127 }
1128 let WeightBytes::Mapped {
1129 mmap: gate_mmap,
1130 range: gate0_range,
1131 } = &gate0
1132 else {
1133 return None;
1134 };
1135 let WeightBytes::Mapped {
1136 mmap: up_mmap,
1137 range: up0_range,
1138 } = &up0
1139 else {
1140 return None;
1141 };
1142 let WeightBytes::Mapped {
1143 mmap: down_mmap,
1144 range: down0_range,
1145 } = &down0
1146 else {
1147 return None;
1148 };
1149
1150 let gate_stride = gate0_range.len();
1151 let up_stride = up0_range.len();
1152 let down_stride = down0_range.len();
1153 if gate_stride == 0 || up_stride == 0 || down_stride == 0 {
1154 return None;
1155 }
1156
1157 let n = experts.len();
1158 for (i, ex) in experts.iter().enumerate().skip(1) {
1159 let (g, fr, gk) = mapped_sg(&ex.gate)?;
1160 let (u, ur, uk) = mapped_sg(&ex.up)?;
1161 let (d, hr, dk) = mapped_sg(&ex.down)?;
1162 if gk != gate_kind || uk != up_kind || dk != down_kind {
1163 return None;
1164 }
1165 let WeightBytes::Mapped { mmap, range } = &g else {
1166 return None;
1167 };
1168 if fr != ffn_rows {
1169 return None;
1170 }
1171 if !Arc::ptr_eq(mmap, gate_mmap)
1172 || range.len() != gate_stride
1173 || range.start != gate0_range.start + i * gate_stride
1174 {
1175 return None;
1176 }
1177 let WeightBytes::Mapped { mmap, range } = &u else {
1178 return None;
1179 };
1180 if ur != ffn_rows
1181 || !Arc::ptr_eq(mmap, up_mmap)
1182 || range.len() != up_stride
1183 || range.start != up0_range.start + i * up_stride
1184 {
1185 return None;
1186 }
1187 let WeightBytes::Mapped { mmap, range } = &d else {
1188 return None;
1189 };
1190 if hr != hidden_rows
1191 || !Arc::ptr_eq(mmap, down_mmap)
1192 || range.len() != down_stride
1193 || range.start != down0_range.start + i * down_stride
1194 {
1195 return None;
1196 }
1197 }
1198
1199 Some(MoePackedQ4Planes::new(
1200 WeightBytes::Mapped {
1201 mmap: Arc::clone(gate_mmap),
1202 range: gate0_range.start..gate0_range.start + n * gate_stride,
1203 },
1204 WeightBytes::Mapped {
1205 mmap: Arc::clone(up_mmap),
1206 range: up0_range.start..up0_range.start + n * up_stride,
1207 },
1208 WeightBytes::Mapped {
1209 mmap: Arc::clone(down_mmap),
1210 range: down0_range.start..down0_range.start + n * down_stride,
1211 },
1212 gate_stride,
1213 up_stride,
1214 down_stride,
1215 n,
1216 ffn_rows,
1217 hidden_rows,
1218 gate_kind,
1219 up_kind,
1220 down_kind,
1221 ))
1222}
1223
1224#[derive(Debug, Clone, Copy)]
1228pub struct StoredMatrixSpec {
1229 pub offset: usize,
1230 pub len: usize,
1231 pub rows: usize,
1232 pub cols: usize,
1233 pub kind: QuantKind,
1234}
1235
1236#[derive(Debug, Clone, Copy)]
1238pub struct StoredExpertLayout {
1239 pub gate: StoredMatrixSpec,
1240 pub up: StoredMatrixSpec,
1241 pub down: StoredMatrixSpec,
1242}
1243
1244impl StoredExpertLayout {
1245 pub fn total_bytes(&self) -> usize {
1246 self.gate.len + self.up.len + self.down.len
1247 }
1248
1249 pub fn materialize(&self, lease: &ferrox_core::expert_store::ExpertLease) -> ExpertWeights {
1253 let mk = |spec: &StoredMatrixSpec| WeightMatrix::Quantized {
1254 data: WeightBytes::Shared {
1255 buf: lease.shared_buf(),
1256 range: spec.offset..spec.offset + spec.len,
1257 },
1258 rows: spec.rows,
1259 cols: spec.cols,
1260 kind: spec.kind,
1261 };
1262 ExpertWeights {
1263 gate: mk(&self.gate),
1264 up: mk(&self.up),
1265 down: mk(&self.down),
1266 }
1267 }
1268}
1269
1270pub struct GgufExpertSource {
1276 files: Vec<std::fs::File>,
1277 segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]>,
1280}
1281
1282impl ExpertSource for GgufExpertSource {
1283 fn expert_len(&self, key: ExpertKey) -> Option<usize> {
1284 self.segments
1285 .get(&key)
1286 .map(|segs| segs.iter().map(|&(_, _, len)| len).sum())
1287 }
1288
1289 fn read_expert(&self, key: ExpertKey) -> std::io::Result<Vec<u8>> {
1290 let segs = self
1291 .segments
1292 .get(&key)
1293 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, format!("{key:?}")))?;
1294 let total: usize = segs.iter().map(|&(_, _, len)| len).sum();
1295 let mut buf = vec![0u8; total];
1296 let mut written = 0;
1297 for &(fi, offset, len) in segs {
1298 let dst = &mut buf[written..written + len];
1299 #[cfg(unix)]
1300 {
1301 use std::os::unix::fs::FileExt;
1302 self.files[fi].read_exact_at(dst, offset)?;
1303 }
1304 #[cfg(not(unix))]
1305 {
1306 use std::io::{Read, Seek, SeekFrom};
1307 let mut f = &self.files[fi];
1308 f.seek(SeekFrom::Start(offset))?;
1309 f.read_exact(dst)?;
1310 }
1311 written += len;
1312 }
1313 Ok(buf)
1314 }
1315}
1316
1317struct StoredTensorSpecs {
1327 shard: usize,
1328 per_expert: Vec<(u64, usize)>,
1329 spec: StoredMatrixSpec,
1330}
1331
1332fn stored_expert_specs(
1333 file: &ShardedGguf,
1334 name: &str,
1335 n_experts: usize,
1336) -> Result<Option<StoredTensorSpecs>, LoadError> {
1337 let info = find_info(file, name)?;
1338 if info.shape.len() != 3 || info.shape[2] as usize != n_experts {
1339 let file_experts = info.shape.last().map(|&d| d as usize).unwrap_or(0);
1340 return Err(LoadError::ExpertCountMismatch(
1341 name.to_string(),
1342 file_experts,
1343 n_experts,
1344 ));
1345 }
1346 let out_dim = info.shape[1] as usize;
1347 let in_dim = info.shape[0] as usize;
1348 let Some(kind) = quant_kind_for(info.dtype) else {
1349 return Ok(None); };
1351 let shard = file
1352 .tensor_shard_index(name)
1353 .expect("find_info succeeded, shard index must exist");
1354 let (_, full_range) = file.tensor_mapped_range(name)?;
1357 let total_len = full_range.end - full_range.start;
1358 let bytes_per_expert = total_len / n_experts;
1359 let per_expert: Vec<(u64, usize)> = (0..n_experts)
1360 .map(|e| {
1361 (
1362 (full_range.start + e * bytes_per_expert) as u64,
1363 bytes_per_expert,
1364 )
1365 })
1366 .collect();
1367 let spec = StoredMatrixSpec {
1368 offset: 0, len: bytes_per_expert,
1370 rows: out_dim,
1371 cols: in_dim,
1372 kind,
1373 };
1374 Ok(Some(StoredTensorSpecs {
1375 shard,
1376 per_expert,
1377 spec,
1378 }))
1379}
1380
1381impl Decoder {
1382 pub fn from_gguf(
1391 path: impl AsRef<std::path::Path>,
1392 config: ModelConfig,
1393 ) -> Result<Self, LoadError> {
1394 Self::from_gguf_with_expert_cache(path, config, None)
1395 }
1396
1397 pub fn from_gguf_with_expert_cache(
1410 path: impl AsRef<std::path::Path>,
1411 mut config: ModelConfig,
1412 expert_cache_bytes: Option<u64>,
1413 ) -> Result<Self, LoadError> {
1414 let path = path.as_ref();
1415 let file = ShardedGguf::open(path)?;
1416
1417 let arch = file
1423 .metadata_str("general.architecture")
1424 .unwrap_or_default()
1425 .to_string();
1426 let is_gpt_oss = arch == "gpt-oss";
1427 let mut gpt_oss_layers: Vec<crate::decoder::GptOssLayer> = Vec::new();
1428
1429 let mut store_segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]> =
1433 std::collections::HashMap::new();
1434 let mut stored_layouts: Vec<Option<Vec<StoredExpertLayout>>> = Vec::new();
1435
1436 let embedding = load_weight_matrix(&file, "token_embd.weight")?;
1441
1442 let mut layers = Vec::with_capacity(config.n_layers);
1443 let mut refined_qk_norm = config.qk_norm_style;
1444 for l in 0..config.n_layers {
1445 let (q_proj, k_proj, v_proj) = load_qkv_projections(&file, l, &config)?;
1446 let q_norm = load_f32_vec_optional(&file, &format!("blk.{l}.attn_q_norm.weight"))?;
1447 let k_norm = load_f32_vec_optional(&file, &format!("blk.{l}.attn_k_norm.weight"))?;
1448 if let Some(ref w) = q_norm {
1450 if w.len() == config.head_dim {
1451 refined_qk_norm = crate::capability::QkNormStyle::PerHead;
1452 } else if w.len() == config.n_heads * config.head_dim {
1453 refined_qk_norm = crate::capability::QkNormStyle::WholeVector;
1454 } else {
1455 return Err(LoadError::UnsupportedFeature(
1456 config.name.to_string(),
1457 format!(
1458 "blk.{l}.attn_q_norm.weight length {} matches neither head_dim={} \
1459 nor n_heads*head_dim={}",
1460 w.len(),
1461 config.head_dim,
1462 config.n_heads * config.head_dim
1463 ),
1464 ));
1465 }
1466 }
1467 let attn = AttnWeights {
1468 q_proj,
1469 k_proj,
1470 v_proj,
1471 o_proj: load_weight_matrix(&file, &format!("blk.{l}.attn_output.weight"))?,
1472 norm_weight: load_f32_vec(&file, &format!("blk.{l}.attn_norm.weight"))?,
1473 q_norm,
1474 k_norm,
1475 q_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_q.bias"))?,
1479 k_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_k.bias"))?,
1480 v_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_v.bias"))?,
1481 post_attn_norm: if is_gpt_oss {
1487 None
1488 } else {
1489 load_f32_vec_optional(&file, &format!("blk.{l}.post_attention_norm.weight"))?
1490 },
1491 post_ffn_norm: load_f32_vec_optional(
1492 &file,
1493 &format!("blk.{l}.post_ffw_norm.weight"),
1494 )?,
1495 };
1496
1497 let is_dense_layer = config.layer_is_dense(l) || config.moe.n_experts <= 1;
1505 let n_experts = if is_dense_layer {
1506 1
1507 } else {
1508 config.moe.n_experts
1509 };
1510 let experts: ExpertBacking = if is_dense_layer {
1511 ExpertBacking::Resident(vec![load_dense_expert(&file, l, &config)?])
1512 } else {
1513 let stored = if expert_cache_bytes.is_some() {
1517 let g = stored_expert_specs(
1518 &file,
1519 &format!("blk.{l}.ffn_gate_exps.weight"),
1520 n_experts,
1521 )?;
1522 let u = stored_expert_specs(
1523 &file,
1524 &format!("blk.{l}.ffn_up_exps.weight"),
1525 n_experts,
1526 )?;
1527 let d = stored_expert_specs(
1528 &file,
1529 &format!("blk.{l}.ffn_down_exps.weight"),
1530 n_experts,
1531 )?;
1532 match (g, u, d) {
1533 (Some(gt), Some(ut), Some(dt)) => {
1534 let mut layouts = Vec::with_capacity(n_experts);
1535 for e in 0..n_experts {
1536 let key = ExpertKey {
1537 layer: l as u32,
1538 expert: e as u32,
1539 };
1540 store_segments.insert(
1541 key,
1542 [
1543 (gt.shard, gt.per_expert[e].0, gt.per_expert[e].1),
1544 (ut.shard, ut.per_expert[e].0, ut.per_expert[e].1),
1545 (dt.shard, dt.per_expert[e].0, dt.per_expert[e].1),
1546 ],
1547 );
1548 let mut gate = gt.spec;
1549 let mut up = ut.spec;
1550 let mut down = dt.spec;
1551 gate.offset = 0;
1552 up.offset = gate.len;
1553 down.offset = gate.len + up.len;
1554 layouts.push(StoredExpertLayout { gate, up, down });
1555 }
1556 Some(layouts)
1557 }
1558 _ => None,
1559 }
1560 } else {
1561 None
1562 };
1563 match stored {
1564 Some(layouts) => {
1565 stored_layouts.push(Some(layouts));
1569 ExpertBacking::Resident(Vec::new())
1570 }
1571 None => {
1572 let gates = split_expert_tensor(
1573 &file,
1574 &format!("blk.{l}.ffn_gate_exps.weight"),
1575 n_experts,
1576 )?;
1577 let ups = split_expert_tensor(
1578 &file,
1579 &format!("blk.{l}.ffn_up_exps.weight"),
1580 n_experts,
1581 )?;
1582 let downs = split_expert_tensor(
1583 &file,
1584 &format!("blk.{l}.ffn_down_exps.weight"),
1585 n_experts,
1586 )?;
1587 ExpertBacking::Resident(
1588 gates
1589 .into_iter()
1590 .zip(ups)
1591 .zip(downs)
1592 .map(|((gate, up), down)| ExpertWeights { gate, up, down })
1593 .collect(),
1594 )
1595 }
1596 }
1597 };
1598 if stored_layouts.len() < layers.len() + 1 {
1599 stored_layouts.push(None);
1600 }
1601
1602 let shared_experts: Vec<ExpertWeights> =
1603 if config.moe.n_shared_experts > 0 && !is_dense_layer {
1604 vec![ExpertWeights {
1605 gate: load_weight_matrix(&file, &format!("blk.{l}.ffn_gate_shexp.weight"))?,
1606 up: load_weight_matrix(&file, &format!("blk.{l}.ffn_up_shexp.weight"))?,
1607 down: load_weight_matrix(&file, &format!("blk.{l}.ffn_down_shexp.weight"))?,
1608 }]
1609 } else {
1610 Vec::new()
1611 };
1612
1613 let router = if !is_dense_layer {
1614 load_weight_matrix(&file, &format!("blk.{l}.ffn_gate_inp.weight"))?
1615 } else {
1616 WeightMatrix::F32(Tensor::zeros(vec![1, config.hidden_dim]))
1619 };
1620
1621 let n_for_counts = match &experts {
1622 ExpertBacking::Resident(v) if v.is_empty() => n_experts,
1623 other => other.n_experts(),
1624 };
1625 let activation_counts = (0..n_for_counts)
1626 .map(|_| std::sync::atomic::AtomicU64::new(0))
1627 .collect();
1628 let shared_expert_gate = if is_dense_layer {
1637 None
1638 } else {
1639 load_f32_vec_optional(&file, &format!("blk.{l}.ffn_gate_inp_shexp.weight"))?
1640 };
1641 #[cfg(feature = "metal")]
1642 let packed_q4 = match &experts {
1643 ExpertBacking::Resident(v) if !v.is_empty() => try_build_moe_packed_q4_planes(v),
1644 _ => None,
1645 };
1646 let moe = MoeWeights {
1647 router,
1648 experts,
1649 shared_experts,
1650 shared_expert_gate,
1651 norm_weight: if is_gpt_oss {
1652 load_f32_vec(&file, &format!("blk.{l}.post_attention_norm.weight"))?
1653 } else {
1654 load_f32_vec(&file, &format!("blk.{l}.ffn_norm.weight"))?
1655 },
1656 activation_counts,
1657 #[cfg(feature = "metal")]
1658 packed_q4,
1659 };
1660
1661 if is_gpt_oss {
1662 gpt_oss_layers.push(load_gpt_oss_layer(&file, l, &config)?);
1663 }
1664
1665 layers.push(LayerWeights { attn, moe });
1666 }
1667
1668 let final_norm = load_f32_vec(&file, "output_norm.weight")?;
1669 let output_head = match load_weight_matrix(&file, "output.weight") {
1674 Ok(w) => w,
1675 Err(_) => load_weight_matrix(&file, "token_embd.weight")?,
1676 };
1677
1678 if !store_segments.is_empty() {
1684 let budget = expert_cache_bytes
1685 .expect("store_segments only populated when a cache budget is set")
1686 as usize;
1687 let files: Result<Vec<std::fs::File>, std::io::Error> =
1688 file.shard_paths().iter().map(std::fs::File::open).collect();
1689 let files = files.map_err(GgufError::from)?;
1690 let store = std::sync::Arc::new(ExpertStore::new(
1691 GgufExpertSource {
1692 files,
1693 segments: store_segments,
1694 },
1695 budget,
1696 ));
1697 for (l, layer) in layers.iter_mut().enumerate() {
1698 if let Some(layouts) = stored_layouts.get_mut(l).and_then(Option::take) {
1699 layer.moe.experts = ExpertBacking::Stored {
1700 store: std::sync::Arc::clone(&store),
1701 layouts,
1702 layer: l as u32,
1703 };
1704 }
1705 }
1706 }
1707
1708 config.qk_norm_style = refined_qk_norm;
1709
1710 let family = crate::capability::resolve_profile(
1711 file.metadata_str("general.architecture").unwrap_or("llama"),
1712 )
1713 .map(|p| p.family)
1714 .unwrap_or(crate::capability::DecoderFamily::StandardGqa);
1715 let memory_kind = crate::capability::resolve_profile(
1716 file.metadata_str("general.architecture").unwrap_or("llama"),
1717 )
1718 .map(|p| p.memory)
1719 .unwrap_or(crate::capability::MemoryKind::KvGqa);
1720 let execution_plan = crate::execution_plan::ExecutionPlan::from_config(
1721 &config,
1722 family,
1723 memory_kind,
1724 crate::execution_plan::ExecutionPlan::probe_metal_caps(),
1725 );
1726
1727 let decoder = Decoder {
1728 config,
1729 embedding,
1730 layers,
1731 final_norm,
1732 output_head,
1733 gpu_vram_budget_bytes: None,
1734 gpt_oss: if is_gpt_oss {
1735 Some(crate::decoder::GptOssWeights {
1736 layers: gpt_oss_layers,
1737 })
1738 } else {
1739 None
1740 },
1741 #[cfg(feature = "metal")]
1742 metal_attn_kv: std::sync::Mutex::new(None),
1743 execution_plan,
1744 plan_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
1745 };
1746 decoder.probe_kernels();
1750 ferrox_core::kernel_registry::seal_or_error()
1751 .map_err(|e| LoadError::StrictKernels(e.to_string()))?;
1752 for name in crate::config::MODEL_LEVEL_TENSORS_READ_BY_CONFIG {
1759 file.note_consumed(name);
1760 }
1761 assert_every_tensor_consumed(&file)?;
1762 Ok(decoder)
1763 }
1764}
1765
1766const IGNORED_TENSOR_PREFIXES: &[&str] = &["mm.", "v.", "mmproj.", "resampler.", "audio."];
1771
1772pub fn assert_every_tensor_consumed(file: &ShardedGguf) -> Result<(), LoadError> {
1794 let mut left: Vec<String> = file
1795 .unconsumed_tensors()
1796 .into_iter()
1797 .filter(|n| !IGNORED_TENSOR_PREFIXES.iter().any(|p| n.starts_with(p)))
1798 .collect();
1799 if left.is_empty() {
1800 return Ok(());
1801 }
1802 left.sort();
1803 let shown = left.iter().take(8).cloned().collect::<Vec<_>>().join(", ");
1804 let listing = if left.len() > 8 {
1805 format!("{shown}, … (+{} more)", left.len() - 8)
1806 } else {
1807 shown
1808 };
1809 if matches!(
1810 std::env::var("FERROX_ALLOW_UNKNOWN_TENSORS")
1811 .ok()
1812 .as_deref(),
1813 Some("1") | Some("true") | Some("on")
1814 ) {
1815 eprintln!(
1816 "ferrox: WARNING — {} tensor(s) in this checkpoint are never read \
1817 ({listing}); output may be wrong (FERROX_ALLOW_UNKNOWN_TENSORS=1)",
1818 left.len()
1819 );
1820 return Ok(());
1821 }
1822 Err(LoadError::UnconsumedTensors(left.len(), listing))
1823}
1824
1825#[cfg(test)]
1826mod tests {
1827 use super::*;
1828 use byteorder::{LittleEndian, WriteBytesExt};
1829 use std::io::Write;
1830
1831 fn write_string(buf: &mut Vec<u8>, s: &str) {
1832 buf.write_u64::<LittleEndian>(s.len() as u64).unwrap();
1833 buf.write_all(s.as_bytes()).unwrap();
1834 }
1835
1836 fn write_kv_str(buf: &mut Vec<u8>, key: &str, val: &str) {
1837 write_string(buf, key);
1838 buf.write_u32::<LittleEndian>(8).unwrap(); write_string(buf, val);
1840 }
1841
1842 fn build_arch_only_gguf(arch: &str) -> Vec<u8> {
1848 let mut buf = Vec::new();
1849 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
1850 .unwrap();
1851 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);
1855 buf
1856 }
1857
1858 #[test]
1859 fn model_config_from_gguf_fails_loudly_when_required_hparams_are_missing() {
1860 let tmp = std::env::temp_dir().join("ferrox_test_arch_only.gguf");
1861 std::fs::write(&tmp, build_arch_only_gguf("llama")).unwrap();
1864 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
1865 std::fs::remove_file(&tmp).ok();
1866
1867 match ModelConfig::from_gguf(&file) {
1868 Err(LoadError::MissingHparam(key)) => {
1869 assert_eq!(key, "llama.block_count");
1870 }
1871 other => panic!(
1872 "expected LoadError::MissingHparam for a file with no hparam keys, got {other:?}"
1873 ),
1874 }
1875 }
1876
1877 #[test]
1878 fn model_config_from_gguf_fails_closed_on_unknown_architecture() {
1879 let tmp = std::env::temp_dir().join("ferrox_test_unknown_arch.gguf");
1880 std::fs::write(&tmp, build_arch_only_gguf("bogus-arch-with-no-hparams")).unwrap();
1881 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
1882 std::fs::remove_file(&tmp).ok();
1883
1884 match ModelConfig::from_gguf(&file) {
1885 Err(LoadError::UnsupportedArchitecture(arch)) => {
1886 assert_eq!(arch, "bogus-arch-with-no-hparams");
1887 }
1888 other => panic!(
1889 "expected LoadError::UnsupportedArchitecture for an unregistered arch, got {other:?}"
1890 ),
1891 }
1892 }
1893
1894 #[test]
1895 fn model_config_from_gguf_rejects_dedicated_architectures() {
1896 let tmp = std::env::temp_dir().join("ferrox_test_dedicated_arch.gguf");
1897 std::fs::write(&tmp, build_arch_only_gguf("deepseek4")).unwrap();
1898 let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
1899 std::fs::remove_file(&tmp).ok();
1900
1901 match ModelConfig::from_gguf(&file) {
1902 Err(LoadError::DedicatedArchitectureRequired(arch, _)) => {
1903 assert_eq!(arch, "deepseek4");
1904 }
1905 other => panic!(
1906 "expected LoadError::DedicatedArchitectureRequired for deepseek4, got {other:?}"
1907 ),
1908 }
1909 }
1910
1911 #[rustfmt::skip]
1915 const Q5_K_TEST_BLOCK: [u8; 176] = [
1916 0x66, 0x2a, 0x66, 0x2a, 0x01, 0x01, 0x01, 0x01, 0x4f, 0x4b, 0x10, 0x12, 0x41, 0xe2, 0xc1,
1917 0xb1, 0x72, 0x2f, 0x20, 0x07, 0x31, 0x0c, 0x38, 0xb3, 0x9c, 0xb8, 0xad, 0x2f, 0x9a, 0xea,
1918 0x17, 0xd0, 0xee, 0x93, 0x9e, 0x3e, 0x74, 0xbb, 0x28, 0x18, 0x39, 0x25, 0xb6, 0x09, 0x18,
1919 0x29, 0x1c, 0x1d, 0x29, 0x41, 0x40, 0x0a, 0x74, 0x7d, 0xfd, 0x21, 0xdd, 0x6d, 0x45, 0x73,
1920 0x0e, 0x1e, 0xc0, 0x4a, 0xfc, 0xf3, 0x8e, 0x24, 0x6b, 0x34, 0x7d, 0xbe, 0x94, 0xde, 0x59,
1921 0x7a, 0x35, 0x30, 0x36, 0x0a, 0xf9, 0x4a, 0x9b, 0xa2, 0x26, 0x21, 0xa2, 0xfa, 0xdf, 0x4b,
1922 0x29, 0x64, 0x6f, 0xbb, 0xca, 0x0f, 0x3c, 0xda, 0x20, 0xf4, 0x93, 0x86, 0xab, 0x6e, 0xb9,
1923 0xe5, 0xd5, 0xa0, 0x82, 0xd6, 0x41, 0xff, 0x12, 0xbc, 0x34, 0xbb, 0xab, 0xb8, 0x20, 0x2f,
1924 0xbb, 0x5f, 0x0c, 0x10, 0xcf, 0x49, 0xc5, 0x86, 0x5c, 0xdf, 0xff, 0x78, 0x44, 0x26, 0x3b,
1925 0xc2, 0x23, 0x3d, 0x2b, 0xe9, 0x00, 0x12, 0xf8, 0xea, 0xe2, 0x9e, 0x5e, 0x50, 0x20, 0x9f,
1926 0x9d, 0x8d, 0x7d, 0x7f, 0xcc, 0x1d, 0x0e, 0x13, 0xf8, 0xc2, 0xf1, 0x3d, 0x08, 0x2f, 0x23,
1927 0x13, 0xac, 0x0d, 0xa7, 0xe7, 0x20, 0xa3, 0x90, 0xb7, 0xc8, 0x28,
1928 ];
1929
1930 fn build_single_q5_k_tensor_gguf() -> Vec<u8> {
1931 let mut buf = Vec::new();
1932 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
1933 .unwrap();
1934 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");
1939
1940 write_string(&mut buf, "test.weight");
1941 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 {
1951 buf.push(0);
1952 }
1953 buf.extend_from_slice(&Q5_K_TEST_BLOCK);
1954 buf
1955 }
1956
1957 #[test]
1958 fn load_weight_matrix_handles_a_real_on_disk_q5_k_tensor_end_to_end() {
1959 let tmp = std::env::temp_dir().join("ferrox_test_q5k_tensor.gguf");
1960 std::fs::write(&tmp, build_single_q5_k_tensor_gguf()).unwrap();
1961 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q5_K GGUF file must parse");
1962 std::fs::remove_file(&tmp).ok();
1963
1964 let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_K tensor must load");
1965 assert_eq!(matrix.rows(), 1);
1966 assert_eq!(matrix.cols(), 256);
1967 match &matrix {
1968 WeightMatrix::Quantized { kind, data, .. } => {
1969 assert_eq!(*kind, QuantKind::Q5K);
1970 assert!(
1971 data.is_mapped(),
1972 "Q5_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
1973 );
1974 }
1975 _ => panic!("expected a Quantized matrix for a Q5_K tensor"),
1976 }
1977
1978 let expected = ferrox_quant::dequant_q5_k(&Q5_K_TEST_BLOCK).unwrap();
1979 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
1980 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
1981
1982 let got = matrix.apply(&x);
1983 assert_eq!(got.len(), 1);
1984 assert!(
1985 (got[0] - expected_dot).abs() < 1e-2,
1986 "end-to-end loaded+applied Q5_K matrix diverged from direct dequant: got={} expected={}",
1987 got[0],
1988 expected_dot
1989 );
1990 }
1991
1992 #[rustfmt::skip]
2001 const Q6_K_TEST_BLOCK: [u8; 210] = [
2002 0xe0, 0xa5, 0x40, 0x5c, 0x8d, 0x3a, 0x0a, 0x26, 0xfb, 0x4b, 0x6e, 0x9a, 0xdf, 0x3e, 0xa3,
2003 0xc4, 0xf8, 0x2b, 0x1d, 0x95, 0x76, 0x7d, 0x3b, 0xcd, 0xfd, 0xef, 0xc2, 0x0b, 0x07, 0x63,
2004 0x29, 0xfb, 0x81, 0x57, 0xbe, 0xbe, 0x06, 0xf7, 0x3a, 0x92, 0xc4, 0x43, 0xff, 0xad, 0xac,
2005 0x7e, 0x0f, 0x00, 0x2a, 0x4f, 0xf0, 0xf8, 0xa9, 0xfa, 0x3c, 0x90, 0x6d, 0x73, 0x2d, 0x5a,
2006 0xe6, 0xc6, 0x46, 0xf2, 0x0d, 0x55, 0x4c, 0x25, 0x38, 0x71, 0x2b, 0x35, 0x38, 0x82, 0x16,
2007 0x37, 0x5f, 0x32, 0x61, 0x02, 0xdd, 0x2f, 0x6f, 0x7b, 0x1f, 0xb4, 0x1a, 0x1b, 0x3e, 0x4f,
2008 0x11, 0xa3, 0x17, 0x40, 0x5a, 0x5f, 0x76, 0xcd, 0x19, 0x27, 0x9b, 0xc7, 0xc8, 0xf7, 0xf7,
2009 0xee, 0xf4, 0x86, 0xd9, 0xfd, 0xa7, 0xfe, 0x9e, 0xac, 0x70, 0x53, 0x5b, 0x76, 0xfb, 0x39,
2010 0xf8, 0x4b, 0x98, 0xfe, 0xd0, 0x06, 0x21, 0x4c, 0x4d, 0xbe, 0x10, 0x2b, 0x06, 0x65, 0xc9,
2011 0x5e, 0xf9, 0x95, 0x72, 0xae, 0x99, 0xd9, 0x7e, 0x15, 0xbd, 0x5e, 0x6d, 0xe8, 0x25, 0x8a,
2012 0xd5, 0x99, 0xc6, 0x6b, 0x69, 0xc7, 0x84, 0xc6, 0xa4, 0xf7, 0xb9, 0x6d, 0x68, 0x45, 0x0e,
2013 0x65, 0x69, 0xeb, 0xe6, 0xeb, 0xe9, 0x28, 0xa6, 0xb9, 0x96, 0xf2, 0xe8, 0xa7, 0x9b, 0x6e,
2014 0x79, 0x8a, 0x68, 0x65, 0x59, 0x98, 0x8b, 0x44, 0x41, 0x98, 0x9a, 0x56, 0x01, 0x01, 0x01,
2015 0x02, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, 0x01, 0x01, 0x01, 0x02, 0x1f, 0x25,
2016 ];
2017
2018 fn build_single_q6_k_tensor_gguf() -> Vec<u8> {
2019 let mut buf = Vec::new();
2020 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2021 .unwrap();
2022 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");
2027
2028 write_string(&mut buf, "test.weight");
2029 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 {
2037 buf.push(0);
2038 }
2039 buf.extend_from_slice(&Q6_K_TEST_BLOCK);
2040 buf
2041 }
2042
2043 #[test]
2044 fn load_weight_matrix_handles_a_real_on_disk_q6_k_tensor_end_to_end() {
2045 let tmp = std::env::temp_dir().join("ferrox_test_q6k_tensor.gguf");
2046 std::fs::write(&tmp, build_single_q6_k_tensor_gguf()).unwrap();
2047 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q6_K GGUF file must parse");
2048 std::fs::remove_file(&tmp).ok();
2049
2050 let matrix = load_weight_matrix(&file, "test.weight").expect("Q6_K tensor must load");
2051 assert_eq!(matrix.rows(), 1);
2052 assert_eq!(matrix.cols(), 256);
2053 match &matrix {
2054 WeightMatrix::Quantized { kind, data, .. } => {
2055 assert_eq!(*kind, QuantKind::Q6K);
2056 assert!(
2057 data.is_mapped(),
2058 "Q6_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
2059 );
2060 }
2061 _ => panic!("expected a Quantized matrix for a Q6_K tensor"),
2062 }
2063
2064 let expected = ferrox_quant::dequant_q6_k(&Q6_K_TEST_BLOCK).unwrap();
2065 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
2066 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
2067
2068 let got = matrix.apply(&x);
2069 assert_eq!(got.len(), 1);
2070 assert!(
2071 (got[0] - expected_dot).abs() < 1e-2,
2072 "end-to-end loaded+applied Q6_K matrix diverged from direct dequant: got={} expected={}",
2073 got[0],
2074 expected_dot
2075 );
2076 }
2077
2078 fn build_single_bf16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
2079 let mut buf = Vec::new();
2080 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2081 .unwrap();
2082 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");
2087
2088 write_string(&mut buf, "test.weight");
2089 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(cols).unwrap();
2092 buf.write_u64::<LittleEndian>(rows).unwrap();
2093 buf.write_u32::<LittleEndian>(30).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
2097 buf.push(0);
2098 }
2099 for &v in values {
2100 let bf16_bits = (v.to_bits() >> 16) as u16;
2104 buf.extend_from_slice(&bf16_bits.to_le_bytes());
2105 }
2106 buf
2107 }
2108
2109 #[test]
2110 fn load_weight_matrix_handles_a_real_on_disk_bf16_tensor_end_to_end() {
2111 let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
2114 let tmp = std::env::temp_dir().join("ferrox_test_bf16_tensor.gguf");
2115 std::fs::write(&tmp, build_single_bf16_tensor_gguf(2, 3, &values)).unwrap();
2116 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real BF16 GGUF file must parse");
2117 std::fs::remove_file(&tmp).ok();
2118
2119 let matrix = load_weight_matrix(&file, "test.weight").expect("BF16 tensor must load");
2120 assert_eq!(matrix.rows(), 2);
2121 assert_eq!(matrix.cols(), 3);
2122 match &matrix {
2123 WeightMatrix::F32(tensor) => {
2124 assert_eq!(tensor.data, values, "BF16 must widen to f32 exactly");
2125 }
2126 _ => panic!("expected an F32 matrix for a BF16 tensor (no fused dot kernel for it)"),
2127 }
2128 }
2129
2130 fn build_single_f16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
2131 let mut buf = Vec::new();
2132 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2133 .unwrap();
2134 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");
2139
2140 write_string(&mut buf, "test.weight");
2141 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(cols).unwrap();
2143 buf.write_u64::<LittleEndian>(rows).unwrap();
2144 buf.write_u32::<LittleEndian>(1).unwrap(); buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
2148 buf.push(0);
2149 }
2150 for &v in values {
2151 buf.extend_from_slice(&half::f16::from_f32(v).to_le_bytes());
2152 }
2153 buf
2154 }
2155
2156 #[test]
2161 fn load_weight_matrix_handles_a_real_on_disk_f16_tensor_end_to_end() {
2162 let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
2163 let tmp = std::env::temp_dir().join("ferrox_test_f16_tensor.gguf");
2164 std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
2165 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
2166 std::fs::remove_file(&tmp).ok();
2167
2168 let matrix = load_weight_matrix(&file, "test.weight").expect("F16 tensor must load");
2169 assert_eq!(matrix.rows(), 2);
2170 assert_eq!(matrix.cols(), 3);
2171 match &matrix {
2172 WeightMatrix::F32(tensor) => {
2173 assert_eq!(tensor.data, values, "F16 must widen to f32 exactly");
2174 }
2175 _ => panic!("expected an F32 matrix for an F16 tensor (no fused dot kernel for it)"),
2176 }
2177
2178 let tmp = std::env::temp_dir().join("ferrox_test_f16_vec.gguf");
2181 std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
2182 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
2183 std::fs::remove_file(&tmp).ok();
2184 assert_eq!(load_f32_vec(&file, "test.weight").unwrap(), values);
2185 }
2186
2187 fn build_single_q5_1_tensor_gguf() -> Vec<u8> {
2188 let mut buf = Vec::new();
2189 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2190 .unwrap();
2191 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");
2196
2197 write_string(&mut buf, "test.weight");
2198 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 {
2206 buf.push(0);
2207 }
2208 buf.extend_from_slice(&0x3400u16.to_le_bytes());
2213 buf.extend_from_slice(&0x3E00u16.to_le_bytes());
2214 buf.extend_from_slice(&[0x9au8, 0x3c, 0xf0, 0x0f]);
2215 buf.extend_from_slice(&(0..16u8).map(|i| i | ((15 - i) << 4)).collect::<Vec<u8>>());
2216 buf
2217 }
2218
2219 #[test]
2220 fn load_weight_matrix_handles_a_real_on_disk_q5_1_tensor_end_to_end() {
2221 let tmp = std::env::temp_dir().join("ferrox_test_q5_1_tensor.gguf");
2222 std::fs::write(&tmp, build_single_q5_1_tensor_gguf()).unwrap();
2223 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q5_1 GGUF file must parse");
2224 std::fs::remove_file(&tmp).ok();
2225
2226 let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_1 tensor must load");
2227 assert_eq!(matrix.rows(), 1);
2228 assert_eq!(matrix.cols(), 32);
2229 let raw = file.tensor_bytes("test.weight").unwrap();
2230 let expected = ferrox_quant::dequant_q5_1(raw).unwrap();
2231 match &matrix {
2232 WeightMatrix::Quantized { kind, data, .. } => {
2233 assert_eq!(*kind, QuantKind::Q5_1);
2234 assert!(data.is_mapped());
2235 }
2236 _ => panic!("expected a Quantized matrix for a Q5_1 tensor"),
2237 }
2238
2239 let x: Vec<f32> = (0..32).map(|i| ((i as f32) * 0.017).cos()).collect();
2240 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
2241 let got = matrix.apply(&x);
2242 assert_eq!(got.len(), 1);
2243 assert!(
2244 (got[0] - expected_dot).abs() < 1e-2,
2245 "end-to-end loaded+applied Q5_1 matrix diverged from direct dequant: got={} expected={}",
2246 got[0],
2247 expected_dot
2248 );
2249 }
2250
2251 const Q3_K_TEST_BLOCK: [u8; 110] = [
2256 0x56, 0xf2, 0xb4, 0x2b, 0xd5, 0x6f, 0x51, 0x71, 0x3c, 0x0a, 0xb9, 0x1d, 0xd0, 0xb9, 0x3b,
2257 0xb3, 0x0f, 0xff, 0x8c, 0xb2, 0x83, 0x3a, 0x3d, 0x24, 0xb1, 0x12, 0x56, 0xe3, 0x23, 0x54,
2258 0xf2, 0xfa, 0x7f, 0xdf, 0x31, 0xe1, 0x18, 0x26, 0x6e, 0xcd, 0x5b, 0x38, 0xee, 0xbd, 0x9f,
2259 0x8c, 0x57, 0x47, 0x0b, 0x11, 0xcb, 0xfb, 0xb4, 0x83, 0xa0, 0x4e, 0x0b, 0xd4, 0xa7, 0x85,
2260 0xe0, 0x60, 0xf3, 0xb3, 0xe3, 0x95, 0x43, 0xc6, 0x05, 0x05, 0x77, 0x53, 0xed, 0x23, 0xcc,
2261 0x6a, 0x0e, 0x89, 0xa1, 0x79, 0x85, 0xf6, 0x6e, 0x5a, 0x23, 0x63, 0xbe, 0x53, 0xfa, 0xa2,
2262 0x2b, 0xe9, 0xcd, 0xce, 0xf8, 0x3d, 0x6f, 0xd0, 0x42, 0x6e, 0x3b, 0x7f, 0x23, 0x26, 0xd3,
2263 0xb9, 0x18, 0xbf, 0xa4, 0x34,
2264 ];
2265
2266 fn build_single_q3_k_tensor_gguf() -> Vec<u8> {
2267 let mut buf = Vec::new();
2268 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2269 .unwrap();
2270 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");
2275
2276 write_string(&mut buf, "test.weight");
2277 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 {
2285 buf.push(0);
2286 }
2287 buf.extend_from_slice(&Q3_K_TEST_BLOCK);
2288 buf
2289 }
2290
2291 #[test]
2292 fn load_weight_matrix_handles_a_real_on_disk_q3_k_tensor_end_to_end() {
2293 let tmp = std::env::temp_dir().join("ferrox_test_q3k_tensor.gguf");
2294 std::fs::write(&tmp, build_single_q3_k_tensor_gguf()).unwrap();
2295 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q3_K GGUF file must parse");
2296 std::fs::remove_file(&tmp).ok();
2297
2298 let matrix = load_weight_matrix(&file, "test.weight").expect("Q3_K tensor must load");
2299 assert_eq!(matrix.rows(), 1);
2300 assert_eq!(matrix.cols(), 256);
2301 match &matrix {
2302 WeightMatrix::Quantized { kind, data, .. } => {
2303 assert_eq!(*kind, QuantKind::Q3K);
2304 assert!(data.is_mapped());
2305 }
2306 _ => panic!("expected a Quantized matrix for a Q3_K tensor"),
2307 }
2308
2309 let expected = ferrox_quant::dequant_q3_k(&Q3_K_TEST_BLOCK).unwrap();
2310 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
2311 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
2312
2313 let got = matrix.apply(&x);
2314 assert_eq!(got.len(), 1);
2315 assert!(
2316 (got[0] - expected_dot).abs() < 1e-1,
2317 "end-to-end loaded+applied Q3_K matrix diverged from direct dequant: got={} expected={}",
2318 got[0],
2319 expected_dot
2320 );
2321 }
2322
2323 const IQ4_XS_TEST_BLOCK: [u8; 136] = [
2327 0x5c, 0x33, 0xb4, 0x39, 0xd1, 0x64, 0x97, 0x82, 0xcb, 0xbd, 0x88, 0x95, 0xf3, 0x60, 0x2a,
2328 0xb5, 0xe7, 0x24, 0xd3, 0xee, 0xfe, 0x71, 0x13, 0xbe, 0x70, 0x84, 0x48, 0x79, 0x7b, 0x3e,
2329 0xf0, 0x55, 0xdc, 0xb2, 0xb2, 0xde, 0x32, 0xa1, 0x5b, 0x02, 0x01, 0xdc, 0x2a, 0xbb, 0xf7,
2330 0x0b, 0x8a, 0x88, 0xdd, 0x0b, 0x02, 0x7e, 0x5e, 0x76, 0x87, 0x30, 0x1e, 0x1c, 0xcf, 0x48,
2331 0xd7, 0x61, 0xf3, 0x51, 0x52, 0x17, 0x98, 0x0a, 0x87, 0xcf, 0x02, 0x91, 0xc8, 0xee, 0xc0,
2332 0x91, 0x69, 0x2a, 0x4f, 0x64, 0x68, 0xa7, 0xb2, 0xe6, 0x98, 0x21, 0x81, 0x75, 0x53, 0x2a,
2333 0x8d, 0x12, 0xae, 0xe0, 0xea, 0x0c, 0x75, 0xff, 0x22, 0x5e, 0x25, 0x19, 0xda, 0x2e, 0x51,
2334 0x4e, 0x81, 0xdc, 0x0e, 0x78, 0x86, 0xd7, 0x58, 0xb5, 0xb7, 0xf6, 0x45, 0xa9, 0x0a, 0x83,
2335 0xfd, 0x2a, 0x12, 0x7d, 0xf0, 0x12, 0x97, 0xe2, 0xfe, 0xf4, 0xd0, 0xa2, 0x11, 0x14, 0x78,
2336 0xdb,
2337 ];
2338
2339 fn build_single_iq4_xs_tensor_gguf() -> Vec<u8> {
2340 let mut buf = Vec::new();
2341 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2342 .unwrap();
2343 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");
2348
2349 write_string(&mut buf, "test.weight");
2350 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 {
2358 buf.push(0);
2359 }
2360 buf.extend_from_slice(&IQ4_XS_TEST_BLOCK);
2361 buf
2362 }
2363
2364 #[test]
2365 fn load_weight_matrix_handles_a_real_on_disk_iq4_xs_tensor_end_to_end() {
2366 let tmp = std::env::temp_dir().join("ferrox_test_iq4xs_tensor.gguf");
2367 std::fs::write(&tmp, build_single_iq4_xs_tensor_gguf()).unwrap();
2368 let file = ferrox_gguf::GgufFile::open(&tmp).expect("real IQ4_XS GGUF file must parse");
2369 std::fs::remove_file(&tmp).ok();
2370
2371 let matrix = load_weight_matrix(&file, "test.weight").expect("IQ4_XS tensor must load");
2372 assert_eq!(matrix.rows(), 1);
2373 assert_eq!(matrix.cols(), 256);
2374 match &matrix {
2375 WeightMatrix::Quantized { kind, data, .. } => {
2376 assert_eq!(*kind, QuantKind::IQ4XS);
2377 assert!(data.is_mapped());
2378 }
2379 _ => panic!("expected a Quantized matrix for an IQ4_XS tensor"),
2380 }
2381
2382 let expected = ferrox_quant::dequant_iq4_xs(&IQ4_XS_TEST_BLOCK).unwrap();
2383 let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
2384 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
2385
2386 let got = matrix.apply(&x);
2387 assert_eq!(got.len(), 1);
2388 assert!(
2389 (got[0] - expected_dot).abs() < 1e-1,
2390 "end-to-end loaded+applied IQ4_XS matrix diverged from direct dequant: got={} expected={}",
2391 got[0],
2392 expected_dot
2393 );
2394 }
2395
2396 const IQ1_S_TEST_BLOCK: [u8; 50] = [
2401 0x0a, 0x2f, 0xfa, 0x06, 0x1e, 0x37, 0x6f, 0xe3, 0x62, 0xd0, 0xb6, 0xa4, 0x25, 0xae, 0x76,
2402 0x14, 0x72, 0x5b, 0xfa, 0x05, 0xd1, 0xf1, 0x2a, 0x4c, 0xad, 0x29, 0xae, 0xf4, 0xcf, 0x0c,
2403 0x96, 0x51, 0x58, 0x03, 0x6d, 0xd3, 0x10, 0x92, 0x70, 0xff, 0x61, 0x58, 0xc8, 0x30, 0x25,
2404 0x64, 0x49, 0x85, 0xc0, 0x24,
2405 ];
2406 const IQ2_XXS_TEST_BLOCK: [u8; 66] = [
2407 0x29, 0x30, 0xd9, 0x33, 0x95, 0x4c, 0x08, 0x1e, 0xad, 0x79, 0x49, 0xf2, 0x8d, 0x5f, 0x93,
2408 0xea, 0x78, 0x18, 0x98, 0xb9, 0x94, 0x14, 0xad, 0xce, 0xca, 0x1d, 0xab, 0x81, 0x53, 0x4a,
2409 0x68, 0xd0, 0x59, 0x96, 0x36, 0x5d, 0xbe, 0x20, 0xc4, 0xff, 0xe4, 0x2c, 0xcd, 0x2f, 0x4f,
2410 0x4f, 0x67, 0x53, 0xc6, 0xd5, 0xa2, 0xfb, 0xc7, 0xf3, 0xe2, 0x6b, 0xf1, 0x99, 0x23, 0x1e,
2411 0x2d, 0x5e, 0x8c, 0x78, 0xc2, 0x31,
2412 ];
2413 const IQ3_XXS_TEST_BLOCK: [u8; 98] = [
2414 0x71, 0x31, 0x16, 0x0a, 0x79, 0x04, 0x5d, 0x87, 0xae, 0x2a, 0x4a, 0x43, 0xfd, 0x02, 0xba,
2415 0x6c, 0x10, 0x42, 0x80, 0xe5, 0x1d, 0x08, 0x22, 0xcb, 0x21, 0x54, 0xf9, 0xaa, 0x8e, 0xc2,
2416 0xf2, 0x34, 0x66, 0x1e, 0x2a, 0xef, 0x19, 0xae, 0x48, 0x47, 0x29, 0xa0, 0x72, 0xd1, 0x31,
2417 0xc0, 0x65, 0x49, 0xde, 0x79, 0x32, 0xe6, 0x4d, 0xb6, 0x55, 0x3f, 0x4d, 0xf1, 0x18, 0xbb,
2418 0x18, 0x59, 0x4c, 0x31, 0xa3, 0xb2, 0x34, 0xdd, 0xf6, 0x4a, 0x91, 0x51, 0x3f, 0x3e, 0x40,
2419 0x69, 0xad, 0xbf, 0x1a, 0xd0, 0x05, 0xfb, 0xbe, 0x8b, 0x0b, 0xdd, 0xdf, 0x7d, 0x94, 0x74,
2420 0x92, 0x3e, 0xff, 0x04, 0x2a, 0xc4, 0xea, 0xc9,
2421 ];
2422
2423 #[rustfmt::skip]
2424 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];
2425
2426 fn build_single_iq_lowbit_tensor_gguf(
2427 arch: &str,
2428 tag: u32,
2429 cols: u64,
2430 block: &[u8],
2431 ) -> Vec<u8> {
2432 let mut buf = Vec::new();
2433 buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2434 .unwrap();
2435 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);
2439 write_string(&mut buf, "test.weight");
2440 buf.write_u32::<LittleEndian>(2).unwrap(); buf.write_u64::<LittleEndian>(cols).unwrap();
2442 buf.write_u64::<LittleEndian>(1).unwrap(); buf.write_u32::<LittleEndian>(tag).unwrap();
2444 buf.write_u64::<LittleEndian>(0).unwrap(); while buf.len() % 32 != 0 {
2446 buf.push(0);
2447 }
2448 buf.extend_from_slice(block);
2449 buf
2450 }
2451
2452 fn pseudo_iq_block(len: usize, seed: u32) -> Vec<u8> {
2459 let mut s = seed;
2460 let mut out = Vec::with_capacity(len);
2461 for _ in 0..len {
2462 s ^= s << 13;
2463 s ^= s >> 17;
2464 s ^= s << 5;
2465 out.push((s >> 24) as u8);
2466 }
2467 out
2468 }
2469
2470 #[test]
2481 fn load_weight_matrix_handles_real_on_disk_iq_lowbit_tensors_end_to_end() {
2482 type DequantFn = fn(&[u8]) -> Result<Vec<f32>, ferrox_quant::QuantError>;
2483 let mut iq1m = pseudo_iq_block(ferrox_quant::IQ1_M_BLOCK_BYTES, 0x2907_31A0);
2490 iq1m[55] = (iq1m[55] & 0x0F) | 0x20;
2491 let mut iq2xs = pseudo_iq_block(ferrox_quant::IQ2_XS_BLOCK_BYTES, 0x2107_31A1);
2492 let mut iq2s = pseudo_iq_block(ferrox_quant::IQ2_S_BLOCK_BYTES, 0x2207_31A2);
2493 let mut iq3s = pseudo_iq_block(ferrox_quant::IQ3_S_BLOCK_BYTES, 0x2307_31A3);
2494 for blk in [&mut iq2xs, &mut iq2s, &mut iq3s] {
2495 blk[0..2].copy_from_slice(&half::f16::from_f32(0.115).to_le_bytes());
2496 }
2497 let cases: [(&str, u32, &[u8], QuantKind, DequantFn); 8] = [
2498 (
2499 "iq1s",
2500 19,
2501 &IQ1_S_TEST_BLOCK,
2502 QuantKind::IQ1S,
2503 ferrox_quant::dequant_iq1_s,
2504 ),
2505 (
2506 "iq1m",
2507 29,
2508 &iq1m,
2509 QuantKind::IQ1M,
2510 ferrox_quant::dequant_iq1_m,
2511 ),
2512 (
2513 "iq2xxs",
2514 16,
2515 &IQ2_XXS_TEST_BLOCK,
2516 QuantKind::IQ2XXS,
2517 ferrox_quant::dequant_iq2_xxs,
2518 ),
2519 (
2520 "iq2xs",
2521 17,
2522 &iq2xs,
2523 QuantKind::IQ2XS,
2524 ferrox_quant::dequant_iq2_xs,
2525 ),
2526 (
2527 "iq2s",
2528 22,
2529 &iq2s,
2530 QuantKind::IQ2S,
2531 ferrox_quant::dequant_iq2_s,
2532 ),
2533 (
2534 "iq3xxs",
2535 18,
2536 &IQ3_XXS_TEST_BLOCK,
2537 QuantKind::IQ3XXS,
2538 ferrox_quant::dequant_iq3_xxs,
2539 ),
2540 (
2541 "iq3s",
2542 21,
2543 &iq3s,
2544 QuantKind::IQ3S,
2545 ferrox_quant::dequant_iq3_s,
2546 ),
2547 (
2548 "mxfp4_gguf",
2549 39,
2550 &MXFP4_GGUF_TEST_BLOCKS,
2551 QuantKind::Mxfp4Gguf,
2552 ferrox_quant::dequant_mxfp4_gguf,
2553 ),
2554 ];
2555 for (name, tag, block, kind, dequant) in cases {
2556 let expected = dequant(block).unwrap();
2557 let cols = expected.len();
2558 let tmp = std::env::temp_dir().join(format!("ferrox_test_{name}_tensor.gguf"));
2559 std::fs::write(
2560 &tmp,
2561 build_single_iq_lowbit_tensor_gguf(name, tag, cols as u64, block),
2562 )
2563 .unwrap();
2564 let file = ferrox_gguf::GgufFile::open(&tmp).expect("file must parse");
2565 std::fs::remove_file(&tmp).ok();
2566
2567 let matrix =
2568 load_weight_matrix(&file, "test.weight").expect("low-bit tensor must load");
2569 assert_eq!((matrix.rows(), matrix.cols()), (1, cols), "{name}");
2570 match &matrix {
2571 WeightMatrix::Quantized { kind: k, data, .. } => {
2572 assert_eq!(*k, kind, "{name}");
2573 assert!(data.is_mapped(), "{name} must load zero-copy");
2574 }
2575 _ => panic!("expected a Quantized matrix for {name}"),
2576 }
2577
2578 let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.013).sin()).collect();
2579 let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
2580 let got = matrix.apply(&x);
2581 assert!(
2582 (got[0] - expected_dot).abs() < 1e-1,
2583 "{name}: loaded+applied diverged from direct dequant: got={} expected={}",
2584 got[0],
2585 expected_dot
2586 );
2587 }
2588 }
2589
2590 #[test]
2591 fn qwen2moe_disables_topk_renorm() {
2592 assert!(
2593 NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"qwen2moe"),
2594 "qwen2moe must have norm_topk_prob=false (llama.cpp build_moe_ffn norm_w=false)"
2595 );
2596 }
2597}