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/batch_scheduling.rs
Line
Count
Source
1
//! Batch Scheduling & Async Processing (PMAT-802)
2
//!
3
//! M25: Token Batching & Speculative Decoding
4
//! M26: Async I/O & Event-Driven Processing
5
//! M27: Request Scheduling & Resource Management
6
7
8
9
// =============================================================================
10
// M25: Token Batching & Speculative Decoding (Phase 16)
11
// =============================================================================
12
13
/// Token batch accumulator for batched processing (M25 - IMP-058)
14
///
15
/// Accumulates tokens until batch is full, then returns for processing.
16
/// Improves throughput by processing multiple tokens together.
17
#[derive(Debug)]
18
pub struct TokenBatch {
19
    tokens: Vec<usize>,
20
    capacity: usize,
21
}
22
23
impl TokenBatch {
24
    /// Create a new token batch with given capacity
25
    #[must_use]
26
7
    pub fn new(capacity: usize) -> Self {
27
7
        Self {
28
7
            tokens: Vec::with_capacity(capacity),
29
7
            capacity,
30
7
        }
31
7
    }
32
33
    /// Get the batch capacity
34
    #[must_use]
35
3
    pub fn capacity(&self) -> usize {
36
3
        self.capacity
37
3
    }
38
39
    /// Get current number of tokens in batch
40
    #[must_use]
41
7
    pub fn len(&self) -> usize {
42
7
        self.tokens.len()
43
7
    }
44
45
    /// Check if batch is empty
46
    #[must_use]
47
4
    pub fn is_empty(&self) -> bool {
48
4
        self.tokens.is_empty()
49
4
    }
50
51
    /// Check if batch is full
52
    #[must_use]
53
21
    pub fn is_full(&self) -> bool {
54
21
        self.tokens.len() >= self.capacity
55
21
    }
56
57
    /// Push a token to the batch
58
    ///
59
    /// Returns `Some(tokens)` when batch becomes full, `None` otherwise.
60
18
    pub fn push(&mut self, token: usize) -> Option<Vec<usize>> {
61
18
        self.tokens.push(token);
62
18
        if self.is_full() {
63
3
            Some(self.flush())
64
        } else {
65
15
            None
66
        }
67
18
    }
68
69
    /// Flush and return all tokens, clearing the batch
70
6
    pub fn flush(&mut self) -> Vec<usize> {
71
6
        std::mem::take(&mut self.tokens)
72
6
    }
73
}
74
75
/// Candidate token for speculative decoding
76
#[derive(Debug, Clone)]
77
struct SpeculativeCandidate {
78
    token: usize,
79
    /// Confidence score (stored for future use in acceptance thresholds)
80
    #[allow(dead_code)]
81
    confidence: f32,
82
}
83
84
/// Speculative token buffer for speculative decoding (M25 - IMP-059)
85
///
86
/// Manages candidate tokens generated speculatively, allowing verification
87
/// against actual model outputs for acceptance or rejection.
88
#[derive(Debug)]
89
pub struct SpeculativeBuffer {
90
    candidates: Vec<SpeculativeCandidate>,
91
    capacity: usize,
92
}
93
94
impl SpeculativeBuffer {
95
    /// Create a new speculative buffer with given capacity
96
    #[must_use]
97
5
    pub fn new(capacity: usize) -> Self {
98
5
        Self {
99
5
            candidates: Vec::with_capacity(capacity),
100
5
            capacity,
101
5
        }
102
5
    }
103
104
    /// Get the buffer capacity
105
    #[must_use]
106
1
    pub fn capacity(&self) -> usize {
107
1
        self.capacity
108
1
    }
109
110
    /// Get current number of candidates
111
    #[must_use]
112
5
    pub fn len(&self) -> usize {
113
5
        self.candidates.len()
114
5
    }
115
116
    /// Check if buffer is empty
117
    #[must_use]
118
1
    pub fn is_empty(&self) -> bool {
119
1
        self.candidates.is_empty()
120
1
    }
121
122
    /// Add a candidate token with confidence score
123
17
    pub fn add_candidate(&mut self, token: usize, confidence: f32) {
124
17
        if self.candidates.len() < self.capacity {
125
17
            self.candidates
126
17
                .push(SpeculativeCandidate { token, confidence });
127
17
        
}0
128
17
    }
129
130
    /// Verify candidates against actual tokens
131
    ///
132
    /// Returns (num_accepted, rejection_index) where rejection_index is
133
    /// the first index where mismatch occurred, or None if all matched.
134
    #[must_use]
135
4
    pub fn verify(&self, actual_tokens: &[usize]) -> (usize, Option<usize>) {
136
4
        let mut accepted = 0;
137
12
        for (i, candidate) in 
self.candidates.iter()4
.
enumerate4
() {
138
12
            if i < actual_tokens.len() && candidate.token == actual_tokens[i] {
139
10
                accepted += 1;
140
10
            } else {
141
2
                return (accepted, Some(i));
142
            }
143
        }
144
2
        (accepted, None)
145
4
    }
146
147
    /// Accept first n candidates, removing them from buffer
148
2
    pub fn accept(&mut self, n: usize) {
149
2
        if n >= self.candidates.len() {
150
0
            self.candidates.clear();
151
2
        } else {
152
2
            self.candidates.drain(0..n);
153
2
        }
154
2
    }
155
156
    /// Reject all remaining candidates
157
4
    pub fn reject(&mut self) {
158
4
        self.candidates.clear();
159
4
    }
160
}
161
162
/// Batch ID for tracking inference batches
163
pub type BatchId = u64;
164
165
/// Inference batch scheduler for coordinating batched processing (M25 - IMP-060)
166
///
167
/// Manages pending and completed batches, allowing asynchronous batch
168
/// submission and result retrieval.
169
#[derive(Debug)]
170
pub struct InferenceBatchScheduler {
171
    next_id: BatchId,
172
    pending: std::collections::HashMap<BatchId, Vec<usize>>,
173
    completed: std::collections::VecDeque<(BatchId, Vec<usize>)>,
174
}
175
176
impl InferenceBatchScheduler {
177
    /// Create a new inference batch scheduler
178
    #[must_use]
179
3
    pub fn new() -> Self {
180
3
        Self {
181
3
            next_id: 0,
182
3
            pending: std::collections::HashMap::new(),
183
3
            completed: std::collections::VecDeque::new(),
184
3
        }
185
3
    }
186
187
    /// Get count of pending batches
188
    #[must_use]
189
5
    pub fn pending_count(&self) -> usize {
190
5
        self.pending.len()
191
5
    }
192
193
    /// Get count of completed batches
194
    #[must_use]
195
5
    pub fn completed_count(&self) -> usize {
196
5
        self.completed.len()
197
5
    }
198
199
    /// Submit a batch for processing
200
    ///
201
    /// Returns a unique batch ID for tracking.
202
4
    pub fn submit(&mut self, tokens: Vec<usize>) -> BatchId {
203
4
        let id = self.next_id;
204
4
        self.next_id += 1;
205
4
        self.pending.insert(id, tokens);
206
4
        id
207
4
    }
208
209
    /// Mark a batch as complete with results
210
4
    pub fn complete(&mut self, batch_id: BatchId, results: Vec<usize>) {
211
4
        self.pending.remove(&batch_id);
212
4
        self.completed.push_back((batch_id, results));
213
4
    }
214
215
    /// Poll for a completed batch
216
    ///
217
    /// Returns `Some((batch_id, results))` if a batch is ready, `None` otherwise.
218
3
    pub fn poll(&mut self) -> Option<(BatchId, Vec<usize>)> {
219
3
        self.completed.pop_front()
220
3
    }
221
222
    /// Drain all completed batches
223
2
    pub fn drain(&mut self) -> Vec<(BatchId, Vec<usize>)> {
224
2
        self.completed.drain(..).collect()
225
2
    }
226
}
227
228
impl Default for InferenceBatchScheduler {
229
0
    fn default() -> Self {
230
0
        Self::new()
231
0
    }
232
}
233
234
// =============================================================================
235
// M26: Async I/O & Event-Driven Processing (Phase 17)
236
// =============================================================================
237
238
/// Async request queue for non-blocking request handling (M26 - IMP-061)
239
///
240
/// Provides a bounded FIFO queue for inference requests with backpressure
241
/// support via try-based operations.
242
#[derive(Debug)]
243
pub struct AsyncRequestQueue<T> {
244
    items: std::collections::VecDeque<T>,
245
    capacity: usize,
246
}
247
248
impl<T> AsyncRequestQueue<T> {
249
    /// Create a new async request queue with specified capacity
250
    #[must_use]
251
3
    pub fn new(capacity: usize) -> Self {
252
3
        Self {
253
3
            items: std::collections::VecDeque::with_capacity(capacity),
254
3
            capacity,
255
3
        }
256
3
    }
257
258
    /// Get queue capacity
259
    #[must_use]
260
1
    pub fn capacity(&self) -> usize {
261
1
        self.capacity
262
1
    }
263
264
    /// Get current queue length
265
    #[must_use]
266
2
    pub fn len(&self) -> usize {
267
2
        self.items.len()
268
2
    }
269
270
    /// Check if queue is empty
271
    #[must_use]
272
3
    pub fn is_empty(&self) -> bool {
273
3
        self.items.is_empty()
274
3
    }
275
276
    /// Check if queue is full
277
    #[must_use]
278
13
    pub fn is_full(&self) -> bool {
279
13
        self.items.len() >= self.capacity
280
13
    }
281
282
    /// Try to push an item to the queue
283
    ///
284
    /// Returns `true` if successful, `false` if queue is full (backpressure).
285
9
    pub fn try_push(&mut self, item: T) -> bool {
286
9
        if self.is_full() {
287
2
            false
288
        } else {
289
7
            self.items.push_back(item);
290
7
            true
291
        }
292
9
    }
293
294
    /// Try to pop an item from the queue
295
    ///
296
    /// Returns `Some(item)` if available, `None` if queue is empty.
297
5
    pub fn try_pop(&mut self) -> Option<T> {
298
5
        self.items.pop_front()
299
5
    }
300
}
301
302
/// Type alias for inference completion handler
303
pub type InferenceCompletionHandler = Box<dyn Fn(u64, &[usize]) + Send + Sync>;
304
305
/// Event notifier for inference completion (M26 - IMP-062)
306
///
307
/// Allows registration of handlers that are called when inference completes.
308
pub struct InferenceEventNotifier {
309
    handlers: Vec<InferenceCompletionHandler>,
310
}
311
312
impl std::fmt::Debug for InferenceEventNotifier {
313
1
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314
1
        f.debug_struct("InferenceEventNotifier")
315
1
            .field("handler_count", &self.handlers.len())
316
1
            .finish()
317
1
    }
