use alloc::boxed::Box;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt::{Debug, Display, Write};
use core::hash::Hash;
use spin::Once;
use alloc::format;
use super::{
AutotuneError, input_generator::InputGenerator, key_generator::KeyGenerator,
tune_inputs::TuneInputs,
};
use super::{Tunable, TunePlan};
type TuneDelegate<I, Out> =
dyn for<'inp> Fn(<I as TuneInputs>::At<'inp>) -> Result<Out, AutotuneError> + Send + Sync;
#[derive(new)]
pub struct TuneFn<I: TuneInputs, Out> {
pub(crate) name: String,
func: Box<TuneDelegate<I, Out>>,
}
impl<I: TuneInputs, Out: 'static> TuneFn<I, Out> {
pub fn execute<'a>(&self, inputs: <I as TuneInputs>::At<'a>) -> Result<Out, AutotuneError> {
(self.func)(inputs)
}
}
pub struct TunableSet<K: AutotuneKey, F: TuneInputs, Output: 'static> {
tunables: Vec<Tunable<K, F, Output>>,
key_gen: Arc<dyn KeyGenerator<K, F> + Send + Sync>,
input_gen: Arc<dyn InputGenerator<K, F> + Send + Sync>,
stack_reference: Option<usize>,
stack_revision: String,
stack_workload: Option<Arc<dyn for<'a> Fn(&F::At<'a>) -> String + Send + Sync>>,
checksum: Once<String>,
stack_manifest: Once<String>,
#[cfg(std_io)]
stack_plan_cache: Option<spin::Mutex<alloc::collections::VecDeque<(K, String, Arc<StaticStackPlan>)>>>,
}
#[cfg(std_io)]
pub(crate) struct StaticStackPlan {
pub(crate) indices: Vec<usize>,
pub(crate) candidates: Vec<super::stack::Candidate>,
pub(crate) key: String,
}
impl<K: AutotuneKey, F: TuneInputs, Output: 'static> TunableSet<K, F, Output> {
pub fn len(&self) -> usize {
self.tunables.len()
}
pub fn is_empty(&self) -> bool {
self.tunables.is_empty()
}
pub fn new(key_gen: impl KeyGenerator<K, F>, input_gen: impl InputGenerator<K, F>) -> Self {
Self {
tunables: Default::default(),
input_gen: Arc::new(input_gen),
key_gen: Arc::new(key_gen),
stack_reference: None,
stack_revision: String::new(),
stack_workload: None,
checksum: Once::new(),
stack_manifest: Once::new(),
#[cfg(std_io)]
stack_plan_cache: None,
}
}
pub fn with_stack_tuning(
mut self, reference: usize, revision: &str,
workload: impl for<'a> Fn(&F::At<'a>) -> String + Send + Sync + 'static,
) -> Self {
self.stack_reference = Some(reference);
self.stack_revision = revision.into();
self.stack_workload = Some(Arc::new(workload));
self.stack_manifest = Once::new();
#[cfg(std_io)]
if let Some(cache) = &self.stack_plan_cache { cache.lock().clear(); }
self
}
pub fn stack_reference(&self) -> Option<usize> { self.stack_reference }
pub fn stack_workload<'a>(&self, inputs: &F::At<'a>) -> Option<String> {
self.stack_workload.as_ref().map(|f| f(inputs))
}
pub fn with_static_stack_plan(mut self) -> Self {
#[cfg(std_io)]
{ self.stack_plan_cache = Some(spin::Mutex::new(Default::default())); }
self
}
#[cfg(std_io)]
pub(crate) fn static_stack_plan_enabled(&self) -> bool { self.stack_plan_cache.is_some() }
#[cfg(std_io)]
pub(crate) fn cached_stack_plan(&self, key: &K, identity: &str) -> Option<Arc<StaticStackPlan>> {
self.stack_plan_cache.as_ref()?.lock().iter()
.find(|(stored_key, stored_identity, _)| stored_key == key && stored_identity == identity)
.map(|(_, _, plan)| plan.clone())
}
#[cfg(std_io)]
pub(crate) fn cache_stack_plan(&self, key: K, identity: String, plan: Arc<StaticStackPlan>) {
if let Some(cache) = &self.stack_plan_cache {
let mut cache = cache.lock();
if cache.iter().any(|(stored_key, stored_identity, _)| stored_key == &key && stored_identity == &identity) { return; }
if cache.len() == 32 { cache.pop_front(); }
cache.push_back((key, identity, plan));
}
}
pub fn new_cloning_inputs(key_gen: impl KeyGenerator<K, F>) -> Self {
Self::new(key_gen, super::CloneInputGenerator)
}
pub fn with(mut self, tunable: Tunable<K, F, Output>) -> Self {
self.tunables.push(tunable);
self.checksum = Once::new();
self.stack_manifest = Once::new();
#[cfg(std_io)]
if let Some(cache) = &self.stack_plan_cache { cache.lock().clear(); }
self
}
pub fn autotunables(&self) -> impl Iterator<Item = &TuneFn<F, Output>> {
self.tunables.iter().map(|tunable| &tunable.function)
}
pub(crate) fn plan(&self, key: &K) -> TunePlan {
TunePlan::new(key, &self.tunables)
}
pub fn fastest(&self, fastest_index: usize) -> &TuneFn<F, Output> {
&self.tunables[fastest_index].function
}
pub fn compute_checksum(&self) -> String {
self.checksum.call_once(|| {
let mut checksum = String::new();
for tune in &self.tunables { checksum += &tune.function.name; }
format!("{:x}", md5::compute(checksum))
}).clone()
}
pub fn stack_checksum(&self) -> String {
String::from(self.stack_checksum_ref())
}
pub(crate) fn stack_checksum_ref(&self) -> &str {
self.stack_manifest.call_once(|| {
let mut checksum = format!("stack-v1:{}:{};reference={:?};", self.stack_revision.len(), self.stack_revision, self.stack_reference);
for tune in &self.tunables {
let _ = write!(checksum, "{}:{}", tune.function.name.len(), tune.function.name);
}
format!("{:x}", md5::compute(checksum))
}).as_str()
}
pub fn generate_key<'a>(&self, inputs: &F::At<'a>) -> K {
self.key_gen.generate(inputs)
}
pub fn generate_inputs<'a>(&self, key: &K, inputs: &F::At<'a>) -> F::At<'a> {
self.input_gen.generate(key, inputs)
}
}
#[cfg(std_io)]
pub trait AutotuneKey:
Clone
+ Debug
+ PartialEq
+ Eq
+ Hash
+ Display
+ serde::Serialize
+ serde::de::DeserializeOwned
+ Send
+ Sync
+ 'static
{
}
#[cfg(not(std_io))]
pub trait AutotuneKey:
Clone + Debug + PartialEq + Eq + Hash + Display + Send + Sync + 'static
{
}
impl AutotuneKey for String {}
#[cfg(all(test, std_io))]
mod static_plan_tests {
use super::*;
fn set() -> TunableSet<String, String, String> {
TunableSet::new_cloning_inputs(|value: &String| value.clone())
.with_static_stack_plan()
}
fn plan() -> Arc<StaticStackPlan> {
Arc::new(StaticStackPlan { indices: alloc::vec![0],
candidates: alloc::vec![super::super::stack::Candidate::new("reference")], key: "full-key".into() })
}
#[test]
fn cache_requires_exact_generated_key_and_full_identity() {
let set = set();
let plan = plan();
set.cache_stack_plan("shape".into(), "device/offset/stride".into(), plan.clone());
assert!(Arc::ptr_eq(&set.cached_stack_plan(&"shape".into(), "device/offset/stride").unwrap(), &plan));
assert!(set.cached_stack_plan(&"other-shape".into(), "device/offset/stride").is_none());
assert!(set.cached_stack_plan(&"shape".into(), "other-device/offset/stride").is_none());
}
#[test]
fn cache_is_bounded_and_duplicate_insert_does_not_evict() {
let set = set();
for index in 0..32 { set.cache_stack_plan(alloc::format!("{index}"), "binding".into(), plan()); }
set.cache_stack_plan("0".into(), "binding".into(), plan());
assert!(set.cached_stack_plan(&"0".into(), "binding").is_some());
set.cache_stack_plan("32".into(), "binding".into(), plan());
assert!(set.cached_stack_plan(&"0".into(), "binding").is_none());
assert!(set.cached_stack_plan(&"1".into(), "binding").is_some());
}
#[test]
fn changing_stack_contract_invalidates_plan_metadata() {
let set = set();
set.cache_stack_plan("shape".into(), "binding".into(), plan());
let set = set.with_stack_tuning(0, "new-revision", |value: &String| value.clone());
assert!(set.cached_stack_plan(&"shape".into(), "binding").is_none());
}
}