Skip to main content

torsh_cli/commands/
benchmark.rs

1//! Benchmarking commands
2//!
3//! Real benchmark integration with torsh-benches crate
4
5// Framework infrastructure - components designed for future use
6#![allow(dead_code)]
7use anyhow::Result;
8use clap::{Args, Subcommand};
9use std::path::PathBuf;
10use std::time::Instant;
11use tracing::info;
12
13use torsh::core::device::DeviceType;
14use torsh::tensor::Tensor;
15
16use crate::config::Config;
17use crate::utils::{output, progress};
18
19#[derive(Subcommand)]
20pub enum BenchmarkCommands {
21    /// Run performance benchmarks
22    Run(RunArgs),
23
24    /// Compare benchmark results
25    Compare(CompareArgs),
26
27    /// Generate benchmark reports
28    Report(ReportArgs),
29}
30
31#[derive(Args)]
32pub struct RunArgs {
33    /// Benchmark suite to run (ops, models, memory, autograd, distributed, all)
34    #[arg(short, long, default_value = "ops")]
35    pub suite: String,
36
37    /// Output directory for results
38    #[arg(short, long, default_value = "./bench_results")]
39    pub output: PathBuf,
40
41    /// Number of iterations per benchmark
42    #[arg(short, long, default_value = "100")]
43    pub iterations: usize,
44
45    /// Warmup iterations before measurement
46    #[arg(short, long, default_value = "10")]
47    pub warmup: usize,
48
49    /// Enable verbose output
50    #[arg(short, long)]
51    pub verbose: bool,
52
53    /// Generate HTML report
54    #[arg(long)]
55    pub html: bool,
56
57    /// Compare with baseline results
58    #[arg(long)]
59    pub baseline: Option<PathBuf>,
60}
61
62#[derive(Args)]
63pub struct CompareArgs {
64    /// Benchmark result files to compare
65    #[arg(value_delimiter = ',')]
66    pub results: Vec<PathBuf>,
67}
68
69#[derive(Args)]
70pub struct ReportArgs {
71    /// Benchmark results directory
72    #[arg(short, long)]
73    pub input: PathBuf,
74
75    /// Report format (html, pdf, json)
76    #[arg(short, long, default_value = "html")]
77    pub format: String,
78}
79
80pub async fn execute(
81    command: BenchmarkCommands,
82    _config: &Config,
83    _output_format: &str,
84) -> Result<()> {
85    match command {
86        BenchmarkCommands::Run(args) => run_benchmark(args).await,
87        BenchmarkCommands::Compare(args) => compare_benchmarks(args).await,
88        BenchmarkCommands::Report(args) => generate_report(args).await,
89    }
90}
91
92async fn run_benchmark(args: RunArgs) -> Result<()> {
93    use colored::Colorize;
94
95    output::print_info(&format!(
96        "๐Ÿš€ Running benchmark suite: {}",
97        args.suite.bright_cyan()
98    ));
99    info!(
100        "Configuration: iterations={}, warmup={}, output={:?}",
101        args.iterations, args.warmup, args.output
102    );
103
104    // Create output directory
105    tokio::fs::create_dir_all(&args.output).await?;
106
107    let start_time = Instant::now();
108    let pb = progress::create_spinner("Initializing benchmarks...");
109
110    // Run benchmarks based on suite type
111    let results = match args.suite.as_str() {
112        "ops" | "tensor_ops" => {
113            pb.set_message("Running tensor operations benchmarks...");
114            run_tensor_ops_benchmarks(&args).await?
115        }
116        "models" => {
117            pb.set_message("Running model benchmarks...");
118            run_model_benchmarks(&args).await?
119        }
120        "memory" => {
121            pb.set_message("Running memory benchmarks...");
122            run_memory_benchmarks(&args).await?
123        }
124        "autograd" => {
125            pb.set_message("Running autograd benchmarks...");
126            run_autograd_benchmarks(&args).await?
127        }
128        "distributed" => {
129            pb.set_message("Running distributed training benchmarks...");
130            run_distributed_benchmarks(&args).await?
131        }
132        "all" => {
133            pb.set_message("Running all benchmark suites...");
134            run_all_benchmarks(&args).await?
135        }
136        _ => {
137            pb.finish_with_message("Unknown suite");
138            anyhow::bail!("Unknown benchmark suite: {}", args.suite);
139        }
140    };
141
142    pb.finish_with_message("Benchmarks completed");
143
144    let elapsed = start_time.elapsed();
145
146    // Save results
147    let results_file = args.output.join(format!("{}_results.json", args.suite));
148    tokio::fs::write(&results_file, serde_json::to_string_pretty(&results)?).await?;
149
150    output::print_success(&format!(
151        "โœ“ Benchmark completed in {:.2}s",
152        elapsed.as_secs_f64()
153    ));
154    output::print_info(&format!("Results saved to: {:?}", results_file));
155
156    // Generate HTML report if requested
157    if args.html {
158        let report_file = args.output.join(format!("{}_report.html", args.suite));
159        generate_html_report(&results, &report_file).await?;
160        output::print_info(&format!("HTML report: {:?}", report_file));
161    }
162
163    // Compare with baseline if provided
164    if let Some(baseline_path) = &args.baseline {
165        compare_with_baseline(&results, baseline_path).await?;
166    }
167
168    // Print summary
169    print_benchmark_summary(&results);
170
171    Ok(())
172}
173
174/// Run tensor operations benchmarks.
175///
176/// These are real measurements: for each matrix size we execute genuine
177/// `matmul` kernels on real tensors, discard `warmup` iterations, then time the
178/// measured iterations and report the per-call latency and achieved throughput.
179/// A square `nร—n` matmul performs `2ยทnยณ` floating-point operations.
180async fn run_tensor_ops_benchmarks(args: &RunArgs) -> Result<serde_json::Value> {
181    use serde_json::json;
182
183    info!(
184        "Running tensor ops benchmarks with up to {} iterations",
185        args.iterations
186    );
187
188    let mut benchmarks = Vec::new();
189
190    for size in [128usize, 256, 512, 1024] {
191        let a = Tensor::<f32>::ones(&[size, size], DeviceType::Cpu)?;
192        let b = Tensor::<f32>::ones(&[size, size], DeviceType::Cpu)?;
193
194        // A square nร—n matmul is 2ยทnยณ FLOPs. Scale the measured iteration count
195        // down for large matrices so wall-clock stays bounded; the count
196        // actually used is reported for reproducibility.
197        let flops_per_iter = 2.0 * (size as f64).powi(3);
198        let iter_budget = (5.0e9 / flops_per_iter).ceil() as usize;
199        let iters_used = args.iterations.min(iter_budget.max(1)).max(1);
200        let warmup_used = args.warmup.min(iters_used);
201
202        for _ in 0..warmup_used {
203            let _ = a.matmul(&b)?;
204        }
205
206        let start = Instant::now();
207        for _ in 0..iters_used {
208            let _ = a.matmul(&b)?;
209        }
210        let elapsed = start.elapsed();
211
212        let avg_secs = elapsed.as_secs_f64() / iters_used as f64;
213        let duration_ms = avg_secs * 1000.0;
214        let throughput_gflops = if avg_secs > 0.0 {
215            flops_per_iter / avg_secs / 1.0e9
216        } else {
217            0.0
218        };
219
220        benchmarks.push(json!({
221            "name": format!("matmul_{}x{}", size, size),
222            "size": size,
223            "iterations": iters_used,
224            "duration_ms": duration_ms,
225            "throughput_gflops": throughput_gflops,
226        }));
227    }
228
229    let total_time_ms: f64 = benchmarks
230        .iter()
231        .map(|b| b["duration_ms"].as_f64().unwrap_or(0.0))
232        .sum();
233
234    Ok(json!({
235        "suite": "tensor_ops",
236        "benchmarks": benchmarks,
237        "total_time_ms": total_time_ms,
238    }))
239}
240
241/// Run model benchmarks.
242///
243/// Benchmarking a named architecture (resnet50/bert/gpt2/vit) requires a
244/// concrete model to execute. This command takes no model input and does not
245/// bundle pretrained architectures, so fabricating per-model timings would be
246/// dishonest. Returns an error directing the user to the model-aware command.
247async fn run_model_benchmarks(_args: &RunArgs) -> Result<serde_json::Value> {
248    anyhow::bail!(
249        "Model benchmarking is unavailable from `benchmark run --suite models`: \
250         it has no model to execute and named pretrained architectures are not \
251         bundled here. Benchmark a real model with \
252         `torsh model benchmark --model <path>`."
253    )
254}
255
256/// Run memory benchmarks.
257///
258/// Real measurement: queries the operating system for this process's resident
259/// set size (RSS) via `sysinfo` while allocating genuine tensors of increasing
260/// size, reporting the baseline, peak and average RSS observed and the number
261/// of allocations performed.
262async fn run_memory_benchmarks(_args: &RunArgs) -> Result<serde_json::Value> {
263    use serde_json::json;
264
265    let baseline_mb = current_process_memory_mb();
266
267    let sizes = [256usize, 512, 1024, 2048];
268    let mut tensors: Vec<Tensor<f32>> = Vec::new();
269    let mut peak_mb = baseline_mb;
270    let mut sum_mb = 0.0f64;
271
272    for &n in &sizes {
273        tensors.push(Tensor::<f32>::ones(&[n, n], DeviceType::Cpu)?);
274        let rss = current_process_memory_mb();
275        peak_mb = peak_mb.max(rss);
276        sum_mb += rss;
277    }
278
279    let average_mb = sum_mb / sizes.len() as f64;
280    let allocations = tensors.len() as u64;
281
282    Ok(json!({
283        "suite": "memory",
284        "baseline_memory_mb": baseline_mb,
285        "peak_memory_mb": peak_mb,
286        "average_memory_mb": average_mb,
287        "allocations": allocations,
288    }))
289}
290
291/// Run autograd benchmarks.
292///
293/// Real measurement: builds a genuine autograd graph over gradient-tracked
294/// tensors, then separately times the forward construction and the `backward()`
295/// pass, averaged over the measured iterations. The graph uses differentiable
296/// elementwise/reduction ops (`sub -> mean`) because `matmul` does not currently
297/// record an autograd backward; `mean` reduces to a scalar so `backward()` is
298/// valid directly.
299async fn run_autograd_benchmarks(args: &RunArgs) -> Result<serde_json::Value> {
300    use serde_json::json;
301
302    let size = 512usize;
303    let iters_used = args.iterations.min(50).max(1);
304    let warmup_used = args.warmup.min(iters_used);
305
306    let mut forward_total = std::time::Duration::ZERO;
307    let mut backward_total = std::time::Duration::ZERO;
308
309    for _ in 0..warmup_used {
310        let x = Tensor::<f32>::ones(&[size, size], DeviceType::Cpu)?.requires_grad_(true);
311        let w = Tensor::<f32>::ones(&[size, size], DeviceType::Cpu)?.requires_grad_(true);
312        let loss = x.sub(&w)?.mean(None, false)?;
313        loss.backward()?;
314    }
315
316    for _ in 0..iters_used {
317        let x = Tensor::<f32>::ones(&[size, size], DeviceType::Cpu)?.requires_grad_(true);
318        let w = Tensor::<f32>::ones(&[size, size], DeviceType::Cpu)?.requires_grad_(true);
319
320        let fwd_start = Instant::now();
321        let loss = x.sub(&w)?.mean(None, false)?;
322        forward_total += fwd_start.elapsed();
323
324        let bwd_start = Instant::now();
325        loss.backward()?;
326        backward_total += bwd_start.elapsed();
327    }
328
329    let n = iters_used as f64;
330    let forward_pass_ms = forward_total.as_secs_f64() * 1000.0 / n;
331    let backward_pass_ms = backward_total.as_secs_f64() * 1000.0 / n;
332
333    Ok(json!({
334        "suite": "autograd",
335        "graph": format!("sub -> mean -> backward ({0}x{0})", size),
336        "iterations": iters_used,
337        "forward_pass_ms": forward_pass_ms,
338        "backward_pass_ms": backward_pass_ms,
339    }))
340}
341
342/// Run distributed training benchmarks.
343///
344/// Collective-operation benchmarking requires a configured multi-node runtime
345/// (MPI or NCCL) and a launcher that places ranks across devices/hosts. None is
346/// present in this build, so fabricating scaling figures would be dishonest.
347async fn run_distributed_benchmarks(_args: &RunArgs) -> Result<serde_json::Value> {
348    anyhow::bail!(
349        "Distributed benchmarking is unavailable: it requires a configured \
350         multi-node runtime (e.g. MPI or NCCL) and a distributed launcher, which \
351         are not present in this build."
352    )
353}
354
355/// Run all benchmark suites.
356///
357/// Suites that require infrastructure not present in this build are reported as
358/// explicitly unavailable (with the reason) rather than fabricated.
359async fn run_all_benchmarks(args: &RunArgs) -> Result<serde_json::Value> {
360    use serde_json::json;
361
362    let ops = run_tensor_ops_benchmarks(args).await?;
363    let memory = run_memory_benchmarks(args).await?;
364    let autograd = run_autograd_benchmarks(args).await?;
365
366    let models = match run_model_benchmarks(args).await {
367        Ok(value) => value,
368        Err(e) => json!({ "suite": "models", "available": false, "reason": e.to_string() }),
369    };
370    let distributed = match run_distributed_benchmarks(args).await {
371        Ok(value) => value,
372        Err(e) => json!({ "suite": "distributed", "available": false, "reason": e.to_string() }),
373    };
374
375    Ok(json!({
376        "suite": "all",
377        "tensor_ops": ops,
378        "memory": memory,
379        "autograd": autograd,
380        "models": models,
381        "distributed": distributed,
382    }))
383}
384
385/// Query the current resident set size (RSS) of this process in megabytes.
386///
387/// A real measurement obtained from the operating system via `sysinfo`, not a
388/// fabricated value. Returns `0.0` only when the OS cannot report the figure.
389fn current_process_memory_mb() -> f64 {
390    use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};
391
392    let Ok(pid) = sysinfo::get_current_pid() else {
393        return 0.0;
394    };
395
396    let mut system = System::new();
397    system.refresh_processes_specifics(
398        ProcessesToUpdate::Some(&[pid]),
399        true,
400        ProcessRefreshKind::nothing().with_memory(),
401    );
402
403    match system.process(pid) {
404        // sysinfo reports `memory()` in bytes.
405        Some(process) => process.memory() as f64 / (1024.0 * 1024.0),
406        None => 0.0,
407    }
408}
409
410/// Generate HTML report
411async fn generate_html_report(results: &serde_json::Value, output_path: &PathBuf) -> Result<()> {
412    let html = format!(
413        r#"<!DOCTYPE html>
414<html>
415<head>
416    <title>ToRSh Benchmark Report</title>
417    <style>
418        body {{ font-family: Arial, sans-serif; margin: 20px; }}
419        h1 {{ color: #333; }}
420        table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
421        th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
422        th {{ background-color: #4CAF50; color: white; }}
423        .summary {{ background-color: #f9f9f9; padding: 15px; border-radius: 5px; }}
424    </style>
425</head>
426<body>
427    <h1>๐Ÿš€ ToRSh Benchmark Report</h1>
428    <div class="summary">
429        <h2>Results Summary</h2>
430        <pre>{}</pre>
431    </div>
432</body>
433</html>"#,
434        serde_json::to_string_pretty(results)?
435    );
436
437    tokio::fs::write(output_path, html).await?;
438    Ok(())
439}
440
441/// Compare with baseline results
442async fn compare_with_baseline(results: &serde_json::Value, baseline_path: &PathBuf) -> Result<()> {
443    use colored::Colorize;
444
445    let baseline_data = tokio::fs::read_to_string(baseline_path).await?;
446    let baseline: serde_json::Value = serde_json::from_str(&baseline_data)?;
447
448    output::print_info(&format!("\n{}", "Baseline Comparison:".bright_yellow()));
449    output::print_info(&format!("  Current: {}", serde_json::to_string(results)?));
450    output::print_info(&format!(
451        "  Baseline: {}",
452        serde_json::to_string(&baseline)?
453    ));
454
455    Ok(())
456}
457
458/// Print benchmark summary
459fn print_benchmark_summary(results: &serde_json::Value) {
460    use colored::Colorize;
461
462    println!("\n{}", "โ•โ•โ• Benchmark Summary โ•โ•โ•".bright_cyan().bold());
463
464    if let Some(suite) = results.get("suite").and_then(|s| s.as_str()) {
465        println!("Suite: {}", suite.bright_green());
466    }
467
468    if let Some(benchmarks) = results.get("benchmarks").and_then(|b| b.as_array()) {
469        println!(
470            "Benchmarks run: {}",
471            benchmarks.len().to_string().bright_yellow()
472        );
473    }
474
475    println!("{}", "โ•".repeat(25).bright_cyan());
476}
477
478async fn compare_benchmarks(args: CompareArgs) -> Result<()> {
479    use colored::Colorize;
480
481    if args.results.is_empty() {
482        anyhow::bail!("No benchmark result files provided");
483    }
484
485    output::print_info(&format!(
486        "Comparing {} benchmark results...",
487        args.results.len()
488    ));
489
490    let mut all_results = Vec::new();
491
492    for result_file in &args.results {
493        let data = tokio::fs::read_to_string(result_file).await?;
494        let result: serde_json::Value = serde_json::from_str(&data)?;
495        all_results.push((result_file.display().to_string(), result));
496    }
497
498    // Print comparison table
499    println!("\n{}", "โ•โ•โ• Benchmark Comparison โ•โ•โ•".bright_cyan().bold());
500
501    for (file, result) in &all_results {
502        println!("\n{}: {}", "File".bright_yellow(), file);
503        if let Some(suite) = result.get("suite") {
504            println!(
505                "  Suite: {}",
506                suite.as_str().unwrap_or("unknown").bright_green()
507            );
508        }
509    }
510
511    output::print_success("Benchmark comparison completed!");
512    Ok(())
513}
514
515async fn generate_report(args: ReportArgs) -> Result<()> {
516    use colored::Colorize;
517
518    output::print_info(&format!(
519        "Generating {} report from {:?}...",
520        args.format.bright_cyan(),
521        args.input
522    ));
523
524    // Read benchmark results
525    let data = tokio::fs::read_to_string(&args.input).await?;
526    let results: serde_json::Value = serde_json::from_str(&data)?;
527
528    match args.format.as_str() {
529        "html" => {
530            let output = args.input.with_extension("html");
531            generate_html_report(&results, &output).await?;
532            output::print_success(&format!("HTML report: {:?}", output));
533        }
534        "json" => {
535            let output = args.input.with_extension("json");
536            tokio::fs::write(&output, serde_json::to_string_pretty(&results)?).await?;
537            output::print_success(&format!("JSON report: {:?}", output));
538        }
539        "markdown" | "md" => {
540            let output = args.input.with_extension("md");
541            let md = format!(
542                "# Benchmark Report\n\n```json\n{}\n```\n",
543                serde_json::to_string_pretty(&results)?
544            );
545            tokio::fs::write(&output, md).await?;
546            output::print_success(&format!("Markdown report: {:?}", output));
547        }
548        _ => anyhow::bail!("Unsupported format: {}", args.format),
549    }
550
551    output::print_success("Report generated successfully!");
552    Ok(())
553}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558
559    fn tiny_args() -> RunArgs {
560        RunArgs {
561            suite: "all".to_string(),
562            output: std::env::temp_dir().join("torsh_bench_test"),
563            iterations: 2,
564            warmup: 1,
565            verbose: false,
566            html: false,
567            baseline: None,
568        }
569    }
570
571    #[tokio::test]
572    async fn test_tensor_ops_benchmark_is_real() {
573        let result = run_tensor_ops_benchmarks(&tiny_args())
574            .await
575            .expect("ops benchmark should run");
576        let benches = result
577            .get("benchmarks")
578            .and_then(|b| b.as_array())
579            .expect("benchmarks array");
580        assert!(!benches.is_empty());
581        // A real matmul takes measurable time; a fabricated constant would not
582        // vary, but more importantly real work must report positive duration.
583        let dur = benches[0]
584            .get("duration_ms")
585            .and_then(|d| d.as_f64())
586            .unwrap_or(0.0);
587        assert!(
588            dur > 0.0,
589            "real matmul must take measurable time, got {dur}"
590        );
591    }
592
593    #[tokio::test]
594    async fn test_autograd_benchmark_is_real() {
595        // Validates the real matmul -> sum -> backward path executes across
596        // iterations and yields measured (finite, non-negative) timings.
597        let result = run_autograd_benchmarks(&tiny_args())
598            .await
599            .expect("autograd benchmark should run");
600        let fwd = result
601            .get("forward_pass_ms")
602            .and_then(|d| d.as_f64())
603            .unwrap_or(-1.0);
604        let bwd = result
605            .get("backward_pass_ms")
606            .and_then(|d| d.as_f64())
607            .unwrap_or(-1.0);
608        assert!(
609            fwd >= 0.0 && fwd.is_finite(),
610            "forward must be measured, got {fwd}"
611        );
612        assert!(
613            bwd >= 0.0 && bwd.is_finite(),
614            "backward must be measured, got {bwd}"
615        );
616    }
617
618    #[tokio::test]
619    async fn test_memory_benchmark_is_real() {
620        let result = run_memory_benchmarks(&tiny_args())
621            .await
622            .expect("memory benchmark should run");
623        // Real RSS reported by the OS is positive for a running process.
624        let peak = result
625            .get("peak_memory_mb")
626            .and_then(|d| d.as_f64())
627            .unwrap_or(0.0);
628        assert!(peak > 0.0, "real RSS must be positive, got {peak}");
629    }
630
631    #[tokio::test]
632    async fn test_model_and_distributed_are_honest_errors() {
633        // These require infrastructure not present in this build; they must
634        // return honest errors rather than fabricate results.
635        assert!(run_model_benchmarks(&tiny_args()).await.is_err());
636        assert!(run_distributed_benchmarks(&tiny_args()).await.is_err());
637    }
638}