use super::{AutotuneError, Evictor, TuneFn, TuneInputs};
use crate::runtime::{client::ComputeClient, backend::Runtime};
use alloc::string::ToString;
use alloc::vec::Vec;
use ruda_core::profile::ProfileDuration;
pub trait AutotuneOutput: Send + 'static {
fn validate_for_tuning(&self, _other: &Self, _absolute: f64, _relative: f64, _max_bytes: u64)
-> Result<bool, alloc::string::String> { Ok(false) }
#[cfg(feature = "runtime-autotune-checks")]
fn check_equivalence(&self, other: Self);
}
impl AutotuneOutput for () {
#[cfg(feature = "runtime-autotune-checks")]
fn check_equivalence(&self, _other: Self) {
}
}
pub fn tune_benchmark<'a, R: Runtime, F: TuneInputs, Out: AutotuneOutput>(
operation: &TuneFn<F, Out>,
inputs: <F as TuneInputs>::At<'a>,
client: ComputeClient<R>,
) -> Result<Vec<ProfileDuration>, AutotuneError> {
tune_benchmark_with_eviction(operation, inputs, client, None)
}
pub fn tune_benchmark_with_eviction<'a, R: Runtime, F: TuneInputs, Out: AutotuneOutput>(
operation: &TuneFn<F, Out>,
inputs: <F as TuneInputs>::At<'a>,
client: ComputeClient<R>,
evictor: Option<&mut Evictor<'_>>,
) -> Result<Vec<ProfileDuration>, AutotuneError> {
client
.clone()
.exclusive(move || profile_exclusive(operation, inputs, client, evictor))
.map_err(|err| AutotuneError::Unknown {
name: operation.name.to_string(),
err: err.to_string(),
})?
}
impl<F: TuneInputs, Out: AutotuneOutput> TuneFn<F, Out> {
pub(crate) fn warmup_once<'a, R: Runtime>(
&self,
inputs: <F as TuneInputs>::At<'a>,
client: &ComputeClient<R>,
) -> Result<(), AutotuneError> {
let _errs = client.flush();
self.sample_once(inputs, client, None).map(|_| ())
}
pub(crate) fn sample_once<'a, R: Runtime>(
&self,
inputs: <F as TuneInputs>::At<'a>,
client: &ComputeClient<R>,
evictor: Option<&mut Evictor<'_>>,
) -> Result<ProfileDuration, AutotuneError> {
if let Some(evict) = evictor
&& let Err(err) = evict()
{
log::error!(
"The eviction before a sample of `{}` failed, so the sample is measured on a warm cache.\n{err:?}",
self.name
);
}
let profiled = client.profile(move || self.execute(inputs), &self.name);
match profiled {
Ok((Ok(_), duration)) => Ok(duration),
Ok((Err(err), _)) => Err(err),
Err(err) => Err(AutotuneError::Unknown {
name: self.name.to_string(),
err: err.to_string(),
}),
}
}
}
fn profile_exclusive<'a, R: Runtime, F: TuneInputs, Out: AutotuneOutput>(
operation: &TuneFn<F, Out>,
inputs: <F as TuneInputs>::At<'a>,
client: ComputeClient<R>,
mut evictor: Option<&mut Evictor<'_>>,
) -> Result<Vec<ProfileDuration>, AutotuneError> {
let _real_run = crate::runtime::dry_run::RealRun::new();
warmup(operation, inputs.clone(), client.clone())?;
use crate::runtime::config::{RudaRuntimeConfig, RuntimeConfig};
let (_, num_samples) = RudaRuntimeConfig::get().autotune.bench.samples();
let mut durations = Vec::with_capacity(num_samples);
for _ in 0..num_samples {
durations.push(operation.sample_once(inputs.clone(), &client, evictor.as_deref_mut())?);
}
if durations.is_empty() {
Err(AutotuneError::InvalidSamples {
name: operation.name.to_string(),
})
} else {
Ok(durations)
}
}
fn warmup<'a, R: Runtime, F: TuneInputs, Out: AutotuneOutput>(
operation: &TuneFn<F, Out>,
inputs: <F as TuneInputs>::At<'a>,
client: ComputeClient<R>,
) -> Result<(), AutotuneError> {
let num_warmup = 3;
let mut num_errors = 0;
let mut last_error = None;
let _errs = client.flush();
for _ in 0..num_warmup {
let inputs = inputs.clone();
let profiled = client.profile(move || operation.execute(inputs), &operation.name);
match profiled {
Ok((Ok(_), _)) => {}
Ok((Err(err), _)) => return Err(err),
Err(err) => {
num_errors += 1;
last_error = Some(err);
}
}
}
if num_errors < num_warmup {
Ok(())
} else {
let msg = alloc::format!("{:?}", last_error.expect("all warmup profiles failed"));
Err(AutotuneError::Unknown {
name: operation.name.to_string(),
err: msg,
})
}
}