1use super::{AutotuneError, AutotuneKey, AutotuneOutput, TunableSet, TuneInputs, Tuner};
2#[cfg(feature = "autotune-checks")]
3use crate::tune::AutotuneLoggerExt;
4use crate::{client::Client, device::HardwareIdentity, tune::TuneCacheResult};
5#[cfg(feature = "autotune-checks")]
6use alloc::string::ToString;
7use alloc::sync::Arc;
8use alloc::vec::Vec;
9use core::{
10 any::{Any, TypeId},
11 hash::Hash,
12};
13use cubecl_environment::collections::HashMap;
14use cubecl_environment::sync::{Mutex, RwLock};
15
16type Sets<ID> = RwLock<Option<HashMap<(TypeId, ID), Arc<dyn Any + Send + Sync>>>>;
21
22pub struct LocalTuner<AK: AutotuneKey, ID> {
25 state: Mutex<Option<Tuners<AK, ID>>>,
26 name: &'static str,
27 sets: Sets<ID>,
28}
29
30struct Tuners<AK: AutotuneKey, ID> {
34 by_id: HashMap<ID, HashMap<u64, Arc<Tuner<AK>>>>,
36 opened: HashMap<TunerIdentity, Arc<Tuner<AK>>>,
37}
38
39#[derive(Clone, Debug, PartialEq, Eq, Hash)]
41struct TunerIdentity {
42 hardware: HardwareIdentity,
43 checksum: u64,
46}
47
48impl<AK: AutotuneKey, ID: Hash + Eq + Clone> Tuners<AK, ID> {
49 fn new() -> Self {
50 Self {
51 by_id: HashMap::new(),
52 opened: HashMap::new(),
53 }
54 }
55
56 fn get_or_open<I: TuneInputs, Out, Id>(
59 &mut self,
60 id: &ID,
61 name: &str,
62 client: &Client,
63 operations: &TunableSet<AK, I, Out, Id>,
64 ) -> Arc<Tuner<AK>> {
65 let checksum = operations.checksum();
66 if let Some(tuner) = self.by_id.get(id).and_then(|lists| lists.get(&checksum)) {
67 return tuner.clone();
68 }
69 let identity = TunerIdentity {
70 hardware: client.hardware_identity(),
71 checksum,
72 };
73 let tuner = self
74 .opened
75 .entry(identity)
76 .or_insert_with_key(|identity| {
77 Arc::new(Tuner::new(&name.replace("::", "-"), &identity.hardware))
78 })
79 .clone();
80 self.by_id
81 .entry(id.clone())
82 .or_default()
83 .insert(checksum, tuner.clone());
84 tuner
85 }
86}
87
88#[macro_export]
90macro_rules! local_tuner {
91 ($name:expr) => {
92 LocalTuner::new(concat!(module_path!(), "-", $name))
93 };
94 () => {
95 LocalTuner::new(module_path!())
96 };
97}
98
99pub use local_tuner;
100
101impl<AK, ID> LocalTuner<AK, ID>
102where
103 AK: AutotuneKey + 'static,
104 ID: Hash + PartialEq + Eq + Clone,
105{
106 pub const fn new(name: &'static str) -> Self {
108 Self {
109 state: Mutex::new(None),
110 name,
111 sets: RwLock::new(None),
112 }
113 }
114
115 pub fn init<I, Out, Id, F>(&self, id: &ID, init_set: F) -> Arc<TunableSet<AK, I, Out, Id>>
130 where
131 F: Fn() -> TunableSet<AK, I, Out, Id> + 'static + Send + Sync,
132 Id: Send + Sync + 'static,
133 I: TuneInputs,
134 Out: AutotuneOutput,
135 {
136 let key = (TypeId::of::<F>(), id.clone());
137 let sets = self.sets.read();
138
139 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.";
140
141 if let Some(sets) = sets.as_ref()
142 && let Some(set) = sets.get(&key)
143 {
144 return set.clone().downcast().expect(DOWNCAST_ERROR);
145 };
146
147 core::mem::drop(sets);
148
149 let mut sets = self.sets.write();
150
151 if let Some(sets) = sets.as_ref()
152 && let Some(set) = sets.get(&key)
153 {
154 return set.clone().downcast().expect(DOWNCAST_ERROR);
155 };
156
157 let content = Arc::new(init_set());
158
159 if let Some(sets) = sets.as_mut() {
160 sets.insert(key, content.clone());
161 } else {
162 let mut map = HashMap::<(TypeId, ID), Arc<dyn Any + Send + Sync>>::new();
163 map.insert(key, content.clone());
164 *sets = Some(map);
165 };
166
167 content
168 }
169
170 pub fn fastest_identity<I, Out, Id>(
183 &self,
184 id: &ID,
185 operations: &TunableSet<AK, I, Out, Id>,
186 key: &AK,
187 ) -> Option<Id>
188 where
189 I: TuneInputs,
190 Id: Clone,
191 {
192 let tuner = self
193 .state
194 .lock()
195 .as_ref()?
196 .by_id
197 .get(id)?
198 .get(&operations.checksum())?
199 .clone();
200 let fastest_index = tuner.settled(key)?;
201 Some(operations.identity(fastest_index).clone())
202 }
203
204 pub fn clear(&self) {
206 self.state.lock().take();
207 }
208
209 #[cfg(feature = "autotune-checks")]
210 fn checks<'a, I: TuneInputs, Out: AutotuneOutput, Id>(
211 &self,
212 operations: &TunableSet<AK, I, Out, Id>,
213 inputs: &<I as TuneInputs>::At<'a>,
214 ) -> alloc::vec::Vec<crate::tune::log::CheckResult>
215 where
216 <I as TuneInputs>::At<'a>: Clone + Send,
217 {
218 let mut checks_outputs = Vec::new();
219 for i in 0..operations.len() {
220 let op = operations.fastest(i);
221 let result = op.execute(inputs.clone());
222 checks_outputs.push((op.name.to_string(), result));
223 }
224 super::check_autotune_outputs(checks_outputs)
225 }
226
227 pub fn execute<'a, I: TuneInputs, Out, Id>(
230 &self,
231 id: &ID,
232 client: &Client,
233 operations: Arc<TunableSet<AK, I, Out, Id>>,
234 inputs: <I as TuneInputs>::At<'a>,
235 ) -> Out
236 where
237 <I as TuneInputs>::At<'a>: Clone + Send,
238 Out: AutotuneOutput,
239 Id: Send + Sync,
240 {
241 let key = operations.generate_key(&inputs);
242
243 let tuner = self
244 .state
245 .lock()
246 .get_or_insert_with(Tuners::new)
247 .get_or_open(id, self.name, client, &operations);
248
249 if let TuneCacheResult::Hit { fastest_index } = tuner.fastest(&key) {
253 #[cfg(feature = "autotune-checks")]
256 {
257 let mut log_context =
258 crate::tune::AutotuneLogContext::new(&mut tuner.logger().lock());
259 log_context.set_checks(|| self.checks::<I, Out, Id>(&operations, &inputs));
260 }
261 return execute_settled(&operations, &key, fastest_index, inputs);
262 }
263
264 #[allow(unused_mut)]
266 let mut log_context = crate::tune::AutotuneLogContext::new(&mut tuner.logger().lock());
267
268 #[cfg(feature = "autotune-checks")]
269 log_context.set_checks(|| self.checks::<I, Out, Id>(&operations, &inputs));
270
271 let fastest =
272 tuner.check_tune::<I, Out, Id>(&key, &inputs, &operations, client, log_context);
273
274 match fastest {
276 TuneCacheResult::Hit { fastest_index } => {
277 execute_settled(&operations, &key, fastest_index, inputs)
278 }
279 TuneCacheResult::Miss => {
280 panic!(
281 "Somehow we STILL didn't check a tuning checksum or start tuning, something has gone wrong."
282 )
283 }
284 TuneCacheResult::Pending | TuneCacheResult::Compiled => {
289 match execute_in_plan_order(&operations, &key, inputs) {
290 Ok(output) => output,
291 Err(failures) => panic!(
292 "All autotune operations failed, no viable operation found.\n{failures:?}"
293 ),
294 }
295 }
296 }
297 }
298}
299
300fn execute_settled<'a, AK: AutotuneKey, I: TuneInputs, Out: 'static, Id>(
316 operations: &TunableSet<AK, I, Out, Id>,
317 key: &AK,
318 fastest_index: usize,
319 inputs: <I as TuneInputs>::At<'a>,
320) -> Out
321where
322 <I as TuneInputs>::At<'a>: Clone,
323{
324 let refused = match operations.fastest(fastest_index).execute(inputs.clone()) {
325 Ok(output) => return output,
326 Err(refused) => refused,
327 };
328 match execute_in_plan_order(operations, key, inputs) {
329 Ok(output) => output,
330 Err(failures) => panic!(
331 "The operation autotune settled on refused this call ({refused:?}), and so did every \
332 other candidate.\n{failures:?}"
333 ),
334 }
335}
336
337fn execute_in_plan_order<'a, AK: AutotuneKey, I: TuneInputs, Out: 'static, Id>(
338 operations: &TunableSet<AK, I, Out, Id>,
339 key: &AK,
340 inputs: <I as TuneInputs>::At<'a>,
341) -> Result<Out, Vec<AutotuneError>>
342where
343 <I as TuneInputs>::At<'a>: Clone,
344{
345 let mut failures = Vec::new();
346 for (index, _) in operations.plan(key).order(operations.len()) {
347 match operations.fastest(index).execute(inputs.clone()) {
348 Ok(output) => return Ok(output),
349 Err(err) => failures.push(err),
350 }
351 }
352 Err(failures)
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use crate::tune::{Tunable, TuneGroup};
359 use alloc::string::String;
360 use core::fmt::Display;
361 use serde::{Deserialize, Serialize};
362
363 #[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Debug)]
364 struct FakeAutotuneKey;
365
366 impl Display for FakeAutotuneKey {
367 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
368 f.write_str("FakeAutotuneKey")
369 }
370 }
371
372 impl AutotuneKey for FakeAutotuneKey {}
373
374 fn set() -> TunableSet<FakeAutotuneKey, (), usize> {
375 TunableSet::new(|_: &()| FakeAutotuneKey, |_: &FakeAutotuneKey, _: &()| ())
376 }
377
378 fn answers(index: usize) -> impl Fn(()) -> Result<usize, String> {
379 move |_| Ok(index)
380 }
381
382 fn fails(_: ()) -> Result<usize, String> {
383 Err("does not launch here".into())
384 }
385
386 #[test]
387 fn the_pending_fallback_runs_the_plan_s_first_candidate_not_the_first_declared() {
388 let group = TuneGroup::<FakeAutotuneKey>::new("group", |_| 1);
389 let operations = set()
390 .with(Tunable::<FakeAutotuneKey, (), usize>::new(
391 "declared_first",
392 answers(0),
393 ))
394 .with(
395 Tunable::<FakeAutotuneKey, (), usize>::new("planned_first", answers(1))
396 .group(&group, |_| 1),
397 );
398
399 assert_eq!(
400 execute_in_plan_order(&operations, &FakeAutotuneKey, ()).ok(),
401 Some(1)
402 );
403 }
404
405 #[test]
406 fn the_pending_fallback_moves_down_the_plan_when_a_candidate_does_not_launch() {
407 let group = TuneGroup::<FakeAutotuneKey>::new("group", |_| 1);
408 let operations = set()
409 .with(
410 Tunable::<FakeAutotuneKey, (), usize>::new("planned_last", answers(0))
411 .group(&group, |_| 0),
412 )
413 .with(
414 Tunable::<FakeAutotuneKey, (), usize>::new("planned_first", fails)
415 .group(&group, |_| 2),
416 )
417 .with(
418 Tunable::<FakeAutotuneKey, (), usize>::new("planned_second", answers(2))
419 .group(&group, |_| 1),
420 );
421
422 assert_eq!(
423 execute_in_plan_order(&operations, &FakeAutotuneKey, ()).ok(),
424 Some(2)
425 );
426 }
427
428 #[test]
429 fn a_settled_winner_that_refuses_a_call_falls_back_down_the_plan() {
430 let group = TuneGroup::<FakeAutotuneKey>::new("group", |_| 1);
431 let operations = set()
432 .with(
433 Tunable::<FakeAutotuneKey, (), usize>::new("settled_winner", fails)
434 .group(&group, |_| 2),
435 )
436 .with(
437 Tunable::<FakeAutotuneKey, (), usize>::new("planned_second", answers(1))
438 .group(&group, |_| 1),
439 );
440
441 assert_eq!(execute_settled(&operations, &FakeAutotuneKey, 0, ()), 1);
442 }
443
444 #[test]
445 fn a_settled_winner_that_serves_the_call_runs() {
446 let operations = set()
447 .with(Tunable::<FakeAutotuneKey, (), usize>::new(
448 "first",
449 answers(0),
450 ))
451 .with(Tunable::<FakeAutotuneKey, (), usize>::new(
452 "settled_winner",
453 answers(1),
454 ));
455
456 assert_eq!(execute_settled(&operations, &FakeAutotuneKey, 1, ()), 1);
457 }
458
459 #[test]
460 fn the_pending_fallback_tries_what_the_plan_left_out_last() {
461 let never = TuneGroup::<FakeAutotuneKey>::new("never", |_| -1);
462 let operations = set()
463 .with(Tunable::<FakeAutotuneKey, (), usize>::new(
464 "declared_first",
465 fails,
466 ))
467 .with(
468 Tunable::<FakeAutotuneKey, (), usize>::new("left_out", answers(1))
469 .group(&never, |_| 1),
470 );
471
472 assert_eq!(
473 execute_in_plan_order(&operations, &FakeAutotuneKey, ()).ok(),
474 Some(1)
475 );
476 let nothing = set().with(Tunable::<FakeAutotuneKey, (), usize>::new("fails", fails));
477 assert!(execute_in_plan_order(¬hing, &FakeAutotuneKey, ()).is_err());
478 }
479}