1#[cfg(autotune_persistence)]
2use alloc::vec::Vec;
3
4#[cfg(autotune_persistence)]
5use cubecl_environment::persistence::StoreError;
6#[cfg(autotune_persistence)]
7use cubecl_environment::persistence::{CacheOption, Namespace, Store, StoreOptions};
8#[cfg(autotune_persistence)]
9use serde::{Deserialize, Serialize};
10
11use super::{AutotuneError, AutotuneKey, AutotuneOutcome};
12use alloc::string::String;
13use cubecl_environment::collections::HashMap;
14
15#[derive(Debug)]
16pub(crate) enum CacheEntry {
17 Done {
18 checksum: ChecksumState,
19 fastest_index: usize,
20 },
21 Pending,
22}
23
24#[derive(Debug)]
25#[allow(dead_code)] pub(crate) enum ChecksumState {
27 Match,
28 NoMatch,
29 ToBeVerified(String),
30}
31
32#[cfg(autotune_persistence)]
34#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)]
35pub struct PersistentCacheKey<K> {
36 pub key: K,
38 checksum: String,
39}
40
41#[cfg(autotune_persistence)]
49#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
50pub struct PersistentCacheValue {
51 pub fastest_index: usize,
53 pub results: Vec<AutotuneResult>,
55 #[serde(default)]
60 pub bounds: Option<crate::tune::Bounds>,
61 #[serde(default)]
65 pub limit: Option<core::time::Duration>,
66}
67
68#[cfg_attr(autotune_persistence, derive(Serialize, Deserialize))]
69#[derive(Debug, Clone)]
70pub struct AutotuneResult {
72 pub outcome: Result<AutotuneOutcome, AutotuneError>,
74}
75
76impl AutotuneResult {
77 pub(crate) fn error(error: AutotuneError) -> Self {
78 Self {
79 outcome: Err(error),
80 }
81 }
82 pub(crate) fn success(outcome: AutotuneOutcome) -> Self {
83 Self {
84 outcome: Ok(outcome),
85 }
86 }
87}
88
89impl Eq for AutotuneResult {}
90impl PartialEq for AutotuneResult {
91 fn eq(&self, other: &Self) -> bool {
92 match (&self.outcome, &other.outcome) {
93 (Ok(lhs), Ok(rhs)) => lhs == rhs,
94 (Ok(_), Err(_)) => false,
95 (Err(_), Ok(_)) => false,
96 (Err(_), Err(_)) => true,
98 }
99 }
100}
101
102#[derive(Debug)]
104pub(crate) struct TuneCache<K> {
105 in_memory_cache: HashMap<K, CacheEntry>,
110 #[cfg(autotune_persistence)]
114 persistent_cache: Option<Store<PersistentCacheKey<K>, PersistentCacheValue>>,
115 #[cfg(autotune_persistence)]
120 hydrated: bool,
121 #[cfg(autotune_persistence)]
124 generation: u32,
125}
126
127#[derive(Debug)]
129pub enum TuneCacheResult {
130 Hit {
132 fastest_index: usize,
134 },
135 Unchecked,
137 Pending,
141 Miss,
143}
144
145impl<K: AutotuneKey> TuneCache<K> {
146 pub(crate) fn new(
147 #[cfg_attr(not(autotune_persistence), allow(unused_variables))] name: &str,
148 #[cfg_attr(not(autotune_persistence), allow(unused_variables))] device_id: &str,
149 ) -> Self {
150 #[cfg(autotune_persistence)]
151 {
152 use crate::config::RuntimeConfig;
153 use alloc::format;
154
155 let config = crate::config::CubeClRuntimeConfig::get();
156
157 if config.autotune.disable_cache {
158 return TuneCache {
159 in_memory_cache: HashMap::new(),
160 persistent_cache: None,
161 hydrated: true,
162 generation: cubecl_environment::environment::generation(),
163 };
164 }
165
166 let generation = cubecl_environment::environment::generation();
170 let namespace = Namespace::scoped("autotune", format!("{device_id}/{name}"));
171 let mut cache = TuneCache {
172 in_memory_cache: HashMap::new(),
173 persistent_cache: Some(Store::new(
174 StoreOptions::new()
175 .storage(namespace)
176 .cache(CacheOption::Lazy),
177 )),
178 hydrated: false,
179 generation,
180 };
181 log::info!("Load autotune cache ...");
182 let loaded = cache.sync_persistent();
183 log::info!("Loaded {loaded} autotune cached entries");
184
185 cache
186 }
187
188 #[cfg(not(autotune_persistence))]
189 {
190 TuneCache {
191 in_memory_cache: HashMap::new(),
192 }
193 }
194 }
195
196 pub fn fastest(&self, key: &K) -> TuneCacheResult {
197 let Some(val) = self.in_memory_cache.get(key) else {
198 return TuneCacheResult::Miss;
199 };
200
201 let CacheEntry::Done {
202 checksum,
203 fastest_index,
204 } = val
205 else {
206 let CacheEntry::Pending = val else {
208 unreachable!()
209 };
210 return TuneCacheResult::Pending;
211 };
212
213 if cfg!(autotune_persistence) {
214 match checksum {
215 ChecksumState::ToBeVerified(..) => TuneCacheResult::Unchecked, ChecksumState::NoMatch => TuneCacheResult::Miss, ChecksumState::Match => TuneCacheResult::Hit {
218 fastest_index: *fastest_index,
219 },
220 }
221 } else {
222 let _ = checksum;
224 TuneCacheResult::Hit {
225 fastest_index: *fastest_index,
226 }
227 }
228 }
229
230 #[cfg(autotune_persistence)]
231 pub fn validate_checksum(&mut self, key: &K, checksum: &str) -> TuneCacheResult {
232 let Some(val) = self.in_memory_cache.get_mut(key) else {
233 return TuneCacheResult::Miss;
234 };
235
236 if let CacheEntry::Done {
237 checksum: checksum_state,
238 ..
239 } = val
240 && let ChecksumState::ToBeVerified(checksum_expected) = checksum_state
241 {
242 if checksum_expected == checksum {
243 *checksum_state = ChecksumState::Match;
244 } else {
245 *checksum_state = ChecksumState::NoMatch;
246 }
247 }
248
249 self.fastest(key)
250 }
251
252 pub(crate) fn mark_pending(&mut self, key: K) {
256 self.in_memory_cache.insert(key, CacheEntry::Pending);
257 }
258
259 pub(crate) fn cache_insert(&mut self, key: K, fastest_index: usize) {
260 self.in_memory_cache.insert(
261 key,
262 CacheEntry::Done {
263 checksum: ChecksumState::Match,
264 fastest_index,
265 },
266 );
267 }
268}
269
270#[cfg(autotune_persistence)]
271impl<K: AutotuneKey> TuneCache<K> {
272 pub(crate) fn reset_if_environment_switched(&mut self) {
280 if self.persistent_cache.is_none() {
283 return;
284 }
285
286 let generation = cubecl_environment::environment::generation();
287 if generation == self.generation {
288 return;
289 }
290
291 log::debug!("Environment switched, resetting the autotune cache");
292 self.generation = generation;
293 self.in_memory_cache.clear();
294 self.hydrated = false;
295 }
296
297 pub(crate) fn sync_persistent(&mut self) -> usize {
308 if self.hydrated {
309 return 0;
310 }
311
312 let Some(persistent_cache) = self.persistent_cache.as_mut() else {
313 return 0;
314 };
315
316 let mut delivered = 0;
317 let complete = persistent_cache.scan(|key, value| {
318 delivered += 1;
319 self.in_memory_cache
320 .entry(key.key)
321 .or_insert(CacheEntry::Done {
322 checksum: ChecksumState::ToBeVerified(key.checksum),
323 fastest_index: value.fastest_index,
324 });
325 });
326 self.hydrated = complete;
327
328 delivered
329 }
330
331 pub(crate) fn persistent_cache_insert(
332 &mut self,
333 key: K,
334 checksum: String,
335 value: PersistentCacheValue,
336 ) {
337 let Some(persistent_cache) = self.persistent_cache.as_mut() else {
338 return;
339 };
340
341 if let Err(err) = persistent_cache.insert(PersistentCacheKey { key, checksum }, value) {
342 match err {
343 StoreError::DuplicatedKey {
344 key,
345 value_previous,
346 value_updated,
347 } => log::warn!(
348 "Autotune the same function multiple times for key {key:?} => old {value_previous:?}, new {value_updated:?}"
349 ),
350 StoreError::KeyOutOfSync { key, .. } => {
355 log::debug!("Autotune result for key {key:?} was already stored concurrently")
356 }
357 StoreError::Backend { key, error } => log::warn!(
358 "Autotune result for key {key:?} could not be stored, it will be retuned: {error}"
359 ),
360 }
361 }
362 }
363}