1use crate::benchmark_logs::{
5 benchmark_log_directories, collect_matching_logs, extract_benchmark_json_string,
6 find_latest_log_in_dirs, is_json_log,
7};
8use chrono::Local;
9use ratatui::{
10 layout::{Alignment, Constraint, Direction, Layout, Rect},
11 style::{Color, Modifier, Style},
12 symbols,
13 text::{Line, Span, Text},
14 widgets::{Axis, Block, Borders, Chart, Dataset, GraphType, Paragraph, Tabs},
15 Frame,
16};
17use serde_json;
18use std::fs;
19use std::path::{Path, PathBuf};
20use std::time::{Duration, SystemTime};
21
22const STALE_FULL_BENCHMARK_WINDOW: Duration = Duration::from_secs(15 * 60);
23const STALE_ERROR_WINDOW: Duration = Duration::from_secs(15 * 60);
24
25#[derive(Debug, Clone)]
26pub struct BenchmarkResults {
27 pub gpus: Vec<GpuInfo>,
28 pub memory_bandwidth: Option<MemoryBandwidthData>,
29 pub tensor_core: Option<TensorCoreData>,
30 pub pytorch: Option<PyTorchData>,
31 pub llama_cpp: Option<LlamaCppData>,
32 pub flash_attention: Option<FlashAttentionData>,
33 pub vllm: Option<VllmData>,
34 pub deepspeed: Option<DeepspeedData>,
35 pub megatron: Option<MegatronData>,
36 pub onnx: Option<OnnxData>,
37 pub errors: Vec<String>,
38 pub baseline: Option<Box<BenchmarkResults>>,
39}
40
41#[derive(Debug, Clone)]
42pub struct DeepspeedData {
43 pub throughput_samples_per_sec: f64,
44 pub avg_latency_ms: f64,
45 pub stage: u32,
46 pub accelerator: String,
47 pub samples: Vec<f64>,
48}
49
50#[derive(Debug, Clone)]
51pub struct MegatronData {
52 pub throughput_samples_per_sec: f64,
53 pub avg_latency_ms: f64,
54 pub backend: String,
55 pub samples: Vec<f64>,
56}
57
58#[derive(Debug, Clone)]
59pub struct OnnxData {
60 pub ort_version: String,
61 pub provider: String,
62 pub providers_available: Vec<String>,
63 pub graph_opt_level: String,
64 pub session_create_ms: f64,
65 pub inference_latency_p50_ms: f64,
66 pub inference_latency_p95_ms: f64,
67 pub inference_latency_p99_ms: f64,
68 pub throughput_inf_per_sec: f64,
69 pub peak_rss_mb: f64,
70 pub quantized_supported: bool,
71 pub inference_samples: Vec<f64>,
72}
73
74#[derive(Debug, Clone)]
75pub struct GpuInfo {
76 pub index: usize,
77 pub model: String,
78 pub vram_gb: f64,
79 pub compute_units: u32,
80 pub tensor_cores: bool,
81 pub temperature_c: Option<f64>,
82 pub power_watts: Option<f64>,
83 pub utilization_percent: Option<f64>,
84 pub memory_percent: Option<f64>,
85 pub sclk_mhz: Option<u32>,
86 pub mclk_mhz: Option<u32>,
87}
88
89#[derive(Debug, Clone)]
90pub struct MemoryBandwidthData {
91 pub hbm_peak_gb_s: f64,
92 pub system_peak_gb_s: f64,
93 pub hbm_ratio: f64,
94 pub hbm_samples: Vec<f64>,
95 pub system_samples: Vec<f64>,
96}
97
98#[derive(Debug, Clone)]
99pub struct TensorCoreData {
100 pub fp16_tflops: f64,
101 pub bf16_tflops: f64,
102 pub fp32_tflops: f64,
103 pub fp16_samples: Vec<f64>,
104}
105
106#[derive(Debug, Clone)]
107pub struct PyTorchData {
108 pub gemm_gflops: f64,
109 pub convolution_gflops: f64,
110 pub autograd_overhead_percent: f64,
111 pub gemm_samples: Vec<f64>,
112 pub conv_samples: Vec<f64>,
113}
114
115#[derive(Debug, Clone)]
116pub struct LlamaCppData {
117 pub gpus: Vec<LlamaCppGpu>,
118 pub prefill: Vec<LlamaCppPerfPoint>,
119 pub decode: Vec<LlamaCppPerfPoint>,
120 pub model: String,
121 pub rocm_version: String,
122}
123
124#[derive(Debug, Clone)]
125pub struct LlamaCppGpu {
126 pub name: String,
127 pub arch: String,
128 pub vram_gb: f64,
129}
130
131#[derive(Debug, Clone)]
132pub struct LlamaCppPerfPoint {
133 pub gpu: usize,
134 pub context: Option<u32>,
135 pub n_gen: Option<u32>,
136 pub avg_tps: f64,
137 pub stddev: f64,
138}
139
140#[derive(Debug, Clone)]
141pub struct FlashAttentionData {
142 pub standard_attention_speed: f64,
143 pub flash_attention_speed: f64,
144 pub speedup: f64,
145 pub memory_savings_gb: f64,
146 pub standard_samples: Vec<f64>,
147 pub flash_samples: Vec<f64>,
148}
149
150#[derive(Debug, Clone)]
151pub struct VllmData {
152 pub throughput_tokens_per_sec: f64,
153 pub latency_ms: f64,
154 pub model: String,
155 pub model_format: String,
156 pub throughput_samples: Vec<f64>,
157}
158
159pub fn load_benchmark_results() -> BenchmarkResults {
161 let log_dirs = benchmark_log_directories();
162
163 if log_dirs.is_empty() {
164 return BenchmarkResults::empty();
165 }
166
167 let mut results = BenchmarkResults::empty();
168 let mut selected_logs: Vec<(&str, PathBuf, SystemTime)> = Vec::new();
169
170 for pattern in &[
172 "rocm_benchmarks",
173 "gpu-capability_benchmarks",
174 "gpu_memory_bandwidth",
175 "memory-bandwidth_benchmarks",
176 "tensor-core_benchmarks",
177 "gemm_benchmarks",
178 "pytorch_performance",
179 "llama_cpp_benchmarks",
180 "llama-cpp",
181 "mlperf_inference",
182 "vllm_benchmarks",
183 "deepspeed_benchmarks",
184 "megatron_benchmarks",
185 "flash-attention_benchmarks",
186 "flash-attention-ck_benchmarks",
187 "onnx_benchmarks",
188 "full_benchmarks",
189 "all_benchmarks",
190 ] {
191 if let Some(log_path) = find_latest_log_in_dirs(&log_dirs, pattern) {
192 let modified = fs::metadata(&log_path)
193 .and_then(|m| m.modified())
194 .unwrap_or(SystemTime::UNIX_EPOCH);
195 selected_logs.push((pattern, log_path, modified));
196 }
197 }
198
199 let newest_component_log = selected_logs
200 .iter()
201 .filter(|(pattern, _, _)| !matches!(*pattern, "full_benchmarks" | "all_benchmarks"))
202 .map(|(_, _, modified)| *modified)
203 .max();
204
205 for (pattern, log_path, modified) in selected_logs {
206 if matches!(pattern, "full_benchmarks" | "all_benchmarks") {
207 if let Some(component_mtime) = newest_component_log {
208 let is_stale = component_mtime
209 .duration_since(modified)
210 .map(|delta| delta > STALE_FULL_BENCHMARK_WINDOW)
211 .unwrap_or(false);
212 if is_stale {
213 continue;
214 }
215 }
216 }
217
218 let include_errors = newest_component_log
219 .and_then(|latest| latest.duration_since(modified).ok())
220 .map(|delta| delta <= STALE_ERROR_WINDOW)
221 .unwrap_or(true);
222
223 if let Ok(contents) = std::fs::read_to_string(&log_path) {
224 let json = extract_json(&contents);
225 if !json.is_empty() {
226 parse_benchmark_json(&json, &mut results, include_errors);
227 }
228 }
229 }
230
231 if results.gpus.is_empty() {
232 results.gpus.push(GpuInfo {
233 index: 0,
234 model: "No GPU detected".to_string(),
235 vram_gb: 0.0,
236 compute_units: 0,
237 tensor_cores: false,
238 temperature_c: None,
239 power_watts: None,
240 utilization_percent: None,
241 memory_percent: None,
242 sclk_mhz: None,
243 mclk_mhz: None,
244 });
245 }
246
247 results.baseline = load_baseline(&log_dirs);
249 dedupe_errors_in_place(&mut results.errors);
250
251 results
252}
253
254fn load_baseline(log_dirs: &[PathBuf]) -> Option<Box<BenchmarkResults>> {
255 let baseline_patterns = [
256 "rocm_benchmarks",
257 "gpu-capability_benchmarks",
258 "memory-bandwidth_benchmarks",
259 "tensor-core_benchmarks",
260 "gemm_benchmarks",
261 "vllm_benchmarks",
262 "deepspeed_benchmarks",
263 "megatron_benchmarks",
264 "flash-attention_benchmarks",
265 "flash-attention-ck_benchmarks",
266 "onnx_benchmarks",
267 "full_benchmarks",
268 "all_benchmarks",
269 ];
270 let mut log_files = collect_matching_logs(log_dirs, &baseline_patterns);
271 let json_files: Vec<PathBuf> = log_files
272 .iter()
273 .filter(|path| is_json_log(path))
274 .cloned()
275 .collect();
276 if !json_files.is_empty() {
277 log_files = json_files;
278 }
279
280 if log_files.len() > 1 {
281 for file in log_files {
282 if let Ok(contents) = std::fs::read_to_string(file) {
283 let json = extract_json(&contents);
284 if json.is_empty() {
285 continue;
286 }
287 let mut baseline_results = BenchmarkResults {
288 gpus: Vec::new(),
289 memory_bandwidth: None,
290 tensor_core: None,
291 pytorch: None,
292 llama_cpp: None,
293 flash_attention: None,
294 vllm: None,
295 deepspeed: None,
296 megatron: None,
297 onnx: None,
298 errors: Vec::new(),
299 baseline: None,
300 };
301 parse_benchmark_json(&json, &mut baseline_results, false);
302 if !baseline_results.gpus.is_empty()
303 || baseline_results.memory_bandwidth.is_some()
304 || baseline_results.tensor_core.is_some()
305 || baseline_results.pytorch.is_some()
306 || baseline_results.flash_attention.is_some()
307 || baseline_results.vllm.is_some()
308 || baseline_results.deepspeed.is_some()
309 || baseline_results.megatron.is_some()
310 || baseline_results.onnx.is_some()
311 {
312 return Some(Box::new(baseline_results));
313 }
314 }
315 }
316 }
317 None
318}
319
320fn push_error_unique(results: &mut BenchmarkResults, message: impl Into<String>) {
321 let message = message.into();
322 let trimmed = message.trim();
323 if trimmed.is_empty() {
324 return;
325 }
326 if results
327 .errors
328 .iter()
329 .any(|existing| existing.trim() == trimmed)
330 {
331 return;
332 }
333 results.errors.push(trimmed.to_string());
334}
335
336fn dedupe_errors_in_place(errors: &mut Vec<String>) {
337 let mut deduped: Vec<String> = Vec::new();
338 for err in errors.drain(..) {
339 let trimmed = err.trim();
340 if trimmed.is_empty() {
341 continue;
342 }
343 if deduped.iter().any(|existing| existing == trimmed) {
344 continue;
345 }
346 deduped.push(trimmed.to_string());
347 }
348 *errors = deduped;
349}
350
351fn parse_benchmark_json(json: &str, results: &mut BenchmarkResults, include_errors: bool) {
352 if let Ok(value) = serde_json::from_str::<serde_json::Value>(json) {
353 if let Some(obj) = value.as_object() {
354 if let Some(res) = obj.get("results") {
355 if res.is_object() {
356 apply_metrics(res, results, include_errors);
357 }
358 if include_errors {
359 if let Some(errors) = obj.get("errors").and_then(|v| v.as_array()).map(|arr| {
360 arr.iter()
361 .filter_map(|e| e.as_str().map(|s| s.to_string()))
362 .collect::<Vec<_>>()
363 }) {
364 for err in errors {
365 push_error_unique(results, err);
366 }
367 }
368 }
369 } else {
370 for (_, val) in obj {
371 apply_metrics(val, results, include_errors);
372 }
373 }
374 }
375 }
376}
377
378fn to_f64_vec(v: &serde_json::Value) -> Vec<f64> {
379 v.as_array()
380 .map(|arr| arr.iter().filter_map(|x| x.as_f64()).collect())
381 .unwrap_or_default()
382}
383
384fn is_integrated_gpu_model(model: &str) -> bool {
385 let lowered = model.to_ascii_lowercase();
386 if lowered.contains("ryzen") {
387 return true;
388 }
389 if lowered.contains("integrated") || lowered.contains("igpu") || lowered.contains("apu") {
390 return true;
391 }
392 if lowered.contains("radeon graphics")
393 && !lowered.contains("rx")
394 && !lowered.contains("pro")
395 && !lowered.contains("instinct")
396 {
397 return true;
398 }
399 [
400 "raphael",
401 "phoenix",
402 "rembrandt",
403 "cezanne",
404 "barcelo",
405 "yellow carp",
406 "green sardine",
407 "pink sardine",
408 "hawk point",
409 ]
410 .iter()
411 .any(|token| lowered.contains(token))
412}
413
414fn apply_metrics(val: &serde_json::Value, results: &mut BenchmarkResults, include_errors: bool) {
415 if let Some(obj) = val.as_object() {
416 if obj.contains_key("metrics")
418 || obj.contains_key("results")
419 || obj.contains_key("gpus")
420 || obj.contains_key("gpu_model")
421 || obj.contains_key("hbm_peak_gb_s")
422 || obj.contains_key("fp16_tflops")
423 || obj.contains_key("gemm_gflops")
424 || obj.contains_key("flash_attention_speed")
425 || obj.contains_key("throughput_tokens_per_sec")
426 || obj.contains_key("throughput_samples_per_sec")
427 || obj.contains_key("megatron_throughput_samples_per_sec")
428 || obj.contains_key("ort_version")
429 || obj.keys().any(|k| k.contains("_tps_gpu"))
430 {
431 apply_metrics_internal(val, results, include_errors);
432 } else {
433 for (_, nested_val) in obj {
435 apply_metrics(nested_val, results, include_errors);
436 }
437 }
438 }
439}
440
441fn apply_metrics_internal(
442 val: &serde_json::Value,
443 results: &mut BenchmarkResults,
444 include_errors: bool,
445) {
446 if include_errors {
447 if let Some(root_obj) = val.as_object() {
448 if let Some(errors) = root_obj
449 .get("errors")
450 .and_then(|v| v.as_array())
451 .map(|arr| {
452 arr.iter()
453 .filter_map(|e| e.as_str().map(|s| s.to_string()))
454 .collect::<Vec<_>>()
455 })
456 {
457 for err in errors {
458 push_error_unique(results, err);
459 }
460 }
461 }
462 }
463
464 let metrics = val
465 .get("metrics")
466 .or_else(|| val.get("results"))
467 .unwrap_or(val);
468
469 if let Some(obj) = metrics.as_object() {
470 if include_errors {
471 if let Some(errors) = obj.get("errors").and_then(|v| v.as_array()).map(|arr| {
472 arr.iter()
473 .filter_map(|e| e.as_str().map(|s| s.to_string()))
474 .collect::<Vec<_>>()
475 }) {
476 for err in errors {
477 push_error_unique(results, err);
478 }
479 }
480 }
481
482 if obj.contains_key("gpus") || obj.contains_key("gpu_model") {
483 let gpus = if let Some(arr) = obj.get("gpus").and_then(|v| v.as_array()) {
484 arr.iter()
485 .filter_map(|g| g.as_object())
486 .enumerate()
487 .filter_map(|(idx, g)| {
488 let model = g
489 .get("gpu_model")
490 .and_then(|v| v.as_str())
491 .unwrap_or("Unknown");
492 if is_integrated_gpu_model(model) {
493 return None;
494 }
495 let gpu_index = g
496 .get("index")
497 .and_then(|v| v.as_u64())
498 .or_else(|| g.get("local_index").and_then(|v| v.as_u64()))
499 .unwrap_or(idx as u64) as usize;
500 Some(GpuInfo {
501 index: gpu_index,
502 model: model.into(),
503 vram_gb: g.get("vram_gb").and_then(|v| v.as_f64()).unwrap_or(0.0),
504 compute_units: g
505 .get("compute_units")
506 .and_then(|v| v.as_u64())
507 .unwrap_or(0) as u32,
508 tensor_cores: g
509 .get("tensor_cores")
510 .and_then(|v| v.as_bool())
511 .unwrap_or(false),
512 temperature_c: g.get("temperature_c").and_then(|v| v.as_f64()),
513 power_watts: g.get("power_watts").and_then(|v| v.as_f64()),
514 utilization_percent: g
515 .get("utilization_percent")
516 .and_then(|v| v.as_f64()),
517 memory_percent: g.get("memory_percent").and_then(|v| v.as_f64()),
518 sclk_mhz: g.get("sclk_mhz").and_then(|v| v.as_u64()).map(|v| v as u32),
519 mclk_mhz: g.get("mclk_mhz").and_then(|v| v.as_u64()).map(|v| v as u32),
520 })
521 })
522 .collect()
523 } else {
524 let model = obj
525 .get("gpu_model")
526 .and_then(|v| v.as_str())
527 .unwrap_or("Unknown");
528 if is_integrated_gpu_model(model) {
529 Vec::new()
530 } else {
531 vec![GpuInfo {
532 index: obj.get("index").and_then(|v| v.as_u64()).unwrap_or(0) as usize,
533 model: model.into(),
534 vram_gb: obj.get("vram_gb").and_then(|v| v.as_f64()).unwrap_or(0.0),
535 compute_units: obj
536 .get("compute_units")
537 .and_then(|v| v.as_u64())
538 .unwrap_or(0) as u32,
539 tensor_cores: obj
540 .get("tensor_cores")
541 .and_then(|v| v.as_bool())
542 .unwrap_or(false),
543 temperature_c: obj.get("temperature_c").and_then(|v| v.as_f64()),
544 power_watts: obj.get("power_watts").and_then(|v| v.as_f64()),
545 utilization_percent: obj
546 .get("utilization_percent")
547 .and_then(|v| v.as_f64()),
548 memory_percent: obj.get("memory_percent").and_then(|v| v.as_f64()),
549 sclk_mhz: obj
550 .get("sclk_mhz")
551 .and_then(|v| v.as_u64())
552 .map(|v| v as u32),
553 mclk_mhz: obj
554 .get("mclk_mhz")
555 .and_then(|v| v.as_u64())
556 .map(|v| v as u32),
557 }]
558 }
559 };
560 results.gpus = gpus;
561 }
562
563 if obj.contains_key("hbm_peak_gb_s") {
564 results.memory_bandwidth = Some(MemoryBandwidthData {
565 hbm_peak_gb_s: obj
566 .get("hbm_peak_gb_s")
567 .and_then(|v| v.as_f64())
568 .unwrap_or(0.0),
569 system_peak_gb_s: obj
570 .get("system_peak_gb_s")
571 .and_then(|v| v.as_f64())
572 .unwrap_or(0.0),
573 hbm_ratio: obj.get("hbm_ratio").and_then(|v| v.as_f64()).unwrap_or(0.0),
574 hbm_samples: obj
575 .get("hbm_samples_gbps")
576 .map(to_f64_vec)
577 .unwrap_or_default(),
578 system_samples: obj
579 .get("system_samples_gbps")
580 .map(to_f64_vec)
581 .unwrap_or_default(),
582 });
583 }
584
585 if obj.contains_key("fp16_tflops") {
586 results.tensor_core = Some(TensorCoreData {
587 fp16_tflops: obj
588 .get("fp16_tflops")
589 .and_then(|v| v.as_f64())
590 .unwrap_or(0.0),
591 bf16_tflops: obj
592 .get("bf16_tflops")
593 .and_then(|v| v.as_f64())
594 .unwrap_or(0.0),
595 fp32_tflops: obj
596 .get("fp32_tflops")
597 .and_then(|v| v.as_f64())
598 .unwrap_or(0.0),
599 fp16_samples: obj
600 .get("fp16_samples_tflops")
601 .map(to_f64_vec)
602 .unwrap_or_default(),
603 });
604 }
605
606 if obj.contains_key("gemm_gflops") || obj.contains_key("convolution_gflops") {
607 results.pytorch = Some(PyTorchData {
608 gemm_gflops: obj
609 .get("gemm_gflops")
610 .and_then(|v| v.as_f64())
611 .unwrap_or(0.0),
612 convolution_gflops: obj
613 .get("convolution_gflops")
614 .and_then(|v| v.as_f64())
615 .unwrap_or(0.0),
616 autograd_overhead_percent: obj
617 .get("autograd_overhead_percent")
618 .and_then(|v| v.as_f64())
619 .unwrap_or(0.0),
620 gemm_samples: obj
621 .get("gemm_samples_gflops")
622 .map(to_f64_vec)
623 .unwrap_or_default(),
624 conv_samples: obj
625 .get("conv_samples_gflops")
626 .map(to_f64_vec)
627 .unwrap_or_default(),
628 });
629 }
630
631 if obj.contains_key("flash_attention_speed") || obj.contains_key("standard_attention_speed")
632 {
633 results.flash_attention = Some(FlashAttentionData {
634 standard_attention_speed: obj
635 .get("standard_attention_speed")
636 .and_then(|v| v.as_f64())
637 .unwrap_or(0.0),
638 flash_attention_speed: obj
639 .get("flash_attention_speed")
640 .and_then(|v| v.as_f64())
641 .unwrap_or(0.0),
642 speedup: obj.get("speedup").and_then(|v| v.as_f64()).unwrap_or(0.0),
643 memory_savings_gb: obj
644 .get("memory_savings_gb")
645 .and_then(|v| v.as_f64())
646 .unwrap_or(0.0),
647 standard_samples: obj
648 .get("standard_samples_tok_s")
649 .map(to_f64_vec)
650 .unwrap_or_default(),
651 flash_samples: obj
652 .get("flash_samples_tok_s")
653 .map(to_f64_vec)
654 .unwrap_or_default(),
655 });
656 }
657
658 if obj.contains_key("throughput_tokens_per_sec") {
659 let model_name = obj
660 .get("model")
661 .and_then(|v| v.as_str())
662 .unwrap_or("Unknown")
663 .to_string();
664 let model_format = obj
665 .get("model_format")
666 .and_then(|v| v.as_str())
667 .map(|s| s.to_string())
668 .unwrap_or_else(|| {
669 if model_name.to_ascii_lowercase().ends_with(".gguf") {
670 "gguf".to_string()
671 } else {
672 "safetensors".to_string()
673 }
674 });
675 results.vllm = Some(VllmData {
676 throughput_tokens_per_sec: obj
677 .get("throughput_tokens_per_sec")
678 .and_then(|v| v.as_f64())
679 .unwrap_or(0.0),
680 latency_ms: obj
681 .get("latency_ms")
682 .and_then(|v| v.as_f64())
683 .unwrap_or(0.0),
684 model: model_name,
685 model_format,
686 throughput_samples: obj
687 .get("throughput_samples")
688 .map(to_f64_vec)
689 .unwrap_or_default(),
690 });
691 let throughput = obj
692 .get("throughput_tokens_per_sec")
693 .and_then(|v| v.as_f64())
694 .unwrap_or(0.0);
695 if throughput <= 0.0 {
696 if let Some(reason) = obj
697 .get("reason")
698 .and_then(|v| v.as_str())
699 .map(|s| s.trim())
700 .filter(|s| !s.is_empty())
701 {
702 push_error_unique(results, format!("vLLM benchmark degraded: {reason}"));
703 } else {
704 push_error_unique(results, "vLLM benchmark degraded: zero throughput");
705 }
706 }
707 }
708
709 if obj.contains_key("throughput_samples_per_sec") {
710 results.deepspeed = Some(DeepspeedData {
711 throughput_samples_per_sec: obj
712 .get("throughput_samples_per_sec")
713 .and_then(|v| v.as_f64())
714 .unwrap_or(0.0),
715 avg_latency_ms: obj
716 .get("avg_latency_ms")
717 .and_then(|v| v.as_f64())
718 .unwrap_or(0.0),
719 stage: obj.get("stage").and_then(|v| v.as_u64()).unwrap_or(1) as u32,
720 accelerator: obj
721 .get("accelerator")
722 .and_then(|v| v.as_str())
723 .unwrap_or("rocm")
724 .to_string(),
725 samples: obj.get("samples").map(to_f64_vec).unwrap_or_default(),
726 });
727 }
728
729 if obj.contains_key("megatron_throughput_samples_per_sec") {
730 results.megatron = Some(MegatronData {
731 throughput_samples_per_sec: obj
732 .get("megatron_throughput_samples_per_sec")
733 .and_then(|v| v.as_f64())
734 .unwrap_or(0.0),
735 avg_latency_ms: obj
736 .get("megatron_avg_latency_ms")
737 .and_then(|v| v.as_f64())
738 .unwrap_or(0.0),
739 backend: obj
740 .get("megatron_backend")
741 .and_then(|v| v.as_str())
742 .unwrap_or("megatron")
743 .to_string(),
744 samples: obj
745 .get("megatron_samples")
746 .map(to_f64_vec)
747 .unwrap_or_default(),
748 });
749 }
750
751 if obj.contains_key("ort_version") {
752 results.onnx = Some(OnnxData {
753 ort_version: obj
754 .get("ort_version")
755 .and_then(|v| v.as_str())
756 .unwrap_or("unknown")
757 .to_string(),
758 provider: obj
759 .get("provider")
760 .and_then(|v| v.as_str())
761 .unwrap_or("CPU")
762 .to_string(),
763 providers_available: obj
764 .get("providers_available")
765 .and_then(|v| v.as_array())
766 .map(|arr| {
767 arr.iter()
768 .filter_map(|x| x.as_str().map(|s| s.to_string()))
769 .collect()
770 })
771 .unwrap_or_default(),
772 graph_opt_level: obj
773 .get("graph_opt_level")
774 .and_then(|v| v.as_str())
775 .unwrap_or("ORI_DISABLE_ALL")
776 .to_string(),
777 session_create_ms: obj
778 .get("session_create_ms")
779 .and_then(|v| v.as_f64())
780 .unwrap_or(0.0),
781 inference_latency_p50_ms: obj
782 .get("inference_latency_p50_ms")
783 .and_then(|v| v.as_f64())
784 .unwrap_or(0.0),
785 inference_latency_p95_ms: obj
786 .get("inference_latency_p95_ms")
787 .and_then(|v| v.as_f64())
788 .unwrap_or(0.0),
789 inference_latency_p99_ms: obj
790 .get("inference_latency_p99_ms")
791 .and_then(|v| v.as_f64())
792 .unwrap_or(0.0),
793 throughput_inf_per_sec: obj
794 .get("throughput_inf_per_sec")
795 .and_then(|v| v.as_f64())
796 .unwrap_or(0.0),
797 peak_rss_mb: obj
798 .get("peak_rss_mb")
799 .and_then(|v| v.as_f64())
800 .unwrap_or(0.0),
801 quantized_supported: obj
802 .get("quantized_supported")
803 .and_then(|v| v.as_bool())
804 .unwrap_or(false),
805 inference_samples: obj
806 .get("inference_samples")
807 .map(to_f64_vec)
808 .unwrap_or_default(),
809 });
810 }
811
812 if obj.keys().any(|k| k.contains("_tps_gpu")) {
814 let mut gpus = Vec::new();
815 let mut prefill = Vec::new();
816 let mut decode = Vec::new();
817
818 let gpu_names: Vec<String> = obj
821 .get("gpu_names")
822 .and_then(|v| v.as_array())
823 .map(|arr| {
824 arr.iter()
825 .filter_map(|v| v.as_str().map(|s| s.to_string()))
826 .collect()
827 })
828 .unwrap_or_default();
829 let gpu_vram: Vec<f64> = obj
830 .get("gpu_vram_gb")
831 .and_then(|v| v.as_array())
832 .map(|arr| arr.iter().filter_map(|v| v.as_f64()).collect())
833 .unwrap_or_default();
834 let gpu_arch = obj
835 .get("gpu_arch")
836 .and_then(|v| v.as_str())
837 .unwrap_or("")
838 .to_string();
839
840 let mut gpu_indices: Vec<usize> = obj
842 .keys()
843 .filter_map(|k| {
844 k.rsplit("_gpu")
845 .next()
846 .and_then(|s| s.parse::<usize>().ok())
847 })
848 .collect();
849 gpu_indices.sort();
850 gpu_indices.dedup();
851
852 for &gpu_idx in &gpu_indices {
853 gpus.push(LlamaCppGpu {
854 name: gpu_names
855 .get(gpu_idx)
856 .cloned()
857 .filter(|s| !s.is_empty())
858 .unwrap_or_else(|| format!("GPU {}", gpu_idx)),
859 arch: gpu_arch.clone(),
860 vram_gb: gpu_vram.get(gpu_idx).copied().unwrap_or(0.0),
861 });
862 }
863
864 for (key, value) in obj.iter() {
865 let tps = value.as_f64().unwrap_or(0.0);
866 let gpu_idx = key
867 .rsplit("_gpu")
868 .next()
869 .and_then(|s| s.parse::<usize>().ok())
870 .unwrap_or(0);
871
872 if key.starts_with("prefill_") && !key.contains("stddev") {
873 let context = key
875 .trim_start_matches("prefill_")
876 .trim_end_matches(&format!("_tps_gpu{}", gpu_idx))
877 .parse::<u32>()
878 .ok();
879 prefill.push(LlamaCppPerfPoint {
880 gpu: gpu_idx,
881 context,
882 n_gen: None,
883 avg_tps: tps,
884 stddev: obj
885 .get(&key.replace("_tps_", "_stddev_tps_"))
886 .and_then(|v| v.as_f64())
887 .unwrap_or(0.0),
888 });
889 } else if key.starts_with("decode_") && !key.contains("stddev") {
890 let n_gen = key
892 .trim_start_matches("decode_")
893 .trim_end_matches(&format!("_tps_gpu{}", gpu_idx))
894 .parse::<u32>()
895 .ok();
896 decode.push(LlamaCppPerfPoint {
897 gpu: gpu_idx,
898 context: None,
899 n_gen,
900 avg_tps: tps,
901 stddev: obj
902 .get(&key.replace("_tps_", "_stddev_tps_"))
903 .and_then(|v| v.as_f64())
904 .unwrap_or(0.0),
905 });
906 }
907 }
908
909 prefill.sort_by(|a, b| a.gpu.cmp(&b.gpu).then(a.context.cmp(&b.context)));
911 decode.sort_by(|a, b| a.gpu.cmp(&b.gpu).then(a.n_gen.cmp(&b.n_gen)));
912
913 results.llama_cpp = Some(LlamaCppData {
914 gpus,
915 prefill,
916 decode,
917 model: obj
918 .get("model")
919 .and_then(|v| v.as_str())
920 .unwrap_or("")
921 .to_string(),
922 rocm_version: obj
923 .get("rocm_version")
924 .and_then(|v| v.as_str())
925 .unwrap_or("")
926 .to_string(),
927 });
928 }
929 }
930}
931
932fn extract_json(content: &str) -> String {
933 extract_benchmark_json_string(content).unwrap_or_default()
934}
935
936impl BenchmarkResults {
937 fn empty() -> Self {
938 BenchmarkResults {
939 gpus: Vec::new(),
940 memory_bandwidth: None,
941 tensor_core: None,
942 pytorch: None,
943 llama_cpp: None,
944 flash_attention: None,
945 vllm: None,
946 deepspeed: None,
947 megatron: None,
948 onnx: None,
949 errors: Vec::new(),
950 baseline: None,
951 }
952 }
953}
954
955pub fn render_benchmark_page(
957 frame: &mut Frame,
958 area: Rect,
959 results: &BenchmarkResults,
960 tab_index: usize,
961) {
962 let tabs = Tabs::new(vec![
963 Line::from("GPU"),
964 Line::from("Memory"),
965 Line::from("Tensor Core"),
966 Line::from("PyTorch"),
967 Line::from("Flash Attn"),
968 Line::from("vLLM"),
969 Line::from("DeepSpeed"),
970 Line::from("Megatron"),
971 Line::from("ONNX"),
972 Line::from("LLaMA"),
973 ])
974 .select(tab_index)
975 .style(Style::default().fg(Color::Cyan))
976 .highlight_style(
977 Style::default()
978 .add_modifier(Modifier::BOLD)
979 .fg(Color::Yellow),
980 );
981
982 let chunks = Layout::default()
983 .direction(Direction::Vertical)
984 .constraints([Constraint::Length(3), Constraint::Min(10)])
985 .split(area);
986
987 frame.render_widget(tabs, chunks[0]);
988
989 match tab_index {
990 0 => render_gpu_tab(frame, chunks[1], results),
991 1 => render_memory_tab(frame, chunks[1], results),
992 2 => render_tensor_core_tab(frame, chunks[1], results),
993 3 => render_pytorch_tab(frame, chunks[1], results),
994 4 => render_flash_attention_tab(frame, chunks[1], results),
995 5 => render_vllm_tab(frame, chunks[1], results),
996 6 => render_deepspeed_tab(frame, chunks[1], results),
997 7 => render_megatron_tab(frame, chunks[1], results),
998 8 => render_onnx_tab(frame, chunks[1], results),
999 9 => render_llama_cpp_tab(frame, chunks[1], results),
1000 _ => render_gpu_tab(frame, chunks[1], results),
1001 }
1002}
1003
1004fn render_gpu_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1005 let mut content = String::from("GPU Information & Performance Summary\n\n");
1006
1007 if results.gpus.is_empty() {
1008 content.push_str("No GPU data available\n\nRun benchmarks to see GPU information");
1009 } else {
1010 for gpu in &results.gpus {
1011 content.push_str(&format!(
1012 "GPU {}: {}\nVRAM: {:.1} GB\nCompute Units: {}\nTensor Cores: {}\n",
1013 gpu.index,
1014 gpu.model,
1015 gpu.vram_gb,
1016 gpu.compute_units,
1017 if gpu.tensor_cores { "Yes" } else { "No" }
1018 ));
1019
1020 if let (Some(temp), Some(power)) = (gpu.temperature_c, gpu.power_watts) {
1021 content.push_str(&format!(
1022 "Temperature: {:.1}°C | Power: {:.1}W\n",
1023 temp, power
1024 ));
1025 }
1026
1027 if let (Some(util), Some(mem)) = (gpu.utilization_percent, gpu.memory_percent) {
1028 content.push_str(&format!(
1029 "GPU Utilization: {:.1}% | Memory Usage: {:.1}%\n",
1030 util, mem
1031 ));
1032 }
1033
1034 if let (Some(sclk), Some(mclk)) = (gpu.sclk_mhz, gpu.mclk_mhz) {
1035 content.push_str(&format!(
1036 "GFX Clock: {} MHz | Memory Clock: {} MHz\n",
1037 sclk, mclk
1038 ));
1039 }
1040
1041 content.push_str("---\n\n");
1042 }
1043
1044 content.push_str("Best Observed Performance:\n");
1045 let mut has_perf = false;
1046
1047 if let Some(mem) = &results.memory_bandwidth {
1048 content.push_str(&format!(
1049 " • Memory Bandwidth: {:.1} GB/s\n",
1050 mem.hbm_peak_gb_s
1051 ));
1052 has_perf = true;
1053 }
1054 if let Some(tc) = &results.tensor_core {
1055 content.push_str(&format!(
1056 " • Tensor Cores (FP16): {:.1} TFLOPS\n",
1057 tc.fp16_tflops
1058 ));
1059 has_perf = true;
1060 }
1061 if let Some(torch) = &results.pytorch {
1062 content.push_str(&format!(
1063 " • PyTorch GEMM: {:.1} GFLOPS\n",
1064 torch.gemm_gflops
1065 ));
1066 has_perf = true;
1067 }
1068 if let Some(llama) = &results.llama_cpp {
1069 content.push_str(&format!(
1070 " • LLaMA.cpp: {} on {} GPU(s)\n",
1071 llama.model,
1072 llama.gpus.len()
1073 ));
1074 has_perf = true;
1075 }
1076 if let Some(fa) = &results.flash_attention {
1077 content.push_str(&format!(
1078 " • Flash Attention: {:.1} tok/s ({:.2}x speedup)\n",
1079 fa.flash_attention_speed, fa.speedup
1080 ));
1081 has_perf = true;
1082 }
1083 if let Some(v) = &results.vllm {
1084 if v.throughput_tokens_per_sec > 0.0 {
1085 content.push_str(&format!(
1086 " • vLLM Throughput: {:.1} tok/s\n",
1087 v.throughput_tokens_per_sec
1088 ));
1089 has_perf = true;
1090 } else {
1091 content.push_str(" • vLLM: Benchmark degraded (see Recent errors)\n");
1092 }
1093 }
1094 if let Some(ds) = &results.deepspeed {
1095 if ds.throughput_samples_per_sec > 0.0 {
1096 content.push_str(&format!(
1097 " • DeepSpeed Throughput: {:.1} samples/s\n",
1098 ds.throughput_samples_per_sec
1099 ));
1100 has_perf = true;
1101 }
1102 }
1103 if let Some(meg) = &results.megatron {
1104 if meg.throughput_samples_per_sec > 0.0 {
1105 content.push_str(&format!(
1106 " • Megatron Throughput: {:.1} samples/s\n",
1107 meg.throughput_samples_per_sec
1108 ));
1109 has_perf = true;
1110 }
1111 }
1112 if let Some(onnx) = &results.onnx {
1113 if onnx.throughput_inf_per_sec > 0.0 {
1114 content.push_str(&format!(
1115 " • ONNX Runtime: {:.1} inf/s ({}, {})\n",
1116 onnx.throughput_inf_per_sec, onnx.provider, onnx.ort_version
1117 ));
1118 has_perf = true;
1119 }
1120 }
1121
1122 if !has_perf {
1123 content.push_str(" (No performance benchmarks run yet)\n");
1124 }
1125 content.push('\n');
1126 }
1127
1128 if !results.errors.is_empty() {
1129 content.push_str("Recent errors:\n");
1130 for err in &results.errors {
1131 content.push_str(&format!("- {}\n", err));
1132 }
1133 }
1134
1135 let paragraph = Paragraph::new(content)
1136 .alignment(Alignment::Left)
1137 .block(Block::default().title("GPU Details").borders(Borders::ALL));
1138
1139 frame.render_widget(paragraph, area);
1140}
1141
1142fn render_memory_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1143 if let Some(mem) = &results.memory_bandwidth {
1144 let chunks = Layout::default()
1145 .direction(Direction::Vertical)
1146 .constraints([Constraint::Length(4), Constraint::Min(7)])
1147 .split(area);
1148
1149 let mut summary = format!(
1150 "HBM Peak: {:.1} GB/s | System Peak: {:.1} GB/s | Ratio: {:.2}x",
1151 mem.hbm_peak_gb_s, mem.system_peak_gb_s, mem.hbm_ratio
1152 );
1153
1154 if let Some(baseline) = &results.baseline {
1155 if let Some(b_mem) = &baseline.memory_bandwidth {
1156 let diff =
1157 ((mem.hbm_peak_gb_s - b_mem.hbm_peak_gb_s) / b_mem.hbm_peak_gb_s) * 100.0;
1158 summary.push_str(&format!(
1159 " (Baseline: {:.1} GB/s | {:+.1}%)",
1160 b_mem.hbm_peak_gb_s, diff
1161 ));
1162 }
1163 }
1164
1165 frame.render_widget(
1166 Paragraph::new(summary)
1167 .alignment(Alignment::Left)
1168 .style(Style::default().fg(if results.baseline.is_some() {
1169 Color::Green
1170 } else {
1171 Color::White
1172 }))
1173 .block(
1174 Block::default()
1175 .borders(Borders::ALL)
1176 .title("Summary & Comparison"),
1177 ),
1178 chunks[0],
1179 );
1180
1181 let hbm_data = prepare_data(&mem.hbm_samples);
1182 let system_data = prepare_data(&mem.system_samples);
1183 let mut datasets = Vec::new();
1184 if !hbm_data.is_empty() {
1185 datasets.push(series_dataset("HBM", &hbm_data, Color::Cyan));
1186 }
1187 if !system_data.is_empty() {
1188 datasets.push(series_dataset("System", &system_data, Color::Yellow));
1189 }
1190 let y_values: Vec<f64> = mem
1191 .hbm_samples
1192 .iter()
1193 .chain(mem.system_samples.iter())
1194 .cloned()
1195 .collect();
1196 let (y0, y1) = y_bounds(&y_values);
1197 let x_bounds = [0.0, (mem.hbm_samples.len().max(1) - 1) as f64];
1198
1199 frame.render_widget(
1200 Chart::new(datasets)
1201 .block(
1202 Block::default()
1203 .borders(Borders::ALL)
1204 .title("Memory Bandwidth Performance"),
1205 )
1206 .x_axis(
1207 Axis::default()
1208 .title("Buffer Size (MB)")
1209 .style(Style::default().fg(Color::Gray))
1210 .bounds(x_bounds)
1211 .labels(vec![
1212 Line::from("64"),
1213 Line::from("128"),
1214 Line::from("256"),
1215 Line::from("512"),
1216 ]),
1217 )
1218 .y_axis(
1219 Axis::default()
1220 .title("GB/s")
1221 .style(Style::default().fg(Color::Gray))
1222 .bounds([y0, y1])
1223 .labels(vec![
1224 Line::from(format!("{:.0}", y0)),
1225 Line::from(format!("{:.0}", (y0 + y1) / 2.0)),
1226 Line::from(format!("{:.0}", y1)),
1227 ]),
1228 ),
1229 chunks[1],
1230 );
1231 } else {
1232 frame.render_widget(
1233 Paragraph::new("No memory data available\n\nRun benchmarks to see memory bandwidth")
1234 .alignment(Alignment::Left)
1235 .block(
1236 Block::default()
1237 .title("Memory Bandwidth")
1238 .borders(Borders::ALL),
1239 ),
1240 area,
1241 );
1242 }
1243}
1244
1245fn render_tensor_core_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1246 if let Some(tc) = &results.tensor_core {
1247 let chunks = Layout::default()
1248 .direction(Direction::Vertical)
1249 .constraints([Constraint::Length(3), Constraint::Min(7)])
1250 .split(area);
1251
1252 let summary = format!(
1253 "FP16: {:.1} | BF16: {:.1} | FP32: {:.1} TFLOPS",
1254 tc.fp16_tflops, tc.bf16_tflops, tc.fp32_tflops
1255 );
1256 frame.render_widget(
1257 Paragraph::new(summary).block(Block::default().borders(Borders::ALL).title("Summary")),
1258 chunks[0],
1259 );
1260
1261 let fp16_data = prepare_data(&tc.fp16_samples);
1262 let mut datasets = Vec::new();
1263 if !fp16_data.is_empty() {
1264 datasets.push(series_dataset("FP16 sweep", &fp16_data, Color::Magenta));
1265 }
1266 let (y0, y1) = y_bounds(&tc.fp16_samples);
1267 let x_bounds = [0.0, (tc.fp16_samples.len().max(1) - 1) as f64];
1268
1269 frame.render_widget(
1270 Chart::new(datasets)
1271 .block(
1272 Block::default()
1273 .borders(Borders::ALL)
1274 .title("Tensor Core Performance"),
1275 )
1276 .x_axis(
1277 Axis::default()
1278 .title("Matrix Size (N x N)")
1279 .style(Style::default().fg(Color::Gray))
1280 .bounds(x_bounds)
1281 .labels(vec![
1282 Line::from("512"),
1283 Line::from("1024"),
1284 Line::from("2048"),
1285 ]),
1286 )
1287 .y_axis(
1288 Axis::default()
1289 .title("TFLOPS")
1290 .style(Style::default().fg(Color::Gray))
1291 .bounds([y0, y1])
1292 .labels(vec![
1293 Line::from(format!("{:.0}", y0)),
1294 Line::from(format!("{:.0}", (y0 + y1) / 2.0)),
1295 Line::from(format!("{:.0}", y1)),
1296 ]),
1297 ),
1298 chunks[1],
1299 );
1300 } else {
1301 frame.render_widget(
1302 Paragraph::new("No Tensor Core data available\n\nRun benchmarks to see performance")
1303 .alignment(Alignment::Left)
1304 .block(
1305 Block::default()
1306 .title("Tensor Core Performance")
1307 .borders(Borders::ALL),
1308 ),
1309 area,
1310 );
1311 }
1312}
1313
1314fn render_pytorch_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1315 if let Some(torch) = &results.pytorch {
1316 let chunks = Layout::default()
1317 .direction(Direction::Vertical)
1318 .constraints([Constraint::Length(3), Constraint::Min(7)])
1319 .split(area);
1320
1321 let summary = format!(
1322 "GEMM: {:.1} GFLOPS | Conv: {:.1} GFLOPS | Autograd overhead: {:.1}%",
1323 torch.gemm_gflops, torch.convolution_gflops, torch.autograd_overhead_percent
1324 );
1325 frame.render_widget(
1326 Paragraph::new(summary).block(Block::default().borders(Borders::ALL).title("Summary")),
1327 chunks[0],
1328 );
1329
1330 let gemm_data = prepare_data(&torch.gemm_samples);
1331 let conv_data = prepare_data(&torch.conv_samples);
1332 let mut datasets = Vec::new();
1333 if !gemm_data.is_empty() {
1334 datasets.push(series_dataset("GEMM", &gemm_data, Color::Cyan));
1335 }
1336 if !conv_data.is_empty() {
1337 datasets.push(series_dataset("Conv", &conv_data, Color::Yellow));
1338 }
1339 let y_values: Vec<f64> = torch
1340 .gemm_samples
1341 .iter()
1342 .chain(torch.conv_samples.iter())
1343 .cloned()
1344 .collect();
1345 let (y0, y1) = y_bounds(&y_values);
1346 let x_bounds = [0.0, (torch.gemm_samples.len().max(1) - 1) as f64];
1347
1348 frame.render_widget(
1349 Chart::new(datasets)
1350 .block(
1351 Block::default()
1352 .borders(Borders::ALL)
1353 .title("PyTorch Kernel Performance"),
1354 )
1355 .x_axis(
1356 Axis::default()
1357 .title("Benchmark Trial")
1358 .style(Style::default().fg(Color::Gray))
1359 .bounds(x_bounds)
1360 .labels(vec![
1361 Line::from("Trial 1"),
1362 Line::from("Trial 2"),
1363 Line::from("Trial 3"),
1364 ]),
1365 )
1366 .y_axis(
1367 Axis::default()
1368 .title("GFLOPS")
1369 .style(Style::default().fg(Color::Gray))
1370 .bounds([y0, y1])
1371 .labels(vec![
1372 Line::from(format!("{:.0}", y0)),
1373 Line::from(format!("{:.0}", (y0 + y1) / 2.0)),
1374 Line::from(format!("{:.0}", y1)),
1375 ]),
1376 ),
1377 chunks[1],
1378 );
1379 } else {
1380 frame.render_widget(
1381 Paragraph::new("No PyTorch data available\n\nRun benchmarks to see performance")
1382 .alignment(Alignment::Left)
1383 .block(
1384 Block::default()
1385 .title("PyTorch Performance")
1386 .borders(Borders::ALL),
1387 ),
1388 area,
1389 );
1390 }
1391}
1392
1393fn render_flash_attention_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1394 if let Some(fa) = &results.flash_attention {
1395 let chunks = Layout::default()
1396 .direction(Direction::Vertical)
1397 .constraints([Constraint::Length(3), Constraint::Min(7)])
1398 .split(area);
1399
1400 let summary = format!(
1401 "Standard: {:.1} tok/s | Flash: {:.1} tok/s | Speedup: {:.2}x | Saved: {:.2} GB",
1402 fa.standard_attention_speed, fa.flash_attention_speed, fa.speedup, fa.memory_savings_gb
1403 );
1404 frame.render_widget(
1405 Paragraph::new(summary).block(Block::default().borders(Borders::ALL).title("Summary")),
1406 chunks[0],
1407 );
1408
1409 let standard_data = prepare_data(&fa.standard_samples);
1410 let flash_data = prepare_data(&fa.flash_samples);
1411 let mut datasets = Vec::new();
1412 if !standard_data.is_empty() {
1413 datasets.push(series_dataset("Standard", &standard_data, Color::Yellow));
1414 }
1415 if !flash_data.is_empty() {
1416 datasets.push(series_dataset("Flash", &flash_data, Color::Green));
1417 }
1418 let y_values: Vec<f64> = fa
1419 .standard_samples
1420 .iter()
1421 .chain(fa.flash_samples.iter())
1422 .cloned()
1423 .collect();
1424 let (y0, y1) = y_bounds(&y_values);
1425 let x_bounds = [0.0, (fa.flash_samples.len().max(1) - 1) as f64];
1426
1427 frame.render_widget(
1428 Chart::new(datasets)
1429 .block(
1430 Block::default()
1431 .borders(Borders::ALL)
1432 .title("Flash Attention Speedup"),
1433 )
1434 .x_axis(
1435 Axis::default()
1436 .title("Sequence Length")
1437 .style(Style::default().fg(Color::Gray))
1438 .bounds(x_bounds)
1439 .labels(vec![
1440 Line::from("128"),
1441 Line::from("256"),
1442 Line::from("512"),
1443 Line::from("1024"),
1444 ]),
1445 )
1446 .y_axis(
1447 Axis::default()
1448 .title("Tokens/s")
1449 .style(Style::default().fg(Color::Gray))
1450 .bounds([y0, y1])
1451 .labels(vec![
1452 Line::from(format!("{:.0}", y0)),
1453 Line::from(format!("{:.0}", (y0 + y1) / 2.0)),
1454 Line::from(format!("{:.0}", y1)),
1455 ]),
1456 ),
1457 chunks[1],
1458 );
1459 } else {
1460 frame.render_widget(
1461 Paragraph::new("No Flash Attention data available\n\nRun benchmarks to see comparison")
1462 .alignment(Alignment::Left)
1463 .block(
1464 Block::default()
1465 .title("Flash Attention vs Standard")
1466 .borders(Borders::ALL),
1467 ),
1468 area,
1469 );
1470 }
1471}
1472
1473fn render_llama_cpp_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1474 if let Some(llama) = &results.llama_cpp {
1475 let chunks = Layout::default()
1476 .direction(Direction::Vertical)
1477 .constraints([
1478 Constraint::Length(5),
1479 Constraint::Length(8),
1480 Constraint::Min(8),
1481 ])
1482 .split(area);
1483
1484 let summary = format!(
1485 "Model: {} | ROCm: {} | GPUs: {}",
1486 llama.model,
1487 llama.rocm_version,
1488 llama.gpus.len()
1489 );
1490 frame.render_widget(
1491 Paragraph::new(summary).block(Block::default().borders(Borders::ALL).title("Summary")),
1492 chunks[0],
1493 );
1494
1495 let gpu_lines = llama
1496 .gpus
1497 .iter()
1498 .enumerate()
1499 .map(|(idx, gpu)| {
1500 Line::from(format!(
1501 "GPU {}: {} | {} | {:.1} GB",
1502 idx, gpu.name, gpu.arch, gpu.vram_gb
1503 ))
1504 })
1505 .collect::<Vec<_>>();
1506 frame.render_widget(
1507 Paragraph::new(Text::from(gpu_lines))
1508 .block(Block::default().borders(Borders::ALL).title("GPU Info")),
1509 chunks[1],
1510 );
1511
1512 let mut body = String::from("Prefill Throughput (tokens/sec)\n");
1513 for gpu in &llama.gpus {
1514 body.push_str(&format!("GPU: {} ({})\n", gpu.name, gpu.arch));
1515 for context in [512_u32, 2048_u32, 8192_u32] {
1516 let value = llama
1517 .prefill
1518 .iter()
1519 .find(|p| {
1520 p.gpu
1521 == llama
1522 .gpus
1523 .iter()
1524 .position(|g| g.name == gpu.name)
1525 .unwrap_or(0)
1526 && p.context == Some(context)
1527 })
1528 .map(|p| p.avg_tps)
1529 .unwrap_or(0.0);
1530 body.push_str(&format!(" {}: {:.2}\n", context, value));
1531 }
1532 body.push('\n');
1533 }
1534 body.push_str("Decode Throughput (tokens/sec)\n");
1535 for gpu in &llama.gpus {
1536 body.push_str(&format!("GPU: {} ({})\n", gpu.name, gpu.arch));
1537 {
1538 let n_gen = 128_u32;
1539 let value = llama
1540 .decode
1541 .iter()
1542 .find(|p| {
1543 p.gpu
1544 == llama
1545 .gpus
1546 .iter()
1547 .position(|g| g.name == gpu.name)
1548 .unwrap_or(0)
1549 && p.n_gen == Some(n_gen)
1550 })
1551 .map(|p| p.avg_tps)
1552 .unwrap_or(0.0);
1553 body.push_str(&format!(" {}: {:.2}\n", n_gen, value));
1554 }
1555 }
1556 frame.render_widget(
1557 Paragraph::new(body).block(
1558 Block::default()
1559 .borders(Borders::ALL)
1560 .title("Throughput Tables"),
1561 ),
1562 chunks[2],
1563 );
1564 } else {
1565 frame.render_widget(
1566 Paragraph::new("No LLaMA.cpp data available\n\nRun llama-cpp benchmark")
1567 .alignment(Alignment::Left)
1568 .block(
1569 Block::default()
1570 .title("LLaMA.cpp Performance")
1571 .borders(Borders::ALL),
1572 ),
1573 area,
1574 );
1575 }
1576}
1577
1578fn render_vllm_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1579 if let Some(v) = &results.vllm {
1580 let chunks = Layout::default()
1581 .direction(Direction::Vertical)
1582 .constraints([Constraint::Length(4), Constraint::Min(7)])
1583 .split(area);
1584
1585 let mut summary = format!(
1586 "Model: {} ({}) | Throughput: {:.1} tok/s | Latency: {:.1} ms",
1587 v.model, v.model_format, v.throughput_tokens_per_sec, v.latency_ms
1588 );
1589
1590 if let Some(baseline) = &results.baseline {
1591 if let Some(b_vllm) = &baseline.vllm {
1592 if b_vllm.throughput_tokens_per_sec > 0.0 && v.throughput_tokens_per_sec > 0.0 {
1593 let diff = ((v.throughput_tokens_per_sec - b_vllm.throughput_tokens_per_sec)
1594 / b_vllm.throughput_tokens_per_sec)
1595 * 100.0;
1596 summary.push_str(&format!(
1597 " (Baseline: {:.1} | {:+.1}%)",
1598 b_vllm.throughput_tokens_per_sec, diff
1599 ));
1600 }
1601 }
1602 }
1603
1604 let mut summary_text = Text::from(summary);
1605 if v.throughput_tokens_per_sec == 0.0 {
1606 summary_text.lines.push(Line::from(""));
1607 summary_text.lines.push(Line::from(vec![
1608 Span::styled("⚠ WARNING: ", Style::default().fg(Color::Yellow)),
1609 Span::raw(
1610 "Benchmark produced no results. Ensure a tiny safetensors model or local tiny GGUF model is configured.",
1611 ),
1612 ]));
1613
1614 for err in &results.errors {
1616 let lowered = err.to_ascii_lowercase();
1617 if lowered.contains("vllm")
1618 || lowered.contains("safetensors model")
1619 || lowered.contains("torch.cuda")
1620 || lowered.contains("hip gpu")
1621 || lowered.contains("engine core initialization failed")
1622 {
1623 summary_text.lines.push(Line::from(vec![
1624 Span::styled("ERROR: ", Style::default().fg(Color::Red)),
1625 Span::raw(err),
1626 ]));
1627 }
1628 }
1629 }
1630
1631 frame.render_widget(
1632 Paragraph::new(summary_text).block(
1633 Block::default()
1634 .borders(Borders::ALL)
1635 .title("Summary & Comparison"),
1636 ),
1637 chunks[0],
1638 );
1639
1640 let throughput_data = prepare_data(&v.throughput_samples);
1641 let mut datasets = Vec::new();
1642 if !throughput_data.is_empty() {
1643 datasets.push(series_dataset("Throughput", &throughput_data, Color::Cyan));
1644 }
1645 let (y0, y1) = y_bounds(&v.throughput_samples);
1646 let x_bounds = [0.0, (v.throughput_samples.len().max(1) - 1) as f64];
1647
1648 frame.render_widget(
1649 Chart::new(datasets)
1650 .block(
1651 Block::default()
1652 .borders(Borders::ALL)
1653 .title("vLLM Throughput Performance"),
1654 )
1655 .x_axis(
1656 Axis::default()
1657 .title("Benchmark Trial")
1658 .style(Style::default().fg(Color::Gray))
1659 .bounds(x_bounds)
1660 .labels(vec![
1661 Line::from("T1"),
1662 Line::from(format!("T{}", v.throughput_samples.len() / 2 + 1)),
1663 Line::from(format!("T{}", v.throughput_samples.len())),
1664 ]),
1665 )
1666 .y_axis(
1667 Axis::default()
1668 .title("Tokens/s")
1669 .style(Style::default().fg(Color::Gray))
1670 .bounds([y0, y1])
1671 .labels(vec![
1672 Line::from(format!("{:.0}", y0)),
1673 Line::from(format!("{:.0}", (y0 + y1) / 2.0)),
1674 Line::from(format!("{:.0}", y1)),
1675 ]),
1676 ),
1677 chunks[1],
1678 );
1679 } else {
1680 frame.render_widget(
1681 Paragraph::new("No vLLM data available\n\nInstall vLLM and rerun benchmarks")
1682 .alignment(Alignment::Left)
1683 .block(
1684 Block::default()
1685 .title("vLLM Performance")
1686 .borders(Borders::ALL),
1687 ),
1688 area,
1689 );
1690 }
1691}
1692
1693fn render_deepspeed_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1694 if let Some(ds) = &results.deepspeed {
1695 let chunks = Layout::default()
1696 .direction(Direction::Vertical)
1697 .constraints([Constraint::Length(4), Constraint::Min(7)])
1698 .split(area);
1699
1700 let mut summary = format!(
1701 "Accelerator: {} | ZeRO Stage: {} | Throughput: {:.1} samples/s | Latency: {:.1} ms",
1702 ds.accelerator, ds.stage, ds.throughput_samples_per_sec, ds.avg_latency_ms
1703 );
1704
1705 if let Some(baseline) = &results.baseline {
1706 if let Some(b_ds) = &baseline.deepspeed {
1707 if b_ds.throughput_samples_per_sec > 0.0 {
1708 let diff = ((ds.throughput_samples_per_sec - b_ds.throughput_samples_per_sec)
1709 / b_ds.throughput_samples_per_sec)
1710 * 100.0;
1711 summary.push_str(&format!(
1712 " (Baseline: {:.1} | {:+.1}%)",
1713 b_ds.throughput_samples_per_sec, diff
1714 ));
1715 }
1716 }
1717 }
1718
1719 frame.render_widget(
1720 Paragraph::new(summary).block(
1721 Block::default()
1722 .borders(Borders::ALL)
1723 .title("Summary & Comparison"),
1724 ),
1725 chunks[0],
1726 );
1727
1728 let ds_data = prepare_data(&ds.samples);
1729 let mut datasets = Vec::new();
1730 if !ds_data.is_empty() {
1731 datasets.push(series_dataset("DeepSpeed", &ds_data, Color::White));
1732 }
1733 let (y0, y1) = y_bounds(&ds.samples);
1734 let x_bounds = [0.0, (ds.samples.len().max(1) - 1) as f64];
1735
1736 frame.render_widget(
1737 Chart::new(datasets)
1738 .block(
1739 Block::default()
1740 .borders(Borders::ALL)
1741 .title("DeepSpeed Throughput Performance"),
1742 )
1743 .x_axis(
1744 Axis::default()
1745 .title("Benchmark Trial")
1746 .style(Style::default().fg(Color::Gray))
1747 .bounds(x_bounds)
1748 .labels(vec![
1749 Line::from("T1"),
1750 Line::from(format!("T{}", ds.samples.len() / 2 + 1)),
1751 Line::from(format!("T{}", ds.samples.len())),
1752 ]),
1753 )
1754 .y_axis(
1755 Axis::default()
1756 .title("Samples/s")
1757 .style(Style::default().fg(Color::Gray))
1758 .bounds([y0, y1])
1759 .labels(vec![
1760 Line::from(format!("{:.0}", y0)),
1761 Line::from(format!("{:.0}", (y0 + y1) / 2.0)),
1762 Line::from(format!("{:.0}", y1)),
1763 ]),
1764 ),
1765 chunks[1],
1766 );
1767 } else {
1768 frame.render_widget(
1769 Paragraph::new("No DeepSpeed data available\n\nRun DeepSpeed Performance benchmark")
1770 .alignment(Alignment::Left)
1771 .block(
1772 Block::default()
1773 .title("DeepSpeed Performance")
1774 .borders(Borders::ALL),
1775 ),
1776 area,
1777 );
1778 }
1779}
1780
1781fn render_megatron_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1782 if let Some(meg) = &results.megatron {
1783 let chunks = Layout::default()
1784 .direction(Direction::Vertical)
1785 .constraints([Constraint::Length(4), Constraint::Min(7)])
1786 .split(area);
1787
1788 let mut summary = format!(
1789 "Backend: {} | Throughput: {:.1} samples/s | Latency: {:.1} ms",
1790 meg.backend, meg.throughput_samples_per_sec, meg.avg_latency_ms
1791 );
1792
1793 if let Some(baseline) = &results.baseline {
1794 if let Some(b_meg) = &baseline.megatron {
1795 if b_meg.throughput_samples_per_sec > 0.0 {
1796 let diff = ((meg.throughput_samples_per_sec
1797 - b_meg.throughput_samples_per_sec)
1798 / b_meg.throughput_samples_per_sec)
1799 * 100.0;
1800 summary.push_str(&format!(
1801 " (Baseline: {:.1} | {:+.1}%)",
1802 b_meg.throughput_samples_per_sec, diff
1803 ));
1804 }
1805 }
1806 }
1807
1808 frame.render_widget(
1809 Paragraph::new(summary).block(
1810 Block::default()
1811 .borders(Borders::ALL)
1812 .title("Summary & Comparison"),
1813 ),
1814 chunks[0],
1815 );
1816
1817 let series = prepare_data(&meg.samples);
1818 let mut datasets = Vec::new();
1819 if !series.is_empty() {
1820 datasets.push(series_dataset("Megatron", &series, Color::LightBlue));
1821 }
1822 let (y0, y1) = y_bounds(&meg.samples);
1823 let x_bounds = [0.0, (meg.samples.len().max(1) - 1) as f64];
1824
1825 frame.render_widget(
1826 Chart::new(datasets)
1827 .block(
1828 Block::default()
1829 .borders(Borders::ALL)
1830 .title("Megatron Throughput Performance"),
1831 )
1832 .x_axis(
1833 Axis::default()
1834 .title("Benchmark Trial")
1835 .style(Style::default().fg(Color::Gray))
1836 .bounds(x_bounds)
1837 .labels(vec![
1838 Line::from("T1"),
1839 Line::from(format!("T{}", meg.samples.len() / 2 + 1)),
1840 Line::from(format!("T{}", meg.samples.len())),
1841 ]),
1842 )
1843 .y_axis(
1844 Axis::default()
1845 .title("Samples/s")
1846 .style(Style::default().fg(Color::Gray))
1847 .bounds([y0, y1])
1848 .labels(vec![
1849 Line::from(format!("{:.0}", y0)),
1850 Line::from(format!("{:.0}", (y0 + y1) / 2.0)),
1851 Line::from(format!("{:.0}", y1)),
1852 ]),
1853 ),
1854 chunks[1],
1855 );
1856 } else {
1857 frame.render_widget(
1858 Paragraph::new("No Megatron data available\n\nRun Megatron-LM Performance benchmark")
1859 .alignment(Alignment::Left)
1860 .block(
1861 Block::default()
1862 .title("Megatron-LM Performance")
1863 .borders(Borders::ALL),
1864 ),
1865 area,
1866 );
1867 }
1868}
1869
1870fn render_onnx_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1871 if let Some(onnx) = &results.onnx {
1872 let chunks = Layout::default()
1873 .direction(Direction::Vertical)
1874 .constraints([Constraint::Length(6), Constraint::Min(7)])
1875 .split(area);
1876
1877 let mut summary = format!(
1878 "ORT {} | Provider: {} | Opt: {}\nThroughput: {:.1} inf/s | Latency p50: {:.2} ms p95: {:.2} ms p99: {:.2} ms",
1879 onnx.ort_version,
1880 onnx.provider,
1881 onnx.graph_opt_level,
1882 onnx.throughput_inf_per_sec,
1883 onnx.inference_latency_p50_ms,
1884 onnx.inference_latency_p95_ms,
1885 onnx.inference_latency_p99_ms,
1886 );
1887
1888 if onnx.quantized_supported {
1889 summary.push_str(" | Quantized: supported");
1890 }
1891
1892 if onnx.peak_rss_mb > 0.0 {
1893 summary.push_str(&format!(" | Peak RSS: {:.0} MB", onnx.peak_rss_mb));
1894 }
1895
1896 if let Some(baseline) = &results.baseline {
1897 if let Some(b_onnx) = &baseline.onnx {
1898 if b_onnx.throughput_inf_per_sec > 0.0 {
1899 let diff = ((onnx.throughput_inf_per_sec - b_onnx.throughput_inf_per_sec)
1900 / b_onnx.throughput_inf_per_sec)
1901 * 100.0;
1902 summary.push_str(&format!(
1903 "\nBaseline: {:.1} inf/s ({:+.1}%) | Session create: {:.1} ms",
1904 b_onnx.throughput_inf_per_sec, diff, onnx.session_create_ms,
1905 ));
1906 }
1907 }
1908 }
1909
1910 if !onnx.providers_available.is_empty() {
1911 summary.push_str(&format!(
1912 "\nAvailable providers: {}",
1913 onnx.providers_available.join(", ")
1914 ));
1915 }
1916
1917 frame.render_widget(
1918 Paragraph::new(summary).block(
1919 Block::default()
1920 .borders(Borders::ALL)
1921 .title("ONNX Runtime Summary"),
1922 ),
1923 chunks[0],
1924 );
1925
1926 let series = prepare_data(&onnx.inference_samples);
1927 let mut datasets = Vec::new();
1928 if !series.is_empty() {
1929 datasets.push(series_dataset("ONNX Inference", &series, Color::LightGreen));
1930 }
1931 let (y0, y1) = y_bounds(&onnx.inference_samples);
1932 let x_bounds = [0.0, (onnx.inference_samples.len().max(1) - 1) as f64];
1933
1934 frame.render_widget(
1935 Chart::new(datasets)
1936 .block(
1937 Block::default()
1938 .borders(Borders::ALL)
1939 .title("ONNX Inference Latency per Trial"),
1940 )
1941 .x_axis(
1942 Axis::default()
1943 .title("Benchmark Trial")
1944 .style(Style::default().fg(Color::Gray))
1945 .bounds(x_bounds)
1946 .labels(vec![
1947 Line::from("T1"),
1948 Line::from(format!("T{}", onnx.inference_samples.len() / 2 + 1)),
1949 Line::from(format!("T{}", onnx.inference_samples.len())),
1950 ]),
1951 )
1952 .y_axis(
1953 Axis::default()
1954 .title("ms")
1955 .style(Style::default().fg(Color::Gray))
1956 .bounds([y0, y1])
1957 .labels(vec![
1958 Line::from(format!("{:.2}", y0)),
1959 Line::from(format!("{:.2}", (y0 + y1) / 2.0)),
1960 Line::from(format!("{:.2}", y1)),
1961 ]),
1962 ),
1963 chunks[1],
1964 );
1965 } else {
1966 frame.render_widget(
1967 Paragraph::new("No ONNX Runtime data available\n\nRun ONNX Performance benchmark")
1968 .alignment(Alignment::Left)
1969 .block(
1970 Block::default()
1971 .title("ONNX Runtime Performance")
1972 .borders(Borders::ALL),
1973 ),
1974 area,
1975 );
1976 }
1977}
1978
1979fn series_dataset<'a>(name: &'a str, data: &'a [(f64, f64)], color: Color) -> Dataset<'a> {
1980 Dataset::default()
1981 .name(name)
1982 .marker(symbols::Marker::Braille)
1983 .style(Style::default().fg(color))
1984 .graph_type(GraphType::Line)
1985 .data(data)
1986}
1987
1988fn prepare_data(values: &[f64]) -> Vec<(f64, f64)> {
1989 values
1990 .iter()
1991 .enumerate()
1992 .map(|(i, v)| (i as f64, *v))
1993 .collect()
1994}
1995
1996fn y_bounds(values: &[f64]) -> (f64, f64) {
1997 let max = values.iter().cloned().fold(0.0_f64, f64::max);
1998 if max == 0.0 {
1999 (0.0, 1.0)
2000 } else {
2001 (0.0, (max * 1.2).ceil())
2002 }
2003}
2004
2005pub fn export_benchmark_report_html(
2006 results: &BenchmarkResults,
2007 output_path: Option<&Path>,
2008) -> Result<PathBuf, String> {
2009 let report_path = resolve_report_path(output_path)?;
2010 if let Some(parent) = report_path.parent() {
2011 fs::create_dir_all(parent).map_err(|e| {
2012 format!(
2013 "failed to create report directory {}: {}",
2014 parent.display(),
2015 e
2016 )
2017 })?;
2018 }
2019
2020 let generated_at = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
2021 let report_data = serde_json::json!({
2022 "generated_at": generated_at,
2023 "summary": build_summary_rows(results),
2024 "metric_rows": build_metric_rows(results),
2025 "sample_rows": build_sample_rows(results),
2026 "errors": results.errors,
2027 "gpus": results.gpus.iter().map(|g| serde_json::json!({
2028 "index": g.index,
2029 "model": g.model,
2030 "vram_gb": g.vram_gb,
2031 "compute_units": g.compute_units,
2032 "tensor_cores": g.tensor_cores,
2033 "temperature_c": g.temperature_c,
2034 "power_watts": g.power_watts,
2035 "utilization_percent": g.utilization_percent,
2036 "memory_percent": g.memory_percent
2037 })).collect::<Vec<_>>(),
2038 "charts": {
2039 "memory_hbm": results.memory_bandwidth.as_ref().map(|m| m.hbm_samples.clone()).unwrap_or_default(),
2040 "memory_system": results.memory_bandwidth.as_ref().map(|m| m.system_samples.clone()).unwrap_or_default(),
2041 "tensor_fp16": results.tensor_core.as_ref().map(|t| t.fp16_samples.clone()).unwrap_or_default(),
2042 "pytorch_gemm": results.pytorch.as_ref().map(|p| p.gemm_samples.clone()).unwrap_or_default(),
2043 "pytorch_conv": results.pytorch.as_ref().map(|p| p.conv_samples.clone()).unwrap_or_default(),
2044 "llama_cpp": results.llama_cpp.as_ref().map(|l| l.prefill.iter().map(|p| p.avg_tps).collect::<Vec<_>>()).unwrap_or_default(),
2045 "flash_standard": results.flash_attention.as_ref().map(|f| f.standard_samples.clone()).unwrap_or_default(),
2046 "flash_optimized": results.flash_attention.as_ref().map(|f| f.flash_samples.clone()).unwrap_or_default(),
2047 "vllm": results.vllm.as_ref().map(|v| v.throughput_samples.clone()).unwrap_or_default(),
2048 "deepspeed": results.deepspeed.as_ref().map(|d| d.samples.clone()).unwrap_or_default(),
2049 "megatron": results.megatron.as_ref().map(|m| m.samples.clone()).unwrap_or_default(),
2050 "onnx": results.onnx.as_ref().map(|o| o.inference_samples.clone()).unwrap_or_default()
2051 },
2052 "key_metrics": {
2053 "memory_hbm_peak": results.memory_bandwidth.as_ref().map(|m| m.hbm_peak_gb_s).unwrap_or(0.0),
2054 "tensor_fp16_tflops": results.tensor_core.as_ref().map(|t| t.fp16_tflops).unwrap_or(0.0),
2055 "pytorch_gemm_gflops": results.pytorch.as_ref().map(|p| p.gemm_gflops).unwrap_or(0.0),
2056 "llama_cpp_model": results.llama_cpp.as_ref().map(|l| l.model.clone()).unwrap_or_default(),
2057 "flash_speedup": results.flash_attention.as_ref().map(|f| f.speedup).unwrap_or(0.0),
2058 "vllm_tps": results.vllm.as_ref().map(|v| v.throughput_tokens_per_sec).unwrap_or(0.0),
2059 "deepspeed_sps": results.deepspeed.as_ref().map(|d| d.throughput_samples_per_sec).unwrap_or(0.0),
2060 "megatron_sps": results.megatron.as_ref().map(|m| m.throughput_samples_per_sec).unwrap_or(0.0),
2061 "onnx_inf_s": results.onnx.as_ref().map(|o| o.throughput_inf_per_sec).unwrap_or(0.0),
2062 "onnx_p50_ms": results.onnx.as_ref().map(|o| o.inference_latency_p50_ms).unwrap_or(0.0),
2063 "onnx_provider": results.onnx.as_ref().map(|o| o.provider.clone()).unwrap_or_default(),
2064 "onnx_ort_version": results.onnx.as_ref().map(|o| o.ort_version.clone()).unwrap_or_default()
2065 }
2066 });
2067 let report_json = serde_json::to_string_pretty(&report_data)
2068 .map_err(|e| format!("json serialization failed: {}", e))?;
2069
2070 let html = format!(
2071 r###"<!doctype html>
2072<html lang="en">
2073<head>
2074 <meta charset="utf-8" />
2075 <meta name="viewport" content="width=device-width, initial-scale=1" />
2076 <title>Rusty-Stack Benchmark Report</title>
2077 <style>
2078 :root {{
2079 --bg-0: #0b1220;
2080 --bg-1: #111a2d;
2081 --bg-2: #18243a;
2082 --card: #0f1728;
2083 --text: #e8edf9;
2084 --muted: #9fb0d0;
2085 --accent-a: #27d7c6;
2086 --accent-b: #74a7ff;
2087 --accent-c: #ffa24d;
2088 --ok: #3ddc84;
2089 --warn: #ffd166;
2090 --err: #ff6b6b;
2091 --line: #2a3a57;
2092 }}
2093 * {{ box-sizing: border-box; }}
2094 body {{
2095 margin: 0;
2096 font-family: "Space Grotesk", "Sora", "Segoe UI", sans-serif;
2097 color: var(--text);
2098 background:
2099 radial-gradient(900px 420px at 10% -10%, #1e365f 0%, transparent 60%),
2100 radial-gradient(700px 380px at 100% 0%, #23334f 0%, transparent 60%),
2101 linear-gradient(160deg, var(--bg-0), var(--bg-1), var(--bg-2));
2102 min-height: 100vh;
2103 padding: 24px;
2104 }}
2105 .wrap {{
2106 max-width: 1440px;
2107 margin: 0 auto;
2108 animation: fadeIn 420ms ease-out;
2109 }}
2110 .hero {{
2111 border: 1px solid var(--line);
2112 border-radius: 18px;
2113 padding: 20px 24px;
2114 background: linear-gradient(160deg, rgba(39, 215, 198, 0.08), rgba(116, 167, 255, 0.06), rgba(255, 162, 77, 0.08));
2115 backdrop-filter: blur(4px);
2116 }}
2117 .hero h1 {{
2118 margin: 0;
2119 font-size: 30px;
2120 letter-spacing: 0.2px;
2121 }}
2122 .meta {{
2123 margin-top: 10px;
2124 color: var(--muted);
2125 font-size: 14px;
2126 }}
2127 .grid {{
2128 margin-top: 20px;
2129 display: grid;
2130 gap: 14px;
2131 grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
2132 }}
2133 .card {{
2134 border: 1px solid var(--line);
2135 border-radius: 14px;
2136 background: rgba(8, 14, 24, 0.75);
2137 padding: 14px;
2138 transform: translateY(10px);
2139 opacity: 0;
2140 animation: rise 400ms ease forwards;
2141 }}
2142 .k {{
2143 color: var(--muted);
2144 font-size: 12px;
2145 text-transform: uppercase;
2146 letter-spacing: 0.7px;
2147 }}
2148 .v {{
2149 margin-top: 8px;
2150 font-size: 28px;
2151 font-weight: 700;
2152 color: var(--accent-a);
2153 }}
2154 .section {{
2155 margin-top: 18px;
2156 border: 1px solid var(--line);
2157 border-radius: 14px;
2158 background: rgba(8, 14, 24, 0.72);
2159 padding: 14px;
2160 }}
2161 h2 {{
2162 margin: 0 0 10px 0;
2163 font-size: 20px;
2164 color: #f3f7ff;
2165 }}
2166 table {{
2167 width: 100%;
2168 border-collapse: collapse;
2169 font-size: 13px;
2170 }}
2171 th, td {{
2172 border-bottom: 1px solid #243452;
2173 padding: 8px 10px;
2174 text-align: left;
2175 vertical-align: top;
2176 }}
2177 th {{
2178 color: #bfccdf;
2179 font-weight: 600;
2180 }}
2181 .charts {{
2182 display: grid;
2183 gap: 14px;
2184 grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
2185 }}
2186 .plot {{
2187 border: 1px solid #253659;
2188 border-radius: 12px;
2189 padding: 10px;
2190 background: #0a1322;
2191 }}
2192 .plot h3 {{
2193 margin: 0 0 8px 0;
2194 font-size: 15px;
2195 color: #dce8ff;
2196 }}
2197 canvas {{
2198 width: 100%;
2199 height: 220px;
2200 border-radius: 8px;
2201 background: linear-gradient(180deg, rgba(26,41,66,0.35), rgba(11,18,32,0.65));
2202 }}
2203 .chip {{
2204 display: inline-block;
2205 padding: 3px 8px;
2206 border-radius: 999px;
2207 font-size: 12px;
2208 border: 1px solid #38517a;
2209 color: #b8c9e7;
2210 margin-right: 6px;
2211 margin-bottom: 6px;
2212 }}
2213 .err li {{
2214 margin-bottom: 6px;
2215 color: #ffd4d4;
2216 }}
2217 @keyframes fadeIn {{
2218 from {{ opacity: 0; }}
2219 to {{ opacity: 1; }}
2220 }}
2221 @keyframes rise {{
2222 from {{ opacity: 0; transform: translateY(10px); }}
2223 to {{ opacity: 1; transform: translateY(0); }}
2224 }}
2225 @media (max-width: 900px) {{
2226 .charts {{ grid-template-columns: 1fr; }}
2227 body {{ padding: 12px; }}
2228 }}
2229 </style>
2230</head>
2231<body>
2232 <div class="wrap">
2233 <div class="hero">
2234 <h1>Rusty-Stack Performance Report</h1>
2235 <div class="meta">Generated: <span id="generatedAt"></span></div>
2236 <div class="meta">Profile: Tiny safetensors/gguf benchmark policy enabled for model-based tests.</div>
2237 </div>
2238
2239 <div class="grid" id="metricCards"></div>
2240
2241 <div class="section">
2242 <h2>Summary Table</h2>
2243 <table id="summaryTable">
2244 <thead><tr><th>Component</th><th>Status</th><th>Primary Metric</th><th>Detail</th></tr></thead>
2245 <tbody></tbody>
2246 </table>
2247 </div>
2248
2249 <div class="section">
2250 <h2>Detailed Metrics</h2>
2251 <table id="metricsTable">
2252 <thead><tr><th>Component</th><th>Metric</th><th>Value</th><th>Unit</th></tr></thead>
2253 <tbody></tbody>
2254 </table>
2255 </div>
2256
2257 <div class="section">
2258 <h2>Sample Data (Text Summary)</h2>
2259 <table id="samplesTable">
2260 <thead><tr><th>Benchmark</th><th>Series</th><th>Samples</th></tr></thead>
2261 <tbody></tbody>
2262 </table>
2263 </div>
2264
2265 <div class="section">
2266 <h2>GPU Inventory</h2>
2267 <div id="gpuChips"></div>
2268 <table id="gpuTable">
2269 <thead><tr><th>GPU</th><th>VRAM (GB)</th><th>CUs</th><th>Tensor Cores</th><th>Utilization (%)</th></tr></thead>
2270 <tbody></tbody>
2271 </table>
2272 </div>
2273
2274 <div class="section">
2275 <h2>Benchmark Graphs</h2>
2276 <div class="charts">
2277 <div class="plot"><h3>Memory Bandwidth</h3><canvas id="chartMemory"></canvas></div>
2278 <div class="plot"><h3>Tensor Core FP16</h3><canvas id="chartTensor"></canvas></div>
2279 <div class="plot"><h3>PyTorch (GEMM/Conv)</h3><canvas id="chartPytorch"></canvas></div>
2280 <div class="plot"><h3>LLaMA.cpp Prefill</h3><canvas id="chartLlamaCpp"></canvas></div>
2281 <div class="plot"><h3>Flash Attention</h3><canvas id="chartFlash"></canvas></div>
2282 <div class="plot"><h3>vLLM Throughput</h3><canvas id="chartVllm"></canvas></div>
2283 <div class="plot"><h3>DeepSpeed Throughput</h3><canvas id="chartDeepspeed"></canvas></div>
2284 <div class="plot"><h3>Megatron Throughput</h3><canvas id="chartMegatron"></canvas></div>
2285 <div class="plot"><h3>ONNX Inference Latency</h3><canvas id="chartOnnx"></canvas></div>
2286 </div>
2287 </div>
2288
2289 <div class="section">
2290 <h2>Errors / Warnings</h2>
2291 <ul class="err" id="errors"></ul>
2292 </div>
2293 </div>
2294
2295 <script>
2296 const reportData = {report_json};
2297 document.getElementById("generatedAt").textContent = reportData.generated_at;
2298
2299 const cards = [
2300 ["HBM Peak (GB/s)", reportData.key_metrics.memory_hbm_peak],
2301 ["FP16 TFLOPS", reportData.key_metrics.tensor_fp16_tflops],
2302 ["PyTorch GEMM (GFLOPS)", reportData.key_metrics.pytorch_gemm_gflops],
2303 ["LLaMA.cpp Model", reportData.key_metrics.llama_cpp_model],
2304 ["Flash Speedup (x)", reportData.key_metrics.flash_speedup],
2305 ["vLLM tok/s", reportData.key_metrics.vllm_tps],
2306 ["DeepSpeed samp/s", reportData.key_metrics.deepspeed_sps],
2307 ["Megatron samp/s", reportData.key_metrics.megatron_sps],
2308 ["ONNX inf/s", reportData.key_metrics.onnx_inf_s]
2309 ];
2310 const metricCards = document.getElementById("metricCards");
2311 cards.forEach((entry, idx) => {{
2312 const c = document.createElement("div");
2313 c.className = "card";
2314 c.style.animationDelay = `${{idx * 35}}ms`;
2315 c.innerHTML = `<div class="k">${{entry[0]}}</div><div class="v">${{Number(entry[1] || 0).toFixed(2)}}</div>`;
2316 metricCards.appendChild(c);
2317 }});
2318
2319 const tbody = document.querySelector("#summaryTable tbody");
2320 (reportData.summary || []).forEach(row => {{
2321 const tr = document.createElement("tr");
2322 tr.innerHTML = `<td>${{row.component}}</td><td>${{row.status}}</td><td>${{row.metric}}</td><td>${{row.detail}}</td>`;
2323 tbody.appendChild(tr);
2324 }});
2325
2326 const metricsTbody = document.querySelector("#metricsTable tbody");
2327 (reportData.metric_rows || []).forEach(row => {{
2328 const tr = document.createElement("tr");
2329 tr.innerHTML = `<td>${{row.component}}</td><td>${{row.metric}}</td><td>${{Number(row.value || 0).toFixed(2)}}</td><td>${{row.unit}}</td>`;
2330 metricsTbody.appendChild(tr);
2331 }});
2332
2333 const samplesTbody = document.querySelector("#samplesTable tbody");
2334 (reportData.sample_rows || []).forEach(row => {{
2335 const tr = document.createElement("tr");
2336 tr.innerHTML = `<td>${{row.benchmark}}</td><td>${{row.series}}</td><td>${{(row.samples || []).map(v => Number(v).toFixed(2)).join(", ") || "-"}}</td>`;
2337 samplesTbody.appendChild(tr);
2338 }});
2339
2340 const gpuChips = document.getElementById("gpuChips");
2341 const gpuTbody = document.querySelector("#gpuTable tbody");
2342 (reportData.gpus || []).forEach(g => {{
2343 const chip = document.createElement("span");
2344 chip.className = "chip";
2345 chip.textContent = `GPU ${{g.index}} • ${{g.model}}`;
2346 gpuChips.appendChild(chip);
2347
2348 const tr = document.createElement("tr");
2349 tr.innerHTML = `<td>${{g.model}}</td><td>${{Number(g.vram_gb||0).toFixed(2)}}</td><td>${{g.compute_units}}</td><td>${{g.tensor_cores ? "yes" : "no"}}</td><td>${{g.utilization_percent ?? "-"}}</td>`;
2350 gpuTbody.appendChild(tr);
2351 }});
2352
2353 const errNode = document.getElementById("errors");
2354 if ((reportData.errors || []).length === 0) {{
2355 const li = document.createElement("li");
2356 li.textContent = "No benchmark errors were recorded.";
2357 errNode.appendChild(li);
2358 }} else {{
2359 reportData.errors.forEach(e => {{
2360 const li = document.createElement("li");
2361 li.textContent = e;
2362 errNode.appendChild(li);
2363 }});
2364 }}
2365
2366 function drawMultiLine(canvasId, series) {{
2367 const canvas = document.getElementById(canvasId);
2368 if (!canvas) return;
2369 const ctx = canvas.getContext("2d");
2370 const dpr = window.devicePixelRatio || 1;
2371 const w = canvas.clientWidth;
2372 const h = canvas.clientHeight;
2373 canvas.width = Math.max(1, Math.floor(w * dpr));
2374 canvas.height = Math.max(1, Math.floor(h * dpr));
2375 ctx.scale(dpr, dpr);
2376 ctx.clearRect(0, 0, w, h);
2377
2378 const all = series.flatMap(s => s.values || []);
2379 if (all.length === 0) {{
2380 ctx.fillStyle = "#9fb0d0";
2381 ctx.font = "14px sans-serif";
2382 ctx.fillText("No data", 16, 24);
2383 return;
2384 }}
2385 const maxV = Math.max(...all, 1);
2386 const minV = Math.min(...all, 0);
2387 const pad = 34;
2388 const x0 = pad, y0 = h - pad, x1 = w - pad, y1 = pad;
2389 const yRange = Math.max(maxV - minV, 1e-9);
2390 const yTicks = 4;
2391 const plotY = (v, t=1) => y0 - ((v - minV) / yRange) * (y0 - y1) * t;
2392
2393 let start = null;
2394 function frame(ts) {{
2395 if (!start) start = ts;
2396 const t = Math.min(1, (ts - start) / 900);
2397 ctx.clearRect(0, 0, w, h);
2398
2399 // Grid and axes with numeric labels
2400 ctx.font = "11px Space Grotesk, Segoe UI, sans-serif";
2401 ctx.fillStyle = "#9fb0d0";
2402 ctx.strokeStyle = "rgba(115,140,180,0.22)";
2403 ctx.lineWidth = 1;
2404 for (let i = 0; i <= yTicks; i++) {{
2405 const yv = minV + (yRange * i / yTicks);
2406 const py = plotY(yv);
2407 ctx.beginPath();
2408 ctx.moveTo(x0, py);
2409 ctx.lineTo(x1, py);
2410 ctx.stroke();
2411 ctx.fillText(Number(yv).toFixed(2), 4, py + 3);
2412 }}
2413
2414 const maxLen = Math.max(1, ...series.map(s => (s.values || []).length));
2415 const xTicks = Math.min(4, Math.max(1, maxLen - 1));
2416 for (let i = 0; i <= xTicks; i++) {{
2417 const xi = maxLen === 1 ? 0 : Math.round(((maxLen - 1) * i) / xTicks);
2418 const px = maxLen === 1 ? (x0 + x1) / 2 : x0 + (xi / (maxLen - 1)) * (x1 - x0);
2419 ctx.beginPath();
2420 ctx.moveTo(px, y0);
2421 ctx.lineTo(px, y0 + 4);
2422 ctx.stroke();
2423 ctx.fillText(`S${{xi + 1}}`, px - 9, y0 + 16);
2424 }}
2425
2426 ctx.strokeStyle = "rgba(115,140,180,0.35)";
2427 ctx.lineWidth = 1;
2428 ctx.beginPath();
2429 ctx.moveTo(x0, y0); ctx.lineTo(x1, y0); ctx.lineTo(x1, y1); ctx.stroke();
2430
2431 series.forEach((s, si) => {{
2432 const vals = s.values || [];
2433 if (vals.length < 1) return;
2434 ctx.strokeStyle = s.color;
2435 ctx.lineWidth = 2;
2436 ctx.beginPath();
2437 vals.forEach((v, i) => {{
2438 const px = vals.length === 1 ? (x0 + x1) / 2 : x0 + (i / (vals.length - 1)) * (x1 - x0);
2439 const py = plotY(v, t);
2440 if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
2441 }});
2442 ctx.stroke();
2443
2444 // Data points
2445 ctx.fillStyle = s.color;
2446 vals.forEach((v, i) => {{
2447 const px = vals.length === 1 ? (x0 + x1) / 2 : x0 + (i / (vals.length - 1)) * (x1 - x0);
2448 const py = plotY(v, t);
2449 ctx.beginPath();
2450 ctx.arc(px, py, 2.5, 0, Math.PI * 2);
2451 ctx.fill();
2452 }});
2453
2454 // Simple legend
2455 const ly = y1 + 14 + si * 14;
2456 ctx.fillStyle = s.color;
2457 ctx.fillRect(x1 - 120, ly - 8, 10, 10);
2458 ctx.fillStyle = "#dce8ff";
2459 ctx.fillText(s.name, x1 - 105, ly);
2460 }});
2461 if (t < 1) requestAnimationFrame(frame);
2462 }}
2463 requestAnimationFrame(frame);
2464 }}
2465
2466 drawMultiLine("chartMemory", [
2467 {{name: "HBM", color: "#27d7c6", values: reportData.charts.memory_hbm}},
2468 {{name: "System", color: "#ffd166", values: reportData.charts.memory_system}}
2469 ]);
2470 drawMultiLine("chartTensor", [{{name: "FP16", color: "#74a7ff", values: reportData.charts.tensor_fp16}}]);
2471 drawMultiLine("chartPytorch", [
2472 {{name: "GEMM", color: "#3ddc84", values: reportData.charts.pytorch_gemm}},
2473 {{name: "Conv", color: "#ffa24d", values: reportData.charts.pytorch_conv}}
2474 ]);
2475 drawMultiLine("chartLlamaCpp", [
2476 {{name: "LLaMA.cpp", color: "#74a7ff", values: reportData.charts.llama_cpp}}
2477 ]);
2478 drawMultiLine("chartFlash", [
2479 {{name: "Standard", color: "#8a99b6", values: reportData.charts.flash_standard}},
2480 {{name: "Optimized", color: "#27d7c6", values: reportData.charts.flash_optimized}}
2481 ]);
2482 drawMultiLine("chartVllm", [{{name: "vLLM", color: "#74a7ff", values: reportData.charts.vllm}}]);
2483 drawMultiLine("chartDeepspeed", [{{name: "DeepSpeed", color: "#ffd166", values: reportData.charts.deepspeed}}]);
2484 drawMultiLine("chartMegatron", [{{name: "Megatron", color: "#ff8fab", values: reportData.charts.megatron}}]);
2485 drawMultiLine("chartOnnx", [{{name: "ONNX Inference", color: "#90ee90", values: reportData.charts.onnx}}]);
2486 </script>
2487</body>
2488</html>"###
2489 );
2490
2491 fs::write(&report_path, html)
2492 .map_err(|e| format!("failed to write report {}: {}", report_path.display(), e))?;
2493 Ok(report_path)
2494}
2495
2496fn resolve_report_path(output_path: Option<&Path>) -> Result<PathBuf, String> {
2497 if let Some(path) = output_path {
2498 return Ok(path.to_path_buf());
2499 }
2500 if let Ok(path) = std::env::var("MLSTACK_BENCHMARK_REPORT_PATH") {
2501 let trimmed = path.trim();
2502 if !trimmed.is_empty() {
2503 return Ok(PathBuf::from(trimmed));
2504 }
2505 }
2506 let base = std::env::var("HOME")
2507 .map(|h| Path::new(&h).join(".mlstack").join("reports"))
2508 .unwrap_or_else(|_| PathBuf::from("/tmp/mlstack/reports"));
2509 let file = format!(
2510 "benchmark_report_{}.html",
2511 Local::now().format("%Y%m%d_%H%M%S")
2512 );
2513 Ok(base.join(file))
2514}
2515
2516fn build_summary_rows(results: &BenchmarkResults) -> Vec<serde_json::Value> {
2517 let mut rows = Vec::new();
2518
2519 rows.push(serde_json::json!({
2520 "component": "GPU",
2521 "status": if results.gpus.is_empty() { "missing" } else { "ok" },
2522 "metric": format!("{} GPU(s)", results.gpus.len()),
2523 "detail": results.gpus.iter().map(|g| g.model.clone()).collect::<Vec<_>>().join(" | ")
2524 }));
2525
2526 if let Some(m) = &results.memory_bandwidth {
2527 rows.push(serde_json::json!({
2528 "component": "Memory Bandwidth",
2529 "status": "ok",
2530 "metric": format!("{:.2} GB/s", m.hbm_peak_gb_s),
2531 "detail": format!("System {:.2} GB/s, ratio {:.2}x", m.system_peak_gb_s, m.hbm_ratio)
2532 }));
2533 }
2534 if let Some(t) = &results.tensor_core {
2535 rows.push(serde_json::json!({
2536 "component": "Tensor Core",
2537 "status": "ok",
2538 "metric": format!("{:.2} TFLOPS", t.fp16_tflops),
2539 "detail": format!("BF16 {:.2} TFLOPS, FP32 {:.2} TFLOPS", t.bf16_tflops, t.fp32_tflops)
2540 }));
2541 }
2542 if let Some(p) = &results.pytorch {
2543 rows.push(serde_json::json!({
2544 "component": "PyTorch",
2545 "status": "ok",
2546 "metric": format!("{:.2} GFLOPS", p.gemm_gflops),
2547 "detail": format!("Conv {:.2} GFLOPS, autograd overhead {:.2}%", p.convolution_gflops, p.autograd_overhead_percent)
2548 }));
2549 }
2550 if let Some(l) = &results.llama_cpp {
2551 rows.push(serde_json::json!({
2552 "component": "LLaMA.cpp",
2553 "status": "ok",
2554 "metric": format!("{} GPUs", l.gpus.len()),
2555 "detail": format!("Model {} | ROCm {}", l.model, l.rocm_version)
2556 }));
2557 }
2558 if let Some(f) = &results.flash_attention {
2559 rows.push(serde_json::json!({
2560 "component": "Flash Attention",
2561 "status": "ok",
2562 "metric": format!("{:.2} tok/s", f.flash_attention_speed),
2563 "detail": format!("Standard {:.2} tok/s, speedup {:.2}x, saved {:.3} GB", f.standard_attention_speed, f.speedup, f.memory_savings_gb)
2564 }));
2565 }
2566 if let Some(v) = &results.vllm {
2567 rows.push(serde_json::json!({
2568 "component": "vLLM",
2569 "status": if v.throughput_tokens_per_sec > 0.0 { "ok" } else { "degraded" },
2570 "metric": format!("{:.2} tok/s", v.throughput_tokens_per_sec),
2571 "detail": format!("Model: {} ({}), latency {:.2} ms", v.model, v.model_format, v.latency_ms)
2572 }));
2573 }
2574 if let Some(d) = &results.deepspeed {
2575 rows.push(serde_json::json!({
2576 "component": "DeepSpeed",
2577 "status": if d.throughput_samples_per_sec > 0.0 { "ok" } else { "degraded" },
2578 "metric": format!("{:.2} samples/s", d.throughput_samples_per_sec),
2579 "detail": format!("{} stage {}, latency {:.2} ms", d.accelerator, d.stage, d.avg_latency_ms)
2580 }));
2581 }
2582 if let Some(m) = &results.megatron {
2583 rows.push(serde_json::json!({
2584 "component": "Megatron-LM",
2585 "status": if m.throughput_samples_per_sec > 0.0 { "ok" } else { "degraded" },
2586 "metric": format!("{:.2} samples/s", m.throughput_samples_per_sec),
2587 "detail": format!("backend {}, latency {:.2} ms", m.backend, m.avg_latency_ms)
2588 }));
2589 }
2590 if let Some(o) = &results.onnx {
2591 rows.push(serde_json::json!({
2592 "component": "ONNX Runtime",
2593 "status": if o.throughput_inf_per_sec > 0.0 { "ok" } else { "degraded" },
2594 "metric": format!("{:.2} inf/s", o.throughput_inf_per_sec),
2595 "detail": format!("{} v{}, provider {}, latency p50 {:.2} ms p95 {:.2} ms p99 {:.2} ms", o.ort_version, "", o.provider, o.inference_latency_p50_ms, o.inference_latency_p95_ms, o.inference_latency_p99_ms)
2596 }));
2597 }
2598
2599 rows
2600}
2601
2602fn build_metric_rows(results: &BenchmarkResults) -> Vec<serde_json::Value> {
2603 let mut rows = Vec::new();
2604
2605 if let Some(m) = &results.memory_bandwidth {
2606 rows.push(serde_json::json!({"component":"Memory","metric":"HBM Peak","value":m.hbm_peak_gb_s,"unit":"GB/s"}));
2607 rows.push(serde_json::json!({"component":"Memory","metric":"System Peak","value":m.system_peak_gb_s,"unit":"GB/s"}));
2608 rows.push(serde_json::json!({"component":"Memory","metric":"HBM Ratio","value":m.hbm_ratio,"unit":"x"}));
2609 }
2610 if let Some(t) = &results.tensor_core {
2611 rows.push(serde_json::json!({"component":"Tensor Core","metric":"FP16","value":t.fp16_tflops,"unit":"TFLOPS"}));
2612 rows.push(serde_json::json!({"component":"Tensor Core","metric":"BF16","value":t.bf16_tflops,"unit":"TFLOPS"}));
2613 rows.push(serde_json::json!({"component":"Tensor Core","metric":"FP32","value":t.fp32_tflops,"unit":"TFLOPS"}));
2614 }
2615 if let Some(p) = &results.pytorch {
2616 rows.push(serde_json::json!({"component":"PyTorch","metric":"GEMM","value":p.gemm_gflops,"unit":"GFLOPS"}));
2617 rows.push(serde_json::json!({"component":"PyTorch","metric":"Convolution","value":p.convolution_gflops,"unit":"GFLOPS"}));
2618 rows.push(serde_json::json!({"component":"PyTorch","metric":"Autograd Overhead","value":p.autograd_overhead_percent,"unit":"%"}));
2619 }
2620 if let Some(l) = &results.llama_cpp {
2621 rows.push(serde_json::json!({"component":"LLaMA.cpp","metric":"Prefill Tokens/s","value":l.prefill.iter().map(|p| p.avg_tps).sum::<f64>(),"unit":"t/s"}));
2622 rows.push(serde_json::json!({"component":"LLaMA.cpp","metric":"Decode Tokens/s","value":l.decode.iter().map(|p| p.avg_tps).sum::<f64>(),"unit":"t/s"}));
2623 }
2624 if let Some(f) = &results.flash_attention {
2625 rows.push(serde_json::json!({"component":"Flash Attention","metric":"Standard","value":f.standard_attention_speed,"unit":"tok/s"}));
2626 rows.push(serde_json::json!({"component":"Flash Attention","metric":"Flash","value":f.flash_attention_speed,"unit":"tok/s"}));
2627 rows.push(serde_json::json!({"component":"Flash Attention","metric":"Speedup","value":f.speedup,"unit":"x"}));
2628 }
2629 if let Some(v) = &results.vllm {
2630 rows.push(serde_json::json!({"component":"vLLM","metric":"Throughput","value":v.throughput_tokens_per_sec,"unit":"tok/s"}));
2631 rows.push(serde_json::json!({"component":"vLLM","metric":"Latency","value":v.latency_ms,"unit":"ms"}));
2632 }
2633 if let Some(d) = &results.deepspeed {
2634 rows.push(serde_json::json!({"component":"DeepSpeed","metric":"Throughput","value":d.throughput_samples_per_sec,"unit":"samples/s"}));
2635 rows.push(serde_json::json!({"component":"DeepSpeed","metric":"Latency","value":d.avg_latency_ms,"unit":"ms"}));
2636 }
2637 if let Some(m) = &results.megatron {
2638 rows.push(serde_json::json!({"component":"Megatron","metric":"Throughput","value":m.throughput_samples_per_sec,"unit":"samples/s"}));
2639 rows.push(serde_json::json!({"component":"Megatron","metric":"Latency","value":m.avg_latency_ms,"unit":"ms"}));
2640 }
2641 if let Some(o) = &results.onnx {
2642 rows.push(serde_json::json!({"component":"ONNX","metric":"Throughput","value":o.throughput_inf_per_sec,"unit":"inf/s"}));
2643 rows.push(serde_json::json!({"component":"ONNX","metric":"Latency p50","value":o.inference_latency_p50_ms,"unit":"ms"}));
2644 rows.push(serde_json::json!({"component":"ONNX","metric":"Latency p95","value":o.inference_latency_p95_ms,"unit":"ms"}));
2645 rows.push(serde_json::json!({"component":"ONNX","metric":"Latency p99","value":o.inference_latency_p99_ms,"unit":"ms"}));
2646 rows.push(serde_json::json!({"component":"ONNX","metric":"Session Create","value":o.session_create_ms,"unit":"ms"}));
2647 rows.push(serde_json::json!({"component":"ONNX","metric":"Peak RSS","value":o.peak_rss_mb,"unit":"MB"}));
2648 }
2649
2650 rows
2651}
2652
2653fn build_sample_rows(results: &BenchmarkResults) -> Vec<serde_json::Value> {
2654 let mut rows = Vec::new();
2655
2656 if let Some(m) = &results.memory_bandwidth {
2657 rows.push(
2658 serde_json::json!({"benchmark":"Memory","series":"HBM GB/s","samples":m.hbm_samples}),
2659 );
2660 rows.push(serde_json::json!({"benchmark":"Memory","series":"System GB/s","samples":m.system_samples}));
2661 }
2662 if let Some(t) = &results.tensor_core {
2663 rows.push(serde_json::json!({"benchmark":"Tensor Core","series":"FP16 TFLOPS","samples":t.fp16_samples}));
2664 }
2665 if let Some(p) = &results.pytorch {
2666 rows.push(serde_json::json!({"benchmark":"PyTorch","series":"GEMM GFLOPS","samples":p.gemm_samples}));
2667 rows.push(serde_json::json!({"benchmark":"PyTorch","series":"Conv GFLOPS","samples":p.conv_samples}));
2668 }
2669 if let Some(l) = &results.llama_cpp {
2670 rows.push(serde_json::json!({"benchmark":"LLaMA.cpp","series":"Prefill t/s","samples":l.prefill.iter().map(|p| p.avg_tps).collect::<Vec<_>>()}));
2671 rows.push(serde_json::json!({"benchmark":"LLaMA.cpp","series":"Decode t/s","samples":l.decode.iter().map(|p| p.avg_tps).collect::<Vec<_>>()}));
2672 }
2673 if let Some(f) = &results.flash_attention {
2674 rows.push(serde_json::json!({"benchmark":"Flash Attention","series":"Standard tok/s","samples":f.standard_samples}));
2675 rows.push(serde_json::json!({"benchmark":"Flash Attention","series":"Flash tok/s","samples":f.flash_samples}));
2676 }
2677 if let Some(v) = &results.vllm {
2678 rows.push(serde_json::json!({"benchmark":"vLLM","series":"Throughput tok/s","samples":v.throughput_samples}));
2679 }
2680 if let Some(d) = &results.deepspeed {
2681 rows.push(serde_json::json!({"benchmark":"DeepSpeed","series":"Throughput samples/s","samples":d.samples}));
2682 }
2683 if let Some(m) = &results.megatron {
2684 rows.push(serde_json::json!({"benchmark":"Megatron","series":"Throughput samples/s","samples":m.samples}));
2685 }
2686 if let Some(o) = &results.onnx {
2687 rows.push(serde_json::json!({"benchmark":"ONNX","series":"Inference latency ms","samples":o.inference_samples}));
2688 }
2689
2690 rows
2691}
2692
2693#[cfg(test)]
2694mod tests {
2695 use super::*;
2696 use std::sync::Mutex;
2697
2698 static ENV_LOCK: Mutex<()> = Mutex::new(());
2699
2700 fn unique_test_home(name: &str) -> PathBuf {
2701 std::env::temp_dir().join(format!("rusty-stack-{name}-{}", std::process::id()))
2702 }
2703
2704 struct ScopedEnv {
2712 home: PathBuf,
2713 old_home: Option<String>,
2714 old_tmpdir: Option<String>,
2715 }
2716
2717 impl ScopedEnv {
2718 fn new(home: PathBuf) -> Self {
2722 let old_home = std::env::var("HOME").ok();
2723 let old_tmpdir = std::env::var("TMPDIR").ok();
2724 std::env::set_var("HOME", &home);
2725 let tmp = home.join("tmp");
2726 let _ = fs::create_dir_all(&tmp);
2727 std::env::set_var("TMPDIR", &tmp);
2728 ScopedEnv {
2729 home,
2730 old_home,
2731 old_tmpdir,
2732 }
2733 }
2734 }
2735
2736 impl Drop for ScopedEnv {
2737 fn drop(&mut self) {
2738 match &self.old_home {
2739 Some(v) => std::env::set_var("HOME", v),
2740 None => std::env::remove_var("HOME"),
2741 }
2742 match &self.old_tmpdir {
2743 Some(v) => std::env::set_var("TMPDIR", v),
2744 None => std::env::remove_var("TMPDIR"),
2745 }
2746 let _ = fs::remove_dir_all(&self.home);
2747 }
2748 }
2749
2750 #[test]
2751 fn load_benchmark_results_reads_native_benchmark_filenames() {
2752 let _global_env = crate::test_support::lock_env();
2753 let _guard = ENV_LOCK.lock().unwrap();
2754 let home = unique_test_home("native-benchmark-filenames");
2755 let _ = fs::remove_dir_all(&home);
2756 let log_dir = home.join(".rusty-stack").join("logs");
2757 fs::create_dir_all(&log_dir).unwrap();
2758 let _env = ScopedEnv::new(home);
2762
2763 let gpu_log = log_dir.join("gpu-capability_benchmarks_1.json");
2764 let mem_log = log_dir.join("memory-bandwidth_benchmarks_2.json");
2765 fs::write(
2766 &gpu_log,
2767 r#"{
2768 "name": "gpu-capability",
2769 "success": true,
2770 "execution_time_ms": 1,
2771 "results": {
2772 "gpus": [{"index": 0, "gpu_model": "AMD Radeon RX 7900 XTX", "vram_gb": 24.0}]
2773 },
2774 "errors": []
2775 }"#,
2776 )
2777 .unwrap();
2778 fs::write(
2779 &mem_log,
2780 r#"{
2781 "name": "memory-bandwidth",
2782 "success": true,
2783 "execution_time_ms": 1,
2784 "results": {
2785 "hbm_peak_gb_s": 123.0,
2786 "system_peak_gb_s": 45.0,
2787 "hbm_ratio": 2.7,
2788 "hbm_samples_gbps": [100.0, 123.0],
2789 "system_samples_gbps": [40.0, 45.0]
2790 },
2791 "errors": []
2792 }"#,
2793 )
2794 .unwrap();
2795 for path in [&gpu_log, &mem_log] {
2796 let _ = std::process::Command::new("touch")
2797 .args(["-d", "2100-01-01"])
2798 .arg(path)
2799 .status();
2800 }
2801
2802 let results = load_benchmark_results();
2803
2804 assert_eq!(results.gpus[0].model, "AMD Radeon RX 7900 XTX");
2805 assert_eq!(
2806 results.memory_bandwidth.as_ref().unwrap().hbm_peak_gb_s,
2807 123.0
2808 );
2809 }
2812}