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

    Line
    Count
    Source
    1
    //! Mock compute backend for coverage testing (Phase 41)
    2
    //!
    3
    //! Allows GPU scheduling/orchestration code to run without actual GPU.
    4
    //! This enables comprehensive testing of GPU host-side logic on CI systems
    5
    //! that may not have GPUs available.
    6
    //!
    7
    //! # Example
    8
    //!
    9
    //! ```rust,ignore
    10
    //! use realizar::gpu::mock_backend::MockBackend;
    11
    //! use realizar::gpu::backend::ComputeBackend;
    12
    //!
    13
    //! // Create mock backend (always succeeds)
    14
    //! let mut backend = MockBackend::new(0).unwrap();
    15
    //!
    16
    //! // Load weights (stored in CPU memory)
    17
    //! backend.load_weights("layer0.weight", &weights)?;
    18
    //!
    19
    //! // Execute matmul (runs on CPU with naive O(n^3) algorithm)
    20
    //! let result = backend.matmul(&a, &b, m, k, n)?;
    21
    //! ```
    22
    23
    #![allow(clippy::many_single_char_names)]
    24
    #![allow(clippy::missing_fields_in_debug)]
    25
    26
    use super::backend::{BackendResult, ComputeBackend};
    27
    use std::collections::HashMap;
    28
    29
    /// Mock backend that runs on CPU for testing GPU host code
    30
    ///
    31
    /// This backend stores weights in CPU memory and executes all operations
    32
    /// using naive CPU algorithms. It's designed for correctness (not speed),
    33
    /// making it ideal for testing GPU orchestration logic without GPU hardware.
    34
    ///
    35
    /// ## Features
    36
    ///
    37
    /// - Always available (no GPU required)
    38
    /// - Stores F32 and quantized weights in `HashMap`
    39
    /// - Naive O(n^3) matmul for correctness verification
    40
    ///
    41
    /// ## Usage
    42
    ///
    43
    /// ```rust,ignore
    44
    /// use realizar::gpu::mock_backend::MockBackend;
    45
    /// use realizar::gpu::backend::ComputeBackend;
    46
    ///
    47
    /// let mut backend = MockBackend::new(0)?;
    48
    ///
    49
    /// // Load weights
    50
    /// backend.load_weights("fc1", &weights)?;
    51
    ///
    52
    /// // Compute
    53
    /// let output = backend.matmul_cached("fc1", &input, 1, 768, 3072)?;
    54
    /// ```
    55
    pub struct MockBackend {
    56
        /// F32 weight storage
    57
        weights: HashMap<String, Vec<f32>>,
    58
        /// Quantized weight storage (raw bytes)
    59
        quantized_weights: HashMap<String, Vec<u8>>,
    60
        /// Quantization types for each quantized weight
    61
        quantized_types: HashMap<String, u32>,
    62
        /// Mock device name
    63
        device_name: String,
    64
    }
    65
    66
    impl MockBackend {
    67
        /// Create a new mock backend with default configuration
    68
        #[must_use]
    69
    18
        pub fn new_mock() -> Self {
    70
    18
            Self {
    71
    18
                weights: HashMap::new(),
    72
    18
                quantized_weights: HashMap::new(),
    73
    18
                quantized_types: HashMap::new(),
    74
    18
                device_name: "MockGPU (CPU fallback)".to_string(),
    75
    18
            }
    76
    18
        }
    77
    78
        /// Get a reference to stored F32 weights
    79
        #[must_use]
    80
    3
        pub fn get_weights(&self, name: &str) -> Option<&Vec<f32>> {
    81
    3
            self.weights.get(name)
    82
    3
        }
    83
    84
        /// Get a reference to stored quantized weights
    85
        #[must_use]
    86
    1
        pub fn get_quantized_weights(&self, name: &str) -> Option<&Vec<u8>> {
    87
    1
            self.quantized_weights.get(name)
    88
    1
        }
    89
    90
        /// Get the quantization type for a weight
    91
        #[must_use]
    92
    1
        pub fn get_quant_type(&self, name: &str) -> Option<u32> {
    93
    1
            self.quantized_types.get(name).copied()
    94
    1
        }
    95
    }
    96
    97
    impl ComputeBackend for MockBackend {
    98
    1
        fn is_available() -> bool
    99
    1
        where
    100
    1
            Self: Sized,
    101
        {
    102
    1
            true // Mock backend is always available
    103
    1
        }
    104
    105
    1
        fn new(_device_id: u32) -> BackendResult<Self>
    106
    1
        where
    107
    1
            Self: Sized,
    108
        {
    109
    1
            Ok(Self::new_mock())
    110
    1
        }
    111
    112
    1
        fn device_name(&self) -> String {
    113
    1
            self.device_name.clone()
    114
    1
        }
    115
    116
    8
        fn load_weights(&mut self, name: &str, data: &[f32]) -> BackendResult<usize> {
    117
    8
            let len = data.len();
    118
    8
            self.weights.insert(name.to_string(), data.to_vec());
    119
    8
            Ok(len)
    120
    8
        }
    121
    122
    4
        fn load_quantized_weights(
    123
    4
            &mut self,
    124
    4
            name: &str,
    125
    4
            data: &[u8],
    126
    4
            qtype: u32,
    127
    4
        ) -> BackendResult<usize> {
    128
    4
            let len = data.len();
    129
    4
            self.quantized_weights.insert(name.to_string(), data.to_vec());
    130
    4
            self.quantized_types.insert(name.to_string(), qtype);
    131
    4
            Ok(len)
    132
    4
        }
    133
    134
    8
        fn has_weights(&self, name: &str) -> bool {
    135
    8
            self.weights.contains_key(name) || self.quantized_weights5.contains_key5(name5)
    136
    8
        }
    137
    138
    1
        fn clear_weights(&mut self) {
    139
    1
            self.weights.clear();
    140
    1
            self.quantized_weights.clear();
    141
    1
            self.quantized_types.clear();
    142
    1
        }
    143
    144
    5
        fn cached_weight_count(&self) -> usize {
    145
    5
            self.weights.len() + self.quantized_weights.len()
    146
    5
        }
    147
    148
    6
        fn matmul(&mut self, a: &[f32], b: &[f32], m: u32, k: u32, n: u32) -> BackendResult<Vec<f32>> {
    149
    6
            let (m, k, n) = (m as usize, k as usize, n as usize);
    150
    151
            // Validate dimensions
    152
    6
            if a.len() != m * k {
    153
    1
                return Err(format!(
    154
    1
                    "Matrix A has {} elements, expected {}*{}={}",
    155
    1
                    a.len(),
    156
    1
                    m,
    157
    1
                    k,
    158
    1
                    m * k
    159
    1
                )
    160
    1
                .into());
    161
    5
            }
    162
    5
            if b.len() != k * n {
    163
    1
                return Err(format!(
    164
    1
                    "Matrix B has {} elements, expected {}*{}={}",
    165
    1
                    b.len(),
    166
    1
                    k,
    167
    1
                    n,
    168
    1
                    k * n
    169
    1
                )
    170
    1
                .into());
    171
    4
            }
    172
    173
            // Naive O(n^3) matmul for correctness
    174
    4
            let mut c = vec![0.0f32; m * n];
    175
    8
            for i in 0..m4 {
    176
    18
                for j in 0..n8 {
    177
    18
                    let mut sum = 0.0f32;
    178
    49
                    for l in 0..k18 {
    179
    49
                        sum += a[i * k + l] * b[l * n + j];
    180
    49
                    }
    181
    18
                    c[i * n + j] = sum;
    182
                }
    183
            }
    184
    185
    4
            Ok(c)
    186
    6
        }
    187
    188
    2
        fn matmul_cached(
    189
    2
            &mut self,
    190
    2
            weight_name: &str,
    191
    2
            x: &[f32],
    192
    2
            m: u32,
    193
    2
            k: u32,
    194
    2
            n: u32,
    195
    2
        ) -> BackendResult<Vec<f32>> {
    196
    2
            let (m, k, n) = (m as usize, k as usize, n as usize);
    197
    198
            // Get cached weight
    199
    2
            let weight1 = self
    200
    2
                .weights
    201
    2
                .get(weight_name)
    202
    2
                .ok_or_else(|| format!1("Weight '{}' not found"1, weight_name))?1
    203
    1
                .clone();
    204
    205
            // Validate input dimensions
    206
    1
            if x.len() != m * k {
    207
    0
                return Err(format!(
    208
    0
                    "Input has {} elements, expected {}*{}={}",
    209
    0
                    x.len(),
    210
    0
                    m,
    211
    0
                    k,
    212
    0
                    m * k
    213
    0
                )
    214
    0
                .into());
    215
    1
            }
    216
    217
            // Weight is [k, n] stored row-major
    218
            // Compute C = X @ W where X is [m, k], W is [k, n]
    219
    1
            let mut c = vec![0.0f32; m * n];
    220
    1
            for i in 0..m {
    221
    2
                for j in 0..n1 {
    222
    2
                    let mut sum = 0.0f32;
    223
    6
                    for p in 0..k2 {
    224
    6
                        sum += x[i * k + p] * weight[p * n + j];
    225
    6
                    }
    226
    2
                    c[i * n + j] = sum;
    227
                }
    228
            }
    229
    230
    1
            Ok(c)
    231
    2
        }
    232
    233
    2
        fn q4k_gemv_cached(
    234
    2
            &mut self,
    235
    2
            weight_name: &str,
    236
    2
            input: &[f32],
    237
    2
            n: u32,
    238
    2
            k: u32,
    239
    2
        ) -> BackendResult<Vec<f32>> {
    240
    2
            let (n, k) = (n as usize, k as usize);
    241
    242
            // Verify weight exists and is quantized
    243
    2
            if !self.quantized_weights.contains_key(weight_name) {
    244
    1
                return Err(format!("Quantized weight '{}' not found", weight_name).into());
    245
    1
            }
    246
    247
            // Verify quantization type (Q4_K = 3 in GGML, but we accept various types for mock)
    248
    1
            let _qtype = self.quantized_types.get(weight_name).ok_or_else(|| {0
    249
    0
                format!(
    250
    0
                    "Quantization type not found for weight '{}'",
    251
                    weight_name
    252
                )
    253
    0
            })?;
    254
    255
            // Validate input dimensions
    256
    1
            if input.len() != k {
    257
    0
                return Err(format!(
    258
    0
                    "Input has {} elements, expected {}",
    259
    0
                    input.len(),
    260
    0
                    k
    261
    0
                )
    262
    0
                .into());
    263
    1
            }
    264
    265
            // Mock implementation: return zeros of correct size
    266
            // A real implementation would dequantize and compute
    267
            // For testing purposes, we return realistic-looking values
    268
            // by computing a simple weighted sum
    269
    1
            let mut output = vec![0.0f32; n];
    270
    271
            // Generate deterministic but non-trivial output for testing
    272
            // This helps catch bugs where outputs are checked but not computed
    273
    1
            let input_sum: f32 = input.iter().sum();
    274
    1
            let scale = input_sum / (k as f32).max(1.0);
    275
    276
    3.07k
            for i in 0..n1 {
    277
    3.07k
                // Deterministic pattern based on index and input
    278
    3.07k
                output[i] = scale * ((i % 7) as f32 - 3.0) * 0.1;
    279
    3.07k
            }
    280
    281
    1
            Ok(output)
    282
    2
        }
    283
    284
    1
        fn synchronize(&self) -> BackendResult<()> {
    285
            // Mock backend is synchronous, nothing to wait for
    286
    1
            Ok(())
    287
    1
        }
    288
    }
    289
    290
    // Implement Send for thread-safety (required by trait)
    291
    // SAFETY: MockBackend only contains thread-safe types (HashMap, String, Vec)
    292
    unsafe impl Send for MockBackend {}
    293
    294
    impl std::fmt::Debug for MockBackend {
    295
    1
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    296
    1
            f.debug_struct("MockBackend")
    297
    1
                .field("device_name", &self.device_name)
    298
    1
                .field("num_weights", &self.weights.len())
    299
    1
                .field("num_quantized_weights", &self.quantized_weights.len())
    300
    1
                .finish()
    301
    1
        }
    302
    }
    303
    304
    #[cfg(test)]
    305
    mod tests {
    306
        use super::*;
    307
    308
        #[test]
    309
    1
        fn test_mock_backend_is_always_available() {
    310
    1
            assert!(MockBackend::is_available());
    311
    1
        }
    312
    313
        #[test]
    314
    1
        fn test_mock_backend_creation() {
    315
    1
            let backend = MockBackend::new(0).expect("Mock backend should always succeed");
    316
    1
            assert_eq!(backend.device_name(), "MockGPU (CPU fallback)");
    317
    1
            assert_eq!(backend.cached_weight_count(), 0);
    318
    1
        }
    319
    320
        #[test]
    321
    1
        fn test_load_weights() {
    322
    1
            let mut backend = MockBackend::new_mock();
    323
    1
            let weights = vec![1.0, 2.0, 3.0, 4.0];
    324
    325
    1
            let handle = backend.load_weights("test", &weights).unwrap();
    326
    327
    1
            assert_eq!(handle, 4);
    328
    1
            assert!(backend.has_weights("test"));
    329
    1
            assert!(!backend.has_weights("nonexistent"));
    330
    1
            assert_eq!(backend.cached_weight_count(), 1);
    331
    1
            assert_eq!(backend.get_weights("test"), Some(&weights));
    332
    1
        }
    333
    334
        #[test]
    335
    1
        fn test_load_quantized_weights() {
    336
    1
            let mut backend = MockBackend::new_mock();
    337
    1
            let data = vec![0u8, 1, 2, 3, 4, 5, 6, 7];
    338
    339
    1
            let handle = backend.load_quantized_weights("q4_test", &data, 2).unwrap();
    340
    341
    1
            assert_eq!(handle, 8);
    342
    1
            assert!(backend.has_weights("q4_test"));
    343
    1
            assert_eq!(backend.get_quantized_weights("q4_test"), Some(&data));
    344
    1
            assert_eq!(backend.get_quant_type("q4_test"), Some(2));
    345
    1
        }
    346
    347
        #[test]
    348
    1
        fn test_clear_weights() {
    349
    1
            let mut backend = MockBackend::new_mock();
    350
    1
            backend.load_weights("test", &[1.0, 2.0]).unwrap();
    351
    1
            backend.load_quantized_weights("q_test", &[0u8; 18], 2).unwrap();
    352
    353
    1
            assert_eq!(backend.cached_weight_count(), 2);
    354
    355
    1
            backend.clear_weights();
    356
    357
    1
            assert_eq!(backend.cached_weight_count(), 0);
    358
    1
            assert!(!backend.has_weights("test"));
    359
    1
            assert!(!backend.has_weights("q_test"));
    360
    1
        }
    361
    362
        #[test]
    363
    1
        fn test_matmul_identity() {
    364
    1
            let mut backend = MockBackend::new_mock();
    365
    366
            // 2x2 identity @ 2x2 matrix = same matrix
    367
    1
            let identity = vec![1.0, 0.0, 0.0, 1.0];
    368
    1
            let matrix = vec![1.0, 2.0, 3.0, 4.0];
    369
    370
    1
            let result = backend.matmul(&identity, &matrix, 2, 2, 2).unwrap();
    371
    372
    1
            assert_eq!(result, matrix);
    373
    1
        }
    374
    375
        #[test]
    376
    1
        fn test_matmul_simple() {
    377
    1
            let mut backend = MockBackend::new_mock();
    378
    379
            // [1, 2] @ [[1], [2]] = [5]
    380
    1
            let a = vec![1.0, 2.0];
    381
    1
            let b = vec![1.0, 2.0];
    382
    383
    1
            let result = backend.matmul(&a, &b, 1, 2, 1).unwrap();
    384
    385
    1
            assert_eq!(result.len(), 1);
    386
    1
            assert!((result[0] - 5.0).abs() < 1e-6);
    387
    1
        }
    388
    389
        #[test]
    390
    1
        fn test_matmul_2x3_3x2() {
    391
    1
            let mut backend = MockBackend::new_mock();
    392
    393
            // A = [[1, 2, 3], [4, 5, 6]] (2x3)
    394
            // B = [[7, 8], [9, 10], [11, 12]] (3x2)
    395
            // C = A @ B = [[58, 64], [139, 154]] (2x2)
    396
    1
            let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
    397
    1
            let b = vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
    398
    399
    1
            let result = backend.matmul(&a, &b, 2, 3, 2).unwrap();
    400
    401
    1
            assert_eq!(result.len(), 4);
    402
    1
            assert!((result[0] - 58.0).abs() < 1e-5);
    403
    1
            assert!((result[1] - 64.0).abs() < 1e-5);
    404
    1
            assert!((result[2] - 139.0).abs() < 1e-5);
    405
    1
            assert!((result[3] - 154.0).abs() < 1e-5);
    406
    1
        }
    407
    408
        #[test]
    409
    1
        fn test_matmul_3x3_identity() {
    410
    1
            let mut backend = MockBackend::new_mock();
    411
    412
            // A = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    413
            // B = identity
    414
            // A @ B = A
    415
    1
            let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
    416
    1
            let identity = vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
    417
    418
    1
            let result = backend.matmul(&a, &identity, 3, 3, 3).unwrap();
    419
    420
    9
            for (i, (&expected, &actual)) in a.iter()1.zip1(result.iter()1).enumerate1() {
    421
    9
                assert!(
    422
    9
                    (expected - actual).abs() < 1e-6,
    423
    0
                    "Mismatch at index {}: expected {}, got {}",
    424
                    i,
    425
                    expected,
    426
                    actual
    427
                );
    428
            }
    429
    1
        }
    430
    431
        #[test]
    432
    1
        fn test_matmul_dimension_validation_a() {
    433
    1
            let mut backend = MockBackend::new_mock();
    434
    435
    1
            let a = vec![1.0, 2.0]; // 2 elements
    436
    1
            let b = vec![1.0, 2.0, 3.0, 4.0]; // 4 elements
    437
    438
            // This should fail: m=2, k=2 means a should have 4 elements
    439
    1
            let result = backend.matmul(&a, &b, 2, 2, 2);
    440
    1
            assert!(result.is_err());
    441
    1
        }
    442
    443
        #[test]
    444
    1
        fn test_matmul_dimension_validation_b() {
    445
    1
            let mut backend = MockBackend::new_mock();
    446
    447
    1
            let a = vec![1.0, 2.0, 3.0, 4.0]; // 4 elements (2x2)
    448
    1
            let b = vec![1.0, 2.0]; // 2 elements
    449
    450
            // This should fail: k=2, n=2 means b should have 4 elements
    451
    1
            let result = backend.matmul(&a, &b, 2, 2, 2);
    452
    1
            assert!(result.is_err());
    453
    1
        }
    454
    455
        #[test]
    456
    1
        fn test_matmul_cached() {
    457
    1
            let mut backend = MockBackend::new_mock();
    458
    459
            // Weight: 3x2 matrix stored as [k=3, n=2]
    460
            // [[1, 2], [3, 4], [5, 6]]
    461
    1
            let weight = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
    462
    1
            backend.load_weights("w", &weight).unwrap();
    463
    464
            // Input: 1x3 vector (m=1, k=3)
    465
    1
            let x = vec![1.0, 1.0, 1.0];
    466
    467
            // y = x @ W = [1*1+1*3+1*5, 1*2+1*4+1*6] = [9, 12]
    468
    1
            let result = backend.matmul_cached("w", &x, 1, 3, 2).unwrap();
    469
    470
    1
            assert_eq!(result.len(), 2);
    471
    1
            assert!((result[0] - 9.0).abs() < 1e-6);
    472
    1
            assert!((result[1] - 12.0).abs() < 1e-6);
    473
    1
        }
    474
    475
        #[test]
    476
    1
        fn test_matmul_cached_weight_not_found() {
    477
    1
            let mut backend = MockBackend::new_mock();
    478
    479
    1
            let x = vec![1.0, 2.0, 3.0];
    480
    1
            let result = backend.matmul_cached("nonexistent", &x, 1, 3, 2);
    481
    482
    1
            assert!(result.is_err());
    483
    1
        }
    484
    485
        #[test]
    486
    1
        fn test_q4k_gemv_cached() {
    487
    1
            let mut backend = MockBackend::new_mock();
    488
    489
            // Load quantized weight
    490
    1
            let q_data = vec![0u8; 256]; // Dummy quantized data
    491
    1
            backend.load_quantized_weights("q_weight", &q_data, 3).unwrap(); // 3 = Q4_K
    492
    493
    1
            let input = vec![1.0f32; 768];
    494
    1
            let result = backend.q4k_gemv_cached("q_weight", &input, 3072, 768).unwrap();
    495
    496
    1
            assert_eq!(result.len(), 3072);
    497
            // Result should be finite (no NaN/Inf)
    498
    3.07k
            assert!1(result.iter()1.all1(|x| x.is_finite()));
    499
    1
        }
    500
    501
        #[test]
    502
    1
        fn test_q4k_gemv_cached_not_found() {
    503
    1
            let mut backend = MockBackend::new_mock();
    504
    505
    1
            let input = vec![1.0f32; 768];
    506
    1
            let result = backend.q4k_gemv_cached("nonexistent", &input, 3072, 768);
    507
    508
    1
            assert!(result.is_err());
    509
    1
        }
    510
    511
        #[test]
    512
    1
        fn test_synchronize() {
    513
    1
            let backend = MockBackend::new_mock();
    514
    515
            // Should always succeed for mock backend
    516
    1
            backend.synchronize().unwrap();
    517
    1
        }
    518
    519
        #[test]
    520
    1
        fn test_debug_impl() {
    521
    1
            let mut backend = MockBackend::new_mock();
    522
    1
            backend.load_weights("test", &[1.0, 2.0]).unwrap();
    523
    524
    1
            let debug_str = format!("{:?}", backend);
    525
    526
    1
            assert!(debug_str.contains("MockBackend"));
    527
    1
            assert!(debug_str.contains("MockGPU"));
    528
    1
            assert!(debug_str.contains("num_weights"));
    529
    1
        }
    530
    531
        #[test]
    532
    1
        fn test_multiple_weights() {
    533
    1
            let mut backend = MockBackend::new_mock();
    534
    535
    1
            backend.load_weights("w1", &[1.0, 2.0]).unwrap();
    536
    1
            backend.load_weights("w2", &[3.0, 4.0]).unwrap();
    537
    1
            backend.load_quantized_weights("q1", &[0u8; 18], 2).unwrap();
    538
    539
    1
            assert_eq!(backend.cached_weight_count(), 3);
    540
    1
            assert!(backend.has_weights("w1"));
    541
    1
            assert!(backend.has_weights("w2"));
    542
    1
            assert!(backend.has_weights("q1"));
    543
    1
        }
    544
    545
        #[test]
    546
    1
        fn test_overwrite_weight() {
    547
    1
            let mut backend = MockBackend::new_mock();
    548
    549
    1
            backend.load_weights("test", &[1.0, 2.0]).unwrap();
    550
    1
            assert_eq!(backend.get_weights("test"), Some(&vec![1.0, 2.0]));
    551
    552
            // Overwrite with different values
    553
    1
            backend.load_weights("test", &[3.0, 4.0, 5.0]).unwrap();
    554
    1
            assert_eq!(backend.get_weights("test"), Some(&vec![3.0, 4.0, 5.0]));
    555
    556
            // Count should still be 1 (not 2)
    557
    1
            assert_eq!(backend.weights.len(), 1);
    558
    1
        }
    559
    }