## 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/backend.rs

    Line
    Count
    Source
    1
    //! ComputeBackend trait for abstracting GPU operations (Phase 41)
    2
    //!
    3
    //! Enables mocking GPU host code for coverage testing.
    4
    5
    use std::error::Error;
    6
    7
    /// Result type for backend operations
    8
    pub type BackendResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
    9
    10
    /// Abstraction over GPU compute backends (CUDA, Mock, future: Metal, HIP)
    11
    ///
    12
    /// This trait allows swapping out the actual GPU implementation with a mock
    13
    /// for testing purposes, enabling coverage of GPU host code without requiring
    14
    /// actual GPU hardware.
    15
    ///
    16
    /// # Example
    17
    ///
    18
    /// ```rust,ignore
    19
    /// use realizar::gpu::backend::{ComputeBackend, BackendResult};
    20
    ///
    21
    /// struct MockBackend {
    22
    ///     weights: std::collections::HashMap<String, Vec<f32>>,
    23
    /// }
    24
    ///
    25
    /// impl ComputeBackend for MockBackend {
    26
    ///     fn is_available() -> bool { true }
    27
    ///     fn new(_device_id: u32) -> BackendResult<Self> {
    28
    ///         Ok(Self { weights: Default::default() })
    29
    ///     }
    30
    ///     // ... implement other methods
    31
    /// }
    32
    /// ```
    33
    pub trait ComputeBackend: Send {
    34
        /// Check if this backend type is available on the system
    35
        fn is_available() -> bool
    36
        where
    37
            Self: Sized;
    38
    39
        /// Create a new backend instance for the given device
    40
        ///
    41
        /// # Errors
    42
        ///
    43
        /// Returns error if device initialization fails.
    44
        fn new(device_id: u32) -> BackendResult<Self>
    45
        where
    46
            Self: Sized;
    47
    48
        /// Get device name
    49
        fn device_name(&self) -> String;
    50
    51
        // =========================================================================
    52
        // Weight Management
    53
        // =========================================================================
    54
    55
        /// Load f32 weights to device memory
    56
        ///
    57
        /// Returns a handle/index for the loaded weights.
    58
        ///
    59
        /// # Errors
    60
        ///
    61
        /// Returns error if memory allocation or transfer fails.
    62
        fn load_weights(&mut self, name: &str, data: &[f32]) -> BackendResult<usize>;
    63
    64
        /// Load quantized weights to device memory
    65
        ///
    66
        /// # Arguments
    67
        ///
    68
        /// * `name` - Unique identifier for the weights
    69
        /// * `data` - Raw quantized bytes
    70
        /// * `qtype` - Quantization type (e.g., 2 for Q4_0, 3 for Q4_K)
    71
        ///
    72
        /// # Errors
    73
        ///
    74
        /// Returns error if memory allocation or transfer fails.
    75
        fn load_quantized_weights(&mut self, name: &str, data: &[u8], qtype: u32)
    76
            -> BackendResult<usize>;
    77
    78
        /// Check if weights are loaded
    79
        fn has_weights(&self, name: &str) -> bool;
    80
    81
        /// Clear all cached weights from device memory
    82
        fn clear_weights(&mut self);
    83
    84
        /// Get count of cached weights
    85
        fn cached_weight_count(&self) -> usize;
    86
    87
        // =========================================================================
    88
        // Core Compute
    89
        // =========================================================================
    90
    91
        /// Matrix multiplication: C = A @ B
    92
        ///
    93
        /// # Arguments
    94
        ///
    95
        /// * `a` - Left matrix, row-major [m, k]
    96
        /// * `b` - Right matrix, row-major [k, n]
    97
        /// * `m` - Rows in A
    98
        /// * `k` - Cols in A / Rows in B
    99
        /// * `n` - Cols in B
    100
        ///
    101
        /// # Errors
    102
        ///
    103
        /// Returns error if dimensions are invalid or compute fails.
    104
        fn matmul(&mut self, a: &[f32], b: &[f32], m: u32, k: u32, n: u32) -> BackendResult<Vec<f32>>;
    105
    106
        /// Matrix multiplication using cached weights
    107
        ///
    108
        /// # Arguments
    109
        ///
    110
        /// * `weight_name` - Name of previously loaded weights
    111
        /// * `x` - Input vector/matrix
    112
        /// * `m` - Batch size (rows in input)
    113
        /// * `k` - Input dimension
    114
        /// * `n` - Output dimension
    115
        ///
    116
        /// # Errors
    117
        ///
    118
        /// Returns error if weights not found or compute fails.
    119
        fn matmul_cached(
    120
            &mut self,
    121
            weight_name: &str,
    122
            x: &[f32],
    123
            m: u32,
    124
            k: u32,
    125
            n: u32,
    126
        ) -> BackendResult<Vec<f32>>;
    127
    128
        // =========================================================================
    129
        // Quantized Operations
    130
        // =========================================================================
    131
    132
        /// Q4_K quantized GEMV using cached weights
    133
        ///
    134
        /// Computes y = dequant(W_q4k) @ x where W is Q4_K quantized.
    135
        ///
    136
        /// # Arguments
    137
        ///
    138
        /// * `weight_name` - Name of previously loaded Q4_K weights
    139
        /// * `input` - Input vector [k]
    140
        /// * `n` - Output dimension (rows in weight matrix)
    141
        /// * `k` - Input dimension (cols in weight matrix)
    142
        ///
    143
        /// # Errors
    144
        ///
    145
        /// Returns error if weights not found, wrong quantization type, or compute fails.
    146
        fn q4k_gemv_cached(
    147
            &mut self,
    148
            weight_name: &str,
    149
            input: &[f32],
    150
            n: u32,
    151
            k: u32,
    152
        ) -> BackendResult<Vec<f32>>;
    153
    154
        // =========================================================================
    155
        // Synchronization
    156
        // =========================================================================
    157
    158
        /// Synchronize device execution
    159
        ///
    160
        /// Blocks until all queued operations complete.
    161
        ///
    162
        /// # Errors
    163
        ///
    164
        /// Returns error if synchronization fails.
    165
        fn synchronize(&self) -> BackendResult<()>;
    166
    }
    167
    168
    #[cfg(test)]
    169
    mod tests {
    170
        use super::*;
    171
        use std::collections::HashMap;
    172
    173
        /// Mock backend for testing trait compliance
    174
        struct MockBackend {
    175
            device_name: String,
    176
            weights: HashMap<String, Vec<f32>>,
    177
            quantized_weights: HashMap<String, (Vec<u8>, u32)>,
    178
        }
    179
    180
        impl ComputeBackend for MockBackend {
    181
    1
            fn is_available() -> bool {
    182
    1
                true
    183
    1
            }
    184
    185
    5
            fn new(device_id: u32) -> BackendResult<Self> {
    186
    5
                Ok(Self {
    187
    5
                    device_name: format!("MockDevice:{}", device_id),
    188
    5
                    weights: HashMap::new(),
    189
    5
                    quantized_weights: HashMap::new(),
    190
    5
                })
    191
    5
            }
    192
    193
    1
            fn device_name(&self) -> String {
    194
    1
                self.device_name.clone()
    195
    1
            }
    196
    197
    2
            fn load_weights(&mut self, name: &str, data: &[f32]) -> BackendResult<usize> {
    198
    2
                let len = data.len();
    199
    2
                self.weights.insert(name.to_string(), data.to_vec());
    200
    2
                Ok(len)
    201
    2
            }
    202
    203
    1
            fn load_quantized_weights(
    204
    1
                &mut self,
    205
    1
                name: &str,
    206
    1
                data: &[u8],
    207
    1
                qtype: u32,
    208
    1
            ) -> BackendResult<usize> {
    209
    1
                let len = data.len();
    210
    1
                self.quantized_weights
    211
    1
                    .insert(name.to_string(), (data.to_vec(), qtype));
    212
    1
                Ok(len)
    213
    1
            }
    214
    215
    4
            fn has_weights(&self, name: &str) -> bool {
    216
    4
                self.weights.contains_key(name) || self.quantized_weights3.contains_key3(name3)
    217
    4
            }
    218
    219
    1
            fn clear_weights(&mut self) {
    220
    1
                self.weights.clear();
    221
    1
                self.quantized_weights.clear();
    222
    1
            }
    223
    224
    4
            fn cached_weight_count(&self) -> usize {
    225
    4
                self.weights.len() + self.quantized_weights.len()
    226
    4
            }
    227
    228
    2
            fn matmul(
    229
    2
                &mut self,
    230
    2
                a: &[f32],
    231
    2
                b: &[f32],
    232
    2
                m: u32,
    233
    2
                k: u32,
    234
    2
                n: u32,
    235
    2
            ) -> BackendResult<Vec<f32>> {
    236
    2
                let m = m as usize;
    237
    2
                let k = k as usize;
    238
    2
                let n = n as usize;
    239
    240
    2
                if a.len() != m * k {
    241
    0
                    return Err(format!("A size {} != m*k {}", a.len(), m * k).into());
    242
    2
                }
    243
    2
                if b.len() != k * n {
    244
    0
                    return Err(format!("B size {} != k*n {}", b.len(), k * n).into());
    245
    2
                }
    246
    247
    2
                let mut c = vec![0.0f32; m * n];
    248
    4
                for i in 0..m2 {
    249
    8
                    for j in 0..n4 {
    250
    8
                        let mut sum = 0.0f32;
    251
    24
                        for p in 0..k8 {
    252
    24
                            sum += a[i * k + p] * b[p * n + j];
    253
    24
                        }
    254
    8
                        c[i * n + j] = sum;
    255
                    }
    256
                }
    257
    2
                Ok(c)
    258
    2
            }
    259
    260
    1
            fn matmul_cached(
    261
    1
                &mut self,
    262
    1
                weight_name: &str,
    263
    1
                x: &[f32],
    264
    1
                m: u32,
    265
    1
                k: u32,
    266
    1
                n: u32,
    267
    1
            ) -> BackendResult<Vec<f32>> {
    268
    1
                let weights = self
    269
    1
                    .weights
    270
    1
                    .get(weight_name)
    271
    1
                    .ok_or_else(|| format!0("Weights '{}' not found"0, weight_name))?0
    272
    1
                    .clone();
    273
    1
                self.matmul(x, &weights, m, k, n)
    274
    1
            }
    275
    276
    0
            fn q4k_gemv_cached(
    277
    0
                &mut self,
    278
    0
                weight_name: &str,
    279
    0
                input: &[f32],
    280
    0
                n: u32,
    281
    0
                k: u32,
    282
    0
            ) -> BackendResult<Vec<f32>> {
    283
                // Mock: just return zeros of correct size
    284
    0
                if !self.quantized_weights.contains_key(weight_name) {
    285
    0
                    return Err(format!("Quantized weights '{}' not found", weight_name).into());
    286
    0
                }
    287
    0
                let _ = input;
    288
    0
                let _ = k;
    289
    0
                Ok(vec![0.0f32; n as usize])
    290
    0
            }
    291
    292
    1
            fn synchronize(&self) -> BackendResult<()> {
    293
    1
                Ok(())
    294
    1
            }
    295
        }
    296
    297
        #[test]
    298
    1
        fn test_mock_backend_creation() {
    299
    1
            assert!(MockBackend::is_available());
    300
    1
            let backend = MockBackend::new(0).unwrap();
    301
    1
            assert_eq!(backend.device_name(), "MockDevice:0");
    302
    1
        }
    303
    304
        #[test]
    305
    1
        fn test_mock_backend_weight_management() {
    306
    1
            let mut backend = MockBackend::new(0).unwrap();
    307
    308
            // Initially empty
    309
    1
            assert_eq!(backend.cached_weight_count(), 0);
    310
    1
            assert!(!backend.has_weights("test"));
    311
    312
            // Load weights
    313
    1
            let data = vec![1.0, 2.0, 3.0, 4.0];
    314
    1
            let handle = backend.load_weights("test", &data).unwrap();
    315
    1
            assert_eq!(handle, 4);
    316
    1
            assert!(backend.has_weights("test"));
    317
    1
            assert_eq!(backend.cached_weight_count(), 1);
    318
    319
            // Load quantized weights
    320
    1
            let qdata = vec![0u8; 18]; // Q4_0 block
    321
    1
            let qhandle = backend.load_quantized_weights("test_q4", &qdata, 2).unwrap();
    322
    1
            assert_eq!(qhandle, 18);
    323
    1
            assert!(backend.has_weights("test_q4"));
    324
    1
            assert_eq!(backend.cached_weight_count(), 2);
    325
    326
            // Clear
    327
    1
            backend.clear_weights();
    328
    1
            assert_eq!(backend.cached_weight_count(), 0);
    329
    1
            assert!(!backend.has_weights("test"));
    330
    1
        }
    331
    332
        #[test]
    333
    1
        fn test_mock_backend_matmul() {
    334
    1
            let mut backend = MockBackend::new(0).unwrap();
    335
    336
            // 2x3 @ 3x2 = 2x2
    337
    1
            let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
    338
    1
            let b = vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
    339
    340
    1
            let c = backend.matmul(&a, &b, 2, 3, 2).unwrap();
    341
    342
            // Expected: [[1*7+2*9+3*11, 1*8+2*10+3*12], [4*7+5*9+6*11, 4*8+5*10+6*12]]
    343
            //         = [[7+18+33, 8+20+36], [28+45+66, 32+50+72]]
    344
            //         = [[58, 64], [139, 154]]
    345
    1
            assert_eq!(c.len(), 4);
    346
    1
            assert!((c[0] - 58.0).abs() < 1e-5);
    347
    1
            assert!((c[1] - 64.0).abs() < 1e-5);
    348
    1
            assert!((c[2] - 139.0).abs() < 1e-5);
    349
    1
            assert!((c[3] - 154.0).abs() < 1e-5);
    350
    1
        }
    351
    352
        #[test]
    353
    1
        fn test_mock_backend_matmul_cached() {
    354
    1
            let mut backend = MockBackend::new(0).unwrap();
    355
    356
            // Load weights: 3x2 matrix
    357
    1
            let weights = vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
    358
    1
            backend.load_weights("W", &weights).unwrap();
    359
    360
            // Input: 2x3
    361
    1
            let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
    362
    363
    1
            let c = backend.matmul_cached("W", &x, 2, 3, 2).unwrap();
    364
    1
            assert_eq!(c.len(), 4);
    365
    1
        }
    366
    367
        #[test]
    368
    1
        fn test_mock_backend_sync() {
    369
    1
            let backend = MockBackend::new(0).unwrap();
    370
    1
            assert!(backend.synchronize().is_ok());
    371
    1
        }
    372
    }