/home/noah/src/realizar/src/gpu/mod.rs
Line | Count | Source |
1 | | //! GPU Acceleration Module (Phase 4) |
2 | | //! |
3 | | //! Provides GPU-accelerated compute primitives via trueno's wgpu backend. |
4 | | //! |
5 | | //! ## Architecture |
6 | | //! |
7 | | //! ```text |
8 | | //! +-----------------------+ |
9 | | //! | GpuCompute API | <- Safe public API |
10 | | //! +-----------------------+ |
11 | | //! | trueno::GpuBackend | <- wgpu-based GPU compute |
12 | | //! +-----------------------+ |
13 | | //! | wgpu Device/Queue | <- WebGPU abstraction |
14 | | //! +-----------------------+ |
15 | | //! ``` |
16 | | //! |
17 | | //! ## Usage |
18 | | //! |
19 | | //! ```rust,ignore |
20 | | //! use realizar::gpu::{GpuCompute, ComputeBackend}; |
21 | | //! |
22 | | //! // Auto-select best backend |
23 | | //! let compute = GpuCompute::auto()?; |
24 | | //! |
25 | | //! // GPU matmul |
26 | | //! let c = compute.matmul(&a, &b, m, k, n)?; |
27 | | //! ``` |
28 | | //! |
29 | | //! ## Performance Targets (Refs REALIZAR-PERF-SPEC-001) |
30 | | //! |
31 | | //! | Operation | GPU Target | CPU Baseline | |
32 | | //! |-----------|------------|--------------| |
33 | | //! | matmul | 20x faster | 1x | |
34 | | //! | tok/s | ≥100 | ≥25 | |
35 | | |
36 | | use crate::error::Result; |
37 | | use std::collections::HashMap; |
38 | | use std::time::Duration; |
39 | | |
40 | | // PMAT-802: Extracted modules |
41 | | pub mod scheduler; |
42 | | pub mod adapters; |
43 | | pub mod backend; |
44 | | pub mod mock_backend; |
45 | | mod allocator; |
46 | | mod diagnostics; |
47 | | mod resilience; |
48 | | mod simd_ops; |
49 | | mod streaming_kv; |
50 | | mod metrics; |
51 | | mod batch_scheduling; |
52 | | |
53 | | // Types available without cuda feature |
54 | | pub use scheduler::{GpuModel, GpuModelConfig, GpuGenerateConfig, WeightType, AttentionBuffers, BlockWeights}; |
55 | | #[cfg(feature = "cuda")] |
56 | | pub use scheduler::CudaScheduler; |
57 | | |
58 | | // Allocator exports (M21, M22) |
59 | | pub use allocator::{ |
60 | | CacheAlignedBuffer, TensorPool, ForwardArena, ScratchBuffer, |
61 | | prefetch_read, sequential_sum, sum_with_prefetch, naive_matmul, blocked_matmul, |
62 | | }; |
63 | | |
64 | | // Diagnostics exports (M32) |
65 | | pub use diagnostics::{ |
66 | | LogLevel, LogEntry, LogConfig, Logger, PhaseTimer, |
67 | | MemoryReport, MemoryTracker, DiagnosticsSummary, DiagnosticsCollector, |
68 | | DebugMode, RequestCapture, StateDump, |
69 | | }; |
70 | | |
71 | | // Resilience exports (M31) |
72 | | pub use resilience::{ |
73 | | ErrorCategory, RetryDecision, RetryConfig, RetryPolicy, |
74 | | CircuitState, CircuitConfig, CircuitBreaker, |
75 | | RequestType, BulkheadPermit, BulkheadConfig, BulkheadStats, BulkheadManager, |
76 | | }; |
77 | | |
78 | | // SIMD ops exports (M18) |
79 | | pub use simd_ops::{scalar_softmax, simd_softmax, scalar_rope, simd_rope}; |
80 | | |
81 | | // Streaming KV cache exports (M6) |
82 | | pub use streaming_kv::{StreamingKVCache, StreamingKVCacheFp16}; |
83 | | |
84 | | // Metrics exports (M28) |
85 | | pub use metrics::{ |
86 | | InferenceMetrics, HealthChecker, ShutdownCoordinator, ComputeBackend, |
87 | | GpuCompute, GpuBufferPool, GpuPoolStats, AsyncGpuResult, HybridScheduler, |
88 | | }; |
89 | | // Internal-only matmul functions (used by scheduler module) |
90 | | pub(crate) use metrics::{cpu_matmul, cpu_matmul_transposed_simd}; |
91 | | |
92 | | // Batch scheduling exports (M25, M26, M27) |
93 | | pub use batch_scheduling::{ |
94 | | TokenBatch, SpeculativeBuffer, InferenceBatchScheduler, BatchId, |
95 | | AsyncRequestQueue, InferenceEventNotifier, InferenceCompletionHandler, |
96 | | TimeoutManager, RequestId, PriorityRequest, PriorityRequestQueue, Priority, |
97 | | TokenRateLimiter, ResourceTracker, AllocationId, |
98 | | }; |
99 | | |
100 | | // ============================================================================ |
101 | | // GPU Buffer Limits (IMP-090, IMP-091) |
102 | | // ============================================================================ |
103 | | |
104 | | /// Maximum GPU buffer size in bytes (wgpu limit: 256MB) |
105 | | const MAX_GPU_BUFFER_BYTES: usize = 256 * 1024 * 1024; |
106 | | |
107 | | /// Vocab size threshold above which we use CPU for embedding/lm_head |
108 | | /// This is calculated as: MAX_GPU_BUFFER_BYTES / (hidden_dim * sizeof(f32)) |
109 | | /// For hidden_dim=1536: 256MB / (1536 * 4) = 43,690 tokens |
110 | | /// We use 65536 as a round threshold that works for most models |
111 | | pub const LARGE_VOCAB_THRESHOLD: usize = 65536; |
112 | | |
113 | | /// Check if a matrix operation would exceed GPU buffer limits (IMP-090) |
114 | | /// |
115 | | /// Returns true if the operation should use CPU fallback |
116 | | #[inline] |
117 | | #[must_use] |
118 | 615 | pub fn exceeds_gpu_buffer_limit(elements: usize) -> bool { |
119 | 615 | elements * std::mem::size_of::<f32>() > MAX_GPU_BUFFER_BYTES |
120 | 615 | } |
121 | | |
122 | | /// Matmul batch operation: (A matrix, B matrix, m rows, k cols, n cols) |
123 | | pub type MatmulOp = (Vec<f32>, Vec<f32>, usize, usize, usize); |
124 | | |
125 | | // ============================================================================ |
126 | | // Contiguous Attention Buffer (M19 - IMP-040) |
127 | | // ============================================================================ |
128 | | |
129 | | /// Contiguous memory buffer for attention tensors (M19 - IMP-040) |
130 | | /// |
131 | | /// Pre-allocates a single contiguous block for Q, K, V, O tensors |
132 | | /// to reduce memory fragmentation during attention computation. |
133 | | #[derive(Debug)] |
134 | | pub struct ContiguousAttentionBuffer { |
135 | | /// Single contiguous allocation for all tensors |
136 | | data: Vec<f32>, |
137 | | /// Maximum sequence length |
138 | | max_seq_len: usize, |
139 | | /// Number of attention heads (stored for future use) |
140 | | #[allow(dead_code)] |
141 | | num_heads: usize, |
142 | | /// Dimension per head (stored for future use) |
143 | | #[allow(dead_code)] |
144 | | head_dim: usize, |
145 | | /// Size of each tensor (Q, K, V, O have same size) |
146 | | tensor_size: usize, |
147 | | } |
148 | | |
149 | | impl ContiguousAttentionBuffer { |
150 | | /// Create a new contiguous attention buffer |
151 | | #[must_use] |
152 | 8 | pub fn new(max_seq_len: usize, num_heads: usize, head_dim: usize) -> Self { |
153 | 8 | let tensor_size = max_seq_len * num_heads * head_dim; |
154 | | // Allocate 4x for Q, K, V, O in single contiguous block |
155 | 8 | let data = vec![0.0f32; tensor_size * 4]; |
156 | | |
157 | 8 | Self { |
158 | 8 | data, |
159 | 8 | max_seq_len, |
160 | 8 | num_heads, |
161 | 8 | head_dim, |
162 | 8 | tensor_size, |
163 | 8 | } |
164 | 8 | } |
165 | | |
166 | | /// Check if buffer is contiguous (always true for this implementation) |
167 | | #[must_use] |
168 | 5 | pub fn is_contiguous(&self) -> bool { |
169 | | // Buffer is contiguous by construction |
170 | 5 | true |
171 | 5 | } |
172 | | |
173 | | /// Get views into Q, K, V, O tensors |
174 | | #[must_use] |
175 | 6 | pub fn get_views(&self) -> (&[f32], &[f32], &[f32], &[f32]) { |
176 | 6 | let q_start = 0; |
177 | 6 | let k_start = self.tensor_size; |
178 | 6 | let v_start = self.tensor_size * 2; |
179 | 6 | let o_start = self.tensor_size * 3; |
180 | | |
181 | 6 | ( |
182 | 6 | &self.data[q_start..k_start], |
183 | 6 | &self.data[k_start..v_start], |
184 | 6 | &self.data[v_start..o_start], |
185 | 6 | &self.data[o_start..], |
186 | 6 | ) |
187 | 6 | } |
188 | | |
189 | | /// Get mutable views into Q, K, V, O tensors |
190 | 3 | pub fn get_views_mut(&mut self) -> (&mut [f32], &mut [f32], &mut [f32], &mut [f32]) { |
191 | 3 | let tensor_size = self.tensor_size; |
192 | | |
193 | | // Split the data into 4 mutable slices |
194 | 3 | let (q, rest) = self.data.split_at_mut(tensor_size); |
195 | 3 | let (k, rest) = rest.split_at_mut(tensor_size); |
196 | 3 | let (v, o) = rest.split_at_mut(tensor_size); |
197 | | |
198 | 3 | (q, k, v, o) |
199 | 3 | } |
200 | | |
201 | | /// Reset buffer to zeros for reuse |
202 | 3 | pub fn reset(&mut self) { |
203 | 3 | self.data.fill(0.0); |
204 | 3 | } |
205 | | |
206 | | /// Get maximum sequence length |
207 | | #[must_use] |
208 | 2 | pub fn max_seq_len(&self) -> usize { |
209 | 2 | self.max_seq_len |
210 | 2 | } |
211 | | } |
212 | | |
213 | | // ============================================================================ |
214 | | // Batch Processing & Parallel Execution (M20 - IMP-043/044/045) |
215 | | // ============================================================================ |
216 | | |
217 | | /// Batch token embedding lookup (M20 - IMP-043) |
218 | | /// |
219 | | /// Performs vectorized embedding lookup for multiple tokens at once. |
220 | | /// More efficient than individual lookups due to better memory access patterns. |
221 | | #[must_use] |
222 | 1.01k | pub fn batch_embed(embedding_table: &[f32], tokens: &[usize], hidden_dim: usize) -> Vec<f32> { |
223 | 1.01k | if tokens.is_empty() || embedding_table1.01k .is_empty1.01k () { |
224 | 3 | return Vec::new(); |
225 | 1.01k | } |
226 | | |
227 | 1.01k | let mut result = Vec::with_capacity(tokens.len() * hidden_dim); |
228 | | |
229 | | // Batch copy embeddings |
230 | 9.07k | for &token8.06k in tokens { |
231 | 8.06k | let start_idx = token * hidden_dim; |
232 | 8.06k | let end_idx = start_idx + hidden_dim; |
233 | 8.06k | if end_idx <= embedding_table.len() { |
234 | 8.06k | result.extend_from_slice(&embedding_table[start_idx..end_idx]); |
235 | 8.06k | } else { |
236 | 1 | // Pad with zeros for out-of-bounds tokens |
237 | 1 | result.extend(std::iter::repeat_n(0.0, hidden_dim)); |
238 | 1 | } |
239 | | } |
240 | | |
241 | 1.01k | result |
242 | 1.01k | } |
243 | | |
244 | | /// Sequential FFN computation (baseline for comparison) |
245 | | /// |
246 | | /// Standard two-layer feed-forward network: up projection -> activation -> down projection |
247 | | #[must_use] |
248 | 260 | pub fn sequential_ffn( |
249 | 260 | input: &[f32], |
250 | 260 | w_up: &[f32], |
251 | 260 | w_down: &[f32], |
252 | 260 | hidden_dim: usize, |
253 | 260 | intermediate_dim: usize, |
254 | 260 | ) -> Vec<f32> { |
255 | 260 | if input.is_empty() { |
256 | 1 | return Vec::new(); |
257 | 259 | } |
258 | | |
259 | | // Up projection: (hidden_dim,) @ (hidden_dim, intermediate_dim) -> (intermediate_dim,) |
260 | 259 | let mut intermediate = vec![0.0f32; intermediate_dim]; |
261 | 130k | for i in 0..intermediate_dim259 { |
262 | 130k | let mut sum = 0.0f32; |
263 | 33.3M | for j in 0..hidden_dim130k { |
264 | 33.3M | sum += input[j] * w_up[j * intermediate_dim + i]; |
265 | 33.3M | } |
266 | | // GELU activation |
267 | 130k | intermediate[i] = |
268 | 130k | sum * 0.5 * (1.0 + (sum * 0.797_884_5 * (1.0 + 0.044_715 * sum * sum)).tanh()); |
269 | | } |
270 | | |
271 | | // Down projection: (intermediate_dim,) @ (intermediate_dim, hidden_dim) -> (hidden_dim,) |
272 | 259 | let mut output = vec![0.0f32; hidden_dim]; |
273 | 65.1k | for i in 0..hidden_dim259 { |
274 | 65.1k | let mut sum = 0.0f32; |
275 | 33.3M | for j in 0..intermediate_dim65.1k { |
276 | 33.3M | sum += intermediate[j] * w_down[j * hidden_dim + i]; |
277 | 33.3M | } |
278 | 65.1k | output[i] = sum; |
279 | | } |
280 | | |
281 | 259 | output |
282 | 260 | } |
283 | | |
284 | | /// Parallel FFN computation (M20 - IMP-044) |
285 | | /// |
286 | | /// Uses rayon parallelism for the down projection matmul. |
287 | | #[must_use] |
288 | 259 | pub fn parallel_ffn( |
289 | 259 | input: &[f32], |
290 | 259 | w_up: &[f32], |
291 | 259 | w_down: &[f32], |
292 | 259 | hidden_dim: usize, |
293 | 259 | intermediate_dim: usize, |
294 | 259 | ) -> Vec<f32> { |
295 | | use rayon::prelude::*; |
296 | | |
297 | 259 | if input.is_empty() { |
298 | 1 | return Vec::new(); |
299 | 258 | } |
300 | | |
301 | | // Up projection with GELU (sequential - typically smaller) |
302 | 258 | let intermediate: Vec<f32> = (0..intermediate_dim) |
303 | 130k | .map258 (|i| { |
304 | 130k | let sum: f32 = (0..hidden_dim) |
305 | 33.3M | .map130k (|j| input[j] * w_up[j * intermediate_dim + i]) |
306 | 130k | .sum(); |
307 | | // GELU activation |
308 | 130k | sum * 0.5 * (1.0 + (sum * 0.797_884_5 * (1.0 + 0.044_715 * sum * sum)).tanh()) |
309 | 130k | }) |
310 | 258 | .collect(); |
311 | | |
312 | | // Down projection with rayon parallelism |
313 | 258 | let output: Vec<f32> = (0..hidden_dim) |
314 | 258 | .into_par_iter() |
315 | 65.1k | .map258 (|i| { |
316 | 65.1k | (0..intermediate_dim) |
317 | 33.3M | .map65.1k (|j| intermediate[j] * w_down[j * hidden_dim + i]) |
318 | 65.1k | .sum() |
319 | 65.1k | }) |
320 | 258 | .collect(); |
321 | | |
322 | 258 | output |
323 | 259 | } |
324 | | |
325 | | /// Standard two-pass layer normalization (baseline for comparison) |
326 | | /// |
327 | | /// First pass computes mean, second pass computes variance. |
328 | | #[must_use] |
329 | 1.01k | pub fn standard_layernorm(input: &[f32], gamma: &[f32], beta: &[f32], eps: f32) -> Vec<f32> { |
330 | 1.01k | if input.is_empty() { |
331 | 1 | return Vec::new(); |
332 | 1.01k | } |
333 | | |
334 | 1.01k | let n = input.len() as f32; |
335 | | |
336 | | // First pass: compute mean |
337 | 1.01k | let mean: f32 = input.iter().sum::<f32>() / n; |
338 | | |
339 | | // Second pass: compute variance |
340 | 257k | let variance1.01k : f321.01k = input1.01k .iter1.01k ().map1.01k (|&x| (x - mean).powi(2)).sum1.01k ::<f32>() / n1.01k ; |
341 | | |
342 | 1.01k | let std_dev = (variance + eps).sqrt(); |
343 | | |
344 | | // Normalize and apply gamma/beta |
345 | 1.01k | input |
346 | 1.01k | .iter() |
347 | 1.01k | .enumerate() |
348 | 257k | .map1.01k (|(i, &x)| { |
349 | 257k | let normalized = (x - mean) / std_dev; |
350 | 257k | normalized * gamma.get(i).copied().unwrap_or(1.0) + beta.get(i).copied().unwrap_or(0.0) |
351 | 257k | }) |
352 | 1.01k | .collect() |
353 | 1.01k | } |
354 | | |
355 | | /// Fused single-pass layer normalization using Welford's algorithm (M20 - IMP-045) |
356 | | /// |
357 | | /// Computes mean and variance in a single pass, reducing memory bandwidth. |
358 | | #[must_use] |
359 | 1.01k | pub fn fused_layernorm(input: &[f32], gamma: &[f32], beta: &[f32], eps: f32) -> Vec<f32> { |
360 | 1.01k | if input.is_empty() { |
361 | 1 | return Vec::new(); |
362 | 1.01k | } |
363 | | |
364 | 1.01k | let n = input.len(); |
365 | | |
366 | | // Welford's online algorithm for mean and variance |
367 | 1.01k | let mut mean = 0.0f32; |
368 | 1.01k | let mut m2 = 0.0f32; |
369 | | |
370 | 257k | for (i, &x) in input1.01k .iter1.01k ().enumerate1.01k () { |
371 | 257k | let delta = x - mean; |
372 | 257k | mean += delta / (i + 1) as f32; |
373 | 257k | let delta2 = x - mean; |
374 | 257k | m2 += delta * delta2; |
375 | 257k | } |
376 | | |
377 | 1.01k | let variance = m2 / n as f32; |
378 | 1.01k | let std_dev = (variance + eps).sqrt(); |
379 | 1.01k | let inv_std = 1.0 / std_dev; |
380 | | |
381 | | // Normalize and apply gamma/beta in single pass |
382 | 1.01k | input |
383 | 1.01k | .iter() |
384 | 1.01k | .enumerate() |
385 | 257k | .map1.01k (|(i, &x)| { |
386 | 257k | let normalized = (x - mean) * inv_std; |
387 | 257k | normalized * gamma.get(i).copied().unwrap_or(1.0) + beta.get(i).copied().unwrap_or(0.0) |
388 | 257k | }) |
389 | 1.01k | .collect() |
390 | 1.01k | } |
391 | | |
392 | | // ============================================================================ |
393 | | // Phase 14: Quantized Compute Kernels (M23) |
394 | | // ============================================================================ |
395 | | |
396 | | /// Quantized dot product for Q4_0 blocks (M23 - IMP-052) |
397 | | /// |
398 | | /// Computes dot product directly on Q4_0 quantized data without full dequantization. |
399 | | /// Q4_0 format: 2 bytes (f16 scale) + 16 bytes (32 x 4-bit values) |
400 | | #[must_use] |
401 | 6 | pub fn quantized_dot_q4(block_a: &[u8], block_b: &[u8]) -> f32 { |
402 | 6 | if block_a.len() < 18 || block_b.len() < 185 { |
403 | 1 | return 0.0; |
404 | 5 | } |
405 | | |
406 | | // Extract scales (f16 little-endian) |
407 | 5 | let scale_a = half::f16::from_le_bytes([block_a[0], block_a[1]]).to_f32(); |
408 | 5 | let scale_b = half::f16::from_le_bytes([block_b[0], block_b[1]]).to_f32(); |
409 | | |
410 | | // Accumulate dot product over packed 4-bit values |
411 | 5 | let mut acc = 0i32; |
412 | | |
413 | 85 | for i80 in 0..16 { |
414 | 80 | let byte_a = block_a[2 + i]; |
415 | 80 | let byte_b = block_b[2 + i]; |
416 | 80 | |
417 | 80 | // Extract low and high nibbles, center at 8 |
418 | 80 | let a_lo = (byte_a & 0x0F) as i32 - 8; |
419 | 80 | let a_hi = ((byte_a >> 4) & 0x0F) as i32 - 8; |
420 | 80 | let b_lo = (byte_b & 0x0F) as i32 - 8; |
421 | 80 | let b_hi = ((byte_b >> 4) & 0x0F) as i32 - 8; |
422 | 80 | |
423 | 80 | acc += a_lo * b_lo + a_hi * b_hi; |
424 | 80 | } |
425 | | |
426 | | // Apply combined scale |
427 | 5 | (acc as f32) * scale_a * scale_b |
428 | 6 | } |
429 | | |
430 | | /// Quantized dot product for Q8_0 blocks (M23 - IMP-052) |
431 | | /// |
432 | | /// Computes dot product directly on Q8_0 quantized data without full dequantization. |
433 | | /// Q8_0 format: 2 bytes (f16 scale) + 32 bytes (32 x i8 values) |
434 | | #[must_use] |
435 | 5 | pub fn quantized_dot_q8(block_a: &[u8], block_b: &[u8]) -> f32 { |
436 | 5 | if block_a.len() < 34 || block_b.len() < 344 { |
437 | 1 | return 0.0; |
438 | 4 | } |
439 | | |
440 | | // Extract scales (f16 little-endian) |
441 | 4 | let scale_a = half::f16::from_le_bytes([block_a[0], block_a[1]]).to_f32(); |
442 | 4 | let scale_b = half::f16::from_le_bytes([block_b[0], block_b[1]]).to_f32(); |
443 | | |
444 | | // Accumulate dot product over i8 values |
445 | 4 | let mut acc = 0i32; |
446 | | |
447 | 132 | for i128 in 0..32 { |
448 | 128 | let a_val = block_a[2 + i] as i8 as i32; |
449 | 128 | let b_val = block_b[2 + i] as i8 as i32; |
450 | 128 | acc += a_val * b_val; |
451 | 128 | } |
452 | | |
453 | | // Apply combined scale |
454 | 4 | (acc as f32) * scale_a * scale_b |
455 | 5 | } |
456 | | |
457 | | /// Quantized matrix-vector multiply for Q4_0 weights (M23 - IMP-053) |
458 | | /// |
459 | | /// Computes y = W @ x where W is Q4_0 quantized without full dequantization. |
460 | | /// Each row of W consists of ceil(cols/32) Q4_0 blocks. |
461 | | #[must_use] |
462 | 4 | pub fn quantized_matvec_q4(weights: &[u8], input: &[f32], rows: usize, cols: usize) -> Vec<f32> { |
463 | | const Q4_BLOCK_SIZE: usize = 18; // 2 bytes scale + 16 bytes data |
464 | | const Q4_BLOCK_VALUES: usize = 32; |
465 | | |
466 | 4 | let blocks_per_row = cols.div_ceil(Q4_BLOCK_VALUES); |
467 | 4 | let row_bytes = blocks_per_row * Q4_BLOCK_SIZE; |
468 | | |
469 | 4 | let mut output = vec![0.0f32; rows]; |
470 | | |
471 | 8 | for (row, out_val) in output.iter_mut()4 .enumerate4 ().take4 (rows4 ) { |
472 | 8 | let row_offset = row * row_bytes; |
473 | 8 | let mut acc = 0.0f32; |
474 | | |
475 | 8 | for block_idx in 0..blocks_per_row { |
476 | 8 | let block_offset = row_offset + block_idx * Q4_BLOCK_SIZE; |
477 | | |
478 | 8 | if block_offset + Q4_BLOCK_SIZE > weights.len() { |
479 | 0 | break; |
480 | 8 | } |
481 | | |
482 | | // Extract scale |
483 | 8 | let scale = |
484 | 8 | half::f16::from_le_bytes([weights[block_offset], weights[block_offset + 1]]) |
485 | 8 | .to_f32(); |
486 | | |
487 | | // Process 32 values in this block |
488 | 8 | let input_offset = block_idx * Q4_BLOCK_VALUES; |
489 | | |
490 | 136 | for i128 in 0..16 { |
491 | 128 | let byte = weights[block_offset + 2 + i]; |
492 | 128 | let val_lo = (byte & 0x0F) as i32 - 8; |
493 | 128 | let val_hi = ((byte >> 4) & 0x0F) as i32 - 8; |
494 | | |
495 | 128 | let in_idx_lo = input_offset + i * 2; |
496 | 128 | let in_idx_hi = input_offset + i * 2 + 1; |
497 | | |
498 | 128 | if in_idx_lo < cols { |
499 | 128 | acc += (val_lo as f32) * scale * input[in_idx_lo]; |
500 | 128 | }0 |
501 | 128 | if in_idx_hi < cols { |
502 | 128 | acc += (val_hi as f32) * scale * input[in_idx_hi]; |
503 | 128 | }0 |
504 | | } |
505 | | } |
506 | | |
507 | 8 | *out_val = acc; |
508 | | } |
509 | | |
510 | 4 | output |
511 | 4 | } |
512 | | |
513 | | /// Quantized matrix-vector multiply for Q8_0 weights (M23 - IMP-053) |
514 | | /// |
515 | | /// Computes y = W @ x where W is Q8_0 quantized without full dequantization. |
516 | | /// Each row of W consists of ceil(cols/32) Q8_0 blocks. |
517 | | #[must_use] |
518 | 3 | pub fn quantized_matvec_q8(weights: &[u8], input: &[f32], rows: usize, cols: usize) -> Vec<f32> { |
519 | | const Q8_BLOCK_SIZE: usize = 34; // 2 bytes scale + 32 bytes data |
520 | | const Q8_BLOCK_VALUES: usize = 32; |
521 | | |
522 | 3 | let blocks_per_row = cols.div_ceil(Q8_BLOCK_VALUES); |
523 | 3 | let row_bytes = blocks_per_row * Q8_BLOCK_SIZE; |
524 | | |
525 | 3 | let mut output = vec![0.0f32; rows]; |
526 | | |
527 | 8 | for (row, out_val) in output.iter_mut()3 .enumerate3 ().take3 (rows3 ) { |
528 | 8 | let row_offset = row * row_bytes; |
529 | 8 | let mut acc = 0.0f32; |
530 | | |
531 | 8 | for block_idx in 0..blocks_per_row { |
532 | 8 | let block_offset = row_offset + block_idx * Q8_BLOCK_SIZE; |
533 | | |
534 | 8 | if block_offset + Q8_BLOCK_SIZE > weights.len() { |
535 | 0 | break; |
536 | 8 | } |
537 | | |
538 | | // Extract scale |
539 | 8 | let scale = |
540 | 8 | half::f16::from_le_bytes([weights[block_offset], weights[block_offset + 1]]) |
541 | 8 | .to_f32(); |
542 | | |
543 | | // Process 32 values in this block |
544 | 8 | let input_offset = block_idx * Q8_BLOCK_VALUES; |
545 | | |
546 | 264 | for i256 in 0..32 { |
547 | 256 | let val = weights[block_offset + 2 + i] as i8 as i32; |
548 | 256 | let in_idx = input_offset + i; |
549 | | |
550 | 256 | if in_idx < cols { |
551 | 256 | acc += (val as f32) * scale * input[in_idx]; |
552 | 256 | }0 |
553 | | } |
554 | | } |
555 | | |
556 | 8 | *out_val = acc; |
557 | | } |
558 | | |
559 | 3 | output |
560 | 3 | } |
561 | | |
562 | | /// Mixed precision accumulator for quantized computations (M23 - IMP-054) |
563 | | /// |
564 | | /// Accumulates values in f32 precision while processing quantized data, |
565 | | /// ensuring numerical accuracy during block-wise operations. |
566 | | #[derive(Debug, Clone, Default)] |
567 | | pub struct QuantizedAccumulator { |
568 | | /// Running sum in f32 precision |
569 | | sum: f32, |
570 | | } |
571 | | |
572 | | impl QuantizedAccumulator { |
573 | | /// Create a new zeroed accumulator |
574 | | #[must_use] |
575 | 7 | pub fn new() -> Self { |
576 | 7 | Self { sum: 0.0 } |
577 | 7 | } |
578 | | |
579 | | /// Get the current accumulated sum |
580 | | #[must_use] |
581 | 13 | pub fn sum(&self) -> f32 { |
582 | 13 | self.sum |
583 | 13 | } |
584 | | |
585 | | /// Reset the accumulator to zero |
586 | 4 | pub fn reset(&mut self) { |
587 | 4 | self.sum = 0.0; |
588 | 4 | } |
589 | | |
590 | | /// Add a scaled value to the accumulator |
591 | | #[inline] |
592 | 7 | pub fn add_scaled(&mut self, value: f32, scale: f32) { |
593 | 7 | self.sum += value * scale; |
594 | 7 | } |
595 | | |
596 | | /// Add a block contribution (block_sum * block_scale) |
597 | | #[inline] |
598 | 14 | pub fn add_block(&mut self, block_sum: f32, block_scale: f32) { |
599 | 14 | self.sum += block_sum * block_scale; |
600 | 14 | } |
601 | | } |
602 | | |
603 | | // ============================================================================= |
604 | | // M24: Streaming & Pipelining (Phase 15) |
605 | | // ============================================================================= |
606 | | |
607 | | /// Double buffer for overlapping compute with memory operations (M24 - IMP-055) |
608 | | /// |
609 | | /// Enables loading next layer weights while computing current layer. |
610 | | /// Front buffer is read-only for compute, back buffer is writable for loading. |
611 | | #[derive(Debug)] |
612 | | pub struct DoubleBuffer<T> { |
613 | | front: Vec<T>, |
614 | | back: Vec<T>, |
615 | | } |
616 | | |
617 | | impl<T: Default + Clone> DoubleBuffer<T> { |
618 | | /// Create a new double buffer with given capacity |
619 | | #[must_use] |
620 | 7 | pub fn new(capacity: usize) -> Self { |
621 | 7 | Self { |
622 | 7 | front: vec![T::default(); capacity], |
623 | 7 | back: vec![T::default(); capacity], |
624 | 7 | } |
625 | 7 | } |
626 | | |
627 | | /// Get the capacity of each buffer |
628 | | #[must_use] |
629 | 3 | pub fn capacity(&self) -> usize { |
630 | 3 | self.front.len() |
631 | 3 | } |
632 | | |
633 | | /// Get immutable reference to front buffer (for reading/compute) |
634 | | #[must_use] |
635 | 9 | pub fn front(&self) -> &[T] { |
636 | 9 | &self.front |
637 | 9 | } |
638 | | |
639 | | /// Get mutable reference to back buffer (for writing/loading) |
640 | 6 | pub fn back_mut(&mut self) -> &mut [T] { |
641 | 6 | &mut self.back |
642 | 6 | } |
643 | | |
644 | | /// Swap front and back buffers |
645 | 6 | pub fn swap(&mut self) { |
646 | 6 | std::mem::swap(&mut self.front, &mut self.back); |
647 | 6 | } |
648 | | } |
649 | | |
650 | | /// Chunked token processor for improved cache utilization (M24 - IMP-056) |
651 | | /// |
652 | | /// Processes tokens in configurable chunks to improve memory locality |
653 | | /// and cache efficiency during batch processing. |
654 | | #[derive(Debug, Clone)] |
655 | | pub struct ChunkedProcessor { |
656 | | chunk_size: usize, |
657 | | } |
658 | | |
659 | | impl ChunkedProcessor { |
660 | | /// Create a new chunked processor with given chunk size |
661 | | #[must_use] |
662 | 9 | pub fn new(chunk_size: usize) -> Self { |
663 | 9 | Self { chunk_size } |
664 | 9 | } |
665 | | |
666 | | /// Get the chunk size |
667 | | #[must_use] |
668 | 2 | pub fn chunk_size(&self) -> usize { |
669 | 2 | self.chunk_size |
670 | 2 | } |
671 | | |
672 | | /// Calculate number of chunks needed for given input length |
673 | | #[must_use] |
674 | 15 | pub fn num_chunks(&self, total_len: usize) -> usize { |
675 | 15 | if total_len == 0 { |
676 | 4 | return 0; |
677 | 11 | } |
678 | 11 | total_len.div_ceil(self.chunk_size) |
679 | 15 | } |
680 | | |
681 | | /// Get bounds (start, end) for a specific chunk index |
682 | | #[must_use] |
683 | 14 | pub fn chunk_bounds(&self, chunk_idx: usize, total_len: usize) -> (usize, usize) { |
684 | 14 | let start = chunk_idx * self.chunk_size; |
685 | 14 | let end = (start + self.chunk_size).min(total_len); |
686 | 14 | (start, end) |
687 | 14 | } |
688 | | |
689 | | /// Process data in chunks, accumulating results |
690 | 4 | pub fn process_chunks<T, F>(&self, data: &[T], mut process: F) -> f32 |
691 | 4 | where |
692 | 4 | F: FnMut(&[T]) -> f32, |
693 | | { |
694 | 4 | let mut total = 0.0f32; |
695 | 4 | let num_chunks = self.num_chunks(data.len()); |
696 | | |
697 | 6 | for chunk_idx in 0..num_chunks4 { |
698 | 6 | let (start, end) = self.chunk_bounds(chunk_idx, data.len()); |
699 | 6 | total += process(&data[start..end]); |
700 | 6 | } |
701 | | |
702 | 4 | total |
703 | 4 | } |
704 | | } |
705 | | |
706 | | /// Inference pipeline stages (M24 - IMP-057) |
707 | | /// |
708 | | /// Represents the different stages of transformer inference. |
709 | | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
710 | | #[repr(u8)] |
711 | | pub enum GpuPipelineStage { |
712 | | /// Token embedding lookup |
713 | | Embed = 0, |
714 | | /// Self-attention computation |
715 | | Attention = 1, |
716 | | /// Feed-forward network |
717 | | FFN = 2, |
718 | | /// Output projection and sampling |
719 | | Output = 3, |
720 | | } |
721 | | |
722 | | /// Inference pipeline coordinator (M24 - IMP-057) |
723 | | /// |
724 | | /// Manages multi-stage inference pipeline with timing tracking. |
725 | | #[derive(Debug)] |
726 | | pub struct InferencePipeline { |
727 | | num_stages: usize, |
728 | | stage_times: std::collections::HashMap<GpuPipelineStage, f32>, |
729 | | } |
730 | | |
731 | | impl InferencePipeline { |
732 | | /// Create a new pipeline with given number of stages |
733 | | #[must_use] |
734 | 6 | pub fn new(num_stages: usize) -> Self { |
735 | 6 | Self { |
736 | 6 | num_stages, |
737 | 6 | stage_times: std::collections::HashMap::new(), |
738 | 6 | } |
739 | 6 | } |
740 | | |
741 | | /// Get number of stages in the pipeline |
742 | | #[must_use] |
743 | 3 | pub fn num_stages(&self) -> usize { |
744 | 3 | self.num_stages |
745 | 3 | } |
746 | | |
747 | | /// Record timing for a pipeline stage (in milliseconds) |
748 | 13 | pub fn record_stage_time(&mut self, stage: GpuPipelineStage, time_ms: f32) { |
749 | 13 | self.stage_times.insert(stage, time_ms); |
750 | 13 | } |
751 | | |
752 | | /// Get total pipeline latency (sum of all stage times) |
753 | | #[must_use] |
754 | 8 | pub fn total_latency(&self) -> f32 { |
755 | 8 | self.stage_times.values().sum() |
756 | 8 | } |
757 | | |
758 | | /// Get breakdown of stage timings |
759 | | #[must_use] |
760 | 3 | pub fn stage_breakdown(&self) -> &std::collections::HashMap<GpuPipelineStage, f32> { |
761 | 3 | &self.stage_times |
762 | 3 | } |
763 | | |
764 | | /// Reset pipeline for new forward pass |
765 | 3 | pub fn reset(&mut self) { |
766 | 3 | self.stage_times.clear(); |
767 | 3 | } |
768 | | } |
769 | | // M29: Error Recovery & Graceful Degradation (Phase 20) |
770 | | // ============================================================================ |
771 | | |
772 | | /// Error classification for recovery decisions |
773 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
774 | | pub enum ErrorClassification { |
775 | | /// Transient error - may succeed on retry |
776 | | Transient, |
777 | | /// Fatal error - should not retry |
778 | | Fatal, |
779 | | /// GPU-specific error - may fallback to CPU |
780 | | GpuFailure, |
781 | | } |
782 | | |
783 | | /// Recovery action to take |
784 | | #[derive(Debug, Clone)] |
785 | | pub enum RecoveryAction { |
786 | | /// Retry the operation with a delay |
787 | | Retry { |
788 | | /// Delay before retry |
789 | | delay: Duration, |
790 | | }, |
791 | | /// Fallback to CPU inference |
792 | | FallbackToCpu, |
793 | | /// Give up and propagate error |
794 | | Fail, |
795 | | } |
796 | | |
797 | | /// Error recovery strategy with exponential backoff |
798 | | pub struct ErrorRecoveryStrategy { |
799 | | max_retries: u32, |
800 | | base_delay: Duration, |
801 | | max_delay: Duration, |
802 | | jitter: f64, |
803 | | } |
804 | | |
805 | | impl ErrorRecoveryStrategy { |
806 | | /// Create new error recovery strategy |
807 | | #[must_use] |
808 | 1 | pub fn new() -> Self { |
809 | 1 | Self { |
810 | 1 | max_retries: 3, |
811 | 1 | base_delay: Duration::from_millis(100), |
812 | 1 | max_delay: Duration::from_secs(10), |
813 | 1 | jitter: 0.1, |
814 | 1 | } |
815 | 1 | } |
816 | | |
817 | | /// Set maximum retries |
818 | | #[must_use] |
819 | 1 | pub fn with_max_retries(mut self, max_retries: u32) -> Self { |
820 | 1 | self.max_retries = max_retries; |
821 | 1 | self |
822 | 1 | } |
823 | | |
824 | | /// Set base delay |
825 | | #[must_use] |
826 | 1 | pub fn with_base_delay(mut self, base_delay: Duration) -> Self { |
827 | 1 | self.base_delay = base_delay; |
828 | 1 | self |
829 | 1 | } |
830 | | |
831 | | /// Set maximum delay |
832 | | #[must_use] |
833 | 1 | pub fn with_max_delay(mut self, max_delay: Duration) -> Self { |
834 | 1 | self.max_delay = max_delay; |
835 | 1 | self |
836 | 1 | } |
837 | | |
838 | | /// Set jitter factor (0.0 - 1.0) |
839 | | #[must_use] |
840 | 1 | pub fn with_jitter(mut self, jitter: f64) -> Self { |
841 | 1 | self.jitter = jitter.clamp(0.0, 1.0); |
842 | 1 | self |
843 | 1 | } |
844 | | |
845 | | /// Get max retries |
846 | | #[must_use] |
847 | 1 | pub fn max_retries(&self) -> u32 { |
848 | 1 | self.max_retries |
849 | 1 | } |
850 | | |
851 | | /// Classify an error |
852 | | #[must_use] |
853 | 3 | pub fn classify_error(&self, error: &std::io::Error) -> ErrorClassification { |
854 | 3 | match error.kind() { |
855 | | std::io::ErrorKind::TimedOut |
856 | | | std::io::ErrorKind::ConnectionReset |
857 | | | std::io::ErrorKind::ConnectionAborted |
858 | | | std::io::ErrorKind::Interrupted |
859 | 2 | | std::io::ErrorKind::WouldBlock => ErrorClassification::Transient, |
860 | | |
861 | | std::io::ErrorKind::Other => { |
862 | 0 | let msg = error.to_string().to_lowercase(); |
863 | 0 | if msg.contains("gpu") || msg.contains("cuda") || msg.contains("wgpu") { |
864 | 0 | ErrorClassification::GpuFailure |
865 | | } else { |
866 | 0 | ErrorClassification::Transient |
867 | | } |
868 | | }, |
869 | | |
870 | 1 | _ => ErrorClassification::Fatal, |
871 | | } |
872 | 3 | } |
873 | | |
874 | | /// Determine recovery action |
875 | | #[must_use] |
876 | 2 | pub fn determine_action(&self, error: &std::io::Error, attempt: u32) -> RecoveryAction { |
877 | 2 | if attempt >= self.max_retries { |
878 | 1 | return RecoveryAction::Fail; |
879 | 1 | } |
880 | | |
881 | 1 | match self.classify_error(error) { |
882 | 1 | ErrorClassification::Transient => RecoveryAction::Retry { |
883 | 1 | delay: self.calculate_delay(attempt), |
884 | 1 | }, |
885 | 0 | ErrorClassification::GpuFailure => RecoveryAction::FallbackToCpu, |
886 | 0 | ErrorClassification::Fatal => RecoveryAction::Fail, |
887 | | } |
888 | 2 | } |
889 | | |
890 | | /// Determine action with explicit GPU fallback |
891 | | #[must_use] |
892 | 1 | pub fn determine_action_with_fallback( |
893 | 1 | &self, |
894 | 1 | error: &std::io::Error, |
895 | 1 | attempt: u32, |
896 | 1 | ) -> RecoveryAction { |
897 | 1 | let msg = error.to_string().to_lowercase(); |
898 | 1 | if msg.contains("gpu") || msg.contains("unavailable")0 { |
899 | 1 | RecoveryAction::FallbackToCpu |
900 | | } else { |
901 | 0 | self.determine_action(error, attempt) |
902 | | } |
903 | 1 | } |
904 | | |
905 | | /// Calculate delay for retry attempt with exponential backoff |
906 | | #[must_use] |
907 | 4 | pub fn calculate_delay(&self, attempt: u32) -> Duration { |
908 | 4 | let base_ms = self.base_delay.as_millis() as f64; |
909 | 4 | let exp_delay = base_ms * 2.0_f64.powi(attempt as i32); |
910 | 4 | let capped_delay = exp_delay.min(self.max_delay.as_millis() as f64); |
911 | | |
912 | | // Add jitter |
913 | 4 | let jitter_range = capped_delay * self.jitter; |
914 | 4 | let jittered = capped_delay + (jitter_range * 0.5); // Simplified jitter |
915 | | |
916 | 4 | Duration::from_millis(jittered as u64) |
917 | 4 | } |
918 | | } |
919 | | |
920 | | impl Default for ErrorRecoveryStrategy { |
921 | 0 | fn default() -> Self { |
922 | 0 | Self::new() |
923 | 0 | } |
924 | | } |
925 | | |
926 | | /// Degradation mode for system state |
927 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
928 | | pub enum DegradationMode { |
929 | | /// Normal operation |
930 | | Normal, |
931 | | /// Running on CPU fallback |
932 | | CpuFallback, |
933 | | /// Memory pressure - reduced capacity |
934 | | MemoryPressure, |
935 | | /// Low latency priority mode |
936 | | LowLatency, |
937 | | /// High throughput priority mode |
938 | | HighThroughput, |
939 | | } |
940 | | |
941 | | /// System load metrics |
942 | | #[derive(Debug, Clone, Copy)] |
943 | | pub struct SystemLoad { |
944 | | /// CPU utilization percentage |
945 | | pub cpu_percent: f64, |
946 | | /// Memory utilization percentage |
947 | | pub memory_percent: f64, |
948 | | /// Current queue depth |
949 | | pub queue_depth: u32, |
950 | | } |
951 | | |
952 | | /// Graceful degradation manager |
953 | | pub struct DegradationManager { |
954 | | gpu_available: bool, |
955 | | memory_pressure: f64, |
956 | | system_load: Option<SystemLoad>, |
957 | | latency_priority: bool, |
958 | | mode: DegradationMode, |
959 | | } |
960 | | |
961 | | impl DegradationManager { |
962 | | /// Create new degradation manager |
963 | | #[must_use] |
964 | 1 | pub fn new() -> Self { |
965 | 1 | Self { |
966 | 1 | gpu_available: true, |
967 | 1 | memory_pressure: 0.0, |
968 | 1 | system_load: None, |
969 | 1 | latency_priority: false, |
970 | 1 | mode: DegradationMode::Normal, |
971 | 1 | } |
972 | 1 | } |
973 | | |
974 | | /// Get current degradation mode |
975 | | #[must_use] |
976 | 4 | pub fn current_mode(&self) -> DegradationMode { |
977 | 4 | self.mode |
978 | 4 | } |
979 | | |
980 | | /// Set GPU availability |
981 | 3 | pub fn set_gpu_available(&mut self, available: bool) { |
982 | 3 | self.gpu_available = available; |
983 | 3 | self.update_mode(); |
984 | 3 | } |
985 | | |
986 | | /// Update memory pressure (0.0 - 1.0) |
987 | 2 | pub fn update_memory_pressure(&mut self, pressure: f64) { |
988 | 2 | self.memory_pressure = pressure.clamp(0.0, 1.0); |
989 | 2 | self.update_mode(); |
990 | 2 | } |
991 | | |
992 | | /// Update system load |
993 | 2 | pub fn update_system_load(&mut self, load: SystemLoad) { |
994 | 2 | self.system_load = Some(load); |
995 | 2 | self.update_mode(); |
996 | 2 | } |
997 | | |
998 | | /// Set latency priority mode |
999 | 2 | pub fn set_latency_priority(&mut self, priority: bool) { |
1000 | 2 | self.latency_priority = priority; |
1001 | 2 | self.update_mode(); |
1002 | 2 | } |
1003 | | |
1004 | | /// Get recommended batch size based on system state |
1005 | | #[must_use] |
1006 | 1 | pub fn recommended_batch_size(&self, requested: usize) -> usize { |
1007 | 1 | if self.memory_pressure > 0.8 { |
1008 | | // Reduce batch size under memory pressure |
1009 | 1 | (requested as f64 * (1.0 - self.memory_pressure)).max(1.0) as usize |
1010 | | } else { |
1011 | 0 | requested |
1012 | | } |
1013 | 1 | } |
1014 | | |
1015 | | /// Get recommended max context length based on system state |
1016 | | #[must_use] |
1017 | 1 | pub fn recommended_max_context(&self, requested: usize) -> usize { |
1018 | 1 | if let Some(load) = &self.system_load { |
1019 | 1 | if load.cpu_percent > 90.0 || load.memory_percent > 80.00 || load.queue_depth > 500 { |
1020 | | // Reduce context length under high load |
1021 | 1 | (requested as f64 * 0.75).max(256.0) as usize |
1022 | | } else { |
1023 | 0 | requested |
1024 | | } |
1025 | | } else { |
1026 | 0 | requested |
1027 | | } |
1028 | 1 | } |
1029 | | |
1030 | 9 | fn update_mode(&mut self) { |
1031 | 9 | self.mode = if !self.gpu_available { |
1032 | 1 | DegradationMode::CpuFallback |
1033 | 8 | } else if self.latency_priority { |
1034 | 3 | DegradationMode::LowLatency |
1035 | 5 | } else if self.memory_pressure > 0.8 { |
1036 | 2 | DegradationMode::MemoryPressure |
1037 | 3 | } else if let Some(load2 ) = &self.system_load { |
1038 | 2 | if load.cpu_percent > 90.0 || load.memory_percent > 80.01 { |
1039 | 1 | DegradationMode::MemoryPressure |
1040 | | } else { |
1041 | 1 | DegradationMode::Normal |
1042 | | } |
1043 | | } else { |
1044 | 1 | DegradationMode::Normal |
1045 | | }; |
1046 | 9 | } |
1047 | | } |
1048 | | |
1049 | | impl Default for DegradationManager { |
1050 | 0 | fn default() -> Self { |
1051 | 0 | Self::new() |
1052 | 0 | } |
1053 | | } |
1054 | | |
1055 | | /// Request outcome for failure tracking |
1056 | | #[derive(Debug, Clone)] |
1057 | | pub enum RequestOutcome { |
1058 | | /// Request completed successfully |
1059 | | Success, |
1060 | | /// Request failed with error message |
1061 | | Failed(String), |
1062 | | } |
1063 | | |
1064 | | /// Failure isolator with circuit breaker |
1065 | | pub struct FailureIsolator { |
1066 | | active_requests: std::sync::atomic::AtomicU64, |
1067 | | success_count: std::sync::atomic::AtomicU64, |
1068 | | failure_count: std::sync::atomic::AtomicU64, |
1069 | | consecutive_failures: std::sync::atomic::AtomicU32, |
1070 | | circuit_open: std::sync::atomic::AtomicBool, |
1071 | | next_request_id: std::sync::atomic::AtomicU64, |
1072 | | failure_threshold: u32, |
1073 | | cleanups: std::sync::Mutex<HashMap<u64, Box<dyn FnOnce() + Send>>>, |
1074 | | } |
1075 | | |
1076 | | impl FailureIsolator { |
1077 | | /// Create new failure isolator |
1078 | | #[must_use] |
1079 | 1 | pub fn new() -> Self { |
1080 | 1 | Self { |
1081 | 1 | active_requests: std::sync::atomic::AtomicU64::new(0), |
1082 | 1 | success_count: std::sync::atomic::AtomicU64::new(0), |
1083 | 1 | failure_count: std::sync::atomic::AtomicU64::new(0), |
1084 | 1 | consecutive_failures: std::sync::atomic::AtomicU32::new(0), |
1085 | 1 | circuit_open: std::sync::atomic::AtomicBool::new(false), |
1086 | 1 | next_request_id: std::sync::atomic::AtomicU64::new(0), |
1087 | 1 | failure_threshold: 5, |
1088 | 1 | cleanups: std::sync::Mutex::new(HashMap::new()), |
1089 | 1 | } |
1090 | 1 | } |
1091 | | |
1092 | | /// Get number of active requests |
1093 | | #[must_use] |
1094 | 3 | pub fn active_requests(&self) -> u64 { |
1095 | 3 | self.active_requests |
1096 | 3 | .load(std::sync::atomic::Ordering::SeqCst) |
1097 | 3 | } |
1098 | | |
1099 | | /// Get success count |
1100 | | #[must_use] |
1101 | 1 | pub fn success_count(&self) -> u64 { |
1102 | 1 | self.success_count.load(std::sync::atomic::Ordering::SeqCst) |
1103 | 1 | } |
1104 | | |
1105 | | /// Get failure count |
1106 | | #[must_use] |
1107 | 1 | pub fn failure_count(&self) -> u64 { |
1108 | 1 | self.failure_count.load(std::sync::atomic::Ordering::SeqCst) |
1109 | 1 | } |
1110 | | |
1111 | | /// Check if circuit is open |
1112 | | #[must_use] |
1113 | 4 | pub fn is_circuit_open(&self) -> bool { |
1114 | 4 | self.circuit_open.load(std::sync::atomic::Ordering::SeqCst) |
1115 | 4 | } |
1116 | | |
1117 | | /// Start a new isolated request |
1118 | | #[must_use] |
1119 | 8 | pub fn start_request(&self) -> u64 { |
1120 | 8 | self.active_requests |
1121 | 8 | .fetch_add(1, std::sync::atomic::Ordering::SeqCst); |
1122 | 8 | self.next_request_id |
1123 | 8 | .fetch_add(1, std::sync::atomic::Ordering::SeqCst) |
1124 | 8 | } |
1125 | | |
1126 | | /// Try to start a request (fails if circuit is open) |
1127 | | /// |
1128 | | /// # Errors |
1129 | | /// Returns error if circuit breaker is open. |
1130 | 2 | pub fn try_start_request(&self) -> std::result::Result<u64, &'static str> { |
1131 | 2 | if self.is_circuit_open() { |
1132 | 1 | Err("Circuit breaker is open") |
1133 | | } else { |
1134 | 1 | Ok(self.start_request()) |
1135 | | } |
1136 | 2 | } |
1137 | | |
1138 | | /// Register cleanup handler for a request |
1139 | 1 | pub fn register_cleanup<F: FnOnce() + Send + 'static>(&self, request_id: u64, cleanup: F) { |
1140 | 1 | if let Ok(mut cleanups) = self.cleanups.lock() { |
1141 | 1 | cleanups.insert(request_id, Box::new(cleanup)); |
1142 | 1 | }0 |
1143 | 1 | } |
1144 | | |
1145 | | /// Complete a request with outcome |
1146 | 7 | pub fn complete_request(&self, request_id: u64, outcome: &RequestOutcome) { |
1147 | 7 | self.active_requests |
1148 | 7 | .fetch_sub(1, std::sync::atomic::Ordering::SeqCst); |
1149 | | |
1150 | 7 | match outcome { |
1151 | 1 | RequestOutcome::Success => { |
1152 | 1 | self.success_count |
1153 | 1 | .fetch_add(1, std::sync::atomic::Ordering::SeqCst); |
1154 | 1 | self.consecutive_failures |
1155 | 1 | .store(0, std::sync::atomic::Ordering::SeqCst); |
1156 | 1 | }, |
1157 | | RequestOutcome::Failed(_) => { |
1158 | 6 | self.failure_count |
1159 | 6 | .fetch_add(1, std::sync::atomic::Ordering::SeqCst); |
1160 | 6 | let failures = self |
1161 | 6 | .consecutive_failures |
1162 | 6 | .fetch_add(1, std::sync::atomic::Ordering::SeqCst) |
1163 | 6 | + 1; |
1164 | | |
1165 | | // Open circuit if threshold exceeded |
1166 | 6 | if failures >= self.failure_threshold { |
1167 | 2 | self.circuit_open |
1168 | 2 | .store(true, std::sync::atomic::Ordering::SeqCst); |
1169 | 4 | } |
1170 | | |
1171 | | // Run cleanup handler |
1172 | 6 | if let Ok(mut cleanups) = self.cleanups.lock() { |
1173 | 6 | if let Some(cleanup1 ) = cleanups.remove(&request_id) { |
1174 | 1 | cleanup(); |
1175 | 5 | } |
1176 | 0 | } |
1177 | | }, |
1178 | | } |
1179 | 7 | } |
1180 | | |
1181 | | /// Reset circuit breaker |
1182 | 1 | pub fn reset_circuit(&self) { |
1183 | 1 | self.circuit_open |
1184 | 1 | .store(false, std::sync::atomic::Ordering::SeqCst); |
1185 | 1 | self.consecutive_failures |
1186 | 1 | .store(0, std::sync::atomic::Ordering::SeqCst); |
1187 | 1 | } |
1188 | | } |
1189 | | |
1190 | | impl Default for FailureIsolator { |
1191 | 0 | fn default() -> Self { |
1192 | 0 | Self::new() |
1193 | 0 | } |
1194 | | } |
1195 | | |
1196 | | /// Isolated request handle (unused but kept for API completeness) |
1197 | | #[allow(dead_code)] |
1198 | | pub struct IsolatedRequest { |
1199 | | id: u64, |
1200 | | } |
1201 | | |
1202 | | // ============================================================================ |
1203 | | // M30: Connection Pooling & Resource Limits (IMP-073, IMP-074, IMP-075) |
1204 | | // ============================================================================ |
1205 | | |
1206 | | /// Connection pool configuration (IMP-073) |
1207 | | #[derive(Debug, Clone)] |
1208 | | pub struct ConnectionConfig { |
1209 | | max_connections: usize, |
1210 | | min_connections: usize, |
1211 | | idle_timeout: Duration, |
1212 | | } |
1213 | | |
1214 | | impl ConnectionConfig { |
1215 | | /// Create new connection config with defaults |
1216 | | #[must_use] |
1217 | 2 | pub fn new() -> Self { |
1218 | 2 | Self { |
1219 | 2 | max_connections: 10, |
1220 | 2 | min_connections: 1, |
1221 | 2 | idle_timeout: Duration::from_secs(300), |
1222 | 2 | } |
1223 | 2 | } |
1224 | | |
1225 | | /// Set maximum connections |
1226 | | #[must_use] |
1227 | 1 | pub fn with_max_connections(mut self, max: usize) -> Self { |
1228 | 1 | self.max_connections = max; |
1229 | 1 | self |
1230 | 1 | } |
1231 | | |
1232 | | /// Set minimum connections |
1233 | | #[must_use] |
1234 | 2 | pub fn with_min_connections(mut self, min: usize) -> Self { |
1235 | 2 | self.min_connections = min; |
1236 | 2 | self |
1237 | 2 | } |
1238 | | |
1239 | | /// Set idle timeout |
1240 | | #[must_use] |
1241 | 1 | pub fn with_idle_timeout(mut self, timeout: Duration) -> Self { |
1242 | 1 | self.idle_timeout = timeout; |
1243 | 1 | self |
1244 | 1 | } |
1245 | | } |
1246 | | |
1247 | | impl Default for ConnectionConfig { |
1248 | 0 | fn default() -> Self { |
1249 | 0 | Self::new() |
1250 | 0 | } |
1251 | | } |
1252 | | |
1253 | | /// Connection state for health checking |
1254 | | #[derive(Debug, Clone, PartialEq, Eq)] |
1255 | | pub enum ConnectionState { |
1256 | | /// Connection is healthy |
1257 | | Healthy, |
1258 | | /// Connection is stale and needs recycling |
1259 | | Stale, |
1260 | | /// Connection is broken |
1261 | | Broken, |
1262 | | } |
1263 | | |
1264 | | /// Connection handle |
1265 | | #[derive(Debug)] |
1266 | | pub struct Connection { |
1267 | | #[allow(dead_code)] |
1268 | | id: u64, |
1269 | | created_at: std::time::Instant, |
1270 | | } |
1271 | | |
1272 | | /// Connection pool with bounded capacity (IMP-073) |
1273 | | pub struct ConnectionPool { |
1274 | | config: ConnectionConfig, |
1275 | | active: std::sync::atomic::AtomicUsize, |
1276 | | idle: std::sync::Mutex<Vec<Connection>>, |
1277 | | next_id: std::sync::atomic::AtomicU64, |
1278 | | } |
1279 | | |
1280 | | impl ConnectionPool { |
1281 | | /// Create new connection pool |
1282 | | #[must_use] |
1283 | 2 | pub fn new(config: ConnectionConfig) -> Self { |
1284 | 2 | Self { |
1285 | 2 | config, |
1286 | 2 | active: std::sync::atomic::AtomicUsize::new(0), |
1287 | 2 | idle: std::sync::Mutex::new(Vec::new()), |
1288 | 2 | next_id: std::sync::atomic::AtomicU64::new(0), |
1289 | 2 | } |
1290 | 2 | } |
1291 | | |
1292 | | /// Get max connections |
1293 | | #[must_use] |
1294 | 1 | pub fn max_connections(&self) -> usize { |
1295 | 1 | self.config.max_connections |
1296 | 1 | } |
1297 | | |
1298 | | /// Get min connections |
1299 | | #[must_use] |
1300 | 1 | pub fn min_connections(&self) -> usize { |
1301 | 1 | self.config.min_connections |
1302 | 1 | } |
1303 | | |
1304 | | /// Get active connection count |
1305 | | #[must_use] |
1306 | 2 | pub fn active_connections(&self) -> usize { |
1307 | 2 | self.active.load(std::sync::atomic::Ordering::SeqCst) |
1308 | 2 | } |
1309 | | |
1310 | | /// Get idle connection count |
1311 | | #[must_use] |
1312 | 2 | pub fn idle_connections(&self) -> usize { |
1313 | 2 | self.idle.lock().expect("mutex poisoned").len() |
1314 | 2 | } |
1315 | | |
1316 | | /// Acquire a connection (blocking) |
1317 | | /// |
1318 | | /// # Errors |
1319 | | /// Returns error if pool is exhausted. |
1320 | 13 | pub fn acquire(&self) -> std::result::Result<Connection, &'static str> { |
1321 | | // Try to get from idle pool first |
1322 | | { |
1323 | 13 | let mut idle = self.idle.lock().expect("mutex poisoned"); |
1324 | 13 | if let Some(conn2 ) = idle.pop() { |
1325 | 2 | self.active |
1326 | 2 | .fetch_add(1, std::sync::atomic::Ordering::SeqCst); |
1327 | 2 | return Ok(conn); |
1328 | 11 | } |
1329 | | } |
1330 | | |
1331 | | // Create new if under limit |
1332 | 11 | let current = self.active.load(std::sync::atomic::Ordering::SeqCst); |
1333 | 11 | if current < self.config.max_connections { |
1334 | 10 | self.active |
1335 | 10 | .fetch_add(1, std::sync::atomic::Ordering::SeqCst); |
1336 | 10 | let id = self |
1337 | 10 | .next_id |
1338 | 10 | .fetch_add(1, std::sync::atomic::Ordering::SeqCst); |
1339 | 10 | return Ok(Connection { |
1340 | 10 | id, |
1341 | 10 | created_at: std::time::Instant::now(), |
1342 | 10 | }); |
1343 | 1 | } |
1344 | | |
1345 | 1 | Err("Pool exhausted") |
1346 | 13 | } |
1347 | | |
1348 | | /// Try to acquire a connection (non-blocking) |
1349 | | /// |
1350 | | /// # Errors |
1351 | | /// Returns error if pool is exhausted. |
1352 | 1 | pub fn try_acquire(&self) -> std::result::Result<Connection, &'static str> { |
1353 | 1 | self.acquire() |
1354 | 1 | } |
1355 | | |
1356 | | /// Release a connection back to pool |
1357 | 12 | pub fn release(&self, conn: Connection) { |
1358 | 12 | self.active |
1359 | 12 | .fetch_sub(1, std::sync::atomic::Ordering::SeqCst); |
1360 | 12 | let mut idle = self.idle.lock().expect("mutex poisoned"); |
1361 | 12 | idle.push(conn); |
1362 | 12 | } |
1363 | | |
1364 | | /// Check connection health |
1365 | | #[must_use] |
1366 | 1 | pub fn check_health(&self, conn: &Connection) -> ConnectionState { |
1367 | 1 | let age = conn.created_at.elapsed(); |
1368 | 1 | if age > self.config.idle_timeout { |
1369 | 0 | ConnectionState::Stale |
1370 | | } else { |
1371 | 1 | ConnectionState::Healthy |
1372 | | } |
1373 | 1 | } |
1374 | | |
1375 | | /// Warm the pool to min_connections |
1376 | 1 | pub fn warm(&self) { |
1377 | 1 | let current_idle = self.idle_connections(); |
1378 | 1 | let need = self.config.min_connections.saturating_sub(current_idle); |
1379 | | |
1380 | 1 | let mut idle = self.idle.lock().expect("mutex poisoned"); |
1381 | 3 | for _ in 0..need1 { |
1382 | 3 | let id = self |
1383 | 3 | .next_id |
1384 | 3 | .fetch_add(1, std::sync::atomic::Ordering::SeqCst); |
1385 | 3 | idle.push(Connection { |
1386 | 3 | id, |
1387 | 3 | created_at: std::time::Instant::now(), |
1388 | 3 | }); |
1389 | 3 | } |
1390 | 1 | } |
1391 | | } |
1392 | | |
1393 | | /// Resource configuration (IMP-074) |
1394 | | #[derive(Debug, Clone)] |
1395 | | #[allow(clippy::struct_field_names)] |
1396 | | pub struct ResourceConfig { |
1397 | | max_memory_per_request: u64, |
1398 | | max_total_memory: u64, |
1399 | | max_compute_time: Duration, |
1400 | | max_queue_depth: usize, |
1401 | | } |
1402 | | |
1403 | | impl ResourceConfig { |
1404 | | /// Create new resource config with defaults |
1405 | | #[must_use] |
1406 | 1 | pub fn new() -> Self { |
1407 | 1 | Self { |
1408 | 1 | max_memory_per_request: 512 * 1024 * 1024, // 512MB |
1409 | 1 | max_total_memory: 4 * 1024 * 1024 * 1024, // 4GB |
1410 | 1 | max_compute_time: Duration::from_secs(30), |
1411 | 1 | max_queue_depth: 100, |
1412 | 1 | } |
1413 | 1 | } |
1414 | | |
1415 | | /// Set max memory per request |
1416 | | #[must_use] |
1417 | 1 | pub fn with_max_memory_per_request(mut self, bytes: u64) -> Self { |
1418 | 1 | self.max_memory_per_request = bytes; |
1419 | 1 | self |
1420 | 1 | } |
1421 | | |
1422 | | /// Set max total memory |
1423 | | #[must_use] |
1424 | 1 | pub fn with_max_total_memory(mut self, bytes: u64) -> Self { |
1425 | 1 | self.max_total_memory = bytes; |
1426 | 1 | self |
1427 | 1 | } |
1428 | | |
1429 | | /// Set max compute time |
1430 | | #[must_use] |
1431 | 1 | pub fn with_max_compute_time(mut self, time: Duration) -> Self { |
1432 | 1 | self.max_compute_time = time; |
1433 | 1 | self |
1434 | 1 | } |
1435 | | |
1436 | | /// Set max queue depth |
1437 | | #[must_use] |
1438 | 1 | pub fn with_max_queue_depth(mut self, depth: usize) -> Self { |
1439 | 1 | self.max_queue_depth = depth; |
1440 | 1 | self |
1441 | 1 | } |
1442 | | } |
1443 | | |
1444 | | impl Default for ResourceConfig { |
1445 | 0 | fn default() -> Self { |
1446 | 0 | Self::new() |
1447 | 0 | } |
1448 | | } |
1449 | | |
1450 | | /// Result of limit check |
1451 | | #[derive(Debug, Clone)] |
1452 | | pub enum LimitResult { |
1453 | | /// Request is allowed |
1454 | | Allowed, |
1455 | | /// Request is denied with reason |
1456 | | Denied { |
1457 | | /// Reason for denial |
1458 | | reason: String, |
1459 | | }, |
1460 | | /// Backpressure should be applied |
1461 | | Backpressure, |
1462 | | } |
1463 | | |
1464 | | /// Resource limiter (IMP-074) |
1465 | | pub struct ResourceLimiter { |
1466 | | config: ResourceConfig, |
1467 | | current_memory: std::sync::atomic::AtomicU64, |
1468 | | queue_depth: std::sync::atomic::AtomicUsize, |
1469 | | } |
1470 | | |
1471 | | impl ResourceLimiter { |
1472 | | /// Create new resource limiter |
1473 | | #[must_use] |
1474 | 1 | pub fn new(config: ResourceConfig) -> Self { |
1475 | 1 | Self { |
1476 | 1 | config, |
1477 | 1 | current_memory: std::sync::atomic::AtomicU64::new(0), |
1478 | 1 | queue_depth: std::sync::atomic::AtomicUsize::new(0), |
1479 | 1 | } |
1480 | 1 | } |
1481 | | |
1482 | | /// Check if memory request is within limits |
1483 | | #[must_use] |
1484 | 3 | pub fn check_memory(&self, bytes: u64) -> LimitResult { |
1485 | 3 | if bytes > self.config.max_memory_per_request { |
1486 | 1 | return LimitResult::Denied { |
1487 | 1 | reason: format!( |
1488 | 1 | "Request {} bytes exceeds per-request limit {} bytes", |
1489 | 1 | bytes, self.config.max_memory_per_request |
1490 | 1 | ), |
1491 | 1 | }; |
1492 | 2 | } |
1493 | | |
1494 | 2 | let current = self |
1495 | 2 | .current_memory |
1496 | 2 | .load(std::sync::atomic::Ordering::SeqCst); |
1497 | 2 | if current + bytes > self.config.max_total_memory { |
1498 | 0 | return LimitResult::Denied { |
1499 | 0 | reason: format!( |
1500 | 0 | "Total memory {} + {} would exceed limit {}", |
1501 | 0 | current, bytes, self.config.max_total_memory |
1502 | 0 | ), |
1503 | 0 | }; |
1504 | 2 | } |
1505 | | |
1506 | 2 | LimitResult::Allowed |
1507 | 3 | } |
1508 | | |
1509 | | /// Allocate memory |
1510 | | /// |
1511 | | /// # Errors |
1512 | | /// Returns error if memory limit exceeded. |
1513 | 1 | pub fn allocate(&self, bytes: u64) -> std::result::Result<(), &'static str> { |
1514 | 1 | if let LimitResult::Denied { .. } = self.check_memory(bytes) { |
1515 | 0 | return Err("Memory limit exceeded"); |
1516 | 1 | } |
1517 | 1 | self.current_memory |
1518 | 1 | .fetch_add(bytes, std::sync::atomic::Ordering::SeqCst); |
1519 | 1 | Ok(()) |
1520 | 1 | } |
1521 | | |
1522 | | /// Deallocate memory |
1523 | 1 | pub fn deallocate(&self, bytes: u64) { |
1524 | 1 | self.current_memory |
1525 | 1 | .fetch_sub(bytes, std::sync::atomic::Ordering::SeqCst); |
1526 | 1 | } |
1527 | | |
1528 | | /// Get current memory usage |
1529 | | #[must_use] |
1530 | 2 | pub fn current_memory(&self) -> u64 { |
1531 | 2 | self.current_memory |
1532 | 2 | .load(std::sync::atomic::Ordering::SeqCst) |
1533 | 2 | } |
1534 | | |
1535 | | /// Enqueue a request |
1536 | 100 | pub fn enqueue(&self) -> LimitResult { |
1537 | 100 | let current = self |
1538 | 100 | .queue_depth |
1539 | 100 | .fetch_add(1, std::sync::atomic::Ordering::SeqCst); |
1540 | 100 | if current >= self.config.max_queue_depth { |
1541 | 0 | self.queue_depth |
1542 | 0 | .fetch_sub(1, std::sync::atomic::Ordering::SeqCst); |
1543 | 0 | LimitResult::Backpressure |
1544 | | } else { |
1545 | 100 | LimitResult::Allowed |
1546 | | } |
1547 | 100 | } |
1548 | | |
1549 | | /// Try to enqueue (returns backpressure if full) |
1550 | | #[must_use] |
1551 | 1 | pub fn try_enqueue(&self) -> LimitResult { |
1552 | 1 | let current = self.queue_depth.load(std::sync::atomic::Ordering::SeqCst); |
1553 | 1 | if current >= self.config.max_queue_depth { |
1554 | 1 | LimitResult::Backpressure |
1555 | | } else { |
1556 | 0 | self.enqueue() |
1557 | | } |
1558 | 1 | } |
1559 | | |
1560 | | /// Dequeue a request |
1561 | 100 | pub fn dequeue(&self) { |
1562 | 100 | let current = self.queue_depth.load(std::sync::atomic::Ordering::SeqCst); |
1563 | 100 | if current > 0 { |
1564 | 100 | self.queue_depth |
1565 | 100 | .fetch_sub(1, std::sync::atomic::Ordering::SeqCst); |
1566 | 100 | }0 |
1567 | 100 | } |
1568 | | |
1569 | | /// Start compute timer |
1570 | | #[must_use] |
1571 | 1 | pub fn start_compute(&self) -> std::time::Instant { |
1572 | 1 | std::time::Instant::now() |
1573 | 1 | } |
1574 | | } |
1575 | | |
1576 | | /// Resource metrics snapshot (IMP-075) |
1577 | | #[derive(Debug, Clone)] |
1578 | | pub struct ResourceMetrics { |
1579 | | /// Current memory usage in bytes |
1580 | | pub memory_bytes: u64, |
1581 | | /// GPU utilization percentage (0-100) |
1582 | | pub gpu_utilization: f64, |
1583 | | /// Current queue depth |
1584 | | pub queue_depth: usize, |
1585 | | /// Last recorded latency in milliseconds |
1586 | | pub last_latency_ms: u64, |
1587 | | } |
1588 | | |
1589 | | /// Latency statistics |
1590 | | #[derive(Debug, Clone)] |
1591 | | pub struct LatencyStats { |
1592 | | /// Minimum latency in ms |
1593 | | pub min_ms: u64, |
1594 | | /// Maximum latency in ms |
1595 | | pub max_ms: u64, |
1596 | | /// Average latency in ms |
1597 | | pub avg_ms: u64, |
1598 | | } |
1599 | | |
1600 | | /// Resource monitor snapshot |
1601 | | #[derive(Debug, Clone)] |
1602 | | pub struct ResourceSnapshot { |
1603 | | /// Unix timestamp |
1604 | | pub timestamp: u64, |
1605 | | /// Memory in bytes |
1606 | | pub memory_bytes: u64, |
1607 | | /// GPU utilization |
1608 | | pub gpu_utilization: f64, |
1609 | | /// Queue depth |
1610 | | pub queue_depth: usize, |
1611 | | } |
1612 | | |
1613 | | /// Resource monitor (IMP-075) |
1614 | | pub struct ResourceMonitor { |
1615 | | memory_bytes: std::sync::atomic::AtomicU64, |
1616 | | gpu_utilization: std::sync::Mutex<f64>, |
1617 | | queue_depth: std::sync::atomic::AtomicUsize, |
1618 | | latencies: std::sync::Mutex<Vec<u64>>, |
1619 | | last_latency_ms: std::sync::atomic::AtomicU64, |
1620 | | } |
1621 | | |
1622 | | impl ResourceMonitor { |
1623 | | /// Create new resource monitor |
1624 | | #[must_use] |
1625 | 1 | pub fn new() -> Self { |
1626 | 1 | Self { |
1627 | 1 | memory_bytes: std::sync::atomic::AtomicU64::new(0), |
1628 | 1 | gpu_utilization: std::sync::Mutex::new(0.0), |
1629 | 1 | queue_depth: std::sync::atomic::AtomicUsize::new(0), |
1630 | 1 | latencies: std::sync::Mutex::new(Vec::new()), |
1631 | 1 | last_latency_ms: std::sync::atomic::AtomicU64::new(0), |
1632 | 1 | } |
1633 | 1 | } |
1634 | | |
1635 | | /// Record memory usage |
1636 | 1 | pub fn record_memory_usage(&self, bytes: u64) { |
1637 | 1 | self.memory_bytes |
1638 | 1 | .store(bytes, std::sync::atomic::Ordering::SeqCst); |
1639 | 1 | } |
1640 | | |
1641 | | /// Record GPU utilization |
1642 | 1 | pub fn record_gpu_utilization(&self, utilization: f64) { |
1643 | 1 | *self.gpu_utilization.lock().expect("mutex poisoned") = utilization; |
1644 | 1 | } |
1645 | | |
1646 | | /// Record queue depth |
1647 | 1 | pub fn record_queue_depth(&self, depth: usize) { |
1648 | 1 | self.queue_depth |
1649 | 1 | .store(depth, std::sync::atomic::Ordering::SeqCst); |
1650 | 1 | } |
1651 | | |
1652 | | /// Record latency |
1653 | 6 | pub fn record_latency(&self, duration: Duration) { |
1654 | 6 | let ms = duration.as_millis() as u64; |
1655 | 6 | self.last_latency_ms |
1656 | 6 | .store(ms, std::sync::atomic::Ordering::SeqCst); |
1657 | 6 | self.latencies.lock().expect("mutex poisoned").push(ms); |
1658 | 6 | } |
1659 | | |
1660 | | /// Get current metrics |
1661 | | #[must_use] |
1662 | 4 | pub fn current_metrics(&self) -> ResourceMetrics { |
1663 | 4 | ResourceMetrics { |
1664 | 4 | memory_bytes: self.memory_bytes.load(std::sync::atomic::Ordering::SeqCst), |
1665 | 4 | gpu_utilization: *self.gpu_utilization.lock().expect("mutex poisoned"), |
1666 | 4 | queue_depth: self.queue_depth.load(std::sync::atomic::Ordering::SeqCst), |
1667 | 4 | last_latency_ms: self |
1668 | 4 | .last_latency_ms |
1669 | 4 | .load(std::sync::atomic::Ordering::SeqCst), |
1670 | 4 | } |
1671 | 4 | } |
1672 | | |
1673 | | /// Get latency statistics |
1674 | | #[must_use] |
1675 | 1 | pub fn latency_stats(&self) -> LatencyStats { |
1676 | 1 | let latencies = self.latencies.lock().expect("mutex poisoned"); |
1677 | 1 | if latencies.is_empty() { |
1678 | 0 | return LatencyStats { |
1679 | 0 | min_ms: 0, |
1680 | 0 | max_ms: 0, |
1681 | 0 | avg_ms: 0, |
1682 | 0 | }; |
1683 | 1 | } |
1684 | | |
1685 | 1 | let min_ms = *latencies.iter().min().unwrap_or(&0); |
1686 | 1 | let max_ms = *latencies.iter().max().unwrap_or(&0); |
1687 | 1 | let sum: u64 = latencies.iter().sum(); |
1688 | 1 | let avg_ms = sum / latencies.len() as u64; |
1689 | | |
1690 | 1 | LatencyStats { |
1691 | 1 | min_ms, |
1692 | 1 | max_ms, |
1693 | 1 | avg_ms, |
1694 | 1 | } |
1695 | 1 | } |
1696 | | |
1697 | | /// Get snapshot for reporting |
1698 | | #[must_use] |
1699 | 1 | pub fn snapshot(&self) -> ResourceSnapshot { |
1700 | | use std::time::{SystemTime, UNIX_EPOCH}; |
1701 | | |
1702 | 1 | let timestamp = SystemTime::now() |
1703 | 1 | .duration_since(UNIX_EPOCH) |
1704 | 1 | .unwrap_or_default() |
1705 | 1 | .as_secs(); |
1706 | | |
1707 | 1 | ResourceSnapshot { |
1708 | 1 | timestamp, |
1709 | 1 | memory_bytes: self.memory_bytes.load(std::sync::atomic::Ordering::SeqCst), |
1710 | 1 | gpu_utilization: *self.gpu_utilization.lock().expect("mutex poisoned"), |
1711 | 1 | queue_depth: self.queue_depth.load(std::sync::atomic::Ordering::SeqCst), |
1712 | 1 | } |
1713 | 1 | } |
1714 | | } |
1715 | | |
1716 | | impl Default for ResourceMonitor { |
1717 | 0 | fn default() -> Self { |
1718 | 0 | Self::new() |
1719 | 0 | } |
1720 | | } |
1721 | | |
1722 | | // ============================================================================ |
1723 | | // M33: GGUF HTTP Serving Integration (IMP-082, IMP-083) |
1724 | | // Per spec v2.15.0: Wire GpuModel to HTTP server |
1725 | | // ============================================================================ |
1726 | | |
1727 | | /// State for holding a loaded GGUF model in HTTP server context (IMP-082) |
1728 | | /// |
1729 | | /// This struct wraps a GpuModel and provides thread-safe access for |
1730 | | /// the HTTP server to perform inference requests. |
1731 | | /// |
1732 | | /// # Example |
1733 | | /// |
1734 | | /// ```rust,ignore |
1735 | | /// use realizar::gpu::GgufModelState; |
1736 | | /// |
1737 | | /// let state = GgufModelState::new(); |
1738 | | /// assert!(!state.is_loaded()); |
1739 | | /// |
1740 | | /// // Load model |
1741 | | /// let state = load_gguf_to_gpu(vocab_size, hidden_dim, num_layers)?; |
1742 | | /// assert!(state.is_loaded()); |
1743 | | /// ``` |
1744 | | pub struct GgufModelState { |
1745 | | /// Loaded GPU model (None if not loaded) |
1746 | | model: Option<GpuModel>, |
1747 | | /// Model name/path |
1748 | | model_name: Option<String>, |
1749 | | /// Whether model is ready for inference |
1750 | | ready: bool, |
1751 | | } |
1752 | | |
1753 | | impl std::fmt::Debug for GgufModelState { |
1754 | 1 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
1755 | 1 | f.debug_struct("GgufModelState") |
1756 | 1 | .field("model_name", &self.model_name) |
1757 | 1 | .field("ready", &self.ready) |
1758 | 1 | .field("is_loaded", &self.model.is_some()) |
1759 | 1 | .finish() |
1760 | 1 | } |
1761 | | } |
1762 | | |
1763 | | impl GgufModelState { |
1764 | | /// Create empty state (no model loaded) |
1765 | | #[must_use] |
1766 | 2 | pub fn new() -> Self { |
1767 | 2 | Self { |
1768 | 2 | model: None, |
1769 | 2 | model_name: None, |
1770 | 2 | ready: false, |
1771 | 2 | } |
1772 | 2 | } |
1773 | | |
1774 | | /// Create state with a loaded model |
1775 | | #[must_use] |
1776 | 1 | pub fn with_model(model: GpuModel, name: String) -> Self { |
1777 | 1 | Self { |
1778 | 1 | model: Some(model), |
1779 | 1 | model_name: Some(name), |
1780 | 1 | ready: true, |
1781 | 1 | } |
1782 | 1 | } |
1783 | | |
1784 | | /// Check if a model is loaded |
1785 | | #[must_use] |
1786 | 2 | pub fn is_loaded(&self) -> bool { |
1787 | 2 | self.model.is_some() |
1788 | 2 | } |
1789 | | |
1790 | | /// Check if model is ready for inference |
1791 | | #[must_use] |
1792 | 2 | pub fn is_ready(&self) -> bool { |
1793 | 2 | self.ready && self.model1 .is_some1 () |
1794 | 2 | } |
1795 | | |
1796 | | /// Get model name |
1797 | | #[must_use] |
1798 | 1 | pub fn model_name(&self) -> Option<&str> { |
1799 | 1 | self.model_name.as_deref() |
1800 | 1 | } |
1801 | | |
1802 | | /// Get vocab size (0 if no model loaded) |
1803 | | #[must_use] |
1804 | 2 | pub fn vocab_size(&self) -> usize { |
1805 | 2 | self.model.as_ref().map_or(0, |m| m1 .config1 ().vocab_size) |
1806 | 2 | } |
1807 | | |
1808 | | /// Get reference to the model (for inference) |
1809 | | #[must_use] |
1810 | 0 | pub fn model(&self) -> Option<&GpuModel> { |
1811 | 0 | self.model.as_ref() |
1812 | 0 | } |
1813 | | |
1814 | | /// Get mutable reference to the model |
1815 | 0 | pub fn model_mut(&mut self) -> Option<&mut GpuModel> { |
1816 | 0 | self.model.as_mut() |
1817 | 0 | } |
1818 | | } |
1819 | | |
1820 | | impl Default for GgufModelState { |
1821 | 0 | fn default() -> Self { |
1822 | 0 | Self::new() |
1823 | 0 | } |
1824 | | } |
1825 | | |
1826 | | /// Load GGUF model to GPU (IMP-083) |
1827 | | /// |
1828 | | /// Creates a minimal GPU model from configuration parameters. |
1829 | | /// This is the pipeline entry point for serving GGUF models via HTTP. |
1830 | | /// |
1831 | | /// # Arguments |
1832 | | /// |
1833 | | /// * `vocab_size` - Vocabulary size |
1834 | | /// * `hidden_dim` - Hidden dimension |
1835 | | /// * `num_layers` - Number of transformer layers |
1836 | | /// |
1837 | | /// # Returns |
1838 | | /// |
1839 | | /// * `Ok(GgufModelState)` - State with loaded model ready for inference |
1840 | | /// * `Err(RealizarError)` - If model creation fails |
1841 | | /// |
1842 | | /// # Errors |
1843 | | /// |
1844 | | /// Returns error if GPU initialization fails or model creation fails. |
1845 | | /// |
1846 | | /// # Example |
1847 | | /// |
1848 | | /// ```rust,ignore |
1849 | | /// use realizar::gpu::load_gguf_to_gpu; |
1850 | | /// |
1851 | | /// let state = load_gguf_to_gpu(32000, 4096, 32)?; |
1852 | | /// assert!(state.is_ready()); |
1853 | | /// ``` |
1854 | 1 | pub fn load_gguf_to_gpu( |
1855 | 1 | vocab_size: usize, |
1856 | 1 | hidden_dim: usize, |
1857 | 1 | num_layers: usize, |
1858 | 1 | ) -> Result<GgufModelState> { |
1859 | | // Create GPU model config |
1860 | 1 | let num_heads = hidden_dim / 64; // Standard head dim of 64 |
1861 | 1 | let config = GpuModelConfig { |
1862 | 1 | vocab_size, |
1863 | 1 | hidden_dim, |
1864 | 1 | num_heads, |
1865 | 1 | num_kv_heads: num_heads, // Standard MHA (no GQA) |
1866 | 1 | num_layers, |
1867 | 1 | intermediate_dim: hidden_dim * 4, // Standard FFN expansion |
1868 | 1 | eps: 1e-5, |
1869 | 1 | rope_theta: 10000.0, // Standard RoPE base frequency |
1870 | 1 | }; |
1871 | | |
1872 | | // Create GPU model |
1873 | 1 | let model = GpuModel::new(config)?0 ; |
1874 | | |
1875 | | // Wrap in state |
1876 | 1 | let model_name = format!("test_{}x{}x{}", vocab_size, hidden_dim, num_layers); |
1877 | 1 | Ok(GgufModelState::with_model(model, model_name)) |
1878 | 1 | } |
1879 | | |
1880 | | #[cfg(test)] |
1881 | | |
1882 | | #[cfg(test)] |
1883 | | mod tests; |