use std::path::PathBuf;
use std::time::{Duration, Instant};
use sysinfo::System;
use super::utils::{path_exists, print_header, print_info, print_kv, print_success};
use crate::cli::{
BenchArgs, BenchCompareArgs, BenchHardwareArgs, BenchInferenceArgs, BenchModelArgs,
BenchSubcommand,
};
use crate::error::{CliError, CliResult};
use axonml_serialize::{StateDict, load_state_dict};
pub fn execute(args: BenchArgs) -> CliResult<()> {
match args.action {
BenchSubcommand::Model(model_args) => execute_model(model_args),
BenchSubcommand::Inference(inference_args) => execute_inference(inference_args),
BenchSubcommand::Compare(compare_args) => execute_compare(compare_args),
BenchSubcommand::Hardware(hardware_args) => execute_hardware(hardware_args),
}
}
fn execute_model(args: BenchModelArgs) -> CliResult<()> {
print_header("Model Benchmark");
let model_path = PathBuf::from(&args.input);
if !path_exists(&model_path) {
return Err(CliError::Model(format!("Model not found: {}", args.input)));
}
print_kv("Model", &args.input);
print_kv("Iterations", &args.iterations.to_string());
print_kv("Warmup", &args.warmup.to_string());
print_kv("Batch Size", &args.batch_size.to_string());
println!();
print_info("Loading model...");
let load_start = Instant::now();
let state_dict = load_state_dict(&model_path)
.map_err(|e| CliError::Model(format!("Failed to load model: {e}")))?;
let load_time = load_start.elapsed();
print_kv("Load Time", &format_duration(load_time));
let param_count = count_parameters(&state_dict);
let model_size = std::fs::metadata(&model_path)?.len();
print_kv("Parameters", &format_number(param_count));
print_kv("Model Size", &format_size(model_size as usize));
println!();
print_header("Forward Pass Benchmark");
let tensor_ops = calculate_tensor_operations(&state_dict, args.batch_size);
print_info(&format!("Warming up ({} iterations)...", args.warmup));
for _ in 0..args.warmup {
run_tensor_benchmark(&state_dict, args.batch_size);
}
print_info(&format!("Benchmarking ({} iterations)...", args.iterations));
let mut times: Vec<Duration> = Vec::new();
for _ in 0..args.iterations {
let start = Instant::now();
run_tensor_benchmark(&state_dict, args.batch_size);
times.push(start.elapsed());
}
let total: Duration = times.iter().sum();
let mean = total / args.iterations as u32;
let min = times.iter().min().copied().unwrap_or(Duration::ZERO);
let max = times.iter().max().copied().unwrap_or(Duration::ZERO);
let mean_nanos = mean.as_nanos() as f64;
let variance: f64 = times
.iter()
.map(|t| {
let diff = t.as_nanos() as f64 - mean_nanos;
diff * diff
})
.sum::<f64>()
/ args.iterations as f64;
let std_dev = Duration::from_nanos(variance.sqrt() as u64);
let samples_per_sec = args.batch_size as f64 / mean.as_secs_f64();
let gflops = tensor_ops as f64 / mean.as_secs_f64() / 1e9;
println!();
print_header("Results");
print_kv("Mean", &format_duration(mean));
print_kv("Std Dev", &format_duration(std_dev));
print_kv("Min", &format_duration(min));
print_kv("Max", &format_duration(max));
print_kv("Throughput", &format!("{samples_per_sec:.2} samples/sec"));
print_kv("Est. GFLOPS", &format!("{gflops:.2}"));
let memory_estimate = estimate_memory(&state_dict, args.batch_size);
print_kv("Est. Memory", &format_size(memory_estimate));
if let Some(output) = &args.output {
let results = serde_json::json!({
"model": args.input,
"iterations": args.iterations,
"batch_size": args.batch_size,
"parameters": param_count,
"model_size_bytes": model_size,
"load_time_ms": load_time.as_millis(),
"mean_ms": mean.as_secs_f64() * 1000.0,
"std_dev_ms": std_dev.as_secs_f64() * 1000.0,
"min_ms": min.as_secs_f64() * 1000.0,
"max_ms": max.as_secs_f64() * 1000.0,
"throughput_samples_per_sec": samples_per_sec,
"gflops": gflops,
"memory_estimate_bytes": memory_estimate,
});
std::fs::write(output, serde_json::to_string_pretty(&results)?)?;
println!();
print_success(&format!("Results saved to {output}"));
}
Ok(())
}
fn execute_inference(args: BenchInferenceArgs) -> CliResult<()> {
print_header("Inference Benchmark");
let model_path = PathBuf::from(&args.model);
if !path_exists(&model_path) {
return Err(CliError::Model(format!("Model not found: {}", args.model)));
}
print_kv("Model", &args.model);
print_kv("Iterations", &args.iterations.to_string());
print_kv("Warmup", &args.warmup.to_string());
println!();
let batch_sizes: Vec<usize> = args
.batch_sizes
.split(',')
.filter_map(|s| s.trim().parse().ok())
.collect();
if batch_sizes.is_empty() {
return Err(CliError::InvalidArgument(
"No valid batch sizes provided".to_string(),
));
}
print_info("Loading model...");
let state_dict = load_state_dict(&model_path)
.map_err(|e| CliError::Model(format!("Failed to load model: {e}")))?;
println!();
print_header("Latency vs Batch Size");
println!();
println!(
"{:<12} {:>12} {:>12} {:>15}",
"Batch Size", "Latency", "Throughput", "Memory Est."
);
println!("{}", "-".repeat(55));
let mut results: Vec<serde_json::Value> = Vec::new();
for batch_size in &batch_sizes {
for _ in 0..args.warmup {
run_tensor_benchmark(&state_dict, *batch_size);
}
let mut times: Vec<Duration> = Vec::new();
for _ in 0..args.iterations {
let start = Instant::now();
run_tensor_benchmark(&state_dict, *batch_size);
times.push(start.elapsed());
}
let total: Duration = times.iter().sum();
let mean = total / args.iterations as u32;
let throughput = *batch_size as f64 / mean.as_secs_f64();
let memory = estimate_memory(&state_dict, *batch_size);
println!(
"{:<12} {:>12} {:>12} {:>15}",
batch_size,
format_duration(mean),
format!("{:.1} s/s", throughput),
format_size(memory)
);
results.push(serde_json::json!({
"batch_size": batch_size,
"latency_ms": mean.as_secs_f64() * 1000.0,
"throughput_samples_per_sec": throughput,
"memory_bytes": memory,
}));
}
println!();
let optimal = batch_sizes
.iter()
.max_by(|a, b| {
let mem_a = estimate_memory(&state_dict, **a);
let mem_b = estimate_memory(&state_dict, **b);
if mem_a <= 8 * 1024 * 1024 * 1024 && mem_b <= 8 * 1024 * 1024 * 1024 {
a.cmp(b)
} else if mem_a <= 8 * 1024 * 1024 * 1024 {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Less
}
})
.copied()
.unwrap_or(1);
print_kv("Recommended Batch Size", &optimal.to_string());
if let Some(output) = &args.output {
let all_results = serde_json::json!({
"model": args.model,
"iterations": args.iterations,
"results": results,
"recommended_batch_size": optimal,
});
std::fs::write(output, serde_json::to_string_pretty(&all_results)?)?;
println!();
print_success(&format!("Results saved to {output}"));
}
Ok(())
}
fn execute_compare(args: BenchCompareArgs) -> CliResult<()> {
print_header("Model Comparison Benchmark");
let models: Vec<&str> = args.models.split(',').map(str::trim).collect();
if models.len() < 2 {
return Err(CliError::InvalidArgument(
"Need at least 2 models to compare".to_string(),
));
}
for model in &models {
let path = PathBuf::from(model);
if !path_exists(&path) {
return Err(CliError::Model(format!("Model not found: {model}")));
}
}
print_kv("Models", &models.len().to_string());
print_kv("Iterations", &args.iterations.to_string());
print_kv("Batch Size", &args.batch_size.to_string());
println!();
println!(
"{:<30} {:>12} {:>12} {:>12} {:>12}",
"Model", "Params", "Size", "Latency", "Throughput"
);
println!("{}", "-".repeat(82));
let mut results: Vec<serde_json::Value> = Vec::new();
let mut fastest_time = Duration::MAX;
let mut fastest_model = String::new();
for model_path in &models {
let path = PathBuf::from(model_path);
let state_dict = load_state_dict(&path)
.map_err(|e| CliError::Model(format!("Failed to load {model_path}: {e}")))?;
let param_count = count_parameters(&state_dict);
let model_size = std::fs::metadata(&path)?.len();
for _ in 0..3 {
run_tensor_benchmark(&state_dict, args.batch_size);
}
let mut times: Vec<Duration> = Vec::new();
for _ in 0..args.iterations {
let start = Instant::now();
run_tensor_benchmark(&state_dict, args.batch_size);
times.push(start.elapsed());
}
let total: Duration = times.iter().sum();
let mean = total / args.iterations as u32;
let throughput = args.batch_size as f64 / mean.as_secs_f64();
if mean < fastest_time {
fastest_time = mean;
fastest_model = (*model_path).to_string();
}
let display_name = if model_path.len() > 28 {
format!("...{}", &model_path[model_path.len() - 25..])
} else {
(*model_path).to_string()
};
println!(
"{:<30} {:>12} {:>12} {:>12} {:>12}",
display_name,
format_number(param_count),
format_size(model_size as usize),
format_duration(mean),
format!("{:.1} s/s", throughput)
);
results.push(serde_json::json!({
"model": model_path,
"parameters": param_count,
"size_bytes": model_size,
"latency_ms": mean.as_secs_f64() * 1000.0,
"throughput_samples_per_sec": throughput,
}));
}
println!();
print_success(&format!("Fastest model: {fastest_model}"));
if let Some(output) = &args.output {
let all_results = serde_json::json!({
"iterations": args.iterations,
"batch_size": args.batch_size,
"models": results,
"fastest_model": fastest_model,
});
std::fs::write(output, serde_json::to_string_pretty(&all_results)?)?;
println!();
print_success(&format!("Results saved to {output}"));
}
Ok(())
}
fn execute_hardware(args: BenchHardwareArgs) -> CliResult<()> {
print_header("Hardware Benchmark");
println!();
let mut sys = System::new_all();
sys.refresh_all();
print_header("CPU Information");
print_kv("CPU Cores (Logical)", &num_cpus::get().to_string());
print_kv(
"CPU Cores (Physical)",
&num_cpus::get_physical().to_string(),
);
if let Some(cpu) = sys.cpus().first() {
print_kv("CPU Model", cpu.brand());
print_kv("CPU Frequency", &format!("{} MHz", cpu.frequency()));
}
println!();
print_header("Memory Information");
print_kv("Total RAM", &format_size(sys.total_memory() as usize));
print_kv("Used RAM", &format_size(sys.used_memory() as usize));
print_kv(
"Free RAM",
&format_size((sys.total_memory() - sys.used_memory()) as usize),
);
println!();
print_header("Memory Bandwidth Test");
let test_sizes: [(usize, &str); 3] = [
(1024 * 1024, "1 MB"),
(16 * 1024 * 1024, "16 MB"),
(64 * 1024 * 1024, "64 MB"),
];
let iterations = args.iterations;
println!();
println!(
"{:<15} {:>18} {:>18}",
"Size", "Read Bandwidth", "Write Bandwidth"
);
println!("{}", "-".repeat(53));
for (size, label) in test_sizes {
let (read_bw, write_bw) = measure_memory_bandwidth(size, iterations);
println!(
"{:<15} {:>18} {:>18}",
label,
format!("{}/s", format_size(read_bw)),
format!("{}/s", format_size(write_bw))
);
}
println!();
print_header("Compute Benchmark (Matrix Multiplication)");
let matrix_sizes = [512, 1024, 2048];
println!();
println!("{:<15} {:>15} {:>15}", "Matrix Size", "Time", "GFLOPS");
println!("{}", "-".repeat(47));
for size in matrix_sizes {
let (time, gflops) = measure_matmul_performance(size, iterations);
println!(
"{:<15} {:>15} {:>15}",
format!("{}x{}", size, size),
format_duration(time),
format!("{:.2}", gflops)
);
}
if let Some(output) = &args.output {
let results = serde_json::json!({
"cpu_cores_logical": num_cpus::get(),
"cpu_cores_physical": num_cpus::get_physical(),
"total_memory_bytes": sys.total_memory(),
"used_memory_bytes": sys.used_memory(),
});
std::fs::write(output, serde_json::to_string_pretty(&results)?)?;
println!();
print_success(&format!("Results saved to {output}"));
}
Ok(())
}
fn count_parameters(state_dict: &StateDict) -> usize {
state_dict
.entries()
.map(|(_, entry)| entry.data.shape.iter().product::<usize>())
.sum()
}
fn estimate_memory(state_dict: &StateDict, batch_size: usize) -> usize {
let param_memory: usize = state_dict
.entries()
.map(|(_, entry)| entry.data.shape.iter().product::<usize>() * 4)
.sum();
let activation_memory = param_memory * batch_size / 10;
let gradient_memory = param_memory;
param_memory + activation_memory + gradient_memory
}
fn calculate_tensor_operations(state_dict: &StateDict, batch_size: usize) -> usize {
let mut total_ops = 0usize;
for (name, entry) in state_dict.entries() {
let shape = &entry.data.shape;
if name.contains("weight") && shape.len() == 2 {
let m = batch_size;
let k = shape[0];
let n = shape[1];
total_ops += 2 * m * k * n;
} else if name.contains("weight") && shape.len() == 4 {
let out_channels = shape[0];
let in_channels = shape[1];
let kernel_h = shape[2];
let kernel_w = shape[3];
let out_hw = 32 * 32; total_ops += 2 * batch_size * out_channels * out_hw * kernel_h * kernel_w * in_channels;
}
}
total_ops
}
fn run_tensor_benchmark(state_dict: &StateDict, batch_size: usize) {
for (_name, entry) in state_dict.entries() {
let shape = &entry.data.shape;
let size = shape.iter().product::<usize>();
let data = &entry.data.values;
let ops_count = (size * batch_size).min(1_000_000);
let mut result = 0.0f32;
for i in 0..ops_count {
let idx = i % data.len();
result = result.mul_add(data[idx], data[(idx + 1) % data.len()]);
}
std::hint::black_box(result);
}
}
fn measure_memory_bandwidth(size: usize, iterations: usize) -> (usize, usize) {
let mut data: Vec<f32> = vec![0.0; size / 4];
let write_start = Instant::now();
for iter in 0..iterations {
for i in 0..data.len() {
data[i] = (i + iter) as f32;
}
std::hint::black_box(&data);
}
let write_time = write_start.elapsed();
let write_bw = (size * iterations) as f64 / write_time.as_secs_f64();
let read_start = Instant::now();
let mut sum = 0.0f32;
for _ in 0..iterations {
for val in &data {
sum += val;
}
}
std::hint::black_box(sum);
let read_time = read_start.elapsed();
let read_bw = (size * iterations) as f64 / read_time.as_secs_f64();
(read_bw as usize, write_bw as usize)
}
fn measure_matmul_performance(n: usize, iterations: usize) -> (Duration, f64) {
let flops_per_iter = 2.0 * (n as f64).powi(3);
let a: Vec<f32> = (0..n * n).map(|i| (i as f32) * 0.001).collect();
let b: Vec<f32> = (0..n * n).map(|i| (i as f32) * 0.001).collect();
let mut c: Vec<f32> = vec![0.0; n * n];
naive_matmul(&a, &b, &mut c, n);
let start = Instant::now();
for _ in 0..iterations {
naive_matmul(&a, &b, &mut c, n);
std::hint::black_box(&c);
}
let elapsed = start.elapsed();
let mean_time = elapsed / iterations as u32;
let total_flops = flops_per_iter * iterations as f64;
let gflops = total_flops / elapsed.as_secs_f64() / 1e9;
(mean_time, gflops)
}
fn naive_matmul(a: &[f32], b: &[f32], c: &mut [f32], n: usize) {
for i in 0..n {
for j in 0..n {
let mut sum = 0.0f32;
for k in 0..n {
sum += a[i * n + k] * b[k * n + j];
}
c[i * n + j] = sum;
}
}
}
fn format_duration(d: Duration) -> String {
let nanos = d.as_nanos();
if nanos < 1000 {
format!("{nanos} ns")
} else if nanos < 1_000_000 {
format!("{:.2} µs", nanos as f64 / 1000.0)
} else if nanos < 1_000_000_000 {
format!("{:.2} ms", nanos as f64 / 1_000_000.0)
} else {
format!("{:.2} s", d.as_secs_f64())
}
}
fn format_size(bytes: usize) -> String {
const KB: usize = 1024;
const MB: usize = KB * 1024;
const GB: usize = MB * 1024;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{bytes} B")
}
}
fn format_number(n: usize) -> String {
if n >= 1_000_000_000 {
format!("{:.2}B", n as f64 / 1_000_000_000.0)
} else if n >= 1_000_000 {
format!("{:.2}M", n as f64 / 1_000_000.0)
} else if n >= 1_000 {
format!("{:.2}K", n as f64 / 1_000.0)
} else {
format!("{n}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_duration() {
assert!(format_duration(Duration::from_nanos(500)).contains("ns"));
assert!(format_duration(Duration::from_micros(500)).contains("µs"));
assert!(format_duration(Duration::from_millis(500)).contains("ms"));
assert!(format_duration(Duration::from_secs(2)).contains('s'));
}
#[test]
fn test_format_size() {
assert!(format_size(500).contains('B'));
assert!(format_size(1500).contains("KB"));
assert!(format_size(1500000).contains("MB"));
assert!(format_size(1500000000).contains("GB"));
}
#[test]
fn test_format_number() {
assert_eq!(format_number(500), "500");
assert!(format_number(1500).contains('K'));
assert!(format_number(1500000).contains('M'));
assert!(format_number(1500000000).contains('B'));
}
#[test]
fn test_memory_bandwidth() {
let (read_bw, write_bw) = measure_memory_bandwidth(1024 * 1024, 1);
assert!(read_bw > 0);
assert!(write_bw > 0);
}
#[test]
fn test_matmul_performance() {
let (time, gflops) = measure_matmul_performance(64, 1);
assert!(time.as_nanos() > 0);
assert!(gflops > 0.0);
}
}