Skip to main content

cubecl_runtime/tune/
base.rs

1use super::{AutotuneError, AutotuneKey, TuneFn, TuneInputs};
2
3use alloc::boxed::Box;
4use alloc::string::ToString;
5use alloc::{string::String, sync::Arc, vec, vec::Vec};
6use core::sync::atomic::{AtomicU32, Ordering};
7use cubecl_environment::collections::HashMap;
8
9/// A single candidate for autotune: a named [`TuneFn`] plus the [groups](TuneGroup) it
10/// belongs to. A tunable is autotuned whenever any of its groups is prioritized.
11pub struct Tunable<K, F: TuneInputs, Output> {
12    pub(crate) function: TuneFn<F, Output>,
13    groups: Vec<(TuneGroup<K>, PriorityFunc<K>)>,
14}
15
16impl<K, F: TuneInputs, Output: 'static> Tunable<K, F, Output> {
17    /// Create a tunable from a closure.
18    ///
19    /// The `for<'a> Fn(F::At<'a>) -> _` bound is spelled out directly in the
20    /// `where`-clause (rather than hidden behind a helper trait) so that Rust closure
21    /// inference sees it: otherwise `move |input| …` picks a single concrete lifetime
22    /// and fails with `implementation of FnOnce is not general enough` whenever
23    /// `F::At<'a>` actually depends on `'a`.
24    ///
25    /// For multi-input kernels, destructure a tuple:
26    /// `Tunable::new("name", |(lhs, rhs, out)| body)`.
27    pub fn new<Func, Err>(name: &str, func: Func) -> Self
28    where
29        Err: Into<String> + 'static,
30        Func: for<'a> Fn(<F as TuneInputs>::At<'a>) -> Result<Output, Err> + Send + Sync + 'static,
31    {
32        let name: String = name.into();
33        let name_for_err = name.clone();
34        Self {
35            function: TuneFn::new(
36                name,
37                Box::new(move |inputs| {
38                    func(inputs).map_err(|err| AutotuneError::Unknown {
39                        name: name_for_err.to_string(),
40                        err: err.into(),
41                    })
42                }),
43            ),
44            groups: Vec::new(),
45        }
46    }
47
48    /// Add this tunable to a [`TuneGroup`] with the given intra-group priority.
49    ///
50    /// Groups are autotuned in order of their priority; within each group, tunables are
51    /// tried in order of `priority(key)`. A negative priority skips the tunable for this
52    /// key.
53    pub fn group(
54        mut self,
55        group: &TuneGroup<K>,
56        priority: impl Fn(&K) -> i8 + Send + Sync + 'static,
57    ) -> Self {
58        self.groups.push((group.clone(), Arc::new(priority)));
59        self
60    }
61}
62
63/// A priority bucket for tunables, computed from the [autotune key](AutotuneKey).
64///
65/// Higher-priority groups are autotuned first; once any tunable in a group returns a
66/// valid result, no later groups are tried.
67pub struct TuneGroup<K> {
68    id: u32,
69    name: Arc<String>,
70    pub(crate) priority: PriorityFunc<K>,
71}
72
73impl<K> core::fmt::Debug for TuneGroup<K> {
74    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
75        f.debug_struct("TuneGroup").field("id", &self.id).finish()
76    }
77}
78
79impl<K> Clone for TuneGroup<K> {
80    fn clone(&self) -> Self {
81        Self {
82            id: self.id,
83            name: self.name.clone(),
84            priority: self.priority.clone(),
85        }
86    }
87}
88
89impl<K> TuneGroup<K> {
90    /// Create a new group based on a priority function.
91    pub fn new(name: &str, f: impl Fn(&K) -> i8 + Send + Sync + 'static) -> Self {
92        let id = GROUP_COUNTER.fetch_add(1, Ordering::Relaxed);
93
94        Self {
95            id,
96            name: Arc::new(name.into()),
97            priority: Arc::new(f),
98        }
99    }
100}
101
102#[derive(Debug)]
103/// A group plan dictates which [tunables](Tunable) should be executed, and in what order.
104pub(crate) struct TunePlan {
105    priorities: Vec<i8>,
106    no_groups: Vec<usize>,
107    groups: HashMap<i8, GroupPlan>,
108    returned: Vec<usize>,
109}
110
111#[derive(Default, Debug)]
112struct GroupPlan {
113    priorities: Vec<i8>,
114    indices: HashMap<i8, Vec<(usize, Arc<String>)>>,
115}
116
117#[derive(Debug)]
118struct Cleanup {
119    groups: Vec<i8>,
120    tunables: Vec<(i8, i8)>,
121    /// Within group priority is too low to even try.
122    skipped: bool,
123}
124
125impl TunePlan {
126    pub fn new<K: AutotuneKey, F: TuneInputs, Out>(
127        key: &K,
128        tunables: &[Tunable<K, F, Out>],
129    ) -> Self {
130        let mut priorities = Vec::<i8>::new();
131        let mut no_groups = Vec::new();
132        let mut groups = HashMap::<i8, GroupPlan>::new();
133
134        for (index, tunable) in tunables.iter().enumerate() {
135            if tunable.groups.is_empty() {
136                no_groups.push(index);
137            } else {
138                for (group, within_group_priority_fn) in tunable.groups.iter() {
139                    let priority_fn = &group.priority;
140                    let priority = priority_fn(key);
141                    if !priorities.contains(&priority) {
142                        priorities.push(priority);
143                    }
144
145                    let group_priorities = match groups.get_mut(&priority) {
146                        Some(val) => val,
147                        None => {
148                            groups.insert(priority, GroupPlan::default());
149                            groups.get_mut(&priority).unwrap()
150                        }
151                    };
152                    let priority = within_group_priority_fn(key);
153
154                    if group_priorities.priorities.contains(&priority) {
155                        group_priorities
156                            .indices
157                            .get_mut(&priority)
158                            .unwrap()
159                            .push((index, group.name.clone()));
160                    } else {
161                        group_priorities.priorities.push(priority);
162                        group_priorities
163                            .indices
164                            .insert(priority, vec![(index, group.name.clone())]);
165                    }
166                }
167            }
168        }
169
170        priorities.sort();
171
172        for group in groups.iter_mut() {
173            group.1.priorities.sort();
174        }
175
176        Self {
177            priorities,
178            no_groups,
179            groups,
180            returned: Vec::new(),
181        }
182    }
183
184    /// Get the next batch of [tunable](Tunable) index to be autotuned.
185    ///
186    /// Note that if the list is empty, it means no more autotuned entry can be executed.
187    pub(crate) fn next(&mut self) -> Vec<usize> {
188        let mut indices = core::mem::take(&mut self.no_groups);
189        let priority = self.priorities.last();
190
191        let priority = match priority {
192            Some(val) => *val,
193            None => return indices,
194        };
195
196        let (group_indices, cleanup) = self.group_plan_next(priority);
197        // Some entries are skipped for this round of prioritizing.
198        let skipped = cleanup.skipped || priority < 0;
199        let mut all_skip = true;
200
201        self.cleanup(cleanup);
202
203        if priority >= 0 {
204            for (index, _name) in group_indices {
205                if !self.returned.contains(&index) && !indices.contains(&index) {
206                    all_skip = false;
207                    indices.push(index);
208                }
209            }
210        }
211
212        // The indices list is empty, but it doesn't mean we should stop
213        // autotuning, since some entries were skipped.
214
215        if indices.is_empty() && (skipped || all_skip) {
216            self.next()
217        } else {
218            for i in indices.iter() {
219                self.returned.push(*i);
220            }
221            indices
222        }
223    }
224
225    fn cleanup(&mut self, cleanup: Cleanup) {
226        for group_p in cleanup.groups {
227            let index = self
228                .priorities
229                .iter()
230                .enumerate()
231                .find(|p| *p.1 == group_p)
232                .unwrap();
233
234            self.priorities.remove(index.0);
235            self.groups.remove(&group_p);
236        }
237
238        for (group_p, tunable_p) in cleanup.tunables {
239            if let Some(group) = self.groups.get_mut(&group_p) {
240                let index = group
241                    .priorities
242                    .iter()
243                    .enumerate()
244                    .find(|p| *p.1 == tunable_p)
245                    .unwrap();
246                group.priorities.remove(index.0);
247                group.indices.remove(&tunable_p);
248            }
249        }
250    }
251
252    fn group_plan_next(&mut self, priority: i8) -> (Vec<(usize, Arc<String>)>, Cleanup) {
253        let group_plan = self.groups.get_mut(&priority).expect("To be filled");
254        let within_group_prio = group_plan.priorities.pop().unwrap();
255        let mut next_indices = group_plan.indices.remove(&within_group_prio).unwrap();
256
257        let mut cleanup_groups = Vec::new();
258        let mut cleanup_tunables = Vec::new();
259
260        for (pg, group) in self.groups.iter_mut() {
261            let mut num_empty_tunables = 0;
262            let num_tunables = group.priorities.len();
263
264            for (pt, indices) in group.indices.iter_mut() {
265                for n in &next_indices {
266                    let entry = indices.iter().enumerate().find(|p| *p.1 == *n);
267                    if let Some(entry) = entry {
268                        indices.remove(entry.0);
269                    }
270                }
271
272                if indices.is_empty() {
273                    num_empty_tunables += 1;
274                    cleanup_tunables.push((*pg, *pt));
275                }
276            }
277
278            if num_empty_tunables == num_tunables {
279                cleanup_groups.push(*pg);
280            }
281        }
282
283        if within_group_prio < 0 {
284            // Discard algorithms with negative priority
285            next_indices.clear();
286        }
287
288        (
289            next_indices,
290            Cleanup {
291                groups: cleanup_groups,
292                tunables: cleanup_tunables,
293                skipped: within_group_prio < 0,
294            },
295        )
296    }
297}
298
299type PriorityFunc<K> = Arc<dyn Fn(&K) -> i8 + Send + Sync>;
300
301static GROUP_COUNTER: AtomicU32 = AtomicU32::new(0);
302
303#[cfg(test)]
304mod tests {
305    use core::fmt::Display;
306
307    use serde::{Deserialize, Serialize};
308
309    use super::*;
310
311    #[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Debug)]
312    struct FakeAutotuneKey;
313
314    impl Display for FakeAutotuneKey {
315        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
316            f.write_str("FakeAutotuneKey")
317        }
318    }
319
320    impl AutotuneKey for FakeAutotuneKey {}
321
322    #[test_log::test]
323    fn test_plan_order() {
324        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
325        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
326
327        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
328        let tunable1 =
329            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
330        let tunable2 =
331            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
332        let tunable3 =
333            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
334
335        let key = FakeAutotuneKey;
336        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
337
338        assert_eq!(plan.next(), vec![0, 2]);
339        assert_eq!(plan.next(), vec![1]);
340        assert_eq!(plan.next(), vec![3]);
341        assert!(plan.next().is_empty());
342    }
343
344    #[test_log::test]
345    fn test_plan_order_multi_groups_same_priority() {
346        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
347        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
348        let group2 = TuneGroup::<FakeAutotuneKey>::new("group2", |_| 1);
349
350        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
351        let tunable1 =
352            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
353        let tunable2 =
354            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
355        let tunable3 =
356            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
357        let tunable4 =
358            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group2, |_| 2);
359
360        let key = FakeAutotuneKey;
361        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3, tunable4]);
362
363        assert_eq!(plan.next(), vec![0, 2]);
364        assert_eq!(plan.next(), vec![1]);
365        assert_eq!(plan.next(), vec![3, 4]);
366        assert!(plan.next().is_empty());
367    }
368
369    #[test_log::test]
370    fn test_plan_order_tunable_multiple_groups() {
371        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 1);
372        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 2);
373
374        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
375        let tunable1 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
376            .group(&group0, |_| 1)
377            .group(&group1, |_| 2);
378        let tunable2 =
379            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
380        let tunable3 =
381            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 3);
382
383        let key = FakeAutotuneKey;
384        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
385
386        assert_eq!(plan.next(), vec![0, 3]);
387        assert_eq!(plan.next(), vec![1]);
388        assert_eq!(plan.next(), vec![2]);
389        assert!(plan.next().is_empty());
390    }
391
392    #[test_log::test]
393    fn test_plan_negative_priority() {
394        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
395        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
396
397        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
398        let tunable1 =
399            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| -1);
400        let tunable2 =
401            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
402        let tunable3 =
403            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
404
405        let key = FakeAutotuneKey;
406        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
407
408        assert_eq!(plan.next(), vec![0, 2]);
409        assert_eq!(plan.next(), vec![3]);
410        assert!(plan.next().is_empty());
411    }
412
413    #[test_log::test]
414    fn test_plan_no_group() {
415        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
416        let tunable1 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
417
418        let key = FakeAutotuneKey;
419        let mut plan = TunePlan::new(&key, &[tunable0, tunable1]);
420
421        assert_eq!(plan.next(), vec![0, 1]);
422        assert!(plan.next().is_empty());
423    }
424
425    #[test_log::test]
426    fn test_plan_falls_through_when_all_group_tunables_fail() {
427        // Every tunable lives in exactly one group; the caller treats every batch as a failure
428        // by continuing to call next(). The plan must still surface every tunable, in priority
429        // order, before going empty.
430        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
431        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
432
433        let tunable0 =
434            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
435        let tunable1 =
436            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
437        let tunable2 =
438            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 1);
439        let tunable3 =
440            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
441
442        let key = FakeAutotuneKey;
443        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
444
445        let mut all_returned: Vec<usize> = Vec::new();
446        loop {
447            let batch = plan.next();
448            if batch.is_empty() {
449                break;
450            }
451            all_returned.extend(batch);
452        }
453
454        // Highest group (prio 2) drains first from highest intra-priority down, then next group.
455        assert_eq!(all_returned, vec![1, 0, 3, 2]);
456    }
457
458    #[test_log::test]
459    fn test_plan_single_group_exhausts_all_intra_priorities() {
460        // A single group with multiple intra-priorities should yield each batch separately,
461        // allowing the caller to continue on failures until the group is exhausted.
462        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 0);
463
464        let tunable0 =
465            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
466        let tunable1 =
467            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
468        let tunable2 =
469            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 3);
470
471        let key = FakeAutotuneKey;
472        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
473
474        assert_eq!(plan.next(), vec![2]);
475        assert_eq!(plan.next(), vec![1]);
476        assert_eq!(plan.next(), vec![0]);
477        assert!(plan.next().is_empty());
478    }
479
480    #[test_log::test]
481    fn test_plan_all_negative_group_advances_to_next_group() {
482        // A group whose every tunable has a negative intra-priority should be skipped entirely
483        // without stopping autotuning — the next group must still be reached.
484        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
485        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
486
487        let tunable0 =
488            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| -1);
489        let tunable1 =
490            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| -2);
491        let tunable2 =
492            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 1);
493
494        let key = FakeAutotuneKey;
495        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
496
497        assert_eq!(plan.next(), vec![2]);
498        assert!(plan.next().is_empty());
499    }
500
501    #[test_log::test]
502    fn test_plan_no_group_tunables_only_emitted_once_even_on_failures() {
503        // The ungrouped tunables are emitted together with the first group batch. If the caller
504        // keeps calling next() (treating the first batch as failing), they must not be
505        // re-emitted, and the plan must still advance to later groups.
506        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
507        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
508
509        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
510        let tunable1 =
511            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
512        let tunable2 =
513            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 1);
514
515        let key = FakeAutotuneKey;
516        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
517
518        assert_eq!(plan.next(), vec![0, 1]);
519        assert_eq!(plan.next(), vec![2]);
520        assert!(plan.next().is_empty());
521    }
522
523    #[test_log::test]
524    fn test_plan_multi_group_tunable_not_duplicated_across_failed_groups() {
525        // tunable1 belongs to both group0 and group1. It must be returned exactly once (via its
526        // higher-priority group), even if the caller continues iterating after failures.
527        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 1);
528        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 2);
529
530        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
531            .group(&group0, |_| 1)
532            .group(&group1, |_| 1);
533        let tunable1 =
534            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
535
536        let key = FakeAutotuneKey;
537        let mut plan = TunePlan::new(&key, &[tunable0, tunable1]);
538
539        let mut all_returned: Vec<usize> = Vec::new();
540        loop {
541            let batch = plan.next();
542            if batch.is_empty() {
543                break;
544            }
545            all_returned.extend(batch);
546        }
547
548        // tunable0 comes from group1 (higher priority). tunable1 is the sole member of group0
549        // after cross-group dedup. No duplicates.
550        assert_eq!(all_returned, vec![0, 1]);
551    }
552
553    #[test_log::test]
554    fn test_plan_recurses_when_batch_is_fully_already_returned() {
555        // Regression test: a tunable that lives in multiple groups was already emitted via its
556        // higher-priority group, so when its lower-priority group's batch fires the only index
557        // is one already present in `returned`. The plan must NOT return an empty batch here
558        // (that signals "no more work" to the caller and aborts with NoValidKernelFound); it
559        // must recurse to the next intra-priority and surface the remaining tunable.
560        //
561        // Cross-group dedup in group_plan_next compares (index, Arc<String> group_name), so a
562        // tunable appearing in both group_hi and group_lo isn't auto-removed from group_lo
563        // when popped from group_hi — the `returned` + `all_skip` path is the only guard.
564        let group_hi = TuneGroup::<FakeAutotuneKey>::new("hi", |_| 2);
565        let group_lo = TuneGroup::<FakeAutotuneKey>::new("lo", |_| 1);
566
567        // tunable0 is in both groups. tunable1 is only in group_lo at a lower intra-priority.
568        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
569            .group(&group_hi, |_| 1)
570            .group(&group_lo, |_| 2);
571        let tunable1 =
572            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group_lo, |_| 1);
573
574        let key = FakeAutotuneKey;
575        let mut plan = TunePlan::new(&key, &[tunable0, tunable1]);
576
577        // First call: group_hi yields tunable0.
578        assert_eq!(plan.next(), vec![0]);
579        // Second call: group_lo's higher intra-priority batch is just tunable0 (already
580        // returned). Without the fix this returns [] and the autotuner aborts. With the fix
581        // the plan recurses and yields tunable1.
582        assert_eq!(plan.next(), vec![1]);
583        assert!(plan.next().is_empty());
584    }
585
586    fn fake_kernel(_: ()) -> Result<(), String> {
587        Ok(())
588    }
589}