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 })
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn f32_round_trips() {
146 let original: Vec<f32> = vec![1.0, -2.5, 3.25, 0.0];
147 let bytes: Vec<u8> = original.iter().flat_map(|v| v.to_le_bytes()).collect();
148 let out = dequant_ggml_bytes(&bytes, GgmlDType::F32, "t").unwrap();
149 assert_eq!(out, original);
150 }
151
152 #[test]
153 fn f32_rejects_a_misaligned_byte_count() {
154 let bytes = vec![0_u8; 3];
155 let err = dequant_ggml_bytes(&bytes, GgmlDType::F32, "t").unwrap_err();
156 assert!(err.to_string().contains("not a multiple of 4"));
157 }
158
159 /// Every wired block-quant variant must go through `to_float`, not silently return zeros
160 /// or error — this is the born-red for the centralization: previously every one of these
161 /// except Q4_0 hit a catch-all `Err` in the per-model loaders this function replaces.
162 /// Q8_1 is deliberately excluded — see `q8_1_declines_with_a_typed_error_not_a_panic`.
163 #[test]
164 fn every_wired_block_quant_variant_dequantizes_without_error() {
165 for dtype in [
166 GgmlDType::Q4_0,
167 GgmlDType::Q4_1,
168 GgmlDType::Q5_0,
169 GgmlDType::Q5_1,
170 GgmlDType::Q8_0,
171 GgmlDType::Q2K,
172 GgmlDType::Q3K,
173 GgmlDType::Q4K,
174 GgmlDType::Q5K,
175 GgmlDType::Q6K,
176 GgmlDType::Q8K,
177 ] {
178 // One full block's worth of zeroed bytes is a valid (if degenerate) input for
179 // every block format here -- big enough that `n_blocks >= 1` for every dtype's
180 // block size, which is what exercises `to_float` rather than a `n_blocks == 0`
181 // no-op.
182 let bytes = vec![0_u8; 4096];
183 let out = dequant_ggml_bytes(&bytes, dtype, "t").unwrap_or_else(|e| {
184 panic!("{dtype:?} must dequantize a whole-zero block without error: {e}")
185 });
186 assert!(
187 !out.is_empty(),
188 "{dtype:?}: 4096 zeroed bytes must yield at least one dequantized block",
189 );
190 }
191 }
192
193 /// Alignment born-red: `Vec<u8>` allocations are only guaranteed 1-byte aligned, and every
194 /// `BlockQX` needs alignment >= 2 (an `f16`/`bf16`/`f32` field). A raw
195 /// `from_raw_parts(bytes.as_ptr() as *const T, ...)` on a misaligned slice is immediate UB
196 /// under the stdlib's contract, not "unlikely in practice" -- and it is reachable: any
197 /// tensor sub-slice at an odd byte offset from a `Vec<u8>` base produces exactly this.
198 ///
199 /// This constructs a slice at a DELIBERATELY misaligned offset and checks the dequantized
200 /// VALUES against an aligned copy of the same bytes, not just that the call returns `Ok` --
201 /// a misaligned read that "happens to work" on this platform would still pass an Ok-only
202 /// check. `Q4_0` is used because its alignment requirement (2, from `d: f16`) is exactly
203 /// the boundary the fix must clear.
204 ///
205 /// `Vec<u8>`'s allocator-provided base address parity is NOT specified by the language --
206 /// it can come back even or odd -- so "index 1 into a fresh `Vec<u8>`" is not reliably
207 /// misaligned (this was measured under Miri: the first version of this test asserted that
208 /// and Miri's allocator handed back an odd base, making offset 1 the ALIGNED one). This
209 /// probes both offset 0 and offset 1 at runtime and picks whichever one `align_offset`
210 /// actually reports as misaligned for `T`, rather than assuming a parity.
211 #[test]
212 fn cast_and_dequant_handles_a_misaligned_byte_slice() {
213 let align = std::mem::align_of::<crate::k_quants::BlockQ4_0>();
214 let block_size = std::mem::size_of::<crate::k_quants::BlockQ4_0>();
215 let n_blocks = 3;
216 let total = n_blocks * block_size;
217
218 // One buffer 1 byte larger than needed so BOTH offset 0 and offset 1 are valid
219 // in-bounds `total`-byte windows into it, deterministically filled.
220 let mut buf = vec![0_u8; total + 1];
221 for (i, b) in buf.iter_mut().enumerate() {
222 *b = (i as u8).wrapping_mul(37).wrapping_add(11);
223 }
224 let offset0_aligned = buf[0..total].as_ptr().align_offset(align) == 0;
225 let (aligned_slice, misaligned_slice) = if offset0_aligned {
226 (&buf[0..total], &buf[1..1 + total])
227 } else {
228 (&buf[1..1 + total], &buf[0..total])
229 };
230 assert_eq!(
231 aligned_slice.as_ptr().align_offset(align),
232 0,
233 "test setup bug"
234 );
235 assert_ne!(
236 misaligned_slice.as_ptr().align_offset(align),
237 0,
238 "test setup bug: this slice must actually BE misaligned for the test to mean \
239 anything -- the two offsets in a 1-byte-larger buffer must have opposite parity \
240 relative to `align`, so exactly one of them is always misaligned",
241 );
242
243 let aligned_out = dequant_ggml_bytes(aligned_slice, GgmlDType::Q4_0, "t").unwrap();
244 let misaligned_out = dequant_ggml_bytes(misaligned_slice, GgmlDType::Q4_0, "t").unwrap();
245
246 // The two slices overlap by `total - 1` bytes rather than being byte-identical, so
247 // compare against a THIRD, independently-constructed aligned copy of exactly the
248 // misaligned slice's own bytes -- the actual claim under test.
249 let misaligned_bytes_copy: Vec<u8> = misaligned_slice.to_vec();
250 let reference_out =
251 dequant_ggml_bytes(&misaligned_bytes_copy, GgmlDType::Q4_0, "t").unwrap();
252 assert_eq!(
253 reference_out, misaligned_out,
254 "dequantizing a byte slice from a misaligned pointer must produce identical values \
255 to dequantizing the same bytes from an aligned one",
256 );
257 // Sanity: the aligned_out computed above is exercised too (not dead), confirming this
258 // path also runs through the same function without panicking.
259 assert_eq!(aligned_out.len(), misaligned_out.len());
260 }
261
262 /// Q8_1 must decline typed, never panic -- `BlockQ8_1::to_float` is `unimplemented!()`
263 /// upstream (GAP-125), so this proves the exclusion is a real early return, not a call
264 /// into `cast_and_dequant` that happens to survive on this input.
265 #[test]
266 fn q8_1_declines_with_a_typed_error_not_a_panic() {
267 let bytes = vec![0_u8; 4096];
268 let err = dequant_ggml_bytes(&bytes, GgmlDType::Q8_1, "t").unwrap_err();
269 assert!(err.to_string().contains("GAP-125"));
270 }
271
272 /// Sabotage: confirm the K-quant arms actually call `to_float` and are not accidentally
273 /// dead code returning the same output as a different variant. Q4_0 (legacy, scale+offset
274 /// per block) and Q6_K (k-quant, superblock scales) must NOT dequantize an identical
275 /// non-zero byte pattern to the same values -- that would mean one of the two match arms
276 /// is not reaching its own type's `to_float`.
277 #[test]
278 fn distinct_dtypes_on_the_same_bytes_diverge() {
279 let bytes = vec![0xAB_u8; 4096];
280 let q4_0 = dequant_ggml_bytes(&bytes, GgmlDType::Q4_0, "t").unwrap();
281 let q6_k = dequant_ggml_bytes(&bytes, GgmlDType::Q6K, "t").unwrap();
282 assert_ne!(
283 q4_0.len(),
284 q6_k.len(),
285 "Q4_0 and Q6_K have different block sizes; equal output lengths would mean one \
286 arm is reaching the wrong to_float",
287 );
288 }
289}