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 if matches!(arch, "phi3" | "phi4") {
200 match gguf {
201 "token_embd.weight" => return Some("model.embed_tokens.weight".into()),
202 "output_norm.weight" => return Some("model.norm.weight".into()),
203 "output.weight" => return Some("lm_head.weight".into()),
204 _ => {}
205 }
206 let rest = gguf.strip_prefix("blk.")?;
207 let dot = rest.find('.')?;
208 let (idx_str, tail_with_dot) = rest.split_at(dot);
209 let tail = &tail_with_dot[1..];
210 let idx: usize = idx_str.parse().ok()?;
211 let hf_tail = match tail {
212 "attn_norm.weight" => "input_layernorm.weight",
213 "ffn_norm.weight" => "post_attention_layernorm.weight",
214 "attn_qkv.weight" => "self_attn.qkv.weight",
215 "attn_output.weight" => "self_attn.o_proj.weight",
216 "ffn_up.weight" => "mlp.gate_up.weight",
217 "ffn_down.weight" => "mlp.down_proj.weight",
218 _ => return None,
219 };
220 return Some(format!("model.layers.{idx}.{hf_tail}"));
221 }
222 gguf_to_hf_name(gguf)
223}
224
225pub fn hf_to_gguf_name_for_arch(hf: &str, arch: &str) -> Option<String> {
229 if matches!(
230 arch,
231 "gemma2" | "gemma3" | "gemma3n" | "gemma4" | "gemma4moe"
232 ) {
233 match hf {
234 "model.embed_tokens.weight" => return Some("token_embd.weight".into()),
235 "model.norm.weight" => return Some("output_norm.weight".into()),
236 "lm_head.weight" => return Some("output.weight".into()),
237 _ => {}
238 }
239 let rest = hf.strip_prefix("model.layers.")?;
240 let dot = rest.find('.')?;
241 let (idx_str, tail_with_dot) = rest.split_at(dot);
242 let tail = &tail_with_dot[1..];
243 let idx: usize = idx_str.parse().ok()?;
244 let gguf_tail = match tail {
245 "input_layernorm.weight" => "attn_norm.weight",
246 "post_attention_layernorm.weight" => "post_attention_norm.weight",
247 "pre_feedforward_layernorm.weight" => "ffn_norm.weight",
248 "post_feedforward_layernorm.weight" => "post_ffw_norm.weight",
249 "self_attn.q_proj.weight" => "attn_q.weight",
250 "self_attn.k_proj.weight" => "attn_k.weight",
251 "self_attn.v_proj.weight" => "attn_v.weight",
252 "self_attn.o_proj.weight" => "attn_output.weight",
253 "self_attn.q_norm.weight" => "attn_q_norm.weight",
254 "self_attn.k_norm.weight" => "attn_k_norm.weight",
255 "self_attn.output_scale.weight" => "layer_output_scale.weight",
256 "mlp.gate_proj.weight" => "ffn_gate.weight",
257 "mlp.up_proj.weight" => "ffn_up.weight",
258 "mlp.down_proj.weight" => "ffn_down.weight",
259 _ => return hf_to_gguf_name(hf),
260 };
261 return Some(format!("blk.{idx}.{gguf_tail}"));
262 }
263 if matches!(arch, "phi3" | "phi4") {
264 match hf {
265 "model.embed_tokens.weight" => return Some("token_embd.weight".into()),
266 "model.norm.weight" => return Some("output_norm.weight".into()),
267 "lm_head.weight" => return Some("output.weight".into()),
268 _ => {}
269 }
270 let rest = hf.strip_prefix("model.layers.")?;
271 let dot = rest.find('.')?;
272 let (idx_str, tail_with_dot) = rest.split_at(dot);
273 let tail = &tail_with_dot[1..];
274 let idx: usize = idx_str.parse().ok()?;
275 let gguf_tail = match tail {
276 "input_layernorm.weight" => "attn_norm.weight",
277 "post_attention_layernorm.weight" => "ffn_norm.weight",
278 "self_attn.qkv.weight" => "attn_qkv.weight",
279 "self_attn.o_proj.weight" => "attn_output.weight",
280 "mlp.gate_up.weight" => "ffn_up.weight",
281 "mlp.down_proj.weight" => "ffn_down.weight",
282 _ => return hf_to_gguf_name(hf),
283 };
284 return Some(format!("blk.{idx}.{gguf_tail}"));
285 }
286 hf_to_gguf_name(hf)
287}
288
289fn is_gemma_norm_weight(name: &str) -> bool {
299 if name == "output_norm.weight" || name == "model.norm.weight" {
300 return true;
301 }
302 if let Some(rest) = name
303 .strip_prefix("blk.")
304 .and_then(|r| r.split_once('.').map(|x| x.1))
305 {
306 return matches!(
307 rest,
308 "attn_norm.weight"
309 | "post_attention_norm.weight"
310 | "ffn_norm.weight"
311 | "post_ffw_norm.weight"
312 | "attn_q_norm.weight"
313 | "attn_k_norm.weight"
314 );
315 }
316 if let Some(rest) = name
317 .strip_prefix("model.layers.")
318 .and_then(|r| r.split_once('.').map(|x| x.1))
319 {
320 return matches!(
321 rest,
322 "input_layernorm.weight"
323 | "post_attention_layernorm.weight"
324 | "pre_feedforward_layernorm.weight"
325 | "post_feedforward_layernorm.weight"
326 | "self_attn.q_norm.weight"
327 | "self_attn.k_norm.weight"
328 );
329 }
330 false
331}
332
333pub fn is_mtp_weight(name: &str) -> bool {
346 name.contains("mtp_") || name.contains(".mtp") || name.starts_with("mtp")
347}
348
349pub fn ggml_type_to_quant_scheme(dtype: rlx_gguf::GgmlType) -> Option<QuantScheme> {
351 use rlx_gguf::GgmlType;
352 match dtype {
353 GgmlType::Q2K => Some(QuantScheme::GgufQ2K),
354 GgmlType::Q3K => Some(QuantScheme::GgufQ3K),
355 GgmlType::Q4K => Some(QuantScheme::GgufQ4K),
356 GgmlType::Q5K => Some(QuantScheme::GgufQ5K),
357 GgmlType::Q6K => Some(QuantScheme::GgufQ6K),
358 GgmlType::Q8K => Some(QuantScheme::GgufQ8K),
359 GgmlType::Q4_0 => Some(QuantScheme::GgufQ4_0),
360 GgmlType::Q5_0 => Some(QuantScheme::GgufQ5_0),
361 GgmlType::Q8_0 => Some(QuantScheme::GgufQ8_0),
362 _ => None,
363 }
364}
365
366pub fn dequant_matmul_supported(scheme: QuantScheme) -> bool {
374 match scheme {
375 QuantScheme::GgufQ6K => q6k_dequant_matmul_supported(),
376 _ => true,
377 }
378}
379
380fn q6k_dequant_matmul_supported() -> bool {
381 static OK: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
382 *OK.get_or_init(probe_q6k_block_dequant)
383}
384
385fn probe_q6k_block_dequant() -> bool {
387 use rlx_gguf::{QK_K, dequant_q6_k, dequant_q6_k_block};
388 const BLK: usize = QK_K / 2 + QK_K / 4 + QK_K / 16 + 2;
389 let mut block = [0u8; BLK];
390 let sc_off = QK_K / 2 + QK_K / 4;
391 block[sc_off] = 0xFF;
392 block[0] = 0x21;
393 block[QK_K / 2] = 0x08;
394 block[BLK - 2..].copy_from_slice(&half::f16::ONE.to_le_bytes());
395
396 let mut out_block = [0f32; QK_K];
397 dequant_q6_k_block(&block, &mut out_block);
398 let full = match dequant_q6_k(&block, QK_K) {
399 Ok(v) => v,
400 Err(_) => return false,
401 };
402 (out_block[0] - full[0]).abs() < 1e-4
403}
404
405pub trait WeightLoader: Send {
411 fn format_id(&self) -> &'static str {
413 "unknown"
414 }
415 fn len(&self) -> usize;
417 fn is_empty(&self) -> bool {
418 self.len() == 0
419 }
420 fn take(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)>;
423 fn take_transposed(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)>;
428 fn take_packed(&mut self, key: &str) -> Result<Option<crate::weight_map::PackedWeightTensor>> {
430 let _ = key;
431 Ok(None)
432 }
433 fn tensor_bytes_borrowed(&self, key: &str) -> Option<&[u8]> {
435 let _ = key;
436 None
437 }
438 fn remaining_keys(&self) -> Vec<String>;
441 fn arch_hint(&self) -> Option<&str> {
447 None
448 }
449}
450
451impl WeightLoader for WeightMap {
452 fn format_id(&self) -> &'static str {
453 "safetensors"
454 }
455 fn len(&self) -> usize {
456 Self::len(self)
457 }
458 fn take(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
459 Self::take(self, key)
460 }
461 fn take_transposed(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
462 Self::take_transposed(self, key)
463 }
464 fn remaining_keys(&self) -> Vec<String> {
465 self.keys().map(|s| s.to_string()).collect()
466 }
467}
468
469pub type ArcF32Tensor = (Arc<Vec<f32>>, Vec<usize>);
472
473pub struct ArcCacheLoader<'a> {
478 cache: &'a HashMap<String, ArcF32Tensor>,
479}
480
481impl<'a> ArcCacheLoader<'a> {
482 pub fn new(cache: &'a HashMap<String, ArcF32Tensor>) -> Self {
483 Self { cache }
484 }
485}
486
487impl WeightLoader for ArcCacheLoader<'_> {
488 fn format_id(&self) -> &'static str {
489 "arc-cache"
490 }
491 fn len(&self) -> usize {
492 self.cache.len()
493 }
494 fn take(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
495 let (data, shape) = self
496 .cache
497 .get(key)
498 .ok_or_else(|| anyhow!("weight not found in arc cache: {key}"))?;
499 Ok((data.as_ref().clone(), shape.clone()))
500 }
501 fn take_transposed(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
502 let (data, shape) = self.take(key)?;
503 if shape.len() != 2 {
504 anyhow::bail!("transpose requires 2D, got {shape:?}");
505 }
506 let (rows, cols) = (shape[0], shape[1]);
507 let mut transposed = vec![0f32; data.len()];
508 for i in 0..rows {
509 for j in 0..cols {
510 transposed[j * rows + i] = data[i * cols + j];
511 }
512 }
513 Ok((transposed, vec![cols, rows]))
514 }
515 fn remaining_keys(&self) -> Vec<String> {
516 self.cache.keys().cloned().collect()
517 }
518}
519
520pub struct HfTranslatingLoader<L: WeightLoader> {
533 inner: L,
534}
535
536impl<L: WeightLoader> HfTranslatingLoader<L> {
537 pub fn new(inner: L) -> Self {
538 Self { inner }
539 }
540 pub fn into_inner(self) -> L {
541 self.inner
542 }
543 pub fn inner(&self) -> &L {
544 &self.inner
545 }
546 pub fn inner_mut(&mut self) -> &mut L {
547 &mut self.inner
548 }
549}
550
551impl<L: WeightLoader> WeightLoader for HfTranslatingLoader<L> {
552 fn format_id(&self) -> &'static str {
553 self.inner.format_id()
554 }
555 fn len(&self) -> usize {
556 self.inner.len()
557 }
558 fn take(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
559 match self.inner.take(key) {
560 Ok(v) => Ok(v),
561 Err(_) => {
562 if let Some(hf) = gguf_to_hf_name(key) {
563 return self.inner.take(&hf);
564 }
565 self.inner.take(key)
566 }
567 }
568 }
569 fn take_transposed(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
570 match self.inner.take_transposed(key) {
571 Ok(v) => Ok(v),
572 Err(_) => {
573 if let Some(hf) = gguf_to_hf_name(key) {
574 return self.inner.take_transposed(&hf);
575 }
576 self.inner.take_transposed(key)
577 }
578 }
579 }
580 fn take_packed(&mut self, key: &str) -> Result<Option<crate::weight_map::PackedWeightTensor>> {
581 self.inner.take_packed(key)
582 }
583 fn tensor_bytes_borrowed(&self, key: &str) -> Option<&[u8]> {
584 self.inner.tensor_bytes_borrowed(key)
585 }
586 fn remaining_keys(&self) -> Vec<String> {
587 self.inner.remaining_keys()
588 }
589}
590
591pub fn load_from_path(path: &str) -> Result<Box<dyn WeightLoader>> {
593 crate::weight_registry::open_weight_loader(Path::new(path))
594}
595
596pub struct GgufLoader {
604 file: rlx_gguf::GgufFile,
605 arch: String,
606 taken: HashSet<String>,
607 include_mtp: bool,
614 mtp_layer_threshold: Option<u32>,
619}
620
621impl GgufLoader {
622 pub fn from_file(path: &str) -> Result<Self> {
623 let file = crate::gguf_support::load_gguf_file(std::path::Path::new(path))?;
624 let arch = gguf_architecture_str(&file)
625 .unwrap_or("unknown")
626 .to_string();
627 let mtp_layer_threshold = compute_mtp_layer_threshold(&file);
628 Ok(Self {
629 file,
630 arch,
631 taken: HashSet::new(),
632 include_mtp: false,
633 mtp_layer_threshold,
634 })
635 }
636
637 pub fn architecture(&self) -> &str {
638 &self.arch
639 }
640
641 pub fn mtp_layer_threshold(&self) -> Option<u32> {
648 self.mtp_layer_threshold
649 }
650
651 pub fn file(&self) -> &rlx_gguf::GgufFile {
655 &self.file
656 }
657
658 pub fn tensor_bytes_borrowed(&self, key: &str) -> Option<&[u8]> {
665 let real = self.resolve(key).ok()?;
666 let t = self.file.get(&real)?;
667 self.file.tensor_bytes(t).ok()
668 }
669
670 pub fn take_packed_metadata(
678 &mut self,
679 key: &str,
680 ) -> Result<Option<(rlx_ir::quant::QuantScheme, Vec<usize>)>> {
681 let real = self.resolve(key)?;
682 if self.taken.contains(&real) {
683 return Err(anyhow!("weight already taken: {key} (→ {real})"));
684 }
685 if !self.include_mtp && self.is_mtp_tensor(&real) {
686 return Err(anyhow!(
687 "refusing to take MTP weight `{real}` without include_mtp(true)"
688 ));
689 }
690 let t = self
691 .file
692 .get(&real)
693 .ok_or_else(|| anyhow!("tensor missing: {real}"))?;
694 let Some(scheme) = ggml_type_to_quant_scheme(t.dtype) else {
695 return Ok(None);
696 };
697 if !dequant_matmul_supported(scheme) {
698 return Ok(None);
699 }
700 let mut shape = t.shape.clone();
701 shape.reverse();
702 self.taken.insert(real);
703 Ok(Some((scheme, shape)))
704 }
705
706 pub fn is_mtp_tensor(&self, name: &str) -> bool {
710 if is_mtp_weight(name) {
711 return true;
712 }
713 if let Some(thresh) = self.mtp_layer_threshold {
714 if let Some(rest) = name.strip_prefix("blk.") {
715 if let Some(dot) = rest.find('.') {
716 if let Ok(idx) = rest[..dot].parse::<u32>() {
717 if idx >= thresh {
718 return true;
719 }
720 }
721 }
722 }
723 }
724 false
725 }
726
727 pub fn include_mtp(&mut self, include: bool) -> &mut Self {
735 self.include_mtp = include;
736 self
737 }
738
739 pub fn take_packed(&mut self, key: &str) -> Result<Option<PackedWeightTensor>> {
753 let real = self.resolve(key)?;
754 if self.taken.contains(&real) {
755 return Err(anyhow!("weight already taken: {key} (→ {real})"));
756 }
757 if !self.include_mtp && self.is_mtp_tensor(&real) {
758 return Err(anyhow!(
759 "refusing to take MTP weight `{real}` without include_mtp(true)"
760 ));
761 }
762 let t = self
763 .file
764 .get(&real)
765 .ok_or_else(|| anyhow!("tensor missing: {real}"))?;
766 let Some(scheme) = ggml_type_to_quant_scheme(t.dtype) else {
771 return Ok(None);
772 };
773 if !dequant_matmul_supported(scheme) {
774 return Ok(None);
775 };
776 let bytes = self
777 .file
778 .tensor_bytes(t)
779 .with_context(|| format!("read packed bytes for {real}"))?
780 .to_vec();
781 let mut shape = t.shape.clone();
782 shape.reverse();
786 self.taken.insert(real);
787 Ok(Some((bytes, scheme, shape)))
788 }
789
790 pub fn take_mtp(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
795 if !self.is_mtp_tensor(key) {
796 return Err(anyhow!("not an MTP weight under this file's scheme: {key}"));
797 }
798 if !self.file.tensors.contains_key(key) {
799 return Err(anyhow!("MTP weight not found in GGUF: {key}"));
800 }
801 if self.taken.contains(key) {
802 return Err(anyhow!("MTP weight already taken: {key}"));
803 }
804 let (data, raw_shape) = self.file.dequant_f32(key)?;
805 self.taken.insert(key.to_string());
806 let mut shape = raw_shape;
807 shape.reverse();
808 Ok((data, shape))
809 }
810}
811
812impl GgufLoader {
813 fn resolve(&self, key: &str) -> Result<String> {
816 resolve_gguf_tensor_name(&self.file, &self.arch, key)
817 .ok_or_else(|| anyhow!("weight not found in GGUF (arch={}): {key}", self.arch))
818 }
819}
820
821impl WeightLoader for GgufLoader {
822 fn format_id(&self) -> &'static str {
823 "gguf"
824 }
825 fn arch_hint(&self) -> Option<&str> {
826 Some(&self.arch)
827 }
828 fn take_packed(&mut self, key: &str) -> Result<Option<crate::weight_map::PackedWeightTensor>> {
829 self.take_packed(key)
830 }
831 fn tensor_bytes_borrowed(&self, key: &str) -> Option<&[u8]> {
832 GgufLoader::tensor_bytes_borrowed(self, key)
833 }
834 fn len(&self) -> usize {
835 self.file
836 .tensors
837 .keys()
838 .filter(|k| !self.taken.contains(*k) && (self.include_mtp || !self.is_mtp_tensor(k)))
839 .count()
840 }
841 fn take(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
842 let real = self.resolve(key)?;
843 if self.taken.contains(&real) {
844 return Err(anyhow!("weight already taken: {key} (→ {real})"));
845 }
846 if !self.include_mtp && self.is_mtp_tensor(&real) {
847 return Err(anyhow!(
848 "refusing to take MTP weight `{real}` without include_mtp(true); \
849 use loader.take_mtp(...) for explicit MTP grabs or \
850 loader.include_mtp(true) to include them in drains"
851 ));
852 }
853 let (mut data, raw_shape) = self.file.dequant_f32(&real)?;
854 self.taken.insert(real.clone());
855 if matches!(
864 self.arch.as_str(),
865 "gemma" | "gemma2" | "gemma3" | "gemma3n" | "gemma4"
866 ) && is_gemma_norm_weight(&real)
867 {
868 for v in data.iter_mut() {
869 *v -= 1.0;
870 }
871 }
872 let mut shape = raw_shape;
878 shape.reverse();
879 Ok((data, shape))
880 }
881 fn take_transposed(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
891 let (data, shape) = self.take(key)?;
894 if shape.len() == 1 {
898 return Ok((data, shape));
899 }
900 if shape.len() != 2 {
901 return Err(anyhow!("transpose requires 2D, got {shape:?}"));
902 }
903 let (rows, cols) = (shape[0], shape[1]);
904 let mut t = vec![0f32; data.len()];
905 for i in 0..rows {
906 for j in 0..cols {
907 t[j * rows + i] = data[i * cols + j];
908 }
909 }
910 Ok((t, vec![cols, rows]))
911 }
912 fn remaining_keys(&self) -> Vec<String> {
913 self.file
918 .tensors
919 .keys()
920 .filter(|k| {
921 !self.taken.contains(k.as_str()) && (self.include_mtp || !self.is_mtp_tensor(k))
922 })
923 .cloned()
924 .collect()
925 }
926}
927
928impl GgufLoader {
929 pub fn mtp_keys(&self) -> Vec<String> {
936 self.file
937 .tensors
938 .keys()
939 .filter(|k| self.is_mtp_tensor(k))
940 .cloned()
941 .collect()
942 }
943}
944
945#[cfg(test)]
946mod tests {
947 use super::*;
948
949 #[test]
950 fn unknown_extension_errors() {
951 let r = load_from_path("/tmp/no-such-thing.bin");
952 match r {
953 Err(e) => assert!(e.to_string().contains("unsupported")),
954 Ok(_) => panic!("expected error"),
955 }
956 }
957
958 #[test]
959 fn gemma_qk_norm_names_get_gguf_gamma_unbake() {
960 assert!(is_gemma_norm_weight("blk.0.attn_q_norm.weight"));
961 assert!(is_gemma_norm_weight("blk.7.attn_k_norm.weight"));
962 assert!(is_gemma_norm_weight(
963 "model.layers.0.self_attn.q_norm.weight"
964 ));
965 assert!(is_gemma_norm_weight(
966 "model.layers.3.self_attn.k_norm.weight"
967 ));
968 assert!(!is_gemma_norm_weight("blk.0.attn_q.weight"));
969 }
970
971 #[test]
972 fn hf_to_gguf_top_level() {
973 assert_eq!(
974 hf_to_gguf_name("model.embed_tokens.weight").as_deref(),
975 Some("token_embd.weight")
976 );
977 assert_eq!(
978 hf_to_gguf_name("model.norm.weight").as_deref(),
979 Some("output_norm.weight")
980 );
981 assert_eq!(
982 hf_to_gguf_name("lm_head.weight").as_deref(),
983 Some("output.weight")
984 );
985 }
986
987 #[test]
988 fn hf_to_gguf_per_layer() {
989 let cases = [
990 (
991 "model.layers.0.self_attn.q_proj.weight",
992 "blk.0.attn_q.weight",
993 ),
994 (
995 "model.layers.7.self_attn.o_proj.weight",
996 "blk.7.attn_output.weight",
997 ),
998 (
999 "model.layers.3.mlp.gate_proj.weight",
1000 "blk.3.ffn_gate.weight",
1001 ),
1002 (
1003 "model.layers.12.mlp.down_proj.weight",
1004 "blk.12.ffn_down.weight",
1005 ),
1006 (
1007 "model.layers.4.input_layernorm.weight",
1008 "blk.4.attn_norm.weight",
1009 ),
1010 (
1011 "model.layers.4.post_attention_layernorm.weight",
1012 "blk.4.ffn_norm.weight",
1013 ),
1014 (
1015 "model.layers.0.self_attn.q_norm.weight",
1016 "blk.0.attn_q_norm.weight",
1017 ),
1018 ];
1019 for (hf, gguf) in cases {
1020 assert_eq!(
1021 hf_to_gguf_name(hf).as_deref(),
1022 Some(gguf),
1023 "mismatch for {hf}"
1024 );
1025 }
1026 }
1027
1028 #[test]
1029 fn hf_to_gguf_gemma4_output_scale() {
1030 assert_eq!(
1031 hf_to_gguf_name_for_arch("model.layers.5.self_attn.output_scale.weight", "gemma4")
1032 .as_deref(),
1033 Some("blk.5.layer_output_scale.weight")
1034 );
1035 assert_eq!(
1036 gguf_to_hf_name_for_arch("blk.5.layer_output_scale.weight", "gemma4").as_deref(),
1037 Some("model.layers.5.self_attn.output_scale.weight")
1038 );
1039 }
1040
1041 #[test]
1042 fn hf_to_gguf_unknown_returns_none() {
1043 assert!(hf_to_gguf_name("model.layers.0.some_new_thing.weight").is_none());
1044 assert!(hf_to_gguf_name("model.layers.foo.input_layernorm.weight").is_none());
1045 }
1046
1047 #[test]
1048 fn mtp_detection() {
1049 assert!(is_mtp_weight("mtp_blk.0.attn_q.weight"));
1050 assert!(is_mtp_weight("output_mtp_0.weight"));
1051 assert!(is_mtp_weight("model.layers.0.mtp_head.weight"));
1052 assert!(!is_mtp_weight("blk.0.attn_q.weight"));
1053 assert!(!is_mtp_weight("output.weight"));
1054 }
1055
1056 #[test]
1063 fn ggml_block_quants_map_to_packed_scheme() {
1064 use rlx_gguf::GgmlType;
1065 assert_eq!(
1066 ggml_type_to_quant_scheme(GgmlType::Q4_0),
1067 Some(rlx_ir::quant::QuantScheme::GgufQ4_0)
1068 );
1069 assert_eq!(
1070 ggml_type_to_quant_scheme(GgmlType::Q5_0),
1071 Some(rlx_ir::quant::QuantScheme::GgufQ5_0)
1072 );
1073 assert_eq!(
1074 ggml_type_to_quant_scheme(GgmlType::Q8_0),
1075 Some(rlx_ir::quant::QuantScheme::GgufQ8_0)
1076 );
1077 }
1078
1079 #[test]
1080 fn q6k_dequant_matmul_follows_block_probe() {
1081 use rlx_ir::quant::QuantScheme;
1082 assert!(dequant_matmul_supported(QuantScheme::GgufQ4K));
1083 assert_eq!(
1084 dequant_matmul_supported(QuantScheme::GgufQ6K),
1085 super::probe_q6k_block_dequant()
1086 );
1087 }
1088
1089 #[test]
1090 fn gguf_loader_threshold_based_mtp_detection() {
1091 let mut buf: Vec<u8> = Vec::new();
1092 buf.extend_from_slice(&rlx_gguf::GGUF_MAGIC.to_le_bytes());
1093 buf.extend_from_slice(&3u32.to_le_bytes());
1094 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| {
1098 buf.extend_from_slice(&(k.len() as u64).to_le_bytes());
1099 buf.extend_from_slice(k.as_bytes());
1100 buf.extend_from_slice(&8u32.to_le_bytes());
1101 buf.extend_from_slice(&(v.len() as u64).to_le_bytes());
1102 buf.extend_from_slice(v.as_bytes());
1103 };
1104 let write_u32 = |buf: &mut Vec<u8>, k: &str, v: u32| {
1105 buf.extend_from_slice(&(k.len() as u64).to_le_bytes());
1106 buf.extend_from_slice(k.as_bytes());
1107 buf.extend_from_slice(&4u32.to_le_bytes()); buf.extend_from_slice(&v.to_le_bytes());
1109 };
1110 write_string(&mut buf, "general.architecture", "qwen35");
1111 write_u32(&mut buf, "qwen35.block_count", 25);
1112 write_u32(&mut buf, "qwen35.nextn_predict_layers", 1);
1113 let write_tensor = |buf: &mut Vec<u8>, name: &str, shape: &[usize], off: u64| {
1116 buf.extend_from_slice(&(name.len() as u64).to_le_bytes());
1117 buf.extend_from_slice(name.as_bytes());
1118 buf.extend_from_slice(&(shape.len() as u32).to_le_bytes());
1119 for &d in shape {
1120 buf.extend_from_slice(&(d as u64).to_le_bytes());
1121 }
1122 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&off.to_le_bytes());
1124 };
1125 write_tensor(&mut buf, "blk.0.attn_q.weight", &[4, 4], 0);
1126 write_tensor(&mut buf, "blk.24.attn_q.weight", &[4, 4], 64);
1127 write_tensor(&mut buf, "token_embd.weight", &[4, 4], 128);
1128 while !buf
1129 .len()
1130 .is_multiple_of(rlx_gguf::DEFAULT_ALIGNMENT as usize)
1131 {
1132 buf.push(0);
1133 }
1134 for _ in 0..(3 * 16) {
1136 buf.extend_from_slice(&0.5f32.to_le_bytes());
1137 }
1138 let path = std::env::temp_dir().join("rlx_mtp_threshold_test.gguf");
1139 std::fs::write(&path, &buf).unwrap();
1140 let loader = GgufLoader::from_file(path.to_str().unwrap()).unwrap();
1141
1142 assert_eq!(loader.mtp_layer_threshold(), Some(24));
1143 assert!(!loader.is_mtp_tensor("blk.0.attn_q.weight"));
1144 assert!(loader.is_mtp_tensor("blk.24.attn_q.weight"));
1145 assert!(!loader.is_mtp_tensor("token_embd.weight"));
1146 let mtp = loader.mtp_keys();
1147 assert_eq!(mtp, vec!["blk.24.attn_q.weight".to_string()]);
1148
1149 std::fs::remove_file(&path).ok();
1150 }
1151
1152 #[test]
1159 fn gguf_loader_resolves_hf_names_and_skips_mtp() {
1160 let mut tensors = Vec::new();
1161 let mut data: Vec<f32> = Vec::new();
1162
1163 let t1: Vec<f32> = (0..12).map(|x| x as f32).collect();
1165 tensors.push(("token_embd.weight", vec![3usize, 4], data.len()));
1166 data.extend_from_slice(&t1);
1167
1168 let t2: Vec<f32> = (100..116).map(|x| x as f32).collect();
1170 tensors.push(("blk.0.attn_q.weight", vec![4usize, 4], data.len()));
1171 data.extend_from_slice(&t2);
1172
1173 let t3: Vec<f32> = vec![0.5f32; 8];
1175 tensors.push(("output_mtp_0.weight", vec![2usize, 4], data.len()));
1176 data.extend_from_slice(&t3);
1177
1178 let mut buf: Vec<u8> = Vec::new();
1180 buf.extend_from_slice(&rlx_gguf::GGUF_MAGIC.to_le_bytes());
1181 buf.extend_from_slice(&3u32.to_le_bytes()); buf.extend_from_slice(&(tensors.len() as u64).to_le_bytes());
1183 buf.extend_from_slice(&0u64.to_le_bytes()); for (name, shape, _) in &tensors {
1187 buf.extend_from_slice(&(name.len() as u64).to_le_bytes());
1188 buf.extend_from_slice(name.as_bytes());
1189 buf.extend_from_slice(&(shape.len() as u32).to_le_bytes());
1190 for &d in shape {
1191 buf.extend_from_slice(&(d as u64).to_le_bytes());
1192 }
1193 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u64.to_le_bytes());
1196 }
1197 while !buf
1199 .len()
1200 .is_multiple_of(rlx_gguf::DEFAULT_ALIGNMENT as usize)
1201 {
1202 buf.push(0);
1203 }
1204 let data_start = buf.len();
1205 for v in &data {
1206 buf.extend_from_slice(&v.to_le_bytes());
1207 }
1208 let header = (4 + 4 + 8 + 8) as usize; let mut cursor = header;
1211 for (name, shape, byte_off) in &tensors {
1212 let name_len_bytes = 8;
1213 let name_bytes = name.len();
1214 let n_dims_bytes = 4;
1215 let dims_bytes = shape.len() * 8;
1216 let dtype_bytes = 4;
1217 let off_bytes = 8;
1218 let info_size =
1219 name_len_bytes + name_bytes + n_dims_bytes + dims_bytes + dtype_bytes + off_bytes;
1220 let off_field_at = cursor + info_size - off_bytes;
1221 let final_off = (*byte_off * 4) as u64; for i in 0..8 {
1223 buf[off_field_at + i] = (final_off >> (i * 8)) as u8;
1224 }
1225 cursor += info_size;
1226 }
1227 let _ = data_start;
1228
1229 let path = std::env::temp_dir().join("rlx_test_qwen3_mini.gguf");
1231 std::fs::write(&path, &buf).unwrap();
1232
1233 let mut loader = GgufLoader::from_file(path.to_str().unwrap()).unwrap();
1234 assert_eq!(loader.len(), 2);
1236
1237 let (out, shape) = loader
1243 .take("model.embed_tokens.weight")
1244 .expect("hf-named token_embd should resolve");
1245 assert_eq!(shape, vec![4, 3]);
1246 assert_eq!(&out, &t1);
1247
1248 let (out, shape) = loader
1249 .take("model.layers.0.self_attn.q_proj.weight")
1250 .expect("hf-named attn_q should resolve");
1251 assert_eq!(shape, vec![4, 4]);
1252 assert_eq!(&out, &t2);
1253
1254 assert_eq!(loader.remaining_keys(), Vec::<String>::new());
1256 assert_eq!(loader.mtp_keys(), vec!["output_mtp_0.weight".to_string()]);
1257
1258 let mut loader2 = GgufLoader::from_file(path.to_str().unwrap()).unwrap();
1262 loader2.include_mtp(true);
1263 let visible: std::collections::HashSet<String> =
1264 loader2.remaining_keys().into_iter().collect();
1265 assert!(visible.contains("token_embd.weight"));
1266 assert!(visible.contains("blk.0.attn_q.weight"));
1267 assert!(
1268 visible.contains("output_mtp_0.weight"),
1269 "MTP weight should be visible with include_mtp(true)"
1270 );
1271 let (mtp_data, mtp_shape) = loader2.take_mtp("output_mtp_0.weight").unwrap();
1272 assert_eq!(mtp_shape, vec![4, 2]);
1273 assert_eq!(mtp_data, t3);
1274
1275 let mut loader3 = GgufLoader::from_file(path.to_str().unwrap()).unwrap();
1277 let err = loader3.take("output_mtp_0.weight").unwrap_err();
1278 let msg = format!("{err:#}");
1279 assert!(
1280 msg.contains("include_mtp(true)"),
1281 "expected MTP guard error, got: {msg}"
1282 );
1283
1284 std::fs::remove_file(&path).ok();
1285 }
1286
1287 #[test]
1288 fn missing_gguf_file_errors() {
1289 let r = load_from_path("/tmp/no-such-thing-rlx-gguf-test.gguf");
1292 assert!(r.is_err());
1293 }
1294}