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
}