## 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/gpu/adapters/apr.rs

    Line
    Count
    Source
    1
    //! APR to GpuModel Adapter (PMAT-106)
    2
    //!
    3
    //! Converts APR transformers to `GpuModel` for GPU inference.
    4
    //!
    5
    //! # Overview
    6
    //!
    7
    //! This module provides adapters for both F32 and Q4 APR formats:
    8
    //! - [`AprF32ToGpuAdapter`] - For `.apr` files with F32 weights (direct copy)
    9
    //! - [`AprToGpuAdapter`] - For GGUF Q4_0 models (dequantizes to F32)
    10
    //!
    11
    //! # Coverage Impact
    12
    //!
    13
    //! Testing these adapters exercises:
    14
    //! - `apr_transformer/mod.rs` - F32 weight extraction
    15
    //! - `apr_transformer/q4_simd.rs` - Q4 weight extraction
    16
    //! - `gpu/scheduler/model.rs` - GpuModel creation
    17
    //! - `quantize/dequant.rs` - Q4_0 dequantization
    18
    19
    use crate::apr_transformer::{
    20
        AprTransformer, AprTransformerConfig, AprTransformerLayer,
    21
        QuantizedAprTransformerQ4, QuantizedAprLayerQ4,
    22
    };
    23
    use crate::gpu::scheduler::{GpuModel, GpuModelConfig, BlockWeights};
    24
    use crate::quantize::dequantize_q4_0;
    25
    use crate::error::Result;
    26
    use thiserror::Error;
    27
    28
    /// Errors during APR to GPU conversion
    29
    #[derive(Debug, Error)]
    30
    pub enum AprGpuError {
    31
        /// Dequantization failed
    32
        #[error("Failed to dequantize Q4_0 weights: {0}")]
    33
        DequantError(String),
    34
    35
        /// Weight dimension mismatch
    36
        #[error("Weight dimension mismatch: expected {expected}, got {actual}")]
    37
        DimensionMismatch {
    38
            /// Expected number of elements
    39
            expected: usize,
    40
            /// Actual number of elements
    41
            actual: usize,
    42
        },
    43
    44
        /// GpuModel creation failed
    45
        #[error("Failed to create GpuModel: {0}")]
    46
        GpuModelError(String),
    47
    }
    48
    49
    /// Adapter for converting F32 APR models to GPU format
    50
    ///
    51
    /// Used for `.apr` files which contain F32 weights. No dequantization needed.
    52
    pub struct AprF32ToGpuAdapter;
    53
    54
    impl AprF32ToGpuAdapter {
    55
        /// Convert F32 APR transformer to GpuModel
    56
        ///
    57
        /// # Arguments
    58
        ///
    59
        /// * `apr` - Source APR transformer with F32 weights
    60
        ///
    61
        /// # Returns
    62
        ///
    63
        /// `GpuModel` ready for GPU inference
    64
        ///
    65
        /// # Example
    66
        ///
    67
        /// ```ignore
    68
        /// use realizar::apr_transformer::AprTransformer;
    69
        /// use realizar::gpu::adapters::AprF32ToGpuAdapter;
    70
        ///
    71
        /// let apr = AprTransformer::from_apr_bytes(&data)?;
    72
        /// let gpu_model = AprF32ToGpuAdapter::to_gpu_model(&apr)?;
    73
        /// ```
    74
    0
        pub fn to_gpu_model(apr: &AprTransformer) -> Result<GpuModel> {
    75
    0
            let config = AprToGpuAdapter::config_to_gpu(&apr.config);
    76
    0
            let hidden_dim = config.hidden_dim;
    77
    0
            let intermediate_dim = config.intermediate_dim;
    78
    79
            // Embedding weights (already F32)
    80
    0
            let embedding_weights = apr.token_embedding.clone();
    81
    82
            // LM head weights (already F32)
    83
    0
            let lm_head_weight = apr.lm_head_weight.clone();
    84
    85
            // Phase 22 FIX: Transpose LM head from APR [vocab_size, hidden_dim] to GPU [hidden_dim, vocab_size]
    86
            // APR stores weights as [out_dim, in_dim], GPU matmul expects [in_dim, out_dim]
    87
    0
            let lm_head_weight_t = transpose_matrix(&lm_head_weight, config.vocab_size, hidden_dim);
    88
    89
            // Convert each layer
    90
    0
            let mut block_weights = Vec::with_capacity(apr.layers.len());
    91
    0
            for layer in &apr.layers {
    92
    0
                block_weights.push(Self::convert_layer(
    93
    0
                    layer,
    94
    0
                    hidden_dim,
    95
    0
                    intermediate_dim,
    96
    0
                    config.num_heads,
    97
    0
                    config.num_kv_heads,
    98
    0
                ));
    99
    0
            }
    100
    101
            // Final norm
    102
    0
            let final_norm_weight = apr.output_norm_weight.clone();
    103
    0
            let final_norm_bias = apr.output_norm_bias.clone().unwrap_or_else(|| vec![0.0; hidden_dim]);
    104
    105
            // LM head bias
    106
    0
            let lm_head_bias = apr.lm_head_bias.clone().unwrap_or_else(|| vec![0.0; config.vocab_size]);
    107
    108
            // Create GpuModel using internal constructor
    109
    0
            GpuModel::from_apr_weights(
    110
    0
                config,
    111
    0
                embedding_weights,
    112
    0
                block_weights,
    113
    0
                final_norm_weight,
    114
    0
                final_norm_bias,
    115
    0
                lm_head_weight,
    116
    0
                lm_head_weight_t,
    117
    0
                lm_head_bias,
    118
            )
    119
    0
        }
    120
    121
        /// Convert a single F32 layer to BlockWeights
    122
    0
        fn convert_layer(
    123
    0
            layer: &AprTransformerLayer,
    124
    0
            hidden_dim: usize,
    125
    0
            intermediate_dim: usize,
    126
    0
            num_heads: usize,
    127
    0
            num_kv_heads: usize,
    128
    0
        ) -> BlockWeights {
    129
            // Phase 21 FIX: APR stores weights as [out_dim, in_dim] row-major,
    130
            // but GPU gemm expects [in_dim, out_dim]. Transpose all projection weights.
    131
    0
            let head_dim = hidden_dim / num_heads;
    132
    0
            let kv_dim = num_kv_heads * head_dim;
    133
    0
            let qkv_out_dim = hidden_dim + 2 * kv_dim;
    134
    135
            // Transpose QKV: [qkv_out_dim, hidden_dim] -> [hidden_dim, qkv_out_dim]
    136
    0
            let qkv_weight_t = transpose_matrix(&layer.qkv_weight, qkv_out_dim, hidden_dim);
    137
    138
            // Transpose output projection: [hidden_dim, hidden_dim] -> [hidden_dim, hidden_dim]
    139
    0
            let out_weight_t = transpose_matrix(&layer.attn_output_weight, hidden_dim, hidden_dim);
    140
    141
            // Transpose FFN up (fc1): [intermediate_dim, hidden_dim] -> [hidden_dim, intermediate_dim]
    142
    0
            let fc1_weight_t = transpose_matrix(&layer.ffn_up_weight, intermediate_dim, hidden_dim);
    143
    144
            // Transpose FFN down (fc2): [hidden_dim, intermediate_dim] -> [intermediate_dim, hidden_dim]
    145
    0
            let fc2_weight_t = transpose_matrix(&layer.ffn_down_weight, hidden_dim, intermediate_dim);
    146
    147
            // Transpose gate weight if present: [intermediate_dim, hidden_dim] -> [hidden_dim, intermediate_dim]
    148
    0
            let gate_weight_t = layer.ffn_gate_weight.as_ref().map(|w| {
    149
    0
                transpose_matrix(w, intermediate_dim, hidden_dim)
    150
    0
            });
    151
    152
            BlockWeights {
    153
    0
                attn_norm_weight: layer.attn_norm_weight.clone(),
    154
    0
                attn_norm_bias: layer.attn_norm_bias.clone().unwrap_or_else(|| vec![0.0; hidden_dim]),
    155
    0
                qkv_weight: qkv_weight_t,
    156
    0
                qkv_bias: layer.qkv_bias.clone().unwrap_or_default(),
    157
    0
                out_weight: out_weight_t,
    158
    0
                out_bias: layer.attn_output_bias.clone().unwrap_or_else(|| vec![0.0; hidden_dim]),
    159
                // Use actual FFN norm if available, otherwise identity (Phase 21 fix)
    160
    0
                ffn_norm_weight: layer.ffn_norm_weight.clone().unwrap_or_else(|| vec![1.0; hidden_dim]),
    161
    0
                ffn_norm_bias: layer.ffn_norm_bias.clone().unwrap_or_else(|| vec![0.0; hidden_dim]),
    162
    0
                ffn_fc1_weight: fc1_weight_t,
    163
    0
                ffn_fc1_bias: layer.ffn_up_bias.clone().unwrap_or_else(|| vec![0.0; intermediate_dim]),
    164
    0
                ffn_fc2_weight: fc2_weight_t,
    165
    0
                ffn_fc2_bias: layer.ffn_down_bias.clone().unwrap_or_else(|| vec![0.0; hidden_dim]),
    166
                // SwiGLU gate weight - critical for Qwen/LLaMA models
    167
    0
                ffn_gate_weight: gate_weight_t,
    168
            }
    169
    0
        }
    170
    }
    171
    172
    /// Adapter for converting Q4 APR models to GPU format
    173
    pub struct AprToGpuAdapter;
    174
    175
    impl AprToGpuAdapter {
    176
        /// Convert APR config to GPU config
    177
        #[must_use]
    178
    1
        pub fn config_to_gpu(apr_config: &AprTransformerConfig) -> GpuModelConfig {
    179
    1
            GpuModelConfig {
    180
    1
                vocab_size: apr_config.vocab_size,
    181
    1
                hidden_dim: apr_config.hidden_dim,
    182
    1
                num_heads: apr_config.num_heads,
    183
    1
                num_kv_heads: apr_config.num_kv_heads,
    184
    1
                num_layers: apr_config.num_layers,
    185
    1
                intermediate_dim: apr_config.intermediate_dim,
    186
    1
                eps: apr_config.eps,
    187
    1
                rope_theta: apr_config.rope_theta,
    188
    1
            }
    189
    1
        }
    190
    191
        /// Dequantize a Q4_0 tensor to F32
    192
        ///
    193
        /// # Arguments
    194
        ///
    195
        /// * `data` - Raw Q4_0 quantized bytes
    196
        /// * `expected_elements` - Expected number of output elements
    197
        ///
    198
        /// # Returns
    199
        ///
    200
        /// Dequantized F32 vector
    201
    0
        pub fn dequantize_tensor(data: &[u8], expected_elements: usize) -> Result<Vec<f32>> {
    202
    0
            let result = dequantize_q4_0(data)?;
    203
    204
            // Validate dimensions
    205
    0
            if result.len() < expected_elements {
    206
                // Pad with zeros if needed (can happen with block alignment)
    207
    0
                let mut padded = result;
    208
    0
                padded.resize(expected_elements, 0.0);
    209
    0
                Ok(padded)
    210
            } else {
    211
                // Truncate to expected size
    212
    0
                Ok(result.into_iter().take(expected_elements).collect())
    213
            }
    214
    0
        }
    215
    216
        /// Extract QKV weights from APR layer
    217
        ///
    218
        /// APR stores QKV as a single tensor, which matches GpuModel format.
    219
    0
        pub fn extract_qkv_weights(
    220
    0
            layer: &QuantizedAprLayerQ4,
    221
    0
            hidden_dim: usize,
    222
    0
            num_heads: usize,
    223
    0
            num_kv_heads: usize,
    224
    0
        ) -> Result<Vec<f32>> {
    225
    0
            let head_dim = hidden_dim / num_heads;
    226
    0
            let kv_dim = num_kv_heads * head_dim;
    227
    0
            let qkv_out_dim = hidden_dim + 2 * kv_dim;
    228
    0
            let expected = hidden_dim * qkv_out_dim;
    229
    230
    0
            Self::dequantize_tensor(&layer.qkv_weight.data, expected)
    231
    0
        }
    232
    233
        /// Extract output projection weights
    234
    0
        pub fn extract_out_weights(
    235
    0
            layer: &QuantizedAprLayerQ4,
    236
    0
            hidden_dim: usize,
    237
    0
        ) -> Result<Vec<f32>> {
    238
    0
            let expected = hidden_dim * hidden_dim;
    239
    0
            Self::dequantize_tensor(&layer.attn_output_weight.data, expected)
    240
    0
        }
    241
    242
        /// Extract FFN weights (fc1 = up, fc2 = down)
    243
        ///
    244
        /// Note: APR uses SwiGLU with separate gate/up, but GpuModel combines them.
    245
        /// For compatibility, we return up weights as fc1.
    246
    0
        pub fn extract_ffn_weights(
    247
    0
            layer: &QuantizedAprLayerQ4,
    248
    0
            hidden_dim: usize,
    249
    0
            intermediate_dim: usize,
    250
    0
        ) -> Result<(Vec<f32>, Vec<f32>)> {
    251
            // FC1 (up projection): [hidden_dim, intermediate_dim]
    252
    0
            let fc1_expected = hidden_dim * intermediate_dim;
    253
    0
            let fc1 = Self::dequantize_tensor(&layer.ffn_up_weight.data, fc1_expected)?;
    254
    255
            // FC2 (down projection): [intermediate_dim, hidden_dim]
    256
    0
            let fc2_expected = intermediate_dim * hidden_dim;
    257
    0
            let fc2 = Self::dequantize_tensor(&layer.ffn_down_weight.data, fc2_expected)?;
    258
    259
    0
            Ok((fc1, fc2))
    260
    0
        }
    261
    262
        /// Convert full APR transformer to GpuModel
    263
        ///
    264
        /// # Arguments
    265
        ///
    266
        /// * `apr` - Source APR transformer with Q4_0 weights
    267
        ///
    268
        /// # Returns
    269
        ///
    270
        /// `GpuModel` ready for GPU inference
    271
        ///
    272
        /// # Example
    273
        ///
    274
        /// ```ignore
    275
        /// use realizar::apr_transformer::QuantizedAprTransformerQ4;
    276
        /// use realizar::gpu::adapters::AprToGpuAdapter;
    277
        ///
    278
        /// let apr = QuantizedAprTransformerQ4::from_gguf(&gguf_model);
    279
        /// let gpu_model = AprToGpuAdapter::to_gpu_model(&apr)?;
    280
        /// ```
    281
    0
        pub fn to_gpu_model(apr: &QuantizedAprTransformerQ4) -> Result<GpuModel> {
    282
    0
            let config = Self::config_to_gpu(&apr.config);
    283
    0
            let hidden_dim = config.hidden_dim;
    284
    0
            let intermediate_dim = config.intermediate_dim;
    285
    286
            // Embedding weights (already F32 in APR)
    287
    0
            let embedding_weights = apr.token_embedding.clone();
    288
    289
            // Dequantize LM head
    290
    0
            let lm_head_expected = hidden_dim * config.vocab_size;
    291
    0
            let lm_head_weight = Self::dequantize_tensor(&apr.lm_head_weight.data, lm_head_expected)?;
    292
    293
            // Phase 22 FIX: Transpose LM head from APR [vocab_size, hidden_dim] to GPU [hidden_dim, vocab_size]
    294
            // APR stores weights as [out_dim, in_dim], GPU matmul expects [in_dim, out_dim]
    295
    0
            let lm_head_weight_t = transpose_matrix(&lm_head_weight, config.vocab_size, hidden_dim);
    296
    297
            // Convert each layer
    298
    0
            let mut block_weights = Vec::with_capacity(apr.layers.len());
    299
    0
            for layer in &apr.layers {
    300
    0
                let qkv = Self::extract_qkv_weights(layer, hidden_dim, config.num_heads, config.num_kv_heads)?;
    301
    0
                let out = Self::extract_out_weights(layer, hidden_dim)?;
    302
    0
                let (fc1, fc2) = Self::extract_ffn_weights(layer, hidden_dim, intermediate_dim)?;
    303
    304
                // Extract gate weight for SwiGLU (optional)
    305
    0
                let ffn_gate_weight = if let Some(ref gate) = layer.ffn_gate_weight {
    306
    0
                    let gate_expected = hidden_dim * intermediate_dim;
    307
    0
                    Some(Self::dequantize_tensor(&gate.data, gate_expected)?)
    308
                } else {
    309
    0
                    None
    310
                };
    311
    312
    0
                block_weights.push(BlockWeights {
    313
    0
                    attn_norm_weight: layer.attn_norm_weight.clone(),
    314
    0
                    attn_norm_bias: vec![0.0; hidden_dim], // APR doesn't use bias
    315
    0
                    qkv_weight: qkv,
    316
    0
                    qkv_bias: vec![], // No bias in APR
    317
    0
                    out_weight: out,
    318
    0
                    out_bias: vec![0.0; hidden_dim],
    319
    0
                    ffn_norm_weight: layer.ffn_norm_weight.clone().unwrap_or_else(|| vec![1.0; hidden_dim]),
    320
    0
                    ffn_norm_bias: vec![0.0; hidden_dim],
    321
    0
                    ffn_fc1_weight: fc1,
    322
    0
                    ffn_fc1_bias: vec![0.0; intermediate_dim],
    323
    0
                    ffn_fc2_weight: fc2,
    324
    0
                    ffn_fc2_bias: vec![0.0; hidden_dim],
    325
    0
                    ffn_gate_weight,
    326
                });
    327
            }
    328
    329
            // Final norm
    330
    0
            let final_norm_weight = apr.output_norm_weight.clone();
    331
    0
            let final_norm_bias = vec![0.0; hidden_dim];
    332
    333
            // LM head bias
    334
    0
            let lm_head_bias = vec![0.0; config.vocab_size];
    335
    336
            // Create GpuModel using internal constructor
    337
    0
            GpuModel::from_apr_weights(
    338
    0
                config,
    339
    0
                embedding_weights,
    340
    0
                block_weights,
    341
    0
                final_norm_weight,
    342
    0
                final_norm_bias,
    343
    0
                lm_head_weight,
    344
    0
                lm_head_weight_t,
    345
    0
                lm_head_bias,
    346
            )
    347
    0
        }
    348
    }
    349
    350
    /// Transpose a row-major matrix
    351
    2
    fn transpose_matrix(data: &[f32], rows: usize, cols: usize) -> Vec<f32> {
    352
    2
        let mut transposed = vec![0.0; rows * cols];
    353
    4
        for i in 0..rows2 {
    354
    10
            for j in 0..cols4 {
    355
    10
                transposed[j * rows + i] = data[i * cols + j];
    356
    10
            }
    357
        }
    358
    2
        transposed
    359
    2
    }
    360
    361
    #[cfg(test)]
    362
    mod tests {
    363
        use super::*;
    364
        use crate::apr_transformer::AprTransformerConfig;
    365
    366
        #[test]
    367
    1
        fn test_config_to_gpu() {
    368
    1
            let apr_config = AprTransformerConfig {
    369
    1
                architecture: "test".to_string(),
    370
    1
                hidden_dim: 512,
    371
    1
                num_layers: 4,
    372
    1
                num_heads: 8,
    373
    1
                num_kv_heads: 4,
    374
    1
                vocab_size: 32000,
    375
    1
                intermediate_dim: 1024,
    376
    1
                context_length: 2048,
    377
    1
                rope_theta: 10000.0,
    378
    1
                eps: 1e-5,
    379
    1
            };
    380
    381
    1
            let gpu_config = AprToGpuAdapter::config_to_gpu(&apr_config);
    382
    383
    1
            assert_eq!(gpu_config.vocab_size, 32000);
    384
    1
            assert_eq!(gpu_config.hidden_dim, 512);
    385
    1
            assert_eq!(gpu_config.num_heads, 8);
    386
    1
            assert_eq!(gpu_config.num_kv_heads, 4);
    387
    1
            assert_eq!(gpu_config.num_layers, 4);
    388
    1
            assert_eq!(gpu_config.intermediate_dim, 1024);
    389
    1
            assert_eq!(gpu_config.eps, 1e-5);
    390
    1
        }
    391
    392
        #[test]
    393
    1
        fn test_transpose_matrix() {
    394
    1
            let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; // 2x3
    395
    1
            let transposed = transpose_matrix(&data, 2, 3); // 3x2
    396
    397
    1
            assert_eq!(transposed, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
    398
    1
        }
    399
    400
        #[test]
    401
    1
        fn test_transpose_identity() {
    402
    1
            let data = vec![1.0, 2.0, 3.0, 4.0]; // 2x2
    403
    1
            let transposed = transpose_matrix(&data, 2, 2);
    404
    405
    1
            assert_eq!(transposed, vec![1.0, 3.0, 2.0, 4.0]);
    406
    1
        }
    407
    }