1#![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(RunArgs),
23
24 Compare(CompareArgs),
26
27 Report(ReportArgs),
29}
30
31#[derive(Args)]
32pub struct RunArgs {
33 #[arg(short, long, default_value = "ops")]
35 pub suite: String,
36
37 #[arg(short, long, default_value = "./bench_results")]
39 pub output: PathBuf,
40
41 #[arg(short, long, default_value = "100")]
43 pub iterations: usize,
44
45 #[arg(short, long, default_value = "10")]
47 pub warmup: usize,
48
49 #[arg(short, long)]
51 pub verbose: bool,
52
53 #[arg(long)]
55 pub html: bool,
56
57 #[arg(long)]
59 pub baseline: Option<PathBuf>,
60}
61
62#[derive(Args)]
63pub struct CompareArgs {
64 #[arg(value_delimiter = ',')]
66 pub results: Vec<PathBuf>,
67}
68
69#[derive(Args)]
70pub struct ReportArgs {
71 #[arg(short, long)]
73 pub input: PathBuf,
74
75 #[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 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 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 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 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 if let Some(baseline_path) = &args.baseline {
165 compare_with_baseline(&results, baseline_path).await?;
166 }
167
168 print_benchmark_summary(&results);
170
171 Ok(())
172}
173
174async 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 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
241async 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
256async 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
291async 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
342async 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
355async 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
385fn 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 Some(process) => process.memory() as f64 / (1024.0 * 1024.0),
406 None => 0.0,
407 }
408}
409
410async 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
441async 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
458fn 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 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 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 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 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 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 assert!(run_model_benchmarks(&tiny_args()).await.is_err());
636 assert!(run_distributed_benchmarks(&tiny_args()).await.is_err());
637 }
638}