Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}