## 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
    }