use core::time::Duration;
use alloc::vec::Vec;
use crate::throughput::{ThroughputKey, ThroughputValue};
use crate::tune::TuneInputs;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(autotune_persistence, 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(autotune_persistence, derive(serde::Serialize, serde::Deserialize))]
pub struct AutotuneBound {
pub throughput: f64,
pub threshold: f32,
pub ops_count: usize,
}
impl PartialEq for AutotuneBound {
fn eq(&self, other: &Self) -> bool {
self.throughput.to_bits() == other.throughput.to_bits()
&& self.threshold.to_bits() == other.threshold.to_bits()
&& self.ops_count == other.ops_count
}
}
impl Eq for AutotuneBound {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
pub struct Work {
pub compute_ops: usize,
pub bytes: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(std_io, 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,
}
}
}
impl Default for Thresholds {
fn default() -> Self {
Self::uniform(1.0)
}
}
pub fn calculate_bounds(
work: Work,
thresholds: Thresholds,
compute_throughput: &ThroughputValue,
memory_throughput: &ThroughputValue,
memory_key: &ThroughputKey,
) -> Vec<AutotuneBound> {
alloc::vec![
AutotuneBound {
ops_count: work.compute_ops,
throughput: compute_throughput.ops_per_s(),
threshold: thresholds.compute,
},
AutotuneBound {
ops_count: work.bytes,
throughput: memory_throughput.bytes_per_s(memory_key),
threshold: thresholds.memory,
},
]
}
impl TimeBound for AutotuneBound {
fn time_limit(&self) -> Option<Duration> {
if self.throughput.is_normal() && self.threshold.is_normal() {
Some(Duration::from_secs_f64(
(self.ops_count as f64 / self.throughput) / self.threshold as f64,
))
} else {
None
}
}
}
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 crate::throughput::ThroughputMode;
use super::*;
use alloc::vec;
fn bound(ops_count: usize, throughput: f64, threshold: f32) -> AutotuneBound {
AutotuneBound {
throughput,
threshold,
ops_count,
}
}
#[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 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 calculate_bounds_applies_a_threshold_per_resource() {
let work = Work {
compute_ops: 8,
bytes: 16,
};
let thresholds = Thresholds {
compute: 0.5,
memory: 1.0,
};
let key = ThroughputKey {
mode: ThroughputMode::Memory,
};
let bounds = calculate_bounds(
work,
thresholds,
&ThroughputValue::ZERO,
&ThroughputValue::ZERO,
&key,
);
assert_eq!(bounds[0].ops_count, 8);
assert_eq!(bounds[0].threshold, 0.5);
assert_eq!(bounds[1].ops_count, 16);
assert_eq!(bounds[1].threshold, 1.0);
}
#[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);
}
}