/home/noah/src/realizar/src/bench_viz.rs
Line | Count | Source |
1 | | //! Benchmark Visualization Module (PAR-040) |
2 | | //! |
3 | | //! Creates 2×3 grid visualizations for inference benchmark comparisons |
4 | | //! and generates profiling logs suitable for chat paste debugging. |
5 | | //! |
6 | | //! ## Layout |
7 | | //! |
8 | | //! ```text |
9 | | //! ┌─────────────────────────────────────────────────────────────────────┐ |
10 | | //! │ GGUF Inference Comparison (tok/s GPU) │ |
11 | | //! ├─────────────────────┬─────────────────────┬─────────────────────────┤ |
12 | | //! │ APR serve GGUF │ Ollama │ llama.cpp │ |
13 | | //! ├─────────────────────┴─────────────────────┴─────────────────────────┤ |
14 | | //! │ APR Server Format Comparison (tok/s GPU) │ |
15 | | //! ├─────────────────────┬─────────────────────┬─────────────────────────┤ |
16 | | //! │ APR serve .apr │ APR serve GGUF │ Ollama / llama.cpp │ |
17 | | //! └─────────────────────┴─────────────────────┴─────────────────────────┘ |
18 | | //! ``` |
19 | | |
20 | | use std::fmt::Write as FmtWrite; |
21 | | use std::time::{Duration, Instant}; |
22 | | |
23 | | // ============================================================================ |
24 | | // Benchmark Result Types |
25 | | // ============================================================================ |
26 | | |
27 | | /// Single benchmark measurement |
28 | | #[derive(Debug, Clone)] |
29 | | pub struct BenchMeasurement { |
30 | | /// Engine name (APR, Ollama, llama.cpp) |
31 | | pub engine: String, |
32 | | /// Format (GGUF, APR) |
33 | | pub format: String, |
34 | | /// Throughput in tokens/second |
35 | | pub tokens_per_sec: f64, |
36 | | /// Time to first token in milliseconds |
37 | | pub ttft_ms: f64, |
38 | | /// Number of tokens generated |
39 | | pub tokens_generated: usize, |
40 | | /// Total duration |
41 | | pub duration: Duration, |
42 | | /// GPU utilization percentage (if available) |
43 | | pub gpu_util: Option<f64>, |
44 | | /// GPU memory used in MB (if available) |
45 | | pub gpu_mem_mb: Option<f64>, |
46 | | } |
47 | | |
48 | | impl BenchMeasurement { |
49 | | /// Create a new benchmark measurement |
50 | 18 | pub fn new(engine: &str, format: &str) -> Self { |
51 | 18 | Self { |
52 | 18 | engine: engine.to_string(), |
53 | 18 | format: format.to_string(), |
54 | 18 | tokens_per_sec: 0.0, |
55 | 18 | ttft_ms: 0.0, |
56 | 18 | tokens_generated: 0, |
57 | 18 | duration: Duration::ZERO, |
58 | 18 | gpu_util: None, |
59 | 18 | gpu_mem_mb: None, |
60 | 18 | } |
61 | 18 | } |
62 | | |
63 | | /// Set throughput |
64 | | #[must_use] |
65 | 13 | pub fn with_throughput(mut self, tps: f64) -> Self { |
66 | 13 | self.tokens_per_sec = tps; |
67 | 13 | self |
68 | 13 | } |
69 | | |
70 | | /// Set TTFT |
71 | | #[must_use] |
72 | 8 | pub fn with_ttft(mut self, ttft_ms: f64) -> Self { |
73 | 8 | self.ttft_ms = ttft_ms; |
74 | 8 | self |
75 | 8 | } |
76 | | |
77 | | /// Set tokens generated |
78 | | #[must_use] |
79 | 2 | pub fn with_tokens(mut self, count: usize, duration: Duration) -> Self { |
80 | 2 | self.tokens_generated = count; |
81 | 2 | self.duration = duration; |
82 | 2 | if duration.as_secs_f64() > 0.0 { |
83 | 1 | self.tokens_per_sec = count as f64 / duration.as_secs_f64(); |
84 | 1 | } |
85 | 2 | self |
86 | 2 | } |
87 | | |
88 | | /// Set GPU metrics |
89 | | #[must_use] |
90 | 3 | pub fn with_gpu(mut self, util: f64, mem_mb: f64) -> Self { |
91 | 3 | self.gpu_util = Some(util); |
92 | 3 | self.gpu_mem_mb = Some(mem_mb); |
93 | 3 | self |
94 | 3 | } |
95 | | } |
96 | | |
97 | | /// Profiling hotspot for debugging |
98 | | #[derive(Debug, Clone)] |
99 | | pub struct ProfilingHotspot { |
100 | | /// Component name |
101 | | pub component: String, |
102 | | /// Time spent |
103 | | pub time: Duration, |
104 | | /// Percentage of total |
105 | | pub percentage: f64, |
106 | | /// Call count |
107 | | pub call_count: u64, |
108 | | /// Average time per call |
109 | | pub avg_per_call: Duration, |
110 | | /// Explanation/recommendation |
111 | | pub explanation: String, |
112 | | /// Is this expected for inference? |
113 | | pub is_expected: bool, |
114 | | } |
115 | | |
116 | | impl ProfilingHotspot { |
117 | | /// Format as single-line report |
118 | 1 | pub fn to_line(&self) -> String { |
119 | 1 | let marker = if self.is_expected { "✓" } else { "⚠"0 }; |
120 | 1 | format!( |
121 | 1 | "{} {:20} {:>6.1}% {:>8.2}ms ({:>6} calls, {:>6.2}µs/call)", |
122 | | marker, |
123 | | self.component, |
124 | | self.percentage, |
125 | 1 | self.time.as_secs_f64() * 1000.0, |
126 | | self.call_count, |
127 | 1 | self.avg_per_call.as_secs_f64() * 1_000_000.0 |
128 | | ) |
129 | 1 | } |
130 | | } |
131 | | |
132 | | // ============================================================================ |
133 | | // Benchmark Grid (2×3) |
134 | | // ============================================================================ |
135 | | |
136 | | /// 2×3 Benchmark comparison grid |
137 | | #[derive(Debug, Clone, Default)] |
138 | | pub struct BenchmarkGrid { |
139 | | /// Row 1, Col 1: APR server serving GGUF format |
140 | | pub gguf_apr: Option<BenchMeasurement>, |
141 | | /// Row 1, Col 2: Ollama serving GGUF format |
142 | | pub gguf_ollama: Option<BenchMeasurement>, |
143 | | /// Row 1, Col 3: llama.cpp serving GGUF format |
144 | | pub gguf_llamacpp: Option<BenchMeasurement>, |
145 | | |
146 | | /// Row 2, Col 1: APR server serving native .apr format |
147 | | pub apr_native: Option<BenchMeasurement>, |
148 | | /// Row 2, Col 2: APR server serving GGUF (for comparison) |
149 | | pub apr_gguf: Option<BenchMeasurement>, |
150 | | /// Row 2, Col 3: Baseline measurement (Ollama/llama.cpp) |
151 | | pub apr_baseline: Option<BenchMeasurement>, |
152 | | |
153 | | /// Profiling hotspots |
154 | | pub hotspots: Vec<ProfilingHotspot>, |
155 | | |
156 | | /// Model name |
157 | | pub model_name: String, |
158 | | /// Model parameters (e.g., "0.5B") |
159 | | pub model_params: String, |
160 | | /// Quantization type (e.g., "Q4_K_M") |
161 | | pub quantization: String, |
162 | | |
163 | | /// GPU name |
164 | | pub gpu_name: String, |
165 | | /// GPU VRAM in GB |
166 | | pub gpu_vram_gb: f64, |
167 | | } |
168 | | |
169 | | impl BenchmarkGrid { |
170 | | /// Create new benchmark grid |
171 | 8 | pub fn new() -> Self { |
172 | 8 | Self::default() |
173 | 8 | } |
174 | | |
175 | | /// Set model info |
176 | | #[must_use] |
177 | 3 | pub fn with_model(mut self, name: &str, params: &str, quant: &str) -> Self { |
178 | 3 | self.model_name = name.to_string(); |
179 | 3 | self.model_params = params.to_string(); |
180 | 3 | self.quantization = quant.to_string(); |
181 | 3 | self |
182 | 3 | } |
183 | | |
184 | | /// Set GPU info |
185 | | #[must_use] |
186 | 3 | pub fn with_gpu(mut self, name: &str, vram_gb: f64) -> Self { |
187 | 3 | self.gpu_name = name.to_string(); |
188 | 3 | self.gpu_vram_gb = vram_gb; |
189 | 3 | self |
190 | 3 | } |
191 | | |
192 | | /// Add GGUF row measurements |
193 | 1 | pub fn set_gguf_row( |
194 | 1 | &mut self, |
195 | 1 | apr: BenchMeasurement, |
196 | 1 | ollama: BenchMeasurement, |
197 | 1 | llamacpp: BenchMeasurement, |
198 | 1 | ) { |
199 | 1 | self.gguf_apr = Some(apr); |
200 | 1 | self.gguf_ollama = Some(ollama); |
201 | 1 | self.gguf_llamacpp = Some(llamacpp); |
202 | 1 | } |
203 | | |
204 | | /// Add APR row measurements |
205 | 1 | pub fn set_apr_row( |
206 | 1 | &mut self, |
207 | 1 | native: BenchMeasurement, |
208 | 1 | gguf: BenchMeasurement, |
209 | 1 | baseline: BenchMeasurement, |
210 | 1 | ) { |
211 | 1 | self.apr_native = Some(native); |
212 | 1 | self.apr_gguf = Some(gguf); |
213 | 1 | self.apr_baseline = Some(baseline); |
214 | 1 | } |
215 | | |
216 | | /// Add profiling hotspot |
217 | 5 | pub fn add_hotspot(&mut self, hotspot: ProfilingHotspot) { |
218 | 5 | self.hotspots.push(hotspot); |
219 | 5 | } |
220 | | |
221 | | // ======================================================================== |
222 | | // Terminal Visualization (ASCII) |
223 | | // ======================================================================== |
224 | | |
225 | | /// Render as ASCII grid for terminal |
226 | 1 | pub fn render_ascii(&self) -> String { |
227 | 1 | let mut out = String::new(); |
228 | | |
229 | | // Header |
230 | 1 | writeln!( |
231 | 1 | out, |
232 | 1 | "╔═══════════════════════════════════════════════════════════════════════╗" |
233 | | ) |
234 | 1 | .expect("failed to write benchmark output"); |
235 | 1 | writeln!( |
236 | 1 | out, |
237 | 1 | "║ INFERENCE BENCHMARK COMPARISON (tok/s GPU) ║" |
238 | | ) |
239 | 1 | .expect("failed to write benchmark output"); |
240 | 1 | writeln!( |
241 | 1 | out, |
242 | 1 | "║ Model: {:30} Quant: {:10} ║", |
243 | 1 | truncate(&self.model_name, 30), |
244 | 1 | truncate(&self.quantization, 10) |
245 | | ) |
246 | 1 | .expect("failed to write benchmark output"); |
247 | 1 | writeln!( |
248 | 1 | out, |
249 | 1 | "║ GPU: {:35} VRAM: {:5.1}GB ║", |
250 | 1 | truncate(&self.gpu_name, 35), |
251 | | self.gpu_vram_gb |
252 | | ) |
253 | 1 | .expect("failed to write benchmark output"); |
254 | 1 | writeln!( |
255 | 1 | out, |
256 | 1 | "╠═══════════════════════════════════════════════════════════════════════╣" |
257 | | ) |
258 | 1 | .expect("failed to write benchmark output"); |
259 | | |
260 | | // Row 1: GGUF comparison |
261 | 1 | writeln!( |
262 | 1 | out, |
263 | 1 | "║ GGUF Format Inference ║" |
264 | | ) |
265 | 1 | .expect("failed to write benchmark output"); |
266 | 1 | writeln!( |
267 | 1 | out, |
268 | 1 | "╠═══════════════════════╦═══════════════════════╦═══════════════════════╣" |
269 | | ) |
270 | 1 | .expect("failed to write benchmark output"); |
271 | 1 | writeln!( |
272 | 1 | out, |
273 | 1 | "║ APR serve GGUF ║ Ollama ║ llama.cpp ║" |
274 | | ) |
275 | 1 | .expect("failed to write benchmark output"); |
276 | 1 | writeln!( |
277 | 1 | out, |
278 | 1 | "╠═══════════════════════╬═══════════════════════╬═══════════════════════╣" |
279 | | ) |
280 | 1 | .expect("failed to write benchmark output"); |
281 | | |
282 | 1 | let gguf_apr_tps = self.gguf_apr.as_ref().map_or(0.0, |m| m.tokens_per_sec); |
283 | 1 | let gguf_ollama_tps = self.gguf_ollama.as_ref().map_or(0.0, |m| m.tokens_per_sec); |
284 | 1 | let gguf_llamacpp_tps = self |
285 | 1 | .gguf_llamacpp |
286 | 1 | .as_ref() |
287 | 1 | .map_or(0.0, |m| m.tokens_per_sec); |
288 | | |
289 | 1 | writeln!( |
290 | 1 | out, |
291 | 1 | "║ {:>8.1} tok/s ║ {:>8.1} tok/s ║ {:>8.1} tok/s ║", |
292 | | gguf_apr_tps, gguf_ollama_tps, gguf_llamacpp_tps |
293 | | ) |
294 | 1 | .expect("failed to write benchmark output"); |
295 | | |
296 | | // Bar visualization |
297 | 1 | let max_tps = [gguf_apr_tps, gguf_ollama_tps, gguf_llamacpp_tps] |
298 | 1 | .iter() |
299 | 1 | .cloned() |
300 | 1 | .fold(1.0, f64::max); |
301 | | |
302 | 1 | writeln!( |
303 | 1 | out, |
304 | 1 | "║ {} ║ {} ║ {} ║", |
305 | 1 | render_bar(gguf_apr_tps, max_tps, 17), |
306 | 1 | render_bar(gguf_ollama_tps, max_tps, 17), |
307 | 1 | render_bar(gguf_llamacpp_tps, max_tps, 17) |
308 | | ) |
309 | 1 | .expect("failed to write benchmark output"); |
310 | | |
311 | | // TTFT |
312 | 1 | let gguf_apr_ttft = self.gguf_apr.as_ref().map_or(0.0, |m| m.ttft_ms); |
313 | 1 | let gguf_ollama_ttft = self.gguf_ollama.as_ref().map_or(0.0, |m| m.ttft_ms); |
314 | 1 | let gguf_llamacpp_ttft = self.gguf_llamacpp.as_ref().map_or(0.0, |m| m.ttft_ms); |
315 | | |
316 | 1 | writeln!( |
317 | 1 | out, |
318 | 1 | "║ TTFT: {:>6.1}ms ║ TTFT: {:>6.1}ms ║ TTFT: {:>6.1}ms ║", |
319 | | gguf_apr_ttft, gguf_ollama_ttft, gguf_llamacpp_ttft |
320 | | ) |
321 | 1 | .expect("failed to write benchmark output"); |
322 | | |
323 | | // Row 2: APR server comparison |
324 | 1 | writeln!( |
325 | 1 | out, |
326 | 1 | "╠═══════════════════════╩═══════════════════════╩═══════════════════════╣" |
327 | | ) |
328 | 1 | .expect("failed to write benchmark output"); |
329 | 1 | writeln!( |
330 | 1 | out, |
331 | 1 | "║ APR Server Format Comparison ║" |
332 | | ) |
333 | 1 | .expect("failed to write benchmark output"); |
334 | 1 | writeln!( |
335 | 1 | out, |
336 | 1 | "╠═══════════════════════╦═══════════════════════╦═══════════════════════╣" |
337 | | ) |
338 | 1 | .expect("failed to write benchmark output"); |
339 | 1 | writeln!( |
340 | 1 | out, |
341 | 1 | "║ APR serve .apr ║ APR serve GGUF ║ Ollama (baseline) ║" |
342 | | ) |
343 | 1 | .expect("failed to write benchmark output"); |
344 | 1 | writeln!( |
345 | 1 | out, |
346 | 1 | "╠═══════════════════════╬═══════════════════════╬═══════════════════════╣" |
347 | | ) |
348 | 1 | .expect("failed to write benchmark output"); |
349 | | |
350 | 1 | let apr_native_tps = self.apr_native.as_ref().map_or(0.0, |m| m.tokens_per_sec); |
351 | 1 | let apr_gguf_tps = self.apr_gguf.as_ref().map_or(0.0, |m| m.tokens_per_sec); |
352 | 1 | let apr_baseline_tps = self.apr_baseline.as_ref().map_or(0.0, |m| m.tokens_per_sec); |
353 | | |
354 | 1 | writeln!( |
355 | 1 | out, |
356 | 1 | "║ {:>8.1} tok/s ║ {:>8.1} tok/s ║ {:>8.1} tok/s ║", |
357 | | apr_native_tps, apr_gguf_tps, apr_baseline_tps |
358 | | ) |
359 | 1 | .expect("failed to write benchmark output"); |
360 | | |
361 | 1 | let max_tps2 = [apr_native_tps, apr_gguf_tps, apr_baseline_tps] |
362 | 1 | .iter() |
363 | 1 | .cloned() |
364 | 1 | .fold(1.0, f64::max); |
365 | | |
366 | 1 | writeln!( |
367 | 1 | out, |
368 | 1 | "║ {} ║ {} ║ {} ║", |
369 | 1 | render_bar(apr_native_tps, max_tps2, 17), |
370 | 1 | render_bar(apr_gguf_tps, max_tps2, 17), |
371 | 1 | render_bar(apr_baseline_tps, max_tps2, 17) |
372 | | ) |
373 | 1 | .expect("failed to write benchmark output"); |
374 | | |
375 | | // Speedup vs baseline |
376 | 1 | let speedup_native = if apr_baseline_tps > 0.0 { |
377 | 1 | apr_native_tps / apr_baseline_tps |
378 | | } else { |
379 | 0 | 0.0 |
380 | | }; |
381 | 1 | let speedup_gguf = if apr_baseline_tps > 0.0 { |
382 | 1 | apr_gguf_tps / apr_baseline_tps |
383 | | } else { |
384 | 0 | 0.0 |
385 | | }; |
386 | | |
387 | 1 | writeln!( |
388 | 1 | out, |
389 | 1 | "║ vs Ollama: {:>5.2}x ║ vs Ollama: {:>5.2}x ║ (baseline) ║", |
390 | | speedup_native, speedup_gguf |
391 | | ) |
392 | 1 | .expect("failed to write benchmark output"); |
393 | | |
394 | 1 | writeln!( |
395 | 1 | out, |
396 | 1 | "╚═══════════════════════╩═══════════════════════╩═══════════════════════╝" |
397 | | ) |
398 | 1 | .expect("failed to write benchmark output"); |
399 | | |
400 | 1 | out |
401 | 1 | } |
402 | | |
403 | | // ======================================================================== |
404 | | // Profiling Log for Chat Paste |
405 | | // ======================================================================== |
406 | | |
407 | | /// Generate profiling log suitable for chat paste |
408 | 1 | pub fn render_profiling_log(&self) -> String { |
409 | 1 | let mut out = String::new(); |
410 | | |
411 | 1 | writeln!(out, "```").expect("failed to write benchmark output"); |
412 | 1 | writeln!( |
413 | 1 | out, |
414 | 1 | "═══════════════════════════════════════════════════════════════════════" |
415 | | ) |
416 | 1 | .expect("failed to write benchmark output"); |
417 | 1 | writeln!(out, "INFERENCE PROFILING REPORT").expect("failed to write benchmark output"); |
418 | 1 | writeln!( |
419 | 1 | out, |
420 | 1 | "═══════════════════════════════════════════════════════════════════════" |
421 | | ) |
422 | 1 | .expect("failed to write benchmark output"); |
423 | 1 | writeln!(out).expect("failed to write benchmark output"); |
424 | | |
425 | | // Model & Hardware |
426 | 1 | writeln!(out, "MODEL: {} ({})", self.model_name, self.model_params) |
427 | 1 | .expect("failed to write benchmark output"); |
428 | 1 | writeln!(out, "QUANT: {}", self.quantization).expect("failed to write benchmark output"); |
429 | 1 | writeln!( |
430 | 1 | out, |
431 | 1 | "GPU: {} ({:.1}GB VRAM)", |
432 | | self.gpu_name, self.gpu_vram_gb |
433 | | ) |
434 | 1 | .expect("failed to write benchmark output"); |
435 | 1 | writeln!(out).expect("failed to write benchmark output"); |
436 | | |
437 | | // Performance Summary |
438 | 1 | writeln!( |
439 | 1 | out, |
440 | 1 | "───────────────────────────────────────────────────────────────────────" |
441 | | ) |
442 | 1 | .expect("failed to write benchmark output"); |
443 | 1 | writeln!(out, "THROUGHPUT COMPARISON (tok/s)").expect("failed to write benchmark output"); |
444 | 1 | writeln!( |
445 | 1 | out, |
446 | 1 | "───────────────────────────────────────────────────────────────────────" |
447 | | ) |
448 | 1 | .expect("failed to write benchmark output"); |
449 | | |
450 | 1 | if let Some(ref m) = self.gguf_apr { |
451 | 1 | writeln!( |
452 | 1 | out, |
453 | 1 | "APR GGUF: {:>8.1} tok/s (TTFT: {:>6.1}ms)", |
454 | 1 | m.tokens_per_sec, m.ttft_ms |
455 | 1 | ) |
456 | 1 | .expect("failed to write benchmark output"); |
457 | 1 | }0 |
458 | 1 | if let Some(ref m0 ) = self.apr_native { |
459 | 0 | writeln!( |
460 | 0 | out, |
461 | 0 | "APR .apr: {:>8.1} tok/s (TTFT: {:>6.1}ms)", |
462 | 0 | m.tokens_per_sec, m.ttft_ms |
463 | 0 | ) |
464 | 0 | .expect("failed to write benchmark output"); |
465 | 1 | } |
466 | 1 | if let Some(ref m0 ) = self.gguf_ollama { |
467 | 0 | writeln!( |
468 | 0 | out, |
469 | 0 | "Ollama: {:>8.1} tok/s (TTFT: {:>6.1}ms)", |
470 | 0 | m.tokens_per_sec, m.ttft_ms |
471 | 0 | ) |
472 | 0 | .expect("failed to write benchmark output"); |
473 | 1 | } |
474 | 1 | if let Some(ref m0 ) = self.gguf_llamacpp { |
475 | 0 | writeln!( |
476 | 0 | out, |
477 | 0 | "llama.cpp: {:>8.1} tok/s (TTFT: {:>6.1}ms)", |
478 | 0 | m.tokens_per_sec, m.ttft_ms |
479 | 0 | ) |
480 | 0 | .expect("failed to write benchmark output"); |
481 | 1 | } |
482 | 1 | writeln!(out).expect("failed to write benchmark output"); |
483 | | |
484 | | // Speedup Analysis |
485 | 1 | writeln!( |
486 | 1 | out, |
487 | 1 | "───────────────────────────────────────────────────────────────────────" |
488 | | ) |
489 | 1 | .expect("failed to write benchmark output"); |
490 | 1 | writeln!(out, "SPEEDUP ANALYSIS").expect("failed to write benchmark output"); |
491 | 1 | writeln!( |
492 | 1 | out, |
493 | 1 | "───────────────────────────────────────────────────────────────────────" |
494 | | ) |
495 | 1 | .expect("failed to write benchmark output"); |
496 | | |
497 | 1 | let ollama_tps = self |
498 | 1 | .gguf_ollama |
499 | 1 | .as_ref() |
500 | 1 | .map_or(318.0, |m| m.tokens_per_sec); |
501 | 1 | let llamacpp_tps = self |
502 | 1 | .gguf_llamacpp |
503 | 1 | .as_ref() |
504 | 1 | .map_or(200.0, |m| m.tokens_per_sec); |
505 | | |
506 | 1 | if let Some(ref m) = self.gguf_apr { |
507 | 1 | let vs_ollama = m.tokens_per_sec / ollama_tps; |
508 | 1 | let vs_llamacpp = m.tokens_per_sec / llamacpp_tps; |
509 | 1 | writeln!( |
510 | 1 | out, |
511 | 1 | "APR GGUF vs Ollama: {:>5.2}x {}", |
512 | | vs_ollama, |
513 | 1 | if vs_ollama >= 1.0 { "✓" } else { "⚠"0 } |
514 | | ) |
515 | 1 | .expect("failed to write benchmark output"); |
516 | 1 | writeln!( |
517 | 1 | out, |
518 | 1 | "APR GGUF vs llama.cpp: {:>5.2}x {}", |
519 | | vs_llamacpp, |
520 | 1 | if vs_llamacpp >= 1.25 { |
521 | 1 | "✓ Point 41 PASS" |
522 | | } else { |
523 | 0 | "⚠ Point 41 FAIL" |
524 | | } |
525 | | ) |
526 | 1 | .expect("failed to write benchmark output"); |
527 | 0 | } |
528 | | |
529 | 1 | if let Some(ref m0 ) = self.apr_native { |
530 | 0 | let vs_ollama = m.tokens_per_sec / ollama_tps; |
531 | 0 | writeln!( |
532 | 0 | out, |
533 | 0 | "APR .apr vs Ollama: {:>5.2}x {}", |
534 | | vs_ollama, |
535 | 0 | if vs_ollama >= 2.0 { |
536 | 0 | "✓ 2x target" |
537 | | } else { |
538 | 0 | "" |
539 | | } |
540 | | ) |
541 | 0 | .expect("failed to write benchmark output"); |
542 | 1 | } |
543 | 1 | writeln!(out).expect("failed to write benchmark output"); |
544 | | |
545 | | // Profiling Hotspots |
546 | 1 | if !self.hotspots.is_empty() { |
547 | 1 | writeln!( |
548 | 1 | out, |
549 | 1 | "───────────────────────────────────────────────────────────────────────" |
550 | | ) |
551 | 1 | .expect("failed to write benchmark output"); |
552 | 1 | writeln!(out, "PROFILING HOTSPOTS (>5% of execution time)") |
553 | 1 | .expect("failed to write benchmark output"); |
554 | 1 | writeln!( |
555 | 1 | out, |
556 | 1 | "───────────────────────────────────────────────────────────────────────" |
557 | | ) |
558 | 1 | .expect("failed to write benchmark output"); |
559 | | |
560 | 2 | for hotspot1 in &self.hotspots { |
561 | 1 | writeln!(out, "{}", hotspot.to_line()).expect("failed to write benchmark output"); |
562 | 1 | if !hotspot.explanation.is_empty() { |
563 | 1 | writeln!(out, " └─ {}", hotspot.explanation) |
564 | 1 | .expect("failed to write benchmark output"); |
565 | 1 | }0 |
566 | | } |
567 | 1 | writeln!(out).expect("failed to write benchmark output"); |
568 | 0 | } |
569 | | |
570 | | // GPU Metrics |
571 | 1 | writeln!( |
572 | 1 | out, |
573 | 1 | "───────────────────────────────────────────────────────────────────────" |
574 | | ) |
575 | 1 | .expect("failed to write benchmark output"); |
576 | 1 | writeln!(out, "GPU METRICS").expect("failed to write benchmark output"); |
577 | 1 | writeln!( |
578 | 1 | out, |
579 | 1 | "───────────────────────────────────────────────────────────────────────" |
580 | | ) |
581 | 1 | .expect("failed to write benchmark output"); |
582 | | |
583 | 1 | if let Some(ref m) = self.gguf_apr { |
584 | 1 | if let (Some(util), Some(mem)) = (m.gpu_util, m.gpu_mem_mb) { |
585 | 1 | writeln!( |
586 | 1 | out, |
587 | 1 | "APR GGUF: GPU Util: {:>5.1}% VRAM: {:>6.0}MB", |
588 | 1 | util, mem |
589 | 1 | ) |
590 | 1 | .expect("failed to write benchmark output"); |
591 | 1 | }0 |
592 | 0 | } |
593 | 1 | if let Some(ref m0 ) = self.apr_native { |
594 | 0 | if let (Some(util), Some(mem)) = (m.gpu_util, m.gpu_mem_mb) { |
595 | 0 | writeln!( |
596 | 0 | out, |
597 | 0 | "APR .apr: GPU Util: {:>5.1}% VRAM: {:>6.0}MB", |
598 | 0 | util, mem |
599 | 0 | ) |
600 | 0 | .expect("failed to write benchmark output"); |
601 | 0 | } |
602 | 1 | } |
603 | 1 | writeln!(out).expect("failed to write benchmark output"); |
604 | | |
605 | | // Recommendations |
606 | 1 | writeln!( |
607 | 1 | out, |
608 | 1 | "───────────────────────────────────────────────────────────────────────" |
609 | | ) |
610 | 1 | .expect("failed to write benchmark output"); |
611 | 1 | writeln!(out, "OPTIMIZATION RECOMMENDATIONS").expect("failed to write benchmark output"); |
612 | 1 | writeln!( |
613 | 1 | out, |
614 | 1 | "───────────────────────────────────────────────────────────────────────" |
615 | | ) |
616 | 1 | .expect("failed to write benchmark output"); |
617 | | |
618 | 1 | let unexpected: Vec<_> = self.hotspots.iter().filter(|h| !h.is_expected).collect(); |
619 | 1 | if unexpected.is_empty() { |
620 | 1 | writeln!(out, "✓ No unexpected hotspots detected") |
621 | 1 | .expect("failed to write benchmark output"); |
622 | 1 | } else { |
623 | 0 | for h in unexpected { |
624 | 0 | writeln!(out, "⚠ {}: {}", h.component, h.explanation) |
625 | 0 | .expect("failed to write benchmark output"); |
626 | 0 | } |
627 | | } |
628 | | |
629 | | // Phase 2 status |
630 | 1 | let apr_tps = self.gguf_apr.as_ref().map_or(0.0, |m| m.tokens_per_sec); |
631 | 1 | if apr_tps < 500.0 { |
632 | 0 | writeln!(out).expect("failed to write benchmark output"); |
633 | 0 | writeln!(out, "Phase 2 Optimizations (projected 3.28x improvement):") |
634 | 0 | .expect("failed to write benchmark output"); |
635 | 0 | writeln!(out, " PAR-036: Persistent threads (1.3x)") |
636 | 0 | .expect("failed to write benchmark output"); |
637 | 0 | writeln!(out, " PAR-037: CUDA graph capture (1.5x)") |
638 | 0 | .expect("failed to write benchmark output"); |
639 | 0 | writeln!(out, " PAR-038: Multi-stream pipeline (1.2x)") |
640 | 0 | .expect("failed to write benchmark output"); |
641 | 0 | writeln!(out, " PAR-039: Megakernel fusion (1.4x)") |
642 | 0 | .expect("failed to write benchmark output"); |
643 | 0 | writeln!( |
644 | 0 | out, |
645 | 0 | " Projected: {:.1} × 3.28 = {:.1} tok/s", |
646 | 0 | apr_tps, |
647 | 0 | apr_tps * 3.28 |
648 | 0 | ) |
649 | 0 | .expect("failed to write benchmark output"); |
650 | 1 | } |
651 | | |
652 | 1 | writeln!( |
653 | 1 | out, |
654 | 1 | "═══════════════════════════════════════════════════════════════════════" |
655 | | ) |
656 | 1 | .expect("failed to write benchmark output"); |
657 | 1 | writeln!(out, "```").expect("failed to write benchmark output"); |
658 | | |
659 | 1 | out |
660 | 1 | } |
661 | | |
662 | | /// Generate compact one-liner for quick comparison |
663 | 1 | pub fn render_compact(&self) -> String { |
664 | 1 | let apr_tps = self.gguf_apr.as_ref().map_or(0.0, |m| m.tokens_per_sec); |
665 | 1 | let ollama_tps = self.gguf_ollama.as_ref().map_or(0.0, |m| m.tokens_per_sec); |
666 | 1 | let llamacpp_tps = self |
667 | 1 | .gguf_llamacpp |
668 | 1 | .as_ref() |
669 | 1 | .map_or(0.0, |m| m.tokens_per_sec); |
670 | | |
671 | 1 | format!( |
672 | 1 | "APR:{:.0} Ollama:{:.0} llama.cpp:{:.0} tok/s | APR vs Ollama:{:.2}x vs llama.cpp:{:.2}x", |
673 | | apr_tps, ollama_tps, llamacpp_tps, |
674 | 1 | apr_tps / ollama_tps.max(1.0), |
675 | 1 | apr_tps / llamacpp_tps.max(1.0) |
676 | | ) |
677 | 1 | } |
678 | | } |
679 | | |
680 | | // ============================================================================ |
681 | | // Benchmark Runner |
682 | | // ============================================================================ |
683 | | |
684 | | /// Benchmark runner with profiling |
685 | | #[derive(Debug)] |
686 | | pub struct BenchmarkRunner { |
687 | | /// Results grid |
688 | | pub grid: BenchmarkGrid, |
689 | | /// Profiling start time |
690 | | start_time: Option<Instant>, |
691 | | /// Component timings |
692 | | component_times: Vec<(String, Duration, u64)>, |
693 | | } |
694 | | |
695 | | impl Default for BenchmarkRunner { |
696 | 0 | fn default() -> Self { |
697 | 0 | Self::new() |
698 | 0 | } |
699 | | } |
700 | | |
701 | | impl BenchmarkRunner { |
702 | | /// Create new benchmark runner |
703 | 1 | pub fn new() -> Self { |
704 | 1 | Self { |
705 | 1 | grid: BenchmarkGrid::new(), |
706 | 1 | start_time: None, |
707 | 1 | component_times: Vec::new(), |
708 | 1 | } |
709 | 1 | } |
710 | | |
711 | | /// Start profiling |
712 | 1 | pub fn start(&mut self) { |
713 | 1 | self.start_time = Some(Instant::now()); |
714 | 1 | } |
715 | | |
716 | | /// Record component timing |
717 | 3 | pub fn record_component(&mut self, name: &str, duration: Duration, calls: u64) { |
718 | 3 | self.component_times |
719 | 3 | .push((name.to_string(), duration, calls)); |
720 | 3 | } |
721 | | |
722 | | /// Measure a component |
723 | 0 | pub fn measure<F, R>(&mut self, name: &str, f: F) -> R |
724 | 0 | where |
725 | 0 | F: FnOnce() -> R, |
726 | | { |
727 | 0 | let start = Instant::now(); |
728 | 0 | let result = f(); |
729 | 0 | self.record_component(name, start.elapsed(), 1); |
730 | 0 | result |
731 | 0 | } |
732 | | |
733 | | /// Finalize and compute hotspots |
734 | 1 | pub fn finalize(&mut self) { |
735 | 1 | let total_time: Duration = self.component_times.iter().map(|(_, d, _)| *d).sum(); |
736 | 1 | let total_nanos = total_time.as_nanos() as f64; |
737 | | |
738 | 1 | if total_nanos == 0.0 { |
739 | 0 | return; |
740 | 1 | } |
741 | | |
742 | 4 | for (name3 , duration3 , calls3 ) in &self.component_times { |
743 | 3 | let percentage = (duration.as_nanos() as f64 / total_nanos) * 100.0; |
744 | | |
745 | 3 | if percentage > 5.0 { |
746 | 3 | let avg_per_call = if *calls > 0 { |
747 | 3 | Duration::from_nanos((duration.as_nanos() / *calls as u128) as u64) |
748 | | } else { |
749 | 0 | Duration::ZERO |
750 | | }; |
751 | | |
752 | 3 | let (explanation, is_expected) = explain_inference_hotspot(name, percentage); |
753 | | |
754 | 3 | self.grid.add_hotspot(ProfilingHotspot { |
755 | 3 | component: name.clone(), |
756 | 3 | time: *duration, |
757 | 3 | percentage, |
758 | 3 | call_count: *calls, |
759 | 3 | avg_per_call, |
760 | 3 | explanation, |
761 | 3 | is_expected, |
762 | 3 | }); |
763 | 0 | } |
764 | | } |
765 | | |
766 | | // Sort by percentage descending |
767 | 2 | self.grid.hotspots1 .sort_by1 (|a, b| { |
768 | 2 | b.percentage |
769 | 2 | .partial_cmp(&a.percentage) |
770 | 2 | .unwrap_or(std::cmp::Ordering::Equal) |
771 | 2 | }); |
772 | 1 | } |
773 | | } |
774 | | |
775 | | // ============================================================================ |
776 | | // Helper Functions |
777 | | // ============================================================================ |
778 | | |
779 | | /// Render ASCII bar |
780 | 10 | fn render_bar(value: f64, max: f64, width: usize) -> String { |
781 | 10 | let ratio = if max > 0.0 { value / max } else { 0.00 }; |
782 | 10 | let filled = ((ratio * width as f64) as usize).min(width); |
783 | 10 | let empty = width - filled; |
784 | | |
785 | 10 | format!("{}{}", "█".repeat(filled), "░".repeat(empty)) |
786 | 10 | } |
787 | | |
788 | | /// Truncate string to max length |
789 | 6 | fn truncate(s: &str, max_len: usize) -> &str { |
790 | 6 | if s.len() <= max_len { |
791 | 5 | s |
792 | | } else { |
793 | 1 | &s[..max_len] |
794 | | } |
795 | 6 | } |
796 | | |
797 | | /// Explain inference hotspot |
798 | 7 | fn explain_inference_hotspot(component: &str, percentage: f64) -> (String, bool) { |
799 | 7 | match component { |
800 | 7 | "Q4K_GEMV" | "MatMul"5 | "GEMM"5 => ( |
801 | 2 | format!( |
802 | 2 | "Matrix ops dominate ({:.1}%) - expected for transformer inference", |
803 | 2 | percentage |
804 | 2 | ), |
805 | 2 | true, |
806 | 2 | ), |
807 | 5 | "Attention" | "FlashAttention"3 => ( |
808 | 2 | format!( |
809 | 2 | "Attention at {:.1}% - normal for autoregressive decoding", |
810 | 2 | percentage |
811 | 2 | ), |
812 | 2 | true, |
813 | 2 | ), |
814 | 3 | "KV_Cache" | "KVCache" => { |
815 | 0 | if percentage > 20.0 { |
816 | 0 | ( |
817 | 0 | "KV cache overhead high - consider FP16 cache or graph capture".to_string(), |
818 | 0 | false, |
819 | 0 | ) |
820 | | } else { |
821 | 0 | ("KV cache within normal range".to_string(), true) |
822 | | } |
823 | | }, |
824 | 3 | "Softmax" => { |
825 | 0 | if percentage > 10.0 { |
826 | 0 | ( |
827 | 0 | "Softmax unusually high - check for redundant computations".to_string(), |
828 | 0 | false, |
829 | 0 | ) |
830 | | } else { |
831 | 0 | ("Softmax within normal range".to_string(), true) |
832 | | } |
833 | | }, |
834 | 3 | "RMSNorm" | "LayerNorm" => { |
835 | 0 | if percentage > 15.0 { |
836 | 0 | ( |
837 | 0 | "Normalization overhead high - consider fused kernels".to_string(), |
838 | 0 | false, |
839 | 0 | ) |
840 | | } else { |
841 | 0 | ("Normalization within normal range".to_string(), true) |
842 | | } |
843 | | }, |
844 | 3 | "MemcpyH2D" | "MemcpyD2H" | "Transfer" => ( |
845 | 0 | "Memory transfer - consider persistent GPU buffers".to_string(), |
846 | 0 | false, |
847 | 0 | ), |
848 | 3 | "KernelLaunch" => ( |
849 | 0 | "Kernel launch overhead - consider CUDA graphs or megakernels".to_string(), |
850 | 0 | false, |
851 | 0 | ), |
852 | 3 | "Embedding" => ( |
853 | 0 | "Embedding lookup - expected at start of inference".to_string(), |
854 | 0 | true, |
855 | 0 | ), |
856 | 3 | "Sampling" | "TopK" | "TopP" => ( |
857 | 0 | "Sampling overhead - expected for token generation".to_string(), |
858 | 0 | true, |
859 | 0 | ), |
860 | | _ => { |
861 | 3 | if percentage > 20.0 { |
862 | 1 | ( |
863 | 1 | format!("Unknown component at {:.1}% - investigate", percentage), |
864 | 1 | false, |
865 | 1 | ) |
866 | | } else { |
867 | 2 | (String::new(), true) |
868 | | } |
869 | | }, |
870 | | } |
871 | 7 | } |
872 | | |
873 | | // ============================================================================ |
874 | | // Tests |
875 | | // ============================================================================ |
876 | | |
877 | | #[cfg(test)] |
878 | | mod tests { |
879 | | use super::*; |
880 | | |
881 | | #[test] |
882 | 1 | fn test_benchmark_grid_ascii() { |
883 | 1 | let mut grid = BenchmarkGrid::new() |
884 | 1 | .with_model("Qwen2.5-Coder-0.5B", "0.5B", "Q4_K_M") |
885 | 1 | .with_gpu("RTX 4090", 24.0); |
886 | | |
887 | 1 | grid.set_gguf_row( |
888 | 1 | BenchMeasurement::new("APR", "GGUF") |
889 | 1 | .with_throughput(500.0) |
890 | 1 | .with_ttft(7.0), |
891 | 1 | BenchMeasurement::new("Ollama", "GGUF") |
892 | 1 | .with_throughput(318.0) |
893 | 1 | .with_ttft(50.0), |
894 | 1 | BenchMeasurement::new("llama.cpp", "GGUF") |
895 | 1 | .with_throughput(200.0) |
896 | 1 | .with_ttft(30.0), |
897 | | ); |
898 | | |
899 | 1 | grid.set_apr_row( |
900 | 1 | BenchMeasurement::new("APR", ".apr") |
901 | 1 | .with_throughput(600.0) |
902 | 1 | .with_ttft(5.0), |
903 | 1 | BenchMeasurement::new("APR", "GGUF") |
904 | 1 | .with_throughput(500.0) |
905 | 1 | .with_ttft(7.0), |
906 | 1 | BenchMeasurement::new("Ollama", "GGUF") |
907 | 1 | .with_throughput(318.0) |
908 | 1 | .with_ttft(50.0), |
909 | | ); |
910 | | |
911 | 1 | let ascii = grid.render_ascii(); |
912 | 1 | assert!(ascii.contains("APR serve GGUF")); |
913 | 1 | assert!(ascii.contains("Ollama")); |
914 | 1 | assert!(ascii.contains("llama.cpp")); |
915 | 1 | assert!(ascii.contains("500.0 tok/s")); |
916 | 1 | } |
917 | | |
918 | | #[test] |
919 | 1 | fn test_profiling_log() { |
920 | 1 | let mut grid = BenchmarkGrid::new() |
921 | 1 | .with_model("Qwen2.5-Coder-0.5B", "0.5B", "Q4_K_M") |
922 | 1 | .with_gpu("RTX 4090", 24.0); |
923 | | |
924 | 1 | grid.gguf_apr = Some( |
925 | 1 | BenchMeasurement::new("APR", "GGUF") |
926 | 1 | .with_throughput(500.0) |
927 | 1 | .with_ttft(7.0) |
928 | 1 | .with_gpu(95.0, 2048.0), |
929 | 1 | ); |
930 | | |
931 | 1 | grid.add_hotspot(ProfilingHotspot { |
932 | 1 | component: "Q4K_GEMV".to_string(), |
933 | 1 | time: Duration::from_millis(150), |
934 | 1 | percentage: 45.0, |
935 | 1 | call_count: 1000, |
936 | 1 | avg_per_call: Duration::from_micros(150), |
937 | 1 | explanation: "Matrix ops dominate - expected".to_string(), |
938 | 1 | is_expected: true, |
939 | 1 | }); |
940 | | |
941 | 1 | let log = grid.render_profiling_log(); |
942 | 1 | assert!(log.contains("PROFILING REPORT")); |
943 | 1 | assert!(log.contains("Q4K_GEMV")); |
944 | 1 | assert!(log.contains("45.0%")); |
945 | 1 | } |
946 | | |
947 | | #[test] |
948 | 1 | fn test_compact_output() { |
949 | 1 | let mut grid = BenchmarkGrid::new(); |
950 | 1 | grid.gguf_apr = Some(BenchMeasurement::new("APR", "GGUF").with_throughput(500.0)); |
951 | 1 | grid.gguf_ollama = Some(BenchMeasurement::new("Ollama", "GGUF").with_throughput(318.0)); |
952 | 1 | grid.gguf_llamacpp = |
953 | 1 | Some(BenchMeasurement::new("llama.cpp", "GGUF").with_throughput(200.0)); |
954 | | |
955 | 1 | let compact = grid.render_compact(); |
956 | 1 | assert!(compact.contains("APR:500")); |
957 | 1 | assert!(compact.contains("vs llama.cpp:2.50x")); |
958 | 1 | } |
959 | | |
960 | | #[test] |
961 | 1 | fn test_runner_profiling() { |
962 | 1 | let mut runner = BenchmarkRunner::new(); |
963 | 1 | runner.start(); |
964 | | |
965 | 1 | runner.record_component("Q4K_GEMV", Duration::from_millis(100), 500); |
966 | 1 | runner.record_component("Attention", Duration::from_millis(50), 500); |
967 | 1 | runner.record_component("Other", Duration::from_millis(10), 100); |
968 | | |
969 | 1 | runner.finalize(); |
970 | | |
971 | 1 | assert!(!runner.grid.hotspots.is_empty()); |
972 | 1 | assert_eq!(runner.grid.hotspots[0].component, "Q4K_GEMV"); |
973 | 1 | } |
974 | | |
975 | | #[test] |
976 | 1 | fn test_render_bar() { |
977 | 1 | let bar = render_bar(50.0, 100.0, 10); |
978 | 10 | assert_eq!1 (bar.chars()1 .filter1 (|c| *c == '█').count1 (), 5); |
979 | 10 | assert_eq!1 (bar.chars()1 .filter1 (|c| *c == '░').count1 (), 5); |
980 | 1 | } |
981 | | |
982 | | // ========================================================================= |
983 | | // Coverage Tests: BenchMeasurement |
984 | | // ========================================================================= |
985 | | |
986 | | #[test] |
987 | 1 | fn test_bench_measurement_new() { |
988 | 1 | let m = BenchMeasurement::new("TestEngine", "TestFormat"); |
989 | 1 | assert_eq!(m.engine, "TestEngine"); |
990 | 1 | assert_eq!(m.format, "TestFormat"); |
991 | 1 | assert_eq!(m.tokens_per_sec, 0.0); |
992 | 1 | assert_eq!(m.ttft_ms, 0.0); |
993 | 1 | assert_eq!(m.tokens_generated, 0); |
994 | 1 | assert!(m.gpu_util.is_none()); |
995 | 1 | assert!(m.gpu_mem_mb.is_none()); |
996 | 1 | } |
997 | | |
998 | | #[test] |
999 | 1 | fn test_bench_measurement_with_throughput() { |
1000 | 1 | let m = BenchMeasurement::new("APR", "GGUF").with_throughput(100.0); |
1001 | 1 | assert_eq!(m.tokens_per_sec, 100.0); |
1002 | 1 | } |
1003 | | |
1004 | | #[test] |
1005 | 1 | fn test_bench_measurement_with_ttft() { |
1006 | 1 | let m = BenchMeasurement::new("APR", "GGUF").with_ttft(25.5); |
1007 | 1 | assert_eq!(m.ttft_ms, 25.5); |
1008 | 1 | } |
1009 | | |
1010 | | #[test] |
1011 | 1 | fn test_bench_measurement_with_tokens() { |
1012 | 1 | let duration = Duration::from_secs(2); |
1013 | 1 | let m = BenchMeasurement::new("APR", "GGUF").with_tokens(200, duration); |
1014 | 1 | assert_eq!(m.tokens_generated, 200); |
1015 | 1 | assert_eq!(m.duration, duration); |
1016 | 1 | assert!((m.tokens_per_sec - 100.0).abs() < 0.1); |
1017 | 1 | } |
1018 | | |
1019 | | #[test] |
1020 | 1 | fn test_bench_measurement_with_tokens_zero_duration() { |
1021 | 1 | let m = BenchMeasurement::new("APR", "GGUF").with_tokens(100, Duration::ZERO); |
1022 | 1 | assert_eq!(m.tokens_generated, 100); |
1023 | | // Zero duration means no TPS calculation |
1024 | 1 | } |
1025 | | |
1026 | | #[test] |
1027 | 1 | fn test_bench_measurement_with_gpu() { |
1028 | 1 | let m = BenchMeasurement::new("APR", "GGUF").with_gpu(95.0, 4096.0); |
1029 | 1 | assert_eq!(m.gpu_util, Some(95.0)); |
1030 | 1 | assert_eq!(m.gpu_mem_mb, Some(4096.0)); |
1031 | 1 | } |
1032 | | |
1033 | | #[test] |
1034 | 1 | fn test_bench_measurement_debug() { |
1035 | 1 | let m = BenchMeasurement::new("APR", "GGUF").with_throughput(100.0); |
1036 | 1 | let debug_str = format!("{:?}", m); |
1037 | 1 | assert!(debug_str.contains("BenchMeasurement")); |
1038 | 1 | assert!(debug_str.contains("APR")); |
1039 | 1 | } |
1040 | | |
1041 | | #[test] |
1042 | 1 | fn test_bench_measurement_clone() { |
1043 | 1 | let m = BenchMeasurement::new("APR", "GGUF") |
1044 | 1 | .with_throughput(100.0) |
1045 | 1 | .with_gpu(90.0, 1024.0); |
1046 | 1 | let cloned = m.clone(); |
1047 | 1 | assert_eq!(cloned.engine, m.engine); |
1048 | 1 | assert_eq!(cloned.tokens_per_sec, m.tokens_per_sec); |
1049 | 1 | assert_eq!(cloned.gpu_util, m.gpu_util); |
1050 | 1 | } |
1051 | | |
1052 | | // ========================================================================= |
1053 | | // Coverage Tests: ProfilingHotspot |
1054 | | // ========================================================================= |
1055 | | |
1056 | | #[test] |
1057 | 1 | fn test_profiling_hotspot_debug() { |
1058 | 1 | let hotspot = ProfilingHotspot { |
1059 | 1 | component: "Attention".to_string(), |
1060 | 1 | time: Duration::from_millis(100), |
1061 | 1 | percentage: 50.0, |
1062 | 1 | call_count: 1000, |
1063 | 1 | avg_per_call: Duration::from_micros(100), |
1064 | 1 | explanation: "Expected".to_string(), |
1065 | 1 | is_expected: true, |
1066 | 1 | }; |
1067 | 1 | let debug_str = format!("{:?}", hotspot); |
1068 | 1 | assert!(debug_str.contains("ProfilingHotspot")); |
1069 | 1 | assert!(debug_str.contains("Attention")); |
1070 | 1 | } |
1071 | | |
1072 | | #[test] |
1073 | 1 | fn test_profiling_hotspot_clone() { |
1074 | 1 | let hotspot = ProfilingHotspot { |
1075 | 1 | component: "GEMM".to_string(), |
1076 | 1 | time: Duration::from_millis(200), |
1077 | 1 | percentage: 75.0, |
1078 | 1 | call_count: 500, |
1079 | 1 | avg_per_call: Duration::from_micros(400), |
1080 | 1 | explanation: "Matrix multiplication".to_string(), |
1081 | 1 | is_expected: true, |
1082 | 1 | }; |
1083 | 1 | let cloned = hotspot.clone(); |
1084 | 1 | assert_eq!(cloned.component, hotspot.component); |
1085 | 1 | assert_eq!(cloned.percentage, hotspot.percentage); |
1086 | 1 | } |
1087 | | |
1088 | | // ========================================================================= |
1089 | | // Coverage Tests: BenchmarkGrid |
1090 | | // ========================================================================= |
1091 | | |
1092 | | #[test] |
1093 | 1 | fn test_benchmark_grid_new() { |
1094 | 1 | let grid = BenchmarkGrid::new(); |
1095 | 1 | assert!(grid.gguf_apr.is_none()); |
1096 | 1 | assert!(grid.gguf_ollama.is_none()); |
1097 | 1 | assert!(grid.gguf_llamacpp.is_none()); |
1098 | 1 | assert!(grid.hotspots.is_empty()); |
1099 | 1 | } |
1100 | | |
1101 | | #[test] |
1102 | 1 | fn test_benchmark_grid_with_model() { |
1103 | 1 | let grid = BenchmarkGrid::new().with_model("Llama-7B", "7B", "Q4_K_M"); |
1104 | 1 | assert_eq!(grid.model_name, "Llama-7B"); |
1105 | 1 | assert_eq!(grid.model_params, "7B"); |
1106 | 1 | assert_eq!(grid.quantization, "Q4_K_M"); |
1107 | 1 | } |
1108 | | |
1109 | | #[test] |
1110 | 1 | fn test_benchmark_grid_with_gpu() { |
1111 | 1 | let grid = BenchmarkGrid::new().with_gpu("RTX 3090", 24.0); |
1112 | 1 | assert_eq!(grid.gpu_name, "RTX 3090"); |
1113 | 1 | assert_eq!(grid.gpu_vram_gb, 24.0); |
1114 | 1 | } |
1115 | | |
1116 | | #[test] |
1117 | 1 | fn test_benchmark_grid_add_hotspot() { |
1118 | 1 | let mut grid = BenchmarkGrid::new(); |
1119 | 1 | grid.add_hotspot(ProfilingHotspot { |
1120 | 1 | component: "Test".to_string(), |
1121 | 1 | time: Duration::from_millis(50), |
1122 | 1 | percentage: 25.0, |
1123 | 1 | call_count: 100, |
1124 | 1 | avg_per_call: Duration::from_micros(500), |
1125 | 1 | explanation: "Test hotspot".to_string(), |
1126 | 1 | is_expected: true, |
1127 | 1 | }); |
1128 | 1 | assert_eq!(grid.hotspots.len(), 1); |
1129 | 1 | assert_eq!(grid.hotspots[0].component, "Test"); |
1130 | 1 | } |
1131 | | |
1132 | | // ========================================================================= |
1133 | | // Coverage Tests: render_bar edge cases |
1134 | | // ========================================================================= |
1135 | | |
1136 | | #[test] |
1137 | 1 | fn test_render_bar_zero() { |
1138 | 1 | let bar = render_bar(0.0, 100.0, 10); |
1139 | 10 | assert_eq!1 (bar.chars()1 .filter1 (|c| *c == '█').count1 (), 0); |
1140 | 10 | assert_eq!1 (bar.chars()1 .filter1 (|c| *c == '░').count1 (), 10); |
1141 | 1 | } |
1142 | | |
1143 | | #[test] |
1144 | 1 | fn test_render_bar_full() { |
1145 | 1 | let bar = render_bar(100.0, 100.0, 10); |
1146 | 10 | assert_eq!1 (bar.chars()1 .filter1 (|c| *c == '█').count1 (), 10); |
1147 | 10 | assert_eq!1 (bar.chars()1 .filter1 (|c| *c == '░').count1 (), 0); |
1148 | 1 | } |
1149 | | |
1150 | | #[test] |
1151 | 1 | fn test_render_bar_over_max() { |
1152 | 1 | let bar = render_bar(150.0, 100.0, 10); |
1153 | | // Should clamp to max |
1154 | 10 | assert_eq!1 (bar.chars()1 .filter1 (|c| *c == '█').count1 (), 10); |
1155 | 1 | } |
1156 | | |
1157 | | // ========================================================================= |
1158 | | // Coverage Tests: truncate |
1159 | | // ========================================================================= |
1160 | | |
1161 | | #[test] |
1162 | 1 | fn test_truncate_short_string() { |
1163 | 1 | let result = truncate("short", 10); |
1164 | 1 | assert_eq!(result, "short"); |
1165 | 1 | } |
1166 | | |
1167 | | #[test] |
1168 | 1 | fn test_truncate_exact_length() { |
1169 | 1 | let result = truncate("exactly10c", 10); |
1170 | 1 | assert_eq!(result, "exactly10c"); |
1171 | 1 | } |
1172 | | |
1173 | | #[test] |
1174 | 1 | fn test_truncate_long_string() { |
1175 | 1 | let result = truncate("this is a very long string", 10); |
1176 | 1 | assert_eq!(result.len(), 10); |
1177 | 1 | } |
1178 | | |
1179 | | // ========================================================================= |
1180 | | // Coverage Tests: explain_inference_hotspot |
1181 | | // ========================================================================= |
1182 | | |
1183 | | #[test] |
1184 | 1 | fn test_explain_inference_hotspot_gemv() { |
1185 | 1 | let (explanation, is_expected) = explain_inference_hotspot("Q4K_GEMV", 50.0); |
1186 | 1 | assert!(is_expected); |
1187 | 1 | assert!(!explanation.is_empty()); |
1188 | 1 | } |
1189 | | |
1190 | | #[test] |
1191 | 1 | fn test_explain_inference_hotspot_attention() { |
1192 | 1 | let (explanation, is_expected) = explain_inference_hotspot("Attention", 30.0); |
1193 | 1 | assert!(is_expected); |
1194 | 1 | assert!(!explanation.is_empty()); |
1195 | 1 | } |
1196 | | |
1197 | | #[test] |
1198 | 1 | fn test_explain_inference_hotspot_unknown() { |
1199 | 1 | let (explanation, is_expected) = explain_inference_hotspot("UnknownComponent", 60.0); |
1200 | | // High percentage for unknown component is unexpected |
1201 | 1 | assert!(!is_expected); |
1202 | 1 | assert!(!explanation.is_empty()); |
1203 | 1 | } |
1204 | | |
1205 | | #[test] |
1206 | 1 | fn test_explain_inference_hotspot_low_percentage() { |
1207 | 1 | let (explanation, is_expected) = explain_inference_hotspot("SomeComponent", 5.0); |
1208 | | // Low percentage unknown component returns empty string and is expected |
1209 | 1 | assert!(is_expected); |
1210 | | // Note: The function returns empty string for low percentage unknown components |
1211 | | // which means "nothing to report" - this is valid behavior |
1212 | 1 | let _ = explanation; |
1213 | 1 | } |
1214 | | } |