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/metrics.rs
Line
Count
Source
1
//! Metrics & Health Monitoring (PMAT-802)
2
//!
3
//! M28: InferenceMetrics, HealthChecker, ShutdownCoordinator, GpuCompute, HybridScheduler.
4
5
use crate::error::{RealizarError, Result};
6
use crate::tensor::Tensor;
7
use super::MatmulOp;
8
9
// =============================================================================
10
// M28: Metrics & Health Monitoring (Phase 19)
11
// =============================================================================
12
13
/// Inference metrics collector (M28 - IMP-067)
14
///
15
/// Collects and aggregates inference performance metrics including
16
/// latency distribution and throughput.
17
#[derive(Debug)]
18
pub struct InferenceMetrics {
19
    latencies: Vec<std::time::Duration>,
20
    total_tokens: u64,
21
    start_time: std::time::Instant,
22
}
23
24
impl InferenceMetrics {
25
    /// Create a new inference metrics collector
26
    #[must_use]
27
12
    pub fn new() -> Self {
28
12
        Self {
29
12
            latencies: Vec::new(),
30
12
            total_tokens: 0,
31
12
            start_time: std::time::Instant::now(),
32
12
        }
33
12
    }
34
35
    /// Get total number of recorded inferences
36
    #[must_use]
37
11
    pub fn total_inferences(&self) -> usize {
38
11
        self.latencies.len()
39
11
    }
40
41
    /// Get total number of tokens processed
42
    #[must_use]
43
10
    pub fn total_tokens(&self) -> u64 {
44
10
        self.total_tokens
45
10
    }
46
47
    /// Record an inference with its latency and token count
48
24
    pub fn record_inference(&mut self, latency: std::time::Duration, tokens: usize) {
49
24
        self.latencies.push(latency);
50
24
        self.total_tokens += tokens as u64;
51
24
    }
52
53
    /// Get latency at given percentile (0-100)
54
    ///
55
    /// Returns None if no inferences recorded.
56
    #[must_use]
57
9
    pub fn latency_percentile(&self, percentile: u8) -> Option<std::time::Duration> {
58
9
        if self.latencies.is_empty() {
59
3
            return None;
60
6
        }
61
62
6
        let mut sorted = self.latencies.clone();
63
6
        sorted.sort();
64
65
6
        let idx = ((percentile as usize) * sorted.len() / 100).min(sorted.len() - 1);
66
6
        Some(sorted[idx])
67
9
    }
68
69
    /// Calculate throughput in tokens per second
70
    #[must_use]
71
2
    pub fn throughput(&self) -> f64 {
72
2
        let elapsed = self.start_time.elapsed().as_secs_f64();
73
2
        if elapsed > 0.0 {
74
2
            self.total_tokens as f64 / elapsed
75
        } else {
76
0
            0.0
77
        }
78
2
    }
79
80
    /// Reset all metrics
81
2
    pub fn reset(&mut self) {
82
2
        self.latencies.clear();
83
2
        self.total_tokens = 0;
84
2
        self.start_time = std::time::Instant::now();
85
2
    }
86
}
87
88
impl Default for InferenceMetrics {
89
1
    fn default() -> Self {
90
1
        Self::new()
91
1
    }
92
}
93
94
/// Type alias for health check function
95
pub type HealthCheckFn = Box<dyn Fn() -> bool + Send + Sync>;
96
97
/// Health checker for system components (M28 - IMP-068)
98
///
99
/// Monitors health status of system components via registered check functions.
100
pub struct HealthChecker {
101
    checks: Vec<(String, HealthCheckFn)>,
102
    last_results: std::collections::HashMap<String, bool>,
103
}
104
105
impl std::fmt::Debug for HealthChecker {
106
2
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107
2
        f.debug_struct("HealthChecker")
108
2
            .field("check_count", &self.checks.len())
109
2
            .field("last_results", &self.last_results)
110
2
            .finish()
111
2
    }
