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
}