use super::{AutotuneError, AutotuneKey, AutotuneOutput, TunableSet, TuneInputs, Tuner};
#[cfg(feature = "autotune-checks")]
use crate::tune::AutotuneLoggerExt;
use crate::{client::Client, device::HardwareIdentity, tune::TuneCacheResult};
#[cfg(feature = "autotune-checks")]
use alloc::string::ToString;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::{
any::{Any, TypeId},
hash::Hash,
};
use cubecl_environment::collections::HashMap;
use cubecl_environment::sync::{Mutex, RwLock};
type Sets<ID> = RwLock<Option<HashMap<(TypeId, ID), Arc<dyn Any + Send + Sync>>>>;
pub struct LocalTuner<AK: AutotuneKey, ID> {
state: Mutex<Option<Tuners<AK, ID>>>,
name: &'static str,
sets: Sets<ID>,
}
struct Tuners<AK: AutotuneKey, ID> {
by_id: HashMap<ID, HashMap<u64, Arc<Tuner<AK>>>>,
opened: HashMap<TunerIdentity, Arc<Tuner<AK>>>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct TunerIdentity {
hardware: HardwareIdentity,
checksum: u64,
}
impl<AK: AutotuneKey, ID: Hash + Eq + Clone> Tuners<AK, ID> {
fn new() -> Self {
Self {
by_id: HashMap::new(),
opened: HashMap::new(),
}
}
fn get_or_open<I: TuneInputs, Out, Id>(
&mut self,
id: &ID,
name: &str,
client: &Client,
operations: &TunableSet<AK, I, Out, Id>,
) -> Arc<Tuner<AK>> {
let checksum = operations.checksum();
if let Some(tuner) = self.by_id.get(id).and_then(|lists| lists.get(&checksum)) {
return tuner.clone();
}
let identity = TunerIdentity {
hardware: client.hardware_identity(),
checksum,
};
let tuner = self
.opened
.entry(identity)
.or_insert_with_key(|identity| {
Arc::new(Tuner::new(&name.replace("::", "-"), &identity.hardware))
})
.clone();
self.by_id
.entry(id.clone())
.or_default()
.insert(checksum, tuner.clone());
tuner
}
}
#[macro_export]
macro_rules! local_tuner {
($name:expr) => {
LocalTuner::new(concat!(module_path!(), "-", $name))
};
() => {
LocalTuner::new(module_path!())
};
}
pub use local_tuner;
impl<AK, ID> LocalTuner<AK, ID>
where
AK: AutotuneKey + 'static,
ID: Hash + PartialEq + Eq + Clone,
{
pub const fn new(name: &'static str) -> Self {
Self {
state: Mutex::new(None),
name,
sets: RwLock::new(None),
}
}
pub fn init<I, Out, Id, F>(&self, id: &ID, init_set: F) -> Arc<TunableSet<AK, I, Out, Id>>
where
F: Fn() -> TunableSet<AK, I, Out, Id> + 'static + Send + Sync,
Id: Send + Sync + 'static,
I: TuneInputs,
Out: AutotuneOutput,
{
let key = (TypeId::of::<F>(), id.clone());
let sets = self.sets.read();
static DOWNCAST_ERROR: &str = "Local tuner only support one set of tunable that must work on the same input and output declared with the init function.";
if let Some(sets) = sets.as_ref()
&& let Some(set) = sets.get(&key)
{
return set.clone().downcast().expect(DOWNCAST_ERROR);
};
core::mem::drop(sets);
let mut sets = self.sets.write();
if let Some(sets) = sets.as_ref()
&& let Some(set) = sets.get(&key)
{
return set.clone().downcast().expect(DOWNCAST_ERROR);
};
let content = Arc::new(init_set());
if let Some(sets) = sets.as_mut() {
sets.insert(key, content.clone());
} else {
let mut map = HashMap::<(TypeId, ID), Arc<dyn Any + Send + Sync>>::new();
map.insert(key, content.clone());
*sets = Some(map);
};
content
}
pub fn fastest_identity<I, Out, Id>(
&self,
id: &ID,
operations: &TunableSet<AK, I, Out, Id>,
key: &AK,
) -> Option<Id>
where
I: TuneInputs,
Id: Clone,
{
let tuner = self
.state
.lock()
.as_ref()?
.by_id
.get(id)?
.get(&operations.checksum())?
.clone();
let fastest_index = tuner.settled(key)?;
Some(operations.identity(fastest_index).clone())
}
pub fn clear(&self) {
self.state.lock().take();
}
#[cfg(feature = "autotune-checks")]
fn checks<'a, I: TuneInputs, Out: AutotuneOutput, Id>(
&self,
operations: &TunableSet<AK, I, Out, Id>,
inputs: &<I as TuneInputs>::At<'a>,
) -> alloc::vec::Vec<crate::tune::log::CheckResult>
where
<I as TuneInputs>::At<'a>: Clone + Send,
{
let mut checks_outputs = Vec::new();
for i in 0..operations.len() {
let op = operations.fastest(i);
let result = op.execute(inputs.clone());
checks_outputs.push((op.name.to_string(), result));
}
super::check_autotune_outputs(checks_outputs)
}
pub fn execute<'a, I: TuneInputs, Out, Id>(
&self,
id: &ID,
client: &Client,
operations: Arc<TunableSet<AK, I, Out, Id>>,
inputs: <I as TuneInputs>::At<'a>,
) -> Out
where
<I as TuneInputs>::At<'a>: Clone + Send,
Out: AutotuneOutput,
Id: Send + Sync,
{
let key = operations.generate_key(&inputs);
let tuner = self
.state
.lock()
.get_or_insert_with(Tuners::new)
.get_or_open(id, self.name, client, &operations);
if let TuneCacheResult::Hit { fastest_index } = tuner.fastest(&key) {
#[cfg(feature = "autotune-checks")]
{
let mut log_context =
crate::tune::AutotuneLogContext::new(&mut tuner.logger().lock());
log_context.set_checks(|| self.checks::<I, Out, Id>(&operations, &inputs));
}
return execute_settled(&operations, &key, fastest_index, inputs);
}
#[allow(unused_mut)]
let mut log_context = crate::tune::AutotuneLogContext::new(&mut tuner.logger().lock());
#[cfg(feature = "autotune-checks")]
log_context.set_checks(|| self.checks::<I, Out, Id>(&operations, &inputs));
let fastest =
tuner.check_tune::<I, Out, Id>(&key, &inputs, &operations, client, log_context);
match fastest {
TuneCacheResult::Hit { fastest_index } => {
execute_settled(&operations, &key, fastest_index, inputs)
}
TuneCacheResult::Miss => {
panic!(
"Somehow we STILL didn't check a tuning checksum or start tuning, something has gone wrong."
)
}
TuneCacheResult::Pending | TuneCacheResult::Compiled => {
match execute_in_plan_order(&operations, &key, inputs) {
Ok(output) => output,
Err(failures) => panic!(
"All autotune operations failed, no viable operation found.\n{failures:?}"
),
}
}
}
}
}
fn execute_settled<'a, AK: AutotuneKey, I: TuneInputs, Out: 'static, Id>(
operations: &TunableSet<AK, I, Out, Id>,
key: &AK,
fastest_index: usize,
inputs: <I as TuneInputs>::At<'a>,
) -> Out
where
<I as TuneInputs>::At<'a>: Clone,
{
let refused = match operations.fastest(fastest_index).execute(inputs.clone()) {
Ok(output) => return output,
Err(refused) => refused,
};
match execute_in_plan_order(operations, key, inputs) {
Ok(output) => output,
Err(failures) => panic!(
"The operation autotune settled on refused this call ({refused:?}), and so did every \
other candidate.\n{failures:?}"
),
}
}
fn execute_in_plan_order<'a, AK: AutotuneKey, I: TuneInputs, Out: 'static, Id>(
operations: &TunableSet<AK, I, Out, Id>,
key: &AK,
inputs: <I as TuneInputs>::At<'a>,
) -> Result<Out, Vec<AutotuneError>>
where
<I as TuneInputs>::At<'a>: Clone,
{
let mut failures = Vec::new();
for (index, _) in operations.plan(key).order(operations.len()) {
match operations.fastest(index).execute(inputs.clone()) {
Ok(output) => return Ok(output),
Err(err) => failures.push(err),
}
}
Err(failures)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tune::{Tunable, TuneGroup};
use alloc::string::String;
use core::fmt::Display;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Debug)]
struct FakeAutotuneKey;
impl Display for FakeAutotuneKey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("FakeAutotuneKey")
}
}
impl AutotuneKey for FakeAutotuneKey {}
fn set() -> TunableSet<FakeAutotuneKey, (), usize> {
TunableSet::new(|_: &()| FakeAutotuneKey, |_: &FakeAutotuneKey, _: &()| ())
}
fn answers(index: usize) -> impl Fn(()) -> Result<usize, String> {
move |_| Ok(index)
}
fn fails(_: ()) -> Result<usize, String> {
Err("does not launch here".into())
}
#[test]
fn the_pending_fallback_runs_the_plan_s_first_candidate_not_the_first_declared() {
let group = TuneGroup::<FakeAutotuneKey>::new("group", |_| 1);
let operations = set()
.with(Tunable::<FakeAutotuneKey, (), usize>::new(
"declared_first",
answers(0),
))
.with(
Tunable::<FakeAutotuneKey, (), usize>::new("planned_first", answers(1))
.group(&group, |_| 1),
);
assert_eq!(
execute_in_plan_order(&operations, &FakeAutotuneKey, ()).ok(),
Some(1)
);
}
#[test]
fn the_pending_fallback_moves_down_the_plan_when_a_candidate_does_not_launch() {
let group = TuneGroup::<FakeAutotuneKey>::new("group", |_| 1);
let operations = set()
.with(
Tunable::<FakeAutotuneKey, (), usize>::new("planned_last", answers(0))
.group(&group, |_| 0),
)
.with(
Tunable::<FakeAutotuneKey, (), usize>::new("planned_first", fails)
.group(&group, |_| 2),
)
.with(
Tunable::<FakeAutotuneKey, (), usize>::new("planned_second", answers(2))
.group(&group, |_| 1),
);
assert_eq!(
execute_in_plan_order(&operations, &FakeAutotuneKey, ()).ok(),
Some(2)
);
}
#[test]
fn a_settled_winner_that_refuses_a_call_falls_back_down_the_plan() {
let group = TuneGroup::<FakeAutotuneKey>::new("group", |_| 1);
let operations = set()
.with(
Tunable::<FakeAutotuneKey, (), usize>::new("settled_winner", fails)
.group(&group, |_| 2),
)
.with(
Tunable::<FakeAutotuneKey, (), usize>::new("planned_second", answers(1))
.group(&group, |_| 1),
);
assert_eq!(execute_settled(&operations, &FakeAutotuneKey, 0, ()), 1);
}
#[test]
fn a_settled_winner_that_serves_the_call_runs() {
let operations = set()
.with(Tunable::<FakeAutotuneKey, (), usize>::new(
"first",
answers(0),
))
.with(Tunable::<FakeAutotuneKey, (), usize>::new(
"settled_winner",
answers(1),
));
assert_eq!(execute_settled(&operations, &FakeAutotuneKey, 1, ()), 1);
}
#[test]
fn the_pending_fallback_tries_what_the_plan_left_out_last() {
let never = TuneGroup::<FakeAutotuneKey>::new("never", |_| -1);
let operations = set()
.with(Tunable::<FakeAutotuneKey, (), usize>::new(
"declared_first",
fails,
))
.with(
Tunable::<FakeAutotuneKey, (), usize>::new("left_out", answers(1))
.group(&never, |_| 1),
);
assert_eq!(
execute_in_plan_order(&operations, &FakeAutotuneKey, ()).ok(),
Some(1)
);
let nothing = set().with(Tunable::<FakeAutotuneKey, (), usize>::new("fails", fails));
assert!(execute_in_plan_order(¬hing, &FakeAutotuneKey, ()).is_err());
}
}