use core::time::Duration;
use alloc::vec::Vec;
use crate::config::autotune::AutotuneLevel;
use crate::tune::TuneInputs;
pub use crate::throughput::ResourceBound;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
pub struct Bounds {
pub bounds: Vec<AutotuneBound>,
pub launch_overhead: Duration,
}
impl Eq for Bounds {}
#[diagnostic::on_unimplemented(
message = "`{Self}` is not a valid bounds generator",
label = "invalid bounds generator"
)]
pub trait BoundsGenerator<K, I: TuneInputs>: Send + Sync + 'static {
fn generate<'a>(&self, key: &K, inputs: &I::At<'a>) -> Bounds;
}
impl<K, Func, A> BoundsGenerator<K, A> for Func
where
A: Clone + Send + Sync + 'static,
K: 'static,
Func: Send + Sync + 'static + Fn(&K, &A) -> Bounds,
{
#[inline]
fn generate<'a>(&self, key: &K, inputs: &<A as TuneInputs>::At<'a>) -> Bounds {
(self)(key, inputs)
}
}
pub trait TimeBound {
fn time_limit(&self) -> Option<Duration>;
}
#[derive(Debug, Clone)]
#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
pub struct AutotuneBound {
pub resource: ResourceBound,
pub threshold: f32,
}
impl PartialEq for AutotuneBound {
fn eq(&self, other: &Self) -> bool {
self.resource.peak_per_s.to_bits() == other.resource.peak_per_s.to_bits()
&& self.threshold.to_bits() == other.threshold.to_bits()
&& self.resource.amount == other.resource.amount
}
}
impl Eq for AutotuneBound {}
pub use cubecl_common::work::Work;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
pub struct Thresholds {
pub compute: f32,
pub memory: f32,
}
impl Thresholds {
pub const fn uniform(fraction: f32) -> Self {
Self {
compute: fraction,
memory: fraction,
}
}
pub const UNBOUNDED: Self = Self::uniform(0.0);
pub const fn for_level(level: &AutotuneLevel) -> Self {
match level {
AutotuneLevel::Minimal => Self::uniform(0.6),
AutotuneLevel::Balanced => Self::uniform(0.8),
AutotuneLevel::Extensive => Self::uniform(0.95),
AutotuneLevel::Full => Self::UNBOUNDED,
}
}
}
impl Default for Thresholds {
fn default() -> Self {
Self::uniform(1.0)
}
}
impl TimeBound for AutotuneBound {
fn time_limit(&self) -> Option<Duration> {
if self.threshold <= 0.0 || !self.threshold.is_normal() {
return None;
}
self.resource
.time_at_peak()
.map(|limit| limit.div_f64(self.threshold as f64))
}
}
impl<B: TimeBound> TimeBound for Vec<B> {
fn time_limit(&self) -> Option<Duration> {
self.iter().filter_map(|b| b.time_limit()).max()
}
}
impl TimeBound for Bounds {
fn time_limit(&self) -> Option<Duration> {
self.bounds
.time_limit()
.map(|limit| limit + self.launch_overhead)
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn bound(ops_count: usize, throughput: f64, threshold: f32) -> AutotuneBound {
AutotuneBound {
resource: ResourceBound {
amount: ops_count,
peak_per_s: throughput,
},
threshold,
}
}
#[test]
fn time_limit_is_ops_over_throughput_scaled_by_threshold() {
let limit = bound(8, 4.0, 0.5).time_limit();
assert_eq!(limit, Some(Duration::from_secs(4)));
}
#[test]
fn time_limit_is_none_when_inputs_are_not_normal() {
assert_eq!(bound(8, 0.0, 0.5).time_limit(), None);
assert_eq!(bound(8, f64::NAN, 0.5).time_limit(), None);
assert_eq!(bound(8, f64::INFINITY, 0.5).time_limit(), None);
assert_eq!(bound(8, 4.0, 0.0).time_limit(), None);
}
#[test]
fn time_limit_declines_a_negative_threshold_rather_than_panicking() {
assert_eq!(bound(8, 4.0, -0.5).time_limit(), None);
assert_eq!(bound(8, 4.0, f32::NEG_INFINITY).time_limit(), None);
}
#[test]
fn vec_time_limit_takes_the_roofline_max_not_min() {
let compute = bound(8, 4.0, 1.0); let memory = bound(8, 8.0, 1.0); let limit = vec![compute, memory].time_limit();
assert_eq!(limit, Some(Duration::from_secs(2)));
}
#[test]
fn vec_time_limit_skips_non_normal_bounds_and_is_none_when_empty() {
let limit = vec![bound(8, 0.0, 1.0), bound(8, 4.0, 1.0)].time_limit();
assert_eq!(limit, Some(Duration::from_secs(2)));
assert_eq!(Vec::<AutotuneBound>::new().time_limit(), None);
}
#[test]
fn bounds_time_limit_adds_launch_overhead() {
let bounds = Bounds {
bounds: vec![bound(8, 4.0, 1.0)], launch_overhead: Duration::from_millis(500),
};
assert_eq!(bounds.time_limit(), Some(Duration::from_millis(2500)));
}
#[test]
fn a_level_tightens_the_limit_as_it_rises() {
let threshold = |level| Thresholds::for_level(&level).compute;
assert!(threshold(AutotuneLevel::Minimal) < threshold(AutotuneLevel::Balanced));
assert!(threshold(AutotuneLevel::Balanced) < threshold(AutotuneLevel::Extensive));
assert_eq!(
Thresholds::for_level(&AutotuneLevel::Full),
Thresholds::UNBOUNDED,
"full measures everything"
);
}
#[test]
fn an_unbounded_threshold_gives_no_time_limit() {
let unbounded = bound(8, 4.0, Thresholds::UNBOUNDED.compute);
assert_eq!(unbounded.time_limit(), None);
}
#[test]
fn the_default_threshold_is_the_roofline_itself() {
assert_eq!(Thresholds::default(), Thresholds::uniform(1.0));
assert_eq!(Thresholds::default().compute, 1.0);
}
#[test]
fn bounds_time_limit_is_none_without_usable_bounds() {
let bounds = Bounds {
bounds: vec![],
launch_overhead: Duration::from_millis(500),
};
assert_eq!(bounds.time_limit(), None);
}
}