318
}
319
320
impl InferenceEventNotifier {
321
    /// Create a new event notifier
322
    #[must_use]
323
3
    pub fn new() -> Self {
324
3
        Self {
325
3
            handlers: Vec::new(),
326
3
        }
327
3
    }
328
329
    /// Get count of registered handlers
330
    #[must_use]
331
6
    pub fn handler_count(&self) -> usize {
332
6
        self.handlers.len()
333
6
    }
334
335
    /// Register a completion handler
336
    ///
337
    /// Handler receives (request_id, output_tokens) when inference completes.
338
5
    pub fn register(&mut self, handler: InferenceCompletionHandler) {
339
5
        self.handlers.push(handler);
340
5
    }
341
342
    /// Notify all handlers of completion
343
    ///
344
    /// Calls each registered handler with the request ID and output tokens.
345
4
    pub fn notify(&self, request_id: u64, tokens: &[usize]) {
346
11
        for 
handler7
in &self.handlers {
347
7
            handler(request_id, tokens);
348
7
        }
349
4
    }
350
351
    /// Clear all registered handlers
352
2
    pub fn clear(&mut self) {
353
2
        self.handlers.clear();
354
2
    }
355
}
356
357
impl Default for InferenceEventNotifier {
358
0
    fn default() -> Self {
359
0
        Self::new()
360
0
    }
361
}
362
363
/// Request ID type for timeout tracking
364
pub type RequestId = u64;
365
366
/// Timeout manager for request deadline tracking (M26 - IMP-063)
367
///
368
/// Tracks request deadlines and identifies expired requests.
369
#[derive(Debug)]
370
pub struct TimeoutManager {
371
    deadlines: std::collections::HashMap<RequestId, std::time::Instant>,
372
}
373
374
impl TimeoutManager {
375
    /// Create a new timeout manager
376
    #[must_use]
377
3
    pub fn new() -> Self {
378
3
        Self {
379
3
            deadlines: std::collections::HashMap::new(),
380
3
        }
381
3
    }
382
383
    /// Get count of active timeout registrations
384
    #[must_use]
385
7
    pub fn active_count(&self) -> usize {
386
7
        self.deadlines.len()
387
7
    }
388
389
    /// Register a timeout for a request
390
    ///
391
    /// The deadline is the absolute time at which the request should timeout.
392
4
    pub fn register(&mut self, request_id: RequestId, deadline: std::time::Instant) {
393
4
        self.deadlines.insert(request_id, deadline);
394
4
    }
395
396
    /// Remove timeout registration for a request
397
    ///
398
    /// Use when request completes before timeout.
399
2
    pub fn remove(&mut self, request_id: RequestId) {
400
2
        self.deadlines.remove(&request_id);
401
2
    }
402
403
    /// Check for expired requests and remove them
404
    ///
405
    /// Returns list of request IDs that have timed out.
406
3
    pub fn check_expired(&mut self) -> Vec<RequestId> {
407
3
        let now = std::time::Instant::now();
408
3
        let expired: Vec<RequestId> = self
409
3
            .deadlines
410
3
            .iter()
411
4
            .
filter3
(|(_, &deadline)| now >= deadline)
412
3
            .map(|(&id, _)| id)
413
3
            .collect();
414
415
5
        for 
id2
in &expired {
416
2
            self.deadlines.remove(id);
417
2
        }
418
419
3
        expired
420
3
    }
421
}
422
423
impl Default for TimeoutManager {
424
0
    fn default() -> Self {
425
0
        Self::new()
426
0
    }
427
}
428
429
// =============================================================================
430
// M27: Request Scheduling & Resource Management (Phase 18)
431
// =============================================================================
432
433
/// Priority level type (higher = more important)
434
pub type Priority = u32;
435
436
/// Priority request wrapper for priority queue (M27 - IMP-064)
437
#[derive(Debug, Clone)]
438
pub struct PriorityRequest<T> {
439
    priority: Priority,
440
    sequence: u64, // For FIFO ordering within same priority
441
    data: T,
442
}
443
444
impl<T> PriorityRequest<T> {
445
    /// Create a new priority request
446
    #[must_use]
447
13
    pub fn new(priority: Priority, data: T) -> Self {
448
13
        Self {
449
13
            priority,
450
13
            sequence: 0, // Will be set by queue
451
13
            data,
452
13
        }
453
13
    }
454
455
    /// Get the priority level
456
    #[must_use]
457
0
    pub fn priority(&self) -> Priority {
458
0
        self.priority
459
0
    }
460
461
    /// Get reference to request data
462
    #[must_use]
463
6
    pub fn data(&self) -> &T {
464
6
        &self.data
465
6
    }
466
467
    /// Consume and return the data
468
    #[must_use]
469
6
    pub fn into_data(self) -> T {
470
6
        self.data
471
6
    }
472
}
473
474
/// Priority request queue for request scheduling (M27 - IMP-064)
475
///
476
/// Implements priority-based scheduling with FIFO ordering for same-priority requests.
477
#[derive(Debug)]
478
pub struct PriorityRequestQueue<T> {
479
    items: Vec<PriorityRequest<T>>,
480
    next_sequence: u64,
481
}
482
483
impl<T> PriorityRequestQueue<T> {
484
    /// Create a new priority request queue
485
    #[must_use]
486
4
    pub fn new() -> Self {
487
4
        Self {
488
4
            items: Vec::new(),
489
4
            next_sequence: 0,
490
4
        }
491
4
    }
492
493
    /// Get number of items in queue
494
    #[must_use]
495
2
    pub fn len(&self) -> usize {
496
2
        self.items.len()
497
2
    }
498
499
    /// Check if queue is empty
500
    #[must_use]
501
3
    pub fn is_empty(&self) -> bool {
502
3
        self.items.is_empty()
503
3
    }
504
505
    /// Enqueue a request with priority
506
12
    pub fn enqueue(&mut self, mut request: PriorityRequest<T>) {
507
12
        request.sequence = self.next_sequence;
508
12
        self.next_sequence += 1;
509
12
        self.items.push(request);
510
12
    }
511
512
    /// Dequeue the highest priority request
513
    ///
514
    /// Returns the request with highest priority. For equal priorities,
515
    /// returns the earliest enqueued (FIFO).
516
13
    pub fn dequeue_highest(&mut self) -> Option<PriorityRequest<T>> {
517
13
        if self.items.is_empty() {
518
1
            return None;
519
12
        }
520
521
        // Find index of highest priority (and earliest sequence for ties)
522
12
        let mut best_idx = 0;
523
12
        for (i, item) in self.items.iter().enumerate().skip(1) {
524
12
            let best = &self.items[best_idx];
525
12
            if item.priority > best.priority
526
8
                || (item.priority == best.priority && 
item.sequence < best.sequence6
)
527
6
            {
528
6
                best_idx = i;
529
6
            }
530
        }
531
532
12
        Some(self.items.swap_remove(best_idx))
533
13
    }
534
}
535
536
impl<T> Default for PriorityRequestQueue<T> {
537
0
    fn default() -> Self {
538
0
        Self::new()
539
0
    }
540
}
541
542
/// Token bucket rate limiter for throughput control (M27 - IMP-065)
543
///
544
/// Implements token bucket algorithm with configurable rate and burst capacity.
545
#[derive(Debug)]
546
pub struct TokenRateLimiter {
547
    tokens: u32,
548
    capacity: u32,
549
    rate: f64, // tokens per second
550
    last_refill: std::time::Instant,
551
}
552
553
impl TokenRateLimiter {
554
    /// Create a new rate limiter
555
    ///
556
    /// # Arguments
557
    /// * `rate` - Tokens per second to refill
558
    /// * `burst_capacity` - Maximum tokens that can accumulate
559
    #[must_use]
560
3
    pub fn new(rate: f64, burst_capacity: u32) -> Self {
561
3
        Self {
562
3
            tokens: burst_capacity, // Start full
563
3
            capacity: burst_capacity,
564
3
            rate,
565
3
            last_refill: std::time::Instant::now(),
566
3
        }
567
3
    }
568
569
    /// Get current available tokens
570
    #[must_use]
571
9
    pub fn tokens_available(&self) -> u32 {
572
9
        self.tokens
573
9
    }
574
575
    /// Try to acquire tokens
576
    ///
577
    /// Returns `true` if tokens were acquired, `false` if insufficient tokens.
578
6
    pub fn try_acquire(&mut self, count: u32) -> bool {
579
6
        if self.tokens >= count {
580
4
            self.tokens -= count;
581
4
            true
582
        } else {
583
2
            false
584
        }
585
6
    }
586
587
    /// Refill tokens based on elapsed time
588
    ///
589
    /// Call periodically to add tokens at the configured rate.
590
1
    pub fn refill(&mut self) {
591
1
        let now = std::time::Instant::now();
592
1
        let elapsed = now.duration_since(self.last_refill).as_secs_f64();
593
1
        let new_tokens = (elapsed * self.rate) as u32;
594
595
1
        if new_tokens > 0 {
596
1
            self.tokens = (self.tokens + new_tokens).min(self.capacity);
597
1
            self.last_refill = now;
598
1
        
}0
599
1
    }
600
}
601
602
/// Allocation ID for resource tracking
603
pub type AllocationId = u64;
604
605
/// Resource allocation record
606
#[derive(Debug, Clone)]
607
struct ResourceAllocation {
608
    memory: u64,
609
    compute: u32,
610
}
611
612
/// Resource usage tracker for memory and compute (M27 - IMP-066)
613
///
614
/// Tracks resource allocations and provides utilization metrics.
615
#[derive(Debug)]
616
pub struct ResourceTracker {
617
    memory_capacity: u64,
618
    compute_capacity: u32,
619
    memory_used: u64,
620
    compute_used: u32,
621
    allocations: std::collections::HashMap<AllocationId, ResourceAllocation>,
622
    next_id: AllocationId,
623
}
624
625
impl ResourceTracker {
626
    /// Create a new resource tracker
627
    ///
628
    /// # Arguments
629
    /// * `memory_capacity` - Total memory capacity in bytes
630
    /// * `compute_capacity` - Total compute capacity (0-100 percentage)
631
    #[must_use]
632
5
    pub fn new(memory_capacity: u64, compute_capacity: u32) -> Self {
633
5
        Self {
634
5
            memory_capacity,
635
5
            compute_capacity,
636
5
            memory_used: 0,
637
5
            compute_used: 0,
638
5
            allocations: std::collections::HashMap::new(),
639
5
            next_id: 0,
640
5
        }
641
5
    }
642
643
    /// Get current memory usage in bytes
644
    #[must_use]
645
6
    pub fn memory_usage(&self) -> u64 {
646
6
        self.memory_used
647
6
    }
648
649
    /// Get current compute usage (0-100)
650
    #[must_use]
651
6
    pub fn compute_usage(&self) -> u32 {
652
6
        self.compute_used
653
6
    }
654
655
    /// Check if allocation is possible
656
    #[must_use]
657
10
    pub fn can_allocate(&self, memory: u64, compute: u32) -> bool {
658
10
        self.memory_used + memory <= self.memory_capacity
659
7
            && self.compute_used + compute <= self.compute_capacity
660
10
    }
661
662
    /// Allocate resources
663
    ///
664
    /// Returns allocation ID if successful, None if insufficient resources.
665
5
    pub fn allocate(&mut self, memory: u64, compute: u32) -> Option<AllocationId> {
666
5
        if !self.can_allocate(memory, compute) {
667
1
            return None;
668
4
        }
669
670
4
        let id = self.next_id;
671
4
        self.next_id += 1;
672
673
4
        self.memory_used += memory;
674
4
        self.compute_used += compute;
675
4
        self.allocations
676
4
            .insert(id, ResourceAllocation { memory, compute });
677
678
4
        Some(id)
679
5
    }
680
681
    /// Release allocated resources
682
2
    pub fn release(&mut self, id: AllocationId) {
683
2
        if let Some(alloc) = self.allocations.remove(&id) {
684
2
            self.memory_used = self.memory_used.saturating_sub(alloc.memory);
685
2
            self.compute_used = self.compute_used.saturating_sub(alloc.compute);
686
2
        
}0
687
2
    }
688
689
    /// Get usage as percentages
690
    ///
691
    /// Returns (memory_percentage, compute_percentage)
692
    #[must_use]
693
3
    pub fn usage_percentage(&self) -> (f64, f64) {
694
3
        let mem_pct = if self.memory_capacity > 0 {
695
2
            (self.memory_used as f64 / self.memory_capacity as f64) * 100.0
696
        } else {
697
1
            0.0
698
        };
699
3
        let compute_pct = if self.compute_capacity > 0 {
700
2
            (self.compute_used as f64 / self.compute_capacity as f64) * 100.0
701
        } else {
702
1
            0.0
703
        };
704
3
        (mem_pct, compute_pct)
705
3
    }
706
}
707
708
impl Default for ResourceTracker {
709
0
    fn default() -> Self {
710
        // Default: 8GB memory, 100% compute
711
0
        Self::new(8 * 1024 * 1024 * 1024, 100)
712
0
    }
713
}