/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 | | } |