1#[cfg(std_io)]
2use std::vec::Vec;
3
4#[cfg(std_io)]
5use cubecl_common::cache::Cache;
6#[cfg(std_io)]
7use cubecl_common::cache::CacheError;
8#[cfg(std_io)]
9use serde::{Deserialize, Serialize};
10
11use super::{AutotuneError, AutotuneKey, AutotuneOutcome};
12use alloc::string::String;
13use hashbrown::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(std_io)]
34#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)]
35pub(crate) struct PersistentCacheKey<K> {
36 key: K,
37 checksum: String,
38}
39
40#[cfg(std_io)]
42#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
43pub(crate) struct PersistentCacheValue {
44 fastest_index: usize,
45 results: Vec<AutotuneResult>,
46}
47
48#[cfg_attr(std_io, derive(Serialize, Deserialize))]
49#[derive(Debug, Clone)]
50pub struct AutotuneResult {
52 pub(crate) outcome: Result<AutotuneOutcome, AutotuneError>,
53}
54
55impl AutotuneResult {
56 pub(crate) fn error(error: AutotuneError) -> Self {
57 Self {
58 outcome: Err(error),
59 }
60 }
61 pub(crate) fn success(outcome: AutotuneOutcome) -> Self {
62 Self {
63 outcome: Ok(outcome),
64 }
65 }
66}
67
68impl Eq for AutotuneResult {}
69impl PartialEq for AutotuneResult {
70 fn eq(&self, other: &Self) -> bool {
71 match (&self.outcome, &other.outcome) {
72 (Ok(lhs), Ok(rhs)) => lhs == rhs,
73 (Ok(_), Err(_)) => false,
74 (Err(_), Ok(_)) => false,
75 (Err(_), Err(_)) => true,
77 }
78 }
79}
80
81#[derive(Debug)]
83pub(crate) struct TuneCache<K> {
84 in_memory_cache: HashMap<K, CacheEntry>,
85 #[cfg(std_io)]
87 persistent_cache: Option<Cache<PersistentCacheKey<K>, PersistentCacheValue>>,
88}
89
90#[derive(Debug)]
92pub enum TuneCacheResult {
93 Hit {
95 fastest_index: usize,
97 },
98 Unchecked,
100 Pending,
104 Miss,
106}
107
108impl<K: AutotuneKey> TuneCache<K> {
109 pub(crate) fn new(
110 #[cfg_attr(not(std_io), allow(unused_variables))] name: &str,
111 #[cfg_attr(not(std_io), allow(unused_variables))] device_id: &str,
112 ) -> Self {
113 #[cfg(std_io)]
114 {
115 use crate::config::RuntimeConfig;
116 use std::format;
117
118 let config = crate::config::CubeClRuntimeConfig::get();
119
120 if config.autotune.disable_cache {
121 return TuneCache {
122 in_memory_cache: HashMap::new(),
123 persistent_cache: None,
124 };
125 }
126
127 let root = config.autotune.cache.root();
128 let options = cubecl_common::cache::CacheOption::default();
129 let mut cache = TuneCache {
130 in_memory_cache: HashMap::new(),
131 persistent_cache: Some(Cache::new(
132 format!("{device_id}/{name}"),
133 options.root(root).name("autotune"),
134 )),
135 };
136 cache.load();
137 cache
138 }
139
140 #[cfg(not(std_io))]
141 {
142 TuneCache {
143 in_memory_cache: HashMap::new(),
144 }
145 }
146 }
147
148 pub fn fastest(&self, key: &K) -> TuneCacheResult {
149 let Some(val) = self.in_memory_cache.get(key) else {
150 return TuneCacheResult::Miss;
151 };
152
153 let CacheEntry::Done {
154 checksum,
155 fastest_index,
156 } = val
157 else {
158 let CacheEntry::Pending = val else {
160 unreachable!()
161 };
162 return TuneCacheResult::Pending;
163 };
164
165 if cfg!(std_io) {
166 match checksum {
167 ChecksumState::ToBeVerified(..) => TuneCacheResult::Unchecked, ChecksumState::NoMatch => TuneCacheResult::Miss, ChecksumState::Match => TuneCacheResult::Hit {
170 fastest_index: *fastest_index,
171 },
172 }
173 } else {
174 let _ = checksum;
176 TuneCacheResult::Hit {
177 fastest_index: *fastest_index,
178 }
179 }
180 }
181
182 #[cfg(std_io)]
183 pub fn validate_checksum(&mut self, key: &K, checksum: &str) -> TuneCacheResult {
184 let Some(val) = self.in_memory_cache.get_mut(key) else {
185 return TuneCacheResult::Miss;
186 };
187
188 if let CacheEntry::Done {
189 checksum: checksum_state,
190 ..
191 } = val
192 && let ChecksumState::ToBeVerified(checksum_expected) = checksum_state
193 {
194 if checksum_expected == checksum {
195 *checksum_state = ChecksumState::Match;
196 } else {
197 *checksum_state = ChecksumState::NoMatch;
198 }
199 }
200
201 self.fastest(key)
202 }
203
204 pub(crate) fn mark_pending(&mut self, key: K) {
208 self.in_memory_cache.insert(key, CacheEntry::Pending);
209 }
210
211 pub(crate) fn cache_insert(&mut self, key: K, fastest_index: usize) {
212 self.in_memory_cache.insert(
213 key,
214 CacheEntry::Done {
215 checksum: ChecksumState::Match,
216 fastest_index,
217 },
218 );
219 }
220}
221
222#[cfg(std_io)]
223impl<K: AutotuneKey> TuneCache<K> {
224 pub(crate) fn persistent_cache_insert(
225 &mut self,
226 key: K,
227 checksum: String,
228 fastest_index: usize,
229 results: Vec<AutotuneResult>,
230 ) {
231 let Some(persistent_cache) = self.persistent_cache.as_mut() else {
232 return;
233 };
234
235 if let Err(err) = persistent_cache.insert(
236 PersistentCacheKey { key, checksum },
237 PersistentCacheValue {
238 fastest_index,
239 results,
240 },
241 ) {
242 match err {
243 CacheError::DuplicatedKey {
244 key,
245 value_previous,
246 value_updated,
247 } => {
248 log::warn!(
249 "Autotune the same function multiple times for key {key:?} => old {value_previous:?}, new {value_updated:?}"
250 );
251 }
252 CacheError::KeyOutOfSync { .. } => {
253 }
255 }
256 }
257 }
259
260 pub(crate) fn load(&mut self) {
262 let Some(persistent_cache) = self.persistent_cache.as_mut() else {
263 return;
264 };
265
266 log::info!("Load autotune cache ...");
267 let mut loaded = 0;
268 persistent_cache.for_each(|key, value| {
269 loaded += 1;
270 self.in_memory_cache.insert(
271 key.key.clone(),
272 CacheEntry::Done {
273 checksum: ChecksumState::ToBeVerified(key.checksum.clone()),
274 fastest_index: value.fastest_index,
275 },
276 );
277 });
278 log::info!("Loaded {loaded} autotune cached entries");
279 }
280}