Skip to main content

rusty_stack/widgets/
benchmarks_page.rs

1//! Benchmark performance page for Rusty-Stack TUI
2//! Renders real benchmark results with ratatui charts and summaries.
3
4use 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
159/// Load benchmark results from all available log files
160pub 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    // primary JSON sources written by the wrapper scripts
171    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    // Try to load baseline (first log file that isn't the current one)
241    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 this object contains recognized benchmark fields, process it
403        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            // Otherwise, iterate through the object as it might be a container of results
420            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        // llama-cpp benchmark: keys like prefill_512_tps_gpu0, decode_128_tps_gpu0
799        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            // Collect unique GPU indices from metric keys
805            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                    // prefill_{context}_tps_gpu{idx}
834                    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                    // decode_{n_gen}_tps_gpu{idx}
851                    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            // Sort by GPU index then context/n_gen
870            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
907/// Render the benchmark results page
908pub 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            // Check for vLLM specific errors
1566            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}