use crate::Device;
use crate::lazy::Tensor;
use fuel_ir::{Result, Shape};
pub struct Nf4Weight {
pub w_packed: Tensor,
pub absmax: Tensor,
pub n: usize,
pub k: usize,
pub block_size: usize,
}
pub fn nf4_from_bytes(
w_packed: impl Into<Vec<u8>>,
absmax: impl Into<Vec<f32>>,
n: usize,
k: usize,
block_size: usize,
device: &Device,
) -> Result<Nf4Weight> {
let w_packed: Vec<u8> = w_packed.into();
let absmax: Vec<f32> = absmax.into();
if k == 0 || !k.is_multiple_of(2) {
return Err(fuel_ir::Error::Msg(format!(
"nf4_from_bytes: k={k} must be even and non-zero",
))
.bt());
}
if block_size == 0 || !k.is_multiple_of(block_size) {
return Err(fuel_ir::Error::Msg(format!(
"nf4_from_bytes: k={k} must be a positive multiple of block_size={block_size}",
))
.bt());
}
let w_expected = n.saturating_mul(k / 2);
if w_packed.len() != w_expected {
return Err(fuel_ir::Error::Msg(format!(
"nf4_from_bytes: w_packed has {} bytes, expected {w_expected} (n={n} × k/2={})",
w_packed.len(),
k / 2,
))
.bt());
}
let abs_expected = n.saturating_mul(k / block_size);
if absmax.len() != abs_expected {
return Err(fuel_ir::Error::Msg(format!(
"nf4_from_bytes: absmax has {} f32 elements, expected {abs_expected} \
(n={n} × k/block_size={})",
absmax.len(),
k / block_size,
))
.bt());
}
let anchor = Tensor::from_f32(vec![0.0_f32], Shape::from_dims(&[1]), device)?;
let w_packed_tensor = anchor
.graph_tensor()
.const_u8_like(w_packed, Shape::from_dims(&[n, k / 2]))?;
let absmax_tensor = anchor
.graph_tensor()
.const_f32_like(absmax, Shape::from_dims(&[n, k / block_size]))?;
Ok(Nf4Weight {
w_packed: Tensor::from_graph_tensor(w_packed_tensor),
absmax: Tensor::from_graph_tensor(absmax_tensor),
n,
k,
block_size,
})
}
impl Nf4Weight {
pub fn matmul(&self, activations: &Tensor) -> Tensor {
activations.nf4_matmul(&self.w_packed, &self.absmax, self.block_size)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BnbQuantState {
pub n: usize,
pub k: usize,
pub block_size: usize,
}
pub fn parse_bnb_quant_state(json_bytes: &[u8]) -> fuel_ir::Result<BnbQuantState> {
let v: serde_json::Value = serde_json::from_slice(json_bytes).map_err(|e| {
fuel_ir::Error::Msg(format!("parse_bnb_quant_state: failed to parse JSON: {e}",)).bt()
})?;
let obj = v.as_object().ok_or_else(|| {
fuel_ir::Error::Msg(
"parse_bnb_quant_state: expected a JSON object at the top level".to_string(),
)
.bt()
})?;
let quant_type = obj
.get("quant_type")
.and_then(|v| v.as_str())
.ok_or_else(|| {
fuel_ir::Error::Msg(
"parse_bnb_quant_state: missing or non-string 'quant_type' field".to_string(),
)
.bt()
})?;
if quant_type != "nf4" {
return Err(fuel_ir::Error::Msg(format!(
"parse_bnb_quant_state: only quant_type=\"nf4\" is supported, \
got {quant_type:?}. Other bnb formats (fp4, etc.) need their \
own loader path.",
))
.bt());
}
if obj.contains_key("nested_absmax") || obj.contains_key("nested_quant_map") {
return Err(fuel_ir::Error::Msg(
"parse_bnb_quant_state: this weight uses double-quantization \
(nested_absmax / nested_quant_map present). v1 only handles \
single-level absmax. Workaround: dequantize the absmax to F32 \
in your loader before calling nf4_from_bytes; pass the resulting \
F32 absmax directly."
.to_string(),
)
.bt());
}
let shape = obj.get("shape").and_then(|v| v.as_array()).ok_or_else(|| {
fuel_ir::Error::Msg("parse_bnb_quant_state: missing or non-array 'shape' field".to_string())
.bt()
})?;
if shape.len() != 2 {
return Err(fuel_ir::Error::Msg(format!(
"parse_bnb_quant_state: 'shape' must be 2-element [N, K], got len={}",
shape.len(),
))
.bt());
}
let n = shape[0].as_u64().ok_or_else(|| {
fuel_ir::Error::Msg(
"parse_bnb_quant_state: shape[0] (N) must be a non-negative integer".to_string(),
)
.bt()
})? as usize;
let k = shape[1].as_u64().ok_or_else(|| {
fuel_ir::Error::Msg(
"parse_bnb_quant_state: shape[1] (K) must be a non-negative integer".to_string(),
)
.bt()
})? as usize;
let block_size = obj
.get("blocksize")
.and_then(|v| v.as_u64())
.ok_or_else(|| {
fuel_ir::Error::Msg(
"parse_bnb_quant_state: missing or non-integer 'blocksize' field".to_string(),
)
.bt()
})? as usize;
if block_size == 0 {
return Err(fuel_ir::Error::Msg(
"parse_bnb_quant_state: blocksize must be positive".to_string(),
)
.bt());
}
if !k.is_multiple_of(block_size) {
return Err(fuel_ir::Error::Msg(format!(
"parse_bnb_quant_state: blocksize={block_size} doesn't divide K={k} \
(this would leave a partial block at the end of each row, which \
bnb's standard format doesn't produce — your checkpoint may be \
corrupted or use a non-standard variant)",
))
.bt());
}
if !k.is_multiple_of(2) {
return Err(fuel_ir::Error::Msg(format!(
"parse_bnb_quant_state: K={k} must be even (NF4 packs 2 codes per byte)",
))
.bt());
}
Ok(BnbQuantState { n, k, block_size })
}
pub fn load_nf4_layer(
st: &safetensors::SafeTensors,
prefix: &str,
device: &Device,
) -> fuel_ir::Result<Nf4Weight> {
let weight_key = format!("{prefix}.weight");
let absmax_key = format!("{prefix}.weight.absmax");
let quant_state_key = format!("{prefix}.weight.quant_state.bitsandbytes__nf4");
let weight_view = st.tensor(&weight_key).map_err(|e| {
fuel_ir::Error::Msg(format!(
"load_nf4_layer: tensor {weight_key:?} not found in safetensors: {e}",
))
.bt()
})?;
let absmax_view = st.tensor(&absmax_key).map_err(|e| {
fuel_ir::Error::Msg(format!(
"load_nf4_layer: tensor {absmax_key:?} not found in safetensors: {e}",
))
.bt()
})?;
let quant_state_view = st.tensor(&quant_state_key).map_err(|e| {
fuel_ir::Error::Msg(format!(
"load_nf4_layer: tensor {quant_state_key:?} not found in safetensors: {e}",
))
.bt()
})?;
let qs = parse_bnb_quant_state(quant_state_view.data())?;
if weight_view.dtype() != safetensors::Dtype::U8 {
return Err(fuel_ir::Error::Msg(format!(
"load_nf4_layer: {weight_key:?} dtype={:?}, expected U8",
weight_view.dtype(),
))
.bt());
}
let w_bytes: Vec<u8> = weight_view.data().to_vec();
if absmax_view.dtype() != safetensors::Dtype::F32 {
return Err(fuel_ir::Error::Msg(format!(
"load_nf4_layer: {absmax_key:?} dtype={:?}, expected F32. \
If this is a double-quantized absmax stored as U8 with its own \
quant_state, dequantize it before calling this loader.",
absmax_view.dtype(),
))
.bt());
}
let absmax_bytes = absmax_view.data();
if absmax_bytes.len() % 4 != 0 {
return Err(fuel_ir::Error::Msg(format!(
"load_nf4_layer: {absmax_key:?} has {} bytes (not a multiple of 4 — corrupt F32 data)",
absmax_bytes.len(),
))
.bt());
}
let absmax_f32: Vec<f32> = absmax_bytes
.as_chunks::<4>()
.0
.iter()
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect();
nf4_from_bytes(w_bytes, absmax_f32, qs.n, qs.k, qs.block_size, device)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nf4_weight_matmul_round_trip() {
let device = Device::cpu();
let weight = nf4_from_bytes(
vec![247_u8, 247, 127, 127],
vec![1.0_f32, 2.0, 10.0, 20.0],
2,
4,
2,
&device,
)
.expect("nf4_from_bytes");
let activations = Tensor::from_f32(
vec![1.0_f32, 2.0, 2.0, 4.0],
Shape::from_dims(&[1, 4]),
&device,
)
.unwrap();
let activations_t = weight
.w_packed
.graph_tensor()
.const_f32_like(vec![1.0_f32, 2.0, 2.0, 4.0], Shape::from_dims(&[1, 4]))
.unwrap();
let _ = activations; let act = Tensor::from_graph_tensor(activations_t);
let y = weight.matmul(&act).realize_f32();
assert_eq!(y.len(), 2);
assert!((y[0] - 10.0).abs() < 1e-5, "out 0: {}", y[0]);
assert!((y[1] - 50.0).abs() < 1e-5, "out 1: {}", y[1]);
}
#[test]
fn nf4_from_bytes_rejects_odd_k() {
let r = nf4_from_bytes(
vec![0_u8; 6],
vec![0.0_f32; 2],
2,
3,
1,
&Device::cpu(),
);
assert!(r.is_err());
}
#[test]
fn nf4_from_bytes_rejects_block_size_mismatch() {
let r = nf4_from_bytes(
vec![0_u8; 4],
vec![0.0_f32; 2],
2,
4,
3,
&Device::cpu(),
);
assert!(r.is_err());
}
#[test]
fn nf4_from_bytes_rejects_wrong_packed_size() {
let r = nf4_from_bytes(
vec![0_u8; 5], vec![0.0_f32; 2],
2,
4,
2,
&Device::cpu(),
);
assert!(r.is_err());
}
#[test]
fn parse_bnb_quant_state_minimal_nf4() {
let json = br#"{"quant_type":"nf4","shape":[256,128],"blocksize":64,"dtype":"float16"}"#;
let qs = parse_bnb_quant_state(json).expect("parse_bnb_quant_state");
assert_eq!(
qs,
BnbQuantState {
n: 256,
k: 128,
block_size: 64
}
);
}
#[test]
fn parse_bnb_quant_state_rejects_fp4() {
let json = br#"{"quant_type":"fp4","shape":[256,128],"blocksize":64}"#;
let r = parse_bnb_quant_state(json);
let err = r.unwrap_err().to_string();
assert!(err.contains("nf4"), "error should mention nf4: {err}");
}
#[test]
fn parse_bnb_quant_state_rejects_nested_absmax() {
let json =
br#"{"quant_type":"nf4","shape":[256,128],"blocksize":64,"nested_absmax":[1.0,2.0]}"#;
let r = parse_bnb_quant_state(json);
let err = r.unwrap_err().to_string();
assert!(
err.contains("double-quantization"),
"error should mention double-quantization: {err}"
);
}
#[test]
fn parse_bnb_quant_state_rejects_misaligned_blocksize() {
let json = br#"{"quant_type":"nf4","shape":[256,130],"blocksize":64}"#;
let r = parse_bnb_quant_state(json);
let err = r.unwrap_err().to_string();
assert!(
err.contains("doesn't divide K") || err.contains("must be even"),
"error should flag misalignment: {err}"
);
}
#[test]
fn parse_bnb_quant_state_rejects_odd_k() {
let json = br#"{"quant_type":"nf4","shape":[256,127],"blocksize":1}"#;
let r = parse_bnb_quant_state(json);
let err = r.unwrap_err().to_string();
assert!(
err.contains("must be even"),
"error should flag odd K: {err}"
);
}
#[test]
fn parse_bnb_quant_state_rejects_malformed_json() {
let r = parse_bnb_quant_state(b"not json at all");
assert!(r.is_err());
}
#[test]
fn parse_bnb_quant_state_rejects_missing_shape() {
let json = br#"{"quant_type":"nf4","blocksize":64}"#;
let r = parse_bnb_quant_state(json);
let err = r.unwrap_err().to_string();
assert!(err.contains("shape"), "error should mention shape: {err}");
}
#[test]
fn load_nf4_layer_round_trip_synthetic_safetensors() {
use safetensors::tensor::TensorView;
use std::collections::HashMap;
let n: usize = 2;
let k: usize = 4;
let block_size: usize = 2;
let weight_bytes = vec![247_u8, 247, 127, 127];
let absmax_floats: Vec<f32> = vec![1.0, 2.0, 10.0, 20.0];
let absmax_bytes: Vec<u8> = absmax_floats.iter().flat_map(|f| f.to_le_bytes()).collect();
let quant_state_json = format!(
r#"{{"quant_type":"nf4","shape":[{n},{k}],"blocksize":{block_size},"dtype":"float16"}}"#,
);
let quant_state_bytes = quant_state_json.as_bytes().to_vec();
let weight_view = TensorView::new(safetensors::Dtype::U8, vec![n * k / 2], &weight_bytes)
.expect("weight TensorView");
let absmax_view = TensorView::new(
safetensors::Dtype::F32,
vec![n * k / block_size],
&absmax_bytes,
)
.expect("absmax TensorView");
let quant_state_view = TensorView::new(
safetensors::Dtype::U8,
vec![quant_state_bytes.len()],
&quant_state_bytes,
)
.expect("quant_state TensorView");
let mut tensors: HashMap<String, TensorView<'_>> = HashMap::new();
tensors.insert("layer.weight".to_string(), weight_view);
tensors.insert("layer.weight.absmax".to_string(), absmax_view);
tensors.insert(
"layer.weight.quant_state.bitsandbytes__nf4".to_string(),
quant_state_view,
);
let metadata: Option<std::collections::HashMap<String, String>> = None;
let serialized =
safetensors::serialize(&tensors, metadata.clone()).expect("safetensors::serialize");
let st =
safetensors::SafeTensors::deserialize(&serialized).expect("SafeTensors::deserialize");
let weight = load_nf4_layer(&st, "layer", &Device::cpu()).expect("load_nf4_layer");
assert_eq!(weight.n, n);
assert_eq!(weight.k, k);
assert_eq!(weight.block_size, block_size);
let act = Tensor::from_graph_tensor(
weight
.w_packed
.graph_tensor()
.const_f32_like(vec![1.0_f32, 2.0, 2.0, 4.0], Shape::from_dims(&[1, 4]))
.unwrap(),
);
let y = weight.matmul(&act).realize_f32();
assert!((y[0] - 10.0).abs() < 1e-5, "out 0: {}", y[0]);
assert!((y[1] - 50.0).abs() < 1e-5, "out 1: {}", y[1]);
}
#[test]
fn load_nf4_layer_missing_weight_errors() {
use safetensors::tensor::TensorView;
use std::collections::HashMap;
let quant_state_bytes = br#"{"quant_type":"nf4","shape":[2,4],"blocksize":2}"#.to_vec();
let absmax_bytes: Vec<u8> = [1.0_f32, 2.0, 10.0, 20.0]
.iter()
.flat_map(|f| f.to_le_bytes())
.collect();
let mut tensors: HashMap<String, TensorView<'_>> = HashMap::new();
tensors.insert(
"layer.weight.absmax".to_string(),
TensorView::new(safetensors::Dtype::F32, vec![4], &absmax_bytes).unwrap(),
);
tensors.insert(
"layer.weight.quant_state.bitsandbytes__nf4".to_string(),
TensorView::new(
safetensors::Dtype::U8,
vec![quant_state_bytes.len()],
&quant_state_bytes,
)
.unwrap(),
);
let metadata: Option<HashMap<String, String>> = None;
let serialized = safetensors::serialize(&tensors, metadata.clone()).unwrap();
let st = safetensors::SafeTensors::deserialize(&serialized).unwrap();
let err = match load_nf4_layer(&st, "layer", &Device::cpu()) {
Ok(_) => panic!("expected an error from load_nf4_layer with missing weight tensor"),
Err(e) => e.to_string(),
};
assert!(
err.contains("layer.weight"),
"error should mention the missing tensor: {err}"
);
}
}