1use anyhow::{Context, Result, anyhow};
30use rlx_gguf::MetaValue;
31use std::collections::{HashMap, HashSet};
32use std::path::Path;
33use std::sync::Arc;
34
35fn compute_mtp_layer_threshold(file: &rlx_gguf::GgufFile) -> Option<u32> {
39 let arch = file
40 .metadata
41 .get("general.architecture")
42 .and_then(MetaValue::as_str)?;
43 let block_count = file
44 .metadata
45 .get(&format!("{arch}.block_count"))
46 .and_then(MetaValue::as_u32)?;
47 let nextn = file
48 .metadata
49 .get(&format!("{arch}.nextn_predict_layers"))
50 .and_then(MetaValue::as_u32)?;
51 if nextn == 0 {
52 return None;
53 }
54 Some(block_count.saturating_sub(nextn))
55}
56
57use crate::gguf_resolve::resolve_gguf_tensor_name;
58use crate::gguf_support::gguf_architecture_str;
59use crate::weight_map::PackedWeightTensor;
60use crate::weight_map::WeightMap;
61use rlx_ir::quant::QuantScheme;
62
63pub fn hf_to_gguf_name(hf: &str) -> Option<String> {
74 match hf {
76 "model.embed_tokens.weight" => return Some("token_embd.weight".into()),
77 "model.norm.weight" => return Some("output_norm.weight".into()),
78 "lm_head.weight" => return Some("output.weight".into()),
79 _ => {}
80 }
81 let rest = hf.strip_prefix("model.layers.")?;
83 let dot = rest.find('.')?;
84 let (idx_str, tail_with_dot) = rest.split_at(dot);
85 let tail = &tail_with_dot[1..]; let idx: usize = idx_str.parse().ok()?;
87 let gguf_tail = match tail {
88 "input_layernorm.weight" => "attn_norm.weight",
89 "post_attention_layernorm.weight" => "ffn_norm.weight",
94 "self_attn.q_proj.weight" => "attn_q.weight",
95 "self_attn.k_proj.weight" => "attn_k.weight",
96 "self_attn.v_proj.weight" => "attn_v.weight",
97 "self_attn.o_proj.weight" => "attn_output.weight",
98 "self_attn.q_proj.bias" => "attn_q.bias",
99 "self_attn.k_proj.bias" => "attn_k.bias",
100 "self_attn.v_proj.bias" => "attn_v.bias",
101 "self_attn.q_norm.weight" => "attn_q_norm.weight",
102 "self_attn.k_norm.weight" => "attn_k_norm.weight",
103 "mlp.gate_proj.weight" => "ffn_gate.weight",
104 "mlp.up_proj.weight" => "ffn_up.weight",
105 "mlp.down_proj.weight" => "ffn_down.weight",
106 _ => return None,
107 };
108 Some(format!("blk.{idx}.{gguf_tail}"))
109}
110
111pub fn gguf_to_hf_name(gguf: &str) -> Option<String> {
117 match gguf {
118 "token_embd.weight" => return Some("model.embed_tokens.weight".into()),
119 "output_norm.weight" => return Some("model.norm.weight".into()),
120 "output.weight" => return Some("lm_head.weight".into()),
121 _ => {}
122 }
123 let rest = gguf.strip_prefix("blk.")?;
124 let dot = rest.find('.')?;
125 let (idx_str, tail_with_dot) = rest.split_at(dot);
126 let tail = &tail_with_dot[1..];
127 let idx: usize = idx_str.parse().ok()?;
128 let hf_tail = match tail {
129 "attn_norm.weight" => "input_layernorm.weight",
130 "ffn_norm.weight" => "post_attention_layernorm.weight",
131 "attn_q.weight" => "self_attn.q_proj.weight",
132 "attn_k.weight" => "self_attn.k_proj.weight",
133 "attn_v.weight" => "self_attn.v_proj.weight",
134 "attn_output.weight" => "self_attn.o_proj.weight",
135 "attn_q.bias" => "self_attn.q_proj.bias",
136 "attn_k.bias" => "self_attn.k_proj.bias",
137 "attn_v.bias" => "self_attn.v_proj.bias",
138 "attn_q_norm.weight" => "self_attn.q_norm.weight",
139 "attn_k_norm.weight" => "self_attn.k_norm.weight",
140 "ffn_gate.weight" => "mlp.gate_proj.weight",
141 "ffn_up.weight" => "mlp.up_proj.weight",
142 "ffn_down.weight" => "mlp.down_proj.weight",
143 _ => return None,
144 };
145 Some(format!("model.layers.{idx}.{hf_tail}"))
146}
147
148pub fn gguf_to_hf_name_for_arch(gguf: &str, arch: &str) -> Option<String> {
156 if matches!(
157 arch,
158 "gemma2" | "gemma3" | "gemma3n" | "gemma4" | "gemma4moe"
159 ) {
160 match gguf {
161 "token_embd.weight" => return Some("model.embed_tokens.weight".into()),
162 "output_norm.weight" => return Some("model.norm.weight".into()),
163 "output.weight" => return Some("lm_head.weight".into()),
164 _ => {}
165 }
166 let rest = gguf.strip_prefix("blk.")?;
167 let dot = rest.find('.')?;
168 let (idx_str, tail_with_dot) = rest.split_at(dot);
169 let tail = &tail_with_dot[1..];
170 let idx: usize = idx_str.parse().ok()?;
171 let hf_tail = match tail {
172 "attn_norm.weight" => "input_layernorm.weight",
173 "post_attention_norm.weight" => "post_attention_layernorm.weight",
174 "ffn_norm.weight" => "pre_feedforward_layernorm.weight",
175 "post_ffw_norm.weight" => "post_feedforward_layernorm.weight",
176 "attn_q.weight" => "self_attn.q_proj.weight",
177 "attn_k.weight" => "self_attn.k_proj.weight",
178 "attn_v.weight" => "self_attn.v_proj.weight",
179 "attn_output.weight" => "self_attn.o_proj.weight",
180 "attn_q_norm.weight" => "self_attn.q_norm.weight",
188 "attn_k_norm.weight" => "self_attn.k_norm.weight",
189 "layer_output_scale.weight" => "self_attn.output_scale.weight",
192 "ffn_gate.weight" => "mlp.gate_proj.weight",
193 "ffn_up.weight" => "mlp.up_proj.weight",
194 "ffn_down.weight" => "mlp.down_proj.weight",
195 _ => return None,
196 };
197 return Some(format!("model.layers.{idx}.{hf_tail}"));
198 }
199 gguf_to_hf_name(gguf)
200}
201
202pub fn hf_to_gguf_name_for_arch(hf: &str, arch: &str) -> Option<String> {
206 if matches!(
207 arch,
208 "gemma2" | "gemma3" | "gemma3n" | "gemma4" | "gemma4moe"
209 ) {
210 match hf {
211 "model.embed_tokens.weight" => return Some("token_embd.weight".into()),
212 "model.norm.weight" => return Some("output_norm.weight".into()),
213 "lm_head.weight" => return Some("output.weight".into()),
214 _ => {}
215 }
216 let rest = hf.strip_prefix("model.layers.")?;
217 let dot = rest.find('.')?;
218 let (idx_str, tail_with_dot) = rest.split_at(dot);
219 let tail = &tail_with_dot[1..];
220 let idx: usize = idx_str.parse().ok()?;
221 let gguf_tail = match tail {
222 "input_layernorm.weight" => "attn_norm.weight",
223 "post_attention_layernorm.weight" => "post_attention_norm.weight",
224 "pre_feedforward_layernorm.weight" => "ffn_norm.weight",
225 "post_feedforward_layernorm.weight" => "post_ffw_norm.weight",
226 "self_attn.q_proj.weight" => "attn_q.weight",
227 "self_attn.k_proj.weight" => "attn_k.weight",
228 "self_attn.v_proj.weight" => "attn_v.weight",
229 "self_attn.o_proj.weight" => "attn_output.weight",
230 "self_attn.q_norm.weight" => "attn_q_norm.weight",
231 "self_attn.k_norm.weight" => "attn_k_norm.weight",
232 "self_attn.output_scale.weight" => "layer_output_scale.weight",
233 "mlp.gate_proj.weight" => "ffn_gate.weight",
234 "mlp.up_proj.weight" => "ffn_up.weight",
235 "mlp.down_proj.weight" => "ffn_down.weight",
236 _ => return hf_to_gguf_name(hf),
237 };
238 return Some(format!("blk.{idx}.{gguf_tail}"));
239 }
240 hf_to_gguf_name(hf)
241}
242
243fn is_gemma_norm_weight(name: &str) -> bool {
249 if name == "output_norm.weight" || name == "model.norm.weight" {
250 return true;
251 }
252 if let Some(rest) = name
253 .strip_prefix("blk.")
254 .and_then(|r| r.split_once('.').map(|x| x.1))
255 {
256 return matches!(
257 rest,
258 "attn_norm.weight"
259 | "post_attention_norm.weight"
260 | "ffn_norm.weight"
261 | "post_ffw_norm.weight"
262 );
263 }
264 if let Some(rest) = name
265 .strip_prefix("model.layers.")
266 .and_then(|r| r.split_once('.').map(|x| x.1))
267 {
268 return matches!(
269 rest,
270 "input_layernorm.weight"
271 | "post_attention_layernorm.weight"
272 | "pre_feedforward_layernorm.weight"
273 | "post_feedforward_layernorm.weight"
274 );
275 }
276 false
277}
278
279pub fn is_mtp_weight(name: &str) -> bool {
292 name.contains("mtp_") || name.contains(".mtp") || name.starts_with("mtp")
293}
294
295pub fn ggml_type_to_quant_scheme(dtype: rlx_gguf::GgmlType) -> Option<QuantScheme> {
297 use rlx_gguf::GgmlType;
298 match dtype {
299 GgmlType::Q2K => Some(QuantScheme::GgufQ2K),
300 GgmlType::Q3K => Some(QuantScheme::GgufQ3K),
301 GgmlType::Q4K => Some(QuantScheme::GgufQ4K),
302 GgmlType::Q5K => Some(QuantScheme::GgufQ5K),
303 GgmlType::Q6K => Some(QuantScheme::GgufQ6K),
304 GgmlType::Q8K => Some(QuantScheme::GgufQ8K),
305 GgmlType::Q4_0 => Some(QuantScheme::GgufQ4_0),
306 GgmlType::Q8_0 => Some(QuantScheme::GgufQ8_0),
307 _ => None,
308 }
309}
310
311pub fn dequant_matmul_supported(scheme: QuantScheme) -> bool {
319 match scheme {
320 QuantScheme::GgufQ6K => q6k_dequant_matmul_supported(),
321 _ => true,
322 }
323}
324
325fn q6k_dequant_matmul_supported() -> bool {
326 static OK: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
327 *OK.get_or_init(probe_q6k_block_dequant)
328}
329
330fn probe_q6k_block_dequant() -> bool {
332 use rlx_gguf::{QK_K, dequant_q6_k, dequant_q6_k_block};
333 const BLK: usize = QK_K / 2 + QK_K / 4 + QK_K / 16 + 2;
334 let mut block = [0u8; BLK];
335 let sc_off = QK_K / 2 + QK_K / 4;
336 block[sc_off] = 0xFF;
337 block[0] = 0x21;
338 block[QK_K / 2] = 0x08;
339 block[BLK - 2..].copy_from_slice(&half::f16::ONE.to_le_bytes());
340
341 let mut out_block = [0f32; QK_K];
342 dequant_q6_k_block(&block, &mut out_block);
343 let full = match dequant_q6_k(&block, QK_K) {
344 Ok(v) => v,
345 Err(_) => return false,
346 };
347 (out_block[0] - full[0]).abs() < 1e-4
348}
349
350pub trait WeightLoader: Send {
356 fn format_id(&self) -> &'static str {
358 "unknown"
359 }
360 fn len(&self) -> usize;
362 fn is_empty(&self) -> bool {
363 self.len() == 0
364 }
365 fn take(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)>;
368 fn take_transposed(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)>;
373 fn take_packed(&mut self, key: &str) -> Result<Option<crate::weight_map::PackedWeightTensor>> {
375 let _ = key;
376 Ok(None)
377 }
378 fn tensor_bytes_borrowed(&self, key: &str) -> Option<&[u8]> {
380 let _ = key;
381 None
382 }
383 fn remaining_keys(&self) -> Vec<String>;
386 fn arch_hint(&self) -> Option<&str> {
392 None
393 }
394}
395
396impl WeightLoader for WeightMap {
397 fn format_id(&self) -> &'static str {
398 "safetensors"
399 }
400 fn len(&self) -> usize {
401 Self::len(self)
402 }
403 fn take(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
404 Self::take(self, key)
405 }
406 fn take_transposed(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
407 Self::take_transposed(self, key)
408 }
409 fn remaining_keys(&self) -> Vec<String> {
410 self.keys().map(|s| s.to_string()).collect()
411 }
412}
413
414pub type ArcF32Tensor = (Arc<Vec<f32>>, Vec<usize>);
417
418pub struct ArcCacheLoader<'a> {
423 cache: &'a HashMap<String, ArcF32Tensor>,
424}
425
426impl<'a> ArcCacheLoader<'a> {
427 pub fn new(cache: &'a HashMap<String, ArcF32Tensor>) -> Self {
428 Self { cache }
429 }
430}
431
432impl WeightLoader for ArcCacheLoader<'_> {
433 fn format_id(&self) -> &'static str {
434 "arc-cache"
435 }
436 fn len(&self) -> usize {
437 self.cache.len()
438 }
439 fn take(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
440 let (data, shape) = self
441 .cache
442 .get(key)
443 .ok_or_else(|| anyhow!("weight not found in arc cache: {key}"))?;
444 Ok((data.as_ref().clone(), shape.clone()))
445 }
446 fn take_transposed(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
447 let (data, shape) = self.take(key)?;
448 if shape.len() != 2 {
449 anyhow::bail!("transpose requires 2D, got {shape:?}");
450 }
451 let (rows, cols) = (shape[0], shape[1]);
452 let mut transposed = vec![0f32; data.len()];
453 for i in 0..rows {
454 for j in 0..cols {
455 transposed[j * rows + i] = data[i * cols + j];
456 }
457 }
458 Ok((transposed, vec![cols, rows]))
459 }
460 fn remaining_keys(&self) -> Vec<String> {
461 self.cache.keys().cloned().collect()
462 }
463}
464
465pub struct HfTranslatingLoader<L: WeightLoader> {
478 inner: L,
479}
480
481impl<L: WeightLoader> HfTranslatingLoader<L> {
482 pub fn new(inner: L) -> Self {
483 Self { inner }
484 }
485 pub fn into_inner(self) -> L {
486 self.inner
487 }
488 pub fn inner(&self) -> &L {
489 &self.inner
490 }
491 pub fn inner_mut(&mut self) -> &mut L {
492 &mut self.inner
493 }
494}
495
496impl<L: WeightLoader> WeightLoader for HfTranslatingLoader<L> {
497 fn format_id(&self) -> &'static str {
498 self.inner.format_id()
499 }
500 fn len(&self) -> usize {
501 self.inner.len()
502 }
503 fn take(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
504 match self.inner.take(key) {
505 Ok(v) => Ok(v),
506 Err(_) => {
507 if let Some(hf) = gguf_to_hf_name(key) {
508 return self.inner.take(&hf);
509 }
510 self.inner.take(key)
511 }
512 }
513 }
514 fn take_transposed(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
515 match self.inner.take_transposed(key) {
516 Ok(v) => Ok(v),
517 Err(_) => {
518 if let Some(hf) = gguf_to_hf_name(key) {
519 return self.inner.take_transposed(&hf);
520 }
521 self.inner.take_transposed(key)
522 }
523 }
524 }
525 fn take_packed(&mut self, key: &str) -> Result<Option<crate::weight_map::PackedWeightTensor>> {
526 self.inner.take_packed(key)
527 }
528 fn tensor_bytes_borrowed(&self, key: &str) -> Option<&[u8]> {
529 self.inner.tensor_bytes_borrowed(key)
530 }
531 fn remaining_keys(&self) -> Vec<String> {
532 self.inner.remaining_keys()
533 }
534}
535
536pub fn load_from_path(path: &str) -> Result<Box<dyn WeightLoader>> {
538 crate::weight_registry::open_weight_loader(Path::new(path))
539}
540
541pub struct GgufLoader {
549 file: rlx_gguf::GgufFile,
550 arch: String,
551 taken: HashSet<String>,
552 include_mtp: bool,
559 mtp_layer_threshold: Option<u32>,
564}
565
566impl GgufLoader {
567 pub fn from_file(path: &str) -> Result<Self> {
568 let file = crate::gguf_support::load_gguf_file(std::path::Path::new(path))?;
569 let arch = gguf_architecture_str(&file)
570 .unwrap_or("unknown")
571 .to_string();
572 let mtp_layer_threshold = compute_mtp_layer_threshold(&file);
573 Ok(Self {
574 file,
575 arch,
576 taken: HashSet::new(),
577 include_mtp: false,
578 mtp_layer_threshold,
579 })
580 }
581
582 pub fn architecture(&self) -> &str {
583 &self.arch
584 }
585
586 pub fn mtp_layer_threshold(&self) -> Option<u32> {
593 self.mtp_layer_threshold
594 }
595
596 pub fn file(&self) -> &rlx_gguf::GgufFile {
600 &self.file
601 }
602
603 pub fn tensor_bytes_borrowed(&self, key: &str) -> Option<&[u8]> {
610 let real = self.resolve(key).ok()?;
611 let t = self.file.get(&real)?;
612 self.file.tensor_bytes(t).ok()
613 }
614
615 pub fn take_packed_metadata(
623 &mut self,
624 key: &str,
625 ) -> Result<Option<(rlx_ir::quant::QuantScheme, Vec<usize>)>> {
626 let real = self.resolve(key)?;
627 if self.taken.contains(&real) {
628 return Err(anyhow!("weight already taken: {key} (→ {real})"));
629 }
630 if !self.include_mtp && self.is_mtp_tensor(&real) {
631 return Err(anyhow!(
632 "refusing to take MTP weight `{real}` without include_mtp(true)"
633 ));
634 }
635 let t = self
636 .file
637 .get(&real)
638 .ok_or_else(|| anyhow!("tensor missing: {real}"))?;
639 let Some(scheme) = ggml_type_to_quant_scheme(t.dtype) else {
640 return Ok(None);
641 };
642 if !dequant_matmul_supported(scheme) {
643 return Ok(None);
644 }
645 let mut shape = t.shape.clone();
646 shape.reverse();
647 self.taken.insert(real);
648 Ok(Some((scheme, shape)))
649 }
650
651 pub fn is_mtp_tensor(&self, name: &str) -> bool {
655 if is_mtp_weight(name) {
656 return true;
657 }
658 if let Some(thresh) = self.mtp_layer_threshold {
659 if let Some(rest) = name.strip_prefix("blk.") {
660 if let Some(dot) = rest.find('.') {
661 if let Ok(idx) = rest[..dot].parse::<u32>() {
662 if idx >= thresh {
663 return true;
664 }
665 }
666 }
667 }
668 }
669 false
670 }
671
672 pub fn include_mtp(&mut self, include: bool) -> &mut Self {
680 self.include_mtp = include;
681 self
682 }
683
684 pub fn take_packed(&mut self, key: &str) -> Result<Option<PackedWeightTensor>> {
698 let real = self.resolve(key)?;
699 if self.taken.contains(&real) {
700 return Err(anyhow!("weight already taken: {key} (→ {real})"));
701 }
702 if !self.include_mtp && self.is_mtp_tensor(&real) {
703 return Err(anyhow!(
704 "refusing to take MTP weight `{real}` without include_mtp(true)"
705 ));
706 }
707 let t = self
708 .file
709 .get(&real)
710 .ok_or_else(|| anyhow!("tensor missing: {real}"))?;
711 let Some(scheme) = ggml_type_to_quant_scheme(t.dtype) else {
716 return Ok(None);
717 };
718 if !dequant_matmul_supported(scheme) {
719 return Ok(None);
720 };
721 let bytes = self
722 .file
723 .tensor_bytes(t)
724 .with_context(|| format!("read packed bytes for {real}"))?
725 .to_vec();
726 let mut shape = t.shape.clone();
727 shape.reverse();
731 self.taken.insert(real);
732 Ok(Some((bytes, scheme, shape)))
733 }
734
735 pub fn take_mtp(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
740 if !self.is_mtp_tensor(key) {
741 return Err(anyhow!("not an MTP weight under this file's scheme: {key}"));
742 }
743 if !self.file.tensors.contains_key(key) {
744 return Err(anyhow!("MTP weight not found in GGUF: {key}"));
745 }
746 if self.taken.contains(key) {
747 return Err(anyhow!("MTP weight already taken: {key}"));
748 }
749 let (data, raw_shape) = self.file.dequant_f32(key)?;
750 self.taken.insert(key.to_string());
751 let mut shape = raw_shape;
752 shape.reverse();
753 Ok((data, shape))
754 }
755}
756
757impl GgufLoader {
758 fn resolve(&self, key: &str) -> Result<String> {
761 resolve_gguf_tensor_name(&self.file, &self.arch, key)
762 .ok_or_else(|| anyhow!("weight not found in GGUF (arch={}): {key}", self.arch))
763 }
764}
765
766impl WeightLoader for GgufLoader {
767 fn format_id(&self) -> &'static str {
768 "gguf"
769 }
770 fn arch_hint(&self) -> Option<&str> {
771 Some(&self.arch)
772 }
773 fn take_packed(&mut self, key: &str) -> Result<Option<crate::weight_map::PackedWeightTensor>> {
774 self.take_packed(key)
775 }
776 fn tensor_bytes_borrowed(&self, key: &str) -> Option<&[u8]> {
777 GgufLoader::tensor_bytes_borrowed(self, key)
778 }
779 fn len(&self) -> usize {
780 self.file
781 .tensors
782 .keys()
783 .filter(|k| !self.taken.contains(*k) && (self.include_mtp || !self.is_mtp_tensor(k)))
784 .count()
785 }
786 fn take(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
787 let real = self.resolve(key)?;
788 if self.taken.contains(&real) {
789 return Err(anyhow!("weight already taken: {key} (→ {real})"));
790 }
791 if !self.include_mtp && self.is_mtp_tensor(&real) {
792 return Err(anyhow!(
793 "refusing to take MTP weight `{real}` without include_mtp(true); \
794 use loader.take_mtp(...) for explicit MTP grabs or \
795 loader.include_mtp(true) to include them in drains"
796 ));
797 }
798 let (mut data, raw_shape) = self.file.dequant_f32(&real)?;
799 self.taken.insert(real.clone());
800 if matches!(
809 self.arch.as_str(),
810 "gemma" | "gemma2" | "gemma3" | "gemma3n" | "gemma4"
811 ) && is_gemma_norm_weight(&real)
812 {
813 for v in data.iter_mut() {
814 *v -= 1.0;
815 }
816 }
817 let mut shape = raw_shape;
823 shape.reverse();
824 Ok((data, shape))
825 }
826 fn take_transposed(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
836 let (data, shape) = self.take(key)?;
839 if shape.len() != 2 {
840 return Err(anyhow!("transpose requires 2D, got {shape:?}"));
841 }
842 let (rows, cols) = (shape[0], shape[1]);
843 let mut t = vec![0f32; data.len()];
844 for i in 0..rows {
845 for j in 0..cols {
846 t[j * rows + i] = data[i * cols + j];
847 }
848 }
849 Ok((t, vec![cols, rows]))
850 }
851 fn remaining_keys(&self) -> Vec<String> {
852 self.file
857 .tensors
858 .keys()
859 .filter(|k| {
860 !self.taken.contains(k.as_str()) && (self.include_mtp || !self.is_mtp_tensor(k))
861 })
862 .cloned()
863 .collect()
864 }
865}
866
867impl GgufLoader {
868 pub fn mtp_keys(&self) -> Vec<String> {
875 self.file
876 .tensors
877 .keys()
878 .filter(|k| self.is_mtp_tensor(k))
879 .cloned()
880 .collect()
881 }
882}
883
884#[cfg(test)]
885mod tests {
886 use super::*;
887
888 #[test]
889 fn unknown_extension_errors() {
890 let r = load_from_path("/tmp/no-such-thing.bin");
891 match r {
892 Err(e) => assert!(e.to_string().contains("unsupported")),
893 Ok(_) => panic!("expected error"),
894 }
895 }
896
897 #[test]
898 fn hf_to_gguf_top_level() {
899 assert_eq!(
900 hf_to_gguf_name("model.embed_tokens.weight").as_deref(),
901 Some("token_embd.weight")
902 );
903 assert_eq!(
904 hf_to_gguf_name("model.norm.weight").as_deref(),
905 Some("output_norm.weight")
906 );
907 assert_eq!(
908 hf_to_gguf_name("lm_head.weight").as_deref(),
909 Some("output.weight")
910 );
911 }
912
913 #[test]
914 fn hf_to_gguf_per_layer() {
915 let cases = [
916 (
917 "model.layers.0.self_attn.q_proj.weight",
918 "blk.0.attn_q.weight",
919 ),
920 (
921 "model.layers.7.self_attn.o_proj.weight",
922 "blk.7.attn_output.weight",
923 ),
924 (
925 "model.layers.3.mlp.gate_proj.weight",
926 "blk.3.ffn_gate.weight",
927 ),
928 (
929 "model.layers.12.mlp.down_proj.weight",
930 "blk.12.ffn_down.weight",
931 ),
932 (
933 "model.layers.4.input_layernorm.weight",
934 "blk.4.attn_norm.weight",
935 ),
936 (
937 "model.layers.4.post_attention_layernorm.weight",
938 "blk.4.ffn_norm.weight",
939 ),
940 (
941 "model.layers.0.self_attn.q_norm.weight",
942 "blk.0.attn_q_norm.weight",
943 ),
944 ];
945 for (hf, gguf) in cases {
946 assert_eq!(
947 hf_to_gguf_name(hf).as_deref(),
948 Some(gguf),
949 "mismatch for {hf}"
950 );
951 }
952 }
953
954 #[test]
955 fn hf_to_gguf_gemma4_output_scale() {
956 assert_eq!(
957 hf_to_gguf_name_for_arch("model.layers.5.self_attn.output_scale.weight", "gemma4")
958 .as_deref(),
959 Some("blk.5.layer_output_scale.weight")
960 );
961 assert_eq!(
962 gguf_to_hf_name_for_arch("blk.5.layer_output_scale.weight", "gemma4").as_deref(),
963 Some("model.layers.5.self_attn.output_scale.weight")
964 );
965 }
966
967 #[test]
968 fn hf_to_gguf_unknown_returns_none() {
969 assert!(hf_to_gguf_name("model.layers.0.some_new_thing.weight").is_none());
970 assert!(hf_to_gguf_name("model.layers.foo.input_layernorm.weight").is_none());
971 }
972
973 #[test]
974 fn mtp_detection() {
975 assert!(is_mtp_weight("mtp_blk.0.attn_q.weight"));
976 assert!(is_mtp_weight("output_mtp_0.weight"));
977 assert!(is_mtp_weight("model.layers.0.mtp_head.weight"));
978 assert!(!is_mtp_weight("blk.0.attn_q.weight"));
979 assert!(!is_mtp_weight("output.weight"));
980 }
981
982 #[test]
989 fn ggml_q4_0_maps_to_packed_scheme() {
990 use rlx_gguf::GgmlType;
991 assert_eq!(
992 ggml_type_to_quant_scheme(GgmlType::Q4_0),
993 Some(rlx_ir::quant::QuantScheme::GgufQ4_0)
994 );
995 assert_eq!(
996 ggml_type_to_quant_scheme(GgmlType::Q8_0),
997 Some(rlx_ir::quant::QuantScheme::GgufQ8_0)
998 );
999 }
1000
1001 #[test]
1002 fn q6k_dequant_matmul_follows_block_probe() {
1003 use rlx_ir::quant::QuantScheme;
1004 assert!(dequant_matmul_supported(QuantScheme::GgufQ4K));
1005 assert_eq!(
1006 dequant_matmul_supported(QuantScheme::GgufQ6K),
1007 super::probe_q6k_block_dequant()
1008 );
1009 }
1010
1011 #[test]
1012 fn gguf_loader_threshold_based_mtp_detection() {
1013 let mut buf: Vec<u8> = Vec::new();
1014 buf.extend_from_slice(&rlx_gguf::GGUF_MAGIC.to_le_bytes());
1015 buf.extend_from_slice(&3u32.to_le_bytes());
1016 buf.extend_from_slice(&3u64.to_le_bytes()); buf.extend_from_slice(&3u64.to_le_bytes()); let write_string = |buf: &mut Vec<u8>, k: &str, v: &str| {
1020 buf.extend_from_slice(&(k.len() as u64).to_le_bytes());
1021 buf.extend_from_slice(k.as_bytes());
1022 buf.extend_from_slice(&8u32.to_le_bytes());
1023 buf.extend_from_slice(&(v.len() as u64).to_le_bytes());
1024 buf.extend_from_slice(v.as_bytes());
1025 };
1026 let write_u32 = |buf: &mut Vec<u8>, k: &str, v: u32| {
1027 buf.extend_from_slice(&(k.len() as u64).to_le_bytes());
1028 buf.extend_from_slice(k.as_bytes());
1029 buf.extend_from_slice(&4u32.to_le_bytes()); buf.extend_from_slice(&v.to_le_bytes());
1031 };
1032 write_string(&mut buf, "general.architecture", "qwen35");
1033 write_u32(&mut buf, "qwen35.block_count", 25);
1034 write_u32(&mut buf, "qwen35.nextn_predict_layers", 1);
1035 let write_tensor = |buf: &mut Vec<u8>, name: &str, shape: &[usize], off: u64| {
1038 buf.extend_from_slice(&(name.len() as u64).to_le_bytes());
1039 buf.extend_from_slice(name.as_bytes());
1040 buf.extend_from_slice(&(shape.len() as u32).to_le_bytes());
1041 for &d in shape {
1042 buf.extend_from_slice(&(d as u64).to_le_bytes());
1043 }
1044 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&off.to_le_bytes());
1046 };
1047 write_tensor(&mut buf, "blk.0.attn_q.weight", &[4, 4], 0);
1048 write_tensor(&mut buf, "blk.24.attn_q.weight", &[4, 4], 64);
1049 write_tensor(&mut buf, "token_embd.weight", &[4, 4], 128);
1050 while !buf
1051 .len()
1052 .is_multiple_of(rlx_gguf::DEFAULT_ALIGNMENT as usize)
1053 {
1054 buf.push(0);
1055 }
1056 for _ in 0..(3 * 16) {
1058 buf.extend_from_slice(&0.5f32.to_le_bytes());
1059 }
1060 let path = std::env::temp_dir().join("rlx_mtp_threshold_test.gguf");
1061 std::fs::write(&path, &buf).unwrap();
1062 let loader = GgufLoader::from_file(path.to_str().unwrap()).unwrap();
1063
1064 assert_eq!(loader.mtp_layer_threshold(), Some(24));
1065 assert!(!loader.is_mtp_tensor("blk.0.attn_q.weight"));
1066 assert!(loader.is_mtp_tensor("blk.24.attn_q.weight"));
1067 assert!(!loader.is_mtp_tensor("token_embd.weight"));
1068 let mtp = loader.mtp_keys();
1069 assert_eq!(mtp, vec!["blk.24.attn_q.weight".to_string()]);
1070
1071 std::fs::remove_file(&path).ok();
1072 }
1073
1074 #[test]
1081 fn gguf_loader_resolves_hf_names_and_skips_mtp() {
1082 let mut tensors = Vec::new();
1083 let mut data: Vec<f32> = Vec::new();
1084
1085 let t1: Vec<f32> = (0..12).map(|x| x as f32).collect();
1087 tensors.push(("token_embd.weight", vec![3usize, 4], data.len()));
1088 data.extend_from_slice(&t1);
1089
1090 let t2: Vec<f32> = (100..116).map(|x| x as f32).collect();
1092 tensors.push(("blk.0.attn_q.weight", vec![4usize, 4], data.len()));
1093 data.extend_from_slice(&t2);
1094
1095 let t3: Vec<f32> = vec![0.5f32; 8];
1097 tensors.push(("output_mtp_0.weight", vec![2usize, 4], data.len()));
1098 data.extend_from_slice(&t3);
1099
1100 let mut buf: Vec<u8> = Vec::new();
1102 buf.extend_from_slice(&rlx_gguf::GGUF_MAGIC.to_le_bytes());
1103 buf.extend_from_slice(&3u32.to_le_bytes()); buf.extend_from_slice(&(tensors.len() as u64).to_le_bytes());
1105 buf.extend_from_slice(&0u64.to_le_bytes()); for (name, shape, _) in &tensors {
1109 buf.extend_from_slice(&(name.len() as u64).to_le_bytes());
1110 buf.extend_from_slice(name.as_bytes());
1111 buf.extend_from_slice(&(shape.len() as u32).to_le_bytes());
1112 for &d in shape {
1113 buf.extend_from_slice(&(d as u64).to_le_bytes());
1114 }
1115 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u64.to_le_bytes());
1118 }
1119 while !buf
1121 .len()
1122 .is_multiple_of(rlx_gguf::DEFAULT_ALIGNMENT as usize)
1123 {
1124 buf.push(0);
1125 }
1126 let data_start = buf.len();
1127 for v in &data {
1128 buf.extend_from_slice(&v.to_le_bytes());
1129 }
1130 let header = (4 + 4 + 8 + 8) as usize; let mut cursor = header;
1133 for (name, shape, byte_off) in &tensors {
1134 let name_len_bytes = 8;
1135 let name_bytes = name.len();
1136 let n_dims_bytes = 4;
1137 let dims_bytes = shape.len() * 8;
1138 let dtype_bytes = 4;
1139 let off_bytes = 8;
1140 let info_size =
1141 name_len_bytes + name_bytes + n_dims_bytes + dims_bytes + dtype_bytes + off_bytes;
1142 let off_field_at = cursor + info_size - off_bytes;
1143 let final_off = (*byte_off * 4) as u64; for i in 0..8 {
1145 buf[off_field_at + i] = (final_off >> (i * 8)) as u8;
1146 }
1147 cursor += info_size;
1148 }
1149 let _ = data_start;
1150
1151 let path = std::env::temp_dir().join("rlx_test_qwen3_mini.gguf");
1153 std::fs::write(&path, &buf).unwrap();
1154
1155 let mut loader = GgufLoader::from_file(path.to_str().unwrap()).unwrap();
1156 assert_eq!(loader.len(), 2);
1158
1159 let (out, shape) = loader
1165 .take("model.embed_tokens.weight")
1166 .expect("hf-named token_embd should resolve");
1167 assert_eq!(shape, vec![4, 3]);
1168 assert_eq!(&out, &t1);
1169
1170 let (out, shape) = loader
1171 .take("model.layers.0.self_attn.q_proj.weight")
1172 .expect("hf-named attn_q should resolve");
1173 assert_eq!(shape, vec![4, 4]);
1174 assert_eq!(&out, &t2);
1175
1176 assert_eq!(loader.remaining_keys(), Vec::<String>::new());
1178 assert_eq!(loader.mtp_keys(), vec!["output_mtp_0.weight".to_string()]);
1179
1180 let mut loader2 = GgufLoader::from_file(path.to_str().unwrap()).unwrap();
1184 loader2.include_mtp(true);
1185 let visible: std::collections::HashSet<String> =
1186 loader2.remaining_keys().into_iter().collect();
1187 assert!(visible.contains("token_embd.weight"));
1188 assert!(visible.contains("blk.0.attn_q.weight"));
1189 assert!(
1190 visible.contains("output_mtp_0.weight"),
1191 "MTP weight should be visible with include_mtp(true)"
1192 );
1193 let (mtp_data, mtp_shape) = loader2.take_mtp("output_mtp_0.weight").unwrap();
1194 assert_eq!(mtp_shape, vec![4, 2]);
1195 assert_eq!(mtp_data, t3);
1196
1197 let mut loader3 = GgufLoader::from_file(path.to_str().unwrap()).unwrap();
1199 let err = loader3.take("output_mtp_0.weight").unwrap_err();
1200 let msg = format!("{err:#}");
1201 assert!(
1202 msg.contains("include_mtp(true)"),
1203 "expected MTP guard error, got: {msg}"
1204 );
1205
1206 std::fs::remove_file(&path).ok();
1207 }
1208
1209 #[test]
1210 fn missing_gguf_file_errors() {
1211 let r = load_from_path("/tmp/no-such-thing-rlx-gguf-test.gguf");
1214 assert!(r.is_err());
1215 }
1216}