112
}
113
114
impl HealthChecker {
115
    /// Create a new health checker
116
    #[must_use]
117
13
    pub fn new() -> Self {
118
13
        Self {
119
13
            checks: Vec::new(),
120
13
            last_results: std::collections::HashMap::new(),
121
13
        }
122
13
    }
123
124
    /// Get number of registered checks
125
    #[must_use]
126
6
    pub fn check_count(&self) -> usize {
127
6
        self.checks.len()
128
6
    }
129
130
    /// Register a health check function
131
14
    pub fn register_check(&mut self, name: &str, check: HealthCheckFn) {
132
14
        self.checks.push((name.to_string(), check));
133
14
    }
134
135
    /// Run all health checks and return results
136
8
    pub fn check_all(&mut self) -> std::collections::HashMap<String, bool> {
137
8
        let mut results = std::collections::HashMap::new();
138
22
        for (
name14
,
check14
) in &self.checks {
139
14
            let healthy = check();
140
14
            results.insert(name.clone(), healthy);
141
14
        }
142
8
        self.last_results.clone_from(&results);
143
8
        results
144
8
    }
145
146
    /// Check if system is overall healthy (all checks pass)
147
    #[must_use]
148
12
    pub fn is_healthy(&self) -> bool {
149
12
        if self.checks.is_empty() {
150
6
            return true;
151
6
        }
152
6
        self.last_results.values().all(|&v| v)
153
12
    }
154
155
    /// Clear all registered checks
156
2
    pub fn clear(&mut self) {
157
2
        self.checks.clear();
158
2
        self.last_results.clear();
159
2
    }
160
}
161
162
impl Default for HealthChecker {
163
1
    fn default() -> Self {
164
1
        Self::new()
165
1
    }
166
}
167
168
/// Type alias for shutdown handler function
169
pub type ShutdownHandlerFn = Box<dyn Fn() + Send + Sync>;
170
171
/// Graceful shutdown coordinator (M28 - IMP-069)
172
///
173
/// Coordinates shutdown sequence with request draining and handler callbacks.
174
pub struct ShutdownCoordinator {
175
    shutting_down: bool,
176
    pending_requests: u32,
177
    handlers: Vec<ShutdownHandlerFn>,
178
}
179
180
impl std::fmt::Debug for ShutdownCoordinator {
181
2
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182
2
        f.debug_struct("ShutdownCoordinator")
183
2
            .field("shutting_down", &self.shutting_down)
184
2
            .field("pending_requests", &self.pending_requests)
185
2
            .field("handler_count", &self.handlers.len())
186
2
            .finish()
187
2
    }
