/home/noah/src/realizar/src/gguf/inference_types.rs
Line | Count | Source |
1 | | //! Inference support types for quantized model execution |
2 | | //! |
3 | | //! This module contains pre-allocated buffers and caches for zero-allocation inference. |
4 | | //! |
5 | | //! Types included: |
6 | | //! - `InferenceScratchBuffer`: Scratch buffers for forward passes |
7 | | //! - `OwnedInferenceScratchBuffer`: Owned variant with Q8K support |
8 | | //! - `ContiguousKVCache`: Cache-aligned KV cache for efficient attention |
9 | | //! - `DispatchMetrics`: Thread-safe CPU/GPU dispatch metrics |
10 | | |
11 | | use super::config::GGUFConfig; |
12 | | |
13 | | /// Cache line size in bytes (typical x86-64) |
14 | | const CACHE_LINE_BYTES: usize = 64; |
15 | | |
16 | | /// Number of f32 elements per cache line (64 bytes / 4 bytes per f32) |
17 | | const FLOATS_PER_CACHE_LINE: usize = CACHE_LINE_BYTES / std::mem::size_of::<f32>(); |
18 | | |
19 | | // ============================================================================ |
20 | | // InferenceScratchBuffer |
21 | | // ============================================================================ |
22 | | |
23 | | /// Pre-allocated scratch buffers for zero-allocation forward passes |
24 | | /// |
25 | | /// ## Buffer Reuse Pattern |
26 | | /// |
27 | | /// - First use: hidden → normed → qkv → q/k/v → attn_out |
28 | | /// - FFN pass: normed → ffn_up/ffn_gate → ffn_down → hidden |
29 | | /// |
30 | | /// PAR-126: Added Q8K scratch buffers for VNNI-accelerated Q4K×Q8K matmul path. |
31 | | #[derive(Debug)] |
32 | | pub struct InferenceScratchBuffer { |
33 | | /// Hidden state buffer [hidden_dim] |
34 | | pub hidden: Vec<f32>, |
35 | | /// Normalized hidden state [hidden_dim] |
36 | | pub normed: Vec<f32>, |
37 | | /// Combined QKV projection [q_dim + k_dim + v_dim] |
38 | | pub qkv: Vec<f32>, |
39 | | /// Query projection [q_dim] |
40 | | pub q: Vec<f32>, |
41 | | /// Key projection [k_dim] |
42 | | pub k: Vec<f32>, |
43 | | /// Value projection [v_dim] |
44 | | pub v: Vec<f32>, |
45 | | /// Attention output [hidden_dim] |
46 | | pub attn_out: Vec<f32>, |
47 | | /// Attention projection output [hidden_dim] |
48 | | pub attn_proj: Vec<f32>, |
49 | | /// FFN up projection [intermediate_dim] |
50 | | pub ffn_up: Vec<f32>, |
51 | | /// FFN gate projection [intermediate_dim] (for SwiGLU) |
52 | | pub ffn_gate: Vec<f32>, |
53 | | /// FFN down projection [hidden_dim] |
54 | | pub ffn_down: Vec<f32>, |
55 | | /// Output logits [vocab_size] |
56 | | pub logits: Vec<f32>, |
57 | | // PAR-126: Q8K scratch buffers for VNNI-accelerated matmul |
58 | | /// Q8K scales for hidden-dim activations [hidden_dim/256] |
59 | | pub q8k_hidden_scales: Vec<f32>, |
60 | | /// Q8K quants for hidden-dim activations [hidden_dim] |
61 | | pub q8k_hidden_quants: Vec<i8>, |
62 | | /// Q8K scales for intermediate-dim activations [intermediate_dim/256] |
63 | | pub q8k_inter_scales: Vec<f32>, |
64 | | /// Q8K quants for intermediate-dim activations [intermediate_dim] |
65 | | pub q8k_inter_quants: Vec<i8>, |
66 | | } |
67 | | |
68 | | impl InferenceScratchBuffer { |
69 | | /// Create scratch buffer from model config |
70 | | /// |
71 | | /// Pre-allocates all buffers to their maximum required size. |
72 | | /// Total memory: ~2.5MB for TinyLlama-1.1B, ~10MB for 7B models. |
73 | | #[must_use] |
74 | 0 | pub fn from_config(config: &GGUFConfig) -> Self { |
75 | 0 | let hidden_dim = config.hidden_dim; |
76 | 0 | let intermediate_dim = config.intermediate_dim; |
77 | 0 | let vocab_size = config.vocab_size; |
78 | 0 | let qkv_dim = hidden_dim * 3; // Max for fused QKV |
79 | | |
80 | | // PAR-126: Q8K uses 256-element super-blocks for VNNI path |
81 | | const QK_K: usize = 256; |
82 | 0 | let q8k_hidden_padded = hidden_dim.div_ceil(QK_K) * QK_K; |
83 | 0 | let q8k_inter_padded = intermediate_dim.div_ceil(QK_K) * QK_K; |
84 | | |
85 | 0 | Self { |
86 | 0 | hidden: vec![0.0; hidden_dim], |
87 | 0 | normed: vec![0.0; hidden_dim], |
88 | 0 | qkv: vec![0.0; qkv_dim], |
89 | 0 | q: vec![0.0; hidden_dim], |
90 | 0 | k: vec![0.0; hidden_dim], |
91 | 0 | v: vec![0.0; hidden_dim], |
92 | 0 | attn_out: vec![0.0; hidden_dim], |
93 | 0 | attn_proj: vec![0.0; hidden_dim], |
94 | 0 | ffn_up: vec![0.0; intermediate_dim], |
95 | 0 | ffn_gate: vec![0.0; intermediate_dim], |
96 | 0 | ffn_down: vec![0.0; hidden_dim], |
97 | 0 | logits: vec![0.0; vocab_size], |
98 | 0 | q8k_hidden_scales: vec![0.0f32; q8k_hidden_padded / QK_K], |
99 | 0 | q8k_hidden_quants: vec![0i8; q8k_hidden_padded], |
100 | 0 | q8k_inter_scales: vec![0.0f32; q8k_inter_padded / QK_K], |
101 | 0 | q8k_inter_quants: vec![0i8; q8k_inter_padded], |
102 | 0 | } |
103 | 0 | } |
104 | | |
105 | | /// Reset all buffers to zero for a new forward pass |
106 | | #[inline] |
107 | 0 | pub fn reset(&mut self) { |
108 | 0 | self.hidden.iter_mut().for_each(|x| *x = 0.0); |
109 | 0 | self.normed.iter_mut().for_each(|x| *x = 0.0); |
110 | 0 | } |
111 | | } |
112 | | |
113 | | // ============================================================================ |
114 | | // OwnedInferenceScratchBuffer |
115 | | // ============================================================================ |
116 | | |
117 | | /// Pre-allocated scratch buffers for OwnedQuantizedModel forward passes |
118 | | /// |
119 | | /// Eliminates per-token allocations by reusing buffers across forward passes. |
120 | | /// For Qwen2.5-0.5B with intermediate_dim=4864, this saves ~40KB per token. |
121 | | /// |
122 | | /// PAR-126: Added Q8K scratch buffers for fused Q4K×Q8K matmul path. |
123 | | #[derive(Debug)] |
124 | | pub struct OwnedInferenceScratchBuffer { |
125 | | /// QKV output buffer [hidden_dim + 2*kv_dim] |
126 | | pub qkv: Vec<f32>, |
127 | | /// Attention output buffer [hidden_dim] |
128 | | pub attn_out: Vec<f32>, |
129 | | /// FFN up projection buffer [intermediate_dim] |
130 | | pub ffn_up: Vec<f32>, |
131 | | /// FFN gate projection buffer [intermediate_dim] |
132 | | pub ffn_gate: Vec<f32>, |
133 | | /// FFN down output buffer [hidden_dim] |
134 | | pub ffn_down: Vec<f32>, |
135 | | /// Expanded V buffer for first token GQA [hidden_dim] |
136 | | pub expanded_v: Vec<f32>, |
137 | | /// Logits buffer [vocab_size] |
138 | | pub logits: Vec<f32>, |
139 | | /// Q8 quantization scales scratch [num_blocks] |
140 | | pub q8_scales: Vec<f32>, |
141 | | /// Q8 quantization values scratch [num_blocks * 32] |
142 | | pub q8_quants: Vec<i8>, |
143 | | // PAR-126: Q8K scratch buffers for VNNI-accelerated matmul |
144 | | /// Q8K scales for hidden-dim activations [hidden_dim/256] |
145 | | pub q8k_hidden_scales: Vec<f32>, |
146 | | /// Q8K quants for hidden-dim activations [hidden_dim] |
147 | | pub q8k_hidden_quants: Vec<i8>, |
148 | | /// Q8K scales for intermediate-dim activations [intermediate_dim/256] |
149 | | pub q8k_inter_scales: Vec<f32>, |
150 | | /// Q8K quants for intermediate-dim activations [intermediate_dim] |
151 | | pub q8k_inter_quants: Vec<i8>, |
152 | | } |
153 | | |
154 | | impl OwnedInferenceScratchBuffer { |
155 | | /// Create scratch buffer from model config |
156 | | #[must_use] |
157 | 0 | pub fn from_config(config: &GGUFConfig) -> Self { |
158 | 0 | let hidden_dim = config.hidden_dim; |
159 | 0 | let num_kv_heads = config.num_kv_heads; |
160 | 0 | let head_dim = hidden_dim / config.num_heads; |
161 | 0 | let kv_dim = num_kv_heads * head_dim; |
162 | 0 | let qkv_dim = hidden_dim + 2 * kv_dim; |
163 | 0 | let intermediate_dim = hidden_dim * 6; // Conservative estimate |
164 | 0 | let num_blocks = hidden_dim.div_ceil(32); |
165 | | |
166 | | const QK_K: usize = 256; |
167 | 0 | let q8k_hidden_padded = hidden_dim.div_ceil(QK_K) * QK_K; |
168 | 0 | let q8k_inter_padded = intermediate_dim.div_ceil(QK_K) * QK_K; |
169 | | |
170 | 0 | Self { |
171 | 0 | qkv: vec![0.0f32; qkv_dim], |
172 | 0 | attn_out: vec![0.0f32; hidden_dim], |
173 | 0 | ffn_up: vec![0.0f32; intermediate_dim], |
174 | 0 | ffn_gate: vec![0.0f32; intermediate_dim], |
175 | 0 | ffn_down: vec![0.0f32; hidden_dim], |
176 | 0 | expanded_v: vec![0.0f32; hidden_dim], |
177 | 0 | logits: vec![0.0f32; config.vocab_size], |
178 | 0 | q8_scales: vec![0.0f32; num_blocks], |
179 | 0 | q8_quants: vec![0i8; num_blocks * 32], |
180 | 0 | q8k_hidden_scales: vec![0.0f32; q8k_hidden_padded / QK_K], |
181 | 0 | q8k_hidden_quants: vec![0i8; q8k_hidden_padded], |
182 | 0 | q8k_inter_scales: vec![0.0f32; q8k_inter_padded / QK_K], |
183 | 0 | q8k_inter_quants: vec![0i8; q8k_inter_padded], |
184 | 0 | } |
185 | 0 | } |
186 | | |
187 | | /// Reset all buffers (clear without deallocating) |
188 | 0 | pub fn reset(&mut self) { |
189 | 0 | self.qkv.clear(); |
190 | 0 | self.attn_out.clear(); |
191 | 0 | self.ffn_up.clear(); |
192 | 0 | self.ffn_gate.clear(); |
193 | 0 | self.ffn_down.clear(); |
194 | 0 | self.expanded_v.clear(); |
195 | 0 | self.logits.clear(); |
196 | 0 | self.q8_scales.clear(); |
197 | 0 | self.q8_quants.clear(); |
198 | 0 | self.q8k_hidden_scales.clear(); |
199 | 0 | self.q8k_hidden_quants.clear(); |
200 | 0 | self.q8k_inter_scales.clear(); |
201 | 0 | self.q8k_inter_quants.clear(); |
202 | 0 | } |
203 | | } |
204 | | |
205 | | // ============================================================================ |
206 | | // ContiguousKVCache (PARITY-005) |
207 | | // ============================================================================ |
208 | | |
209 | | /// Contiguous KV cache with 64-byte cache line alignment (PARITY-005) |
210 | | /// |
211 | | /// This cache uses a single contiguous allocation for all K and V data, |
212 | | /// aligned to 64-byte cache lines for optimal L2 cache performance. |
213 | | /// |
214 | | /// ## Memory Layout |
215 | | /// |
216 | | /// ```text |
217 | | /// K cache: [layer_0][layer_1]...[layer_n] (all contiguous) |
218 | | /// V cache: [layer_0][layer_1]...[layer_n] (all contiguous) |
219 | | /// |
220 | | /// Each layer: [pos_0][pos_1]...[pos_max_seq] where each pos is [hidden_dim] |
221 | | /// ``` |
222 | | #[derive(Debug)] |
223 | | pub struct ContiguousKVCache { |
224 | | num_layers: usize, |
225 | | hidden_dim: usize, |
226 | | max_seq_len: usize, |
227 | | seq_len: usize, |
228 | | layer_stride: usize, |
229 | | k_data: Vec<f32>, |
230 | | v_data: Vec<f32>, |
231 | | } |
232 | | |
233 | | impl ContiguousKVCache { |
234 | | /// Create a new contiguous KV cache |
235 | | #[must_use] |
236 | 109 | pub fn new(num_layers: usize, hidden_dim: usize, max_seq_len: usize) -> Self { |
237 | 109 | let raw_layer_size = max_seq_len * hidden_dim; |
238 | 109 | let layer_stride = Self::align_to_cache_line(raw_layer_size); |
239 | 109 | let total_size = num_layers * layer_stride; |
240 | | |
241 | 109 | Self { |
242 | 109 | num_layers, |
243 | 109 | hidden_dim, |
244 | 109 | max_seq_len, |
245 | 109 | seq_len: 0, |
246 | 109 | layer_stride, |
247 | 109 | k_data: vec![0.0f32; total_size], |
248 | 109 | v_data: vec![0.0f32; total_size], |
249 | 109 | } |
250 | 109 | } |
251 | | |
252 | | #[inline] |
253 | 109 | fn align_to_cache_line(size: usize) -> usize { |
254 | 109 | let remainder = size % FLOATS_PER_CACHE_LINE; |
255 | 109 | if remainder == 0 { size } else { size + FLOATS_PER_CACHE_LINE - remainder0 } |
256 | 109 | } |
257 | | |
258 | | /// Create cache from model configuration |
259 | | #[must_use] |
260 | 0 | pub fn from_config(config: &GGUFConfig, max_seq_len: usize) -> Self { |
261 | 0 | Self::new(config.num_layers, config.hidden_dim, max_seq_len) |
262 | 0 | } |
263 | | |
264 | | /// Check if this cache has contiguous layout |
265 | | #[must_use] |
266 | 1 | pub const fn is_contiguous(&self) -> bool { true } |
267 | | |
268 | | /// Check if data is cache-line aligned |
269 | | #[must_use] |
270 | 5 | pub fn is_cache_aligned(&self) -> bool { |
271 | 5 | self.layer_stride % FLOATS_PER_CACHE_LINE == 0 |
272 | 5 | } |
273 | | |
274 | | /// Get the layer stride |
275 | | #[must_use] |
276 | 5 | pub fn layer_stride(&self) -> usize { self.layer_stride } |
277 | | |
278 | | #[inline] |
279 | 25.6k | fn layer_offset(&self, layer: usize) -> usize { layer * self.layer_stride } |
280 | | |
281 | | /// Append K and V vectors for a single position to a layer's cache |
282 | 25.6k | pub fn append(&mut self, layer: usize, k: &[f32], v: &[f32]) { |
283 | 25.6k | if layer >= self.num_layers || self.seq_len >= self.max_seq_len { return0 ; } |
284 | 25.6k | let start = self.layer_offset(layer) + self.seq_len * self.hidden_dim; |
285 | 25.6k | let end = start + self.hidden_dim; |
286 | 25.6k | if end <= self.k_data.len() { |
287 | 25.6k | self.k_data[start..end].copy_from_slice(k); |
288 | 25.6k | self.v_data[start..end].copy_from_slice(v); |
289 | 25.6k | }0 |
290 | 25.6k | } |
291 | | |
292 | | /// Advance the sequence position |
293 | 6.40k | pub fn advance(&mut self) { |
294 | 6.40k | if self.seq_len < self.max_seq_len { self.seq_len += 1; }0 |
295 | 6.40k | } |
296 | | |
297 | | /// Get cached keys for a layer |
298 | | #[must_use] |
299 | 3 | pub fn get_k(&self, layer: usize) -> &[f32] { |
300 | 3 | if layer >= self.num_layers { return &[]0 ; } |
301 | 3 | let start = self.layer_offset(layer); |
302 | 3 | &self.k_data[start..start + self.seq_len * self.hidden_dim] |
303 | 3 | } |
304 | | |
305 | | /// Get cached values for a layer |
306 | | #[must_use] |
307 | 0 | pub fn get_v(&self, layer: usize) -> &[f32] { |
308 | 0 | if layer >= self.num_layers { return &[]; } |
309 | 0 | let start = self.layer_offset(layer); |
310 | 0 | &self.v_data[start..start + self.seq_len * self.hidden_dim] |
311 | 0 | } |
312 | | |
313 | | /// Get mutable cached keys for a layer |
314 | 0 | pub fn get_k_mut(&mut self, layer: usize) -> &mut [f32] { |
315 | 0 | if layer >= self.num_layers { return &mut []; } |
316 | 0 | let start = self.layer_offset(layer); |
317 | 0 | let len = self.seq_len * self.hidden_dim; |
318 | 0 | &mut self.k_data[start..start + len] |
319 | 0 | } |
320 | | |
321 | | /// Get mutable cached values for a layer |
322 | 0 | pub fn get_v_mut(&mut self, layer: usize) -> &mut [f32] { |
323 | 0 | if layer >= self.num_layers { return &mut []; } |
324 | 0 | let start = self.layer_offset(layer); |
325 | 0 | let len = self.seq_len * self.hidden_dim; |
326 | 0 | &mut self.v_data[start..start + len] |
327 | 0 | } |
328 | | |
329 | | /// Current sequence length |
330 | | #[must_use] |
331 | 4 | pub fn len(&self) -> usize { self.seq_len } |
332 | | |
333 | | /// Check if cache is empty |
334 | | #[must_use] |
335 | 0 | pub fn is_empty(&self) -> bool { self.seq_len == 0 } |
336 | | |
337 | | /// Reset cache for new generation |
338 | 1 | pub fn reset(&mut self) { self.seq_len = 0; } |
339 | | |
340 | | /// Reset cache and zero all data |
341 | 0 | pub fn reset_and_zero(&mut self) { |
342 | 0 | self.seq_len = 0; |
343 | 0 | self.k_data.fill(0.0); |
344 | 0 | self.v_data.fill(0.0); |
345 | 0 | } |
346 | | |
347 | | /// Get maximum sequence length |
348 | | #[must_use] |
349 | 0 | pub fn max_len(&self) -> usize { self.max_seq_len } |
350 | | |
351 | | /// Get total memory usage in bytes |
352 | | #[must_use] |
353 | 1 | pub fn memory_bytes(&self) -> usize { |
354 | 1 | (self.k_data.len() + self.v_data.len()) * std::mem::size_of::<f32>() |
355 | 1 | } |
356 | | |
357 | | /// Prefetch K cache for a layer |
358 | | #[inline] |
359 | 0 | pub fn prefetch_k(&self, layer: usize) { |
360 | 0 | if layer < self.num_layers { |
361 | 0 | let _ = self.k_data.get(self.layer_offset(layer)); |
362 | 0 | } |
363 | 0 | } |
364 | | |
365 | | /// Prefetch V cache for a layer |
366 | | #[inline] |
367 | 0 | pub fn prefetch_v(&self, layer: usize) { |
368 | 0 | if layer < self.num_layers { |
369 | 0 | let _ = self.v_data.get(self.layer_offset(layer)); |
370 | 0 | } |
371 | 0 | } |
372 | | } |
373 | | |
374 | | // ============================================================================ |
375 | | // DispatchMetrics (IMP-123) |
376 | | // ============================================================================ |
377 | | |
378 | | /// Thread-safe metrics for tracking CPU vs GPU dispatch decisions |
379 | | /// |
380 | | /// Tracks dispatch counts and latency histograms for performance analysis. |
381 | | #[derive(Debug)] |
382 | | pub struct DispatchMetrics { |
383 | | cpu_dispatches: std::sync::atomic::AtomicUsize, |
384 | | gpu_dispatches: std::sync::atomic::AtomicUsize, |
385 | | cpu_latency_count: std::sync::atomic::AtomicUsize, |
386 | | cpu_latency_sum_us: std::sync::atomic::AtomicU64, |
387 | | gpu_latency_count: std::sync::atomic::AtomicUsize, |
388 | | gpu_latency_sum_us: std::sync::atomic::AtomicU64, |
389 | | cpu_latency_buckets: [std::sync::atomic::AtomicUsize; 5], |
390 | | gpu_latency_buckets: [std::sync::atomic::AtomicUsize; 5], |
391 | | cpu_latency_min_us: std::sync::atomic::AtomicU64, |
392 | | cpu_latency_max_us: std::sync::atomic::AtomicU64, |
393 | | gpu_latency_min_us: std::sync::atomic::AtomicU64, |
394 | | gpu_latency_max_us: std::sync::atomic::AtomicU64, |
395 | | cpu_latency_sum_sq_us: std::sync::atomic::AtomicU64, |
396 | | gpu_latency_sum_sq_us: std::sync::atomic::AtomicU64, |
397 | | start_time_ms: std::sync::atomic::AtomicU64, |
398 | | } |
399 | | |
400 | | impl DispatchMetrics { |
401 | | /// Histogram bucket boundaries in microseconds |
402 | | pub const BUCKET_BOUNDARIES: [u64; 4] = [100, 500, 1000, 5000]; |
403 | | |
404 | | /// Create new metrics tracker |
405 | | #[must_use] |
406 | 78 | pub fn new() -> Self { |
407 | | Self { |
408 | 78 | cpu_dispatches: std::sync::atomic::AtomicUsize::new(0), |
409 | 78 | gpu_dispatches: std::sync::atomic::AtomicUsize::new(0), |
410 | 78 | cpu_latency_count: std::sync::atomic::AtomicUsize::new(0), |
411 | 78 | cpu_latency_sum_us: std::sync::atomic::AtomicU64::new(0), |
412 | 78 | gpu_latency_count: std::sync::atomic::AtomicUsize::new(0), |
413 | 78 | gpu_latency_sum_us: std::sync::atomic::AtomicU64::new(0), |
414 | 78 | cpu_latency_buckets: [ |
415 | 78 | std::sync::atomic::AtomicUsize::new(0), |
416 | 78 | std::sync::atomic::AtomicUsize::new(0), |
417 | 78 | std::sync::atomic::AtomicUsize::new(0), |
418 | 78 | std::sync::atomic::AtomicUsize::new(0), |
419 | 78 | std::sync::atomic::AtomicUsize::new(0), |
420 | 78 | ], |
421 | 78 | gpu_latency_buckets: [ |
422 | 78 | std::sync::atomic::AtomicUsize::new(0), |
423 | 78 | std::sync::atomic::AtomicUsize::new(0), |
424 | 78 | std::sync::atomic::AtomicUsize::new(0), |
425 | 78 | std::sync::atomic::AtomicUsize::new(0), |
426 | 78 | std::sync::atomic::AtomicUsize::new(0), |
427 | 78 | ], |
428 | 78 | cpu_latency_min_us: std::sync::atomic::AtomicU64::new(u64::MAX), |
429 | 78 | cpu_latency_max_us: std::sync::atomic::AtomicU64::new(0), |
430 | 78 | gpu_latency_min_us: std::sync::atomic::AtomicU64::new(u64::MAX), |
431 | 78 | gpu_latency_max_us: std::sync::atomic::AtomicU64::new(0), |
432 | 78 | cpu_latency_sum_sq_us: std::sync::atomic::AtomicU64::new(0), |
433 | 78 | gpu_latency_sum_sq_us: std::sync::atomic::AtomicU64::new(0), |
434 | 78 | start_time_ms: std::sync::atomic::AtomicU64::new( |
435 | 78 | std::time::SystemTime::now() |
436 | 78 | .duration_since(std::time::UNIX_EPOCH) |
437 | 78 | .map(|d| d.as_millis() as u64) |
438 | 78 | .unwrap_or(0), |
439 | | ), |
440 | | } |
441 | 78 | } |
442 | | |
443 | 938 | fn bucket_index(latency_us: u64) -> usize { |
444 | 1.61k | for (i, &boundary) in Self::BUCKET_BOUNDARIES938 .iter938 ().enumerate938 () { |
445 | 1.61k | if latency_us < boundary { return i874 ; }743 |
446 | | } |
447 | 64 | 4 |
448 | 938 | } |
449 | | |
450 | | /// Record a CPU dispatch |
451 | 629 | pub fn record_cpu_dispatch(&self) { |
452 | 629 | self.cpu_dispatches.fetch_add(1, std::sync::atomic::Ordering::Relaxed); |
453 | 629 | } |
454 | | |
455 | | /// Record a GPU dispatch |
456 | 272 | pub fn record_gpu_dispatch(&self) { |
457 | 272 | self.gpu_dispatches.fetch_add(1, std::sync::atomic::Ordering::Relaxed); |
458 | 272 | } |
459 | | |
460 | | /// Record CPU dispatch latency |
461 | 653 | pub fn record_cpu_latency(&self, latency: std::time::Duration) { |
462 | 653 | let latency_us = latency.as_micros() as u64; |
463 | 653 | self.cpu_latency_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); |
464 | 653 | self.cpu_latency_sum_us.fetch_add(latency_us, std::sync::atomic::Ordering::Relaxed); |
465 | 653 | self.cpu_latency_buckets[Self::bucket_index(latency_us)].fetch_add(1, std::sync::atomic::Ordering::Relaxed); |
466 | 653 | self.cpu_latency_min_us.fetch_min(latency_us, std::sync::atomic::Ordering::Relaxed); |
467 | 653 | self.cpu_latency_max_us.fetch_max(latency_us, std::sync::atomic::Ordering::Relaxed); |
468 | 653 | self.cpu_latency_sum_sq_us.fetch_add(latency_us * latency_us, std::sync::atomic::Ordering::Relaxed); |
469 | 653 | } |
470 | | |
471 | | /// Record GPU dispatch latency |
472 | 285 | pub fn record_gpu_latency(&self, latency: std::time::Duration) { |
473 | 285 | let latency_us = latency.as_micros() as u64; |
474 | 285 | self.gpu_latency_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); |
475 | 285 | self.gpu_latency_sum_us.fetch_add(latency_us, std::sync::atomic::Ordering::Relaxed); |
476 | 285 | self.gpu_latency_buckets[Self::bucket_index(latency_us)].fetch_add(1, std::sync::atomic::Ordering::Relaxed); |
477 | 285 | self.gpu_latency_min_us.fetch_min(latency_us, std::sync::atomic::Ordering::Relaxed); |
478 | 285 | self.gpu_latency_max_us.fetch_max(latency_us, std::sync::atomic::Ordering::Relaxed); |
479 | 285 | self.gpu_latency_sum_sq_us.fetch_add(latency_us * latency_us, std::sync::atomic::Ordering::Relaxed); |
480 | 285 | } |
481 | | |
482 | | /// Get CPU dispatch count |
483 | | #[must_use] |
484 | 78 | pub fn cpu_dispatches(&self) -> usize { |
485 | 78 | self.cpu_dispatches.load(std::sync::atomic::Ordering::Relaxed) |
486 | 78 | } |
487 | | |
488 | | /// Get GPU dispatch count |
489 | | #[must_use] |
490 | 84 | pub fn gpu_dispatches(&self) -> usize { |
491 | 84 | self.gpu_dispatches.load(std::sync::atomic::Ordering::Relaxed) |
492 | 84 | } |
493 | | |
494 | | /// Get total dispatches |
495 | | #[must_use] |
496 | 46 | pub fn total_dispatches(&self) -> usize { |
497 | 46 | self.cpu_dispatches() + self.gpu_dispatches() |
498 | 46 | } |
499 | | |
500 | | /// Get GPU dispatch ratio |
501 | | #[must_use] |
502 | 21 | pub fn gpu_ratio(&self) -> f64 { |
503 | 21 | let total = self.total_dispatches(); |
504 | 21 | if total == 0 { 0.015 } else { self.gpu_dispatches() as f64 / total as f646 } |
505 | 21 | } |
506 | | |
507 | | /// Get CPU latency count |
508 | | #[must_use] |
509 | 70 | pub fn cpu_latency_count(&self) -> usize { |
510 | 70 | self.cpu_latency_count.load(std::sync::atomic::Ordering::Relaxed) |
511 | 70 | } |
512 | | |
513 | | /// Get GPU latency count |
514 | | #[must_use] |
515 | 58 | pub fn gpu_latency_count(&self) -> usize { |
516 | 58 | self.gpu_latency_count.load(std::sync::atomic::Ordering::Relaxed) |
517 | 58 | } |
518 | | |
519 | | /// Get mean CPU latency in microseconds |
520 | | #[must_use] |
521 | 16 | pub fn cpu_latency_mean_us(&self) -> f64 { |
522 | 16 | let count = self.cpu_latency_count(); |
523 | 16 | if count == 0 { 0.08 } else { |
524 | 8 | self.cpu_latency_sum_us.load(std::sync::atomic::Ordering::Relaxed) as f64 / count as f64 |
525 | | } |
526 | 16 | } |
527 | | |
528 | | /// Get mean GPU latency in microseconds |
529 | | #[must_use] |
530 | 14 | pub fn gpu_latency_mean_us(&self) -> f64 { |
531 | 14 | let count = self.gpu_latency_count(); |
532 | 14 | if count == 0 { 0.09 } else { |
533 | 5 | self.gpu_latency_sum_us.load(std::sync::atomic::Ordering::Relaxed) as f64 / count as f64 |
534 | | } |
535 | 14 | } |
536 | | |
537 | | /// Get CPU latency sum |
538 | | #[must_use] |
539 | 10 | pub fn cpu_latency_sum_us(&self) -> u64 { |
540 | 10 | self.cpu_latency_sum_us.load(std::sync::atomic::Ordering::Relaxed) |
541 | 10 | } |
542 | | |
543 | | /// Get GPU latency sum |
544 | | #[must_use] |
545 | 10 | pub fn gpu_latency_sum_us(&self) -> u64 { |
546 | 10 | self.gpu_latency_sum_us.load(std::sync::atomic::Ordering::Relaxed) |
547 | 10 | } |
548 | | |
549 | | /// Get CPU latency min |
550 | | #[must_use] |
551 | 12 | pub fn cpu_latency_min_us(&self) -> u64 { |
552 | 12 | if self.cpu_latency_count() == 0 { 07 } else { |
553 | 5 | self.cpu_latency_min_us.load(std::sync::atomic::Ordering::Relaxed) |
554 | | } |
555 | 12 | } |
556 | | |
557 | | /// Get CPU latency max |
558 | | #[must_use] |
559 | 12 | pub fn cpu_latency_max_us(&self) -> u64 { |
560 | 12 | self.cpu_latency_max_us.load(std::sync::atomic::Ordering::Relaxed) |
561 | 12 | } |
562 | | |
563 | | /// Get GPU latency min |
564 | | #[must_use] |
565 | 10 | pub fn gpu_latency_min_us(&self) -> u64 { |
566 | 10 | if self.gpu_latency_count() == 0 { 08 } else { |
567 | 2 | self.gpu_latency_min_us.load(std::sync::atomic::Ordering::Relaxed) |
568 | | } |
569 | 10 | } |
570 | | |
571 | | /// Get GPU latency max |
572 | | #[must_use] |
573 | 10 | pub fn gpu_latency_max_us(&self) -> u64 { |
574 | 10 | self.gpu_latency_max_us.load(std::sync::atomic::Ordering::Relaxed) |
575 | 10 | } |
576 | | |
577 | | /// Get CPU latency variance |
578 | | #[must_use] |
579 | 25 | pub fn cpu_latency_variance_us(&self) -> f64 { |
580 | 25 | let count = self.cpu_latency_count(); |
581 | 25 | if count < 2 { return 0.018 ; }7 |
582 | 7 | let sum = self.cpu_latency_sum_us.load(std::sync::atomic::Ordering::Relaxed) as f64; |
583 | 7 | let sum_sq = self.cpu_latency_sum_sq_us.load(std::sync::atomic::Ordering::Relaxed) as f64; |
584 | 7 | let n = count as f64; |
585 | 7 | (sum_sq / n) - (sum / n).powi(2) |
586 | 25 | } |
587 | | |
588 | | /// Get CPU latency stddev |
589 | | #[must_use] |
590 | 13 | pub fn cpu_latency_stddev_us(&self) -> f64 { self.cpu_latency_variance_us().sqrt() } |
591 | | |
592 | | /// Get GPU latency variance |
593 | | #[must_use] |
594 | 19 | pub fn gpu_latency_variance_us(&self) -> f64 { |
595 | 19 | let count = self.gpu_latency_count(); |
596 | 19 | if count < 2 { return 0.016 ; }3 |
597 | 3 | let sum = self.gpu_latency_sum_us.load(std::sync::atomic::Ordering::Relaxed) as f64; |
598 | 3 | let sum_sq = self.gpu_latency_sum_sq_us.load(std::sync::atomic::Ordering::Relaxed) as f64; |
599 | 3 | let n = count as f64; |
600 | 3 | (sum_sq / n) - (sum / n).powi(2) |
601 | 19 | } |
602 | | |
603 | | /// Get GPU latency stddev |
604 | | #[must_use] |
605 | 10 | pub fn gpu_latency_stddev_us(&self) -> f64 { self.gpu_latency_variance_us().sqrt() } |
606 | | |
607 | | /// Get CPU latency histogram buckets |
608 | | #[must_use] |
609 | 50 | pub fn cpu_latency_buckets(&self) -> [usize; 5] { |
610 | 50 | [ |
611 | 50 | self.cpu_latency_buckets[0].load(std::sync::atomic::Ordering::Relaxed), |
612 | 50 | self.cpu_latency_buckets[1].load(std::sync::atomic::Ordering::Relaxed), |
613 | 50 | self.cpu_latency_buckets[2].load(std::sync::atomic::Ordering::Relaxed), |
614 | 50 | self.cpu_latency_buckets[3].load(std::sync::atomic::Ordering::Relaxed), |
615 | 50 | self.cpu_latency_buckets[4].load(std::sync::atomic::Ordering::Relaxed), |
616 | 50 | ] |
617 | 50 | } |
618 | | |
619 | | /// Get GPU latency histogram buckets |
620 | | #[must_use] |
621 | 43 | pub fn gpu_latency_buckets(&self) -> [usize; 5] { |
622 | 43 | [ |
623 | 43 | self.gpu_latency_buckets[0].load(std::sync::atomic::Ordering::Relaxed), |
624 | 43 | self.gpu_latency_buckets[1].load(std::sync::atomic::Ordering::Relaxed), |
625 | 43 | self.gpu_latency_buckets[2].load(std::sync::atomic::Ordering::Relaxed), |
626 | 43 | self.gpu_latency_buckets[3].load(std::sync::atomic::Ordering::Relaxed), |
627 | 43 | self.gpu_latency_buckets[4].load(std::sync::atomic::Ordering::Relaxed), |
628 | 43 | ] |
629 | 43 | } |
630 | | |
631 | 54 | fn estimate_percentile_from_buckets(buckets: &[usize; 5], percentile: f64) -> f64 { |
632 | | const BUCKET_UPPER_BOUNDS: [f64; 5] = [100.0, 500.0, 1000.0, 5000.0, 10000.0]; |
633 | | const BUCKET_LOWER_BOUNDS: [f64; 5] = [0.0, 100.0, 500.0, 1000.0, 5000.0]; |
634 | 54 | let total: usize = buckets.iter().sum(); |
635 | 54 | if total == 0 { return 0.037 ; }17 |
636 | 17 | let target_rank = (percentile / 100.0) * total as f64; |
637 | 17 | let mut cumulative: f64 = 0.0; |
638 | 35 | for (i, &count) in buckets17 .iter17 ().enumerate17 () { |
639 | 35 | let prev_cumulative = cumulative; |
640 | 35 | cumulative += count as f64; |
641 | 35 | if cumulative >= target_rank { |
642 | 17 | if count == 0 { return BUCKET_LOWER_BOUNDS[i]0 ; } |
643 | 17 | let fraction = (target_rank - prev_cumulative) / count as f64; |
644 | 17 | return BUCKET_LOWER_BOUNDS[i] + fraction * (BUCKET_UPPER_BOUNDS[i] - BUCKET_LOWER_BOUNDS[i]); |
645 | 18 | } |
646 | | } |
647 | 0 | BUCKET_UPPER_BOUNDS[4] |
648 | 54 | } |
649 | | |
650 | | /// Get CPU p50 latency |
651 | | #[must_use] |
652 | 10 | pub fn cpu_latency_p50_us(&self) -> f64 { Self::estimate_percentile_from_buckets(&self.cpu_latency_buckets(), 50.0) } |
653 | | |
654 | | /// Get CPU p95 latency |
655 | | #[must_use] |
656 | 10 | pub fn cpu_latency_p95_us(&self) -> f64 { Self::estimate_percentile_from_buckets(&self.cpu_latency_buckets(), 95.0) } |
657 | | |
658 | | /// Get CPU p99 latency |
659 | | #[must_use] |
660 | 9 | pub fn cpu_latency_p99_us(&self) -> f64 { Self::estimate_percentile_from_buckets(&self.cpu_latency_buckets(), 99.0) } |
661 | | |
662 | | /// Get GPU p50 latency |
663 | | #[must_use] |
664 | 9 | pub fn gpu_latency_p50_us(&self) -> f64 { Self::estimate_percentile_from_buckets(&self.gpu_latency_buckets(), 50.0) } |
665 | | |
666 | | /// Get GPU p95 latency |
667 | | #[must_use] |
668 | 8 | pub fn gpu_latency_p95_us(&self) -> f64 { Self::estimate_percentile_from_buckets(&self.gpu_latency_buckets(), 95.0) } |
669 | | |
670 | | /// Get GPU p99 latency |
671 | | #[must_use] |
672 | 8 | pub fn gpu_latency_p99_us(&self) -> f64 { Self::estimate_percentile_from_buckets(&self.gpu_latency_buckets(), 99.0) } |
673 | | |
674 | | /// Get bucket boundaries as strings |
675 | | #[must_use] |
676 | 9 | pub fn bucket_boundaries_us(&self) -> Vec<String> { |
677 | 9 | vec![ |
678 | 9 | format!("0-{}", Self::BUCKET_BOUNDARIES[0]), |
679 | 9 | format!("{}-{}", Self::BUCKET_BOUNDARIES[0], Self::BUCKET_BOUNDARIES[1]), |
680 | 9 | format!("{}-{}", Self::BUCKET_BOUNDARIES[1], Self::BUCKET_BOUNDARIES[2]), |
681 | 9 | format!("{}-{}", Self::BUCKET_BOUNDARIES[2], Self::BUCKET_BOUNDARIES[3]), |
682 | 9 | format!("{}+", Self::BUCKET_BOUNDARIES[3]), |
683 | | ] |
684 | 9 | } |
685 | | |
686 | | /// Get start time |
687 | | #[must_use] |
688 | 37 | pub fn start_time_ms(&self) -> u64 { |
689 | 37 | self.start_time_ms.load(std::sync::atomic::Ordering::Relaxed) |
690 | 37 | } |
691 | | |
692 | | /// Get elapsed seconds |
693 | | #[must_use] |
694 | 36 | pub fn elapsed_seconds(&self) -> f64 { |
695 | 36 | let start = self.start_time_ms(); |
696 | 36 | let now = std::time::SystemTime::now() |
697 | 36 | .duration_since(std::time::UNIX_EPOCH) |
698 | 36 | .map(|d| d.as_millis() as u64) |
699 | 36 | .unwrap_or(0); |
700 | 36 | (now.saturating_sub(start)) as f64 / 1000.0 |
701 | 36 | } |
702 | | |
703 | | /// Get throughput |
704 | | #[must_use] |
705 | 18 | pub fn throughput_rps(&self) -> f64 { |
706 | 18 | let elapsed = self.elapsed_seconds(); |
707 | 18 | if elapsed < 0.001 { 0.011 } else { self.total_dispatches() as f64 / elapsed7 } |
708 | 18 | } |
709 | | |
710 | | /// Get CPU latency CV |
711 | | #[must_use] |
712 | 1 | pub fn cpu_latency_cv(&self) -> f64 { |
713 | 1 | let mean = self.cpu_latency_mean_us(); |
714 | 1 | if mean < 0.001 { 0.00 } else { (self.cpu_latency_stddev_us() / mean) * 100.0 } |
715 | 1 | } |
716 | | |
717 | | /// Get GPU latency CV |
718 | | #[must_use] |
719 | 1 | pub fn gpu_latency_cv(&self) -> f64 { |
720 | 1 | let mean = self.gpu_latency_mean_us(); |
721 | 1 | if mean < 0.001 { 0.00 } else { (self.gpu_latency_stddev_us() / mean) * 100.0 } |
722 | 1 | } |
723 | | |
724 | | /// Get CPU/GPU speedup |
725 | | #[must_use] |
726 | 2 | pub fn cpu_gpu_speedup(&self) -> f64 { |
727 | 2 | let gpu_mean = self.gpu_latency_mean_us(); |
728 | 2 | if gpu_mean < 0.001 { 0.01 } else { self1 .cpu_latency_mean_us() / gpu_mean } |
729 | 2 | } |
730 | | |
731 | | /// Reset all metrics |
732 | 5 | pub fn reset(&self) { |
733 | 5 | self.cpu_dispatches.store(0, std::sync::atomic::Ordering::Relaxed); |
734 | 5 | self.gpu_dispatches.store(0, std::sync::atomic::Ordering::Relaxed); |
735 | 5 | self.cpu_latency_count.store(0, std::sync::atomic::Ordering::Relaxed); |
736 | 5 | self.cpu_latency_sum_us.store(0, std::sync::atomic::Ordering::Relaxed); |
737 | 5 | self.gpu_latency_count.store(0, std::sync::atomic::Ordering::Relaxed); |
738 | 5 | self.gpu_latency_sum_us.store(0, std::sync::atomic::Ordering::Relaxed); |
739 | 5 | self.cpu_latency_min_us.store(u64::MAX, std::sync::atomic::Ordering::Relaxed); |
740 | 5 | self.cpu_latency_max_us.store(0, std::sync::atomic::Ordering::Relaxed); |
741 | 5 | self.gpu_latency_min_us.store(u64::MAX, std::sync::atomic::Ordering::Relaxed); |
742 | 5 | self.gpu_latency_max_us.store(0, std::sync::atomic::Ordering::Relaxed); |
743 | 5 | self.cpu_latency_sum_sq_us.store(0, std::sync::atomic::Ordering::Relaxed); |
744 | 5 | self.gpu_latency_sum_sq_us.store(0, std::sync::atomic::Ordering::Relaxed); |
745 | 30 | for bucket25 in &self.cpu_latency_buckets { |
746 | 25 | bucket.store(0, std::sync::atomic::Ordering::Relaxed); |
747 | 25 | } |
748 | 30 | for bucket25 in &self.gpu_latency_buckets { |
749 | 25 | bucket.store(0, std::sync::atomic::Ordering::Relaxed); |
750 | 25 | } |
751 | 5 | let now = std::time::SystemTime::now() |
752 | 5 | .duration_since(std::time::UNIX_EPOCH) |
753 | 5 | .map(|d| d.as_millis() as u64) |
754 | 5 | .unwrap_or(0); |
755 | 5 | self.start_time_ms.store(now, std::sync::atomic::Ordering::Relaxed); |
756 | 5 | } |
757 | | } |
758 | | |
759 | | impl Default for DispatchMetrics { |
760 | 0 | fn default() -> Self { Self::new() } |
761 | | } |