1use alloc::format;
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4use cubecl_common::profile::ProfileDuration;
5use derive_more::Display;
6
7use core::time::Duration;
8
9use alloc::string::{String, ToString};
10use cubecl_common::benchmark::{BenchmarkComputations, BenchmarkDurations};
11
12use crate::config::{Logger, autotune::AutotuneLogLevel};
13use crate::server::LaunchError;
14use crate::tune::{AutotuneResult, TimeBound, TuneCache, tune_benchmark};
15use crate::{client::ComputeClient, runtime::Runtime};
16
17use super::{AutotuneKey, AutotuneOutput, TunableSet, TuneCacheResult, TuneInputs};
18
19#[derive(Debug)]
20pub struct Tuner<K: AutotuneKey> {
26 cache: Arc<spin::Mutex<TuneCache<K>>>,
27 logger: Arc<spin::Mutex<Logger>>,
28}
29
30#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
32#[derive(new, Debug, Clone, PartialEq, Eq)]
33pub struct AutotuneOutcome {
34 name: String,
35 index: usize,
36 computation: BenchmarkComputations,
37}
38
39impl core::fmt::Display for AutotuneOutcome {
40 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
41 write!(
42 f,
43 "Autotune[{}] name {} => {:?}",
44 self.index, self.name, self.computation
45 )
46 }
47}
48
49#[derive(Clone, Display)]
51#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
52pub enum AutotuneError {
53 #[display("{name}: An unknown error happened.\n{err}")]
55 Unknown {
56 name: String,
58 err: String,
60 },
61 #[display("{name}: All samples are invalid.")]
63 InvalidSamples {
64 name: String,
66 },
67 #[display("No autotune was flagged as valid for the problem.\n{context}")]
73 NoValidKernelFound {
74 context: String,
76 },
77 #[display("{name}: The autotune is skipped manually.")]
79 Skip {
80 name: String,
82 },
83
84 Launch(LaunchError),
86}
87
88impl core::fmt::Debug for AutotuneError {
89 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
90 write!(f, "{self}")
91 }
92}
93
94impl From<LaunchError> for AutotuneError {
95 fn from(value: LaunchError) -> Self {
96 Self::Launch(value)
97 }
98}
99
100struct PendingBench {
102 index: usize,
103 name: String,
104 profiles: Vec<ProfileDuration>,
105}
106
107struct TuneRequest<K: AutotuneKey> {
110 key: K,
111 results: Vec<AutotuneResult>,
112 #[cfg(std_io)]
113 checksum: String,
114 context_logs: Option<String>,
115 pending: Vec<PendingBench>,
116}
117
118#[allow(clippy::new_without_default)]
119impl<K: AutotuneKey> Tuner<K> {
120 pub fn new(name: &str, device_id: &str) -> Self {
123 Self {
124 cache: Arc::new(spin::Mutex::new(TuneCache::new(name, device_id))),
125 logger: Arc::new(spin::Mutex::new(Logger::new())),
126 }
127 }
128
129 pub fn fastest(&self, key: &K) -> TuneCacheResult {
131 self.cache.lock().fastest(key)
132 }
133
134 pub fn check_tune<'a, R: Runtime, F: TuneInputs, Out: AutotuneOutput>(
137 &self,
138 key: &K,
139 inputs: &F::At<'a>,
140 tunables: &TunableSet<K, F, Out>,
141 #[cfg_attr(not(std_io), allow(unused))] checksum: impl FnOnce() -> String + Send + Sync,
142 client: &ComputeClient<R>,
143 ) -> TuneCacheResult
144 where
145 <F as TuneInputs>::At<'a>: Clone + Send,
146 {
147 {
148 let mut cache = self.cache.lock();
149 let cur = cache.fastest(key);
150
151 #[cfg(std_io)]
152 let cur = if matches!(cur, TuneCacheResult::Unchecked) {
153 let mut log = self.logger.lock();
154 let checksum = checksum();
155 if let AutotuneLogLevel::Full = log.log_level_autotune() {
156 log.log_autotune(&format!("validate checksum key={key}, checksum={checksum}"));
157 }
158 cache.validate_checksum(key, &checksum)
159 } else {
160 cur
161 };
162
163 match cur {
164 TuneCacheResult::Hit { .. } | TuneCacheResult::Pending => return cur,
165 TuneCacheResult::Miss | TuneCacheResult::Unchecked => {
166 cache.mark_pending(key.clone())
167 }
168 }
169 }
172
173 log::info!("Tuning {key}");
174
175 let autotunables = tunables.autotunables().collect::<Vec<_>>();
176 let mut results: Vec<AutotuneResult> = autotunables
177 .iter()
178 .map(|a| {
179 AutotuneResult::error(AutotuneError::Skip {
180 name: a.name.to_string(),
181 })
182 })
183 .collect();
184
185 #[cfg(std_io)]
186 let checksum = tunables.compute_checksum();
187
188 if results.len() == 1 {
190 self.cache.lock().cache_insert(key.clone(), 0);
191 return TuneCacheResult::Hit { fastest_index: 0 };
192 }
193
194 let test_inputs = tunables.generate_inputs(key, inputs);
195 let mut plan = tunables.plan(key);
196 let mut context_logs = match self.logger.lock().log_level_autotune() {
197 AutotuneLogLevel::Full => Some(String::new()),
198 _ => None,
199 };
200
201 #[cfg(not(target_family = "wasm"))]
202 let threshold_limit = tunables
203 .bounds(key, inputs)
204 .map(|bounds| bounds.time_limit())
205 .unwrap_or_default();
206
207 #[cfg(not(target_family = "wasm"))]
211 let mut batch_success = false;
212 #[cfg(target_family = "wasm")]
213 let batch_success = false;
214
215 let mut pending = Vec::<PendingBench>::new();
220 loop {
221 let tunable_indices = plan.next(context_logs.as_mut());
222
223 if tunable_indices.is_empty() {
224 panic!(
225 "Can't execute the autotune plan for key: {key:?}\n - plan: {plan:?}\n - results: {results:?}"
226 );
227 }
228
229 for index in tunable_indices {
230 let op = autotunables[index];
231
232 match tune_benchmark(op, test_inputs.clone(), client.clone()) {
233 Ok(profiles) => {
234 let bench = PendingBench {
235 index,
236 name: op.name.clone(),
237 profiles,
238 };
239
240 #[cfg(not(target_family = "wasm"))]
241 if let Some(limit) = threshold_limit {
242 let result = cubecl_common::future::block_on(resolve_bench(bench));
243 let close_enough = result
244 .outcome
245 .as_ref()
246 .is_ok_and(|outcome| outcome.computation.median <= limit);
247
248 batch_success |= result.outcome.is_ok();
249 results[index] = result;
250
251 if close_enough {
252 break;
253 }
254
255 continue;
256 }
257
258 pending.push(bench);
259 }
260 Err(err) => {
261 results[index] = AutotuneResult::error(err);
262 }
263 }
264 }
265
266 if !pending.is_empty() || (cfg!(not(target_family = "wasm")) && batch_success) {
267 break;
268 }
269 }
270
271 let request = TuneRequest {
272 key: key.clone(),
273 results,
274 #[cfg(std_io)]
275 checksum,
276 context_logs,
277 pending,
278 };
279
280 #[cfg(target_family = "wasm")]
283 {
284 let cache = self.cache.clone();
285 let logger = self.logger.clone();
286 wasm_bindgen_futures::spawn_local(async move {
287 process_request(request, &cache, &logger).await;
288 });
289
290 return TuneCacheResult::Pending;
291 }
292
293 #[cfg(not(target_family = "wasm"))]
294 cubecl_common::future::block_on(process_request(request, &self.cache, &self.logger))
295 }
296}
297
298async fn resolve_bench(bench: PendingBench) -> AutotuneResult {
304 let PendingBench {
305 index,
306 name,
307 profiles,
308 } = bench;
309
310 let Some(first) = profiles.first() else {
311 return AutotuneResult::error(AutotuneError::Unknown {
312 name: name.to_string(),
313 err: "No profiling available".to_string(),
314 });
315 };
316 let timing_method = first.timing_method();
317
318 let durations: Vec<Duration> =
319 futures_util::future::join_all(profiles.into_iter().map(ProfileDuration::resolve))
320 .await
321 .into_iter()
322 .map(|ticks| ticks.duration())
323 .collect();
324
325 AutotuneResult::success(AutotuneOutcome::new(
326 name,
327 index,
328 BenchmarkComputations::new(&BenchmarkDurations::from_durations(
329 timing_method,
330 durations,
331 )),
332 ))
333}
334
335async fn process_request<K: AutotuneKey>(
337 request: TuneRequest<K>,
338 cache: &spin::Mutex<TuneCache<K>>,
339 logger: &spin::Mutex<Logger>,
340) -> TuneCacheResult {
341 let TuneRequest {
342 key,
343 mut results,
344 #[cfg(std_io)]
345 checksum,
346 context_logs,
347 pending,
348 } = request;
349
350 for bench in pending {
351 let index = bench.index;
352 results[index] = resolve_bench(bench).await;
353 }
354
355 results.sort_by(|a, b| {
356 let a = a
357 .outcome
358 .as_ref()
359 .map(|r| r.computation.score())
360 .unwrap_or(u64::MAX);
361 let b = b
362 .outcome
363 .as_ref()
364 .map(|r| r.computation.score())
365 .unwrap_or(u64::MAX);
366 a.cmp(&b)
367 });
368
369 let fastest_index = results
370 .first()
371 .expect("At least one kernel needed.")
372 .outcome
373 .as_ref()
374 .expect("At least one kernel has to succeed.")
375 .index;
376
377 {
378 log_result(&mut logger.lock(), &key, &results, context_logs.as_deref());
379 cache.lock().cache_insert(key.clone(), fastest_index);
380 #[cfg(std_io)]
381 cache
382 .lock()
383 .persistent_cache_insert(key, checksum, fastest_index, results);
384 }
385
386 TuneCacheResult::Hit { fastest_index }
387}
388
389fn log_result<K: AutotuneKey>(
391 logger: &mut Logger,
392 key: &K,
393 results: &[AutotuneResult],
394 context_logs: Option<&str>,
395) {
396 match logger.log_level_autotune() {
397 AutotuneLogLevel::Minimal => {
398 let top_times = results
399 .iter()
400 .map(|r| {
401 let time = r
402 .outcome
403 .as_ref()
404 .map(|r| r.computation.median)
405 .unwrap_or(Duration::MAX);
406
407 let index = r.outcome.as_ref().map(|r| r.index).unwrap_or_default();
408 (index, time)
409 })
410 .take(3)
411 .collect::<Vec<_>>();
412
413 let result = results
414 .first()
415 .expect("At least one kernel needed.")
416 .outcome
417 .as_ref()
418 .expect("At least one kernel has to succeed.");
419
420 let context = context_logs.unwrap_or("");
421 logger.log_autotune(&format!(
422 "Fastest result {}-{key}. \n Top 3 times: {top_times:?}, context: {context}",
423 result.name,
424 ));
425 }
426 AutotuneLogLevel::Full => {
427 let result = results
428 .first()
429 .expect("At least one kernel needed.")
430 .outcome
431 .as_ref()
432 .expect("At least one kernel has to succeed.");
433
434 let context = context_logs.unwrap_or("");
435 logger.log_autotune(&format!(
436 "Fastest result {}-{key}. Context: {context}",
437 result.name,
438 ));
439
440 for result in results.iter() {
441 match &result.outcome {
442 Ok(val) => {
443 logger.log_autotune(&format!("{val}"));
444 }
445 Err(err) => logger.log_autotune(&format!("{err}")),
446 }
447 }
448 }
449 AutotuneLogLevel::Disabled => {}
450 }
451}
452
453#[cfg(feature = "autotune-checks")]
454pub(crate) fn check_autotune_outputs<O: AutotuneOutput>(
455 mut checks_outputs: Vec<Result<O, AutotuneError>>,
456) {
457 let reference = checks_outputs.remove(checks_outputs.len() - 1);
458
459 if let Ok(reference) = reference {
460 for other in checks_outputs.into_iter().flatten() {
461 reference.check_equivalence(other);
462 }
463 }
464}