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::Rejected {
73                        name: name_for_err.to_string(),
74                        reason: 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    /// Every one of `len` tunables in the order the plan releases them, with the
461    /// batch each comes in. A tunable no batch holds, its every priority
462    /// negative, comes last in declaration order, in no batch.
463    pub(crate) fn order(&self, len: usize) -> Vec<(usize, Option<usize>)> {
464        let mut plan = self.clone();
465        let batches = core::iter::from_fn(|| {
466            Some(plan.next())
467                .filter(|batch| !batch.is_empty())
468                .map(|batch| batch.indices())
469        });
470        let mut order: Vec<_> = batches
471            .enumerate()
472            .flat_map(|(batch, indices)| indices.into_iter().map(move |index| (index, Some(batch))))
473            .collect();
474        let excluded = (0..len).filter(|index| !order.iter().any(|(seen, _)| seen == index));
475        order.extend(excluded.map(|index| (index, None)).collect::<Vec<_>>());
476        order
477    }
478
479    /// The whole plan as a recorded tune reports it: every tunable named by
480    /// `names`, in the [order](Self::order) the plan releases them, with the
481    /// batch each comes in and its place in each of its groups.
482    #[cfg(persistence)]
483    pub(crate) fn account(&self, names: &[&str]) -> Vec<super::record::PlannedCandidate> {
484        self.order(names.len())
485            .into_iter()
486            .map(|(index, batch)| super::record::PlannedCandidate {
487                name: names[index].to_string(),
488                index,
489                batch,
490                groups: self.placements[index].clone(),
491            })
492            .collect()
493    }
494
495    /// The next batch to benchmark. An empty one means the plan is exhausted.
496    pub(crate) fn next(&mut self) -> Batch {
497        // A tunable in no group states no priority, so it trails the batch: the grouped
498        // candidates decide what is compiled and benchmarked first.
499        let ungrouped = core::mem::take(&mut self.no_groups);
500        let priority = self.priorities.last();
501
502        let priority = match priority {
503            Some(val) => *val,
504            None => return Batch::unconditional(ungrouped),
505        };
506        let mut batch = Batch {
507            entries: Vec::new(),
508            patience: self.patience.clone(),
509        };
510
511        let (group_indices, cleanup) = self.group_plan_next(priority);
512        // Some entries are skipped for this round of prioritizing.
513        let skipped = cleanup.skipped || priority < 0;
514        let mut all_skip = true;
515
516        self.cleanup(cleanup);
517
518        if priority >= 0 {
519            for planned in group_indices {
520                if self.returned.contains(&planned.index) {
521                    continue;
522                }
523                all_skip = false;
524                batch.push(&planned);
525            }
526        }
527
528        for index in ungrouped {
529            batch.push_unconditional(index);
530        }
531
532        // The batch is empty, but it doesn't mean we should stop
533        // autotuning, since some entries were skipped.
534
535        if batch.is_empty() && (skipped || all_skip) {
536            self.next()
537        } else {
538            for entry in batch.entries.iter() {
539                self.returned.push(entry.index);
540            }
541            batch
542        }
543    }
544
545    fn cleanup(&mut self, cleanup: Cleanup) {
546        for group_p in cleanup.groups {
547            let index = self
548                .priorities
549                .iter()
550                .enumerate()
551                .find(|p| *p.1 == group_p)
552                .unwrap();
553
554            self.priorities.remove(index.0);
555            self.groups.remove(&group_p);
556        }
557
558        for (group_p, tunable_p) in cleanup.tunables {
559            if let Some(group) = self.groups.get_mut(&group_p) {
560                let index = group
561                    .priorities
562                    .iter()
563                    .enumerate()
564                    .find(|p| *p.1 == tunable_p)
565                    .unwrap();
566                group.priorities.remove(index.0);
567                group.indices.remove(&tunable_p);
568            }
569        }
570    }
571
572    fn group_plan_next(&mut self, priority: i8) -> (Vec<Planned>, Cleanup) {
573        let group_plan = self.groups.get_mut(&priority).expect("To be filled");
574        let within_group_prio = group_plan.priorities.pop().unwrap();
575        let mut next_indices = group_plan.indices.remove(&within_group_prio).unwrap();
576        // Highest priority first, registration order among equals.
577        next_indices.sort_by_key(|planned| (core::cmp::Reverse(planned.priority), planned.index));
578
579        let mut cleanup_groups = Vec::new();
580        let mut cleanup_tunables = Vec::new();
581
582        for (pg, group) in self.groups.iter_mut() {
583            let mut num_empty_tunables = 0;
584            let num_tunables = group.priorities.len();
585
586            for (pt, indices) in group.indices.iter_mut() {
587                for n in &next_indices {
588                    let entry = indices
589                        .iter()
590                        .position(|p| p.index == n.index && p.group == n.group);
591                    if let Some(entry) = entry {
592                        indices.remove(entry);
593                    }
594                }
595
596                if indices.is_empty() {
597                    num_empty_tunables += 1;
598                    cleanup_tunables.push((*pg, *pt));
599                }
600            }
601
602            if num_empty_tunables == num_tunables {
603                cleanup_groups.push(*pg);
604            }
605        }
606
607        if within_group_prio < 0 {
608            // Discard algorithms with negative priority
609            next_indices.clear();
610        }
611
612        (
613            next_indices,
614            Cleanup {
615                groups: cleanup_groups,
616                tunables: cleanup_tunables,
617                skipped: within_group_prio < 0,
618            },
619        )
620    }
621}
622
623type PriorityFunc<K> = Arc<dyn Fn(&K) -> i8 + Send + Sync>;
624
625static GROUP_COUNTER: AtomicU32 = AtomicU32::new(0);
626
627#[cfg(test)]
628mod tests {
629    use core::fmt::Display;
630
631    use serde::{Deserialize, Serialize};
632
633    use super::*;
634
635    #[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Debug)]
636    struct FakeAutotuneKey;
637
638    impl Display for FakeAutotuneKey {
639        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
640            f.write_str("FakeAutotuneKey")
641        }
642    }
643
644    impl AutotuneKey for FakeAutotuneKey {}
645
646    #[test_log::test]
647    fn test_plan_order() {
648        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
649        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
650
651        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
652        let tunable1 =
653            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
654        let tunable2 =
655            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
656        let tunable3 =
657            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
658
659        let key = FakeAutotuneKey;
660        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
661
662        // tunable0 is in no group, so it trails the batch rather than leading it.
663        assert_eq!(plan.next().indices(), vec![2, 0]);
664        assert_eq!(plan.next().indices(), vec![1]);
665        assert_eq!(plan.next().indices(), vec![3]);
666        assert!(plan.next().is_empty());
667    }
668
669    #[test_log::test]
670    fn test_plan_order_multi_groups_same_priority() {
671        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
672        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
673        let group2 = TuneGroup::<FakeAutotuneKey>::new("group2", |_| 1);
674
675        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
676        let tunable1 =
677            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
678        let tunable2 =
679            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
680        let tunable3 =
681            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
682        let tunable4 =
683            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group2, |_| 2);
684
685        let key = FakeAutotuneKey;
686        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3, tunable4]);
687
688        assert_eq!(plan.next().indices(), vec![2, 0]);
689        assert_eq!(plan.next().indices(), vec![1]);
690        assert_eq!(plan.next().indices(), vec![3, 4]);
691        assert!(plan.next().is_empty());
692    }
693
694    #[test_log::test]
695    fn test_plan_order_tunable_multiple_groups() {
696        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 1);
697        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 2);
698
699        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
700        let tunable1 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
701            .group(&group0, |_| 1)
702            .group(&group1, |_| 2);
703        let tunable2 =
704            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
705        let tunable3 =
706            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 3);
707
708        let key = FakeAutotuneKey;
709        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
710
711        assert_eq!(plan.next().indices(), vec![3, 0]);
712        assert_eq!(plan.next().indices(), vec![1]);
713        assert_eq!(plan.next().indices(), vec![2]);
714        assert!(plan.next().is_empty());
715    }
716
717    #[test_log::test]
718    fn test_plan_negative_priority() {
719        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
720        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
721
722        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
723        let tunable1 =
724            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| -1);
725        let tunable2 =
726            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
727        let tunable3 =
728            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
729
730        let key = FakeAutotuneKey;
731        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
732
733        assert_eq!(plan.next().indices(), vec![2, 0]);
734        assert_eq!(plan.next().indices(), vec![3]);
735        assert!(plan.next().is_empty());
736    }
737
738    #[cfg(persistence)]
739    #[test_log::test]
740    fn test_plan_account_lists_every_tunable_in_release_order() {
741        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
742        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
743
744        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
745        let tunable1 =
746            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| -1);
747        let tunable2 =
748            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
749        let tunable3 =
750            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
751
752        let key = FakeAutotuneKey;
753        let plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
754        let account = plan.account(&["t0", "t1", "t2", "t3"]);
755
756        let order: Vec<_> = account
757            .iter()
758            .map(|planned| (planned.name.as_str(), planned.batch))
759            .collect();
760        assert_eq!(
761            order,
762            vec![
763                ("t2", Some(0)),
764                ("t0", Some(0)),
765                ("t3", Some(1)),
766                ("t1", None)
767            ]
768        );
769        assert_eq!(
770            account[0].groups,
771            vec![Placement {
772                group: "group0".into(),
773                ordered: false,
774                group_priority: 2,
775                priority: 2,
776            }]
777        );
778        assert!(account[1].groups.is_empty());
779        // Accounting for the plan leaves it to run from its first batch.
780        let mut plan = plan;
781        assert_eq!(plan.next().indices(), vec![2, 0]);
782    }
783
784    #[test_log::test]
785    fn test_plan_no_group() {
786        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
787        let tunable1 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
788
789        let key = FakeAutotuneKey;
790        let mut plan = TunePlan::new(&key, &[tunable0, tunable1]);
791
792        assert_eq!(plan.next().indices(), vec![0, 1]);
793        assert!(plan.next().is_empty());
794    }
795
796    #[test_log::test]
797    fn test_plan_falls_through_when_all_group_tunables_fail() {
798        // Every tunable lives in exactly one group; the caller treats every batch as a failure
799        // by continuing to call next(). The plan must still surface every tunable, in priority
800        // order, before going empty.
801        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
802        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
803
804        let tunable0 =
805            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
806        let tunable1 =
807            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
808        let tunable2 =
809            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 1);
810        let tunable3 =
811            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 2);
812
813        let key = FakeAutotuneKey;
814        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
815
816        let mut all_returned: Vec<usize> = Vec::new();
817        loop {
818            let batch = plan.next();
819            if batch.is_empty() {
820                break;
821            }
822            all_returned.extend(batch.indices());
823        }
824
825        // Highest group (prio 2) drains first from highest intra-priority down, then next group.
826        assert_eq!(all_returned, vec![1, 0, 3, 2]);
827    }
828
829    #[test_log::test]
830    fn test_plan_single_group_exhausts_all_intra_priorities() {
831        // A single group with multiple intra-priorities should yield each batch separately,
832        // allowing the caller to continue on failures until the group is exhausted.
833        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 0);
834
835        let tunable0 =
836            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
837        let tunable1 =
838            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
839        let tunable2 =
840            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 3);
841
842        let key = FakeAutotuneKey;
843        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
844
845        assert_eq!(plan.next().indices(), vec![2]);
846        assert_eq!(plan.next().indices(), vec![1]);
847        assert_eq!(plan.next().indices(), vec![0]);
848        assert!(plan.next().is_empty());
849    }
850
851    #[test_log::test]
852    fn test_plan_all_negative_group_advances_to_next_group() {
853        // A group whose every tunable has a negative intra-priority should be skipped entirely
854        // without stopping autotuning — the next group must still be reached.
855        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
856        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
857
858        let tunable0 =
859            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| -1);
860        let tunable1 =
861            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| -2);
862        let tunable2 =
863            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 1);
864
865        let key = FakeAutotuneKey;
866        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
867
868        assert_eq!(plan.next().indices(), vec![2]);
869        assert!(plan.next().is_empty());
870    }
871
872    #[test_log::test]
873    fn test_plan_no_group_tunables_only_emitted_once_even_on_failures() {
874        // The ungrouped tunables are emitted together with the first group batch. If the caller
875        // keeps calling next() (treating the first batch as failing), they must not be
876        // re-emitted, and the plan must still advance to later groups.
877        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 2);
878        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 1);
879
880        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
881        let tunable1 =
882            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 1);
883        let tunable2 =
884            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group1, |_| 1);
885
886        let key = FakeAutotuneKey;
887        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
888
889        assert_eq!(plan.next().indices(), vec![1, 0]);
890        assert_eq!(plan.next().indices(), vec![2]);
891        assert!(plan.next().is_empty());
892    }
893
894    #[test_log::test]
895    fn test_plan_multi_group_tunable_not_duplicated_across_failed_groups() {
896        // tunable1 belongs to both group0 and group1. It must be returned exactly once (via its
897        // higher-priority group), even if the caller continues iterating after failures.
898        let group0 = TuneGroup::<FakeAutotuneKey>::new("group0", |_| 1);
899        let group1 = TuneGroup::<FakeAutotuneKey>::new("group1", |_| 2);
900
901        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
902            .group(&group0, |_| 1)
903            .group(&group1, |_| 1);
904        let tunable1 =
905            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group0, |_| 2);
906
907        let key = FakeAutotuneKey;
908        let mut plan = TunePlan::new(&key, &[tunable0, tunable1]);
909
910        let mut all_returned: Vec<usize> = Vec::new();
911        loop {
912            let batch = plan.next();
913            if batch.is_empty() {
914                break;
915            }
916            all_returned.extend(batch.indices());
917        }
918
919        // tunable0 comes from group1 (higher priority). tunable1 is the sole member of group0
920        // after cross-group dedup. No duplicates.
921        assert_eq!(all_returned, vec![0, 1]);
922    }
923
924    #[test_log::test]
925    fn test_plan_recurses_when_batch_is_fully_already_returned() {
926        // Regression test: a tunable that lives in multiple groups was already emitted via its
927        // higher-priority group, so when its lower-priority group's batch fires the only index
928        // is one already present in `returned`. The plan must NOT return an empty batch here
929        // (that signals "no more work" to the caller and aborts with NoValidKernelFound); it
930        // must recurse to the next intra-priority and surface the remaining tunable.
931        //
932        // Cross-group dedup in group_plan_next compares (index, Arc<String> group_name), so a
933        // tunable appearing in both group_hi and group_lo isn't auto-removed from group_lo
934        // when popped from group_hi — the `returned` + `all_skip` path is the only guard.
935        let group_hi = TuneGroup::<FakeAutotuneKey>::new("hi", |_| 2);
936        let group_lo = TuneGroup::<FakeAutotuneKey>::new("lo", |_| 1);
937
938        // tunable0 is in both groups. tunable1 is only in group_lo at a lower intra-priority.
939        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
940            .group(&group_hi, |_| 1)
941            .group(&group_lo, |_| 2);
942        let tunable1 =
943            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group_lo, |_| 1);
944
945        let key = FakeAutotuneKey;
946        let mut plan = TunePlan::new(&key, &[tunable0, tunable1]);
947
948        // First call: group_hi yields tunable0.
949        assert_eq!(plan.next().indices(), vec![0]);
950        // Second call: group_lo's higher intra-priority batch is just tunable0 (already
951        // returned). Without the fix this returns [] and the autotuner aborts. With the fix
952        // the plan recurses and yields tunable1.
953        assert_eq!(plan.next().indices(), vec![1]);
954        assert!(plan.next().is_empty());
955    }
956
957    #[test_log::test]
958    fn test_plan_ordered_group_is_one_batch_best_first() {
959        // Every member is in the round, best first, and a negative priority still skips.
960        let group =
961            TuneGroup::<FakeAutotuneKey>::ordered("ordered", |_| 1, OrderedSettings::default());
962
963        let tunable0 =
964            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 1);
965        let tunable1 =
966            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 3);
967        let tunable2 =
968            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| -1);
969        let tunable3 =
970            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 3);
971        let tunable4 =
972            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 2);
973
974        let key = FakeAutotuneKey;
975        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3, tunable4]);
976
977        assert_eq!(plan.next().indices(), vec![1, 3, 4, 0]);
978        assert!(plan.next().is_empty());
979    }
980
981    #[test_log::test]
982    fn test_plan_batch_carries_every_group_of_an_entry() {
983        // The batch carries patience only for the group that has one, and a tunable two
984        // groups bring in is one entry naming both.
985        let patience = Patience {
986            min_measured: 10,
987            max_misses: 5,
988        };
989        let ordered = TuneGroup::<FakeAutotuneKey>::ordered(
990            "ordered",
991            |_| 1,
992            OrderedSettings::default().with_patience(patience),
993        );
994        let cutoff = TuneGroup::<FakeAutotuneKey>::new("cutoff", |_| 1);
995
996        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
997        let tunable1 =
998            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&ordered, |_| 2);
999        let tunable2 =
1000            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&cutoff, |_| 2);
1001        let tunable3 =
1002            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&ordered, |_| 1);
1003        let tunable4 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
1004            .group(&cutoff, |_| 2)
1005            .group(&ordered, |_| 1);
1006
1007        let key = FakeAutotuneKey;
1008        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3, tunable4]);
1009        let batch = plan.next();
1010        let entry = |index, groups: &[GroupId]| BatchEntry {
1011            index,
1012            groups: groups.to_vec(),
1013        };
1014
1015        assert_eq!(
1016            batch.entries,
1017            vec![
1018                entry(1, &[ordered.id]),
1019                entry(2, &[cutoff.id]),
1020                entry(4, &[cutoff.id, ordered.id]),
1021                entry(3, &[ordered.id]),
1022                entry(0, &[]),
1023            ]
1024        );
1025        assert_eq!(batch.patience, HashMap::from_iter([(ordered.id, patience)]));
1026    }
1027
1028    #[test_log::test]
1029    fn test_plan_entry_names_a_group_listed_twice_once() {
1030        // A group listed twice on one tunable is still one group: naming it twice would
1031        // count each of the tunable's times twice against its patience.
1032        let ordered =
1033            TuneGroup::<FakeAutotuneKey>::ordered("ordered", |_| 1, OrderedSettings::default());
1034
1035        let tunable = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
1036            .group(&ordered, |_| 2)
1037            .group(&ordered, |_| 1);
1038
1039        let key = FakeAutotuneKey;
1040        let mut plan = TunePlan::new(&key, &[tunable]);
1041
1042        assert_eq!(
1043            plan.next().entries,
1044            vec![BatchEntry {
1045                index: 0,
1046                groups: vec![ordered.id],
1047            }]
1048        );
1049    }
1050
1051    #[test_log::test]
1052    fn test_plan_ordered_group_keeps_the_group_cutoff() {
1053        // The order is within the group. A lower-priority group is still a fallback,
1054        // reached only once the ordered batch fails.
1055        let first =
1056            TuneGroup::<FakeAutotuneKey>::ordered("first", |_| 2, OrderedSettings::default());
1057        let fallback = TuneGroup::<FakeAutotuneKey>::new("fallback", |_| 1);
1058
1059        let tunable0 =
1060            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&first, |_| 1);
1061        let tunable1 =
1062            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&first, |_| 2);
1063        let tunable2 =
1064            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&fallback, |_| 1);
1065
1066        let key = FakeAutotuneKey;
1067        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
1068
1069        assert_eq!(plan.next().indices(), vec![1, 0]);
1070        assert_eq!(plan.next().indices(), vec![2]);
1071        assert!(plan.next().is_empty());
1072    }
1073
1074    #[test_log::test]
1075    fn test_plan_ordered_batch_leads_the_ungrouped_tunables() {
1076        // A tunable in no group must not preempt the ordered group's best candidate. The
1077        // batch stops at the first candidate under the bound, so whatever leads it is
1078        // what gets compiled.
1079        let group =
1080            TuneGroup::<FakeAutotuneKey>::ordered("ordered", |_| 1, OrderedSettings::default());
1081
1082        let tunable0 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel);
1083        let tunable1 =
1084            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 1);
1085        let tunable2 =
1086            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&group, |_| 5);
1087
1088        let key = FakeAutotuneKey;
1089        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
1090
1091        assert_eq!(plan.next().indices(), vec![2, 1, 0]);
1092        assert!(plan.next().is_empty());
1093    }
1094
1095    #[test_log::test]
1096    fn test_plan_ordered_batch_is_not_jumped_by_a_cutoff_group_beside_it() {
1097        // Both groups sit at group priority 1. The ordered batch is planned at its best
1098        // member's priority, so the cutoff member under it is a fallback behind the batch
1099        // and cannot take a round of its own in front of it, where a short circuit would
1100        // skip the ordered group whole.
1101        let ordered =
1102            TuneGroup::<FakeAutotuneKey>::ordered("ordered", |_| 1, OrderedSettings::default());
1103        let cutoff = TuneGroup::<FakeAutotuneKey>::new("cutoff", |_| 1);
1104
1105        let tunable0 =
1106            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&ordered, |_| 3);
1107        let tunable1 =
1108            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&ordered, |_| 1);
1109        let tunable2 =
1110            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&cutoff, |_| 2);
1111
1112        let key = FakeAutotuneKey;
1113        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2]);
1114
1115        assert_eq!(plan.next().indices(), vec![0, 1]);
1116        assert_eq!(plan.next().indices(), vec![2]);
1117        assert!(plan.next().is_empty());
1118    }
1119
1120    #[test_log::test]
1121    fn test_plan_cutoff_member_above_the_ordered_batch_still_leads() {
1122        // The interleaving cuts both ways. A cutoff member priced above the ordered
1123        // group's best keeps its round in front, and one priced level with it joins the
1124        // batch, ordered among the members by priority.
1125        let ordered =
1126            TuneGroup::<FakeAutotuneKey>::ordered("ordered", |_| 1, OrderedSettings::default());
1127        let cutoff = TuneGroup::<FakeAutotuneKey>::new("cutoff", |_| 1);
1128
1129        let tunable0 =
1130            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&ordered, |_| 2);
1131        let tunable1 =
1132            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&ordered, |_| 1);
1133        let tunable2 =
1134            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&cutoff, |_| 3);
1135        let tunable3 =
1136            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&cutoff, |_| 2);
1137
1138        let key = FakeAutotuneKey;
1139        let mut plan = TunePlan::new(&key, &[tunable0, tunable1, tunable2, tunable3]);
1140
1141        assert_eq!(plan.next().indices(), vec![2]);
1142        assert_eq!(plan.next().indices(), vec![0, 3, 1]);
1143        assert!(plan.next().is_empty());
1144    }
1145
1146    #[test_log::test]
1147    fn test_plan_same_named_groups_do_not_strike_each_other_out() {
1148        // Two groups may share a name, and the cross-level dedup must still tell them
1149        // apart. Keyed on the name, popping `hi`'s discarded negative level takes
1150        // tunable1 out of `lo`'s plan and the only viable candidate is never benchmarked.
1151        let hi = TuneGroup::<FakeAutotuneKey>::new("shared", |_| 2);
1152        let lo = TuneGroup::<FakeAutotuneKey>::new("shared", |_| 1);
1153
1154        let tunable0 =
1155            Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel).group(&hi, |_| 1);
1156        let tunable1 = Tunable::<FakeAutotuneKey, (), ()>::new("fake", fake_kernel)
1157            .group(&hi, |_| -1)
1158            .group(&lo, |_| 1);
1159
1160        let key = FakeAutotuneKey;
1161        let mut plan = TunePlan::new(&key, &[tunable0, tunable1]);
1162
1163        assert_eq!(plan.next().indices(), vec![0]);
1164        assert_eq!(plan.next().indices(), vec![1]);
1165        assert!(plan.next().is_empty());
1166    }
1167
1168    fn fake_kernel(_: ()) -> Result<(), String> {
1169        Ok(())
1170    }
1171}