/home/noah/src/trueno/src/brick/exec_graph/node.rs
Line | Count | Source |
1 | | //! Execution Graph Node Types and Profiling Primitives |
2 | | //! |
3 | | //! This module contains all type definitions for execution path tracking: |
4 | | //! |
5 | | //! - **PAR-073**: BrickSample, BrickBottleneck - foundational profiling primitives |
6 | | //! - **PAR-200**: BrickId, BrickCategory, SyncMode - O(1) hot path brick identification |
7 | | //! - **PAR-201**: ExecutionNode, EdgeType, etc. - execution hierarchy types |
8 | | |
9 | | use std::collections::HashMap; |
10 | | use std::fmt; |
11 | | |
12 | | // ============================================================================ |
13 | | // BrickProfiler: FOUNDATIONAL Real-Time Per-Brick Timing (PAR-073) |
14 | | // ============================================================================ |
15 | | |
16 | | /// Individual brick timing sample. |
17 | | /// Pure Rust timing using `std::time::Instant`. |
18 | | #[derive(Debug, Clone, Copy)] |
19 | | pub struct BrickSample { |
20 | | /// Brick name hash (for fast lookup) |
21 | | pub brick_id: u64, |
22 | | /// Elapsed time in nanoseconds |
23 | | pub elapsed_ns: u64, |
24 | | /// Number of elements processed |
25 | | pub elements: u64, |
26 | | } |
27 | | |
28 | | /// Bottleneck classification for roofline analysis (PMAT-451) |
29 | | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
30 | | pub enum BrickBottleneck { |
31 | | /// Not classified |
32 | | #[default] |
33 | | Unknown, |
34 | | /// Limited by memory bandwidth |
35 | | Memory, |
36 | | /// Limited by compute throughput |
37 | | Compute, |
38 | | } |
39 | | |
40 | | impl fmt::Display for BrickBottleneck { |
41 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
42 | 0 | match self { |
43 | 0 | BrickBottleneck::Unknown => write!(f, "unknown"), |
44 | 0 | BrickBottleneck::Memory => write!(f, "memory"), |
45 | 0 | BrickBottleneck::Compute => write!(f, "compute"), |
46 | | } |
47 | 0 | } |
48 | | } |
49 | | |
50 | | // ============================================================================ |
51 | | // PAR-200: BrickProfiler v2 - O(1) Hot Path with BrickId Enum |
52 | | // ============================================================================ |
53 | | |
54 | | /// Well-known brick types for O(1) lookup on hot path. |
55 | | /// |
56 | | /// PAR-200: Eliminates string allocation and HashMap hashing during profiling. |
57 | | /// Use `BrickId::Custom` with string fallback for unknown brick types. |
58 | | /// |
59 | | /// # Example |
60 | | /// ```rust |
61 | | /// use trueno::brick::BrickId; |
62 | | /// |
63 | | /// let brick = BrickId::RmsNorm; |
64 | | /// assert_eq!(brick.category(), trueno::brick::BrickCategory::Norm); |
65 | | /// assert_eq!(brick.name(), "RmsNorm"); |
66 | | /// ``` |
67 | | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
68 | | #[repr(u8)] |
69 | | pub enum BrickId { |
70 | | // Normalization (0-1) |
71 | | /// RMS normalization layer |
72 | | RmsNorm = 0, |
73 | | /// Layer normalization |
74 | | LayerNorm = 1, |
75 | | |
76 | | // Attention (2-7) |
77 | | /// Q/K/V projection (combined or separate) |
78 | | QkvProjection = 2, |
79 | | /// Rotary position embedding |
80 | | RopeEmbedding = 3, |
81 | | /// Attention score computation (Q @ K^T) |
82 | | AttentionScore = 4, |
83 | | /// Attention softmax |
84 | | AttentionSoftmax = 5, |
85 | | /// Attention output (scores @ V) |
86 | | AttentionOutput = 6, |
87 | | /// Output projection after attention |
88 | | OutputProjection = 7, |
89 | | |
90 | | // FFN (8-11) |
91 | | /// Gate projection (for gated FFN) |
92 | | GateProjection = 8, |
93 | | /// Up projection |
94 | | UpProjection = 9, |
95 | | /// SiLU/GELU/ReLU activation |
96 | | Activation = 10, |
97 | | /// Down projection |
98 | | DownProjection = 11, |
99 | | |
100 | | // Other (12-14) |
101 | | /// Token embedding lookup |
102 | | Embedding = 12, |
103 | | /// Language model head (logits) |
104 | | LmHead = 13, |
105 | | /// Token sampling |
106 | | Sampling = 14, |
107 | | } |
108 | | |
109 | | impl BrickId { |
110 | | /// Number of well-known brick types. |
111 | | pub const COUNT: usize = 15; |
112 | | |
113 | | /// Get the category for hierarchical aggregation. |
114 | | #[inline] |
115 | 0 | pub fn category(self) -> BrickCategory { |
116 | 0 | match self { |
117 | 0 | Self::RmsNorm | Self::LayerNorm => BrickCategory::Norm, |
118 | | Self::QkvProjection |
119 | | | Self::RopeEmbedding |
120 | | | Self::AttentionScore |
121 | | | Self::AttentionSoftmax |
122 | | | Self::AttentionOutput |
123 | 0 | | Self::OutputProjection => BrickCategory::Attention, |
124 | | Self::GateProjection | Self::UpProjection | Self::Activation | Self::DownProjection => { |
125 | 0 | BrickCategory::Ffn |
126 | | } |
127 | 0 | Self::Embedding | Self::LmHead | Self::Sampling => BrickCategory::Other, |
128 | | } |
129 | 0 | } |
130 | | |
131 | | /// Get the string name of this brick. |
132 | | #[inline] |
133 | 0 | pub const fn name(self) -> &'static str { |
134 | 0 | match self { |
135 | 0 | Self::RmsNorm => "RmsNorm", |
136 | 0 | Self::LayerNorm => "LayerNorm", |
137 | 0 | Self::QkvProjection => "QkvProjection", |
138 | 0 | Self::RopeEmbedding => "RopeEmbedding", |
139 | 0 | Self::AttentionScore => "AttentionScore", |
140 | 0 | Self::AttentionSoftmax => "AttentionSoftmax", |
141 | 0 | Self::AttentionOutput => "AttentionOutput", |
142 | 0 | Self::OutputProjection => "OutputProjection", |
143 | 0 | Self::GateProjection => "GateProjection", |
144 | 0 | Self::UpProjection => "UpProjection", |
145 | 0 | Self::Activation => "Activation", |
146 | 0 | Self::DownProjection => "DownProjection", |
147 | 0 | Self::Embedding => "Embedding", |
148 | 0 | Self::LmHead => "LmHead", |
149 | 0 | Self::Sampling => "Sampling", |
150 | | } |
151 | 0 | } |
152 | | |
153 | | /// Try to parse a string into a BrickId. |
154 | | #[allow(clippy::should_implement_trait)] |
155 | 0 | pub fn from_str(s: &str) -> Option<Self> { |
156 | 0 | match s { |
157 | 0 | "RmsNorm" => Some(Self::RmsNorm), |
158 | 0 | "LayerNorm" => Some(Self::LayerNorm), |
159 | 0 | "QkvProjection" | "Qkv" => Some(Self::QkvProjection), |
160 | 0 | "RopeEmbedding" | "Rope" | "RoPE" => Some(Self::RopeEmbedding), |
161 | 0 | "AttentionScore" => Some(Self::AttentionScore), |
162 | 0 | "AttentionSoftmax" | "Softmax" => Some(Self::AttentionSoftmax), |
163 | 0 | "AttentionOutput" => Some(Self::AttentionOutput), |
164 | 0 | "OutputProjection" | "OutProj" => Some(Self::OutputProjection), |
165 | 0 | "GateProjection" | "Gate" => Some(Self::GateProjection), |
166 | 0 | "UpProjection" | "Up" => Some(Self::UpProjection), |
167 | 0 | "Activation" | "SiLU" | "GELU" | "ReLU" => Some(Self::Activation), |
168 | 0 | "DownProjection" | "Down" => Some(Self::DownProjection), |
169 | 0 | "Embedding" | "Embed" => Some(Self::Embedding), |
170 | 0 | "LmHead" | "Head" => Some(Self::LmHead), |
171 | 0 | "Sampling" | "Sample" => Some(Self::Sampling), |
172 | 0 | _ => None, |
173 | | } |
174 | 0 | } |
175 | | } |
176 | | |
177 | | impl fmt::Display for BrickId { |
178 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
179 | 0 | write!(f, "{}", self.name()) |
180 | 0 | } |
181 | | } |
182 | | |
183 | | /// Category for hierarchical aggregation of brick statistics. |
184 | | /// |
185 | | /// PAR-200: Groups related bricks for high-level performance analysis. |
186 | | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] |
187 | | #[repr(u8)] |
188 | | pub enum BrickCategory { |
189 | | /// Normalization layers (RmsNorm, LayerNorm) |
190 | | Norm = 0, |
191 | | /// Attention mechanism (QKV, RoPE, scores, softmax, output) |
192 | | Attention = 1, |
193 | | /// Feed-forward network (gate, up, activation, down) |
194 | | Ffn = 2, |
195 | | /// Other operations (embedding, lm_head, sampling) |
196 | | #[default] |
197 | | Other = 3, |
198 | | } |
199 | | |
200 | | impl BrickCategory { |
201 | | /// Number of categories. |
202 | | pub const COUNT: usize = 4; |
203 | | |
204 | | /// Get the string name of this category. |
205 | | #[inline] |
206 | 0 | pub const fn name(self) -> &'static str { |
207 | 0 | match self { |
208 | 0 | Self::Norm => "Norm", |
209 | 0 | Self::Attention => "Attention", |
210 | 0 | Self::Ffn => "FFN", |
211 | 0 | Self::Other => "Other", |
212 | | } |
213 | 0 | } |
214 | | } |
215 | | |
216 | | impl fmt::Display for BrickCategory { |
217 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
218 | 0 | write!(f, "{}", self.name()) |
219 | 0 | } |
220 | | } |
221 | | |
222 | | /// Synchronization mode for GPU profiling. |
223 | | /// |
224 | | /// PAR-200: Controls the trade-off between accuracy and overhead. |
225 | | /// |
226 | | /// # Performance Characteristics |
227 | | /// |
228 | | /// | Mode | Overhead | Accuracy | Use Case | |
229 | | /// |------|----------|----------|----------| |
230 | | /// | `Immediate` | ~200% | Exact per-kernel | Debugging | |
231 | | /// | `PerLayer` | ~20% | Per-layer exact | Development | |
232 | | /// | `Deferred` | ~5% | Approximate | Production | |
233 | | /// | `None` | 0% | N/A | Disabled | |
234 | | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
235 | | pub enum SyncMode { |
236 | | /// Sync after each kernel (accurate but slow). |
237 | | /// Best for debugging and detailed optimization. |
238 | | Immediate, |
239 | | /// Sync once per transformer layer. |
240 | | /// Good balance for development. |
241 | | PerLayer, |
242 | | /// Sync once per forward pass (fast, approximate). |
243 | | /// Best for production profiling. |
244 | | #[default] |
245 | | Deferred, |
246 | | /// No synchronization (profiling disabled or CPU-only). |
247 | | None, |
248 | | } |
249 | | |
250 | | // ============================================================================ |
251 | | // PAR-201: Execution Path Graph Types |
252 | | // ============================================================================ |
253 | | |
254 | | /// Node ID in the execution graph. |
255 | | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
256 | | pub struct ExecutionNodeId(pub u32); |
257 | | |
258 | | /// Execution graph node types. |
259 | | /// |
260 | | /// PAR-201: Represents different levels of the execution hierarchy. |
261 | | #[derive(Debug, Clone)] |
262 | | pub enum ExecutionNode { |
263 | | /// High-level brick (BrickId from v2) |
264 | | Brick { |
265 | | id: BrickId, |
266 | | timing_ns: u64, |
267 | | elements: u64, |
268 | | }, |
269 | | /// GPU kernel launch |
270 | | Kernel { |
271 | | name: String, |
272 | | /// FNV-1a hash of PTX source for identity |
273 | | ptx_hash: u64, |
274 | | /// Grid dimensions (blocks) |
275 | | grid: (u32, u32, u32), |
276 | | /// Block dimensions (threads) |
277 | | block: (u32, u32, u32), |
278 | | /// Shared memory bytes |
279 | | shared_mem: u32, |
280 | | /// Kernel execution time in nanoseconds (Phase 9: for CPA) |
281 | | timing_ns: Option<u64>, |
282 | | /// Arithmetic intensity (FLOPs/byte) for roofline analysis (Phase 9) |
283 | | arithmetic_intensity: Option<f32>, |
284 | | /// Achieved throughput in TFLOP/s (Phase 9) |
285 | | achieved_tflops: Option<f32>, |
286 | | }, |
287 | | /// Memory transfer operation (Phase 9: data movement topology) |
288 | | Transfer { |
289 | | /// Source location description |
290 | | src: String, |
291 | | /// Destination location description |
292 | | dst: String, |
293 | | /// Bytes transferred |
294 | | bytes: u64, |
295 | | /// Transfer direction |
296 | | direction: TransferDirection, |
297 | | /// Transfer time in nanoseconds |
298 | | timing_ns: Option<u64>, |
299 | | }, |
300 | | /// Rust function (from DWARF or manual annotation) |
301 | | Function { |
302 | | name: String, |
303 | | file: Option<String>, |
304 | | line: Option<u32>, |
305 | | }, |
306 | | /// Transformer layer grouping |
307 | | Layer { index: u32 }, |
308 | | /// Phase 11 (E.9.4): Async task metrics for poll efficiency tracking |
309 | | AsyncTask { |
310 | | /// Task name for identification |
311 | | name: String, |
312 | | /// Number of times poll() was called |
313 | | poll_count: u64, |
314 | | /// Number of times poll() returned Pending |
315 | | yield_count: u64, |
316 | | /// Total time spent in poll() (nanoseconds) |
317 | | total_poll_ns: u64, |
318 | | }, |
319 | | } |
320 | | |
321 | | impl ExecutionNode { |
322 | | /// Get the display name of this node. |
323 | 0 | pub fn name(&self) -> String { |
324 | 0 | match self { |
325 | 0 | Self::Brick { id, .. } => id.name().to_string(), |
326 | 0 | Self::Kernel { name, .. } => name.clone(), |
327 | 0 | Self::Function { name, .. } => name.clone(), |
328 | 0 | Self::Layer { index } => format!("Layer{}", index), |
329 | | Self::Transfer { |
330 | 0 | src, |
331 | 0 | dst, |
332 | 0 | direction, |
333 | | .. |
334 | | } => { |
335 | 0 | let dir = match direction { |
336 | 0 | TransferDirection::H2D => "H2D", |
337 | 0 | TransferDirection::D2H => "D2H", |
338 | 0 | TransferDirection::D2D => "D2D", |
339 | | }; |
340 | 0 | format!("{}:{}->{}", dir, src, dst) |
341 | | } |
342 | 0 | Self::AsyncTask { name, .. } => name.clone(), |
343 | | } |
344 | 0 | } |
345 | | |
346 | | /// Check if this is a kernel node. |
347 | 0 | pub fn is_kernel(&self) -> bool { |
348 | 0 | matches!(self, Self::Kernel { .. }) |
349 | 0 | } |
350 | | |
351 | | /// Check if this is a brick node. |
352 | 0 | pub fn is_brick(&self) -> bool { |
353 | 0 | matches!(self, Self::Brick { .. }) |
354 | 0 | } |
355 | | |
356 | | /// Check if this is a transfer node. |
357 | 0 | pub fn is_transfer(&self) -> bool { |
358 | 0 | matches!(self, Self::Transfer { .. }) |
359 | 0 | } |
360 | | |
361 | | /// Get timing if available (bricks, kernels, and transfers). |
362 | 0 | pub fn timing_ns(&self) -> Option<u64> { |
363 | 0 | match self { |
364 | 0 | Self::Brick { timing_ns, .. } => Some(*timing_ns), |
365 | 0 | Self::Kernel { timing_ns, .. } => *timing_ns, |
366 | 0 | Self::Transfer { timing_ns, .. } => *timing_ns, |
367 | 0 | _ => None, |
368 | | } |
369 | 0 | } |
370 | | |
371 | | /// Get PTX hash if available (kernels only). |
372 | 0 | pub fn ptx_hash(&self) -> Option<u64> { |
373 | 0 | match self { |
374 | 0 | Self::Kernel { ptx_hash, .. } => Some(*ptx_hash), |
375 | 0 | _ => None, |
376 | | } |
377 | 0 | } |
378 | | |
379 | | /// Get arithmetic intensity if available (kernels only, Phase 9). |
380 | 0 | pub fn arithmetic_intensity(&self) -> Option<f32> { |
381 | 0 | match self { |
382 | | Self::Kernel { |
383 | 0 | arithmetic_intensity, |
384 | | .. |
385 | 0 | } => *arithmetic_intensity, |
386 | 0 | _ => None, |
387 | | } |
388 | 0 | } |
389 | | |
390 | | /// Get achieved TFLOP/s if available (kernels only, Phase 9). |
391 | 0 | pub fn achieved_tflops(&self) -> Option<f32> { |
392 | 0 | match self { |
393 | | Self::Kernel { |
394 | 0 | achieved_tflops, .. |
395 | 0 | } => *achieved_tflops, |
396 | 0 | _ => None, |
397 | | } |
398 | 0 | } |
399 | | |
400 | | /// Get transfer bytes if available (transfers only, Phase 9). |
401 | 0 | pub fn transfer_bytes(&self) -> Option<u64> { |
402 | 0 | match self { |
403 | 0 | Self::Transfer { bytes, .. } => Some(*bytes), |
404 | 0 | _ => None, |
405 | | } |
406 | 0 | } |
407 | | } |
408 | | |
409 | | /// Edge types in execution graph. |
410 | | /// |
411 | | /// PAR-201: Describes relationships between execution nodes. |
412 | | /// Phase 9 (E.7.12): Added DependsOn and Transfer for advanced profiling. |
413 | | #[derive(Debug, Clone, PartialEq)] |
414 | | pub enum EdgeType { |
415 | | /// Function calls function |
416 | | Calls, |
417 | | /// Brick contains sub-operations |
418 | | Contains, |
419 | | /// Function launches GPU kernel |
420 | | Launches, |
421 | | /// Temporal sequence (A happens before B) |
422 | | Sequence, |
423 | | /// Dependency edge for critical path analysis (CUDA events, stream sync) |
424 | | /// PAR-201 Phase 9: CPA requires tracking true dependencies vs containment |
425 | | DependsOn, |
426 | | /// Data transfer edge with byte count (H2D/D2H/D2D) |
427 | | /// PAR-201 Phase 9: For data movement topology and ping-pong detection |
428 | | Transfer { |
429 | | /// Bytes transferred |
430 | | bytes: u64, |
431 | | /// Transfer direction |
432 | | direction: TransferDirection, |
433 | | }, |
434 | | } |
435 | | |
436 | | /// Direction of memory transfer. |
437 | | /// |
438 | | /// PAR-201 Phase 9: Used with EdgeType::Transfer for data movement analysis. |
439 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
440 | | pub enum TransferDirection { |
441 | | /// Host to Device |
442 | | H2D, |
443 | | /// Device to Host |
444 | | D2H, |
445 | | /// Device to Device |
446 | | D2D, |
447 | | } |
448 | | |
449 | | /// An edge in the execution graph. |
450 | | #[derive(Debug, Clone)] |
451 | | pub struct ExecutionEdge { |
452 | | /// Source node ID |
453 | | pub src: ExecutionNodeId, |
454 | | /// Destination node ID |
455 | | pub dst: ExecutionNodeId, |
456 | | /// Edge type |
457 | | pub edge_type: EdgeType, |
458 | | /// Optional weight (e.g., call count, timing) |
459 | | pub weight: f32, |
460 | | } |
461 | | |
462 | | // ============================================================================ |
463 | | // PTX Registry and Statistics Types |
464 | | // ============================================================================ |
465 | | |
466 | | /// PTX kernel registry for execution graph correlation. |
467 | | /// |
468 | | /// PAR-201: Maps PTX hashes to source code for debugging and analysis. |
469 | | #[derive(Debug, Default)] |
470 | | pub struct PtxRegistry { |
471 | | /// Hash → (kernel_name, ptx_source, file_path) |
472 | | kernels: HashMap<u64, (String, String, Option<std::path::PathBuf>)>, |
473 | | } |
474 | | |
475 | | impl PtxRegistry { |
476 | | /// Create a new empty registry. |
477 | 0 | pub fn new() -> Self { |
478 | 0 | Self::default() |
479 | 0 | } |
480 | | |
481 | | /// Register PTX source code. |
482 | | /// |
483 | | /// # Arguments |
484 | | /// - `name`: Kernel name (e.g., "batched_q4k_gemv") |
485 | | /// - `ptx`: PTX source code |
486 | | /// - `path`: Optional file path for source correlation |
487 | 0 | pub fn register(&mut self, name: &str, ptx: &str, path: Option<&std::path::Path>) { |
488 | 0 | let hash = Self::hash_ptx(ptx); |
489 | 0 | self.kernels.insert( |
490 | 0 | hash, |
491 | | ( |
492 | 0 | name.to_string(), |
493 | 0 | ptx.to_string(), |
494 | 0 | path.map(|p| p.to_path_buf()), |
495 | | ), |
496 | | ); |
497 | 0 | } |
498 | | |
499 | | /// Compute FNV-1a hash of PTX source. |
500 | | #[inline] |
501 | 0 | pub fn hash_ptx(ptx: &str) -> u64 { |
502 | | // FNV-1a hash |
503 | 0 | let mut hash: u64 = 0xcbf29ce484222325; |
504 | 0 | for byte in ptx.bytes() { |
505 | 0 | hash ^= byte as u64; |
506 | 0 | hash = hash.wrapping_mul(0x100000001b3); |
507 | 0 | } |
508 | 0 | hash |
509 | 0 | } |
510 | | |
511 | | /// Lookup PTX source by hash. |
512 | 0 | pub fn lookup(&self, hash: u64) -> Option<&str> { |
513 | 0 | self.kernels.get(&hash).map(|(_, ptx, _)| ptx.as_str()) |
514 | 0 | } |
515 | | |
516 | | /// Lookup kernel name by hash. |
517 | 0 | pub fn lookup_name(&self, hash: u64) -> Option<&str> { |
518 | 0 | self.kernels.get(&hash).map(|(name, _, _)| name.as_str()) |
519 | 0 | } |
520 | | |
521 | | /// Lookup file path by hash. |
522 | 0 | pub fn lookup_path(&self, hash: u64) -> Option<&std::path::Path> { |
523 | 0 | self.kernels |
524 | 0 | .get(&hash) |
525 | 0 | .and_then(|(_, _, path)| path.as_deref()) |
526 | 0 | } |
527 | | |
528 | | /// Get all registered hashes. |
529 | 0 | pub fn hashes(&self) -> impl Iterator<Item = u64> + '_ { |
530 | 0 | self.kernels.keys().copied() |
531 | 0 | } |
532 | | |
533 | | /// Number of registered kernels. |
534 | 0 | pub fn len(&self) -> usize { |
535 | 0 | self.kernels.len() |
536 | 0 | } |
537 | | |
538 | | /// Check if registry is empty. |
539 | 0 | pub fn is_empty(&self) -> bool { |
540 | 0 | self.kernels.is_empty() |
541 | 0 | } |
542 | | } |
543 | | |
544 | | /// Aggregated statistics for a brick category. |
545 | | #[derive(Debug, Clone, Copy, Default)] |
546 | | pub struct CategoryStats { |
547 | | /// Total elapsed time (nanoseconds) |
548 | | pub total_ns: u64, |
549 | | /// Total elements processed |
550 | | pub total_elements: u64, |
551 | | /// Total samples |
552 | | pub count: u64, |
553 | | } |
554 | | |
555 | | impl CategoryStats { |
556 | | /// Average time per sample in microseconds. |
557 | | #[inline] |
558 | 0 | pub fn avg_us(&self) -> f64 { |
559 | 0 | if self.count == 0 { |
560 | 0 | 0.0 |
561 | | } else { |
562 | 0 | self.total_ns as f64 / self.count as f64 / 1000.0 |
563 | | } |
564 | 0 | } |
565 | | |
566 | | /// Throughput in elements per second. |
567 | | #[inline] |
568 | 0 | pub fn throughput(&self) -> f64 { |
569 | 0 | if self.total_ns == 0 { |
570 | 0 | 0.0 |
571 | | } else { |
572 | 0 | self.total_elements as f64 / (self.total_ns as f64 / 1_000_000_000.0) |
573 | | } |
574 | 0 | } |
575 | | |
576 | | /// Percentage of total time (given total_ns across all categories). |
577 | | #[inline] |
578 | 0 | pub fn percentage(&self, total: u64) -> f64 { |
579 | 0 | if total == 0 { |
580 | 0 | 0.0 |
581 | | } else { |
582 | 0 | 100.0 * self.total_ns as f64 / total as f64 |
583 | | } |
584 | 0 | } |
585 | | } |
586 | | |
587 | | /// Accumulated per-brick statistics. |
588 | | #[derive(Debug, Clone, Default)] |
589 | | pub struct BrickStats { |
590 | | /// Brick name |
591 | | pub name: String, |
592 | | /// Total samples collected |
593 | | pub count: u64, |
594 | | /// Total elapsed time (nanoseconds) |
595 | | pub total_ns: u64, |
596 | | /// Min elapsed time (nanoseconds) |
597 | | pub min_ns: u64, |
598 | | /// Max elapsed time (nanoseconds) |
599 | | pub max_ns: u64, |
600 | | /// Total elements processed |
601 | | pub total_elements: u64, |
602 | | /// PMAT-451: Total bytes processed (for throughput calculation) |
603 | | pub total_bytes: u64, |
604 | | /// PMAT-451: Total compressed bytes (for compression ratio) |
605 | | pub total_compressed_bytes: u64, |
606 | | /// PMAT-451: Bottleneck classification |
607 | | pub bottleneck: BrickBottleneck, |
608 | | /// Phase 11 (E.9.2): Total CPU cycles (from RDTSCP/CNTVCT) |
609 | | pub total_cycles: u64, |
610 | | /// Phase 11: Minimum CPU cycles observed |
611 | | pub min_cycles: u64, |
612 | | /// Phase 11: Maximum CPU cycles observed |
613 | | pub max_cycles: u64, |
614 | | } |
615 | | |
616 | | impl BrickStats { |
617 | | /// Create new stats for a brick. |
618 | 0 | pub fn new(name: &str) -> Self { |
619 | 0 | Self { |
620 | 0 | name: name.to_string(), |
621 | 0 | count: 0, |
622 | 0 | total_ns: 0, |
623 | 0 | min_ns: u64::MAX, |
624 | 0 | max_ns: 0, |
625 | 0 | total_elements: 0, |
626 | 0 | total_bytes: 0, |
627 | 0 | total_compressed_bytes: 0, |
628 | 0 | bottleneck: BrickBottleneck::Unknown, |
629 | 0 | total_cycles: 0, |
630 | 0 | min_cycles: u64::MAX, |
631 | 0 | max_cycles: 0, |
632 | 0 | } |
633 | 0 | } |
634 | | |
635 | | /// Add a sample to statistics. |
636 | 0 | pub fn add_sample(&mut self, elapsed_ns: u64, elements: u64) { |
637 | 0 | self.count += 1; |
638 | 0 | self.total_ns += elapsed_ns; |
639 | 0 | self.min_ns = self.min_ns.min(elapsed_ns); |
640 | 0 | self.max_ns = self.max_ns.max(elapsed_ns); |
641 | 0 | self.total_elements += elements; |
642 | 0 | } |
643 | | |
644 | | /// Phase 11 (E.9.2): Add a sample with CPU cycle count. |
645 | | /// |
646 | | /// Use this for frequency-invariant performance analysis. |
647 | | /// Cycles are immune to CPU frequency scaling (turbo boost). |
648 | 0 | pub fn add_sample_with_cycles(&mut self, elapsed_ns: u64, elements: u64, cycles: u64) { |
649 | 0 | self.add_sample(elapsed_ns, elements); |
650 | 0 | self.total_cycles += cycles; |
651 | 0 | self.min_cycles = self.min_cycles.min(cycles); |
652 | 0 | self.max_cycles = self.max_cycles.max(cycles); |
653 | 0 | } |
654 | | |
655 | | /// Phase 11: Cycles per element (frequency-invariant throughput metric). |
656 | | /// |
657 | | /// Lower is better. This metric is immune to CPU frequency scaling. |
658 | | #[must_use] |
659 | 0 | pub fn cycles_per_element(&self) -> f64 { |
660 | 0 | if self.total_elements == 0 { |
661 | 0 | 0.0 |
662 | | } else { |
663 | 0 | self.total_cycles as f64 / self.total_elements as f64 |
664 | | } |
665 | 0 | } |
666 | | |
667 | | /// Phase 11: Average cycles per sample. |
668 | | #[must_use] |
669 | 0 | pub fn avg_cycles(&self) -> f64 { |
670 | 0 | if self.count == 0 { |
671 | 0 | 0.0 |
672 | | } else { |
673 | 0 | self.total_cycles as f64 / self.count as f64 |
674 | | } |
675 | 0 | } |
676 | | |
677 | | /// Phase 11: Estimated IPC (Instructions Per Cycle). |
678 | | /// |
679 | | /// Approximation assuming ~1 instruction per element for simple ops. |
680 | | /// - Low IPC (<1.0): Memory stalls (cache misses, memory latency) |
681 | | /// - High IPC (>2.0): Compute bound (efficient execution) |
682 | | #[must_use] |
683 | 0 | pub fn estimated_ipc(&self) -> f64 { |
684 | 0 | if self.total_cycles == 0 { |
685 | 0 | 0.0 |
686 | | } else { |
687 | | // Rough approximation: assume 1 instruction per element |
688 | 0 | self.total_elements as f64 / self.total_cycles as f64 |
689 | | } |
690 | 0 | } |
691 | | |
692 | | /// Phase 11: Diagnose bottleneck based on cycles vs time ratio. |
693 | | /// |
694 | | /// High cycles + low time = likely cache misses |
695 | | /// Low cycles + high time = likely CPU throttling or context switches |
696 | | #[must_use] |
697 | 0 | pub fn diagnose_from_cycles(&self) -> &'static str { |
698 | 0 | if self.total_cycles == 0 || self.total_ns == 0 { |
699 | 0 | return "insufficient data"; |
700 | 0 | } |
701 | | |
702 | 0 | let ipc = self.estimated_ipc(); |
703 | 0 | let ns_per_cycle = self.total_ns as f64 / self.total_cycles as f64; |
704 | | |
705 | | // Typical CPU runs at ~3GHz, so 1 cycle ≈ 0.33ns |
706 | | // If ns_per_cycle >> 0.33, we're seeing stalls or throttling |
707 | 0 | if ipc < 0.5 { |
708 | 0 | "memory-bound (low IPC, likely cache misses)" |
709 | 0 | } else if ipc > 2.0 { |
710 | 0 | "compute-bound (efficient)" |
711 | 0 | } else if ns_per_cycle > 1.0 { |
712 | 0 | "throttled or context-switched" |
713 | | } else { |
714 | 0 | "balanced" |
715 | | } |
716 | 0 | } |
717 | | |
718 | | /// PMAT-451: Add a sample with byte metrics for compression workloads. |
719 | | /// |
720 | | /// # Arguments |
721 | | /// - `elapsed_ns`: Time taken in nanoseconds |
722 | | /// - `elements`: Number of elements processed (e.g., pages) |
723 | | /// - `input_bytes`: Original uncompressed size |
724 | | /// - `output_bytes`: Compressed output size |
725 | 0 | pub fn add_sample_with_bytes( |
726 | 0 | &mut self, |
727 | 0 | elapsed_ns: u64, |
728 | 0 | elements: u64, |
729 | 0 | input_bytes: u64, |
730 | 0 | output_bytes: u64, |
731 | 0 | ) { |
732 | 0 | self.add_sample(elapsed_ns, elements); |
733 | 0 | self.total_bytes += input_bytes; |
734 | 0 | self.total_compressed_bytes += output_bytes; |
735 | 0 | } |
736 | | |
737 | | /// PMAT-451: Calculate compression ratio (input_size / output_size). |
738 | | /// Returns 1.0 if no compression data available. |
739 | | #[must_use] |
740 | 0 | pub fn compression_ratio(&self) -> f64 { |
741 | 0 | if self.total_compressed_bytes == 0 { |
742 | 0 | 1.0 |
743 | | } else { |
744 | 0 | self.total_bytes as f64 / self.total_compressed_bytes as f64 |
745 | | } |
746 | 0 | } |
747 | | |
748 | | /// PMAT-451: Calculate throughput in GB/s. |
749 | | /// Based on total input bytes processed. |
750 | | #[must_use] |
751 | 0 | pub fn throughput_gbps(&self) -> f64 { |
752 | 0 | if self.total_ns == 0 { |
753 | 0 | 0.0 |
754 | | } else { |
755 | 0 | let bytes_per_ns = self.total_bytes as f64 / self.total_ns as f64; |
756 | 0 | bytes_per_ns * 1e9 / 1e9 // Convert to GB/s (ns to sec, bytes to GB) |
757 | | } |
758 | 0 | } |
759 | | |
760 | | /// PMAT-451: Set bottleneck classification. |
761 | 0 | pub fn set_bottleneck(&mut self, bottleneck: BrickBottleneck) { |
762 | 0 | self.bottleneck = bottleneck; |
763 | 0 | } |
764 | | |
765 | | /// PMAT-451: Get bottleneck classification. |
766 | | #[must_use] |
767 | 0 | pub fn get_bottleneck(&self) -> BrickBottleneck { |
768 | 0 | self.bottleneck |
769 | 0 | } |
770 | | |
771 | | /// Average time in microseconds. |
772 | | #[must_use] |
773 | 0 | pub fn avg_us(&self) -> f64 { |
774 | 0 | if self.count == 0 { |
775 | 0 | 0.0 |
776 | | } else { |
777 | 0 | self.total_ns as f64 / self.count as f64 / 1000.0 |
778 | | } |
779 | 0 | } |
780 | | |
781 | | /// Throughput in elements/second. |
782 | | #[must_use] |
783 | 0 | pub fn throughput(&self) -> f64 { |
784 | 0 | if self.total_ns == 0 { |
785 | 0 | 0.0 |
786 | | } else { |
787 | 0 | self.total_elements as f64 / (self.total_ns as f64 / 1_000_000_000.0) |
788 | | } |
789 | 0 | } |
790 | | |
791 | | /// Throughput in tokens/second (alias for throughput). |
792 | | #[must_use] |
793 | 0 | pub fn tokens_per_sec(&self) -> f64 { |
794 | 0 | self.throughput() |
795 | 0 | } |
796 | | |
797 | | /// Minimum time in microseconds. |
798 | | #[must_use] |
799 | 0 | pub fn min_us(&self) -> f64 { |
800 | 0 | if self.min_ns == u64::MAX { |
801 | 0 | 0.0 |
802 | | } else { |
803 | 0 | self.min_ns as f64 / 1000.0 |
804 | | } |
805 | 0 | } |
806 | | |
807 | | /// Maximum time in microseconds. |
808 | | #[must_use] |
809 | 0 | pub fn max_us(&self) -> f64 { |
810 | 0 | self.max_ns as f64 / 1000.0 |
811 | 0 | } |
812 | | } |