Skip to main content

cubecl_runtime/tune/
tune_benchmark.rs

1use super::{AutotuneError, TuneFn, TuneInputs};
2use crate::{client::ComputeClient, runtime::Runtime};
3use alloc::string::ToString;
4use alloc::vec::Vec;
5use cubecl_common::profile::ProfileDuration;
6use cubecl_environment::config::RuntimeConfig;
7
8/// The trait to be implemented by an autotune output.
9pub trait AutotuneOutput: Send + 'static {
10    #[cfg(feature = "autotune-checks")]
11    /// Checks if the output of an autotune operation is the same as another one on the same
12    /// problem.
13    fn check_equivalence(&self, other: Self);
14}
15
16impl AutotuneOutput for () {
17    #[cfg(feature = "autotune-checks")]
18    fn check_equivalence(&self, _other: Self) {
19        //
20    }
21}
22
23/// Benchmark how long this operation takes for a number of samples.
24///
25/// Returns at least one duration, otherwise an error is returned.
26pub fn tune_benchmark<'a, R: Runtime, F: TuneInputs, Out: AutotuneOutput>(
27    operation: &TuneFn<F, Out>,
28    inputs: <F as TuneInputs>::At<'a>,
29    client: ComputeClient<R>,
30) -> Result<Vec<ProfileDuration>, AutotuneError> {
31    // `scoped` holds exclusive device access for the whole benchmark loop and
32    // accepts non-`'static` closures.
33    client
34        .clone()
35        .exclusive(move || profile_exclusive(operation, inputs, client))
36        .map_err(|err| AutotuneError::Unknown {
37            name: operation.name.to_string(),
38            err: err.to_string(),
39        })?
40}
41
42impl<F: TuneInputs, Out: AutotuneOutput> TuneFn<F, Out> {
43    /// Run the operation once without measuring it, to trigger compilation.
44    ///
45    /// Expects to already hold exclusive device access; the adaptive driver takes it once for
46    /// the whole round robin rather than once per candidate.
47    pub(crate) fn warmup_once<'a, R: Runtime>(
48        &self,
49        inputs: <F as TuneInputs>::At<'a>,
50        client: &ComputeClient<R>,
51    ) -> Result<(), AutotuneError> {
52        // We make sure the server is in a correct state.
53        let _errs = client.flush();
54
55        // The profile is dropped without being resolved: a warmup only exists to surface a
56        // failure to compile or launch, which is what the error carries.
57        self.sample_once(inputs, client).map(|_| ())
58    }
59
60    /// Queue a single measured execution. See [`Self::warmup_once`] for the locking expectation.
61    pub(crate) fn sample_once<'a, R: Runtime>(
62        &self,
63        inputs: <F as TuneInputs>::At<'a>,
64        client: &ComputeClient<R>,
65    ) -> Result<ProfileDuration, AutotuneError> {
66        // The output is returned so dead code elimination can't drop the work being profiled.
67        let profiled = client.profile(move || self.execute(inputs), &self.name);
68
69        match profiled {
70            Ok((Ok(_), duration)) => Ok(duration),
71            Ok((Err(err), _)) => Err(err),
72            Err(err) => Err(AutotuneError::Unknown {
73                name: self.name.to_string(),
74                err: err.to_string(),
75            }),
76        }
77    }
78}
79
80fn profile_exclusive<'a, R: Runtime, F: TuneInputs, Out: AutotuneOutput>(
81    operation: &TuneFn<F, Out>,
82    inputs: <F as TuneInputs>::At<'a>,
83    client: ComputeClient<R>,
84) -> Result<Vec<ProfileDuration>, AutotuneError> {
85    // These launches are the measurement, so they run even inside a dry run:
86    // that mode exists to skip the *workload*, not the tuning it is there to
87    // provoke. The guard covers the warm-up too, since a candidate measured
88    // without one is measured on its slowest run.
89    //
90    // It has to live here rather than around the `exclusive` call in
91    // `tune_benchmark`: the guard is thread-local, and `exclusive` runs this
92    // body on the device thread, which is where the launches below are issued
93    // from.
94    let _real_run = crate::dry_run::RealRun::new();
95
96    warmup(operation, inputs.clone(), client.clone())?;
97
98    // The same budget the adaptive scheduler reads. This pass takes the ceiling: with no
99    // elimination, there is nothing for a smaller budget to buy, and a candidate that stops early
100    // here would just be measured on less evidence than its rivals.
101    let (_, num_samples) = crate::config::CubeClRuntimeConfig::get()
102        .autotune
103        .bench
104        .samples();
105    let mut durations = Vec::new();
106
107    for _ in 0..num_samples {
108        // A candidate that fails once is disqualified regardless of how the remaining samples
109        // go, so the loop stops on the first error and hands it back untouched. Sampling on
110        // would only pay more device round trips to reach the same verdict, with the reason
111        // for the failure replaced by `InvalidSamples`.
112        durations.push(operation.sample_once(inputs.clone(), &client)?);
113    }
114
115    if durations.is_empty() {
116        Err(AutotuneError::InvalidSamples {
117            name: operation.name.to_string(),
118        })
119    } else {
120        Ok(durations)
121    }
122}
123
124fn warmup<'a, R: Runtime, F: TuneInputs, Out: AutotuneOutput>(
125    operation: &TuneFn<F, Out>,
126    inputs: <F as TuneInputs>::At<'a>,
127    client: ComputeClient<R>,
128) -> Result<(), AutotuneError> {
129    let num_warmup = 3;
130
131    let mut errors = Vec::with_capacity(num_warmup);
132    // We make sure the server is in a correct state.
133    let _errs = client.flush();
134
135    for _ in 0..num_warmup {
136        let inputs = inputs.clone();
137        let profiled = client.profile(move || operation.execute(inputs), &operation.name);
138
139        match profiled {
140            // The tunable rejected its own configuration, which it will do identically on
141            // every call, so the remaining warmups and the whole sampling loop are skipped.
142            // The error is propagated as-is to keep the reason it was rejected.
143            Ok((Err(err), _)) => return Err(err),
144            Ok(_) => {}
145            Err(err) => errors.push(err),
146        }
147    }
148
149    if errors.len() < num_warmup {
150        Ok(())
151    } else {
152        let msg = alloc::format!("{:?}", errors.remove(num_warmup - 1));
153        Err(AutotuneError::Unknown {
154            name: operation.name.to_string(),
155            err: msg,
156        })
157    }
158}