Skip to main content

cubecl_runtime/tune/
local.rs

1use super::{AutotuneKey, AutotuneOutput, TunableSet, TuneInputs, Tuner};
2#[cfg(feature = "autotune-checks")]
3use crate::tune::AutotuneLoggerExt;
4use crate::{client::ComputeClient, runtime::Runtime, tune::TuneCacheResult};
5use alloc::string::ToString;
6use alloc::sync::Arc;
7use core::{
8    any::{Any, TypeId},
9    fmt::Display,
10    hash::Hash,
11};
12use cubecl_environment::collections::HashMap;
13use cubecl_environment::sync::{Mutex, RwLock};
14
15/// A local tuner allows to create a tuner for a specific key that can be different from the server
16/// key.
17pub struct LocalTuner<AK: AutotuneKey, ID> {
18    state: Mutex<Option<HashMap<ID, Arc<Tuner<AK>>>>>,
19    name: &'static str,
20    sets: RwLock<Option<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>>,
21}
22
23/// Create a local tuner with the provided name.
24#[macro_export]
25macro_rules! local_tuner {
26    ($name:expr) => {
27        LocalTuner::new(concat!(module_path!(), "-", $name));
28    };
29    () => {
30        LocalTuner::new(module_path!());
31    };
32}
33
34pub use local_tuner;
35
36impl<AK, ID> LocalTuner<AK, ID>
37where
38    AK: AutotuneKey + 'static,
39    ID: Hash + PartialEq + Eq + Clone + Display,
40{
41    /// Create a new local tuner.
42    pub const fn new(name: &'static str) -> Self {
43        Self {
44            state: Mutex::new(None),
45            name,
46            sets: RwLock::new(None),
47        }
48    }
49
50    /// Get or initialize the [`TunableSet`] for this tuner.
51    ///
52    /// Returns a cached `Arc<TunableSet>` keyed by the `TypeId` of `init_set`. The
53    /// initializer runs at most once per process.
54    pub fn init<I, Out, F>(&self, init_set: F) -> Arc<TunableSet<AK, I, Out>>
55    where
56        F: Fn() -> TunableSet<AK, I, Out> + 'static + Send + Sync,
57        I: TuneInputs,
58        Out: AutotuneOutput,
59    {
60        let sets = self.sets.read();
61        let type_id = TypeId::of::<F>();
62
63        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.";
64
65        if let Some(sets) = sets.as_ref()
66            && let Some(set) = sets.get(&type_id)
67        {
68            return set.clone().downcast().expect(DOWNCAST_ERROR);
69        };
70
71        core::mem::drop(sets);
72
73        let mut sets = self.sets.write();
74
75        if let Some(sets) = sets.as_ref()
76            && let Some(set) = sets.get(&type_id)
77        {
78            return set.clone().downcast().expect(DOWNCAST_ERROR);
79        };
80
81        let content = Arc::new(init_set());
82
83        if let Some(sets) = sets.as_mut() {
84            sets.insert(type_id, content.clone());
85        } else {
86            let mut map = HashMap::<TypeId, Arc<dyn Any + Send + Sync>>::new();
87            map.insert(type_id, content.clone());
88            *sets = Some(map);
89        };
90
91        content
92    }
93
94    /// Clear the autotune state.
95    pub fn clear(&self) {
96        if let Some(s) = self.state.lock().as_mut() {
97            s.clear()
98        }
99    }
100
101    #[cfg(feature = "autotune-checks")]
102    fn checks<'a, I: TuneInputs, Out: AutotuneOutput>(
103        &self,
104        operations: &TunableSet<AK, I, Out>,
105        inputs: &<I as TuneInputs>::At<'a>,
106    ) -> alloc::vec::Vec<crate::tune::log::CheckResult>
107    where
108        <I as TuneInputs>::At<'a>: Clone + Send,
109    {
110        use alloc::vec::Vec;
111
112        let mut checks_outputs = Vec::new();
113        for i in 0..operations.len() {
114            let op = operations.fastest(i);
115            let result = op.execute(inputs.clone());
116            checks_outputs.push((op.name.to_string(), result));
117        }
118        super::check_autotune_outputs(checks_outputs)
119    }
120
121    /// Execute the fastest operation in a [`TunableSet`], triggering a tuning pass on
122    /// the first call for a given key.
123    pub fn execute<'a, R: Runtime, I: TuneInputs, Out>(
124        &self,
125        id: &ID,
126        client: &ComputeClient<R>,
127        operations: Arc<TunableSet<AK, I, Out>>,
128        inputs: <I as TuneInputs>::At<'a>,
129    ) -> Out
130    where
131        <I as TuneInputs>::At<'a>: Clone + Send,
132        Out: AutotuneOutput,
133    {
134        let key = operations.generate_key(&inputs);
135
136        let tuner = {
137            let mut state_lock = self.state.lock();
138            let state_map = state_lock.get_or_insert_with(|| HashMap::new());
139            state_map
140                .entry(id.clone())
141                .or_insert_with(move || {
142                    let name = self.name.replace("::", "-");
143                    Arc::new(Tuner::new(&name, &id.to_string()))
144                })
145                .clone()
146        };
147
148        #[allow(unused_mut)]
149        let mut log_context = crate::tune::AutotuneLogContext::new(&mut tuner.logger().lock());
150
151        #[cfg(feature = "autotune-checks")]
152        log_context.set_checks(|| self.checks::<I, Out>(&operations, &inputs));
153
154        // Fast path: a cached hit skips straight to the fastest operation.
155        // `fastest` also resets the tuner cache if the environment switched, so
156        // a miss here falls through to `check_tune`, which re-hydrates.
157        if let TuneCacheResult::Hit { fastest_index } = tuner.fastest(&key) {
158            return operations
159                .fastest(fastest_index)
160                .execute(inputs)
161                .expect("Should run when selected by autotune.");
162        }
163
164        let fastest = tuner.check_tune::<R, I, Out>(
165            &key,
166            &inputs,
167            &operations,
168            || operations.compute_checksum(),
169            client,
170            log_context,
171        );
172
173        // Run the execution depending on the cache state.
174        match fastest {
175            TuneCacheResult::Hit { fastest_index } => operations
176                .fastest(fastest_index)
177                .execute(inputs)
178                .expect("Should run when selected by autotune."),
179            TuneCacheResult::Unchecked | TuneCacheResult::Miss => {
180                panic!(
181                    "Somehow we STILL didn't check a tuning checksum or start tuning, something has gone wrong."
182                )
183            }
184            TuneCacheResult::Pending => {
185                // Still waiting (e.g. on wasm). Try all operations as a fallback.
186                for i in 0..operations.len() {
187                    if let Ok(output) = operations.fastest(i).execute(inputs.clone()) {
188                        return output;
189                    }
190                }
191                panic!("All autotune operations failed, no viable operation found.");
192            }
193        }
194    }
195}