/home/noah/src/trueno/src/backends/q4k/mod.rs
Line | Count | Source |
1 | | //! Fused Q4_K Matrix-Vector Multiply (F-GPU-130) |
2 | | //! |
3 | | //! This module implements fused quantized matrix-vector multiplication that operates |
4 | | //! directly on Q4_K compressed weights without full dequantization. |
5 | | //! |
6 | | //! # Q4_K Format (llama.cpp compatible) |
7 | | //! |
8 | | //! Super-block layout (144 bytes per 256 elements): |
9 | | //! - `d`: 2 bytes (f16 global scale) |
10 | | //! - `dmin`: 2 bytes (f16 global min scale) |
11 | | //! - `scales`: 12 bytes (packed 6-bit scales and mins for 8 sub-blocks) |
12 | | //! - `qs`: 128 bytes (4-bit quantized values, interleaved low/high nibbles) |
13 | | //! |
14 | | //! # Golden Test Invariant (Section 12.4 of spec) |
15 | | //! |
16 | | //! For all Q4K weight W and input x: |
17 | | //! ```text |
18 | | //! matmul_q4k_f32(W, x) ≈ matmul(dequant_q4k_to_f32(W), x) within ε = 1e-3 |
19 | | //! ``` |
20 | | //! |
21 | | //! # Performance Targets |
22 | | //! |
23 | | //! - Baseline (dequant+matmul): 0.27 tok/s |
24 | | //! - Target (fused): >5 tok/s CPU, >100 tok/s GPU |
25 | | //! |
26 | | //! # Example |
27 | | //! |
28 | | //! ```rust,ignore |
29 | | //! use trueno::backends::q4k::matmul_q4k_f32; |
30 | | //! |
31 | | //! let q4k_weights = load_q4k_tensor("gate_proj.weight"); |
32 | | //! let input = vec![1.0f32; 896]; |
33 | | //! let output = matmul_q4k_f32(&q4k_weights, &input, 4864, 896); |
34 | | //! ``` |
35 | | |
36 | | #![allow(dead_code)] |
37 | | |
38 | | // Sub-modules |
39 | | mod colmajor; |
40 | | mod dequant; |
41 | | mod gemv; |
42 | | |
43 | | // Re-exports |
44 | | pub use colmajor::{matmul_q4k_f32_colmajor, matmul_q4k_f32_colmajor_dispatch}; |
45 | | pub use dequant::dequantize_q4k_to_f32; |
46 | | pub use gemv::{matmul_q4k_f32, matmul_q4k_f32_dispatch, matmul_q4k_f32_scalar}; |
47 | | |
48 | | // Constants (pub(crate) for submodule access) |
49 | | pub(crate) const SUPER_BLOCK_SIZE: usize = 256; |
50 | | pub(crate) const SUPER_BLOCK_BYTES: usize = 144; |
51 | | #[allow(dead_code)] // Reserved for future sub-block optimizations |
52 | | pub(crate) const SUB_BLOCK_SIZE: usize = 32; |
53 | | |
54 | | /// Convert f16 bits to f32 |
55 | | #[inline(always)] |
56 | 0 | fn f16_to_f32(bits: u16) -> f32 { |
57 | 0 | let sign = ((bits & 0x8000) as u32) << 16; |
58 | 0 | let exp = (bits >> 10) & 0x1F; |
59 | 0 | let mantissa = (bits & 0x3FF) as u32; |
60 | | |
61 | 0 | if exp == 0 { |
62 | 0 | if mantissa == 0 { |
63 | 0 | f32::from_bits(sign) |
64 | | } else { |
65 | | // Subnormal |
66 | 0 | let mut m = mantissa; |
67 | 0 | let mut e = 0i32; |
68 | 0 | while (m & 0x400) == 0 { |
69 | 0 | m <<= 1; |
70 | 0 | e -= 1; |
71 | 0 | } |
72 | 0 | let new_exp = ((127 - 15 + 1 + e) as u32) << 23; |
73 | 0 | let new_mantissa = (m & 0x3FF) << 13; |
74 | 0 | f32::from_bits(sign | new_exp | new_mantissa) |
75 | | } |
76 | 0 | } else if exp == 31 { |
77 | 0 | f32::from_bits(sign | (0xFF << 23) | (mantissa << 13)) |
78 | | } else { |
79 | 0 | let new_exp = ((exp as i32 - 15 + 127) as u32) << 23; |
80 | 0 | f32::from_bits(sign | new_exp | (mantissa << 13)) |
81 | | } |
82 | 0 | } |
83 | | |
84 | | /// Parse Q4_K super-block header and scales |
85 | | /// |
86 | | /// Returns (d, dmin, scales[8], mins[8]) |
87 | | #[inline(always)] |
88 | 0 | pub(crate) fn parse_q4k_header(block: &[u8]) -> (f32, f32, [u8; 8], [u8; 8]) { |
89 | 0 | debug_assert!(block.len() >= 16); |
90 | | |
91 | | // Read d and dmin (f16) |
92 | 0 | let d = f16_to_f32(u16::from_le_bytes([block[0], block[1]])); |
93 | 0 | let dmin = f16_to_f32(u16::from_le_bytes([block[2], block[3]])); |
94 | | |
95 | | // Unpack scales and mins (llama.cpp format) |
96 | 0 | let scales_bytes = &block[4..16]; |
97 | 0 | let mut scales = [0u8; 8]; |
98 | 0 | let mut mins = [0u8; 8]; |
99 | | |
100 | 0 | for i in 0..4 { |
101 | 0 | // Blocks 0-3: lower 6 bits of bytes 0-3 and 4-7 |
102 | 0 | scales[i] = scales_bytes[i] & 0x3F; |
103 | 0 | mins[i] = scales_bytes[i + 4] & 0x3F; |
104 | 0 | // Blocks 4-7: lower 4 bits from bytes 8-11, upper 2 bits from bytes 0-3/4-7 |
105 | 0 | scales[i + 4] = (scales_bytes[i + 8] & 0x0F) | ((scales_bytes[i] >> 6) << 4); |
106 | 0 | mins[i + 4] = (scales_bytes[i + 8] >> 4) | ((scales_bytes[i + 4] >> 6) << 4); |
107 | 0 | } |
108 | | |
109 | 0 | (d, dmin, scales, mins) |
110 | 0 | } |
111 | | |
112 | | #[cfg(test)] |
113 | | mod tests { |
114 | | use super::gemv::compute_chunk_q4k_scalar; |
115 | | use super::*; |
116 | | |
117 | | /// Golden Test: Fused kernel must match dequant+matmul within ε = 1e-3 |
118 | | /// This is the core falsification test from Section 12.4 of the spec. |
119 | | #[test] |
120 | | fn test_fused_q4k_golden_parity() { |
121 | | // Create synthetic Q4K data (one super-block = 256 elements) |
122 | | let in_dim = 256; |
123 | | let out_dim = 4; |
124 | | let num_blocks = 1; |
125 | | |
126 | | // Build Q4K test data |
127 | | let mut q4k_data = Vec::with_capacity(out_dim * num_blocks * SUPER_BLOCK_BYTES); |
128 | | |
129 | | for row in 0..out_dim { |
130 | | // d = 0.1, dmin = 0.05 (as f16) |
131 | | let d: u16 = 0x2E66; // ~0.1 in f16 |
132 | | let dmin: u16 = 0x2A66; // ~0.05 in f16 |
133 | | q4k_data.extend_from_slice(&d.to_le_bytes()); |
134 | | q4k_data.extend_from_slice(&dmin.to_le_bytes()); |
135 | | |
136 | | // Scales and mins (all set to 1 for simplicity) |
137 | | let scales_packed = [0x01u8; 12]; |
138 | | q4k_data.extend_from_slice(&scales_packed); |
139 | | |
140 | | // Quantized values: pattern based on row |
141 | | let mut qs = [0u8; 128]; |
142 | | for (i, q) in qs.iter_mut().enumerate() { |
143 | | // Low nibble: (row + i) % 16, High nibble: (row + i + 1) % 16 |
144 | | let low = ((row + i) % 16) as u8; |
145 | | let high = ((row + i + 1) % 16) as u8; |
146 | | *q = low | (high << 4); |
147 | | } |
148 | | q4k_data.extend_from_slice(&qs); |
149 | | } |
150 | | |
151 | | // Create input vector |
152 | | let input: Vec<f32> = (0..in_dim).map(|i| (i as f32) * 0.01).collect(); |
153 | | |
154 | | // Compute using fused kernel |
155 | | let fused_output = matmul_q4k_f32(&q4k_data, &input, out_dim, in_dim); |
156 | | |
157 | | // Compute reference: dequant then matmul |
158 | | let mut reference_output = vec![0.0f32; out_dim]; |
159 | | for row in 0..out_dim { |
160 | | let row_start = row * SUPER_BLOCK_BYTES; |
161 | | let row_q4k = &q4k_data[row_start..row_start + SUPER_BLOCK_BYTES]; |
162 | | let f32_weights = dequantize_q4k_to_f32(row_q4k, in_dim); |
163 | | |
164 | | let mut sum = 0.0f32; |
165 | | for (w, x) in f32_weights.iter().zip(input.iter()) { |
166 | | sum += w * x; |
167 | | } |
168 | | reference_output[row] = sum; |
169 | | } |
170 | | |
171 | | // Golden parity check: |fused - reference| < 1e-3 |
172 | | for (i, (fused, reference)) in fused_output.iter().zip(reference_output.iter()).enumerate() |
173 | | { |
174 | | let diff = (fused - reference).abs(); |
175 | | assert!( |
176 | | diff < 1e-3, |
177 | | "Row {}: Fused kernel divergence: {} vs {} (Δ={})", |
178 | | i, |
179 | | fused, |
180 | | reference, |
181 | | diff |
182 | | ); |
183 | | } |
184 | | } |
185 | | |
186 | | /// Test scalar implementation matches optimized version |
187 | | #[test] |
188 | | fn test_scalar_vs_optimized_parity() { |
189 | | let in_dim = 256; |
190 | | let out_dim = 2; |
191 | | |
192 | | // Build simple Q4K test data |
193 | | let mut q4k_data = Vec::new(); |
194 | | for _ in 0..out_dim { |
195 | | q4k_data.extend_from_slice(&[0x00, 0x3C]); // d ~ 1.0 |
196 | | q4k_data.extend_from_slice(&[0x00, 0x00]); // dmin = 0 |
197 | | q4k_data.extend_from_slice(&[0x01u8; 12]); // scales |
198 | | q4k_data.extend_from_slice(&[0x55u8; 128]); // qs = 5 | (5 << 4) |
199 | | } |
200 | | |
201 | | let input: Vec<f32> = (0..in_dim).map(|i| (i as f32) * 0.001).collect(); |
202 | | |
203 | | let scalar_output = matmul_q4k_f32_scalar(&q4k_data, &input, out_dim, in_dim); |
204 | | let optimized_output = matmul_q4k_f32(&q4k_data, &input, out_dim, in_dim); |
205 | | |
206 | | for (i, (s, o)) in scalar_output.iter().zip(optimized_output.iter()).enumerate() { |
207 | | let diff = (s - o).abs(); |
208 | | // Allow small FP differences from mul_add vs separate multiply-add |
209 | | assert!( |
210 | | diff < 1e-4, |
211 | | "Row {}: Scalar vs optimized divergence: {} vs {} (Δ={})", |
212 | | i, |
213 | | s, |
214 | | o, |
215 | | diff |
216 | | ); |
217 | | } |
218 | | } |
219 | | |
220 | | /// Test that output contains no NaN or Inf |
221 | | #[test] |
222 | | fn test_no_nan_inf() { |
223 | | let in_dim = 256; |
224 | | let out_dim = 4; |
225 | | |
226 | | let mut q4k_data = Vec::new(); |
227 | | for _ in 0..out_dim { |
228 | | q4k_data.extend_from_slice(&[0x00, 0x3C]); // d ~ 1.0 |
229 | | q4k_data.extend_from_slice(&[0x00, 0x38]); // dmin ~ 0.5 |
230 | | q4k_data.extend_from_slice(&[0x3Fu8; 12]); // max scales |
231 | | q4k_data.extend_from_slice(&[0xFFu8; 128]); // max qs |
232 | | } |
233 | | |
234 | | let input: Vec<f32> = (0..in_dim).map(|i| (i as f32) * 0.01).collect(); |
235 | | let output = matmul_q4k_f32(&q4k_data, &input, out_dim, in_dim); |
236 | | |
237 | | for (i, &val) in output.iter().enumerate() { |
238 | | assert!(val.is_finite(), "Row {}: Output is not finite: {}", i, val); |
239 | | } |
240 | | } |
241 | | |
242 | | /// Test AVX2 implementation matches scalar within tolerance |
243 | | #[cfg(target_arch = "x86_64")] |
244 | | #[test] |
245 | | fn test_avx2_vs_scalar_parity() { |
246 | | if !is_x86_feature_detected!("avx2") || !is_x86_feature_detected!("fma") { |
247 | | eprintln!("Skipping AVX2 test - CPU doesn't support AVX2+FMA"); |
248 | | return; |
249 | | } |
250 | | |
251 | | let in_dim = 512; // 2 super-blocks |
252 | | let out_dim = 4; |
253 | | |
254 | | // Build Q4K test data with varied values |
255 | | let mut q4k_data = Vec::new(); |
256 | | for row in 0..out_dim { |
257 | | // d ~ 0.1, dmin ~ 0.05 |
258 | | q4k_data.extend_from_slice(&[0x66, 0x2E]); // d |
259 | | q4k_data.extend_from_slice(&[0x66, 0x2A]); // dmin |
260 | | // Varied scales |
261 | | let scale_val = (row as u8 + 1) | ((row as u8 + 2) << 4); |
262 | | q4k_data.extend_from_slice(&[scale_val; 12]); |
263 | | // Varied quantized values |
264 | | for i in 0..128 { |
265 | | let low = ((row + i) % 16) as u8; |
266 | | let high = ((row + i + 3) % 16) as u8; |
267 | | q4k_data.push(low | (high << 4)); |
268 | | } |
269 | | } |
270 | | // Duplicate for second super-block |
271 | | let single_row_bytes = q4k_data.len() / out_dim; |
272 | | let mut full_data = Vec::with_capacity(out_dim * single_row_bytes * 2); |
273 | | for row in 0..out_dim { |
274 | | let row_start = row * single_row_bytes; |
275 | | full_data.extend_from_slice(&q4k_data[row_start..row_start + single_row_bytes]); |
276 | | full_data.extend_from_slice(&q4k_data[row_start..row_start + single_row_bytes]); |
277 | | } |
278 | | |
279 | | let input: Vec<f32> = (0..in_dim).map(|i| (i as f32) * 0.002 - 0.5).collect(); |
280 | | |
281 | | let scalar_output = matmul_q4k_f32(&full_data, &input, out_dim, in_dim); |
282 | | let dispatch_output = matmul_q4k_f32_dispatch(&full_data, &input, out_dim, in_dim); |
283 | | |
284 | | for (i, (scalar, dispatch)) in scalar_output.iter().zip(dispatch_output.iter()).enumerate() { |
285 | | let diff = (scalar - dispatch).abs(); |
286 | | let rel_diff = if scalar.abs() > 1e-6 { |
287 | | diff / scalar.abs() |
288 | | } else { |
289 | | diff |
290 | | }; |
291 | | // Allow 1e-5 relative error for FMA differences |
292 | | assert!( |
293 | | rel_diff < 1e-5 || diff < 1e-5, |
294 | | "Row {}: AVX2 vs scalar divergence: {} vs {} (Δ={}, rel={})", |
295 | | i, |
296 | | dispatch, |
297 | | scalar, |
298 | | diff, |
299 | | rel_diff |
300 | | ); |
301 | | } |
302 | | } |
303 | | |
304 | | /// Test determinism: same input produces same output |
305 | | #[test] |
306 | | fn test_determinism() { |
307 | | let in_dim = 256; |
308 | | let out_dim = 2; |
309 | | |
310 | | let mut q4k_data = Vec::new(); |
311 | | for _ in 0..out_dim { |
312 | | q4k_data.extend_from_slice(&[0x66, 0x2E]); // d |
313 | | q4k_data.extend_from_slice(&[0x66, 0x2A]); // dmin |
314 | | q4k_data.extend_from_slice(&[0x15u8; 12]); |
315 | | q4k_data.extend_from_slice(&[0xABu8; 128]); |
316 | | } |
317 | | |
318 | | let input: Vec<f32> = (0..in_dim).map(|i| (i as f32) * 0.005).collect(); |
319 | | |
320 | | let output1 = matmul_q4k_f32(&q4k_data, &input, out_dim, in_dim); |
321 | | let output2 = matmul_q4k_f32(&q4k_data, &input, out_dim, in_dim); |
322 | | |
323 | | for (i, (a, b)) in output1.iter().zip(output2.iter()).enumerate() { |
324 | | assert_eq!( |
325 | | a.to_bits(), |
326 | | b.to_bits(), |
327 | | "Row {}: Non-deterministic output: {} vs {}", |
328 | | i, |
329 | | a, |
330 | | b |
331 | | ); |
332 | | } |
333 | | } |
334 | | |
335 | | /// Test f16 conversion correctness |
336 | | #[test] |
337 | | fn test_f16_to_f32() { |
338 | | // Test normal values |
339 | | assert!((f16_to_f32(0x3C00) - 1.0).abs() < 1e-3); // 1.0 |
340 | | assert!((f16_to_f32(0x4000) - 2.0).abs() < 1e-3); // 2.0 |
341 | | assert!((f16_to_f32(0x3800) - 0.5).abs() < 1e-3); // 0.5 |
342 | | |
343 | | // Test zero |
344 | | assert_eq!(f16_to_f32(0x0000), 0.0); |
345 | | assert_eq!(f16_to_f32(0x8000), -0.0); |
346 | | |
347 | | // Test subnormals (small values) |
348 | | let small = f16_to_f32(0x0001); |
349 | | assert!(small > 0.0 && small < 1e-4); |
350 | | } |
351 | | |
352 | | #[test] |
353 | | fn test_f16_to_f32_infinity_nan() { |
354 | | // Positive infinity = 0x7C00 |
355 | | let inf = f16_to_f32(0x7C00); |
356 | | assert!(inf.is_infinite() && inf.is_sign_positive()); |
357 | | |
358 | | // Negative infinity = 0xFC00 |
359 | | let neg_inf = f16_to_f32(0xFC00); |
360 | | assert!(neg_inf.is_infinite() && neg_inf.is_sign_negative()); |
361 | | |
362 | | // Negative value |
363 | | let neg_one = f16_to_f32(0xBC00); // -1.0 |
364 | | assert!((neg_one + 1.0).abs() < 1e-3); |
365 | | } |
366 | | |
367 | | #[test] |
368 | | fn test_dequantize_q4k_to_f32_basic() { |
369 | | // Create a single Q4K block (144 bytes for 256 elements) |
370 | | let mut block = vec![0u8; SUPER_BLOCK_BYTES]; |
371 | | // d = 1.0 (0x3C00) |
372 | | block[0] = 0x00; |
373 | | block[1] = 0x3C; |
374 | | // dmin = 0 (0x0000) |
375 | | block[2] = 0x00; |
376 | | block[3] = 0x00; |
377 | | // scales = all zeros |
378 | | block[4..16].fill(0x00); |
379 | | // qs = 0x55 (5 | 5<<4) for all values |
380 | | block[16..144].fill(0x55); |
381 | | |
382 | | let result = dequantize_q4k_to_f32(&block, 256); |
383 | | assert_eq!(result.len(), 256); |
384 | | |
385 | | // All values should be finite |
386 | | for val in &result { |
387 | | assert!(val.is_finite()); |
388 | | } |
389 | | } |
390 | | |
391 | | #[test] |
392 | | fn test_dequantize_q4k_to_f32_varies_scales() { |
393 | | let mut block = vec![0u8; SUPER_BLOCK_BYTES]; |
394 | | block[0] = 0x00; |
395 | | block[1] = 0x3C; // d = 1.0 |
396 | | block[2] = 0x00; |
397 | | block[3] = 0x00; // dmin = 0 |
398 | | |
399 | | // Set different scales for each group |
400 | | for i in 0..12 { |
401 | | block[4 + i] = (i * 10) as u8; |
402 | | } |
403 | | |
404 | | // Set quantized values |
405 | | block[16..144].fill(0x33); // 3 | 3<<4 |
406 | | |
407 | | let result = dequantize_q4k_to_f32(&block, 256); |
408 | | assert_eq!(result.len(), 256); |
409 | | for val in &result { |
410 | | assert!(val.is_finite()); |
411 | | } |
412 | | } |
413 | | |
414 | | #[test] |
415 | | fn test_matmul_q4k_f32_colmajor_basic() { |
416 | | let in_dim = 256; |
417 | | let out_dim = 2; |
418 | | |
419 | | let mut q4k_data = Vec::new(); |
420 | | for _ in 0..out_dim { |
421 | | q4k_data.extend_from_slice(&[0x00, 0x3C]); // d ~ 1.0 |
422 | | q4k_data.extend_from_slice(&[0x00, 0x00]); // dmin = 0 |
423 | | q4k_data.extend_from_slice(&[0x01u8; 12]); // scales |
424 | | q4k_data.extend_from_slice(&[0x55u8; 128]); // qs |
425 | | } |
426 | | |
427 | | let input: Vec<f32> = (0..in_dim).map(|i| (i as f32) * 0.01).collect(); |
428 | | let output = matmul_q4k_f32_colmajor(&q4k_data, &input, out_dim, in_dim); |
429 | | |
430 | | assert_eq!(output.len(), out_dim); |
431 | | for val in &output { |
432 | | assert!(val.is_finite()); |
433 | | } |
434 | | } |
435 | | |
436 | | #[test] |
437 | | fn test_matmul_q4k_f32_colmajor_dispatch_basic() { |
438 | | let in_dim = 256; |
439 | | let out_dim = 4; |
440 | | |
441 | | let mut q4k_data = Vec::new(); |
442 | | for row in 0..out_dim { |
443 | | q4k_data.extend_from_slice(&[0x66, 0x2E]); // d ~ 0.1 |
444 | | q4k_data.extend_from_slice(&[0x00, 0x00]); // dmin = 0 |
445 | | q4k_data.extend_from_slice(&[(row as u8 + 1); 12]); // varying scales |
446 | | q4k_data.extend_from_slice(&[(row as u8 * 17).wrapping_add(0x44); 128]); // varying qs |
447 | | } |
448 | | |
449 | | let input: Vec<f32> = (0..in_dim).map(|i| (i as f32) * 0.01 - 1.0).collect(); |
450 | | let output = matmul_q4k_f32_colmajor_dispatch(&q4k_data, &input, out_dim, in_dim); |
451 | | |
452 | | assert_eq!(output.len(), out_dim); |
453 | | for val in &output { |
454 | | assert!(val.is_finite()); |
455 | | } |
456 | | } |
457 | | |
458 | | #[test] |
459 | | fn test_matmul_q4k_colmajor_produces_finite() { |
460 | | // Column-major layout test: verify it produces valid finite outputs |
461 | | // Note: colmajor and rowmajor have different data layout assumptions |
462 | | let in_dim = 256; |
463 | | let out_dim = 2; |
464 | | |
465 | | let mut q4k_data = Vec::new(); |
466 | | for _ in 0..out_dim { |
467 | | q4k_data.extend_from_slice(&[0x00, 0x3C]); // d ~ 1.0 |
468 | | q4k_data.extend_from_slice(&[0x00, 0x38]); // dmin ~ 0.5 |
469 | | q4k_data.extend_from_slice(&[0x01u8; 12]); |
470 | | q4k_data.extend_from_slice(&[0x55u8; 128]); |
471 | | } |
472 | | |
473 | | let input: Vec<f32> = (0..in_dim).map(|i| (i as f32) * 0.005).collect(); |
474 | | |
475 | | let rowmajor = matmul_q4k_f32(&q4k_data, &input, out_dim, in_dim); |
476 | | let colmajor = matmul_q4k_f32_colmajor(&q4k_data, &input, out_dim, in_dim); |
477 | | |
478 | | // Both should produce finite results |
479 | | for (i, r) in rowmajor.iter().enumerate() { |
480 | | assert!(r.is_finite(), "Row {}: rowmajor non-finite", i); |
481 | | } |
482 | | for (i, c) in colmajor.iter().enumerate() { |
483 | | assert!(c.is_finite(), "Row {}: colmajor non-finite", i); |
484 | | } |
485 | | } |
486 | | |
487 | | #[test] |
488 | | fn test_matmul_q4k_unaligned_dimensions() { |
489 | | // Test with dimensions not aligned to block size (256) |
490 | | let in_dim = 300; |
491 | | let out_dim = 3; |
492 | | let num_blocks = (in_dim + 255) / 256; // = 2 blocks |
493 | | |
494 | | let mut q4k_data = Vec::new(); |
495 | | for _ in 0..out_dim { |
496 | | for _ in 0..num_blocks { |
497 | | q4k_data.extend_from_slice(&[0x00, 0x3C]); // d |
498 | | q4k_data.extend_from_slice(&[0x00, 0x00]); // dmin |
499 | | q4k_data.extend_from_slice(&[0x01u8; 12]); // scales |
500 | | q4k_data.extend_from_slice(&[0x33u8; 128]); // qs |
501 | | } |
502 | | } |
503 | | |
504 | | let input: Vec<f32> = (0..in_dim).map(|i| (i as f32) * 0.01).collect(); |
505 | | let output = matmul_q4k_f32(&q4k_data, &input, out_dim, in_dim); |
506 | | |
507 | | assert_eq!(output.len(), out_dim); |
508 | | for val in &output { |
509 | | assert!(val.is_finite()); |
510 | | } |
511 | | } |
512 | | |
513 | | #[test] |
514 | | fn test_matmul_q4k_zero_input() { |
515 | | let in_dim = 256; |
516 | | let out_dim = 2; |
517 | | |
518 | | let mut q4k_data = Vec::new(); |
519 | | for _ in 0..out_dim { |
520 | | q4k_data.extend_from_slice(&[0x00, 0x3C]); // d ~ 1.0 |
521 | | q4k_data.extend_from_slice(&[0x00, 0x38]); // dmin ~ 0.5 |
522 | | q4k_data.extend_from_slice(&[0x7Fu8; 12]); // max scales |
523 | | q4k_data.extend_from_slice(&[0xFFu8; 128]); // max qs |
524 | | } |
525 | | |
526 | | let input: Vec<f32> = vec![0.0; in_dim]; |
527 | | let output = matmul_q4k_f32(&q4k_data, &input, out_dim, in_dim); |
528 | | |
529 | | assert_eq!(output.len(), out_dim); |
530 | | for val in &output { |
531 | | assert_eq!(*val, 0.0, "Output should be zero when input is zero"); |
532 | | } |
533 | | } |
534 | | |
535 | | #[test] |
536 | | fn test_matmul_q4k_large_dimensions() { |
537 | | let in_dim = 1024; |
538 | | let out_dim = 8; |
539 | | let num_blocks = in_dim / 256; |
540 | | |
541 | | let mut q4k_data = Vec::new(); |
542 | | for row in 0..out_dim { |
543 | | for blk in 0..num_blocks { |
544 | | let val = ((row * num_blocks + blk) as u8).wrapping_mul(17); |
545 | | q4k_data.extend_from_slice(&[0x66, 0x2E]); // d ~ 0.1 |
546 | | q4k_data.extend_from_slice(&[0x33, 0x2A]); // dmin ~ 0.05 |
547 | | q4k_data.extend_from_slice(&[(val.wrapping_add(1)); 12]); |
548 | | q4k_data.extend_from_slice(&[val.wrapping_add(0x55); 128]); |
549 | | } |
550 | | } |
551 | | |
552 | | let input: Vec<f32> = (0..in_dim).map(|i| ((i % 100) as f32) * 0.01).collect(); |
553 | | let output = matmul_q4k_f32(&q4k_data, &input, out_dim, in_dim); |
554 | | |
555 | | assert_eq!(output.len(), out_dim); |
556 | | for val in &output { |
557 | | assert!(val.is_finite()); |
558 | | } |
559 | | } |
560 | | |
561 | | #[test] |
562 | | fn test_parse_q4k_header() { |
563 | | let mut block = vec![0u8; 144]; |
564 | | // d = 1.0 (0x3C00), dmin = 0.5 (0x3800) |
565 | | block[0] = 0x00; |
566 | | block[1] = 0x3C; |
567 | | block[2] = 0x00; |
568 | | block[3] = 0x38; |
569 | | // scales_bytes[0..12] for llama.cpp format |
570 | | // bytes 0-3: lower 6 bits = scales[0-3], upper 2 bits = scales[4-7] upper bits |
571 | | // bytes 4-7: lower 6 bits = mins[0-3], upper 2 bits = mins[4-7] upper bits |
572 | | // bytes 8-11: lower 4 bits = scales[4-7] lower bits, upper 4 bits = mins[4-7] lower bits |
573 | | block[4..8].copy_from_slice(&[0x01, 0x02, 0x03, 0x04]); // scales[0-3] = 1,2,3,4 |
574 | | block[8..12].copy_from_slice(&[0x0A, 0x0B, 0x0C, 0x0D]); // mins[0-3] = 10,11,12,13 |
575 | | block[12..16].copy_from_slice(&[0x55, 0x66, 0x77, 0x88]); // combined lower nibbles |
576 | | |
577 | | let (d, dmin, scales, mins) = parse_q4k_header(&block); |
578 | | |
579 | | assert!((d - 1.0).abs() < 0.01, "d should be ~1.0, got {}", d); |
580 | | assert!((dmin - 0.5).abs() < 0.01, "dmin should be ~0.5, got {}", dmin); |
581 | | // Check first scales/mins have expected low 6-bit values |
582 | | assert_eq!(scales[0], 0x01, "scales[0] should be 1"); |
583 | | assert_eq!(scales[1], 0x02, "scales[1] should be 2"); |
584 | | assert_eq!(mins[0], 0x0A, "mins[0] should be 10"); |
585 | | assert_eq!(mins[1], 0x0B, "mins[1] should be 11"); |
586 | | } |
587 | | |
588 | | #[test] |
589 | | fn test_matmul_q4k_single_row() { |
590 | | let in_dim = 256; |
591 | | let out_dim = 1; |
592 | | |
593 | | let mut q4k_data = Vec::new(); |
594 | | q4k_data.extend_from_slice(&[0x00, 0x3C]); // d ~ 1.0 |
595 | | q4k_data.extend_from_slice(&[0x00, 0x00]); // dmin = 0 |
596 | | q4k_data.extend_from_slice(&[0x01u8; 12]); // scales |
597 | | q4k_data.extend_from_slice(&[0xAAu8; 128]); // qs |
598 | | |
599 | | let input: Vec<f32> = vec![1.0; in_dim]; |
600 | | let output = matmul_q4k_f32(&q4k_data, &input, out_dim, in_dim); |
601 | | |
602 | | assert_eq!(output.len(), 1); |
603 | | assert!(output[0].is_finite()); |
604 | | } |
605 | | |
606 | | /// Test AVX2 matmul with large dimensions (exercises full SIMD paths) |
607 | | #[cfg(target_arch = "x86_64")] |
608 | | #[test] |
609 | | fn test_avx2_large_matrix_mul() { |
610 | | if !is_x86_feature_detected!("avx2") || !is_x86_feature_detected!("fma") { |
611 | | eprintln!("Skipping AVX2 large matrix test - CPU doesn't support AVX2+FMA"); |
612 | | return; |
613 | | } |
614 | | |
615 | | let in_dim = 4096; // 16 super-blocks |
616 | | let out_dim = 32; |
617 | | |
618 | | // Build Q4K test data with realistic values |
619 | | let mut q4k_data = Vec::new(); |
620 | | for row in 0..out_dim { |
621 | | for _sb in 0..(in_dim / 256) { |
622 | | // d ~ 0.1, dmin ~ 0.05 |
623 | | q4k_data.extend_from_slice(&[0x66, 0x2E]); // d |
624 | | q4k_data.extend_from_slice(&[0x66, 0x2A]); // dmin |
625 | | // Varied scales based on row |
626 | | let scale_val = (row as u8 % 16) | (((row + 1) as u8 % 16) << 4); |
627 | | q4k_data.extend_from_slice(&[scale_val; 12]); |
628 | | // Varied quantized values |
629 | | for i in 0..128 { |
630 | | let low = ((row + i) % 16) as u8; |
631 | | let high = ((row + i + 3) % 16) as u8; |
632 | | q4k_data.push(low | (high << 4)); |
633 | | } |
634 | | } |
635 | | } |
636 | | |
637 | | let input: Vec<f32> = (0..in_dim).map(|i| (i as f32) * 0.001 - 2.0).collect(); |
638 | | |
639 | | let scalar_output = matmul_q4k_f32(&q4k_data, &input, out_dim, in_dim); |
640 | | let dispatch_output = matmul_q4k_f32_dispatch(&q4k_data, &input, out_dim, in_dim); |
641 | | |
642 | | for (i, (scalar, dispatch)) in scalar_output.iter().zip(dispatch_output.iter()).enumerate() { |
643 | | let diff = (scalar - dispatch).abs(); |
644 | | let rel_diff = if scalar.abs() > 1e-6 { |
645 | | diff / scalar.abs() |
646 | | } else { |
647 | | diff |
648 | | }; |
649 | | assert!( |
650 | | rel_diff < 1e-4 || diff < 1e-4, |
651 | | "Row {}: AVX2 vs scalar divergence: {} vs {} (Δ={}, rel={})", |
652 | | i, dispatch, scalar, diff, rel_diff |
653 | | ); |
654 | | } |
655 | | } |
656 | | |
657 | | /// Test colmajor AVX2 path with realistic dimensions |
658 | | #[cfg(target_arch = "x86_64")] |
659 | | #[test] |
660 | | fn test_avx2_colmajor_large() { |
661 | | if !is_x86_feature_detected!("avx2") || !is_x86_feature_detected!("fma") { |
662 | | eprintln!("Skipping AVX2 colmajor test - CPU doesn't support AVX2+FMA"); |
663 | | return; |
664 | | } |
665 | | |
666 | | let in_dim = 2048; // 8 super-blocks |
667 | | let out_dim = 16; |
668 | | |
669 | | let mut q4k_data = Vec::new(); |
670 | | for row in 0..out_dim { |
671 | | for sb in 0..(in_dim / 256) { |
672 | | q4k_data.extend_from_slice(&[0x66, 0x2E]); // d |
673 | | q4k_data.extend_from_slice(&[0x33, 0x2A]); // dmin |
674 | | let scale_val = ((row + sb) as u8 % 16) | (((row + sb + 1) as u8 % 16) << 4); |
675 | | q4k_data.extend_from_slice(&[scale_val; 12]); |
676 | | for i in 0..128 { |
677 | | q4k_data.push(((i % 16) | ((i + 1) % 16 << 4)) as u8); |
678 | | } |
679 | | } |
680 | | } |
681 | | |
682 | | let input: Vec<f32> = (0..in_dim).map(|i| (i as f32) * 0.002 - 1.0).collect(); |
683 | | |
684 | | let output = matmul_q4k_f32_colmajor(&q4k_data, &input, out_dim, in_dim); |
685 | | let output_dispatch = matmul_q4k_f32_colmajor_dispatch(&q4k_data, &input, out_dim, in_dim); |
686 | | |
687 | | assert_eq!(output.len(), out_dim); |
688 | | assert_eq!(output_dispatch.len(), out_dim); |
689 | | |
690 | | for (i, (base, dispatched)) in output.iter().zip(output_dispatch.iter()).enumerate() { |
691 | | let diff = (base - dispatched).abs(); |
692 | | assert!( |
693 | | diff < 1e-3 || (diff / base.abs()) < 1e-4, |
694 | | "Row {}: colmajor mismatch: {} vs {} (diff={})", |
695 | | i, base, dispatched, diff |
696 | | ); |
697 | | } |
698 | | } |
699 | | |
700 | | /// Test non-aligned dimensions (exercises scalar remainder handling) |
701 | | #[cfg(target_arch = "x86_64")] |
702 | | #[test] |
703 | | fn test_avx2_non_aligned_dimensions() { |
704 | | if !is_x86_feature_detected!("avx2") || !is_x86_feature_detected!("fma") { |
705 | | eprintln!("Skipping AVX2 non-aligned test - CPU doesn't support AVX2+FMA"); |
706 | | return; |
707 | | } |
708 | | |
709 | | // Non-aligned: 768 = 3 super-blocks (not power of 2) |
710 | | let in_dim = 768; |
711 | | let out_dim = 7; // Odd number |
712 | | |
713 | | let mut q4k_data = Vec::new(); |
714 | | for row in 0..out_dim { |
715 | | for _sb in 0..(in_dim / 256) { |
716 | | q4k_data.extend_from_slice(&[0x66, 0x2E]); |
717 | | q4k_data.extend_from_slice(&[0x66, 0x2A]); |
718 | | let scale_val = (row as u8 % 16) | (((row + 1) as u8 % 16) << 4); |
719 | | q4k_data.extend_from_slice(&[scale_val; 12]); |
720 | | for i in 0..128 { |
721 | | q4k_data.push(((i % 16) | ((i + 5) % 16 << 4)) as u8); |
722 | | } |
723 | | } |
724 | | } |
725 | | |
726 | | let input: Vec<f32> = (0..in_dim).map(|i| ((i as f32) * 0.003).sin()).collect(); |
727 | | |
728 | | let scalar_output = matmul_q4k_f32(&q4k_data, &input, out_dim, in_dim); |
729 | | let dispatch_output = matmul_q4k_f32_dispatch(&q4k_data, &input, out_dim, in_dim); |
730 | | |
731 | | assert_eq!(scalar_output.len(), out_dim); |
732 | | assert_eq!(dispatch_output.len(), out_dim); |
733 | | |
734 | | for (i, (scalar, dispatch)) in scalar_output.iter().zip(dispatch_output.iter()).enumerate() { |
735 | | let diff = (scalar - dispatch).abs(); |
736 | | let rel_diff = if scalar.abs() > 1e-6 { |
737 | | diff / scalar.abs() |
738 | | } else { |
739 | | diff |
740 | | }; |
741 | | // FMA operations can have ordering differences, allow 1e-5 relative error |
742 | | assert!( |
743 | | rel_diff < 1e-5 || diff < 1e-2, |
744 | | "Row {}: non-aligned AVX2 mismatch: {} vs {} (diff={}, rel={})", |
745 | | i, scalar, dispatch, diff, rel_diff |
746 | | ); |
747 | | } |
748 | | } |
749 | | |
750 | | /// Test parallel SIMD execution (exercises compute_chunk_q4k_avx2) |
751 | | #[cfg(all(target_arch = "x86_64", feature = "parallel"))] |
752 | | #[test] |
753 | | fn test_parallel_avx2_large_batch() { |
754 | | if !is_x86_feature_detected!("avx2") || !is_x86_feature_detected!("fma") { |
755 | | eprintln!("Skipping parallel AVX2 test - CPU doesn't support AVX2+FMA"); |
756 | | return; |
757 | | } |
758 | | |
759 | | // Large enough to trigger parallel path (>1000 rows) |
760 | | let in_dim = 1024; |
761 | | let out_dim = 2048; // Large output dim for parallel execution |
762 | | |
763 | | let mut q4k_data = Vec::new(); |
764 | | for row in 0..out_dim { |
765 | | for _sb in 0..(in_dim / 256) { |
766 | | q4k_data.extend_from_slice(&[0x66, 0x2E]); |
767 | | q4k_data.extend_from_slice(&[0x33, 0x2A]); |
768 | | let scale_val = ((row % 256) as u8) | (((row / 256) % 16) as u8 * 16); |
769 | | q4k_data.extend_from_slice(&[scale_val; 12]); |
770 | | for i in 0..128 { |
771 | | q4k_data.push(((i * row) % 256) as u8); |
772 | | } |
773 | | } |
774 | | } |
775 | | |
776 | | let input: Vec<f32> = (0..in_dim).map(|i| (i as f32) * 0.001).collect(); |
777 | | |
778 | | let output = matmul_q4k_f32_colmajor_dispatch(&q4k_data, &input, out_dim, in_dim); |
779 | | |
780 | | assert_eq!(output.len(), out_dim); |
781 | | for (i, val) in output.iter().enumerate() { |
782 | | assert!( |
783 | | val.is_finite(), |
784 | | "Row {}: parallel AVX2 produced non-finite: {}", |
785 | | i, val |
786 | | ); |
787 | | } |
788 | | } |
789 | | |
790 | | // ========================================================================= |
791 | | // Golden Vector Tests (Section 12.4: Q4K fused matmul ≈ dequant+f32_matmul) |
792 | | // ========================================================================= |
793 | | |
794 | | /// Helper: naive f32 matrix-vector multiplication |
795 | | fn matmul_f32_naive(weights: &[f32], input: &[f32], out_dim: usize, in_dim: usize) -> Vec<f32> { |
796 | | let mut output = vec![0.0f32; out_dim]; |
797 | | for row in 0..out_dim { |
798 | | let mut sum = 0.0f32; |
799 | | for col in 0..in_dim { |
800 | | sum += weights[row * in_dim + col] * input[col]; |
801 | | } |
802 | | output[row] = sum; |
803 | | } |
804 | | output |
805 | | } |
806 | | |
807 | | /// Golden Vector Test: Q4K matmul ≈ dequant + f32 matmul |
808 | | /// |
809 | | /// This test verifies the invariant from Section 12.4: |
810 | | /// matmul_q4k_f32(W, x) ≈ matmul(dequant_q4k_to_f32(W), x) within ε |
811 | | /// |
812 | | /// Quantization introduces error, so we use a relaxed tolerance (5%). |
813 | | #[test] |
814 | | fn test_golden_vector_q4k_matmul_vs_dequant() { |
815 | | use crate::backends::q4k::dequantize_q4k_to_f32; |
816 | | |
817 | | // Realistic dimensions for LLM layers |
818 | | let in_dim = 512; // 2 super-blocks |
819 | | let out_dim = 8; |
820 | | |
821 | | // Build Q4K test data with realistic distribution |
822 | | let mut q4k_data = Vec::new(); |
823 | | for row in 0..out_dim { |
824 | | for sb in 0..(in_dim / 256) { |
825 | | // d ~ 0.1, dmin ~ 0.05 (realistic for normalized weights) |
826 | | q4k_data.extend_from_slice(&[0x66, 0x2E]); // d |
827 | | q4k_data.extend_from_slice(&[0x66, 0x2A]); // dmin |
828 | | // Varied scales based on position |
829 | | let scale_base = ((row * 7 + sb * 3) % 16) as u8; |
830 | | for i in 0..12 { |
831 | | q4k_data.push(scale_base + (i as u8 % 4)); |
832 | | } |
833 | | // Varied quantized values (4-bit, so 0-15) |
834 | | for i in 0..128 { |
835 | | let low = ((row + sb + i) % 16) as u8; |
836 | | let high = ((row + sb + i + 5) % 16) as u8; |
837 | | q4k_data.push(low | (high << 4)); |
838 | | } |
839 | | } |
840 | | } |
841 | | |
842 | | // Random-ish input vector (sinusoidal distribution) |
843 | | let input: Vec<f32> = (0..in_dim) |
844 | | .map(|i| ((i as f32) * 0.017).sin() * 0.5) |
845 | | .collect(); |
846 | | |
847 | | // Method 1: Fused Q4K matmul |
848 | | let fused_output = matmul_q4k_f32(&q4k_data, &input, out_dim, in_dim); |
849 | | |
850 | | // Method 2: Dequantize + f32 matmul |
851 | | let total_elements = in_dim * out_dim; |
852 | | let dequantized_weights = dequantize_q4k_to_f32(&q4k_data, total_elements); |
853 | | let reference_output = matmul_f32_naive(&dequantized_weights, &input, out_dim, in_dim); |
854 | | |
855 | | // Verify Golden Invariant: error within 5% or absolute 0.01 |
856 | | assert_eq!(fused_output.len(), reference_output.len()); |
857 | | let mut max_rel_error = 0.0f32; |
858 | | let mut max_abs_error = 0.0f32; |
859 | | |
860 | | for (i, (fused, reference)) in fused_output.iter().zip(reference_output.iter()).enumerate() { |
861 | | let abs_error = (fused - reference).abs(); |
862 | | let rel_error = if reference.abs() > 1e-6 { |
863 | | abs_error / reference.abs() |
864 | | } else { |
865 | | abs_error |
866 | | }; |
867 | | max_rel_error = max_rel_error.max(rel_error); |
868 | | max_abs_error = max_abs_error.max(abs_error); |
869 | | |
870 | | assert!( |
871 | | rel_error < 0.05 || abs_error < 0.01, |
872 | | "Golden invariant violated at row {}: fused={}, reference={}, \ |
873 | | rel_error={:.4}%, abs_error={:.6}", |
874 | | i, fused, reference, rel_error * 100.0, abs_error |
875 | | ); |
876 | | } |
877 | | |
878 | | // Report max errors for visibility |
879 | | eprintln!( |
880 | | "[Golden Q4K Test] max_rel_error={:.4}%, max_abs_error={:.6}", |
881 | | max_rel_error * 100.0, max_abs_error |
882 | | ); |
883 | | } |
884 | | |
885 | | /// Golden Vector Test: dispatch path also satisfies invariant |
886 | | #[test] |
887 | | fn test_golden_vector_q4k_dispatch_vs_dequant() { |
888 | | use crate::backends::q4k::dequantize_q4k_to_f32; |
889 | | |
890 | | // Larger dimensions to exercise SIMD paths |
891 | | let in_dim = 1024; // 4 super-blocks |
892 | | let out_dim = 16; |
893 | | |
894 | | let mut q4k_data = Vec::new(); |
895 | | for row in 0..out_dim { |
896 | | for sb in 0..(in_dim / 256) { |
897 | | q4k_data.extend_from_slice(&[0x00, 0x3C]); // d ~ 1.0 |
898 | | q4k_data.extend_from_slice(&[0x00, 0x38]); // dmin ~ 0.5 |
899 | | for i in 0..12 { |
900 | | q4k_data.push(((row + sb + i) % 64) as u8); |
901 | | } |
902 | | for i in 0..128 { |
903 | | let low = ((row * 3 + sb * 7 + i) % 16) as u8; |
904 | | let high = ((row * 5 + sb * 11 + i * 2) % 16) as u8; |
905 | | q4k_data.push(low | (high << 4)); |
906 | | } |
907 | | } |
908 | | } |
909 | | |
910 | | let input: Vec<f32> = (0..in_dim) |
911 | | .map(|i| ((i as f32) * 0.013 + 0.5).cos() * 0.3) |
912 | | .collect(); |
913 | | |
914 | | // Dispatch (may use AVX2/SIMD) |
915 | | let dispatch_output = matmul_q4k_f32_dispatch(&q4k_data, &input, out_dim, in_dim); |
916 | | |
917 | | // Reference: dequantize + f32 |
918 | | let total_elements = in_dim * out_dim; |
919 | | let dequantized = dequantize_q4k_to_f32(&q4k_data, total_elements); |
920 | | let reference_output = matmul_f32_naive(&dequantized, &input, out_dim, in_dim); |
921 | | |
922 | | let mut max_rel_error = 0.0f32; |
923 | | for (i, (dispatch, reference)) in dispatch_output.iter().zip(reference_output.iter()).enumerate() { |
924 | | let abs_error = (dispatch - reference).abs(); |
925 | | let rel_error = if reference.abs() > 1e-6 { |
926 | | abs_error / reference.abs() |
927 | | } else { |
928 | | abs_error |
929 | | }; |
930 | | max_rel_error = max_rel_error.max(rel_error); |
931 | | |
932 | | assert!( |
933 | | rel_error < 0.05 || abs_error < 0.01, |
934 | | "Golden invariant violated (dispatch) at row {}: \ |
935 | | dispatch={}, reference={}, rel_error={:.4}%", |
936 | | i, dispatch, reference, rel_error * 100.0 |
937 | | ); |
938 | | } |
939 | | |
940 | | eprintln!( |
941 | | "[Golden Q4K Dispatch Test] max_rel_error={:.4}%", |
942 | | max_rel_error * 100.0 |
943 | | ); |
944 | | } |
945 | | |
946 | | /// Edge case: zero input vector should produce zero output |
947 | | #[test] |
948 | | fn test_golden_vector_zero_input() { |
949 | | let in_dim = 256; |
950 | | let out_dim = 4; |
951 | | |
952 | | let mut q4k_data = Vec::new(); |
953 | | for _row in 0..out_dim { |
954 | | q4k_data.extend_from_slice(&[0x66, 0x2E]); // d |
955 | | q4k_data.extend_from_slice(&[0x00, 0x00]); // dmin = 0 (important for zero output) |
956 | | q4k_data.extend_from_slice(&[0x01u8; 12]); |
957 | | q4k_data.extend_from_slice(&[0x55u8; 128]); // Non-zero weights |
958 | | } |
959 | | |
960 | | let input = vec![0.0f32; in_dim]; |
961 | | let output = matmul_q4k_f32(&q4k_data, &input, out_dim, in_dim); |
962 | | |
963 | | // With dmin=0 and all-zero input, output should be near zero |
964 | | for (i, val) in output.iter().enumerate() { |
965 | | assert!( |
966 | | val.abs() < 1e-6, |
967 | | "Zero input should give ~zero output, got {} at row {}", |
968 | | val, i |
969 | | ); |
970 | | } |
971 | | } |
972 | | |
973 | | /// Edge case: uniform input vector |
974 | | #[test] |
975 | | fn test_golden_vector_uniform_input() { |
976 | | use crate::backends::q4k::dequantize_q4k_to_f32; |
977 | | |
978 | | let in_dim = 256; |
979 | | let out_dim = 2; |
980 | | |
981 | | let mut q4k_data = Vec::new(); |
982 | | for row in 0..out_dim { |
983 | | q4k_data.extend_from_slice(&[0x00, 0x3C]); // d ~ 1.0 |
984 | | q4k_data.extend_from_slice(&[0x00, 0x00]); // dmin = 0 |
985 | | q4k_data.extend_from_slice(&[0x01u8; 12]); |
986 | | // Uniform quantized weights |
987 | | q4k_data.extend_from_slice(&[((row + 1) * 0x11) as u8; 128]); |
988 | | } |
989 | | |
990 | | let input = vec![1.0f32; in_dim]; // All ones |
991 | | let fused_output = matmul_q4k_f32(&q4k_data, &input, out_dim, in_dim); |
992 | | |
993 | | let total_elements = in_dim * out_dim; |
994 | | let dequantized = dequantize_q4k_to_f32(&q4k_data, total_elements); |
995 | | let reference_output = matmul_f32_naive(&dequantized, &input, out_dim, in_dim); |
996 | | |
997 | | for (i, (fused, reference)) in fused_output.iter().zip(reference_output.iter()).enumerate() { |
998 | | let rel_error = if reference.abs() > 1e-6 { |
999 | | (fused - reference).abs() / reference.abs() |
1000 | | } else { |
1001 | | (fused - reference).abs() |
1002 | | }; |
1003 | | assert!( |
1004 | | rel_error < 0.05, |
1005 | | "Uniform input failed at row {}: fused={}, ref={}, err={:.2}%", |
1006 | | i, fused, reference, rel_error * 100.0 |
1007 | | ); |
1008 | | } |
1009 | | } |
1010 | | |
1011 | | #[test] |
1012 | | fn test_parallel_dispatch_large_matrix() { |
1013 | | // Test parallel path: total_work >= 8_000_000 |
1014 | | // Use 4096 x 2048 = 8_388_608 ops (triggers parallel) |
1015 | | let out_dim = 4096; |
1016 | | let in_dim = 2048; // Must be multiple of 256 (SUPER_BLOCK_SIZE) |
1017 | | let total_work = out_dim * in_dim; |
1018 | | assert!( |
1019 | | total_work >= 8_000_000, |
1020 | | "Test must trigger parallel path" |
1021 | | ); |
1022 | | |
1023 | | let num_superblocks_per_row = (in_dim + SUPER_BLOCK_SIZE - 1) / SUPER_BLOCK_SIZE; |
1024 | | let row_bytes = num_superblocks_per_row * SUPER_BLOCK_BYTES; |
1025 | | let total_bytes = out_dim * row_bytes; |
1026 | | |
1027 | | // Create deterministic test data |
1028 | | let mut q4k_data = vec![0u8; total_bytes]; |
1029 | | for row in 0..out_dim { |
1030 | | for sb in 0..num_superblocks_per_row { |
1031 | | let offset = row * row_bytes + sb * SUPER_BLOCK_BYTES; |
1032 | | // d = 1.0 as f16 |
1033 | | q4k_data[offset] = 0x00; |
1034 | | q4k_data[offset + 1] = 0x3C; |
1035 | | // dmin = 0.0 |
1036 | | q4k_data[offset + 2] = 0x00; |
1037 | | q4k_data[offset + 3] = 0x00; |
1038 | | // scales = 1 for all |
1039 | | for i in 0..12 { |
1040 | | q4k_data[offset + 4 + i] = 0x01; |
1041 | | } |
1042 | | // qs = predictable pattern |
1043 | | for i in 0..128 { |
1044 | | q4k_data[offset + 16 + i] = ((row + sb + i) % 16) as u8; |
1045 | | } |
1046 | | } |
1047 | | } |
1048 | | |
1049 | | let input: Vec<f32> = (0..in_dim).map(|i| (i % 10) as f32 * 0.1).collect(); |
1050 | | |
1051 | | // Call dispatch - should use parallel path |
1052 | | let result = matmul_q4k_f32_dispatch(&q4k_data, &input, out_dim, in_dim); |
1053 | | |
1054 | | // Verify dimensions and finiteness |
1055 | | assert_eq!(result.len(), out_dim); |
1056 | | for (i, &val) in result.iter().enumerate() { |
1057 | | assert!( |
1058 | | val.is_finite(), |
1059 | | "Result[{}] is not finite: {}", |
1060 | | i, |
1061 | | val |
1062 | | ); |
1063 | | } |
1064 | | |
1065 | | // Compare a few rows against scalar for consistency |
1066 | | let scalar_result = matmul_q4k_f32_scalar(&q4k_data, &input, out_dim, in_dim); |
1067 | | for i in (0..out_dim).step_by(512) { |
1068 | | let diff = (result[i] - scalar_result[i]).abs(); |
1069 | | let tol = scalar_result[i].abs() * 0.01 + 1e-5; |
1070 | | assert!( |
1071 | | diff < tol, |
1072 | | "Parallel vs scalar mismatch at row {}: parallel={}, scalar={}, diff={}", |
1073 | | i, |
1074 | | result[i], |
1075 | | scalar_result[i], |
1076 | | diff |
1077 | | ); |
1078 | | } |
1079 | | } |
1080 | | |
1081 | | #[test] |
1082 | | fn test_parallel_colmajor_large_matrix() { |
1083 | | // Test colmajor path |
1084 | | // ne0 = output dimension (rows), ne1 = input dimension (columns) |
1085 | | // Input must have length ne1 |
1086 | | let ne0 = 2048; // output dimension (rows), must be multiple of 256 |
1087 | | let ne1 = 4096; // input dimension (columns) |
1088 | | |
1089 | | let blocks_per_col = (ne0 + SUPER_BLOCK_SIZE - 1) / SUPER_BLOCK_SIZE; |
1090 | | let col_bytes = blocks_per_col * SUPER_BLOCK_BYTES; |
1091 | | let total_bytes = ne1 * col_bytes; |
1092 | | |
1093 | | let mut q4k_data = vec![0u8; total_bytes]; |
1094 | | for col in 0..ne1 { |
1095 | | for sb in 0..blocks_per_col { |
1096 | | let offset = col * col_bytes + sb * SUPER_BLOCK_BYTES; |
1097 | | // d = 0.5 as f16 |
1098 | | q4k_data[offset] = 0x00; |
1099 | | q4k_data[offset + 1] = 0x38; |
1100 | | // dmin = 0.0 |
1101 | | q4k_data[offset + 2] = 0x00; |
1102 | | q4k_data[offset + 3] = 0x00; |
1103 | | // scales |
1104 | | for i in 0..12 { |
1105 | | q4k_data[offset + 4 + i] = 0x02; |
1106 | | } |
1107 | | // qs |
1108 | | for i in 0..128 { |
1109 | | q4k_data[offset + 16 + i] = ((col ^ sb ^ i) % 16) as u8; |
1110 | | } |
1111 | | } |
1112 | | } |
1113 | | |
1114 | | // Input must have length ne1 (input dimension) |
1115 | | let input: Vec<f32> = (0..ne1).map(|i| ((i % 7) as f32 - 3.0) * 0.1).collect(); |
1116 | | |
1117 | | // Use colmajor dispatch |
1118 | | let result = matmul_q4k_f32_colmajor_dispatch(&q4k_data, &input, ne0, ne1); |
1119 | | |
1120 | | // Output has ne0 elements |
1121 | | assert_eq!(result.len(), ne0); |
1122 | | for (i, &val) in result.iter().enumerate() { |
1123 | | assert!(val.is_finite(), "Result[{}] is not finite: {}", i, val); |
1124 | | } |
1125 | | } |
1126 | | |
1127 | | #[test] |
1128 | | fn test_compute_chunk_scalar_small() { |
1129 | | // Directly test compute_chunk_q4k_scalar |
1130 | | let in_dim = 256; |
1131 | | let out_dim = 4; |
1132 | | let num_blocks_per_row = 1; |
1133 | | let row_bytes = SUPER_BLOCK_BYTES; |
1134 | | |
1135 | | let mut q4k_data = vec![0u8; out_dim * row_bytes]; |
1136 | | for row in 0..out_dim { |
1137 | | let offset = row * row_bytes; |
1138 | | // d = 1.0 as f16 |
1139 | | q4k_data[offset] = 0x00; |
1140 | | q4k_data[offset + 1] = 0x3C; |
1141 | | // dmin = 0.0 |
1142 | | q4k_data[offset + 2] = 0x00; |
1143 | | q4k_data[offset + 3] = 0x00; |
1144 | | // scales = 1 |
1145 | | for i in 0..12 { |
1146 | | q4k_data[offset + 4 + i] = 0x01; |
1147 | | } |
1148 | | // qs = all zeros (simplest case) |
1149 | | for i in 0..128 { |
1150 | | q4k_data[offset + 16 + i] = 0x00; |
1151 | | } |
1152 | | } |
1153 | | |
1154 | | let input = vec![1.0f32; in_dim]; |
1155 | | let mut chunk = vec![0.0f32; out_dim]; |
1156 | | |
1157 | | compute_chunk_q4k_scalar( |
1158 | | &q4k_data, |
1159 | | &input, |
1160 | | &mut chunk, |
1161 | | 0, |
1162 | | out_dim, |
1163 | | in_dim, |
1164 | | num_blocks_per_row, |
1165 | | row_bytes, |
1166 | | ); |
1167 | | |
1168 | | // With qs=0, d=1, scales=1, dmin=0, result should be negative |
1169 | | // Each element: d * scale * 0 - dmin * min = 0 - 0 = 0 |
1170 | | // Actually with all zeros in qs and dmin=0, output should be 0 |
1171 | | for (i, &val) in chunk.iter().enumerate() { |
1172 | | assert!( |
1173 | | val.is_finite(), |
1174 | | "Chunk[{}] is not finite: {}", |
1175 | | i, |
1176 | | val |
1177 | | ); |
1178 | | } |
1179 | | } |
1180 | | } |