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 in the `where`-clause rather
20    /// than hidden behind a helper trait, so that closure inference sees it: otherwise
21    /// `move |input| …` picks one concrete lifetime and fails with `implementation of
22    /// FnOnce is not general enough` wherever `F::At<'a>` depends on `'a`.
23    ///
24    /// For multi-input kernels, destructure a tuple:
25    /// `Tunable::new("name", |(lhs, rhs, out)| body)`.
26    ///
27    /// A tunable in no [group](Tunable::group) states no priority, so it trails the
28    /// grouped candidates of its round and a short circuit among them leaves it
29    /// unmeasured.
30    pub fn new<Func, Err>(name: &str, func: Func) -> Self
31    where
32        Err: Into<String> + 'static,
33        Func: for<'a> Fn(<F as TuneInputs>::At<'a>) -> Result<Output, Err> + Send + Sync + 'static,
34    {
35        let name: String = name.into();
36        let name_for_err = name.clone();
37        Self {
38            function: TuneFn::new(
39                name,
40                Box::new(move |inputs| {
41                    func(inputs).map_err(|err| AutotuneError::Unknown {
42                        name: name_for_err.to_string(),
43                        err: err.into(),
44                    })
45                }),
46            ),
47            groups: Vec::new(),
48        }
49    }
50
51    /// Add this tunable to a [`TuneGroup`] with the given intra-group priority.
52    ///
53    /// Groups run in order of their own priority. Within a cutoff group the member
54    /// priority picks one level as the round and leaves the levels under it as fallbacks.
55    /// Within an [ordered](TuneGroup::ordered) group it orders one round that holds every
56    /// member. A negative priority skips the tunable for this key.
57    pub fn group(
58        mut self,
59        group: &TuneGroup<K>,
60        priority: impl Fn(&K) -> i8 + Send + Sync + 'static,
61    ) -> Self {
62        self.groups.push((group.clone(), Arc::new(priority)));
63        self
64    }
65}
66
67/// A priority bucket for tunables, computed from the [autotune key](AutotuneKey).
68///
69/// Higher-priority groups are autotuned first; once any tunable in a group returns a
70/// valid result, no later groups are tried.
71pub struct TuneGroup<K> {
72    id: u32,
73    name: Arc<String>,
74    pub(crate) priority: PriorityFunc<K>,
75    /// Whether a member's priority orders the round rather than cutting it off.
76    ordered: bool,
77}
78
79impl<K> core::fmt::Debug for TuneGroup<K> {
80    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
81        f.debug_struct("TuneGroup")
82            .field("id", &self.id)
83            .field("name", &self.name)
84            .finish()
85    }
86}
87
88impl<K> Clone for TuneGroup<K> {
89    fn clone(&self) -> Self {
90        Self {
91            id: self.id,
92            name: self.name.clone(),
93            priority: self.priority.clone(),
94            ordered: self.ordered,
95        }
96    }
97}
98
99impl<K> TuneGroup<K> {
100    /// Create a new group based on a priority function.
101    ///
102    /// A member's own priority ([`Tunable::group`]) is a cutoff: the highest level is the
103    /// round, and the levels under it are fallbacks reached as each round fails.
104    pub fn new(name: &str, f: impl Fn(&K) -> i8 + Send + Sync + 'static) -> Self {
105        Self::build(name, f, false)
106    }
107
108    /// Create a group whose member priorities order one round rather than cut it off:
109    /// every member with a non-negative priority is in the batch, best first.
110    ///
111    /// The batch is benchmarked in that order and the short circuit ends it at the first
112    /// candidate under the [bounds](super::Bounds)' time limit, so the priority decides
113    /// how much of the group is compiled rather than which of it can win. The batch costs
114    /// its whole membership without a bounds generator, on wasm, or with the short circuit
115    /// disabled.
116    ///
117    /// The batch sits at the priority of its best member, so a cutoff group at the same
118    /// group priority interleaves with it by priority.
119    pub fn ordered(name: &str, f: impl Fn(&K) -> i8 + Send + Sync + 'static) -> Self {
120        Self::build(name, f, true)
121    }
122
123    fn build(name: &str, f: impl Fn(&K) -> i8 + Send + Sync + 'static, ordered: bool) -> Self {
124        let id = GROUP_COUNTER.fetch_add(1, Ordering::Relaxed);
125
126        Self {
127            id,
128            name: Arc::new(name.into()),
129            priority: Arc::new(f),
130            ordered,
131        }
132    }
133}
134
135#[derive(Debug)]
136/// A group plan dictates which [tunables](Tunable) should be executed, and in what order.
137pub(crate) struct TunePlan {
138    priorities: Vec<i8>,
139    no_groups: Vec<usize>,
140    groups: HashMap<i8, GroupPlan>,
141    returned: Vec<usize>,
142}
143
144#[derive(Default, Debug)]
145struct GroupPlan {
146    priorities: Vec<i8>,
147    indices: HashMap<i8, Vec<Planned>>,
148}
149
150/// One tunable's place in a [`GroupPlan`]: which tunable, through which group, and the
151/// priority that orders it within its batch.
152///
153/// The group is its id, not its name: a name is the caller's label and two groups may
154/// share one, which the cross-level dedup in [`TunePlan::group_plan_next`] would then
155/// read as a single group and strike a live candidate out of the plan.
156#[derive(Debug)]
157struct Planned {
158    index: usize,
159    group: u32,
160    priority: i8,
161}
162
163#[derive(Debug)]
164struct Cleanup {
165    groups: Vec<i8>,
166    tunables: Vec<(i8, i8)>,
167    /// Within group priority is too low to even try.
168    skipped: bool,
169}
170
171impl TunePlan {
172    pub fn new<K: AutotuneKey, F: TuneInputs, Out>(
173        key: &K,
174        tunables: &[Tunable<K, F, Out>],
175    ) -> Self {
176        let mut priorities = Vec::<i8>::new();
177        let mut no_groups = Vec::new();
178        let mut groups = HashMap::<i8, GroupPlan>::new();
179
180        // Each of the caller's priority functions is asked once and its answer carried:
181        // an ordered group's level is its best member's priority, which is known only
182        // once every member is priced.
183        let mut priced = Vec::new();
184        let mut ordered_levels = HashMap::<u32, i8>::new();
185
186        for (index, tunable) in tunables.iter().enumerate() {
187            if tunable.groups.is_empty() {
188                no_groups.push(index);
189                continue;
190            }
191
192            for (group, within_group_priority_fn) in tunable.groups.iter() {
193                let group_priority = (group.priority)(key);
194                let priority = within_group_priority_fn(key);
195
196                if group.ordered && priority >= 0 {
197                    let level = ordered_levels.entry(group.id).or_insert(priority);
198                    *level = (*level).max(priority);
199                }
200
201                priced.push((index, group, group_priority, priority));
202            }
203        }
204
205        for (index, group, group_priority, priority) in priced {
206            if !priorities.contains(&group_priority) {
207                priorities.push(group_priority);
208            }
209
210            let group_plan = match groups.get_mut(&group_priority) {
211                Some(val) => val,
212                None => {
213                    groups.insert(group_priority, GroupPlan::default());
214                    groups.get_mut(&group_priority).unwrap()
215                }
216            };
217
218            // An ordered group is one batch at its best member's level, so a cutoff
219            // group at the same group priority interleaves with it by priority.
220            let level = match group.ordered && priority >= 0 {
221                true => ordered_levels[&group.id],
222                false => priority,
223            };
224            let planned = Planned {
225                index,
226                group: group.id,
227                priority,
228            };
229
230            if group_plan.priorities.contains(&level) {
231                group_plan.indices.get_mut(&level).unwrap().push(planned);
232            } else {
233                group_plan.priorities.push(level);
234                group_plan.indices.insert(level, vec![planned]);
235            }
236        }
237
238        priorities.sort();
239
240        for group in groups.iter_mut() {
241            group.1.priorities.sort();
242        }
243
244        Self {
245            priorities,
246            no_groups,
247            groups,
248            returned: Vec::new(),
249        }
250    }
251
252    /// Get the next batch of [tunable](Tunable) index to be autotuned.
253    ///
254    /// Note that if the list is empty, it means no more autotuned entry can be executed.
255    pub(crate) fn next(&mut self) -> Vec<usize> {
256        // A tunable in no group states no priority, so it trails the batch: the grouped
257        // candidates decide what is compiled and benchmarked first.
258        let ungrouped = core::mem::take(&mut self.no_groups);
259        let mut indices = Vec::new();
260        let priority = self.priorities.last();
261
262        let priority = match priority {
263            Some(val) => *val,
264            None => return ungrouped,
265        };
266
267        let (group_indices, cleanup) = self.group_plan_next(priority);
268        // Some entries are skipped for this round of prioritizing.
269        let skipped = cleanup.skipped || priority < 0;
270        let mut all_skip = true;
271
272        self.cleanup(cleanup);
273
274        if priority >= 0 {
275            for index in group_indices {
276                if !self.returned.contains(&index) && !indices.contains(&index) {
277                    all_skip = false;
278                    indices.push(index);
279                }
280            }
281        }
282
283        indices.extend(ungrouped);
284
285        // The indices list is empty, but it doesn't mean we should stop
286        // autotuning, since some entries were skipped.
287
288        if indices.is_empty() && (skipped || all_skip) {
289            self.next()
290        } else {
291            for i in indices.iter() {
292                self.returned.push(*i);
293            }
294            indices
295        }
296    }
297
298    fn cleanup(&mut self, cleanup: Cleanup) {
299        for group_p in cleanup.groups {
300            let index = self
301                .priorities
302                .iter()
303                .enumerate()
304                .find(|p| *p.1 == group_p)
305                .unwrap();
306
307            self.priorities.remove(index.0);
308            self.groups.remove(&group_p);
309        }
310
311        for (group_p, tunable_p) in cleanup.tunables {
312            if let Some(group) = self.groups.get_mut(&group_p) {
313                let index = group
314                    .priorities
315                    .iter()
316                    .enumerate()
317                    .find(|p| *p.1 == tunable_p)
318                    .unwrap();
319                group.priorities.remove(index.0);
320                group.indices.remove(&tunable_p);
321            }
322        }
323    }
324
325    fn group_plan_next(&mut self, priority: i8) -> (Vec<usize>, Cleanup) {
326        let group_plan = self.groups.get_mut(&priority).expect("To be filled");
327        let within_group_prio = group_plan.priorities.pop().unwrap();
328        let mut next_indices = group_plan.indices.remove(&within_group_prio).unwrap();
329        // Highest priority first, registration order among equals.
330        next_indices.sort_by_key(|planned| (core::cmp::Reverse(planned.priority), planned.index));
331
332        let mut cleanup_groups = Vec::new();
333        let mut cleanup_tunables = Vec::new();
334
335        for (pg, group) in self.groups.iter_mut() {
336            let mut num_empty_tunables = 0;
337            let num_tunables = group.priorities.len();
338
339            for (pt, indices) in group.indices.iter_mut() {
340                for n in &next_indices {
341                    let entry = indices
342                        .iter()
343                        .position(|p| p.index == n.index && p.group == n.group);
344                    if let Some(entry) = entry {
345                        indices.remove(entry);
346                    }
347                }
348
349                if indices.is_empty() {
350                    num_empty_tunables += 1;
351                    cleanup_tunables.push((*pg, *pt));
352                }
353            }
354
355            if num_empty_tunables == num_tunables {
356                cleanup_groups.push(*pg);
357            }
358        }
359
360        if within_group_prio < 0 {
361            // Discard algorithms with negative priority
362            next_indices.clear();
363        }
364
365        (
366            next_indices
367                .into_iter()
368                .map(|planned| planned.index)
369                .collect(),
370            Cleanup {
371                groups: cleanup_groups,
372                tunables: cleanup_tunables,
373                skipped: within_group_prio < 0,
374            },
375        )
376    }
377}
378
379type PriorityFunc<K> = Arc<dyn Fn(&K) -> i8 + Send + Sync>;
380
381static GROUP_COUNTER: AtomicU32 = AtomicU32::new(0);
382
383#[cfg(test)]
384mod tests {
385    use core::fmt::Display;
386
387    use serde::{Deserialize, Serialize};
388
389    use super::*;
390
391    #[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Debug)]
392    struct FakeAutotuneKey;
393
394    impl Display for FakeAutotuneKey {
395        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
396            f.write_str("FakeAutotuneKey")
397        }
398    }
399
400    impl AutotuneKey for FakeAutotuneKey {}
401
402    #[test_log::test]
403    fn test_plan_order() {
404        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
405        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
406
407        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
408        let tunable1 =
409            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
410        let tunable2 =
411            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
412        let tunable3 =
413            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
414
415        let key = FakeAutotuneKey;
416        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
417
418        // tunable0 is in no group, so it trails the batch rather than leading it.
419        assert_eq!(plan.next(), vec![2, 0]);
420        assert_eq!(plan.next(), vec![1]);
421        assert_eq!(plan.next(), vec![3]);
422        assert!(plan.next().is_empty());
423    }
424
425    #[test_log::test]
426    fn test_plan_order_multi_groups_same_priority() {
427        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
428        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
429        let group2 = TuneGroup::<FakeAutotuneKey>::new("group2", |_| 1);
430
431        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
432        let tunable1 =
433            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
434        let tunable2 =
435            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
436        let tunable3 =
437            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
438        let tunable4 =
439            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group2, |_| 2);
440
441        let key = FakeAutotuneKey;
442        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3, tunable4]);
443
444        assert_eq!(plan.next(), vec![2, 0]);
445        assert_eq!(plan.next(), vec![1]);
446        assert_eq!(plan.next(), vec![3, 4]);
447        assert!(plan.next().is_empty());
448    }
449
450    #[test_log::test]
451    fn test_plan_order_tunable_multiple_groups() {
452        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 1);
453        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 2);
454
455        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
456        let tunable1 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
457            .group(&group0, |_| 1)
458            .group(&group1, |_| 2);
459        let tunable2 =
460            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
461        let tunable3 =
462            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 3);
463
464        let key = FakeAutotuneKey;
465        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
466
467        assert_eq!(plan.next(), vec![3, 0]);
468        assert_eq!(plan.next(), vec![1]);
469        assert_eq!(plan.next(), vec![2]);
470        assert!(plan.next().is_empty());
471    }
472
473    #[test_log::test]
474    fn test_plan_negative_priority() {
475        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
476        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
477
478        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
479        let tunable1 =
480            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| -1);
481        let tunable2 =
482            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
483        let tunable3 =
484            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
485
486        let key = FakeAutotuneKey;
487        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
488
489        assert_eq!(plan.next(), vec![2, 0]);
490        assert_eq!(plan.next(), vec![3]);
491        assert!(plan.next().is_empty());
492    }
493
494    #[test_log::test]
495    fn test_plan_no_group() {
496        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
497        let tunable1 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
498
499        let key = FakeAutotuneKey;
500        let mut plan = TunePlan::new(&key, &[tunable0, tunable1]);
501
502        assert_eq!(plan.next(), vec![0, 1]);
503        assert!(plan.next().is_empty());
504    }
505
506    #[test_log::test]
507    fn test_plan_falls_through_when_all_group_tunables_fail() {
508        // Every tunable lives in exactly one group; the caller treats every batch as a failure
509        // by continuing to call next(). The plan must still surface every tunable, in priority
510        // order, before going empty.
511        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
512        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
513
514        let tunable0 =
515            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
516        let tunable1 =
517            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
518        let tunable2 =
519            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 1);
520        let tunable3 =
521            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
522
523        let key = FakeAutotuneKey;
524        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
525
526        let mut all_returned: Vec<usize> = Vec::new();
527        loop {
528            let batch = plan.next();
529            if batch.is_empty() {
530                break;
531            }
532            all_returned.extend(batch);
533        }
534
535        // Highest group (prio 2) drains first from highest intra-priority down, then next group.
536        assert_eq!(all_returned, vec![1, 0, 3, 2]);
537    }
538
539    #[test_log::test]
540    fn test_plan_single_group_exhausts_all_intra_priorities() {
541        // A single group with multiple intra-priorities should yield each batch separately,
542        // allowing the caller to continue on failures until the group is exhausted.
543        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 0);
544
545        let tunable0 =
546            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
547        let tunable1 =
548            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
549        let tunable2 =
550            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 3);
551
552        let key = FakeAutotuneKey;
553        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
554
555        assert_eq!(plan.next(), vec![2]);
556        assert_eq!(plan.next(), vec![1]);
557        assert_eq!(plan.next(), vec![0]);
558        assert!(plan.next().is_empty());
559    }
560
561    #[test_log::test]
562    fn test_plan_all_negative_group_advances_to_next_group() {
563        // A group whose every tunable has a negative intra-priority should be skipped entirely
564        // without stopping autotuning — the next group must still be reached.
565        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
566        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
567
568        let tunable0 =
569            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| -1);
570        let tunable1 =
571            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| -2);
572        let tunable2 =
573            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 1);
574
575        let key = FakeAutotuneKey;
576        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
577
578        assert_eq!(plan.next(), vec![2]);
579        assert!(plan.next().is_empty());
580    }
581
582    #[test_log::test]
583    fn test_plan_no_group_tunables_only_emitted_once_even_on_failures() {
584        // The ungrouped tunables are emitted together with the first group batch. If the caller
585        // keeps calling next() (treating the first batch as failing), they must not be
586        // re-emitted, and the plan must still advance to later groups.
587        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
588        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
589
590        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
591        let tunable1 =
592            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
593        let tunable2 =
594            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 1);
595
596        let key = FakeAutotuneKey;
597        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
598
599        assert_eq!(plan.next(), vec![1, 0]);
600        assert_eq!(plan.next(), vec![2]);
601        assert!(plan.next().is_empty());
602    }
603
604    #[test_log::test]
605    fn test_plan_multi_group_tunable_not_duplicated_across_failed_groups() {
606        // tunable1 belongs to both group0 and group1. It must be returned exactly once (via its
607        // higher-priority group), even if the caller continues iterating after failures.
608        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 1);
609        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 2);
610
611        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
612            .group(&group0, |_| 1)
613            .group(&group1, |_| 1);
614        let tunable1 =
615            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
616
617        let key = FakeAutotuneKey;
618        let mut plan = TunePlan::new(&key, &[tunable0, tunable1]);
619
620        let mut all_returned: Vec<usize> = Vec::new();
621        loop {
622            let batch = plan.next();
623            if batch.is_empty() {
624                break;
625            }
626            all_returned.extend(batch);
627        }
628
629        // tunable0 comes from group1 (higher priority). tunable1 is the sole member of group0
630        // after cross-group dedup. No duplicates.
631        assert_eq!(all_returned, vec![0, 1]);
632    }
633
634    #[test_log::test]
635    fn test_plan_recurses_when_batch_is_fully_already_returned() {
636        // Regression test: a tunable that lives in multiple groups was already emitted via its
637        // higher-priority group, so when its lower-priority group's batch fires the only index
638        // is one already present in `returned`. The plan must NOT return an empty batch here
639        // (that signals "no more work" to the caller and aborts with NoValidKernelFound); it
640        // must recurse to the next intra-priority and surface the remaining tunable.
641        //
642        // Cross-group dedup in group_plan_next compares (index, Arc<String> group_name), so a
643        // tunable appearing in both group_hi and group_lo isn't auto-removed from group_lo
644        // when popped from group_hi — the `returned` + `all_skip` path is the only guard.
645        let group_hi = TuneGroup::<FakeAutotuneKey>::new("hi", |_| 2);
646        let group_lo = TuneGroup::<FakeAutotuneKey>::new("lo", |_| 1);
647
648        // tunable0 is in both groups. tunable1 is only in group_lo at a lower intra-priority.
649        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
650            .group(&group_hi, |_| 1)
651            .group(&group_lo, |_| 2);
652        let tunable1 =
653            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group_lo, |_| 1);
654
655        let key = FakeAutotuneKey;
656        let mut plan = TunePlan::new(&key, &[tunable0, tunable1]);
657
658        // First call: group_hi yields tunable0.
659        assert_eq!(plan.next(), vec![0]);
660        // Second call: group_lo's higher intra-priority batch is just tunable0 (already
661        // returned). Without the fix this returns [] and the autotuner aborts. With the fix
662        // the plan recurses and yields tunable1.
663        assert_eq!(plan.next(), vec![1]);
664        assert!(plan.next().is_empty());
665    }
666
667    #[test_log::test]
668    fn test_plan_ordered_group_is_one_batch_best_first() {
669        // Every member is in the round, best first, and a negative priority still skips.
670        let group = TuneGroup::<FakeAutotuneKey>::ordered("ordered", |_| 1);
671
672        let tunable0 =
673            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 1);
674        let tunable1 =
675            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 3);
676        let tunable2 =
677            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| -1);
678        let tunable3 =
679            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 3);
680        let tunable4 =
681            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 2);
682
683        let key = FakeAutotuneKey;
684        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3, tunable4]);
685
686        assert_eq!(plan.next(), vec![1, 3, 4, 0]);
687        assert!(plan.next().is_empty());
688    }
689
690    #[test_log::test]
691    fn test_plan_ordered_group_keeps_the_group_cutoff() {
692        // The order is within the group. A lower-priority group is still a fallback,
693        // reached only once the ordered batch fails.
694        let first = TuneGroup::<FakeAutotuneKey>::ordered("first", |_| 2);
695        let fallback = TuneGroup::<FakeAutotuneKey>::new("fallback", |_| 1);
696
697        let tunable0 =
698            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&first, |_| 1);
699        let tunable1 =
700            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&first, |_| 2);
701        let tunable2 =
702            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&fallback, |_| 1);
703
704        let key = FakeAutotuneKey;
705        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
706
707        assert_eq!(plan.next(), vec![1, 0]);
708        assert_eq!(plan.next(), vec![2]);
709        assert!(plan.next().is_empty());
710    }
711
712    #[test_log::test]
713    fn test_plan_ordered_batch_leads_the_ungrouped_tunables() {
714        // A tunable in no group must not preempt the ordered group's best candidate. The
715        // batch stops at the first candidate under the bound, so whatever leads it is
716        // what gets compiled.
717        let group = TuneGroup::<FakeAutotuneKey>::ordered("ordered", |_| 1);
718
719        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
720        let tunable1 =
721            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 1);
722        let tunable2 =
723            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 5);
724
725        let key = FakeAutotuneKey;
726        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
727
728        assert_eq!(plan.next(), vec![2, 1, 0]);
729        assert!(plan.next().is_empty());
730    }
731
732    #[test_log::test]
733    fn test_plan_ordered_batch_is_not_jumped_by_a_cutoff_group_beside_it() {
734        // Both groups sit at group priority 1. The ordered batch is planned at its best
735        // member's priority, so the cutoff member under it is a fallback behind the batch
736        // and cannot take a round of its own in front of it, where a short circuit would
737        // skip the ordered group whole.
738        let ordered = TuneGroup::<FakeAutotuneKey>::ordered("ordered", |_| 1);
739        let cutoff = TuneGroup::<FakeAutotuneKey>::new("cutoff", |_| 1);
740
741        let tunable0 =
742            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&ordered, |_| 3);
743        let tunable1 =
744            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&ordered, |_| 1);
745        let tunable2 =
746            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&cutoff, |_| 2);
747
748        let key = FakeAutotuneKey;
749        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
750
751        assert_eq!(plan.next(), vec![0, 1]);
752        assert_eq!(plan.next(), vec![2]);
753        assert!(plan.next().is_empty());
754    }
755
756    #[test_log::test]
757    fn test_plan_cutoff_member_above_the_ordered_batch_still_leads() {
758        // The interleaving cuts both ways. A cutoff member priced above the ordered
759        // group's best keeps its round in front, and one priced level with it joins the
760        // batch, ordered among the members by priority.
761        let ordered = TuneGroup::<FakeAutotuneKey>::ordered("ordered", |_| 1);
762        let cutoff = TuneGroup::<FakeAutotuneKey>::new("cutoff", |_| 1);
763
764        let tunable0 =
765            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&ordered, |_| 2);
766        let tunable1 =
767            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&ordered, |_| 1);
768        let tunable2 =
769            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&cutoff, |_| 3);
770        let tunable3 =
771            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&cutoff, |_| 2);
772
773        let key = FakeAutotuneKey;
774        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
775
776        assert_eq!(plan.next(), vec![2]);
777        assert_eq!(plan.next(), vec![0, 3, 1]);
778        assert!(plan.next().is_empty());
779    }
780
781    #[test_log::test]
782    fn test_plan_same_named_groups_do_not_strike_each_other_out() {
783        // Two groups may share a name, and the cross-level dedup must still tell them
784        // apart. Keyed on the name, popping `hi`'s discarded negative level takes
785        // tunable1 out of `lo`'s plan and the only viable candidate is never benchmarked.
786        let hi = TuneGroup::<FakeAutotuneKey>::new("shared", |_| 2);
787        let lo = TuneGroup::<FakeAutotuneKey>::new("shared", |_| 1);
788
789        let tunable0 =
790            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&hi, |_| 1);
791        let tunable1 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
792            .group(&hi, |_| -1)
793            .group(&lo, |_| 1);
794
795        let key = FakeAutotuneKey;
796        let mut plan = TunePlan::new(&key, &[tunable0, tunable1]);
797
798        assert_eq!(plan.next(), vec![0]);
799        assert_eq!(plan.next(), vec![1]);
800        assert!(plan.next().is_empty());
801    }
802
803    fn fake_kernel(_: ()) -> Result<(), String> {
804        Ok(())
805    }
806}