use crate::{
config::CubeClRuntimeConfig,
throughput::{ThroughputCache, ThroughputKey, ThroughputValue},
};
use alloc::boxed::Box;
use alloc::sync::Arc;
use cubecl_common::profile::Duration;
use cubecl_environment::config::RuntimeConfig;
use cubecl_environment::sync::Mutex;
type Cache = Arc<Mutex<ThroughputCache>>;
pub struct KernelConfig {
pub sample: Box<dyn Fn(usize) -> Duration>,
pub ops_count: usize,
}
pub struct ThroughputBenchmarker {
cache: Cache,
cache_enabled: bool,
}
impl ThroughputBenchmarker {
pub fn new(cache: Cache) -> Self {
let cache_enabled = !CubeClRuntimeConfig::get().throughput.disable_cache;
Self {
cache,
cache_enabled,
}
}
pub fn measure(&mut self, key: ThroughputKey, kernel_config: KernelConfig) -> ThroughputValue {
if self.cache_enabled
&& let Some(cached_value) = self.cache.lock().get(&key)
{
return *cached_value;
}
let sample = kernel_config.sample;
let iterations = self.warmup(&sample);
let duration = self.sample_peak_duration(iterations, &sample);
let value = ThroughputValue {
ops_count: kernel_config.ops_count,
duration,
};
if self.cache_enabled {
self.cache.lock().insert(key, value);
}
value
}
fn warmup(&self, sample: impl Fn(usize) -> Duration) -> usize {
const MAX_WARMUP: usize = 50;
const MAX_ITERATIONS: usize = 1_000;
const PLATEAU_TOL: f64 = 0.03;
const PATIENCE: usize = 3;
const TARGET_DURATION_MS: f64 = 20.0;
let mut best = f64::INFINITY;
let mut stable = 0;
let mut iterations = 1;
for _ in 0..MAX_WARMUP {
let duration = sample(iterations).as_secs_f64() * 1000.0;
if duration < TARGET_DURATION_MS {
let extra_iters = if duration > 1e-6 {
let duration_per_iter = duration / iterations as f64;
((TARGET_DURATION_MS - duration) / duration_per_iter).ceil() as usize
} else {
iterations
};
iterations = (iterations + extra_iters.max(1)).min(MAX_ITERATIONS);
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;
} else {
best = best.min(duration_per_iter);
stable += 1;
if stable >= PATIENCE {
break;
}
}
}
iterations
}
fn sample_peak_duration(
&self,
iterations: usize,
sample_once: impl Fn(usize) -> Duration,
) -> Duration {
debug_assert!(
iterations > 0,
"iterations must be positive to avoid division by zero"
);
const MIN_SAMPLES: usize = 20;
const MAX_SAMPLES: usize = 200;
const REL_TOL: f64 = 0.01;
const PATIENCE: usize = 12;
let mut best = f64::INFINITY;
let mut stale = 0;
for i 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 i > MIN_SAMPLES && stale >= PATIENCE {
break;
}
}
Duration::from_secs_f64(best / iterations as f64)
}
}