## Coverage Report

#### Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/realizar/src/apr_transformer/dequant.rs

    Line
    Count
    Source
    1
    //! GGUF K-quant Dequantization Helpers (PMAT-802)
    2
    //!
    3
    //! Dequantization routines for APR Q4_K/Q6_K support.
    4
    5
    // ============================================================================
    6
    // GGUF K-quant Dequantization Helpers (for APR Q4_K/Q6_K support)
    7
    // =============================================================================
    8
    9
    /// Convert IEEE 754 half-precision (f16) bits to f32
    10
    16
    pub(crate) fn f16_to_f32(bits: u16) -> f32 {
    11
    16
        let sign = u32::from((bits >> 15) & 1);
    12
    16
        let exp = u32::from((bits >> 10) & 0x1F);
    13
    16
        let mant = u32::from(bits & 0x3FF);
    14
    15
    16
        if exp == 0 {
    16
    7
            if mant == 0 {
    17
                // Zero
    18
    6
                f32::from_bits(sign << 31)
    19
            } else {
    20
                // Subnormal - convert to normalized f32
    21
    1
                let mut m = mant;
    22
    1
                let mut e = 0i32;
    23
    11
                while (m & 0x400) == 0 {
    24
    10
                    m <<= 1;
    25
    10
                    e -= 1;
    26
    10
                }
    27
    1
                m &= 0x3FF;
    28
    1
                let f32_exp = (127 - 15 + 1 + e) as u32;
    29
    1
                f32::from_bits((sign << 31) | (f32_exp << 23) | (m << 13))
    30
            }
    31
    9
        } else if exp == 31 {
    32
            // Inf or NaN
    33
    3
            if mant == 0 {
    34
    2
                f32::from_bits((sign << 31) | (0xFF << 23))
    35
            } else {
    36
    1
                f32::from_bits((sign << 31) | (0xFF << 23) | (mant << 13))
    37
            }
    38
        } else {
    39
            // Normal number
    40
    6
            let f32_exp = (exp as i32 - 15 + 127) as u32;
    41
    6
            f32::from_bits((sign << 31) | (f32_exp << 23) | (mant << 13))
    42
        }
    43
    16
    }
    44
    45
    /// Extract scale and min from Q4_K 12-byte packed scales
    46
    ///
    47
    /// PAR-001 FIX: Matches llama.cpp's get_scale_min_k4 packing scheme:
    48
    /// - Blocks 0-3: scale = q[j] & 63, min = q[j+4] & 63
    49
    /// - Blocks 4-7: scale = (q[j+4] & 0xF) | ((q[j-4] >> 6) << 4)
    50
    ///   min = (q[j+4] >> 4) | ((q[j] >> 6) << 4)
    51
    #[inline]
    52
    19
    pub(crate) fn extract_scale_min_apr(scales: &[u8], block_idx: usize) -> (f32, f32) {
    53
    19
        let j = block_idx;
    54
    19
        let (scale_bits, min_bits) = if j < 4 {
    55
            // First 4 blocks: simple layout
    56
    10
            let d = scales[j] & 63;
    57
    10
            let m = scales[j + 4] & 63;
    58
    10
            (d, m)
    59
        } else {
    60
            // Last 4 blocks: packed layout using high bits from first 4 bytes
    61
    9
            let d = (scales[j + 4] & 0x0F) | ((scales[j - 4] >> 6) << 4);
    62
    9
            let m = (scales[j + 4] >> 4) | ((scales[j] >> 6) << 4);
    63
    9
            (d, m)
    64
        };
    65
    66
    19
        (f32::from(scale_bits), f32::from(min_bits))
    67
    19
    }
    68
    69
    /// Dequantize Q4_K format (K-quants) for APR tensors
    70
    /// Q4_K: super blocks of 256 elements
    71
    /// Each super block: d (f16) + dmin (f16) + scales (12 bytes) + qs (128 bytes) = 144 bytes
    72
    ///
    73
    /// PMAT-086 FIX: Correct implementation matching llama.cpp/candle layout:
    74
    /// - For each 64-value chunk, output 32 low nibbles THEN 32 high nibbles
    75
    /// - Use sc1/dm1 for low nibbles, sc2/dm2 for high nibbles (different scales per half)
    76
    4
    pub(crate) fn dequantize_q4_k_apr(data: &[u8], num_elements: usize) -> Vec<f32> {
    77
        const QK_K: usize = 256; // Super-block size
    78
        const SUPER_BLOCK_BYTES: usize = 2 + 2 + 12 + 128; // 144 bytes
    79
    80
    4
        let num_blocks = num_elements.div_ceil(QK_K);
    81
    4
        let total_bytes = num_blocks * SUPER_BLOCK_BYTES;
    82
    83
    4
        if total_bytes > data.len() {
    84
            // Return zeros if data is insufficient
    85
    1
            return vec![0.0; num_elements];
    86
    3
        }
    87
    88
    3
        let mut result = vec![0.0f32; num_blocks * QK_K];
    89
    90
    3
        for sb_idx2 in 0..num_blocks {
    91
    2
            let sb_start = sb_idx * SUPER_BLOCK_BYTES;
    92
    2
            let out_start = sb_idx * QK_K;
    93
    94
            // Read d (f16 scale) and dmin (f16 min)
    95
    2
            let d = f16_to_f32(u16::from_le_bytes([data[sb_start], data[sb_start + 1]]));
    96
    2
            let dmin = f16_to_f32(u16::from_le_bytes([data[sb_start + 2], data[sb_start + 3]]));
    97
    98
            // Read scales (12 bytes)
    99
    2
            let scales = &data[sb_start + 4..sb_start + 16];
    100
    101
            // Read qs (128 bytes)
    102
    2
            let qs = &data[sb_start + 16..sb_start + 144];
    103
    104
            // Dequantize following candle's layout:
    105
            // For each 64-value chunk, output 32 low nibbles then 32 high nibbles
    106
    2
            let mut ys_index = out_start;
    107
    108
    8
            for j in (0..QK_K)2.step_by2(64) {
    109
    8
                let q = &qs[j / 2..j / 2 + 32];
    110
    111
                // Get scales for the two 32-value halves
    112
    8
                let is = j / 32;
    113
    8
                let (sc1, m1) = extract_scale_min_apr(scales, is);
    114
    8
                let d1 = d * sc1;
    115
    8
                let dm1 = dmin * m1;
    116
    117
    8
                let (sc2, m2) = extract_scale_min_apr(scales, is + 1);
    118
    8
                let d2 = d * sc2;
    119
    8
                let dm2 = dmin * m2;
    120
    121
                // First pass: 32 low nibbles
    122
    264
                for &byte256 in q {
    123
    256
                    result[ys_index] = d1 * (byte & 0xF) as f32 - dm1;
    124
    256
                    ys_index += 1;
    125
    256
                }
    126
    127
                // Second pass: 32 high nibbles
    128
    264
                for &byte256 in q {
    129
    256
                    result[ys_index] = d2 * (byte >> 4) as f32 - dm2;
    130
    256
                    ys_index += 1;
    131
    256
                }
    132
            }
    133
        }
    134
    135
    3
        result.truncate(num_elements);
    136
    3
        result
    137
    4
    }
    138
    139
    /// Dequantize Q6_K format (K-quants) for APR tensors
    140
    /// Q6_K super-block layout (per llama.cpp block_q6_K and candle):
    141
    /// - ql: 128 bytes (low 4 bits, 256 values, 2 per byte)
    142
    /// - qh: 64 bytes (high 2 bits, 256 values, 4 per byte)
    143
    /// - scales: 16 bytes (i8 signed scales for 16 blocks)
    144
    /// - d: 2 bytes (f16)
    145
    ///
    146
    /// Total: 128 + 64 + 16 + 2 = 210 bytes
    147
    4
    pub(crate) fn dequantize_q6_k_apr(data: &[u8], num_elements: usize) -> Vec<f32> {
    148
        const QK_K: usize = 256;
    149
        const SUPER_BLOCK_BYTES: usize = 210;
    150
    151
    4
        let num_blocks = num_elements.div_ceil(QK_K);
    152
    4
        let total_bytes = num_blocks * SUPER_BLOCK_BYTES;
    153
    154
    4
        if total_bytes > data.len() {
    155
    1
            return vec![0.0; num_elements];
    156
    3
        }
    157
    158
    3
        let mut result = vec![0.0f32; num_blocks * QK_K];
    159
    160
    3
        for sb_idx2 in 0..num_blocks {
    161
    2
            let sb_start = sb_idx * SUPER_BLOCK_BYTES;
    162
    2
            let out_start = sb_idx * QK_K;
    163
    164
            // Read ql - low 4 bits (128 bytes) at offset 0
    165
    2
            let ql = &data[sb_start..sb_start + 128];
    166
    167
            // Read qh - high 2 bits (64 bytes) at offset 128
    168
    2
            let qh = &data[sb_start + 128..sb_start + 192];
    169
    170
            // Read scales (16 bytes, i8) at offset 192
    171
    2
            let mut scales = [0i8; 16];
    172
            #[allow(clippy::cast_possible_wrap)]
    173
    32
            for (i, scale) in scales2.iter_mut2().enumerate2() {
    174
    32
                *scale = data[sb_start + 192 + i] as i8;
    175
    32
            }
    176
    177
            // Read d (f16 -> f32) at offset 208 (last 2 bytes)
    178
    2
            let d = f16_to_f32(u16::from_le_bytes([
    179
    2
                data[sb_start + 208],
    180
    2
                data[sb_start + 209],
    181
    2
            ]));
    182
    183
            // Dequantize 256 values following candle's exact layout
    184
            // Process 128 values at a time (n=0, n=128)
    185
    4
            for n in (0..QK_K)2.step_by2(128) {
    186
    4
                let idx = n / 128;
    187
    4
                let sc = &scales[8 * idx..];
    188
    4
                let ql_slice = &ql[64 * idx..];
    189
    4
                let qh_slice = &qh[32 * idx..];
    190
    191
    132
                for l128 in 0..32 {
    192
    128
                    let is = l / 16; // Scale index selector (0 or 1 within this 128-block)
    193
    128
    194
    128
                    // Extract 4 values per iteration (at positions l, l+32, l+64, l+96)
    195
    128
                    // q1: low 4 bits of ql[l] + bits 0-1 of qh[l]
    196
    128
                    let q1 = ((ql_slice[l] & 0xF) | ((qh_slice[l] & 3) << 4)) as i32 - 32;
    197
    128
                    // q2: low 4 bits of ql[l+32] + bits 2-3 of qh[l]
    198
    128
                    let q2 = ((ql_slice[l + 32] & 0xF) | (((qh_slice[l] >> 2) & 3) << 4)) as i32 - 32;
    199
    128
                    // q3: high 4 bits of ql[l] + bits 4-5 of qh[l]
    200
    128
                    let q3 = ((ql_slice[l] >> 4) | (((qh_slice[l] >> 4) & 3) << 4)) as i32 - 32;
    201
    128
                    // q4: high 4 bits of ql[l+32] + bits 6-7 of qh[l]
    202
    128
                    let q4 = ((ql_slice[l + 32] >> 4) | (((qh_slice[l] >> 6) & 3) << 4)) as i32 - 32;
    203
    128
    204
    128
                    // Write to output with correct scale indexing
    205
    128
                    result[out_start + n + l] = d * (sc[is] as f32) * (q1 as f32);
    206
    128
                    result[out_start + n + l + 32] = d * (sc[is + 2] as f32) * (q2 as f32);
    207
    128
                    result[out_start + n + l + 64] = d * (sc[is + 4] as f32) * (q3 as f32);
    208
    128
                    result[out_start + n + l + 96] = d * (sc[is + 6] as f32) * (q4 as f32);
    209
    128
                }
    210
            }
    211
        }
    212
    213
    3
        result.truncate(num_elements);
    214
    3
        result
    215
    4
    }
    216
    217
    // Tests moved to tests/ directory (PMAT-803)