mod types;
pub use types::*;
use crate::brick::Scorable;
use crate::bricks::generators::SimdLoadBrick;
use crate::config::{ComputeBackend, WorkloadType};
use crate::error::CbtopError;
use std::time::{Duration, Instant};
pub struct HeadlessBenchmark {
backend: ComputeBackend,
workload: WorkloadType,
size: usize,
duration: Duration,
}
impl HeadlessBenchmark {
pub fn new(
backend: ComputeBackend,
workload: WorkloadType,
size: usize,
duration: Duration,
) -> Self {
Self {
backend,
workload,
size,
duration,
}
}
pub fn run(&self) -> Result<BenchmarkResult, CbtopError> {
let system = SystemInfo::detect();
let start_time = Instant::now();
let mut brick = SimdLoadBrick::new(self.size);
brick.set_workload(self.workload);
brick.set_intensity(1.0); brick.start();
let base_warmup_ms = (self.duration.as_millis() / 10).max(100) as u64;
let warmup_duration = if self.size < 100_000 {
Duration::from_millis(base_warmup_ms * 2) } else {
Duration::from_millis(base_warmup_ms)
};
let warmup_start = Instant::now();
while warmup_start.elapsed() < warmup_duration {
brick.run_iteration();
}
let mut brick = SimdLoadBrick::new(self.size);
brick.set_workload(self.workload);
brick.set_intensity(1.0);
brick.start();
let start_freq_mhz = Self::sample_cpu_freq();
let min_iterations: u64 = if self.size < 10_000 {
5000 } else if self.size < 100_000 {
1000 } else if self.size < 1_000_000 {
100 } else {
10 };
let mut iterations = 0u64;
let measure_start = Instant::now();
while measure_start.elapsed() < self.duration || iterations < min_iterations {
brick.run_iteration();
iterations += 1;
if iterations >= 100_000 {
break;
}
}
let total_duration = start_time.elapsed();
brick.stop();
let end_freq_mhz = Self::sample_cpu_freq();
let latencies = brick.latency_history_slice();
let latency_stats = Self::calculate_latency_stats(&latencies);
let gflops = brick.gflops();
let throughput = if latency_stats.mean > 0.0 {
1000.0 / latency_stats.mean
} else {
0.0
};
let score = brick.score();
let mut warnings = system.check_benchmark_readiness();
if let (Some(start), Some(end)) = (start_freq_mhz, end_freq_mhz) {
if start > 0 {
let drop_percent = ((start as f64 - end as f64) / start as f64) * 100.0;
if drop_percent > 5.0 {
warnings.push(format!(
"CPU frequency dropped {}MHz -> {}MHz ({:.1}% drop) during benchmark. \
Possible thermal throttling.",
start, end, drop_percent
));
}
}
}
Ok(BenchmarkResult {
version: env!("CARGO_PKG_VERSION").to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
duration_secs: total_duration.as_secs_f64(),
system,
benchmark: BenchmarkConfig {
backend: format!("{:?}", self.backend),
workload: format!("{:?}", self.workload),
size: self.size,
iterations,
},
results: BenchmarkResults {
gflops,
throughput_ops_sec: throughput,
latency_ms: latency_stats,
},
score: score.into(),
warnings,
})
}
fn sample_cpu_freq() -> Option<u32> {
#[cfg(target_os = "linux")]
{
let path = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq";
if let Ok(content) = std::fs::read_to_string(path) {
return content.trim().parse::<u32>().ok().map(|khz| khz / 1000);
}
}
None
}
fn calculate_latency_stats(latencies: &[f64]) -> LatencyStats {
if latencies.is_empty() {
return LatencyStats {
mean: 0.0,
min: 0.0,
max: 0.0,
p50: 0.0,
p95: 0.0,
p99: 0.0,
cv_percent: 0.0,
};
}
let filtered = Self::filter_outliers_iqr(latencies);
let data = if filtered.len() >= 10 {
&filtered
} else {
latencies
};
let n = data.len() as f64;
let mean = data.iter().sum::<f64>() / n;
let min = data.iter().cloned().fold(f64::INFINITY, f64::min);
let max = data.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let variance = data.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n;
let std_dev = variance.sqrt();
let cv_percent = if mean > 0.0 {
(std_dev / mean) * 100.0
} else {
0.0
};
let mut sorted = latencies.to_vec();
sorted.sort_by(|a, b| {
a.partial_cmp(b)
.expect("latency values MUST be comparable (no NaN)")
});
let percentile = |p: f64| -> f64 {
let idx = (p * (sorted.len() - 1) as f64).round() as usize;
sorted[idx.min(sorted.len() - 1)]
};
LatencyStats {
mean,
min,
max,
p50: percentile(0.50),
p95: percentile(0.95),
p99: percentile(0.99),
cv_percent,
}
}
fn filter_outliers_iqr(data: &[f64]) -> Vec<f64> {
if data.len() < 4 {
return data.to_vec();
}
let mut sorted = data.to_vec();
sorted.sort_by(|a, b| {
a.partial_cmp(b)
.expect("data values MUST be comparable (no NaN)")
});
let n = sorted.len();
let q1_idx = n / 4;
let q3_idx = (3 * n) / 4;
let q1 = sorted[q1_idx];
let q3 = sorted[q3_idx];
let iqr = q3 - q1;
let lower_bound = q1 - 1.5 * iqr;
let upper_bound = q3 + 1.5 * iqr;
data.iter()
.cloned()
.filter(|&x| x >= lower_bound && x <= upper_bound)
.collect()
}
}
#[derive(Default)]
pub struct BenchmarkBuilder {
backend: Option<ComputeBackend>,
workload: Option<WorkloadType>,
size: Option<usize>,
duration: Option<Duration>,
}
impl BenchmarkBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn backend(mut self, backend: ComputeBackend) -> Self {
self.backend = Some(backend);
self
}
pub fn backend_str(mut self, backend: &str) -> Self {
self.backend = Some(match backend.to_lowercase().as_str() {
"cuda" => ComputeBackend::Cuda,
"wgpu" => ComputeBackend::Wgpu,
"simd" => ComputeBackend::Simd,
_ => ComputeBackend::Simd, });
self
}
pub fn workload_type(mut self, workload: WorkloadType) -> Self {
self.workload = Some(workload);
self
}
pub fn workload(mut self, workload: &str) -> Self {
self.workload = Some(match workload.to_lowercase().as_str() {
"dot" | "dotproduct" | "dot_product" => WorkloadType::Gemm,
"elementwise" | "element_wise" => WorkloadType::Elementwise,
"reduction" | "reduce" => WorkloadType::Reduction,
"bandwidth" | "memcpy" => WorkloadType::Bandwidth,
"conv2d" | "conv" | "convolution" => WorkloadType::Conv2d,
"attention" | "attn" => WorkloadType::Attention,
"all" => WorkloadType::All,
_ => WorkloadType::Gemm, });
self
}
pub fn size(mut self, size: usize) -> Self {
self.size = Some(size);
self
}
pub fn duration(mut self, duration: Duration) -> Self {
self.duration = Some(duration);
self
}
pub fn duration_secs(mut self, secs: u64) -> Self {
self.duration = Some(Duration::from_secs(secs));
self
}
pub fn build(self) -> Result<Benchmark, CbtopError> {
Ok(Benchmark {
inner: HeadlessBenchmark::new(
self.backend.unwrap_or(ComputeBackend::Simd),
self.workload.unwrap_or(WorkloadType::Gemm),
self.size.unwrap_or(1_000_000),
self.duration.unwrap_or(Duration::from_secs(5)),
),
})
}
}
pub struct Benchmark {
inner: HeadlessBenchmark,
}
impl Benchmark {
pub fn builder() -> BenchmarkBuilder {
BenchmarkBuilder::new()
}
pub fn run(&self) -> Result<BenchmarkResult, CbtopError> {
self.inner.run()
}
pub fn run_with_baseline(
&self,
baseline: &BenchmarkResult,
threshold: f64,
) -> Result<(BenchmarkResult, RegressionResult), CbtopError> {
let result = self.inner.run()?;
let regression = result.check_regression(baseline, threshold);
Ok((result, regression))
}
}
#[cfg(test)]
mod tests;