1use alloc::format;
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4use ruda_core::profile::ProfileDuration;
5
6use core::time::Duration;
7
8use alloc::string::{String, ToString};
9use ruda_core::benchmark::{BenchmarkComputations, BenchmarkDurations};
10
11use crate::runtime::config::{Logger, autotune::AutotuneLogLevel};
12use crate::runtime::server::LaunchError;
13use crate::runtime::tune::{AutotuneResult, TuneCache, tune_benchmark};
14use crate::runtime::{client::ComputeClient, backend::Runtime};
15
16use super::{AutotuneKey, AutotuneOutput, TunableSet, TuneCacheResult, TuneInputs};
17
18#[derive(Debug)]
19pub struct Tuner<K: AutotuneKey> {
25 cache: Arc<spin::Mutex<TuneCache<K>>>,
26 logger: Arc<spin::Mutex<Logger>>,
27}
28
29#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
31#[derive(new, Debug, Clone, PartialEq, Eq)]
32pub struct AutotuneOutcome {
33 name: String,
34 index: usize,
35 computation: BenchmarkComputations,
36}
37
38impl core::fmt::Display for AutotuneOutcome {
39 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
40 write!(
41 f,
42 "Autotune[{}] name {} => {:?}",
43 self.index, self.name, self.computation
44 )
45 }
46}
47
48#[derive(Debug, Clone)]
50#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
51pub enum AutotuneError {
52 Unknown {
54 name: String,
56 err: String,
58 },
59 InvalidSamples {
61 name: String,
63 },
64 NoValidKernelFound {
70 context: String,
72 },
73 Skip {
75 name: String,
77 },
78
79 Launch(LaunchError),
81}
82
83impl From<LaunchError> for AutotuneError {
84 fn from(value: LaunchError) -> Self {
85 Self::Launch(value)
86 }
87}
88
89struct PendingBench {
91 index: usize,
92 name: String,
93 profiles: Vec<ProfileDuration>,
94}
95
96struct TuneRequest<K: AutotuneKey> {
99 key: K,
100 results: Vec<AutotuneResult>,
101 #[cfg(std_io)]
102 checksum: String,
103 context_logs: Option<String>,
104 pending: Vec<PendingBench>,
105}
106
107#[allow(clippy::new_without_default)]
108impl<K: AutotuneKey> Tuner<K> {
109 pub fn new(name: &str, device_id: &str) -> Self {
112 Self {
113 cache: Arc::new(spin::Mutex::new(TuneCache::new(name, device_id))),
114 logger: Arc::new(spin::Mutex::new(Logger::new())),
115 }
116 }
117
118 pub fn fastest(&self, key: &K) -> TuneCacheResult {
120 self.cache.lock().fastest(key)
121 }
122
123 pub fn check_tune<'a, R: Runtime, F: TuneInputs, Out: AutotuneOutput>(
126 &self,
127 key: &K,
128 inputs: &F::At<'a>,
129 tunables: &TunableSet<K, F, Out>,
130 #[cfg_attr(not(std_io), allow(unused))] checksum: impl FnOnce() -> String + Send + Sync,
131 client: &ComputeClient<R>,
132 ) -> TuneCacheResult
133 where
134 <F as TuneInputs>::At<'a>: Clone + Send,
135 {
136 client.flush().expect("Cannot autotune with pre-existing stream errors");
139 {
140 let mut cache = self.cache.lock();
141 let cur = cache.fastest(key);
142
143 #[cfg(std_io)]
144 let cur = if matches!(cur, TuneCacheResult::Unchecked) {
145 let mut log = self.logger.lock();
146 let checksum = checksum();
147 if let AutotuneLogLevel::Full = log.log_level_autotune() {
148 log.log_autotune(&format!("validate checksum key={key}, checksum={checksum}"));
149 }
150 cache.validate_checksum(key, &checksum)
151 } else {
152 cur
153 };
154
155 match cur {
156 TuneCacheResult::Hit { .. } | TuneCacheResult::Pending => return cur,
157 TuneCacheResult::Miss | TuneCacheResult::Unchecked => {
158 cache.mark_pending(key.clone())
159 }
160 }
161 }
164
165 log::info!("Tuning {key}");
166
167 let autotunables = tunables.autotunables().collect::<Vec<_>>();
168 let mut results: Vec<AutotuneResult> = autotunables
169 .iter()
170 .map(|a| {
171 AutotuneResult::error(AutotuneError::Skip {
172 name: a.name.to_string(),
173 })
174 })
175 .collect();
176
177 #[cfg(std_io)]
178 let checksum = tunables.compute_checksum();
179
180 if results.len() == 1 {
182 self.cache.lock().cache_insert(key.clone(), 0);
183 return TuneCacheResult::Hit { fastest_index: 0 };
184 }
185
186 let test_inputs = tunables.generate_inputs(key, inputs);
187 client.flush().expect("Autotune input generation failed");
188 let mut plan = tunables.plan(key);
189 let mut context_logs = match self.logger.lock().log_level_autotune() {
190 AutotuneLogLevel::Full => Some(String::new()),
191 _ => None,
192 };
193
194 let mut pending = Vec::<PendingBench>::new();
199 loop {
200 let tunable_indices = plan.next(context_logs.as_mut());
201
202 if tunable_indices.is_empty() {
203 panic!(
204 "Can't execute the autotune plan for key: {key:?}\n - plan: {plan:?}\n - results: {results:?}"
205 );
206 }
207
208 for index in tunable_indices {
209 let op = autotunables[index];
210
211 match tune_benchmark(op, test_inputs.clone(), client.clone()) {
212 Ok(profiles) => pending.push(PendingBench {
213 index,
214 name: op.name.clone(),
215 profiles,
216 }),
217 Err(err) => {
218 results[index] = AutotuneResult::error(err);
219 }
220 }
221 }
222
223 if !pending.is_empty() {
224 break;
225 }
226 }
227
228 let request = TuneRequest {
229 key: key.clone(),
230 results,
231 #[cfg(std_io)]
232 checksum,
233 context_logs,
234 pending,
235 };
236
237 #[cfg(target_family = "wasm")]
240 {
241 let cache = self.cache.clone();
242 let logger = self.logger.clone();
243 wasm_bindgen_futures::spawn_local(async move {
244 process_request(request, &cache, &logger).await;
245 });
246
247 return TuneCacheResult::Pending;
248 }
249
250 #[cfg(not(target_family = "wasm"))]
251 ruda_core::future::block_on(process_request(request, &self.cache, &self.logger))
252 }
253}
254
255async fn process_request<K: AutotuneKey>(
257 request: TuneRequest<K>,
258 cache: &spin::Mutex<TuneCache<K>>,
259 logger: &spin::Mutex<Logger>,
260) -> TuneCacheResult {
261 let TuneRequest {
262 key,
263 mut results,
264 #[cfg(std_io)]
265 checksum,
266 context_logs,
267 pending,
268 } = request;
269
270 for bench in pending {
271 let PendingBench {
272 index,
273 name,
274 profiles,
275 } = bench;
276
277 if profiles.is_empty() {
278 results[index] = AutotuneResult::error(AutotuneError::Unknown {
279 name: name.to_string(),
280 err: "No profiling available".to_string(),
281 });
282 continue;
283 }
284
285 let timing_method = profiles.first().unwrap().timing_method();
286 let mut durations = Vec::with_capacity(profiles.len());
287 for profile in profiles {
288 durations.push(profile.resolve().await.duration());
289 }
290
291 results[index] = AutotuneResult::success(AutotuneOutcome::new(
292 name.to_string(),
293 index,
294 BenchmarkComputations::new(&BenchmarkDurations::from_durations(
295 timing_method,
296 durations,
297 )),
298 ));
299 }
300
301 results.sort_by(|a, b| {
302 let a = a
303 .outcome
304 .as_ref()
305 .map(|r| r.computation.score())
306 .unwrap_or(u64::MAX);
307 let b = b
308 .outcome
309 .as_ref()
310 .map(|r| r.computation.score())
311 .unwrap_or(u64::MAX);
312 a.cmp(&b)
313 });
314
315 let fastest_index = results
316 .first()
317 .expect("At least one kernel needed.")
318 .outcome
319 .as_ref()
320 .expect("At least one kernel has to succeed.")
321 .index;
322
323 {
324 log_result(&mut logger.lock(), &key, &results, context_logs.as_deref());
325 cache.lock().cache_insert(key.clone(), fastest_index);
326 #[cfg(std_io)]
327 cache
328 .lock()
329 .persistent_cache_insert(key, checksum, fastest_index, results);
330 }
331
332 TuneCacheResult::Hit { fastest_index }
333}
334
335fn log_result<K: AutotuneKey>(
337 logger: &mut Logger,
338 key: &K,
339 results: &[AutotuneResult],
340 context_logs: Option<&str>,
341) {
342 match logger.log_level_autotune() {
343 AutotuneLogLevel::Minimal => {
344 let top_times = results
345 .iter()
346 .map(|r| {
347 let time = r
348 .outcome
349 .as_ref()
350 .map(|r| r.computation.median)
351 .unwrap_or(Duration::MAX);
352
353 let index = r.outcome.as_ref().map(|r| r.index).unwrap_or_default();
354 (index, time)
355 })
356 .take(3)
357 .collect::<Vec<_>>();
358
359 let result = results
360 .first()
361 .expect("At least one kernel needed.")
362 .outcome
363 .as_ref()
364 .expect("At least one kernel has to succeed.");
365
366 let context = context_logs.unwrap_or("");
367 logger.log_autotune(&format!(
368 "Fastest result {}-{key}. \n Top 3 times: {top_times:?}, context: {context}",
369 result.name,
370 ));
371 }
372 AutotuneLogLevel::Full => {
373 let result = results
374 .first()
375 .expect("At least one kernel needed.")
376 .outcome
377 .as_ref()
378 .expect("At least one kernel has to succeed.");
379
380 let context = context_logs.unwrap_or("");
381 logger.log_autotune(&format!(
382 "Fastest result {}-{key}. Context: {context}",
383 result.name,
384 ));
385
386 for result in results.iter() {
387 match &result.outcome {
388 Ok(val) => {
389 logger.log_autotune(&format!("{val}"));
390 }
391 Err(err) => logger.log_autotune(&format!("{err:?}")),
392 }
393 }
394 }
395 AutotuneLogLevel::Disabled => {}
396 }
397}
398
399#[cfg(feature = "runtime-autotune-checks")]
400pub(crate) fn check_autotune_outputs<O: AutotuneOutput>(
401 mut checks_outputs: Vec<Result<O, AutotuneError>>,
402) {
403 let reference = checks_outputs.remove(checks_outputs.len() - 1);
404
405 if let Ok(reference) = reference {
406 for other in checks_outputs.into_iter().flatten() {
407 reference.check_equivalence(other);
408 }
409 }
410}