use super::roofline::{Bounded, ReadCold};
use super::{CandidatePriority, GroupPriority, Roofline, TuneCandidate, priority_of};
use core::fmt::Display;
use cubecl::tune::{
AutotuneKey, CloneInputGenerator, InputGenerator, KeyGenerator, OrderedSettings, Tunable,
TunableSet, TuneGroup, TuneInputs,
};
use std::sync::Arc;
pub struct Autotune<K: AutotuneKey, I: TuneInputs, O: 'static, Id = ()> {
set: TunableSet<K, I, O, Id>,
layout: GroupLayout,
floor: TuneGroup<K>,
}
type Launch<I, O> = dyn for<'a> Fn(<I as TuneInputs>::At<'a>) -> Result<O, String> + Send + Sync;
pub trait Identity: Sized {
fn named<K, I, O, Func, Err>(name: &str, func: Func) -> Tunable<K, I, O, Self>
where
I: TuneInputs,
O: 'static,
Err: Into<String> + 'static,
Func: for<'a> Fn(<I as TuneInputs>::At<'a>) -> Result<O, Err> + Send + Sync + 'static;
}
impl Identity for () {
fn named<K, I, O, Func, Err>(name: &str, func: Func) -> Tunable<K, I, O, Self>
where
I: TuneInputs,
O: 'static,
Err: Into<String> + 'static,
Func: for<'a> Fn(<I as TuneInputs>::At<'a>) -> Result<O, Err> + Send + Sync + 'static,
{
Tunable::new(name, func)
}
}
impl<K: AutotuneKey, I: TuneInputs, O: 'static, Id: Identity> Autotune<K, I, O, Id> {
pub fn new(key_gen: impl KeyGenerator<K, I>) -> Self {
Self::with_inputs(key_gen, CloneInputGenerator, None)
}
pub fn measured_on(
key_gen: impl KeyGenerator<K, I>,
inputs: &'static str,
input_gen: impl InputGenerator<K, I>,
) -> Self {
Self::with_inputs(key_gen, input_gen, Some(inputs))
}
fn with_inputs(
key_gen: impl KeyGenerator<K, I>,
input_gen: impl InputGenerator<K, I>,
measured: Option<&'static str>,
) -> Self {
Self {
set: TunableSet::new(key_gen, input_gen),
layout: GroupLayout {
groups: Vec::new(),
measured,
},
floor: GroupLayout::priority_group("floor", GroupPriority::FALLBACK),
}
}
pub fn group(&mut self, name: &'static str, priority: GroupPriority) -> TuneGroup<K> {
self.layout.groups.push((name, priority));
GroupLayout::priority_group(name, priority)
}
pub fn candidate(self, group: &TuneGroup<K>, tunable: Tunable<K, I, O, Id>) -> Self {
self.at(group, tunable, |_| Some(CandidatePriority::FIRST))
}
pub fn candidate_after(self, group: &TuneGroup<K>, tunable: Tunable<K, I, O, Id>) -> Self {
self.at(group, tunable, |_| Some(CandidatePriority::LAST))
}
pub fn prioritized<C, Run>(self, group: &TuneGroup<K>, candidates: Vec<C>, run: Run) -> Self
where
C: TuneCandidate<Key = K> + Send + Sync + 'static,
Run: for<'a> Fn(&C, <I as TuneInputs>::At<'a>) -> Result<O, String> + Send + Sync + 'static,
{
self.listed(group, candidates, run, |candidate, launch| {
Id::named(&candidate.strategy().to_string(), launch)
})
}
pub fn identified<C, Identify, Run>(
self,
group: &TuneGroup<K>,
candidates: Vec<C>,
identify: Identify,
run: Run,
) -> Self
where
C: TuneCandidate<Key = K> + Send + Sync + 'static,
Id: Display,
Identify: Fn(&C) -> Id,
Run: for<'a> Fn(&C, <I as TuneInputs>::At<'a>) -> Result<O, String> + Send + Sync + 'static,
{
self.listed(group, candidates, run, |candidate, launch| {
Tunable::identified(identify(candidate), launch)
})
}
fn listed<C, Run>(
self,
group: &TuneGroup<K>,
candidates: Vec<C>,
run: Run,
tunable: impl Fn(&C, Box<Launch<I, O>>) -> Tunable<K, I, O, Id>,
) -> Self
where
C: TuneCandidate<Key = K> + Send + Sync + 'static,
Run: for<'a> Fn(&C, <I as TuneInputs>::At<'a>) -> Result<O, String> + Send + Sync + 'static,
{
let candidates: Arc<[C]> = candidates.into();
let run = Arc::new(run);
let mut autotune = self;
for position in 0..candidates.len() {
let (prioritized_against, running, run) =
(candidates.clone(), candidates.clone(), run.clone());
let launch: Box<Launch<I, O>> = Box::new(move |inputs| run(&running[position], inputs));
autotune = autotune.at(group, tunable(&candidates[position], launch), move |key| {
priority_of(&prioritized_against, position, key)
});
}
autotune
}
fn at(
self,
group: &TuneGroup<K>,
tunable: Tunable<K, I, O, Id>,
priority: impl Fn(&K) -> Option<CandidatePriority> + Send + Sync + 'static,
) -> Self {
Self {
set: self.set.with(tunable.group(group, move |key| {
priority(key).map_or(GroupPriority::OUT_OF_THE_ROUND, CandidatePriority::get)
})),
..self
}
}
pub fn bounded<F>(self, roofline: F) -> Self
where
F: for<'a> Fn(&K, &<I as TuneInputs>::At<'a>) -> Roofline + Send + Sync + 'static,
{
let roofline = Arc::new(roofline);
Self {
set: self
.set
.with_bounds(Arc::new(Bounded(roofline.clone())))
.with_eviction(Arc::new(ReadCold(roofline))),
..self
}
}
pub fn read_cold<F>(self, roofline: F) -> Self
where
F: for<'a> Fn(&K, &<I as TuneInputs>::At<'a>) -> Roofline + Send + Sync + 'static,
{
Self {
set: self
.set
.with_eviction(Arc::new(ReadCold(Arc::new(roofline)))),
..self
}
}
pub fn floor(self, tunable: Tunable<K, I, O, Id>) -> Self {
let tunable = tunable.group(&self.floor, GroupLayout::within_group);
Self {
set: self.set.with(tunable),
..self
}
}
pub fn finish(self) -> TunableSet<K, I, O, Id> {
let unplanned = GroupLayout::priority_group("group-layout", GroupPriority::UNPLANNED);
self.set.with(
Id::named(&self.layout.to_string(), |_| {
Err::<O, String>("the group layout is a name, not a kernel".to_string())
})
.group(&unplanned, GroupLayout::within_group),
)
}
}
struct GroupLayout {
groups: Vec<(&'static str, GroupPriority)>,
measured: Option<&'static str>,
}
impl GroupLayout {
fn priority_group<K: AutotuneKey>(name: &str, priority: GroupPriority) -> TuneGroup<K> {
let priority = priority.get();
TuneGroup::ordered(name, move |_key| priority, OrderedSettings::default())
}
fn within_group<K>(_key: &K) -> i8 {
CandidatePriority::LAST.get()
}
}
impl Display for GroupLayout {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("groups")?;
for (group, priority) in &self.groups {
write!(f, "-{group}{}", priority.get())?;
}
if let Some(inputs) = self.measured {
write!(f, "-measured-{inputs}")?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CandidateIdentity<S> {
Routine(S),
Named(String),
}
impl<S> CandidateIdentity<S> {
pub fn new<C: TuneCandidate>(candidate: &C) -> Self
where
C::Strategy: Copy + Into<S>,
{
Self::Routine((*candidate.strategy()).into())
}
pub fn routine(self) -> Option<S> {
match self {
Self::Routine(settings) => Some(settings),
Self::Named(_) => None,
}
}
}
impl<S: Display> Display for CandidateIdentity<S> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Routine(settings) => settings.fmt(f),
Self::Named(name) => f.write_str(name),
}
}
}
impl<S: Display> Identity for CandidateIdentity<S> {
fn named<K, I, O, Func, Err>(name: &str, func: Func) -> Tunable<K, I, O, Self>
where
I: TuneInputs,
O: 'static,
Err: Into<String> + 'static,
Func: for<'a> Fn(<I as TuneInputs>::At<'a>) -> Result<O, Err> + Send + Sync + 'static,
{
Tunable::identified(Self::Named(name.to_string()), func)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_group_layout_names_itself() {
let shipped = GroupPriority::DEFAULT;
let raced = GroupLayout {
groups: vec![("tile", shipped), ("cubek", shipped)],
measured: None,
}
.to_string();
assert_ne!(
raced,
GroupLayout {
groups: vec![("tile", shipped), ("cubek", GroupPriority::UNPLANNED)],
measured: None
}
.to_string()
);
assert_ne!(
raced,
GroupLayout {
groups: vec![("tile", shipped)],
measured: None
}
.to_string()
);
assert_ne!(
GroupLayout {
groups: vec![("tile", shipped)],
measured: None
}
.to_string(),
GroupLayout {
groups: vec![("cubek", shipped)],
measured: None
}
.to_string()
);
assert_ne!(
raced,
GroupLayout {
groups: vec![("tile", shipped), ("cubek", shipped)],
measured: Some("at-capacity")
}
.to_string()
);
}
}