Skip to main content

cubecl_runtime/tune/
tune_cache.rs

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)] // Some variants are not created when the cache isn't saved.
26pub(crate) enum ChecksumState {
27    Match,
28    NoMatch,
29    ToBeVerified(String),
30}
31
32/// Persistent cache key
33#[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/// Persistent cache entry
41#[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)]
50/// The result of an autotune job.
51pub 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            // We don't have to check the error
76            (Err(_), Err(_)) => true,
77        }
78    }
79}
80
81/// Use to find and reuse the best kernel for some input
82#[derive(Debug)]
83pub(crate) struct TuneCache<K> {
84    in_memory_cache: HashMap<K, CacheEntry>,
85    /// `None` when the persistent cache is disabled, so no cache file is ever touched.
86    #[cfg(std_io)]
87    persistent_cache: Option<Cache<PersistentCacheKey<K>, PersistentCacheValue>>,
88}
89
90/// Result of the cache try
91#[derive(Debug)]
92pub enum TuneCacheResult {
93    /// An operation is found.
94    Hit {
95        /// The index of the fastest operation to execute.
96        fastest_index: usize,
97    },
98    /// The operation might be cached, but we don't know yet whether the checksum is valid.
99    Unchecked,
100    /// A tuning job is in flight for this key — the worker hasn't published a result yet.
101    /// The receiver wakes (with `Err(RecvError)`) when the worker commits the result. Native
102    /// callers `block_on` it and re-query; wasm callers drop it and fall back.
103    Pending,
104    /// No operation is found yet.
105    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            // Pending: clone the receiver so the caller can subscribe to the in-flight tune.
159            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, // Don't know yet.
168                ChecksumState::NoMatch => TuneCacheResult::Miss,               // Can't use this.
169                ChecksumState::Match => TuneCacheResult::Hit {
170                    fastest_index: *fastest_index,
171                },
172            }
173        } else {
174            // Clippy;
175            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    /// Mark a key as being tuned. Used by [`Tuner::tune`] under the cache mutex so that
205    /// concurrent callers see [`TuneCacheResult::Pending`] and wait on the same job instead of
206    /// starting a second one. Returns `(Sender, Receiver)`:
207    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                    // This is OK.
254                }
255            }
256        }
257        // .expect();
258    }
259
260    /// Load the persistent cache data from disk
261    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}