Skip to main content

fuel_tensor/
nf4.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! bitsandbytes NF4 weight materialization helpers.
3//!
4//! Companion to [`fuel_graph::NodeHandle::nf4_matmul`] (registered as
5//! [`fuel_graph::registry::FusedOps::NF4_MATMUL`], CPU dispatch
6//! shipped in `fuel-cpu-backend::byte_kernels::nf4_matmul_{f32,f16,bf16}`).
7//! The fused op takes 3 inputs: activations `[..., M, K]` + a packed
8//! weight tensor `[N, K/2]` U8 + a per-block absmax scale tensor
9//! `[N, K/block_size]` F32. This module takes a caller-supplied
10//! pair of byte buffers (typically loaded via the user's preferred
11//! safetensors parser) and constructs the two `Tensor` inputs in
12//! the layout the fused op expects.
13//!
14//! ## bitsandbytes on-disk layout primer
15//!
16//! bnb 0.43+ stores a 4-bit-quantized linear layer as three
17//! safetensors entries per `Linear4bit`:
18//!
19//! 1. **`<prefix>.weight`**: U8 tensor of shape `[N * K / 2]` flat.
20//!    Two 4-bit NF4 codes per byte, K-fastest packing. **The UPPER
21//!    nibble of byte `(n * K + k) / 2` (for even k) holds the code for
22//!    `(n, k)`; the lower nibble holds the next k position** (bnb packs
23//!    `(q[::2] << 4) | q[1::2]`; verified against a real checkpoint, see
24//!    `tests/fixtures/bnb_double_quant/PROVENANCE.md`). Fuel's fused NF4
25//!    op uses the opposite order (lower nibble = even k), so
26//!    [`load_nf4_layer`] swaps the nibbles of every byte at load;
27//!    [`nf4_from_bytes`] takes bytes already in Fuel's order.
28//! 2. **`<prefix>.weight.absmax`**: F32 tensor of shape
29//!    `[N * K / block_size]` flat. Per-output-row, per-block
30//!    (block_size typically 64) scale.
31//! 3. **`<prefix>.weight.quant_state.bitsandbytes__nf4`**: JSON blob
32//!    in the safetensors `__metadata__` carrying `blocksize`, the
33//!    original `shape: [N, K]`, and the `quant_type: "nf4"` discriminant.
34//!
35//! Checkpoints saved with `bnb_4bit_use_double_quant=True` additionally
36//! quantize the absmax itself: `<prefix>.weight.absmax` is then **U8**,
37//! with the 256-entry code map in `<prefix>.weight.nested_quant_map`
38//! (F32), the second-level scales in `<prefix>.weight.nested_absmax`
39//! (F32, one per `nested_blocksize` codes), and `nested_blocksize`,
40//! `nested_dtype`, `nested_offset` in the JSON blob. [`load_nf4_layer`]
41//! decodes it once at load with [`dequantize_nested_absmax`]
42//! (`absmax[i] = nested_quant_map[q[i]] * nested_absmax[i / nested_blocksize]`
43//! plus `nested_offset`, all in f32, as bitsandbytes' `dequantize_blockwise`
44//! does) and then runs the ordinary single-level path.
45//!
46//! ## What this module does NOT do
47//!
48//! - Parse safetensors files (use [`fuel_loaders::safetensors`] or your
49//!   preferred loader).
50//! - Parse bnb `quant_state` JSON metadata (read `blocksize`, `shape`,
51//!   etc. from your own loader and pass them as args).
52//! - Wire baracuda CUDA dispatch (`nf4_gemv_m{1,2,4,8}_{f16,bf16}`)
53//!   — that's a separate session blocked on the parallel dispatch
54//!   migration.
55//!
56//! What it DOES: take the three concrete pieces (packed bytes,
57//! absmax scales, geometry) and build the two `Tensor` inputs
58//! the fused op expects, including the reshape from bnb's flat
59//! layouts to Fuel's 2D ones.
60
61use crate::Device;
62use crate::lazy::Tensor;
63use fuel_ir::{Result, Shape};
64
65/// A bitsandbytes-style NF4-quantized weight, materialized as the two
66/// `Tensor` inputs that [`fuel_graph::NodeHandle::nf4_matmul`]
67/// expects, plus the cached `(n, k, block_size)` geometry for the
68/// matmul builder.
69///
70/// The two inputs are kept as separate `Tensor` fields rather
71/// than bundled into a single tensor — that matches the fused op's
72/// 3-input signature exactly. [`Self::matmul`] is the convenience
73/// builder for the common case where the caller has an `activations:
74/// Tensor` and just wants the linear-layer output.
75pub struct Nf4Weight {
76    /// Packed weight bytes, shape `[N, K/2]` U8.
77    pub w_packed: Tensor,
78    /// Per-block absmax scales, shape `[N, K/block_size]` F32.
79    pub absmax: Tensor,
80    /// Output features (rows of the weight matrix).
81    pub n: usize,
82    /// Input features (the K in `(M, K) @ (K, N) → (M, N)`).
83    pub k: usize,
84    /// Quantization block size (typically 64 in bitsandbytes).
85    pub block_size: usize,
86}
87
88/// Build an [`Nf4Weight`] from caller-supplied byte/scale buffers.
89///
90/// The caller is responsible for loading `w_packed` and `absmax`
91/// from their checkpoint source (safetensors, GGUF, raw files, …)
92/// and supplying the geometry. This function:
93///
94/// 1. Validates the buffer sizes against the geometry.
95/// 2. Constructs the two `Tensor`s on `device` with the 2D
96///    shapes the fused op expects (`[n, k/2]` and `[n, k/block_size]`,
97///    reshaping bnb's flat layouts).
98/// 3. Returns the `Nf4Weight` ready for use.
99///
100/// Requirements:
101/// - `k` must be even (NF4 packs 2 codes per byte along K).
102/// - `k` must be a multiple of `block_size`.
103/// - `w_packed.len() == n * k / 2`.
104/// - `absmax.len() == n * (k / block_size)`.
105pub 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    // Build the Tensors. const_u8_like / const_f32_like need a
147    // host tensor as the "graph anchor"; we anchor on a tiny f32
148    // scalar built directly from `device`. Once both are pushed onto
149    // the same graph, they're ready to feed nf4_matmul.
150    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    /// Convenience: run `activations @ dequant(weight)` via the
168    /// fused [`fuel_graph::NodeHandle::nf4_matmul`] op. `activations`
169    /// must live on the same graph as `self.w_packed` and `self.absmax`;
170    /// the caller threads this by building `activations` from
171    /// `self.w_packed.graph_tensor()` (or any other tensor on the
172    /// same graph).
173    ///
174    /// Output dtype matches `activations`' dtype (F32 / F16 / BF16
175    /// — see [`fuel_graph::NodeHandle::nf4_matmul`] for the full
176    /// contract).
177    pub fn matmul(&self, activations: &Tensor) -> Tensor {
178        activations.nf4_matmul(&self.w_packed, &self.absmax, self.block_size)
179    }
180}
181
182// =============================================================================
183// bitsandbytes quant_state JSON parser + safetensors-view loader
184// =============================================================================
185
186/// The geometry + format discriminators extracted from a
187/// bitsandbytes `quant_state` JSON blob. Parsed via
188/// [`parse_bnb_quant_state`]; consumed by [`load_nf4_layer`] to
189/// reconstruct the `[N, K/block_size]` absmax layout from bnb's
190/// flat `[N * K / block_size]` storage.
191///
192/// Only the fields needed to drive [`Nf4Weight`] construction are
193/// extracted; the parser ignores unknown fields (forward-compat
194/// with future bnb metadata additions).
195#[derive(Debug, Clone, PartialEq)]
196pub struct BnbQuantState {
197    /// Number of output features (rows of the weight matrix).
198    pub n: usize,
199    /// Number of input features (the K in `(M, K) @ (K, N) → (M, N)`).
200    pub k: usize,
201    /// First-level (un-nested) quantization block size — typically 64.
202    pub block_size: usize,
203    /// Second-level (double-quantization) parameters when the absmax is
204    /// itself quantized; `None` for a plain single-level checkpoint.
205    pub nested: Option<BnbNestedState>,
206}
207
208/// The JSON-carried half of bitsandbytes double quantization. The other
209/// half (`nested_absmax`, `nested_quant_map`) is stored as separate
210/// safetensors tensors, not in the JSON blob.
211#[derive(Debug, Clone, PartialEq)]
212pub struct BnbNestedState {
213    /// Number of first-level absmax codes sharing one second-level scale
214    /// (bnb's default is 256).
215    pub block_size: usize,
216    /// Mean of the first-level absmax, added back after dequantization.
217    pub offset: f32,
218}
219
220/// Parse the JSON `quant_state` blob that bitsandbytes 0.43+ stores
221/// alongside an NF4-quantized weight. Format documented at
222/// <https://github.com/bitsandbytes-foundation/bitsandbytes/blob/main/bitsandbytes/functional.py>
223/// (search for `quant_state` serialization).
224///
225/// **Validated fields:**
226/// - `quant_type == "nf4"` — refuses other formats (fp4, etc.) with a
227///   clear error: FP4 needs a different 16-entry codebook than the fused
228///   NF4 op carries (tracked as a follow-up).
229/// - `shape` is `[N, K]` two integers.
230/// - `blocksize` is a positive usize that divides K.
231///
232/// **Double quantization:** when `nested_blocksize` is present the
233/// returned state carries a [`BnbNestedState`] (`nested_blocksize`,
234/// `nested_offset`; `nested_dtype` must be `"float32"`). Supplying only
235/// some of the three is an error.
236///
237/// **Rejected with a clear error:**
238/// - `nested_absmax` / `nested_quant_map` inside the JSON. bitsandbytes
239///   stores those as tensors, so seeing them here means the blob is not
240///   in the format this parser understands.
241///
242/// **Ignored fields:** `dtype` (original pre-quantization dtype —
243/// the loader uses the activations' dtype at matmul time, not the
244/// original weight dtype), `device`, and any future bnb additions.
245pub 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    // Discriminator: must be "nf4". bnb also has "fp4" + variants;
256    // we explicitly reject those so the caller knows v1 scope.
257    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    // bitsandbytes keeps nested_absmax / nested_quant_map as tensors; they
275    // never appear in the JSON blob.
276    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    // shape: [N, K]
287    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    // blocksize
311    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
349/// Read the optional `nested_*` JSON fields: all of `nested_blocksize`,
350/// `nested_offset` and `nested_dtype` (= `"float32"`), or none.
351fn 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
371/// Validate the three present `nested_*` values.
372fn 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
393/// Decode a double-quantized absmax exactly as bitsandbytes does:
394/// `absmax[i] = nested_quant_map[q[i]] * nested_absmax[i / nested_block_size] + offset`,
395/// every operation in f32. `q` is the U8 `<prefix>.weight.absmax` tensor.
396///
397/// Errors (never panics) when `nested_quant_map` is not 256 entries, the
398/// block size is zero, or `nested_absmax` does not hold exactly one scale
399/// per block of `q`.
400pub 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
433/// Little-endian f32 values from a raw tensor payload.
434fn 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
450/// Load a single NF4-quantized layer from a parsed safetensors view
451/// (typically obtained via the `safetensors` crate's
452/// `SafeTensors::deserialize` or fuel-core's
453/// [`fuel_loaders::safetensors::MmapedSafetensors`]).
454///
455/// `prefix` is the layer's name in the safetensors file
456/// (e.g. `"model.layers.0.self_attn.q_proj"`). The function expects
457/// three tensors at:
458/// - `{prefix}.weight` — U8, flat `[N * K / 2]`.
459/// - `{prefix}.weight.absmax` — F32, flat `[N * K / block_size]`.
460/// - `{prefix}.weight.quant_state.bitsandbytes__nf4` — U8 bytes
461///   holding the JSON metadata blob (see [`parse_bnb_quant_state`]).
462///
463/// All three tensors are read into host memory, validated against
464/// the geometry from the JSON blob, and reshaped into the 2D
465/// layouts that [`Nf4Weight`] / [`fuel_graph::NodeHandle::nf4_matmul`]
466/// expect.
467///
468/// **Double quantization** (`bnb_4bit_use_double_quant=True`): when the JSON
469/// carries `nested_*` fields, `{prefix}.weight.absmax` must be U8 and the
470/// loader additionally reads `{prefix}.weight.nested_absmax` (F32) and
471/// `{prefix}.weight.nested_quant_map` (F32, 256 entries), decoding the
472/// absmax with [`dequantize_nested_absmax`]. A U8 absmax without nested
473/// parameters, or an F32 absmax with them, is rejected.
474///
475/// **Nibble order:** bitsandbytes stores the even-k code in the upper
476/// nibble; Fuel's fused op reads it from the lower one, so every packed
477/// byte is nibble-swapped here before it reaches [`nf4_from_bytes`].
478///
479/// **Limitations** (see `parse_bnb_quant_state` for the full list):
480/// - Non-NF4 bnb formats (fp4, etc.) are rejected: FP4 needs its own
481///   16-entry codebook, which the fused NF4 op does not carry.
482/// - `{prefix}.weight.quant_map` is not cross-checked against Fuel's NF4
483///   table.
484pub 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    // quant_state is a U8 byte stream holding UTF-8 JSON. Parse it
513    // first so we have the geometry to validate the other two
514    // tensors' shapes against.
515    let qs = parse_bnb_quant_state(quant_state_view.data())?;
516
517    // weight: U8, flat [N*K/2].
518    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    // bnb: even-k code in the UPPER nibble. Fuel's op: LOWER nibble.
526    let w_bytes: Vec<u8> = weight_view
527        .data()
528        .iter()
529        .map(|b| b.rotate_left(4))
530        .collect();
531
532    // absmax: flat [N*K/block_size]. F32 for a plain checkpoint; U8 codes
533    // plus nested tensors for a double-quantized one.
534    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    // Hand off to the bytes-in-hand helper for layout validation +
585    // Tensor construction. The byte/scale counts get checked
586    // against (n, k, block_size) there.
587    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    /// Round-trip: build an Nf4Weight from bytes, run matmul, verify
595    /// against the hand-computed two-outputs-two-blocks case from the
596    /// byte-kernel test.
597    #[test]
598    fn nf4_weight_matmul_round_trip() {
599        let device = Device::cpu();
600        // Same numbers as fuel-cpu-backend's nf4_matmul_f32_two_outputs_two_blocks:
601        //   activations [1, 2, 2, 4] (m=1, k=4)
602        //   w_packed bytes [247, 247, 127, 127] (n=2, k/2=2)
603        //   absmax [1.0, 2.0, 10.0, 20.0] (n=2, k/block_size=2)
604        //   block_size = 2
605        //   expected output [10.0, 50.0]
606        let weight = nf4_from_bytes(
607            vec![247_u8, 247, 127, 127],
608            vec![1.0_f32, 2.0, 10.0, 20.0],
609            /* n */ 2,
610            /* k */ 4,
611            /* block_size */ 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        // Must be on the same graph as the weight tensors — go
622        // through the weight's graph anchor.
623        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; // keep the original visible for symmetry in the docs
629        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    /// Validation: k must be even.
637    #[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            /* n */ 2,
643            /* k */ 3,
644            /* block_size */ 1,
645            &Device::cpu(),
646        );
647        assert!(r.is_err());
648    }
649
650    /// Validation: k must be a multiple of block_size.
651    #[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            /* n */ 2,
657            /* k */ 4,
658            /* block_size */ 3,
659            &Device::cpu(),
660        );
661        assert!(r.is_err());
662    }
663
664    /// Validation: w_packed byte count must match `n × k/2`.
665    #[test]
666    fn nf4_from_bytes_rejects_wrong_packed_size() {
667        let r = nf4_from_bytes(
668            vec![0_u8; 5], // wrong: should be 4
669            vec![0.0_f32; 2],
670            /* n */ 2,
671            /* k */ 4,
672            /* block_size */ 2,
673            &Device::cpu(),
674        );
675        assert!(r.is_err());
676    }
677
678    /// Quant-state parser: well-formed minimal NF4 metadata.
679    #[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    /// Quant-state parser: rejects non-nf4 quant_type with a clear error.
695    #[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    /// Quant-state parser: `nested_absmax` / `nested_quant_map` are tensors in
704    /// real checkpoints, so finding them in the JSON is a malformed blob.
705    #[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    /// Quant-state parser: rejects blocksize that doesn't divide K.
718    #[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    /// Quant-state parser: rejects odd K (NF4 packs 2 codes/byte).
730    #[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    /// Quant-state parser: gracefully errors on malformed JSON.
742    #[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    /// Quant-state parser: rejects missing required fields.
749    #[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    /// End-to-end: synthesize a minimal in-memory safetensors file
758    /// containing the 3 expected entries (weight U8 + absmax F32 +
759    /// quant_state JSON-as-bytes), then load it via load_nf4_layer.
760    ///
761    /// Uses the same hand-computed numbers as
762    /// `nf4_weight_matmul_round_trip` so we know the loaded weight
763    /// produces the expected matmul output.
764    #[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        // weight bytes [N * K / 2] = 4 bytes: the hand-computed test's bytes in
774        // Fuel's order (247, 247, 127, 127) nibble-swapped into bitsandbytes'
775        // order (even-k code in the upper nibble), which the loader converts.
776        let weight_bytes = vec![0x7F_u8, 0x7F, 0xF7, 0xF7];
777
778        // absmax bytes: [N * K / block_size] = 4 f32 = 16 bytes.
779        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        // quant_state: UTF-8 JSON blob.
783        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        // Build TensorViews. safetensors::TensorView::new takes
789        // (dtype, shape, data) — shape is just metadata, data is
790        // the raw bytes.
791        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        // Serialize to in-memory safetensors bytes.
807        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        // Deserialize + load via the new loader.
819        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        // Sanity matmul: same expected output as nf4_weight_matmul_round_trip.
827        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    /// load_nf4_layer: missing weight tensor produces a clear error
840    /// pointing at the missing key.
841    #[test]
842    fn load_nf4_layer_missing_weight_errors() {
843        use safetensors::tensor::TensorView;
844        use std::collections::HashMap;
845        // Only the quant_state and absmax — no weight.
846        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        // `unwrap_err` would require Nf4Weight: Debug; match the Err
869        // arm explicitly so we don't pull in that bound.
870        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    // -----------------------------------------------------------------
881    // Double quantization (bitsandbytes `bnb_4bit_use_double_quant=True`)
882    // -----------------------------------------------------------------
883
884    /// First 16 rows of `model.layers.1.self_attn.k_proj` from
885    /// `unsloth/Qwen3-0.6B-unsloth-bnb-4bit` (NF4, double-quantized), with
886    /// the JSON `shape` patched to `[16, 1024]`; see the fixture's
887    /// PROVENANCE.md. Real bitsandbytes output, not synthetic.
888    const DQ_FIXTURE: &[u8] = include_bytes!(
889        "../tests/fixtures/bnb_double_quant/bnb_double_quant_k_proj_16rows.safetensors"
890    );
891    /// The same 16 rows of the unquantized `Qwen/Qwen3-0.6B` weight, bf16.
892    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    /// Relative RMS error of `got[n*K + k]` against the original bf16 rows.
908    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    /// Dequantized weight `W[n][k]` through Fuel's own fused NF4 op: with an
925    /// identity activation matrix the matmul output is `W^T`, so
926    /// `y[m * N + n] = W[n][m]`. This also pins the nibble order against
927    /// real bitsandbytes data.
928    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        // map[q] * nested[i / 2] + offset, in f32.
962        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        // 4 codes at block 2 need exactly 2 scales.
977        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    /// The headline gate. A real double-quantized bitsandbytes layer loads,
1002    /// and its dequantized weights match the ORIGINAL bf16 weights to NF4's
1003    /// ordinary 4-bit error. A wrong absmax decode (wrong block index,
1004    /// missing scale or offset) lands far outside that band; the sabotage
1005    /// sibling below proves the band can tell.
1006    #[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    /// Retained sabotage sibling: decode the same real checkpoint but drop
1020    /// `nested_offset` (and, separately, the second-level scale). Each must
1021    /// land well outside the band the gate above accepts.
1022    #[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        // Fuel's order, as `load_nf4_layer` produces it.
1031        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        // Un-swapped (raw bnb) nibbles are as wrong as a bad scale decode.
1040        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    /// A U8 absmax without nested parameters is a malformed checkpoint, not
1068    /// something to reinterpret.
1069    #[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}