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