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::fmt::Display;
7use core::sync::atomic::{AtomicU32, Ordering};
8use cubecl_environment::collections::HashMap;
9
10/// A single candidate for autotune: a named [`TuneFn`] plus the [groups](TuneGroup) it
11/// belongs to. A tunable is autotuned whenever any of its groups is prioritized.
12///
13/// `Id` is what the tunables of a set are [identified](Self::identified) by, `()` for a set
14/// whose tunables are [named](Self::new) and nothing more.
15pub struct Tunable<K, F: TuneInputs, Output, Id = ()> {
16    pub(crate) function: TuneFn<F, Output>,
17    groups: Vec<(TuneGroup<K>, PriorityFunc<K>)>,
18    identity: Id,
19}
20
21impl<K, F: TuneInputs, Output: 'static> Tunable<K, F, Output> {
22    /// Create a tunable from a closure.
23    ///
24    /// The `for<'a> Fn(F::At<'a>) -> _` bound is spelled out in the `where`-clause rather
25    /// than hidden behind a helper trait, so that closure inference sees it: otherwise
26    /// `move |input| …` picks one concrete lifetime and fails with `implementation of
27    /// FnOnce is not general enough` wherever `F::At<'a>` depends on `'a`.
28    ///
29    /// For multi-input kernels, destructure a tuple:
30    /// `Tunable::new("name", |(lhs, rhs, out)| body)`.
31    ///
32    /// A tunable in no [group](Tunable::group) states no priority, so it trails the
33    /// grouped candidates of its round and a short circuit among them leaves it
34    /// unmeasured.
35    pub fn new<Func, Err>(name: &str, func: Func) -> Self
36    where
37        Err: Into<String> + 'static,
38        Func: for<'a> Fn(<F as TuneInputs>::At<'a>) -> Result<Output, Err> + Send + Sync + 'static,
39    {
40        Self::build(name, (), func)
41    }
42}
43
44impl<K, F: TuneInputs, Output: 'static, Id> Tunable<K, F, Output, Id> {
45    /// A tunable identified by `identity`, and named by it: the value
46    /// [`LocalTuner::fastest_identity`](super::LocalTuner::fastest_identity) hands back once
47    /// the tunable is the fastest for a key.
48    ///
49    /// For a caller that builds something else from the winner — a variant of the same kernel,
50    /// say — and so needs to know *which* candidate won rather than only to run it again. The
51    /// name a cached result is checksummed by is `identity`'s own, so the two cannot disagree.
52    pub fn identified<Func, Err>(identity: Id, func: Func) -> Self
53    where
54        Id: Display,
55        Err: Into<String> + 'static,
56        Func: for<'a> Fn(<F as TuneInputs>::At<'a>) -> Result<Output, Err> + Send + Sync + 'static,
57    {
58        Self::build(&identity.to_string(), identity, func)
59    }
60
61    fn build<Func, Err>(name: &str, identity: Id, func: Func) -> Self
62    where
63        Err: Into<String> + 'static,
64        Func: for<'a> Fn(<F as TuneInputs>::At<'a>) -> Result<Output, Err> + Send + Sync + 'static,
65    {
66        let name: String = name.into();
67        let name_for_err = name.clone();
68        Self {
69            function: TuneFn::new(
70                name,
71                Box::new(move |inputs| {
72                    func(inputs).map_err(|err| AutotuneError::Unknown {
73                        name: name_for_err.to_string(),
74                        err: err.into(),
75                    })
76                }),
77            ),
78            groups: Vec::new(),
79            identity,
80        }
81    }
82
83    /// What this tunable is [identified](Self::identified) by.
84    pub(crate) fn identity(&self) -> &Id {
85        &self.identity
86    }
87
88    /// Add this tunable to a [`TuneGroup`] with the given intra-group priority.
89    ///
90    /// Groups run in order of their own priority. Within a cutoff group the member
91    /// priority picks one level as the round and leaves the levels under it as fallbacks.
92    /// Within an [ordered](TuneGroup::ordered) group it orders one round that holds every
93    /// member. A negative priority skips the tunable for this key.
94    pub fn group(
95        mut self,
96        group: &TuneGroup<K>,
97        priority: impl Fn(&K) -> i8 + Send + Sync + 'static,
98    ) -> Self {
99        self.groups.push((group.clone(), Arc::new(priority)));
100        self
101    }
102}
103
104/// A priority bucket for tunables, computed from the [autotune key](AutotuneKey).
105///
106/// Higher-priority groups are autotuned first; once any tunable in a group returns a
107/// valid result, no later groups are tried.
108pub struct TuneGroup<K> {
109    id: GroupId,
110    name: Arc<String>,
111    pub(crate) priority: PriorityFunc<K>,
112    order: GroupOrder,
113}
114
115/// Identifies a [`TuneGroup`]: two groups may share a name.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
117pub(crate) struct GroupId(u32);
118
119impl GroupId {
120    #[cfg(test)]
121    pub(crate) fn new(id: u32) -> Self {
122        Self(id)
123    }
124}
125
126/// How a [`TuneGroup`]'s member priorities shape its batch.
127#[derive(Debug, Clone, Copy)]
128enum GroupOrder {
129    /// The highest level is the round, the levels under it are fallbacks.
130    Cutoff,
131    /// Every member is in one round, best first.
132    Ordered(OrderedSettings),
133}
134
135/// The settings of an [ordered](TuneGroup::ordered) group.
136#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
137pub struct OrderedSettings {
138    /// When to stop measuring the group's members. Without one, every member is measured.
139    pub patience: Option<Patience>,
140}
141
142impl OrderedSettings {
143    /// Stop measuring the group once its members stop beating its best one.
144    pub fn with_patience(mut self, patience: Patience) -> Self {
145        self.patience = Some(patience);
146        self
147    }
148}
149
150/// When to stop measuring an [ordered](TuneGroup::ordered) group: once at least
151/// [`min_measured`](Self::min_measured) of its members were measured and the last
152/// [`max_misses`](Self::max_misses) of them in a row missed, none faster than the group's
153/// leader by more than 2%. The leader is the first member measured, then each member that
154/// was faster than it by more than 2%.
155///
156/// The group is benchmarked best first, so a run of members that cannot beat its leader says
157/// the rest of the order is unlikely to either, and they are never compiled. The members
158/// already measured still compete as usual, the ones skipped are reported as such, and the
159/// rest of the batch is measured as if the group had ended there. A member is skipped only
160/// when every group that brought it into the batch ran out of patience. Should every member
161/// measured fail later on, the skipped ones are measured after all.
162///
163/// A member is judged on its best first-pass time, which follows a single warmup: a lucky one
164/// makes a leader the rest of the group struggles to beat. A member that fails is neither
165/// measured nor a miss: it says nothing about how the order is faring.
166///
167/// Adaptive scheduler only: the fixed-count pass, the only one on wasm, measures every member.
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub struct Patience {
170    /// Members measured before the group may stop. One member is always measured.
171    pub min_measured: usize,
172    /// Members in a row that miss before the group stops. Zero stops it at
173    /// [`min_measured`](Self::min_measured), whatever they measured.
174    pub max_misses: usize,
175}
176
177impl<K> core::fmt::Debug for TuneGroup<K> {
178    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
179        f.debug_struct("TuneGroup")
180            .field("id", &self.id)
181            .field("name", &self.name)
182            .finish()
183    }
184}
185
186impl<K> Clone for TuneGroup<K> {
187    fn clone(&self) -> Self {
188        Self {
189            id: self.id,
190            name: self.name.clone(),
191            priority: self.priority.clone(),
192            order: self.order,
193        }
194    }
195}
196
197impl<K> TuneGroup<K> {
198    /// Create a new group based on a priority function.
199    ///
200    /// A member's own priority ([`Tunable::group`]) is a cutoff: the highest level is the
201    /// round, and the levels under it are fallbacks reached as each round fails.
202    pub fn new(name: &str, f: impl Fn(&K) -> i8 + Send + Sync + 'static) -> Self {
203        Self::build(name, f, GroupOrder::Cutoff)
204    }
205
206    /// Create a group whose member priorities order one round rather than cut it off:
207    /// every member with a non-negative priority is in the batch, best first.
208    ///
209    /// The batch is benchmarked in that order and the short circuit ends it at the first
210    /// candidate under the [bounds](super::Bounds)' time limit, so the priority decides
211    /// how much of the group is compiled rather than which of it can win. The batch costs
212    /// its whole membership without a bounds generator, on wasm, or with the short circuit
213    /// disabled.
214    ///
215    /// The batch sits at the priority of its best member, so a cutoff group at the same
216    /// group priority interleaves with it by priority.
217    ///
218    /// [`OrderedSettings`] holds the group's settings, such as the [`Patience`] that stops
219    /// measuring it once its members stop beating its best one.
220    pub fn ordered(
221        name: &str,
222        f: impl Fn(&K) -> i8 + Send + Sync + 'static,
223        settings: OrderedSettings,
224    ) -> Self {
225        Self::build(name, f, GroupOrder::Ordered(settings))
226    }
227
228    fn build(name: &str, f: impl Fn(&K) -> i8 + Send + Sync + 'static, order: GroupOrder) -> Self {
229        let id = GroupId(GROUP_COUNTER.fetch_add(1, Ordering::Relaxed));
230
231        Self {
232            id,
233            name: Arc::new(name.into()),
234            priority: Arc::new(f),
235            order,
236        }
237    }
238
239    fn is_ordered(&self) -> bool {
240        matches!(self.order, GroupOrder::Ordered(_))
241    }
242
243    fn patience(&self) -> Option<Patience> {
244        match self.order {
245            GroupOrder::Ordered(settings) => settings.patience,
246            GroupOrder::Cutoff => None,
247        }
248    }
249}
250
251/// One batch of the [`TunePlan`]: the [tunables](Tunable) to benchmark, in order, and the
252/// [`Patience`] of each group that brought one in.
253#[derive(Debug, Default, PartialEq, Eq)]
254pub(crate) struct Batch {
255    pub(crate) entries: Vec<BatchEntry>,
256    pub(crate) patience: HashMap<GroupId, Patience>,
257}
258
259/// One [tunable](Tunable) of a [`Batch`], with every group that brought it into the batch.
260/// An ungrouped tunable has none.
261#[derive(Debug, Clone, PartialEq, Eq)]
262pub(crate) struct BatchEntry {
263    pub(crate) index: usize,
264    pub(crate) groups: Vec<GroupId>,
265}
266
267impl Batch {
268    /// A batch that measures every one of `indices`, no group's patience applying.
269    pub(crate) fn unconditional(indices: impl IntoIterator<Item = usize>) -> Self {
270        let mut batch = Self::default();
271        for index in indices {
272            batch.push_unconditional(index);
273        }
274        batch
275    }
276
277    pub(crate) fn is_empty(&self) -> bool {
278        self.entries.is_empty()
279    }
280
281    /// The tunable indices, in the order they are benchmarked.
282    pub(crate) fn indices(&self) -> Vec<usize> {
283        self.entries.iter().map(|entry| entry.index).collect()
284    }
285
286    /// Add a tunable through one of its groups. A tunable several groups bring in is
287    /// benchmarked once, at its first place, and answers to each of them.
288    fn push(&mut self, planned: &Planned) {
289        match self.entries.iter_mut().find(|e| e.index == planned.index) {
290            Some(entry) if entry.groups.contains(&planned.group) => {}
291            Some(entry) => entry.groups.push(planned.group),
292            None => self.entries.push(BatchEntry {
293                index: planned.index,
294                groups: vec![planned.group],
295            }),
296        }
297    }
298
299    fn push_unconditional(&mut self, index: usize) {
300        self.entries.push(BatchEntry {
301            index,
302            groups: Vec::new(),
303        });
304    }
305}
306
307#[derive(Debug, Clone)]
308/// A group plan dictates which [tunables](Tunable) should be executed, and in what order.
309pub(crate) struct TunePlan {
310    priorities: Vec<i8>,
311    no_groups: Vec<usize>,
312    groups: HashMap<i8, GroupPlan>,
313    returned: Vec<usize>,
314    /// The [`Patience`] of every group that has one.
315    patience: HashMap<GroupId, Patience>,
316    /// Each tunable's place in each of its groups, by tunable index, as the
317    /// key priced it.
318    #[cfg(persistence)]
319    placements: Vec<Vec<Placement>>,
320}
321
322/// A tunable's place in one of its [groups](TuneGroup), as one key priced it.
323#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
324pub struct Placement {
325    /// The group's name.
326    pub group: String,
327    /// Whether the group is [ordered](TuneGroup::ordered): its members'
328    /// priorities order one round rather than cutting it into rounds.
329    pub ordered: bool,
330    /// The group's own priority: higher groups run first.
331    pub group_priority: i8,
332    /// The tunable's priority within the group; negative skips it.
333    pub priority: i8,
334}
335
336#[derive(Default, Debug, Clone)]
337struct GroupPlan {
338    priorities: Vec<i8>,
339    indices: HashMap<i8, Vec<Planned>>,
340}
341
342/// One tunable's place in a [`GroupPlan`]: which tunable, through which group, and the
343/// priority that orders it within its batch.
344///
345/// The group is its id, not its name: a name is the caller's label and two groups may
346/// share one, which the cross-level dedup in [`TunePlan::group_plan_next`] would then
347/// read as a single group and strike a live candidate out of the plan.
348#[derive(Debug, Clone)]
349struct Planned {
350    index: usize,
351    group: GroupId,
352    priority: i8,
353}
354
355#[derive(Debug)]
356struct Cleanup {
357    groups: Vec<i8>,
358    tunables: Vec<(i8, i8)>,
359    /// Within group priority is too low to even try.
360    skipped: bool,
361}
362
363impl TunePlan {
364    pub fn new<K: AutotuneKey, F: TuneInputs, Out, Id>(
365        key: &K,
366        tunables: &[Tunable<K, F, Out, Id>],
367    ) -> Self {
368        let mut priorities = Vec::<i8>::new();
369        let mut no_groups = Vec::new();
370        let mut groups = HashMap::<i8, GroupPlan>::new();
371
372        // Each of the caller's priority functions is asked once and its answer carried:
373        // an ordered group's level is its best member's priority, which is known only
374        // once every member is priced.
375        let mut priced = Vec::new();
376        let mut ordered_levels = HashMap::<GroupId, i8>::new();
377        let mut patience = HashMap::<GroupId, Patience>::new();
378        #[cfg(persistence)]
379        let mut placements = vec![Vec::new(); tunables.len()];
380
381        for (index, tunable) in tunables.iter().enumerate() {
382            if tunable.groups.is_empty() {
383                no_groups.push(index);
384                continue;
385            }
386
387            for (group, within_group_priority_fn) in tunable.groups.iter() {
388                if let Some(group_patience) = group.patience() {
389                    patience.insert(group.id, group_patience);
390                }
391                let group_priority = (group.priority)(key);
392                let priority = within_group_priority_fn(key);
393                #[cfg(persistence)]
394                placements[index].push(Placement {
395                    group: group.name.to_string(),
396                    ordered: group.is_ordered(),
397                    group_priority,
398                    priority,
399                });
400
401                if group.is_ordered() && priority >= 0 {
402                    let level = ordered_levels.entry(group.id).or_insert(priority);
403                    *level = (*level).max(priority);
404                }
405
406                priced.push((index, group, group_priority, priority));
407            }
408        }
409
410        for (index, group, group_priority, priority) in priced {
411            if !priorities.contains(&group_priority) {
412                priorities.push(group_priority);
413            }
414
415            let group_plan = match groups.get_mut(&group_priority) {
416                Some(val) => val,
417                None => {
418                    groups.insert(group_priority, GroupPlan::default());
419                    groups.get_mut(&group_priority).unwrap()
420                }
421            };
422
423            // An ordered group is one batch at its best member's level, so a cutoff
424            // group at the same group priority interleaves with it by priority.
425            let level = match group.is_ordered() && priority >= 0 {
426                true => ordered_levels[&group.id],
427                false => priority,
428            };
429            let planned = Planned {
430                index,
431                group: group.id,
432                priority,
433            };
434
435            if group_plan.priorities.contains(&level) {
436                group_plan.indices.get_mut(&level).unwrap().push(planned);
437            } else {
438                group_plan.priorities.push(level);
439                group_plan.indices.insert(level, vec![planned]);
440            }
441        }
442
443        priorities.sort();
444
445        for group in groups.iter_mut() {
446            group.1.priorities.sort();
447        }
448
449        Self {
450            priorities,
451            no_groups,
452            groups,
453            returned: Vec::new(),
454            patience,
455            #[cfg(persistence)]
456            placements,
457        }
458    }
459
460    /// The whole plan as a recorded tune reports it: every tunable named by
461    /// `names`, in the order the plan releases them, with the batch each
462    /// comes in and its place in each of its groups. A tunable no batch
463    /// holds, its every priority negative, comes last.
464    #[cfg(persistence)]
465    pub(crate) fn account(&self, names: &[&str]) -> Vec<super::record::PlannedCandidate> {
466        let mut plan = self.clone();
467        let batches = core::iter::from_fn(|| {
468            Some(plan.next())
469                .filter(|batch| !batch.is_empty())
470                .map(|batch| batch.indices())
471        });
472        let mut account: Vec<_> = batches
473            .enumerate()
474            .flat_map(|(batch, indices)| indices.into_iter().map(move |index| (index, Some(batch))))
475            .collect();
476        let excluded =
477            (0..names.len()).filter(|index| !account.iter().any(|(seen, _)| seen == index));
478        account.extend(excluded.map(|index| (index, None)).collect::<Vec<_>>());
479        account
480            .into_iter()
481            .map(|(index, batch)| super::record::PlannedCandidate {
482                name: names[index].to_string(),
483                index,
484                batch,
485                groups: self.placements[index].clone(),
486            })
487            .collect()
488    }
489
490    /// The next batch to benchmark. An empty one means the plan is exhausted.
491    pub(crate) fn next(&mut self) -> Batch {
492        // A tunable in no group states no priority, so it trails the batch: the grouped
493        // candidates decide what is compiled and benchmarked first.
494        let ungrouped = core::mem::take(&mut self.no_groups);
495        let priority = self.priorities.last();
496
497        let priority = match priority {
498            Some(val) => *val,
499            None => return Batch::unconditional(ungrouped),
500        };
501        let mut batch = Batch {
502            entries: Vec::new(),
503            patience: self.patience.clone(),
504        };
505
506        let (group_indices, cleanup) = self.group_plan_next(priority);
507        // Some entries are skipped for this round of prioritizing.
508        let skipped = cleanup.skipped || priority < 0;
509        let mut all_skip = true;
510
511        self.cleanup(cleanup);
512
513        if priority >= 0 {
514            for planned in group_indices {
515                if self.returned.contains(&planned.index) {
516                    continue;
517                }
518                all_skip = false;
519                batch.push(&planned);
520            }
521        }
522
523        for index in ungrouped {
524            batch.push_unconditional(index);
525        }
526
527        // The batch is empty, but it doesn't mean we should stop
528        // autotuning, since some entries were skipped.
529
530        if batch.is_empty() && (skipped || all_skip) {
531            self.next()
532        } else {
533            for entry in batch.entries.iter() {
534                self.returned.push(entry.index);
535            }
536            batch
537        }
538    }
539
540    fn cleanup(&mut self, cleanup: Cleanup) {
541        for group_p in cleanup.groups {
542            let index = self
543                .priorities
544                .iter()
545                .enumerate()
546                .find(|p| *p.1 == group_p)
547                .unwrap();
548
549            self.priorities.remove(index.0);
550            self.groups.remove(&group_p);
551        }
552
553        for (group_p, tunable_p) in cleanup.tunables {
554            if let Some(group) = self.groups.get_mut(&group_p) {
555                let index = group
556                    .priorities
557                    .iter()
558                    .enumerate()
559                    .find(|p| *p.1 == tunable_p)
560                    .unwrap();
561                group.priorities.remove(index.0);
562                group.indices.remove(&tunable_p);
563            }
564        }
565    }
566
567    fn group_plan_next(&mut self, priority: i8) -> (Vec<Planned>, Cleanup) {
568        let group_plan = self.groups.get_mut(&priority).expect("To be filled");
569        let within_group_prio = group_plan.priorities.pop().unwrap();
570        let mut next_indices = group_plan.indices.remove(&within_group_prio).unwrap();
571        // Highest priority first, registration order among equals.
572        next_indices.sort_by_key(|planned| (core::cmp::Reverse(planned.priority), planned.index));
573
574        let mut cleanup_groups = Vec::new();
575        let mut cleanup_tunables = Vec::new();
576
577        for (pg, group) in self.groups.iter_mut() {
578            let mut num_empty_tunables = 0;
579            let num_tunables = group.priorities.len();
580
581            for (pt, indices) in group.indices.iter_mut() {
582                for n in &next_indices {
583                    let entry = indices
584                        .iter()
585                        .position(|p| p.index == n.index && p.group == n.group);
586                    if let Some(entry) = entry {
587                        indices.remove(entry);
588                    }
589                }
590
591                if indices.is_empty() {
592                    num_empty_tunables += 1;
593                    cleanup_tunables.push((*pg, *pt));
594                }
595            }
596
597            if num_empty_tunables == num_tunables {
598                cleanup_groups.push(*pg);
599            }
600        }
601
602        if within_group_prio < 0 {
603            // Discard algorithms with negative priority
604            next_indices.clear();
605        }
606
607        (
608            next_indices,
609            Cleanup {
610                groups: cleanup_groups,
611                tunables: cleanup_tunables,
612                skipped: within_group_prio < 0,
613            },
614        )
615    }
616}
617
618type PriorityFunc<K> = Arc<dyn Fn(&K) -> i8 + Send + Sync>;
619
620static GROUP_COUNTER: AtomicU32 = AtomicU32::new(0);
621
622#[cfg(test)]
623mod tests {
624    use core::fmt::Display;
625
626    use serde::{Deserialize, Serialize};
627
628    use super::*;
629
630    #[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Debug)]
631    struct FakeAutotuneKey;
632
633    impl Display for FakeAutotuneKey {
634        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
635            f.write_str("FakeAutotuneKey")
636        }
637    }
638
639    impl AutotuneKey for FakeAutotuneKey {}
640
641    #[test_log::test]
642    fn test_plan_order() {
643        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
644        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
645
646        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
647        let tunable1 =
648            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
649        let tunable2 =
650            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
651        let tunable3 =
652            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
653
654        let key = FakeAutotuneKey;
655        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
656
657        // tunable0 is in no group, so it trails the batch rather than leading it.
658        assert_eq!(plan.next().indices(), vec![2, 0]);
659        assert_eq!(plan.next().indices(), vec![1]);
660        assert_eq!(plan.next().indices(), vec![3]);
661        assert!(plan.next().is_empty());
662    }
663
664    #[test_log::test]
665    fn test_plan_order_multi_groups_same_priority() {
666        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
667        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
668        let group2 = TuneGroup::<FakeAutotuneKey>::new("group2", |_| 1);
669
670        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
671        let tunable1 =
672            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
673        let tunable2 =
674            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
675        let tunable3 =
676            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
677        let tunable4 =
678            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group2, |_| 2);
679
680        let key = FakeAutotuneKey;
681        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3, tunable4]);
682
683        assert_eq!(plan.next().indices(), vec![2, 0]);
684        assert_eq!(plan.next().indices(), vec![1]);
685        assert_eq!(plan.next().indices(), vec![3, 4]);
686        assert!(plan.next().is_empty());
687    }
688
689    #[test_log::test]
690    fn test_plan_order_tunable_multiple_groups() {
691        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 1);
692        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 2);
693
694        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
695        let tunable1 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
696            .group(&group0, |_| 1)
697            .group(&group1, |_| 2);
698        let tunable2 =
699            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
700        let tunable3 =
701            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 3);
702
703        let key = FakeAutotuneKey;
704        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
705
706        assert_eq!(plan.next().indices(), vec![3, 0]);
707        assert_eq!(plan.next().indices(), vec![1]);
708        assert_eq!(plan.next().indices(), vec![2]);
709        assert!(plan.next().is_empty());
710    }
711
712    #[test_log::test]
713    fn test_plan_negative_priority() {
714        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
715        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
716
717        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
718        let tunable1 =
719            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| -1);
720        let tunable2 =
721            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
722        let tunable3 =
723            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
724
725        let key = FakeAutotuneKey;
726        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
727
728        assert_eq!(plan.next().indices(), vec![2, 0]);
729        assert_eq!(plan.next().indices(), vec![3]);
730        assert!(plan.next().is_empty());
731    }
732
733    #[cfg(persistence)]
734    #[test_log::test]
735    fn test_plan_account_lists_every_tunable_in_release_order() {
736        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
737        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
738
739        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
740        let tunable1 =
741            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| -1);
742        let tunable2 =
743            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
744        let tunable3 =
745            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
746
747        let key = FakeAutotuneKey;
748        let plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
749        let account = plan.account(&["t0", "t1", "t2", "t3"]);
750
751        let order: Vec<_> = account
752            .iter()
753            .map(|planned| (planned.name.as_str(), planned.batch))
754            .collect();
755        assert_eq!(
756            order,
757            vec![
758                ("t2", Some(0)),
759                ("t0", Some(0)),
760                ("t3", Some(1)),
761                ("t1", None)
762            ]
763        );
764        assert_eq!(
765            account[0].groups,
766            vec![Placement {
767                group: "group0".into(),
768                ordered: false,
769                group_priority: 2,
770                priority: 2,
771            }]
772        );
773        assert!(account[1].groups.is_empty());
774        // Accounting for the plan leaves it to run from its first batch.
775        let mut plan = plan;
776        assert_eq!(plan.next().indices(), vec![2, 0]);
777    }
778
779    #[test_log::test]
780    fn test_plan_no_group() {
781        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
782        let tunable1 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
783
784        let key = FakeAutotuneKey;
785        let mut plan = TunePlan::new(&key, &[tunable0, tunable1]);
786
787        assert_eq!(plan.next().indices(), vec![0, 1]);
788        assert!(plan.next().is_empty());
789    }
790
791    #[test_log::test]
792    fn test_plan_falls_through_when_all_group_tunables_fail() {
793        // Every tunable lives in exactly one group; the caller treats every batch as a failure
794        // by continuing to call next(). The plan must still surface every tunable, in priority
795        // order, before going empty.
796        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
797        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
798
799        let tunable0 =
800            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
801        let tunable1 =
802            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
803        let tunable2 =
804            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 1);
805        let tunable3 =
806            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
807
808        let key = FakeAutotuneKey;
809        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
810
811        let mut all_returned: Vec<usize> = Vec::new();
812        loop {
813            let batch = plan.next();
814            if batch.is_empty() {
815                break;
816            }
817            all_returned.extend(batch.indices());
818        }
819
820        // Highest group (prio 2) drains first from highest intra-priority down, then next group.
821        assert_eq!(all_returned, vec![1, 0, 3, 2]);
822    }
823
824    #[test_log::test]
825    fn test_plan_single_group_exhausts_all_intra_priorities() {
826        // A single group with multiple intra-priorities should yield each batch separately,
827        // allowing the caller to continue on failures until the group is exhausted.
828        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 0);
829
830        let tunable0 =
831            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
832        let tunable1 =
833            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
834        let tunable2 =
835            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 3);
836
837        let key = FakeAutotuneKey;
838        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
839
840        assert_eq!(plan.next().indices(), vec![2]);
841        assert_eq!(plan.next().indices(), vec![1]);
842        assert_eq!(plan.next().indices(), vec![0]);
843        assert!(plan.next().is_empty());
844    }
845
846    #[test_log::test]
847    fn test_plan_all_negative_group_advances_to_next_group() {
848        // A group whose every tunable has a negative intra-priority should be skipped entirely
849        // without stopping autotuning — the next group must still be reached.
850        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
851        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
852
853        let tunable0 =
854            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| -1);
855        let tunable1 =
856            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| -2);
857        let tunable2 =
858            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 1);
859
860        let key = FakeAutotuneKey;
861        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
862
863        assert_eq!(plan.next().indices(), vec![2]);
864        assert!(plan.next().is_empty());
865    }
866
867    #[test_log::test]
868    fn test_plan_no_group_tunables_only_emitted_once_even_on_failures() {
869        // The ungrouped tunables are emitted together with the first group batch. If the caller
870        // keeps calling next() (treating the first batch as failing), they must not be
871        // re-emitted, and the plan must still advance to later groups.
872        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
873        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
874
875        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
876        let tunable1 =
877            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
878        let tunable2 =
879            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 1);
880
881        let key = FakeAutotuneKey;
882        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
883
884        assert_eq!(plan.next().indices(), vec![1, 0]);
885        assert_eq!(plan.next().indices(), vec![2]);
886        assert!(plan.next().is_empty());
887    }
888
889    #[test_log::test]
890    fn test_plan_multi_group_tunable_not_duplicated_across_failed_groups() {
891        // tunable1 belongs to both group0 and group1. It must be returned exactly once (via its
892        // higher-priority group), even if the caller continues iterating after failures.
893        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 1);
894        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 2);
895
896        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
897            .group(&group0, |_| 1)
898            .group(&group1, |_| 1);
899        let tunable1 =
900            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
901
902        let key = FakeAutotuneKey;
903        let mut plan = TunePlan::new(&key, &[tunable0, tunable1]);
904
905        let mut all_returned: Vec<usize> = Vec::new();
906        loop {
907            let batch = plan.next();
908            if batch.is_empty() {
909                break;
910            }
911            all_returned.extend(batch.indices());
912        }
913
914        // tunable0 comes from group1 (higher priority). tunable1 is the sole member of group0
915        // after cross-group dedup. No duplicates.
916        assert_eq!(all_returned, vec![0, 1]);
917    }
918
919    #[test_log::test]
920    fn test_plan_recurses_when_batch_is_fully_already_returned() {
921        // Regression test: a tunable that lives in multiple groups was already emitted via its
922        // higher-priority group, so when its lower-priority group's batch fires the only index
923        // is one already present in `returned`. The plan must NOT return an empty batch here
924        // (that signals "no more work" to the caller and aborts with NoValidKernelFound); it
925        // must recurse to the next intra-priority and surface the remaining tunable.
926        //
927        // Cross-group dedup in group_plan_next compares (index, Arc<String> group_name), so a
928        // tunable appearing in both group_hi and group_lo isn't auto-removed from group_lo
929        // when popped from group_hi — the `returned` + `all_skip` path is the only guard.
930        let group_hi = TuneGroup::<FakeAutotuneKey>::new("hi", |_| 2);
931        let group_lo = TuneGroup::<FakeAutotuneKey>::new("lo", |_| 1);
932
933        // tunable0 is in both groups. tunable1 is only in group_lo at a lower intra-priority.
934        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
935            .group(&group_hi, |_| 1)
936            .group(&group_lo, |_| 2);
937        let tunable1 =
938            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group_lo, |_| 1);
939
940        let key = FakeAutotuneKey;
941        let mut plan = TunePlan::new(&key, &[tunable0, tunable1]);
942
943        // First call: group_hi yields tunable0.
944        assert_eq!(plan.next().indices(), vec![0]);
945        // Second call: group_lo's higher intra-priority batch is just tunable0 (already
946        // returned). Without the fix this returns [] and the autotuner aborts. With the fix
947        // the plan recurses and yields tunable1.
948        assert_eq!(plan.next().indices(), vec![1]);
949        assert!(plan.next().is_empty());
950    }
951
952    #[test_log::test]
953    fn test_plan_ordered_group_is_one_batch_best_first() {
954        // Every member is in the round, best first, and a negative priority still skips.
955        let group =
956            TuneGroup::<FakeAutotuneKey>::ordered("ordered", |_| 1, OrderedSettings::default());
957
958        let tunable0 =
959            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 1);
960        let tunable1 =
961            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 3);
962        let tunable2 =
963            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| -1);
964        let tunable3 =
965            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 3);
966        let tunable4 =
967            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 2);
968
969        let key = FakeAutotuneKey;
970        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3, tunable4]);
971
972        assert_eq!(plan.next().indices(), vec![1, 3, 4, 0]);
973        assert!(plan.next().is_empty());
974    }
975
976    #[test_log::test]
977    fn test_plan_batch_carries_every_group_of_an_entry() {
978        // The batch carries patience only for the group that has one, and a tunable two
979        // groups bring in is one entry naming both.
980        let patience = Patience {
981            min_measured: 10,
982            max_misses: 5,
983        };
984        let ordered = TuneGroup::<FakeAutotuneKey>::ordered(
985            "ordered",
986            |_| 1,
987            OrderedSettings::default().with_patience(patience),
988        );
989        let cutoff = TuneGroup::<FakeAutotuneKey>::new("cutoff", |_| 1);
990
991        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
992        let tunable1 =
993            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&ordered, |_| 2);
994        let tunable2 =
995            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&cutoff, |_| 2);
996        let tunable3 =
997            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&ordered, |_| 1);
998        let tunable4 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
999            .group(&cutoff, |_| 2)
1000            .group(&ordered, |_| 1);
1001
1002        let key = FakeAutotuneKey;
1003        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3, tunable4]);
1004        let batch = plan.next();
1005        let entry = |index, groups: &[GroupId]| BatchEntry {
1006            index,
1007            groups: groups.to_vec(),
1008        };
1009
1010        assert_eq!(
1011            batch.entries,
1012            vec![
1013                entry(1, &[ordered.id]),
1014                entry(2, &[cutoff.id]),
1015                entry(4, &[cutoff.id, ordered.id]),
1016                entry(3, &[ordered.id]),
1017                entry(0, &[]),
1018            ]
1019        );
1020        assert_eq!(batch.patience, HashMap::from_iter([(ordered.id, patience)]));
1021    }
1022
1023    #[test_log::test]
1024    fn test_plan_entry_names_a_group_listed_twice_once() {
1025        // A group listed twice on one tunable is still one group: naming it twice would
1026        // count each of the tunable's times twice against its patience.
1027        let ordered =
1028            TuneGroup::<FakeAutotuneKey>::ordered("ordered", |_| 1, OrderedSettings::default());
1029
1030        let tunable = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
1031            .group(&ordered, |_| 2)
1032            .group(&ordered, |_| 1);
1033
1034        let key = FakeAutotuneKey;
1035        let mut plan = TunePlan::new(&key, &[tunable]);
1036
1037        assert_eq!(
1038            plan.next().entries,
1039            vec![BatchEntry {
1040                index: 0,
1041                groups: vec![ordered.id],
1042            }]
1043        );
1044    }
1045
1046    #[test_log::test]
1047    fn test_plan_ordered_group_keeps_the_group_cutoff() {
1048        // The order is within the group. A lower-priority group is still a fallback,
1049        // reached only once the ordered batch fails.
1050        let first =
1051            TuneGroup::<FakeAutotuneKey>::ordered("first", |_| 2, OrderedSettings::default());
1052        let fallback = TuneGroup::<FakeAutotuneKey>::new("fallback", |_| 1);
1053
1054        let tunable0 =
1055            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&first, |_| 1);
1056        let tunable1 =
1057            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&first, |_| 2);
1058        let tunable2 =
1059            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&fallback, |_| 1);
1060
1061        let key = FakeAutotuneKey;
1062        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
1063
1064        assert_eq!(plan.next().indices(), vec![1, 0]);
1065        assert_eq!(plan.next().indices(), vec![2]);
1066        assert!(plan.next().is_empty());
1067    }
1068
1069    #[test_log::test]
1070    fn test_plan_ordered_batch_leads_the_ungrouped_tunables() {
1071        // A tunable in no group must not preempt the ordered group's best candidate. The
1072        // batch stops at the first candidate under the bound, so whatever leads it is
1073        // what gets compiled.
1074        let group =
1075            TuneGroup::<FakeAutotuneKey>::ordered("ordered", |_| 1, OrderedSettings::default());
1076
1077        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
1078        let tunable1 =
1079            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 1);
1080        let tunable2 =
1081            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 5);
1082
1083        let key = FakeAutotuneKey;
1084        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
1085
1086        assert_eq!(plan.next().indices(), vec![2, 1, 0]);
1087        assert!(plan.next().is_empty());
1088    }
1089
1090    #[test_log::test]
1091    fn test_plan_ordered_batch_is_not_jumped_by_a_cutoff_group_beside_it() {
1092        // Both groups sit at group priority 1. The ordered batch is planned at its best
1093        // member's priority, so the cutoff member under it is a fallback behind the batch
1094        // and cannot take a round of its own in front of it, where a short circuit would
1095        // skip the ordered group whole.
1096        let ordered =
1097            TuneGroup::<FakeAutotuneKey>::ordered("ordered", |_| 1, OrderedSettings::default());
1098        let cutoff = TuneGroup::<FakeAutotuneKey>::new("cutoff", |_| 1);
1099
1100        let tunable0 =
1101            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&ordered, |_| 3);
1102        let tunable1 =
1103            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&ordered, |_| 1);
1104        let tunable2 =
1105            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&cutoff, |_| 2);
1106
1107        let key = FakeAutotuneKey;
1108        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
1109
1110        assert_eq!(plan.next().indices(), vec![0, 1]);
1111        assert_eq!(plan.next().indices(), vec![2]);
1112        assert!(plan.next().is_empty());
1113    }
1114
1115    #[test_log::test]
1116    fn test_plan_cutoff_member_above_the_ordered_batch_still_leads() {
1117        // The interleaving cuts both ways. A cutoff member priced above the ordered
1118        // group's best keeps its round in front, and one priced level with it joins the
1119        // batch, ordered among the members by priority.
1120        let ordered =
1121            TuneGroup::<FakeAutotuneKey>::ordered("ordered", |_| 1, OrderedSettings::default());
1122        let cutoff = TuneGroup::<FakeAutotuneKey>::new("cutoff", |_| 1);
1123
1124        let tunable0 =
1125            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&ordered, |_| 2);
1126        let tunable1 =
1127            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&ordered, |_| 1);
1128        let tunable2 =
1129            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&cutoff, |_| 3);
1130        let tunable3 =
1131            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&cutoff, |_| 2);
1132
1133        let key = FakeAutotuneKey;
1134        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
1135
1136        assert_eq!(plan.next().indices(), vec![2]);
1137        assert_eq!(plan.next().indices(), vec![0, 3, 1]);
1138        assert!(plan.next().is_empty());
1139    }
1140
1141    #[test_log::test]
1142    fn test_plan_same_named_groups_do_not_strike_each_other_out() {
1143        // Two groups may share a name, and the cross-level dedup must still tell them
1144        // apart. Keyed on the name, popping `hi`'s discarded negative level takes
1145        // tunable1 out of `lo`'s plan and the only viable candidate is never benchmarked.
1146        let hi = TuneGroup::<FakeAutotuneKey>::new("shared", |_| 2);
1147        let lo = TuneGroup::<FakeAutotuneKey>::new("shared", |_| 1);
1148
1149        let tunable0 =
1150            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&hi, |_| 1);
1151        let tunable1 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
1152            .group(&hi, |_| -1)
1153            .group(&lo, |_| 1);
1154
1155        let key = FakeAutotuneKey;
1156        let mut plan = TunePlan::new(&key, &[tunable0, tunable1]);
1157
1158        assert_eq!(plan.next().indices(), vec![0]);
1159        assert_eq!(plan.next().indices(), vec![1]);
1160        assert!(plan.next().is_empty());
1161    }
1162
1163    fn fake_kernel(_: ()) -> Result<(), String> {
1164        Ok(())
1165    }
1166}