1#[cfg(persistence)]
2use alloc::vec::Vec;
3
4#[cfg(persistence)]
5use cubecl_environment::persistence::StoreError;
6#[cfg(persistence)]
7use cubecl_environment::persistence::{CacheOption, Namespace, Store, StoreOptions};
8#[cfg(serializable)]
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(persistence)]
34#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)]
35pub struct PersistentCacheKey<K> {
36 pub key: K,
38 pub checksum: String,
41}
42
43#[cfg(persistence)]
51#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
52pub struct PersistentCacheValue {
53 pub fastest_index: usize,
55 pub results: Vec<AutotuneResult>,
57 #[serde(default)]
62 pub bounds: Option<crate::tune::Bounds>,
63 #[serde(default)]
67 pub limit: Option<core::time::Duration>,
68}
69
70#[cfg_attr(serializable, derive(Serialize, Deserialize))]
71#[derive(Debug, Clone)]
72pub struct AutotuneResult {
74 pub outcome: Result<AutotuneOutcome, AutotuneError>,
76}
77
78impl AutotuneResult {
79 pub(crate) fn error(error: AutotuneError) -> Self {
80 Self {
81 outcome: Err(error),
82 }
83 }
84 pub(crate) fn success(outcome: AutotuneOutcome) -> Self {
85 Self {
86 outcome: Ok(outcome),
87 }
88 }
89}
90
91impl Eq for AutotuneResult {}
92impl PartialEq for AutotuneResult {
93 fn eq(&self, other: &Self) -> bool {
94 match (&self.outcome, &other.outcome) {
95 (Ok(lhs), Ok(rhs)) => lhs == rhs,
96 (Ok(_), Err(_)) => false,
97 (Err(_), Ok(_)) => false,
98 (Err(_), Err(_)) => true,
100 }
101 }
102}
103
104#[derive(Debug)]
106pub(crate) struct TuneCache<K> {
107 in_memory_cache: HashMap<K, CacheEntry>,
112 #[cfg(persistence)]
116 persistent_cache: Option<Store<PersistentCacheKey<K>, PersistentCacheValue>>,
117 #[cfg(persistence)]
120 table: String,
121 #[cfg(persistence)]
126 hydrated: bool,
127 #[cfg(persistence)]
130 generation: u32,
131}
132
133#[derive(Debug)]
135pub enum TuneCacheResult {
136 Hit {
138 fastest_index: usize,
140 },
141 Unchecked,
143 Pending,
147 Miss,
149}
150
151impl<K: AutotuneKey> TuneCache<K> {
152 pub(crate) fn new(
153 #[cfg_attr(not(persistence), allow(unused_variables))] name: &str,
154 #[cfg_attr(not(persistence), allow(unused_variables))] device_id: &str,
155 ) -> Self {
156 #[cfg(persistence)]
157 {
158 use crate::config::RuntimeConfig;
159 use alloc::format;
160
161 let config = crate::config::CubeClRuntimeConfig::get();
162 let namespace = Namespace::scoped("autotune", format!("{device_id}/{name}"));
163 let table = namespace.as_str().into();
164
165 if config.autotune.disable_cache {
166 return TuneCache {
167 in_memory_cache: HashMap::new(),
168 persistent_cache: None,
169 table,
170 hydrated: true,
171 generation: cubecl_environment::environment::generation(),
172 };
173 }
174
175 let generation = cubecl_environment::environment::generation();
179 let mut cache = TuneCache {
180 in_memory_cache: HashMap::new(),
181 persistent_cache: Some(Store::new(
182 StoreOptions::new()
183 .storage(namespace)
184 .cache(CacheOption::Lazy),
185 )),
186 table,
187 hydrated: false,
188 generation,
189 };
190 log::info!("Load autotune cache ...");
191 let loaded = cache.sync_persistent();
192 log::info!("Loaded {loaded} autotune cached entries");
193
194 cache
195 }
196
197 #[cfg(not(persistence))]
198 {
199 TuneCache {
200 in_memory_cache: HashMap::new(),
201 }
202 }
203 }
204
205 pub fn fastest(&self, key: &K) -> TuneCacheResult {
206 let Some(val) = self.in_memory_cache.get(key) else {
207 return TuneCacheResult::Miss;
208 };
209
210 let CacheEntry::Done {
211 checksum,
212 fastest_index,
213 } = val
214 else {
215 let CacheEntry::Pending = val else {
217 unreachable!()
218 };
219 return TuneCacheResult::Pending;
220 };
221
222 if cfg!(persistence) {
223 match checksum {
224 ChecksumState::ToBeVerified(..) => TuneCacheResult::Unchecked, ChecksumState::NoMatch => TuneCacheResult::Miss, ChecksumState::Match => TuneCacheResult::Hit {
227 fastest_index: *fastest_index,
228 },
229 }
230 } else {
231 let _ = checksum;
233 TuneCacheResult::Hit {
234 fastest_index: *fastest_index,
235 }
236 }
237 }
238
239 #[cfg(persistence)]
240 pub fn validate_checksum(&mut self, key: &K, checksum: &str) -> TuneCacheResult {
241 let Some(val) = self.in_memory_cache.get_mut(key) else {
242 return TuneCacheResult::Miss;
243 };
244
245 if let CacheEntry::Done {
246 checksum: checksum_state,
247 ..
248 } = val
249 && let ChecksumState::ToBeVerified(checksum_expected) = checksum_state
250 {
251 if checksum_expected == checksum {
252 *checksum_state = ChecksumState::Match;
253 } else {
254 *checksum_state = ChecksumState::NoMatch;
255 }
256 }
257
258 self.fastest(key)
259 }
260
261 pub(crate) fn mark_pending(&mut self, key: K) {
265 self.in_memory_cache.insert(key, CacheEntry::Pending);
266 }
267
268 pub(crate) fn cache_insert(&mut self, key: K, fastest_index: usize) {
269 self.in_memory_cache.insert(
270 key,
271 CacheEntry::Done {
272 checksum: ChecksumState::Match,
273 fastest_index,
274 },
275 );
276 }
277}
278
279#[cfg(persistence)]
280impl<K: AutotuneKey> TuneCache<K> {
281 pub(crate) fn reset_if_environment_switched(&mut self) {
289 if self.persistent_cache.is_none() {
292 return;
293 }
294
295 let generation = cubecl_environment::environment::generation();
296 if generation == self.generation {
297 return;
298 }
299
300 log::debug!("Environment switched, resetting the autotune cache");
301 self.generation = generation;
302 self.in_memory_cache.clear();
303 self.hydrated = false;
304 }
305
306 pub(crate) fn sync_persistent(&mut self) -> usize {
316 if self.hydrated {
317 return 0;
318 }
319
320 let Some(persistent_cache) = self.persistent_cache.as_mut() else {
321 return 0;
322 };
323
324 let mut delivered = 0;
325 persistent_cache.scan(|key, value| {
326 delivered += 1;
327 self.in_memory_cache
328 .entry(key.key)
329 .or_insert(CacheEntry::Done {
330 checksum: ChecksumState::ToBeVerified(key.checksum),
331 fastest_index: value.fastest_index,
332 });
333 });
334 self.hydrated = true;
335
336 delivered
337 }
338
339 pub(crate) fn table(&self) -> &str {
341 &self.table
342 }
343
344 pub(crate) fn persistent_cache_insert(
347 &mut self,
348 key: K,
349 checksum: String,
350 value: PersistentCacheValue,
351 ) -> bool {
352 let Some(persistent_cache) = self.persistent_cache.as_mut() else {
353 return false;
354 };
355
356 let Err(err) = persistent_cache.insert(PersistentCacheKey { key, checksum }, value) else {
357 return true;
358 };
359 match err {
360 StoreError::DuplicatedKey {
361 key,
362 value_previous,
363 value_updated,
364 } => log::warn!(
365 "Autotune the same function multiple times for key {key:?} => old {value_previous:?}, new {value_updated:?}"
366 ),
367 StoreError::KeyOutOfSync { key, .. } => {
372 log::debug!("Autotune result for key {key:?} was already stored concurrently")
373 }
374 StoreError::Backend { key, error } => log::warn!(
375 "Autotune result for key {key:?} could not be stored, it will be retuned: {error}"
376 ),
377 }
378 false
379 }
380}