Skip to main content

fuel_quantized/
dequant.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Byte-to-`f32` dequantization dispatched on [`GgmlDType`], for callers holding a raw GGUF
3//! tensor byte slice and its declared dtype (the common shape at a model-loader's `from_gguf`
4//! site).
5//!
6//! Centralizes what used to be independently hand-rolled in 10 `fuel-transformers` model
7//! files (`dequant_bytes_to_f32` + `cpu_dequant_q4_0_bytes`, copy-pasted identically across
8//! `lazy_quantized_{llama,qwen2,qwen3,qwen3_moe,gemma3,glm4,lfm2,phi3,smollm3,t5}.rs`), each
9//! wiring only `F32`/`F16`/`BF16`/`Q4_0` and erroring on the other 10 `GgmlDType` variants —
10//! including `Q6_K`, llama.cpp's ordinary choice for `output.weight`/`token_embd.weight` even
11//! in an otherwise-`Q4_0` checkpoint, so a real-world GGUF file routinely hit the catch-all.
12//! The `Q4_0` arm those files DID wire was also a hand re-derivation of the block math,
13//! bypassing [`GgmlType::to_float`] entirely — so the tested implementation was not the one
14//! executing.
15//!
16//! 14 of the 15 [`GgmlDType`] variants are wired here. The one exclusion: `Q8_1` declines with
17//! a typed error citing GAP-125, because `BlockQ8_1::to_float` is `unimplemented!()` upstream
18//! (a real numerics gap in this crate, not a dispatch-layer one — wiring it here would only
19//! trade an `Err` for a panic).
20//!
21//! ⚠️ This centralization is not purely subtractive. The ten hand-rolled implementations it
22//! replaces were each numerically incomplete but memory-safe; the first version of the single
23//! function replacing them introduced a real soundness bug (an unaligned `&[u8]` cast to
24//! `&[T]` for a `T` needing alignment 2+ — see [`cast_and_dequant`]'s doc), caught before merge
25//! by review, not by any test in the original diff. Fixed with an alignment check, an
26//! alternate copy-to-`Vec<T>` path for the misaligned case, and a Miri-verified born-red.
27
28use crate::k_quants::{
29    BlockQ2K, BlockQ3K, BlockQ4_0, BlockQ4_1, BlockQ4K, BlockQ5_0, BlockQ5_1, BlockQ5K, BlockQ6K,
30    BlockQ8_0, BlockQ8K, GgmlType,
31};
32use fuel_ir::Result;
33use fuel_ir::quantized::GgmlDType;
34use half::{bf16, f16};
35
36/// Reinterpret `bytes` as a dense `[T]` array (GGUF's on-disk block layout) and dequantize via
37/// [`GgmlType::to_float`] — the tested implementation, not a re-derivation of its math.
38///
39/// `bytes.len()` need not be an exact multiple of `size_of::<T>()`; the remainder is silently
40/// dropped, matching every pre-existing per-model `cpu_dequant_*_bytes` this replaces.
41///
42/// # Alignment
43/// `bytes: &[u8]` guarantees only 1-byte alignment, while every `BlockQX` here contains an
44/// `f16`/`bf16`/`f32` field and so needs alignment 2 or 4. GGUF's *file* offsets being
45/// block-aligned says nothing about the *in-memory* address of a `Vec<u8>` a reader loaded
46/// those bytes into, or of a sub-slice taken at a byte offset within it — those two are
47/// different objects, and only the second is what `from_raw_parts` requires. When the pointer
48/// isn't aligned for `T`, this copies into a `Vec<T>` (whose allocator-provided memory *is*
49/// aligned for `T`) instead of casting the borrowed bytes directly.
50fn cast_and_dequant<T: GgmlType>(bytes: &[u8]) -> Vec<f32> {
51    // `blocks_from_bytes_cow` borrows when `bytes` is aligned for `T` and copies
52    // into an aligned `Vec<T>` otherwise. `T: Pod` (a supertrait of `GgmlType`)
53    // makes both paths safe code, where this function used to hand-write the
54    // `from_raw_parts` / `copy_nonoverlapping` + `set_len` pair and justify
55    // each in a comment.
56    let blocks = crate::k_quants::blocks_from_bytes_cow::<T>(bytes);
57    let mut out = vec![0.0_f32; blocks.len() * T::BLCK_SIZE];
58    T::to_float(&blocks, &mut out);
59    out
60}
61
62/// Dequantize a raw GGUF tensor byte slice of the given [`GgmlDType`] to `f32`.
63///
64/// `name` is used only to name the tensor in an error message on a malformed byte count for
65/// `F32`/`F16`/`BF16` (the three unpacked formats, checked exactly; the block formats silently
66/// drop a partial trailing block, matching prior per-model behavior).
67pub fn dequant_ggml_bytes(bytes: &[u8], dtype: GgmlDType, name: &str) -> Result<Vec<f32>> {
68    Ok(match dtype {
69        GgmlDType::F32 => {
70            if !bytes.len().is_multiple_of(4) {
71                return Err(fuel_ir::Error::Msg(format!(
72                    "gguf {name}: F32 byte count {} not a multiple of 4",
73                    bytes.len(),
74                ))
75                .bt());
76            }
77            bytes
78                .as_chunks::<4>()
79                .0
80                .iter()
81                .map(|c| f32::from_le_bytes(*c))
82                .collect()
83        }
84        GgmlDType::F16 => {
85            if !bytes.len().is_multiple_of(2) {
86                return Err(fuel_ir::Error::Msg(format!(
87                    "gguf {name}: F16 byte count {} not a multiple of 2",
88                    bytes.len(),
89                ))
90                .bt());
91            }
92            bytes
93                .as_chunks::<2>()
94                .0
95                .iter()
96                .map(|c| f16::from_le_bytes(*c).to_f32())
97                .collect()
98        }
99        GgmlDType::BF16 => {
100            if !bytes.len().is_multiple_of(2) {
101                return Err(fuel_ir::Error::Msg(format!(
102                    "gguf {name}: BF16 byte count {} not a multiple of 2",
103                    bytes.len(),
104                ))
105                .bt());
106            }
107            bytes
108                .as_chunks::<2>()
109                .0
110                .iter()
111                .map(|c| bf16::from_le_bytes(*c).to_f32())
112                .collect()
113        }
114        GgmlDType::Q4_0 => cast_and_dequant::<BlockQ4_0>(bytes),
115        GgmlDType::Q4_1 => cast_and_dequant::<BlockQ4_1>(bytes),
116        GgmlDType::Q5_0 => cast_and_dequant::<BlockQ5_0>(bytes),
117        GgmlDType::Q5_1 => cast_and_dequant::<BlockQ5_1>(bytes),
118        GgmlDType::Q8_0 => cast_and_dequant::<BlockQ8_0>(bytes),
119        // NOT wired: `BlockQ8_1::to_float` is `unimplemented!()` upstream (GAP-125) -- this
120        // is the one dtype this crate cannot dequantize today, a real numerics gap, not a
121        // dispatch-layer one like the other 9 this function fixes. Calling `to_float` would
122        // panic (CLAUDE.md: never panic on a production path), so decline with a typed error
123        // that names the real cause instead.
124        GgmlDType::Q8_1 => {
125            return Err(fuel_ir::Error::Msg(format!(
126                "gguf {name}: Q8_1 dequantization is unimplemented upstream (GAP-125) -- \
127                 BlockQ8_1::to_float has no body",
128            ))
129            .bt());
130        }
131        GgmlDType::Q2K => cast_and_dequant::<BlockQ2K>(bytes),
132        GgmlDType::Q3K => cast_and_dequant::<BlockQ3K>(bytes),
133        GgmlDType::Q4K => cast_and_dequant::<BlockQ4K>(bytes),
134        GgmlDType::Q5K => cast_and_dequant::<BlockQ5K>(bytes),
135        GgmlDType::Q6K => cast_and_dequant::<BlockQ6K>(bytes),
136        GgmlDType::Q8K => cast_and_dequant::<BlockQ8K>(bytes),
137        GgmlDType::Iq4Nl => cast_and_dequant::<crate::iq4::BlockIq4Nl>(bytes),
138        GgmlDType::Iq4Xs => cast_and_dequant::<crate::iq4::BlockIq4Xs>(bytes),
139        GgmlDType::Iq2Xxs => cast_and_dequant::<crate::iq_grid_types::BlockIq2Xxs>(bytes),
140        GgmlDType::Iq2Xs => cast_and_dequant::<crate::iq_grid_types::BlockIq2Xs>(bytes),
141        GgmlDType::Iq3Xxs => cast_and_dequant::<crate::iq_grid_types::BlockIq3Xxs>(bytes),
142        GgmlDType::Iq1S => cast_and_dequant::<crate::iq_grid_types::BlockIq1S>(bytes),
143        GgmlDType::Iq3S => cast_and_dequant::<crate::iq_grid_types::BlockIq3S>(bytes),
144        GgmlDType::Iq2S => cast_and_dequant::<crate::iq_grid_types::BlockIq2S>(bytes),
145        GgmlDType::Iq1M => cast_and_dequant::<crate::iq_grid_types::BlockIq1M>(bytes),
146        GgmlDType::Tq1_0 => cast_and_dequant::<crate::tq_mx_types::BlockTq1_0>(bytes),
147        GgmlDType::Tq2_0 => cast_and_dequant::<crate::tq_mx_types::BlockTq2_0>(bytes),
148        GgmlDType::Mxfp4 => cast_and_dequant::<crate::tq_mx_types::BlockMxfp4>(bytes),
149        GgmlDType::Nvfp4 => cast_and_dequant::<crate::tq_mx_types::BlockNvfp4>(bytes),
150    })
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn f32_round_trips() {
159        let original: Vec<f32> = vec![1.0, -2.5, 3.25, 0.0];
160        let bytes: Vec<u8> = original.iter().flat_map(|v| v.to_le_bytes()).collect();
161        let out = dequant_ggml_bytes(&bytes, GgmlDType::F32, "t").unwrap();
162        assert_eq!(out, original);
163    }
164
165    #[test]
166    fn f32_rejects_a_misaligned_byte_count() {
167        let bytes = vec![0_u8; 3];
168        let err = dequant_ggml_bytes(&bytes, GgmlDType::F32, "t").unwrap_err();
169        assert!(err.to_string().contains("not a multiple of 4"));
170    }
171
172    /// Every wired block-quant variant must go through `to_float`, not silently return zeros
173    /// or error — this is the born-red for the centralization: previously every one of these
174    /// except Q4_0 hit a catch-all `Err` in the per-model loaders this function replaces.
175    /// Q8_1 is deliberately excluded — see `q8_1_declines_with_a_typed_error_not_a_panic`.
176    #[test]
177    fn every_wired_block_quant_variant_dequantizes_without_error() {
178        for dtype in [
179            GgmlDType::Q4_0,
180            GgmlDType::Q4_1,
181            GgmlDType::Q5_0,
182            GgmlDType::Q5_1,
183            GgmlDType::Q8_0,
184            GgmlDType::Q2K,
185            GgmlDType::Q3K,
186            GgmlDType::Q4K,
187            GgmlDType::Q5K,
188            GgmlDType::Q6K,
189            GgmlDType::Q8K,
190            GgmlDType::Iq4Nl,
191            GgmlDType::Iq4Xs,
192            GgmlDType::Iq2Xxs,
193            GgmlDType::Iq2Xs,
194            GgmlDType::Iq3Xxs,
195            GgmlDType::Iq1S,
196            GgmlDType::Iq3S,
197            GgmlDType::Iq2S,
198            GgmlDType::Iq1M,
199            GgmlDType::Tq1_0,
200            GgmlDType::Tq2_0,
201            GgmlDType::Mxfp4,
202            GgmlDType::Nvfp4,
203        ] {
204            // One full block's worth of zeroed bytes is a valid (if degenerate) input for
205            // every block format here -- big enough that `n_blocks >= 1` for every dtype's
206            // block size, which is what exercises `to_float` rather than a `n_blocks == 0`
207            // no-op.
208            let bytes = vec![0_u8; 4096];
209            let out = dequant_ggml_bytes(&bytes, dtype, "t").unwrap_or_else(|e| {
210                panic!("{dtype:?} must dequantize a whole-zero block without error: {e}")
211            });
212            assert!(
213                !out.is_empty(),
214                "{dtype:?}: 4096 zeroed bytes must yield at least one dequantized block",
215            );
216        }
217    }
218
219    /// Alignment born-red: `Vec<u8>` allocations are only guaranteed 1-byte aligned, and every
220    /// `BlockQX` needs alignment >= 2 (an `f16`/`bf16`/`f32` field). A raw
221    /// `from_raw_parts(bytes.as_ptr() as *const T, ...)` on a misaligned slice is immediate UB
222    /// under the stdlib's contract, not "unlikely in practice" -- and it is reachable: any
223    /// tensor sub-slice at an odd byte offset from a `Vec<u8>` base produces exactly this.
224    ///
225    /// This constructs a slice at a DELIBERATELY misaligned offset and checks the dequantized
226    /// VALUES against an aligned copy of the same bytes, not just that the call returns `Ok` --
227    /// a misaligned read that "happens to work" on this platform would still pass an Ok-only
228    /// check. `Q4_0` is used because its alignment requirement (2, from `d: f16`) is exactly
229    /// the boundary the fix must clear.
230    ///
231    /// `Vec<u8>`'s allocator-provided base address parity is NOT specified by the language --
232    /// it can come back even or odd -- so "index 1 into a fresh `Vec<u8>`" is not reliably
233    /// misaligned (this was measured under Miri: the first version of this test asserted that
234    /// and Miri's allocator handed back an odd base, making offset 1 the ALIGNED one). This
235    /// probes both offset 0 and offset 1 at runtime and picks whichever one `align_offset`
236    /// actually reports as misaligned for `T`, rather than assuming a parity.
237    #[test]
238    fn cast_and_dequant_handles_a_misaligned_byte_slice() {
239        let align = std::mem::align_of::<crate::k_quants::BlockQ4_0>();
240        let block_size = std::mem::size_of::<crate::k_quants::BlockQ4_0>();
241        let n_blocks = 3;
242        let total = n_blocks * block_size;
243
244        // One buffer 1 byte larger than needed so BOTH offset 0 and offset 1 are valid
245        // in-bounds `total`-byte windows into it, deterministically filled.
246        let mut buf = vec![0_u8; total + 1];
247        for (i, b) in buf.iter_mut().enumerate() {
248            *b = (i as u8).wrapping_mul(37).wrapping_add(11);
249        }
250        let offset0_aligned = buf[0..total].as_ptr().align_offset(align) == 0;
251        let (aligned_slice, misaligned_slice) = if offset0_aligned {
252            (&buf[0..total], &buf[1..1 + total])
253        } else {
254            (&buf[1..1 + total], &buf[0..total])
255        };
256        assert_eq!(
257            aligned_slice.as_ptr().align_offset(align),
258            0,
259            "test setup bug"
260        );
261        assert_ne!(
262            misaligned_slice.as_ptr().align_offset(align),
263            0,
264            "test setup bug: this slice must actually BE misaligned for the test to mean \
265             anything -- the two offsets in a 1-byte-larger buffer must have opposite parity \
266             relative to `align`, so exactly one of them is always misaligned",
267        );
268
269        let aligned_out = dequant_ggml_bytes(aligned_slice, GgmlDType::Q4_0, "t").unwrap();
270        let misaligned_out = dequant_ggml_bytes(misaligned_slice, GgmlDType::Q4_0, "t").unwrap();
271
272        // The two slices overlap by `total - 1` bytes rather than being byte-identical, so
273        // compare against a THIRD, independently-constructed aligned copy of exactly the
274        // misaligned slice's own bytes -- the actual claim under test.
275        let misaligned_bytes_copy: Vec<u8> = misaligned_slice.to_vec();
276        let reference_out =
277            dequant_ggml_bytes(&misaligned_bytes_copy, GgmlDType::Q4_0, "t").unwrap();
278        assert_eq!(
279            reference_out, misaligned_out,
280            "dequantizing a byte slice from a misaligned pointer must produce identical values \
281             to dequantizing the same bytes from an aligned one",
282        );
283        // Sanity: the aligned_out computed above is exercised too (not dead), confirming this
284        // path also runs through the same function without panicking.
285        assert_eq!(aligned_out.len(), misaligned_out.len());
286    }
287
288    /// Q8_1 must decline typed, never panic -- `BlockQ8_1::to_float` is `unimplemented!()`
289    /// upstream (GAP-125), so this proves the exclusion is a real early return, not a call
290    /// into `cast_and_dequant` that happens to survive on this input.
291    #[test]
292    fn q8_1_declines_with_a_typed_error_not_a_panic() {
293        let bytes = vec![0_u8; 4096];
294        let err = dequant_ggml_bytes(&bytes, GgmlDType::Q8_1, "t").unwrap_err();
295        assert!(err.to_string().contains("GAP-125"));
296    }
297
298    /// Sabotage: confirm the K-quant arms actually call `to_float` and are not accidentally
299    /// dead code returning the same output as a different variant. Q4_0 (legacy, scale+offset
300    /// per block) and Q6_K (k-quant, superblock scales) must NOT dequantize an identical
301    /// non-zero byte pattern to the same values -- that would mean one of the two match arms
302    /// is not reaching its own type's `to_float`.
303    #[test]
304    fn distinct_dtypes_on_the_same_bytes_diverge() {
305        let bytes = vec![0xAB_u8; 4096];
306        let q4_0 = dequant_ggml_bytes(&bytes, GgmlDType::Q4_0, "t").unwrap();
307        let q6_k = dequant_ggml_bytes(&bytes, GgmlDType::Q6K, "t").unwrap();
308        assert_ne!(
309            q4_0.len(),
310            q6_k.len(),
311            "Q4_0 and Q6_K have different block sizes; equal output lengths would mean one \
312             arm is reaching the wrong to_float",
313        );
314    }
315}