188
}
189
190
impl ShutdownCoordinator {
191
    /// Create a new shutdown coordinator
192
    #[must_use]
193
11
    pub fn new() -> Self {
194
11
        Self {
195
11
            shutting_down: false,
196
11
            pending_requests: 0,
197
11
            handlers: Vec::new(),
198
11
        }
199
11
    }
200
201
    /// Check if shutdown has been initiated
202
    #[must_use]
203
6
    pub fn is_shutting_down(&self) -> bool {
204
6
        self.shutting_down
205
6
    }
206
207
    /// Get number of pending requests
208
    #[must_use]
209
12
    pub fn pending_requests(&self) -> u32 {
210
12
        self.pending_requests
211
12
    }
212
213
    /// Get number of registered handlers
214
    #[must_use]
215
4
    pub fn handler_count(&self) -> usize {
216
4
        self.handlers.len()
217
4
    }
218
219
    /// Register a shutdown handler
220
5
    pub fn register_handler(&mut self, handler: ShutdownHandlerFn) {
221
5
        self.handlers.push(handler);
222
5
    }
223
224
    /// Mark that a request has started
225
5
    pub fn request_started(&mut self) {
226
5
        self.pending_requests += 1;
227
5
    }
228
229
    /// Mark that a request has completed
230
7
    pub fn request_completed(&mut self) {
231
7
        self.pending_requests = self.pending_requests.saturating_sub(1);
232
7
    }
233
234
    /// Initiate shutdown sequence
235
    ///
236
    /// Calls all registered handlers.
237
6
    pub fn initiate_shutdown(&mut self) {
238
6
        if self.shutting_down {
239
2
            return;
240
4
        }
241
4
        self.shutting_down = true;
242
243
        // Call all handlers
244
7
        for 
handler3
in &self.handlers {
245
3
            handler();
246
3
        }
247
6
    }
248
249
    /// Check if shutdown is complete (initiated + no pending requests)
250
    #[must_use]
251
4
    pub fn is_complete(&self) -> bool {
252
4
        self.shutting_down && 
self.pending_requests == 03
253
4
    }
254
}
255
256
impl Default for ShutdownCoordinator {
257
1
    fn default() -> Self {
258
1
        Self::new()
259
1
    }
260
}
261
262
/// Compute backend selection
263
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
264
pub enum ComputeBackend {
265
    /// GPU compute via trueno's wgpu backend
266
    Gpu,
267
    /// CPU compute (fallback)
268
    Cpu,
269
    /// Auto-select best available backend
270
    #[default]
271
    Auto,
272
}
273
274
/// GPU compute context
275
///
276
/// Provides GPU-accelerated operations with automatic fallback to CPU
277
/// when GPU is not available.
278
pub struct GpuCompute {
279
    backend: ComputeBackend,
280
    gpu: Option<trueno::backends::gpu::GpuBackend>,
281
}
282
283
impl GpuCompute {
284
    /// Create GPU compute context with auto-detected backend
285
    ///
286
    /// Attempts to initialize GPU backend, falls back to CPU if unavailable.
287
    ///
288
    /// # Errors
289
    ///
290
    /// Returns error if both GPU and CPU initialization fail (should not happen).
291
153
    pub fn auto() -> Result<Self> {
292
153
        Self::new(ComputeBackend::Auto)
293
153
    }
294
295
    /// Create GPU compute context with specified backend
296
    ///
297
    /// # Arguments
298
    ///
299
    /// * `backend` - Backend selection (Gpu, Cpu, or Auto)
300
    ///
301
    /// # Errors
302
    ///
303
    /// Returns error if:
304
    /// - `Gpu` backend requested but GPU is not available
305
    /// - Backend initialization fails
306
177
    pub fn new(backend: ComputeBackend) -> Result<Self> {
307
177
        match backend {
308
            ComputeBackend::Gpu => {
309
2
                if trueno::backends::gpu::GpuBackend::is_available() {
310
2
                    Ok(Self {
311
2
                        backend: ComputeBackend::Gpu,
312
2
                        gpu: Some(trueno::backends::gpu::GpuBackend::new()),
313
2
                    })
314
                } else {
315
0
                    Err(RealizarError::GpuError {
316
0
                        reason: "GPU not available".to_string(),
317
0
                    })
318
                }
319
            },
320
22
            ComputeBackend::Cpu => Ok(Self {
321
22
                backend: ComputeBackend::Cpu,
322
22
                gpu: None,
323
22
            }),
324
            ComputeBackend::Auto => {
325
153
                if trueno::backends::gpu::GpuBackend::is_available() {
326
153
                    Ok(Self {
327
153
                        backend: ComputeBackend::Gpu,
328
153
                        gpu: Some(trueno::backends::gpu::GpuBackend::new()),
329
153
                    })
330
                } else {
331
0
                    Ok(Self {
332
0
                        backend: ComputeBackend::Cpu,
333
0
                        gpu: None,
334
0
                    })
335
                }
336
            },
337
        }
338
177
    }
339
340
    /// Check if GPU backend is active
341
    #[must_use]
342
879
    pub fn is_gpu(&self) -> bool {
343
879
        self.backend == ComputeBackend::Gpu && 
self.gpu878
.
is_some878
()
344
879
    }
345
346
    /// Get active backend type
347
    #[must_use]
348
3
    pub fn backend(&self) -> ComputeBackend {
349
3
        self.backend
350
3
    }
351
352
    /// GPU-accelerated matrix multiplication
353
    ///
354
    /// Computes `C = A @ B` where:
355
    /// - A is `[m, k]`
356
    /// - B is `[k, n]`
357
    /// - C is `[m, n]`
358
    ///
359
    /// # Arguments
360
    ///
361
    /// * `a` - Left matrix as flat f32 slice, row-major `[m, k]`
362
    /// * `b` - Right matrix as flat f32 slice, row-major `[k, n]`
363
    /// * `m` - Rows in A and C
364
    /// * `k` - Cols in A, rows in B
365
    /// * `n` - Cols in B and C
366
    ///
367
    /// # Errors
368
    ///
369
    /// Returns error if:
370
    /// - Input dimensions don't match
371
    /// - GPU compute fails
372
    #[allow(clippy::many_single_char_names)]
373
458
    pub fn matmul(
374
458
        &mut self,
375
458
        a: &[f32],
376
458
        b: &[f32],
377
458
        m: usize,
378
458
        k: usize,
379
458
        n: usize,
380
458
    ) -> Result<Vec<f32>> {
381
        // Validate dimensions
382
458
        if a.len() != m * k {
383
1
            return Err(RealizarError::InvalidShape {
384
1
                reason: format!(
385
1
                    "Matrix A size {} doesn't match m*k={}*{}={}",
386
1
                    a.len(),
387
1
                    m,
388
1
                    k,
389
1
                    m * k
390
1
                ),
391
1
            });
392
457
        }
393
457
        if b.len() != k * n {
394
0
            return Err(RealizarError::InvalidShape {
395
0
                reason: format!(
396
0
                    "Matrix B size {} doesn't match k*n={}*{}={}",
397
0
                    b.len(),
398
0
                    k,
399
0
                    n,
400
0
                    k * n
401
0
                ),
402
0
            });
403
457
        }
404
405
457
        if let Some(
gpu438
) = &mut self.gpu {
406
            // GPU path
407
            #[allow(clippy::implicit_clone)]
408
438
            gpu.matmul(a, b, m, k, n)
409
438
                .map_err(|e| RealizarError::GpuError {
410
0
                    reason: e.to_string(),
411
0
                })
412
        } else {
413
            // CPU fallback: naive matmul
414
19
            Ok(cpu_matmul(a, b, m, k, n))
415
        }
416
458
    }
417
418
    /// GPU-accelerated matrix multiplication with Tensor input/output
419
    ///
420
    /// # Arguments
421
    ///
422
    /// * `a` - Left tensor `[m, k]`
423
    /// * `b` - Right tensor `[k, n]`
424
    ///
425
    /// # Errors
426
    ///
427
    /// Returns error if tensors are not 2D or dimensions don't match.
428
    #[allow(clippy::many_single_char_names)]
429
2
    pub fn matmul_tensor(&mut self, a: &Tensor<f32>, b: &Tensor<f32>) -> Result<Tensor<f32>> {
430
2
        let a_shape = a.shape();
431
2
        let b_shape = b.shape();
432
433
2
        if a_shape.len() != 2 || b_shape.len() != 2 {
434
0
            return Err(RealizarError::InvalidShape {
435
0
                reason: "matmul_tensor requires 2D tensors".to_string(),
436
0
            });
437
2
        }
438
439
2
        let m = a_shape[0];
440
2
        let k = a_shape[1];
441
2
        let k2 = b_shape[0];
442
2
        let n = b_shape[1];
443
444
2
        if k != k2 {
445
1
            return Err(RealizarError::InvalidShape {
446
1
                reason: format!("Inner dimensions don't match: A[{m},{k}] @ B[{k2},{n}]"),
447
1
            });
448
1
        }
449
450
1
        let result = self.matmul(a.data(), b.data(), m, k, n)
?0
;
451
1
        Tensor::from_vec(vec![m, n], result)
452
2
    }
453
454
    /// GPU-accelerated vector dot product
455
    ///
456
    /// # Errors
457
    ///
458
    /// Returns error if vectors have different lengths or GPU compute fails.
459
3
    pub fn dot(&mut self, a: &[f32], b: &[f32]) -> Result<f32> {
460
3
        if a.len() != b.len() {
461
1
            return Err(RealizarError::InvalidShape {
462
1
                reason: format!("Vector lengths don't match: {} vs {}", a.len(), b.len()),
463
1
            });
464
2
        }
465
466
2
        if let Some(
gpu0
) = &mut self.gpu {
467
            #[allow(clippy::implicit_clone)]
468
0
            gpu.dot(a, b).map_err(|e| RealizarError::GpuError {
469
0
                reason: e.to_string(),
470
0
            })
471
        } else {
472
            // CPU fallback
473
3
            Ok(
a2
.
iter2
().
zip2
(
b2
.
iter2
()).
map2
(|(x, y)| x * y).
sum2
())
474
        }
475
3
    }
476
477
    /// GPU-accelerated ReLU activation
478
    ///
479
    /// # Errors
480
    ///
481
    /// Returns error if GPU compute fails.
482
3
    pub fn relu(&mut self, input: &[f32]) -> Result<Vec<f32>> {
483
3
        if let Some(
gpu0
) = &mut self.gpu {
484
            #[allow(clippy::implicit_clone)]
485
0
            gpu.relu(input).map_err(|e| RealizarError::GpuError {
486
0
                reason: e.to_string(),
487
0
            })
488
        } else {
489
5
            Ok(
input3
.
iter3
().
map3
(|&x| x.max(0.0)).
collect3
())
490
        }
491
3
    }
492
493
    /// GPU-accelerated sigmoid activation
494
    ///
495
    /// # Errors
496
    ///
497
    /// Returns error if GPU compute fails.
498
2
    pub fn sigmoid(&mut self, input: &[f32]) -> Result<Vec<f32>> {
499
2
        if let Some(
gpu0
) = &mut self.gpu {
500
            #[allow(clippy::implicit_clone)]
501
0
            gpu.sigmoid(input).map_err(|e| RealizarError::GpuError {
502
0
                reason: e.to_string(),
503
0
            })
504
        } else {
505
4
            Ok(
input2
.
iter2
().
map2
(|&x| 1.0 / (1.0 + (-x).exp())).
collect2
())
506
        }
507
2
    }
508
}
509
510
/// CPU fallback matmul implementation
511
#[allow(clippy::many_single_char_names)]
512
7.99k
pub(crate) fn cpu_matmul(a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
513
    // For m=1 (vector-matrix multiply), use optimized path
514
7.99k
    if m == 1 {
515
7.58k
        return cpu_vector_matmul(a, b, k, n);
516
412
    }
517
518
412
    let mut c = vec![0.0; m * n];
519
4.80k
    for i in 0..
m412
{
520
894k
        for j in 0..
n4.80k
{
521
894k
            let mut sum = 0.0;
522
199M
            for p in 0..
k894k
{
523
199M
                sum += a[i * k + p] * b[p * n + j];
524
199M
            }
525
894k
            c[i * n + j] = sum;
526
        }
527
    }
528
412
    c
529
7.99k
}
530
531
/// IMP-098: Parallelized vector-matrix multiply: a[1,k] @ b[k,n] -> c[1,n]
532
///
533
/// Uses parallel output chunks for multi-core utilization.
534
/// Each thread accumulates its chunk of outputs independently.
535
#[allow(clippy::many_single_char_names)]
536
7.58k
fn cpu_vector_matmul(a: &[f32], b: &[f32], k: usize, n: usize) -> Vec<f32> {
537
    use rayon::prelude::*;
538
539
    // For small n, use sequential (avoids rayon overhead)
540
7.58k
    if n < 2048 {
541
7.58k
        return cpu_vector_matmul_seq(a, b, k, n);
542
0
    }
543
544
    // Parallel over output chunks
545
    const CHUNK_SIZE: usize = 1024;
546
0
    let num_chunks = n.div_ceil(CHUNK_SIZE);
547
548
0
    let chunks: Vec<Vec<f32>> = (0..num_chunks)
549
0
        .into_par_iter()
550
0
        .map(|chunk_idx| {
551
0
            let start = chunk_idx * CHUNK_SIZE;
552
0
            let end = (start + CHUNK_SIZE).min(n);
553
0
            let chunk_len = end - start;
554
0
            let mut chunk_c = vec![0.0f32; chunk_len];
555
556
            // Accumulate this chunk of outputs
557
0
            for (p, &a_val) in a.iter().enumerate() {
558
0
                let row_start = p * n + start;
559
0
                let row = &b[row_start..row_start + chunk_len];
560
0
                for (j, &b_val) in row.iter().enumerate() {
561
0
                    chunk_c[j] += a_val * b_val;
562
0
                }
563
            }
564
0
            chunk_c
565
0
        })
566
0
        .collect();
567
568
    // Flatten chunks into result
569
0
    chunks.into_iter().flatten().collect()
570
7.58k
}
571
572
/// Sequential fallback for small outputs
573
#[allow(clippy::many_single_char_names)]
574
7.58k
fn cpu_vector_matmul_seq(a: &[f32], b: &[f32], _k: usize, n: usize) -> Vec<f32> {
575
7.58k
    let mut c = vec![0.0f32; n];
576
577
    // Row-major accumulation: for each row of B, scale by corresponding a[p]
578
960k
    for (p, &a_val) in 
a7.58k
.
iter7.58k
().
enumerate7.58k
() {
579
960k
        let row = &b[p * n..(p + 1) * n];
580
181M
        for (j, &b_val) in 
row960k
.
iter960k
().
enumerate960k
() {
581
181M
            c[j] += a_val * b_val;
582
181M
        }
583
    }
584
585
7.58k
    c
586
7.58k
}
587
588
/// CPU matmul with B transposed: A @ B^T
589
/// a[m,k] @ b[n,k]^T -> c[m,n]
590
#[allow(clippy::many_single_char_names)]
591
41
pub(crate) fn cpu_matmul_transpose_b(a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
592
41
    let mut c = vec![0.0; m * n];
593
130
    for i in 0..
m41
{
594
484
        for j in 0..
n130
{
595
484
            let mut sum = 0.0;
596
14.9k
            for p in 0..
k484
{
597
14.9k
                // a[i,p] * b[j,p] (b is stored row-major as [n,k])
598
14.9k
                sum += a[i * k + p] * b[j * k + p];
599
14.9k
            }
600
484
            c[i * n + j] = sum;
601
        }
602
    }
603
41
    c
604
41
}
605
606
/// Transpose a matrix from [rows, cols] to [cols, rows]
607
0
pub(crate) fn transpose(data: &[f32], rows: usize, cols: usize) -> Vec<f32> {
608
0
    let mut result = vec![0.0; data.len()];
609
0
    for i in 0..rows {
610
0
        for j in 0..cols {
611
0
            result[j * rows + i] = data[i * cols + j];
612
0
        }
613
    }
614
0
    result
615
0
}
616
617
/// IMP-096: Parallel SIMD vector-matrix multiply using transposed weights
618
///
619
/// Computes a[1,k] @ weight_t[n,k]^T + bias[n] -> c[n]
620
/// Each output c[j] = dot(a, weight_t[j,:]) + bias[j]
621
///
622
/// Uses transposed weights for row-major access pattern (contiguous dot products).
623
/// Parallelized with rayon. Compiler auto-vectorizes the inner dot product.
624
#[allow(clippy::many_single_char_names)]
625
0
pub(crate) fn cpu_matmul_transposed_simd(
626
0
    a: &[f32],        // Input vector: [k]
627
0
    weight_t: &[f32], // Transposed weights: [n, k] (row-major)
628
0
    bias: &[f32],     // Bias: [n]
629
0
    k: usize,
630
0
    n: usize,
631
0
) -> Vec<f32> {
632
    use rayon::prelude::*;
633
634
    // Process in chunks for better parallelism and cache locality
635
    const CHUNK_SIZE: usize = 4096;
636
637
0
    (0..n)
638
0
        .into_par_iter()
639
0
        .step_by(CHUNK_SIZE)
640
0
        .flat_map(|chunk_start| {
641
0
            let chunk_end = (chunk_start + CHUNK_SIZE).min(n);
642
0
            (chunk_start..chunk_end)
643
0
                .map(|j| {
644
                    // Row-major access: weight_t[j, :] is contiguous in memory
645
0
                    let row = &weight_t[j * k..(j + 1) * k];
646
647
                    // Compiler auto-vectorizes this dot product pattern
648
0
                    let dot: f32 = row.iter().zip(a.iter()).map(|(&w, &h)| w * h).sum();
649
0
                    dot + bias[j]
650
0
                })
651
0
                .collect::<Vec<_>>()
652
0
        })
653
0
        .collect()
654
0
}
655
656
/// GPU buffer pool for memory reuse and reduced allocation overhead
657
pub struct GpuBufferPool {
658
    /// Available buffers indexed by size bucket
659
    available_buffers: std::collections::HashMap<usize, Vec<Vec<f32>>>,
660
    /// Size buckets for efficient pooling (powers of 2)
661
    bucket_sizes: Vec<usize>,
662
    /// Maximum cached buffers per bucket
663
    max_per_bucket: usize,
664
}
665
666
impl GpuBufferPool {
667
    /// Create new buffer pool with default configuration
668
    #[must_use]
669
167
    pub fn new() -> Self {
670
        Self {
671
167
            available_buffers: std::collections::HashMap::new(),
672
2.50k
            bucket_sizes: (
10..=24167
).
map167
(|i| 1 << i).
collect167
(), // 1KB to 16MB
673
            max_per_bucket: 4,
674
        }
675
167
    }
676
677
    /// Get bucket size for requested allocation
678
42
    fn get_bucket(&self, size: usize) -> usize {
679
42
        *self
680
42
            .bucket_sizes
681
42
            .iter()
682
68
            .
find42
(|&&b| b >= size)
683
42
            .unwrap_or(&size)
684
42
    }
685
686
    /// Acquire buffer of at least `size` elements
687
24
    pub fn acquire(&mut self, size: usize) -> Vec<f32> {
688
24
        let bucket = self.get_bucket(size);
689
24
        if let Some(
buffers4
) = self.available_buffers.get_mut(&bucket) {
690
4
            if let Some(mut buf) = buffers.pop() {
691
4
                buf.resize(size, 0.0);
692
4
                return buf;
693
0
            }
694
20
        }
695
20
        vec![0.0; size]
696
24
    }
697
698
    /// Release buffer back to pool for reuse
699
18
    pub fn release(&mut self, mut buffer: Vec<f32>) {
700
18
        let bucket = self.get_bucket(buffer.capacity());
701
18
        let buffers = self.available_buffers.entry(bucket).or_default();
702
18
        if buffers.len() < self.max_per_bucket {
703
18
            buffer.clear();
704
18
            buffers.push(buffer);
705
18
        
}0
706
        // Otherwise just drop it
707
18
    }
708
709
    /// Clear all cached buffers
710
2
    pub fn clear(&mut self) {
711
2
        self.available_buffers.clear();
712
2
    }
713
714
    /// Get configured bucket sizes
715
    #[must_use]
716
2
    pub fn bucket_sizes(&self) -> &[usize] {
717
2
        &self.bucket_sizes
718
2
    }
719
720
    /// Get pool statistics
721
    #[must_use]
722
14
    pub fn stats(&self) -> GpuPoolStats {
723
14
        let total_buffers: usize = self.available_buffers.values().map(Vec::len).sum();
724
14
        let total_bytes: usize = self
725
14
            .available_buffers
726
14
            .iter()
727
14
            .map(|(bucket, buffers)| 
bucket11
* buffers.len() * 4)
728
14
            .sum();
729
14
        GpuPoolStats {
730
14
            cached_buffers: total_buffers,
731
14
            cached_bytes: total_bytes,
732
14
        }
733
14
    }
734
}
735
736
impl Default for GpuBufferPool {
737
1
    fn default() -> Self {
738
1
        Self::new()
739
1
    }
740
}
741
742
/// GPU buffer pool statistics
743
#[derive(Debug, Clone, Copy)]
744
pub struct GpuPoolStats {
745
    /// Number of cached buffers
746
    pub cached_buffers: usize,
747
    /// Total cached bytes
748
    pub cached_bytes: usize,
749
}
750
751
/// Async GPU compute handle for non-blocking operations
752
///
753
/// Per spec: "Async transfer - No host blocking"
754
pub struct AsyncGpuResult {
755
    /// Result data when ready
756
    result: Option<Vec<f32>>,
757
    /// Whether computation is complete
758
    ready: bool,
759
}
760
761
impl AsyncGpuResult {
762
    /// Create result that's immediately ready (CPU fallback)
763
6
    pub fn ready(data: Vec<f32>) -> Self {
764
6
        Self {
765
6
            result: Some(data),
766
6
            ready: true,
767
6
        }
768
6
    }
769
770
    /// Create pending result (GPU async)
771
5
    pub fn pending() -> Self {
772
5
        Self {
773
5
            result: None,
774
5
            ready: false,
775
5
        }
776
5
    }
777
778
    /// Check if result is ready
779
    #[must_use]
780
9
    pub fn is_ready(&self) -> bool {
781
9
        self.ready
782
9
    }
783
784
    /// Mark as ready with result
785
4
    pub fn set_result(&mut self, data: Vec<f32>) {
786
4
        self.result = Some(data);
787
4
        self.ready = true;
788
4
    }
789
790
    /// Block until result is ready (for synchronization points)
791
6
    pub fn wait(self) -> Vec<f32> {
792
6
        self.result.expect("Result not ready")
793
6
    }
794
795
    /// Try to get result without blocking
796
5
    pub fn try_get(&self) -> Option<&Vec<f32>> {
797
5
        if self.ready {
798
3
            self.result.as_ref()
799
        } else {
800
2
            None
801
        }
802
5
    }
803
}
804
805
/// Hybrid CPU/GPU scheduler
806
///
807
/// Automatically selects optimal backend based on workload size.
808
pub struct HybridScheduler {
809
    gpu_compute: GpuCompute,
810
    /// Minimum matrix size (m*k*n) to use GPU
811
    gpu_threshold: usize,
812
    /// Buffer pool for memory reuse
813
    buffer_pool: GpuBufferPool,
814
}
815
816
impl HybridScheduler {
817
    /// Create hybrid scheduler with auto-detected GPU
818
    ///
819
    /// # Errors
820
    ///
821
    /// Returns error if compute initialization fails.
822
36
    pub fn new() -> Result<Self> {
823
        Ok(Self {
824
36
            gpu_compute: GpuCompute::auto()
?0
,
825
36
            gpu_threshold: 64 * 64 * 64, // 262K elements
826
36
            buffer_pool: GpuBufferPool::new(),
827
        })
828
36
    }
829
830
    /// Create scheduler with custom threshold
831
    ///
832
    /// # Arguments
833
    ///
834
    /// * `gpu_threshold` - Minimum m*k*n to trigger GPU acceleration
835
    ///
836
    /// # Errors
837
    ///
838
    /// Returns error if compute initialization fails.
839
115
    pub fn with_threshold(gpu_threshold: usize) -> Result<Self> {
840
        Ok(Self {
841
115
            gpu_compute: GpuCompute::auto()
?0
,
842
115
            gpu_threshold,
843
115
            buffer_pool: GpuBufferPool::new(),
844
        })
845
115
    }
846
847
    /// Check if GPU is available
848
    #[must_use]
849
11
    pub fn has_gpu(&self) -> bool {
850
11
        self.gpu_compute.is_gpu()
851
11
    }
852
853
    /// Get GPU threshold
854
    #[must_use]
855
4
    pub fn gpu_threshold(&self) -> usize {
856
4
        self.gpu_threshold
857
4
    }
858
859
    /// Decide whether to use GPU for given workload
860
    ///
861
    /// IMP-097: For m=1 (single-token inference), CPU is faster due to:
862
    /// - No GPU data transfer overhead
863
    /// - No kernel launch latency
864
    /// - CPU SIMD is sufficient for vector-matrix multiply
865
    #[must_use]
866
    #[allow(clippy::many_single_char_names)]
867
8.45k
    pub fn should_use_gpu(&self, m: usize, k: usize, n: usize) -> bool {
868
        // IMP-097: Force CPU for single-token operations (m=1)
869
        // GPU kernel launch overhead exceeds compute benefit for small batch sizes
870
8.45k
        if m <= 1 {
871
7.59k
            return false;
872
866
        }
873
866
        self.gpu_compute.is_gpu() && (m * k * n) >= self.gpu_threshold
874
8.45k
    }
875
876
    /// Execute matmul with automatic backend selection
877
    ///
878
    /// Uses GPU for large matrices, CPU for small ones.
879
    ///
880
    /// # Errors
881
    ///
882
    /// Returns error if compute fails.
883
    #[allow(clippy::many_single_char_names)]
884
8.39k
    pub fn matmul(
885
8.39k
        &mut self,
886
8.39k
        a: &[f32],
887
8.39k
        b: &[f32],
888
8.39k
        m: usize,
889
8.39k
        k: usize,
890
8.39k
        n: usize,
891
8.39k
    ) -> Result<Vec<f32>> {
892
8.39k
        if self.should_use_gpu(m, k, n) {
893
426
            self.gpu_compute.matmul(a, b, m, k, n)
894
        } else {
895
7.96k
            Ok(cpu_matmul(a, b, m, k, n))
896
        }
897
8.39k
    }
898
899
    /// Execute matmul with pooled output buffer
900
    ///
901
    /// Reduces allocation overhead by reusing buffers.
902
    ///
903
    /// # Errors
904
    ///
905
    /// Returns error if compute fails.
906
    #[allow(clippy::many_single_char_names)]
907
4
    pub fn matmul_pooled(
908
4
        &mut self,
909
4
        a: &[f32],
910
4
        b: &[f32],
911
4
        m: usize,
912
4
        k: usize,
913
4
        n: usize,
914
4
    ) -> Result<Vec<f32>> {
915
        // Acquire buffer from pool
916
4
        let mut output = self.buffer_pool.acquire(m * n);
917
918
        // Compute result
919
4
        let result = if self.should_use_gpu(m, k, n) {
920
0
            self.gpu_compute.matmul(a, b, m, k, n)?
921
        } else {
922
4
            cpu_matmul(a, b, m, k, n)
923
        };
924
925
        // Copy to pooled buffer
926
4
        output.copy_from_slice(&result);
927
4
        Ok(output)
928
4
    }
929
930
    /// Release buffer back to pool
931
    ///
932
    /// Call this when done with a buffer returned by `matmul_pooled`.
933
4
    pub fn release_buffer(&mut self, buffer: Vec<f32>) {
934
4
        self.buffer_pool.release(buffer);
935
4
    }
936
937
    /// Get buffer pool statistics
938
    #[must_use]
939
3
    pub fn pool_stats(&self) -> GpuPoolStats {
940
3
        self.buffer_pool.stats()
941
3
    }
942
943
    /// Execute matmul asynchronously (non-blocking on CPU fallback)
944
    ///
945
    /// Per spec: "Async transfer - No host blocking"
946
    ///
947
    /// # Errors
948
    ///
949
    /// Returns error if compute setup fails.
950
    #[allow(clippy::many_single_char_names)]
951
1
    pub fn matmul_async(
952
1
        &mut self,
953
1
        a: &[f32],
954
1
        b: &[f32],
955
1
        m: usize,
956
1
        k: usize,
957
1
        n: usize,
958
1
    ) -> Result<AsyncGpuResult> {
959
        // For CPU fallback, compute immediately
960
        // For GPU, this would submit to command queue without blocking
961
1
        let result = if self.should_use_gpu(m, k, n) {
962
0
            self.gpu_compute.matmul(a, b, m, k, n)?
963
        } else {
964
1
            cpu_matmul(a, b, m, k, n)
965
        };
966
967
1
        Ok(AsyncGpuResult::ready(result))
968
1
    }
969
970
    /// Process batch of matmuls with optimal scheduling
971
    ///
972
    /// Batches small operations for CPU, pipelines large ones for GPU.
973
    ///
974
    /// # Errors
975
    ///
976
    /// Returns error if any compute fails.
977
4
    pub fn matmul_batch(&mut self, operations: &[MatmulOp]) -> Result<Vec<Vec<f32>>> {
978
4
        let mut results = Vec::with_capacity(operations.len());
979
980
9
        for (
a5
,
b5
,
m5
,
k5
,
n5
) in operations {
981
5
            let result = self.matmul(a, b, *m, *k, *n)
?0
;
982
5
            results.push(result);
983
        }
984
985
4
        Ok(results)
986
4
    }
987
988
    /// Execute matmul with B transposed: A @ B^T
989
    ///
990
    /// Computes C[m,n] = A[m,k] @ B[n,k]^T
991
    /// where B is stored row-major as [n, k].
992
    ///
993
    /// # Errors
994
    ///
995
    /// Returns error if compute fails.
996
    #[allow(clippy::many_single_char_names)]
997
41
    pub fn matmul_transpose_b(
998
41
        &mut self,
999
41
        a: &[f32],
1000
41
        b: &[f32],
1001
41
        m: usize,
1002
41
        k: usize,
1003
41
        n: usize,
1004
41
    ) -> Result<Vec<f32>> {
1005
        // For attention: Q[seq, head_dim] @ K[seq, head_dim]^T = scores[seq, seq]
1006
        // B is stored as [n, k], we need B^T which is [k, n]
1007
41
        if self.should_use_gpu(m, k, n) {
1008
            // Transpose B and use GPU matmul
1009
0
            let b_t = transpose(b, n, k);
1010
0
            self.gpu_compute.matmul(a, &b_t, m, k, n)
1011
        } else {
1012
            // CPU: compute A @ B^T directly
1013
41
            Ok(cpu_matmul_transpose_b(a, b, m, k, n))
1014
        }
1015
41
    }
1016
}
1017