use crate::runtime::{
config::RudaRuntimeConfig,
throughput::{ThroughputCache, ThroughputError, ThroughputKey, ThroughputValue},
};
use alloc::boxed::Box;
use alloc::sync::Arc;
use ruda_core::profile::{Duration, Instant};
use ruda_core::config::RuntimeConfig;
use spin::Mutex;
#[allow(unused_imports)]
use num_traits::Float;
type Cache = Arc<Mutex<ThroughputCache>>;
const WARMUP_BUDGET: Duration = Duration::from_secs(2);
const PLATEAU_FLOOR: Duration = Duration::from_millis(250);
const SAMPLE_BUDGET: Duration = Duration::from_millis(200);
const SAMPLE_PATIENCE: usize = 12;
const TARGET_DURATION: Duration = Duration::from_millis(20);
const RANK_SAMPLES: usize = 3;
pub struct KernelConfig {
pub sample: Box<dyn Fn(usize) -> Result<Duration, ThroughputError>>,
pub ops_count: usize,
pub min_iterations: usize,
}
pub struct Ranked {
pub value: ThroughputValue,
pub iterations: usize,
}
pub struct ThroughputBenchmarker {
cache: Cache,
cache_enabled: bool,
}
impl ThroughputBenchmarker {
pub fn new(cache: Cache) -> Self {
let cache_enabled = !RudaRuntimeConfig::get().throughput.disable_cache;
Self {
cache,
cache_enabled,
}
}
pub fn cached(&self, key: ThroughputKey) -> Option<ThroughputValue> {
if !self.cache_enabled {
return None;
}
self.cache.lock().get_current(&key).copied()
}
pub fn measure(
&mut self,
key: ThroughputKey,
probe: impl FnOnce() -> Result<ThroughputValue, ThroughputError>,
) -> Result<ThroughputValue, ThroughputError> {
if self.cache_enabled
&& let Some(cached_value) = self.cache.lock().get_current(&key)
{
return Ok(*cached_value);
}
let _real_run = crate::runtime::dry_run::RealRun::new();
let value = probe()?;
if self.cache_enabled {
self.cache.lock().insert(key, value);
}
Ok(value)
}
pub fn sample(kernel_config: KernelConfig) -> Result<ThroughputValue, ThroughputError> {
let _real_run = crate::runtime::dry_run::RealRun::new();
let sample = kernel_config.sample;
let iterations = Self::warmup(kernel_config.min_iterations, WARMUP_BUDGET, &sample)?;
let duration =
Self::sample_peak_duration(iterations, &sample, SAMPLE_BUDGET, SAMPLE_PATIENCE)?;
Ok(ThroughputValue {
ops_count: kernel_config.ops_count,
duration,
})
}
pub fn warm(kernel_config: &KernelConfig) -> Result<usize, ThroughputError> {
let _real_run = crate::runtime::dry_run::RealRun::new();
Self::warmup(
kernel_config.min_iterations,
WARMUP_BUDGET,
&kernel_config.sample,
)
}
pub fn sample_at(kernel_config: &KernelConfig, iterations: usize) -> Result<ThroughputValue, ThroughputError> {
let _real_run = crate::runtime::dry_run::RealRun::new();
let iterations = iterations.max(kernel_config.min_iterations).max(1);
let duration = Self::sample_peak_duration(
iterations,
&kernel_config.sample,
SAMPLE_BUDGET,
SAMPLE_PATIENCE,
)?;
Ok(ThroughputValue {
ops_count: kernel_config.ops_count,
duration,
})
}
pub fn rank(kernel_config: &KernelConfig, iterations: usize) -> Result<Ranked, ThroughputError> {
let _real_run = crate::runtime::dry_run::RealRun::new();
let settling = iterations.max(kernel_config.min_iterations).max(1);
let _ = (kernel_config.sample)(settling)?;
let took = (kernel_config.sample)(settling)?;
let iterations = Self::retarget(settling, took)
.max(kernel_config.min_iterations)
.max(1);
let mut fastest = Duration::MAX;
for _ in 0..RANK_SAMPLES {
fastest = fastest.min((kernel_config.sample)(iterations)?);
}
let duration = fastest / iterations as u32;
Ok(Ranked {
value: ThroughputValue {
ops_count: kernel_config.ops_count,
duration,
},
iterations,
})
}
fn retarget(iterations: usize, took: Duration) -> usize {
let took = took.as_secs_f64();
if took <= 0.0 {
return iterations;
}
let scaled = iterations as f64 * (TARGET_DURATION.as_secs_f64() / took);
if scaled.is_finite() {
(scaled as usize).max(1)
} else {
iterations
}
}
fn warmup(
min_iterations: usize,
budget: Duration,
sample: impl Fn(usize) -> Result<Duration, ThroughputError>,
) -> Result<usize, ThroughputError> {
const MAX_WARMUP: usize = 50;
const MAX_ITERATIONS: usize = 1 << 24;
const MAX_BLIND_ITERATIONS: usize = 1 << 10;
const PLATEAU_TOL: f64 = 0.03;
const PATIENCE: usize = 3;
let target_ms = TARGET_DURATION.as_secs_f64() * 1000.0;
let mut best = f64::INFINITY;
let mut stable = 0;
let mut iterations = min_iterations.max(1);
let start = Instant::now();
let mut plateau_start = start;
for _ in 0..MAX_WARMUP {
let duration = sample(iterations)?.as_secs_f64() * 1000.0;
if duration < target_ms {
let (extra_iters, ceiling) = if duration > 1e-6 {
let duration_per_iter = duration / iterations as f64;
(
((target_ms - duration) / duration_per_iter).ceil() as usize,
MAX_ITERATIONS,
)
} else {
(iterations, MAX_BLIND_ITERATIONS)
};
let ceiling = ceiling.max(min_iterations);
if iterations >= ceiling || start.elapsed() >= budget {
break;
}
iterations = (iterations + extra_iters.max(1)).min(ceiling);
best = f64::INFINITY;
stable = 0;
continue;
}
let duration_per_iter = duration / iterations as f64;
if duration_per_iter < best * (1.0 - PLATEAU_TOL) {
best = duration_per_iter;
stable = 0;
plateau_start = Instant::now();
} else {
best = best.min(duration_per_iter);
stable += 1;
if stable >= PATIENCE && plateau_start.elapsed() >= PLATEAU_FLOOR {
break;
}
}
}
Ok(iterations)
}
fn sample_peak_duration(
iterations: usize,
sample_once: impl Fn(usize) -> Result<Duration, ThroughputError>,
budget: Duration,
patience: usize,
) -> Result<Duration, ThroughputError> {
debug_assert!(
iterations > 0,
"iterations must be positive to avoid division by zero"
);
const MAX_SAMPLES: usize = 200;
const REL_TOL: f64 = 0.01;
let mut best = f64::INFINITY;
let mut stale = 0;
let start = Instant::now();
for _ in 0..MAX_SAMPLES {
let s = sample_once(iterations)?.as_secs_f64();
if s < best * (1.0 - REL_TOL) {
best = s;
stale = 0;
} else {
best = best.min(s);
stale += 1;
}
if stale >= patience || start.elapsed() >= budget {
break;
}
}
Ok(Duration::from_secs_f64(best / iterations as f64))
}
}