/home/noah/src/trueno/src/backends/q4k/colmajor.rs
Line | Count | Source |
1 | | //! Column-major Q4_K matrix-vector multiplication. |
2 | | //! |
3 | | //! This module implements column-major GEMV for GGML/GGUF format weights, |
4 | | //! where weights are stored column-first for cache-efficient streaming. |
5 | | |
6 | | use super::{parse_q4k_header, SUPER_BLOCK_BYTES, SUPER_BLOCK_SIZE}; |
7 | | |
8 | | /// Fused Q4_K matrix-vector multiply for GGML column-major layout |
9 | | /// |
10 | | /// Computes: output = input @ Q4K_weight (GGML convention: y = x @ W) |
11 | | /// where weight is stored in Q4_K format with GGML column-major super-block organization. |
12 | | /// |
13 | | /// # GGML Column-Major Layout |
14 | | /// |
15 | | /// For a weight tensor with shape [ne0, ne1] in GGML notation: |
16 | | /// - ne0 is the output dimension (rows) |
17 | | /// - ne1 is the input/reduction dimension (columns) |
18 | | /// - Elements are stored column-major: W[i,j] at offset i + j*ne0 |
19 | | /// - Each column j (length ne0) contains weights from input[j] to all outputs |
20 | | /// |
21 | | /// # Arguments |
22 | | /// * `q4k_data` - Raw Q4K bytes in GGML column-major layout [ne0, ne1] |
23 | | /// * `input` - F32 input vector [ne1] (input/reduction dimension) |
24 | | /// * `ne0` - Size of output dimension (rows in GGML, output size) |
25 | | /// * `ne1` - Size of input/reduction dimension (columns in GGML, input size) |
26 | | /// |
27 | | /// # Returns |
28 | | /// F32 output vector [ne0] |
29 | 0 | pub fn matmul_q4k_f32_colmajor( |
30 | 0 | q4k_data: &[u8], |
31 | 0 | input: &[f32], |
32 | 0 | ne0: usize, // output dimension (rows) |
33 | 0 | ne1: usize, // input/reduction dimension (columns) |
34 | 0 | ) -> Vec<f32> { |
35 | 0 | assert_eq!(input.len(), ne1, "Input length must match ne1 (input dimension)"); |
36 | | |
37 | | // Number of super-blocks per column (each column has ne0 elements = output_dim) |
38 | 0 | let blocks_per_col = (ne0 + SUPER_BLOCK_SIZE - 1) / SUPER_BLOCK_SIZE; |
39 | 0 | let col_bytes = blocks_per_col * SUPER_BLOCK_BYTES; |
40 | | |
41 | 0 | let mut output = vec![0.0f32; ne0]; |
42 | | |
43 | | // Process each input column and accumulate to outputs |
44 | | // Column j contains weights from input[j] to all ne0 outputs |
45 | 0 | for col_idx in 0..ne1 { |
46 | 0 | let col_start = col_idx * col_bytes; |
47 | 0 | let x_j = input[col_idx]; // Input value for this column |
48 | | |
49 | | // Skip if input is zero (common in sparse activations) |
50 | 0 | if x_j == 0.0 { |
51 | 0 | continue; |
52 | 0 | } |
53 | | |
54 | | // Process super-blocks for this column |
55 | 0 | for sb_idx in 0..blocks_per_col { |
56 | 0 | let sb_start = col_start + sb_idx * SUPER_BLOCK_BYTES; |
57 | | |
58 | 0 | if sb_start + SUPER_BLOCK_BYTES > q4k_data.len() { |
59 | 0 | break; |
60 | 0 | } |
61 | | |
62 | 0 | let sb_data = &q4k_data[sb_start..sb_start + SUPER_BLOCK_BYTES]; |
63 | | |
64 | | // Parse header |
65 | 0 | let (d, dmin, scales, mins) = parse_q4k_header(sb_data); |
66 | 0 | let qs = &sb_data[16..144]; |
67 | | |
68 | | // Output offset for this super-block |
69 | 0 | let output_offset = sb_idx * SUPER_BLOCK_SIZE; |
70 | | |
71 | | // Process 4 chunks of 64 values each |
72 | 0 | for chunk in 0..4 { |
73 | 0 | let chunk_start = chunk * 64; |
74 | 0 | let q_start = chunk * 32; |
75 | | |
76 | 0 | let scale_idx_low = chunk * 2; |
77 | 0 | let scale_idx_high = chunk * 2 + 1; |
78 | | |
79 | 0 | let d1 = d * f32::from(scales[scale_idx_low]); |
80 | 0 | let dm1 = dmin * f32::from(mins[scale_idx_low]); |
81 | 0 | let d2 = d * f32::from(scales[scale_idx_high]); |
82 | 0 | let dm2 = dmin * f32::from(mins[scale_idx_high]); |
83 | | |
84 | | // Process low nibbles (first 32 values) |
85 | 0 | for i in 0..32 { |
86 | 0 | let output_idx = output_offset + chunk_start + i; |
87 | 0 | if output_idx < ne0 { |
88 | 0 | let q_val = (qs[q_start + i] & 0x0F) as f32; |
89 | 0 | let dequant = d1 * q_val - dm1; |
90 | 0 | output[output_idx] += x_j * dequant; |
91 | 0 | } |
92 | | } |
93 | | |
94 | | // Process high nibbles (next 32 values) |
95 | 0 | for i in 0..32 { |
96 | 0 | let output_idx = output_offset + chunk_start + 32 + i; |
97 | 0 | if output_idx < ne0 { |
98 | 0 | let q_val = (qs[q_start + i] >> 4) as f32; |
99 | 0 | let dequant = d2 * q_val - dm2; |
100 | 0 | output[output_idx] += x_j * dequant; |
101 | 0 | } |
102 | | } |
103 | | } |
104 | | } |
105 | | } |
106 | | |
107 | 0 | output |
108 | 0 | } |
109 | | |
110 | | /// Runtime dispatch for column-major Q4K matmul |
111 | | /// |
112 | | /// Uses scalar implementation for correctness. |
113 | | /// Matches GGUF tensor layout without requiring transposition. |
114 | | #[inline] |
115 | 0 | pub fn matmul_q4k_f32_colmajor_dispatch( |
116 | 0 | q4k_data: &[u8], |
117 | 0 | input: &[f32], |
118 | 0 | ne0: usize, |
119 | 0 | ne1: usize, |
120 | 0 | ) -> Vec<f32> { |
121 | 0 | matmul_q4k_f32_colmajor(q4k_data, input, ne0, ne1) |
122 | 0 | } |