Skip to main content

ruda_runtime/runtime/tune/
tune_benchmark.rs

1use super::{AutotuneError, TuneFn, TuneInputs};
2use crate::runtime::{client::ComputeClient, backend::Runtime};
3use alloc::string::ToString;
4use alloc::vec::Vec;
5use ruda_core::profile::ProfileDuration;
6
7/// The trait to be implemented by an autotune output.
8pub trait AutotuneOutput: Send + 'static {
9    /// Fallible correctness gate for stack autotuning. `Ok(false)` explicitly means that this
10    /// output has no validator; a successful launch alone is NOT a correctness check.
11    /// Existing implementers need not add a method. Strict stack policy rejects unsupported output.
12    fn validate_for_tuning(&self, _other: &Self, _absolute: f64, _relative: f64, _max_bytes: u64)
13        -> Result<bool, alloc::string::String> { Ok(false) }
14
15    #[cfg(feature = "runtime-autotune-checks")]
16    /// Checks if the output of an autotune operation is the same as another one on the same
17    /// problem.
18    fn check_equivalence(&self, other: Self);
19}
20
21impl AutotuneOutput for () {
22    #[cfg(feature = "runtime-autotune-checks")]
23    fn check_equivalence(&self, _other: Self) {
24        //
25    }
26}
27
28/// Benchmark how long this operation takes for a number of samples.
29///
30/// Returns at least one duration, otherwise an error is returned.
31pub fn tune_benchmark<'a, R: Runtime, F: TuneInputs, Out: AutotuneOutput>(
32    operation: &TuneFn<F, Out>,
33    inputs: <F as TuneInputs>::At<'a>,
34    client: ComputeClient<R>,
35) -> Result<Vec<ProfileDuration>, AutotuneError> {
36    // `scoped` holds exclusive device access for the whole benchmark loop and
37    // accepts non-`'static` closures.
38    client
39        .clone()
40        .exclusive(move || profile_exclusive(operation, inputs, client))
41        .map_err(|err| AutotuneError::Unknown {
42            name: operation.name.to_string(),
43            err: err.to_string(),
44        })?
45}
46
47fn profile_exclusive<'a, R: Runtime, F: TuneInputs, Out: AutotuneOutput>(
48    operation: &TuneFn<F, Out>,
49    inputs: <F as TuneInputs>::At<'a>,
50    client: ComputeClient<R>,
51) -> Result<Vec<ProfileDuration>, AutotuneError> {
52    warmup(operation, inputs.clone(), client.clone())?;
53
54    let num_samples = 10;
55    let mut durations = Vec::with_capacity(num_samples);
56
57    for _ in 0..num_samples {
58        let result: Result<
59            (Result<Out, AutotuneError>, ProfileDuration),
60            crate::runtime::server::ProfileError,
61        > = {
62            let inputs = inputs.clone();
63
64            client.profile(
65                move || {
66                    // It is important to return the output since otherwise deadcode elimination
67                    // might optimize away code that needs to be profiled.
68                    operation.execute(inputs)
69                },
70                &operation.name,
71            )
72        };
73
74        let result = match result {
75            Ok((out, duration)) => match out {
76                Ok(_) => Some(duration),
77                Err(err) => {
78                    log::trace!("Error while autotuning {err:?}");
79                    None
80                }
81            },
82            Err(err) => {
83                log::trace!("Error while autotuning {err:?}");
84                None
85            }
86        };
87
88        if let Some(item) = result {
89            durations.push(item);
90        }
91    }
92
93    if durations.is_empty() {
94        Err(AutotuneError::InvalidSamples {
95            name: operation.name.to_string(),
96        })
97    } else {
98        Ok(durations)
99    }
100}
101
102fn warmup<'a, R: Runtime, F: TuneInputs, Out: AutotuneOutput>(
103    operation: &TuneFn<F, Out>,
104    inputs: <F as TuneInputs>::At<'a>,
105    client: ComputeClient<R>,
106) -> Result<(), AutotuneError> {
107    let num_warmup = 3;
108
109    let mut num_errors = 0;
110    let mut last_error = None;
111    // We make sure the server is in a correct state.
112    let _errs = client.flush();
113
114    for _ in 0..num_warmup {
115        let inputs = inputs.clone();
116        let profiled = client.profile(move || operation.execute(inputs), &operation.name);
117
118        match profiled {
119            Ok((Ok(_), _)) => {}
120            Ok((Err(err), _)) => return Err(err),
121            Err(err) => {
122                num_errors += 1;
123                last_error = Some(err);
124            }
125        }
126    }
127
128    if num_errors < num_warmup {
129        Ok(())
130    } else {
131        let msg = alloc::format!("{:?}", last_error.expect("all warmup profiles failed"));
132        Err(AutotuneError::Unknown {
133            name: operation.name.to_string(),
134            err: msg,
135        })
136    }
137}