use cubecl_runtime::throughput::{
KernelConfig, ThroughputBenchmarker, ThroughputError, ThroughputValue,
};
pub(super) struct ShapeSweep<S> {
shapes: alloc::vec::Vec<S>,
}
impl<S: Copy> ShapeSweep<S> {
pub(super) fn new(shapes: alloc::vec::Vec<S>) -> Self {
Self { shapes }
}
pub(super) fn fastest(
&self,
build: impl Fn(S) -> Result<KernelConfig, ThroughputError>,
) -> Result<(ThroughputValue, S), ThroughputError> {
let (fastest, warmed) = match self.shapes.len() {
0 => return Err(ThroughputError::NoTiming),
1 => (self.shapes[0], None),
_ => {
let (fastest, iterations) = self.ranked(&build)?;
(fastest, Some(iterations))
}
};
let config = build(fastest)?;
let value = match warmed {
Some(iterations) => ThroughputBenchmarker::sample_at(&config, iterations),
None => ThroughputBenchmarker::sample(config),
};
value
.ops_per_s()
.is_finite()
.then_some((value, fastest))
.ok_or(ThroughputError::NoTiming)
}
fn ranked(
&self,
build: impl Fn(S) -> Result<KernelConfig, ThroughputError>,
) -> Result<(S, usize), ThroughputError> {
let mut warmed = None;
let mut fastest: Option<(f64, S)> = None;
for shape in &self.shapes {
let config = build(*shape)?;
let start = *warmed.get_or_insert_with(|| ThroughputBenchmarker::warm(&config));
let ranked = ThroughputBenchmarker::rank(&config, start);
let rate = ranked.value.ops_per_s();
warmed = Some(ranked.iterations);
if rate.is_finite() && fastest.is_none_or(|(best, _)| rate > best) {
fastest = Some((rate, *shape));
}
}
fastest
.zip(warmed)
.map(|((_, shape), iterations)| (shape, iterations))
.ok_or(ThroughputError::NoTiming)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_shape_that_ranks_fastest_is_the_one_measured() {
let build = |rate: u64| {
Ok(KernelConfig {
sample: alloc::boxed::Box::new(move |iterations| {
core::time::Duration::from_nanos(iterations as u64 * 1000 / rate)
}),
ops_count: 1,
min_iterations: 1,
})
};
let (value, fastest) = ShapeSweep::new(alloc::vec![1, 4, 2])
.fastest(build)
.expect("a shape ran");
assert_eq!(fastest, 4);
assert!(value.ops_per_s().is_finite());
}
#[test]
fn a_shape_that_cannot_be_built_fails_the_sweep() {
let build = |_: u64| Err(ThroughputError::Allocation);
for shapes in [alloc::vec![1], alloc::vec![1, 2]] {
let failed = ShapeSweep::new(shapes).fastest(build);
assert_eq!(failed.err(), Some(ThroughputError::Allocation));
}
}
}