ferrox_vulkan/
q8_0_reference.rs1use crate::q8_0_shader::{BLOCK_BYTES, BLOCK_ELEMS};
34
35pub fn pack_words(bytes: &[u8]) -> Vec<u32> {
43 let mut words = Vec::with_capacity(bytes.len().div_ceil(4));
44 for chunk in bytes.chunks(4) {
45 let mut w = [0u8; 4];
46 w[..chunk.len()].copy_from_slice(chunk);
47 words.push(u32::from_le_bytes(w));
48 }
49 words
50}
51
52#[inline]
54fn weight_byte(words: &[u32], k: usize) -> u32 {
55 (words[k >> 2] >> ((k & 3) * 8)) & 0xff
56}
57
58pub fn f16_to_f32(h: u32) -> f32 {
64 let sign = h >> 15;
65 let exp = (h >> 10) & 0x1f;
66 let mant = h & 0x3ff;
67 let mant_hi = mant << 13;
68
69 let normal = f32::from_bits(((exp + 112) << 23) | mant_hi);
70 let subnormal = mant as f32 * f32::from_bits(0x3380_0000);
71 let inf_or_nan = f32::from_bits(0x7f80_0000 | mant_hi);
72
73 let magnitude = if exp == 0 {
74 subnormal
75 } else if exp == 31 {
76 inf_or_nan
77 } else {
78 normal
79 };
80 if sign != 0 {
81 -magnitude
82 } else {
83 magnitude
84 }
85}
86
87pub fn matvec_reference(
96 weight_words: &[u32],
97 x: &[f32],
98 rows: usize,
99 row_bytes: usize,
100 n_blocks_per_row: usize,
101) -> Vec<f32> {
102 let mut out = vec![0f32; rows];
103 for (row, y) in out.iter_mut().enumerate() {
104 let row_base = row * row_bytes;
108 let mut acc = 0f32;
109 for b in 0..n_blocks_per_row {
110 let off = row_base + b * BLOCK_BYTES;
111 let lo = weight_byte(weight_words, off);
112 let hi = weight_byte(weight_words, off + 1);
113 let scale = f16_to_f32(lo | (hi << 8));
114 let x_base = b * BLOCK_ELEMS;
115 let q_base = off + 2;
116 for j in 0..BLOCK_ELEMS {
117 let q_byte = weight_byte(weight_words, q_base + j);
118 let biased = (q_byte ^ 128).wrapping_sub(128);
120 let q = (biased as i32) as f32;
121 acc += scale * q * x[x_base + j];
122 }
123 }
124 *y = acc;
125 }
126 out
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 fn pseudo_random(seed: u64, n: usize) -> Vec<f32> {
135 let mut s = seed | 1;
136 (0..n)
137 .map(|_| {
138 s = s
139 .wrapping_mul(6364136223846793005)
140 .wrapping_add(1442695040888963407);
141 ((s >> 33) as u32 as f32 / u32::MAX as f32) * 2.0 - 1.0
142 })
143 .collect()
144 }
145
146 fn build_rows(rows: usize, cols: usize, seed: u64) -> (Vec<u8>, Vec<Vec<f32>>) {
147 let mut bytes = Vec::new();
148 let mut dense = Vec::new();
149 for r in 0..rows {
150 let row = pseudo_random(seed + r as u64 * 7919, cols);
151 bytes.extend(ferrox_quant::quantize_q8_0(&row));
152 dense.push(row);
153 }
154 (bytes, dense)
155 }
156
157 #[test]
158 fn f16_decode_matches_half_crate_on_every_bit_pattern() {
159 let mut checked = 0u32;
160 for bits in 0..=u16::MAX {
161 let want = half::f16::from_bits(bits).to_f32();
162 let got = f16_to_f32(bits as u32);
163 if want.is_nan() {
164 assert!(got.is_nan(), "0x{bits:04x}: expected NaN, got {got}");
165 } else {
166 assert_eq!(
167 got.to_bits(),
168 want.to_bits(),
169 "0x{bits:04x}: {got} != {want}"
170 );
171 }
172 checked += 1;
173 }
174 assert_eq!(checked, 65_536);
175 }
176
177 #[test]
178 fn pack_words_zero_pads_a_partial_tail() {
179 assert_eq!(pack_words(&[1, 2, 3, 4]), vec![0x0403_0201]);
180 assert_eq!(pack_words(&[1, 2, 3]), vec![0x0003_0201]);
181 assert_eq!(pack_words(&[]), Vec::<u32>::new());
182 assert_eq!(pack_words(&[0u8; BLOCK_BYTES]).len(), 9);
184 }
185
186 #[test]
191 fn reference_matches_ferrox_quant_dequant_then_dot() {
192 for (rows, blocks) in [(1usize, 1usize), (5, 3), (64, 8), (7, 2)] {
193 let cols = blocks * BLOCK_ELEMS;
194 let (bytes, _) = build_rows(rows, cols, 0xfe11 + rows as u64);
195 let row_bytes = blocks * BLOCK_BYTES;
196 assert_eq!(bytes.len(), rows * row_bytes);
197 let x = pseudo_random(0xa5a5, cols);
198 let got = matvec_reference(&pack_words(&bytes), &x, rows, row_bytes, blocks);
199
200 for (r, g) in got.iter().enumerate() {
201 let dequantized =
202 ferrox_quant::dequant_q8_0(&bytes[r * row_bytes..(r + 1) * row_bytes]).unwrap();
203 let want: f32 = dequantized
204 .iter()
205 .zip(&x)
206 .fold(0f32, |acc, (w, xv)| acc + w * xv);
207 assert_eq!(
208 g.to_bits(),
209 want.to_bits(),
210 "rows={rows} blocks={blocks} row={r}: {g} != {want}"
211 );
212 }
213 }
214 }
215
216 #[test]
222 fn reference_is_correct_when_rows_are_not_word_aligned() {
223 let blocks = 3;
224 let cols = blocks * BLOCK_ELEMS;
225 let row_bytes = blocks * BLOCK_BYTES;
226 assert_eq!(row_bytes % 4, 2, "this test needs a misaligned row stride");
227 let rows = 9;
228 let (bytes, _) = build_rows(rows, cols, 0x0dd0);
229 let x = pseudo_random(0x1234, cols);
230 let got = matvec_reference(&pack_words(&bytes), &x, rows, row_bytes, blocks);
231 for (r, g) in got.iter().enumerate() {
232 let dequantized =
233 ferrox_quant::dequant_q8_0(&bytes[r * row_bytes..(r + 1) * row_bytes]).unwrap();
234 let want: f32 = dequantized
235 .iter()
236 .zip(&x)
237 .fold(0f32, |acc, (w, xv)| acc + w * xv);
238 assert_eq!(g.to_bits(), want.to_bits(), "row {r}");
239 }
240 }
241
242 #[test]
243 fn sign_extension_covers_the_whole_int8_range() {
244 for v in 0..=255u32 {
245 let got = ((v ^ 128).wrapping_sub(128) as i32) as f32;
246 let want = (v as u8 as i8) as f32;
247 assert_eq!(got, want, "byte {v}");
248 }
249 }
250}