1use crate::Device;
62use crate::lazy::Tensor;
63use fuel_ir::{Result, Shape};
64
65pub struct Nf4Weight {
76 pub w_packed: Tensor,
78 pub absmax: Tensor,
80 pub n: usize,
82 pub k: usize,
84 pub block_size: usize,
86}
87
88pub fn nf4_from_bytes(
106 w_packed: impl Into<Vec<u8>>,
107 absmax: impl Into<Vec<f32>>,
108 n: usize,
109 k: usize,
110 block_size: usize,
111 device: &Device,
112) -> Result<Nf4Weight> {
113 let w_packed: Vec<u8> = w_packed.into();
114 let absmax: Vec<f32> = absmax.into();
115 if k == 0 || !k.is_multiple_of(2) {
116 return Err(fuel_ir::Error::Msg(format!(
117 "nf4_from_bytes: k={k} must be even and non-zero",
118 ))
119 .bt());
120 }
121 if block_size == 0 || !k.is_multiple_of(block_size) {
122 return Err(fuel_ir::Error::Msg(format!(
123 "nf4_from_bytes: k={k} must be a positive multiple of block_size={block_size}",
124 ))
125 .bt());
126 }
127 let w_expected = n.saturating_mul(k / 2);
128 if w_packed.len() != w_expected {
129 return Err(fuel_ir::Error::Msg(format!(
130 "nf4_from_bytes: w_packed has {} bytes, expected {w_expected} (n={n} × k/2={})",
131 w_packed.len(),
132 k / 2,
133 ))
134 .bt());
135 }
136 let abs_expected = n.saturating_mul(k / block_size);
137 if absmax.len() != abs_expected {
138 return Err(fuel_ir::Error::Msg(format!(
139 "nf4_from_bytes: absmax has {} f32 elements, expected {abs_expected} \
140 (n={n} × k/block_size={})",
141 absmax.len(),
142 k / block_size,
143 ))
144 .bt());
145 }
146 let anchor = Tensor::from_f32(vec![0.0_f32], Shape::from_dims(&[1]), device)?;
151 let w_packed_tensor = anchor
152 .graph_tensor()
153 .const_u8_like(w_packed, Shape::from_dims(&[n, k / 2]))?;
154 let absmax_tensor = anchor
155 .graph_tensor()
156 .const_f32_like(absmax, Shape::from_dims(&[n, k / block_size]))?;
157 Ok(Nf4Weight {
158 w_packed: Tensor::from_graph_tensor(w_packed_tensor),
159 absmax: Tensor::from_graph_tensor(absmax_tensor),
160 n,
161 k,
162 block_size,
163 })
164}
165
166impl Nf4Weight {
167 pub fn matmul(&self, activations: &Tensor) -> Tensor {
178 activations.nf4_matmul(&self.w_packed, &self.absmax, self.block_size)
179 }
180}
181
182#[derive(Debug, Clone, PartialEq)]
196pub struct BnbQuantState {
197 pub n: usize,
199 pub k: usize,
201 pub block_size: usize,
203 pub nested: Option<BnbNestedState>,
206}
207
208#[derive(Debug, Clone, PartialEq)]
212pub struct BnbNestedState {
213 pub block_size: usize,
216 pub offset: f32,
218}
219
220pub fn parse_bnb_quant_state(json_bytes: &[u8]) -> fuel_ir::Result<BnbQuantState> {
246 let v: serde_json::Value = serde_json::from_slice(json_bytes).map_err(|e| {
247 fuel_ir::Error::Msg(format!("parse_bnb_quant_state: failed to parse JSON: {e}",)).bt()
248 })?;
249 let obj = v.as_object().ok_or_else(|| {
250 fuel_ir::Error::Msg(
251 "parse_bnb_quant_state: expected a JSON object at the top level".to_string(),
252 )
253 .bt()
254 })?;
255 let quant_type = obj
258 .get("quant_type")
259 .and_then(|v| v.as_str())
260 .ok_or_else(|| {
261 fuel_ir::Error::Msg(
262 "parse_bnb_quant_state: missing or non-string 'quant_type' field".to_string(),
263 )
264 .bt()
265 })?;
266 if quant_type != "nf4" {
267 return Err(fuel_ir::Error::Msg(format!(
268 "parse_bnb_quant_state: only quant_type=\"nf4\" is supported, \
269 got {quant_type:?}. Other bnb formats (fp4, etc.) need their \
270 own loader path.",
271 ))
272 .bt());
273 }
274 if obj.contains_key("nested_absmax") || obj.contains_key("nested_quant_map") {
277 return Err(fuel_ir::Error::Msg(
278 "parse_bnb_quant_state: nested_absmax / nested_quant_map are tensors in \
279 bitsandbytes checkpoints, not JSON fields; this blob is not in the \
280 packed quant_state format"
281 .to_string(),
282 )
283 .bt());
284 }
285 let nested = parse_nested(obj)?;
286 let shape = obj.get("shape").and_then(|v| v.as_array()).ok_or_else(|| {
288 fuel_ir::Error::Msg("parse_bnb_quant_state: missing or non-array 'shape' field".to_string())
289 .bt()
290 })?;
291 if shape.len() != 2 {
292 return Err(fuel_ir::Error::Msg(format!(
293 "parse_bnb_quant_state: 'shape' must be 2-element [N, K], got len={}",
294 shape.len(),
295 ))
296 .bt());
297 }
298 let n = shape[0].as_u64().ok_or_else(|| {
299 fuel_ir::Error::Msg(
300 "parse_bnb_quant_state: shape[0] (N) must be a non-negative integer".to_string(),
301 )
302 .bt()
303 })? as usize;
304 let k = shape[1].as_u64().ok_or_else(|| {
305 fuel_ir::Error::Msg(
306 "parse_bnb_quant_state: shape[1] (K) must be a non-negative integer".to_string(),
307 )
308 .bt()
309 })? as usize;
310 let block_size = obj
312 .get("blocksize")
313 .and_then(|v| v.as_u64())
314 .ok_or_else(|| {
315 fuel_ir::Error::Msg(
316 "parse_bnb_quant_state: missing or non-integer 'blocksize' field".to_string(),
317 )
318 .bt()
319 })? as usize;
320 if block_size == 0 {
321 return Err(fuel_ir::Error::Msg(
322 "parse_bnb_quant_state: blocksize must be positive".to_string(),
323 )
324 .bt());
325 }
326 if !k.is_multiple_of(block_size) {
327 return Err(fuel_ir::Error::Msg(format!(
328 "parse_bnb_quant_state: blocksize={block_size} doesn't divide K={k} \
329 (this would leave a partial block at the end of each row, which \
330 bnb's standard format doesn't produce — your checkpoint may be \
331 corrupted or use a non-standard variant)",
332 ))
333 .bt());
334 }
335 if !k.is_multiple_of(2) {
336 return Err(fuel_ir::Error::Msg(format!(
337 "parse_bnb_quant_state: K={k} must be even (NF4 packs 2 codes per byte)",
338 ))
339 .bt());
340 }
341 Ok(BnbQuantState {
342 n,
343 k,
344 block_size,
345 nested,
346 })
347}
348
349fn parse_nested(
352 obj: &serde_json::Map<String, serde_json::Value>,
353) -> fuel_ir::Result<Option<BnbNestedState>> {
354 match (
355 obj.get("nested_blocksize"),
356 obj.get("nested_offset"),
357 obj.get("nested_dtype"),
358 ) {
359 (None, None, None) => Ok(None),
360 (Some(bs), Some(off), Some(dt)) => nested_from_fields(bs, off, dt).map(Some),
361 _ => Err(nested_err(
362 "double quantization needs all of nested_blocksize, nested_offset and nested_dtype",
363 )),
364 }
365}
366
367fn nested_err(m: &str) -> fuel_ir::Error {
368 fuel_ir::Error::Msg(format!("parse_bnb_quant_state: {m}")).bt()
369}
370
371fn nested_from_fields(
373 bs: &serde_json::Value,
374 off: &serde_json::Value,
375 dt: &serde_json::Value,
376) -> fuel_ir::Result<BnbNestedState> {
377 if dt.as_str() != Some("float32") {
378 return Err(nested_err(&format!(
379 "only nested_dtype \"float32\" is supported, got {dt}"
380 )));
381 }
382 let block_size = bs
383 .as_u64()
384 .filter(|b| *b > 0)
385 .ok_or_else(|| nested_err("nested_blocksize must be a positive integer"))?
386 as usize;
387 let offset = off
388 .as_f64()
389 .ok_or_else(|| nested_err("nested_offset must be a number"))? as f32;
390 Ok(BnbNestedState { block_size, offset })
391}
392
393pub fn dequantize_nested_absmax(
401 q: &[u8],
402 nested_absmax: &[f32],
403 nested_quant_map: &[f32],
404 nested_block_size: usize,
405 offset: f32,
406) -> fuel_ir::Result<Vec<f32>> {
407 let err = |m: String| fuel_ir::Error::Msg(format!("dequantize_nested_absmax: {m}")).bt();
408 if nested_quant_map.len() != 256 {
409 return Err(err(format!(
410 "nested_quant_map has {} entries, expected 256",
411 nested_quant_map.len()
412 )));
413 }
414 if nested_block_size == 0 {
415 return Err(err("nested block size must be positive".to_string()));
416 }
417 let want = q.len().div_ceil(nested_block_size);
418 if nested_absmax.len() != want {
419 return Err(err(format!(
420 "nested_absmax has {} scales, expected {want} ({} codes / block {nested_block_size})",
421 nested_absmax.len(),
422 q.len()
423 )));
424 }
425 Ok(q.iter()
426 .enumerate()
427 .map(|(i, &code)| {
428 nested_quant_map[code as usize] * nested_absmax[i / nested_block_size] + offset
429 })
430 .collect())
431}
432
433fn f32_from_le_bytes(bytes: &[u8], key: &str) -> fuel_ir::Result<Vec<f32>> {
435 if !bytes.len().is_multiple_of(4) {
436 return Err(fuel_ir::Error::Msg(format!(
437 "load_nf4_layer: {key:?} has {} bytes (not a multiple of 4 — corrupt F32 data)",
438 bytes.len(),
439 ))
440 .bt());
441 }
442 Ok(bytes
443 .as_chunks::<4>()
444 .0
445 .iter()
446 .map(|c| f32::from_le_bytes(*c))
447 .collect())
448}
449
450pub fn load_nf4_layer(
485 st: &safetensors::SafeTensors,
486 prefix: &str,
487 device: &Device,
488) -> fuel_ir::Result<Nf4Weight> {
489 let weight_key = format!("{prefix}.weight");
490 let absmax_key = format!("{prefix}.weight.absmax");
491 let quant_state_key = format!("{prefix}.weight.quant_state.bitsandbytes__nf4");
492
493 let weight_view = st.tensor(&weight_key).map_err(|e| {
494 fuel_ir::Error::Msg(format!(
495 "load_nf4_layer: tensor {weight_key:?} not found in safetensors: {e}",
496 ))
497 .bt()
498 })?;
499 let absmax_view = st.tensor(&absmax_key).map_err(|e| {
500 fuel_ir::Error::Msg(format!(
501 "load_nf4_layer: tensor {absmax_key:?} not found in safetensors: {e}",
502 ))
503 .bt()
504 })?;
505 let quant_state_view = st.tensor(&quant_state_key).map_err(|e| {
506 fuel_ir::Error::Msg(format!(
507 "load_nf4_layer: tensor {quant_state_key:?} not found in safetensors: {e}",
508 ))
509 .bt()
510 })?;
511
512 let qs = parse_bnb_quant_state(quant_state_view.data())?;
516
517 if weight_view.dtype() != safetensors::Dtype::U8 {
519 return Err(fuel_ir::Error::Msg(format!(
520 "load_nf4_layer: {weight_key:?} dtype={:?}, expected U8",
521 weight_view.dtype(),
522 ))
523 .bt());
524 }
525 let w_bytes: Vec<u8> = weight_view
527 .data()
528 .iter()
529 .map(|b| b.rotate_left(4))
530 .collect();
531
532 let absmax_f32: Vec<f32> = match &qs.nested {
535 None => {
536 if absmax_view.dtype() != safetensors::Dtype::F32 {
537 return Err(fuel_ir::Error::Msg(format!(
538 "load_nf4_layer: {absmax_key:?} dtype={:?}, expected F32 (the quant_state \
539 blob has no nested_* fields, so the absmax is not double-quantized)",
540 absmax_view.dtype(),
541 ))
542 .bt());
543 }
544 f32_from_le_bytes(absmax_view.data(), &absmax_key)?
545 }
546 Some(nested) => {
547 if absmax_view.dtype() != safetensors::Dtype::U8 {
548 return Err(fuel_ir::Error::Msg(format!(
549 "load_nf4_layer: {absmax_key:?} dtype={:?}, expected U8 (the quant_state \
550 blob is double-quantized)",
551 absmax_view.dtype(),
552 ))
553 .bt());
554 }
555 let read_f32 = |suffix: &str| -> fuel_ir::Result<Vec<f32>> {
556 let key = format!("{prefix}.weight.{suffix}");
557 let view = st.tensor(&key).map_err(|e| {
558 fuel_ir::Error::Msg(format!(
559 "load_nf4_layer: tensor {key:?} not found in safetensors: {e}",
560 ))
561 .bt()
562 })?;
563 if view.dtype() != safetensors::Dtype::F32 {
564 return Err(fuel_ir::Error::Msg(format!(
565 "load_nf4_layer: {key:?} dtype={:?}, expected F32",
566 view.dtype(),
567 ))
568 .bt());
569 }
570 f32_from_le_bytes(view.data(), &key)
571 };
572 let nested_absmax = read_f32("nested_absmax")?;
573 let nested_quant_map = read_f32("nested_quant_map")?;
574 dequantize_nested_absmax(
575 absmax_view.data(),
576 &nested_absmax,
577 &nested_quant_map,
578 nested.block_size,
579 nested.offset,
580 )?
581 }
582 };
583
584 nf4_from_bytes(w_bytes, absmax_f32, qs.n, qs.k, qs.block_size, device)
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593
594 #[test]
598 fn nf4_weight_matmul_round_trip() {
599 let device = Device::cpu();
600 let weight = nf4_from_bytes(
607 vec![247_u8, 247, 127, 127],
608 vec![1.0_f32, 2.0, 10.0, 20.0],
609 2,
610 4,
611 2,
612 &device,
613 )
614 .expect("nf4_from_bytes");
615 let activations = Tensor::from_f32(
616 vec![1.0_f32, 2.0, 2.0, 4.0],
617 Shape::from_dims(&[1, 4]),
618 &device,
619 )
620 .unwrap();
621 let activations_t = weight
624 .w_packed
625 .graph_tensor()
626 .const_f32_like(vec![1.0_f32, 2.0, 2.0, 4.0], Shape::from_dims(&[1, 4]))
627 .unwrap();
628 let _ = activations; let act = Tensor::from_graph_tensor(activations_t);
630 let y = weight.matmul(&act).realize_f32();
631 assert_eq!(y.len(), 2);
632 assert!((y[0] - 10.0).abs() < 1e-5, "out 0: {}", y[0]);
633 assert!((y[1] - 50.0).abs() < 1e-5, "out 1: {}", y[1]);
634 }
635
636 #[test]
638 fn nf4_from_bytes_rejects_odd_k() {
639 let r = nf4_from_bytes(
640 vec![0_u8; 6],
641 vec![0.0_f32; 2],
642 2,
643 3,
644 1,
645 &Device::cpu(),
646 );
647 assert!(r.is_err());
648 }
649
650 #[test]
652 fn nf4_from_bytes_rejects_block_size_mismatch() {
653 let r = nf4_from_bytes(
654 vec![0_u8; 4],
655 vec![0.0_f32; 2],
656 2,
657 4,
658 3,
659 &Device::cpu(),
660 );
661 assert!(r.is_err());
662 }
663
664 #[test]
666 fn nf4_from_bytes_rejects_wrong_packed_size() {
667 let r = nf4_from_bytes(
668 vec![0_u8; 5], vec![0.0_f32; 2],
670 2,
671 4,
672 2,
673 &Device::cpu(),
674 );
675 assert!(r.is_err());
676 }
677
678 #[test]
680 fn parse_bnb_quant_state_minimal_nf4() {
681 let json = br#"{"quant_type":"nf4","shape":[256,128],"blocksize":64,"dtype":"float16"}"#;
682 let qs = parse_bnb_quant_state(json).expect("parse_bnb_quant_state");
683 assert_eq!(
684 qs,
685 BnbQuantState {
686 n: 256,
687 k: 128,
688 block_size: 64,
689 nested: None,
690 }
691 );
692 }
693
694 #[test]
696 fn parse_bnb_quant_state_rejects_fp4() {
697 let json = br#"{"quant_type":"fp4","shape":[256,128],"blocksize":64}"#;
698 let r = parse_bnb_quant_state(json);
699 let err = r.unwrap_err().to_string();
700 assert!(err.contains("nf4"), "error should mention nf4: {err}");
701 }
702
703 #[test]
706 fn parse_bnb_quant_state_rejects_nested_absmax() {
707 let json =
708 br#"{"quant_type":"nf4","shape":[256,128],"blocksize":64,"nested_absmax":[1.0,2.0]}"#;
709 let r = parse_bnb_quant_state(json);
710 let err = r.unwrap_err().to_string();
711 assert!(
712 err.contains("nested_absmax"),
713 "error should name the offending field: {err}"
714 );
715 }
716
717 #[test]
719 fn parse_bnb_quant_state_rejects_misaligned_blocksize() {
720 let json = br#"{"quant_type":"nf4","shape":[256,130],"blocksize":64}"#;
721 let r = parse_bnb_quant_state(json);
722 let err = r.unwrap_err().to_string();
723 assert!(
724 err.contains("doesn't divide K") || err.contains("must be even"),
725 "error should flag misalignment: {err}"
726 );
727 }
728
729 #[test]
731 fn parse_bnb_quant_state_rejects_odd_k() {
732 let json = br#"{"quant_type":"nf4","shape":[256,127],"blocksize":1}"#;
733 let r = parse_bnb_quant_state(json);
734 let err = r.unwrap_err().to_string();
735 assert!(
736 err.contains("must be even"),
737 "error should flag odd K: {err}"
738 );
739 }
740
741 #[test]
743 fn parse_bnb_quant_state_rejects_malformed_json() {
744 let r = parse_bnb_quant_state(b"not json at all");
745 assert!(r.is_err());
746 }
747
748 #[test]
750 fn parse_bnb_quant_state_rejects_missing_shape() {
751 let json = br#"{"quant_type":"nf4","blocksize":64}"#;
752 let r = parse_bnb_quant_state(json);
753 let err = r.unwrap_err().to_string();
754 assert!(err.contains("shape"), "error should mention shape: {err}");
755 }
756
757 #[test]
765 fn load_nf4_layer_round_trip_synthetic_safetensors() {
766 use safetensors::tensor::TensorView;
767 use std::collections::HashMap;
768
769 let n: usize = 2;
770 let k: usize = 4;
771 let block_size: usize = 2;
772
773 let weight_bytes = vec![0x7F_u8, 0x7F, 0xF7, 0xF7];
777
778 let absmax_floats: Vec<f32> = vec![1.0, 2.0, 10.0, 20.0];
780 let absmax_bytes: Vec<u8> = absmax_floats.iter().flat_map(|f| f.to_le_bytes()).collect();
781
782 let quant_state_json = format!(
784 r#"{{"quant_type":"nf4","shape":[{n},{k}],"blocksize":{block_size},"dtype":"float16"}}"#,
785 );
786 let quant_state_bytes = quant_state_json.as_bytes().to_vec();
787
788 let weight_view = TensorView::new(safetensors::Dtype::U8, vec![n * k / 2], &weight_bytes)
792 .expect("weight TensorView");
793 let absmax_view = TensorView::new(
794 safetensors::Dtype::F32,
795 vec![n * k / block_size],
796 &absmax_bytes,
797 )
798 .expect("absmax TensorView");
799 let quant_state_view = TensorView::new(
800 safetensors::Dtype::U8,
801 vec![quant_state_bytes.len()],
802 &quant_state_bytes,
803 )
804 .expect("quant_state TensorView");
805
806 let mut tensors: HashMap<String, TensorView<'_>> = HashMap::new();
808 tensors.insert("layer.weight".to_string(), weight_view);
809 tensors.insert("layer.weight.absmax".to_string(), absmax_view);
810 tensors.insert(
811 "layer.weight.quant_state.bitsandbytes__nf4".to_string(),
812 quant_state_view,
813 );
814 let metadata: Option<std::collections::HashMap<String, String>> = None;
815 let serialized =
816 safetensors::serialize(&tensors, metadata.clone()).expect("safetensors::serialize");
817
818 let st =
820 safetensors::SafeTensors::deserialize(&serialized).expect("SafeTensors::deserialize");
821 let weight = load_nf4_layer(&st, "layer", &Device::cpu()).expect("load_nf4_layer");
822 assert_eq!(weight.n, n);
823 assert_eq!(weight.k, k);
824 assert_eq!(weight.block_size, block_size);
825
826 let act = Tensor::from_graph_tensor(
828 weight
829 .w_packed
830 .graph_tensor()
831 .const_f32_like(vec![1.0_f32, 2.0, 2.0, 4.0], Shape::from_dims(&[1, 4]))
832 .unwrap(),
833 );
834 let y = weight.matmul(&act).realize_f32();
835 assert!((y[0] - 10.0).abs() < 1e-5, "out 0: {}", y[0]);
836 assert!((y[1] - 50.0).abs() < 1e-5, "out 1: {}", y[1]);
837 }
838
839 #[test]
842 fn load_nf4_layer_missing_weight_errors() {
843 use safetensors::tensor::TensorView;
844 use std::collections::HashMap;
845 let quant_state_bytes = br#"{"quant_type":"nf4","shape":[2,4],"blocksize":2}"#.to_vec();
847 let absmax_bytes: Vec<u8> = [1.0_f32, 2.0, 10.0, 20.0]
848 .iter()
849 .flat_map(|f| f.to_le_bytes())
850 .collect();
851 let mut tensors: HashMap<String, TensorView<'_>> = HashMap::new();
852 tensors.insert(
853 "layer.weight.absmax".to_string(),
854 TensorView::new(safetensors::Dtype::F32, vec![4], &absmax_bytes).unwrap(),
855 );
856 tensors.insert(
857 "layer.weight.quant_state.bitsandbytes__nf4".to_string(),
858 TensorView::new(
859 safetensors::Dtype::U8,
860 vec![quant_state_bytes.len()],
861 &quant_state_bytes,
862 )
863 .unwrap(),
864 );
865 let metadata: Option<HashMap<String, String>> = None;
866 let serialized = safetensors::serialize(&tensors, metadata.clone()).unwrap();
867 let st = safetensors::SafeTensors::deserialize(&serialized).unwrap();
868 let err = match load_nf4_layer(&st, "layer", &Device::cpu()) {
871 Ok(_) => panic!("expected an error from load_nf4_layer with missing weight tensor"),
872 Err(e) => e.to_string(),
873 };
874 assert!(
875 err.contains("layer.weight"),
876 "error should mention the missing tensor: {err}"
877 );
878 }
879
880 const DQ_FIXTURE: &[u8] = include_bytes!(
889 "../tests/fixtures/bnb_double_quant/bnb_double_quant_k_proj_16rows.safetensors"
890 );
891 const DQ_ORIGINAL_BF16: &[u8] =
893 include_bytes!("../tests/fixtures/bnb_double_quant/orig_k_proj_16rows_bf16.bin");
894 const DQ_PREFIX: &str = "model.layers.1.self_attn.k_proj";
895 const DQ_N: usize = 16;
896 const DQ_K: usize = 1024;
897
898 fn f32s(bytes: &[u8]) -> Vec<f32> {
899 bytes
900 .as_chunks::<4>()
901 .0
902 .iter()
903 .map(|c| f32::from_le_bytes(*c))
904 .collect()
905 }
906
907 fn rel_rms_vs_original(got: impl Fn(usize, usize) -> f32) -> f64 {
909 let (mut num, mut den) = (0.0f64, 0.0f64);
910 for n in 0..DQ_N {
911 for k in 0..DQ_K {
912 let i = n * DQ_K + k;
913 let bits =
914 u16::from_le_bytes([DQ_ORIGINAL_BF16[2 * i], DQ_ORIGINAL_BF16[2 * i + 1]]);
915 let want = f32::from_bits((bits as u32) << 16) as f64;
916 let d = got(n, k) as f64 - want;
917 num += d * d;
918 den += want * want;
919 }
920 }
921 (num / den).sqrt()
922 }
923
924 fn dequant_through_fuel(w: &Nf4Weight) -> Vec<f32> {
929 let mut eye = vec![0.0f32; DQ_K * DQ_K];
930 for i in 0..DQ_K {
931 eye[i * DQ_K + i] = 1.0;
932 }
933 let act = Tensor::from_graph_tensor(
934 w.w_packed
935 .graph_tensor()
936 .const_f32_like(eye, Shape::from_dims(&[DQ_K, DQ_K]))
937 .unwrap(),
938 );
939 w.matmul(&act).realize_f32()
940 }
941
942 #[test]
943 fn double_quant_fixture_json_parses_with_nested_state() {
944 let st = safetensors::SafeTensors::deserialize(DQ_FIXTURE).unwrap();
945 let blob = st
946 .tensor(&format!("{DQ_PREFIX}.weight.quant_state.bitsandbytes__nf4"))
947 .unwrap();
948 let qs = parse_bnb_quant_state(blob.data()).expect("real bnb blob parses");
949 assert_eq!((qs.n, qs.k, qs.block_size), (DQ_N, DQ_K, 64));
950 let nested = qs.nested.expect("double-quant blob carries nested state");
951 assert_eq!(nested.block_size, 256);
952 assert!(
953 (nested.offset - 0.072_918_33).abs() < 1e-7,
954 "{}",
955 nested.offset
956 );
957 }
958
959 #[test]
960 fn dequantize_nested_absmax_hand_computed() {
961 let mut map = vec![0.0f32; 256];
963 map[0] = -1.0;
964 map[1] = 0.5;
965 map[255] = 1.0;
966 let out =
967 dequantize_nested_absmax(&[0, 1, 255, 1, 0], &[2.0, 4.0, 8.0], &map, 2, 0.25).unwrap();
968 assert_eq!(out, vec![-1.75, 1.25, 4.25, 2.25, -7.75]);
969 }
970
971 #[test]
972 fn dequantize_nested_absmax_rejects_bad_geometry() {
973 let map = vec![0.0f32; 256];
974 assert!(dequantize_nested_absmax(&[0; 4], &[1.0; 2], &map[..255], 2, 0.0).is_err());
975 assert!(dequantize_nested_absmax(&[0; 4], &[1.0; 2], &map, 0, 0.0).is_err());
976 assert!(dequantize_nested_absmax(&[0; 4], &[1.0; 3], &map, 2, 0.0).is_err());
978 assert!(dequantize_nested_absmax(&[0; 4], &[1.0; 1], &map, 2, 0.0).is_err());
979 }
980
981 #[test]
982 fn parse_nested_requires_all_three_fields() {
983 let ok = br#"{"quant_type":"nf4","shape":[16,128],"blocksize":64,
984 "nested_blocksize":256,"nested_dtype":"float32","nested_offset":0.5}"#;
985 let qs = parse_bnb_quant_state(ok).unwrap();
986 assert_eq!(
987 qs.nested,
988 Some(BnbNestedState {
989 block_size: 256,
990 offset: 0.5
991 })
992 );
993 let missing_offset = br#"{"quant_type":"nf4","shape":[16,128],"blocksize":64,
994 "nested_blocksize":256,"nested_dtype":"float32"}"#;
995 assert!(parse_bnb_quant_state(missing_offset).is_err());
996 let wrong_dtype = br#"{"quant_type":"nf4","shape":[16,128],"blocksize":64,
997 "nested_blocksize":256,"nested_dtype":"float16","nested_offset":0.5}"#;
998 assert!(parse_bnb_quant_state(wrong_dtype).is_err());
999 }
1000
1001 #[test]
1007 fn load_nf4_layer_double_quant_matches_original_weights() {
1008 let st = safetensors::SafeTensors::deserialize(DQ_FIXTURE).unwrap();
1009 let w = match load_nf4_layer(&st, DQ_PREFIX, &Device::cpu()) {
1010 Ok(w) => w,
1011 Err(e) => panic!("double-quant load failed: {e}"),
1012 };
1013 assert_eq!((w.n, w.k, w.block_size), (DQ_N, DQ_K, 64));
1014 let y = dequant_through_fuel(&w);
1015 let err = rel_rms_vs_original(|n, k| y[k * DQ_N + n]);
1016 assert!(err < 0.16, "relative RMS error vs original weights {err}");
1017 }
1018
1019 #[test]
1023 fn double_quant_error_band_rejects_a_wrong_absmax_decode() {
1024 let st = safetensors::SafeTensors::deserialize(DQ_FIXTURE).unwrap();
1025 let get = |suffix: &str| st.tensor(&format!("{DQ_PREFIX}.weight{suffix}")).unwrap();
1026 let q = get(".absmax").data().to_vec();
1027 let nested_absmax = f32s(get(".nested_absmax").data());
1028 let map = f32s(get(".nested_quant_map").data());
1029 let raw_packed = get("").data().to_vec();
1030 let packed: Vec<u8> = raw_packed.iter().map(|b| b.rotate_left(4)).collect();
1032 let build = |absmax: Vec<f32>| {
1033 let w = nf4_from_bytes(packed.clone(), absmax, DQ_N, DQ_K, 64, &Device::cpu()).unwrap();
1034 let y = dequant_through_fuel(&w);
1035 rel_rms_vs_original(|n, k| y[k * DQ_N + n])
1036 };
1037 let good =
1038 build(dequantize_nested_absmax(&q, &nested_absmax, &map, 256, 0.072_918_33).unwrap());
1039 let raw_nibbles = {
1041 let absmax =
1042 dequantize_nested_absmax(&q, &nested_absmax, &map, 256, 0.072_918_33).unwrap();
1043 let w =
1044 nf4_from_bytes(raw_packed.clone(), absmax, DQ_N, DQ_K, 64, &Device::cpu()).unwrap();
1045 let y = dequant_through_fuel(&w);
1046 rel_rms_vs_original(|n, k| y[k * DQ_N + n])
1047 };
1048 assert!(
1049 raw_nibbles > 2.0 * good,
1050 "raw-nibble {raw_nibbles} vs good {good}"
1051 );
1052 let no_offset =
1053 build(dequantize_nested_absmax(&q, &nested_absmax, &map, 256, 0.0).unwrap());
1054 let no_scale =
1055 build(dequantize_nested_absmax(&q, &[1.0], &map, 256, 0.072_918_33).unwrap());
1056 assert!(good < 0.16, "good decode {good}");
1057 assert!(
1058 no_offset > 2.0 * good,
1059 "offset-less {no_offset} vs good {good}"
1060 );
1061 assert!(
1062 no_scale > 2.0 * good,
1063 "scale-less {no_scale} vs good {good}"
1064 );
1065 }
1066
1067 #[test]
1070 fn load_nf4_layer_rejects_u8_absmax_without_nested_state() {
1071 use safetensors::tensor::TensorView;
1072 use std::collections::HashMap;
1073 let w = vec![0u8; 4];
1074 let am = vec![0u8; 2];
1075 let qs = br#"{"quant_type":"nf4","shape":[2,4],"blocksize":2}"#.to_vec();
1076 let mut t: HashMap<String, TensorView<'_>> = HashMap::new();
1077 t.insert(
1078 "l.weight".into(),
1079 TensorView::new(safetensors::Dtype::U8, vec![4], &w).unwrap(),
1080 );
1081 t.insert(
1082 "l.weight.absmax".into(),
1083 TensorView::new(safetensors::Dtype::U8, vec![2], &am).unwrap(),
1084 );
1085 t.insert(
1086 "l.weight.quant_state.bitsandbytes__nf4".into(),
1087 TensorView::new(safetensors::Dtype::U8, vec![qs.len()], &qs).unwrap(),
1088 );
1089 let ser = safetensors::serialize(&t, None::<HashMap<String, String>>).unwrap();
1090 let st = safetensors::SafeTensors::deserialize(&ser).unwrap();
1091 assert!(load_nf4_layer(&st, "l", &Device::cpu()).is_err());
1092 }
1093}