cubecl_runtime/tune/
tune_cache.rs

1#[cfg(autotune_persistent_cache)]
2mod std_imports {
3    pub use std::fs;
4    pub use std::fs::File;
5    pub use std::io;
6    pub use std::path::Path;
7    pub use std::path::PathBuf;
8}
9
10#[cfg(autotune_persistent_cache)]
11use std_imports::*;
12
13#[cfg(autotune_persistent_cache)]
14use serde::{Deserialize, Serialize};
15
16use super::AutotuneKey;
17use hashbrown::HashMap;
18
19#[cfg(autotune_persistent_cache)]
20/// Return the file path for the persistent cache on disk
21/// prefix should be the device id computed at the backend level
22pub fn get_persistent_cache_file_path(prefix: &str) -> PathBuf {
23    let home_dir = dirs::home_dir().expect("An home directory should exist");
24    let path_dir = home_dir.join(".cache").join("cubecl").join("autotune");
25    let path = Path::new(&path_dir);
26    path.join(format!("{}-autotune-cache.json", prefix))
27}
28
29/// In-memory cache entry
30#[derive(Debug)]
31pub(crate) enum CacheEntry {
32    Done {
33        checksum_matches: Option<bool>,
34        fastest_index: usize,
35    },
36    Pending,
37}
38
39/// Persistent cache entry
40#[cfg(autotune_persistent_cache)]
41#[derive(Debug, Serialize, Deserialize)]
42pub(crate) struct PersistentCacheEntry {
43    checksum: String,
44    fastest_index: usize,
45}
46
47/// Use to find and reuse the best kernel for some input
48#[derive(Debug)]
49pub(crate) struct TuneCache<K> {
50    in_memory_cache: HashMap<K, CacheEntry>,
51    #[cfg(autotune_persistent_cache)]
52    persistent_cache: HashMap<K, PersistentCacheEntry>,
53    #[cfg(autotune_persistent_cache)]
54    device_id: String,
55    #[cfg(autotune_persistent_cache)]
56    name: String,
57}
58
59/// Result of the cache try
60#[derive(Debug)]
61pub enum TuneCacheResult {
62    /// An operation is found.
63    Hit {
64        /// The index of the fastest operation to execute.
65        fastest_index: usize,
66    },
67    /// The operation might be cached, but we don't know yet whether the checksum is valid.
68    Unchecked,
69    /// We don't know yet what is fastest, but are waiting for a result to come in.
70    Pending,
71    /// No operation is found yet.
72    Miss,
73}
74
75impl<K: AutotuneKey> TuneCache<K> {
76    pub(crate) fn new(
77        #[cfg_attr(not(autotune_persistent_cache), allow(unused_variables))] name: &str,
78        #[cfg_attr(not(autotune_persistent_cache), allow(unused_variables))] device_id: &str,
79    ) -> Self {
80        #[cfg(autotune_persistent_cache)]
81        {
82            let mut cache = TuneCache {
83                in_memory_cache: HashMap::new(),
84                persistent_cache: HashMap::new(),
85                device_id: device_id.to_string(),
86                name: name.to_string(),
87            };
88            if let Err(e) = cache.load() {
89                log::warn!(
90                    "Unable to load autotune cache. Cache will be ignored ({}).",
91                    e
92                );
93            }
94            cache
95        }
96
97        #[cfg(not(autotune_persistent_cache))]
98        {
99            TuneCache {
100                in_memory_cache: HashMap::new(),
101            }
102        }
103    }
104
105    pub fn fastest(&self, key: &K) -> TuneCacheResult {
106        let result = self.in_memory_cache.get(key);
107
108        let Some(val) = result else {
109            return TuneCacheResult::Miss;
110        };
111
112        match val {
113            CacheEntry::Done {
114                checksum_matches,
115                fastest_index,
116            } => {
117                if cfg!(autotune_persistent_cache) {
118                    match checksum_matches {
119                        None => TuneCacheResult::Unchecked,   // Don't know yet.
120                        Some(false) => TuneCacheResult::Miss, // Can't use this.
121                        Some(true) => TuneCacheResult::Hit {
122                            fastest_index: *fastest_index,
123                        },
124                    }
125                } else {
126                    let _ = checksum_matches;
127                    TuneCacheResult::Hit {
128                        fastest_index: *fastest_index,
129                    }
130                }
131            }
132            CacheEntry::Pending {} => TuneCacheResult::Pending,
133        }
134    }
135
136    #[cfg(autotune_persistent_cache)]
137    pub fn validate_checksum(&mut self, key: &K, checksum: &str) {
138        let result = self.in_memory_cache.get_mut(key);
139        let Some(val) = result else {
140            return;
141        };
142
143        if let CacheEntry::Done {
144            checksum_matches, ..
145        } = val
146        {
147            if checksum_matches.is_none() {
148                let persistent_entry = self
149                    .persistent_cache
150                    .get(key)
151                    .expect("Both caches should be in sync");
152
153                *checksum_matches = Some(checksum == persistent_entry.checksum);
154            }
155        }
156    }
157
158    pub(crate) fn mark_pending(&mut self, key: K) {
159        self.in_memory_cache.insert(key, CacheEntry::Pending);
160    }
161
162    pub(crate) fn cache_insert(&mut self, key: K, fastest_index: usize) {
163        self.in_memory_cache.insert(
164            key,
165            CacheEntry::Done {
166                checksum_matches: Some(true),
167                fastest_index,
168            },
169        );
170    }
171}
172
173#[cfg(autotune_persistent_cache)]
174impl<K: AutotuneKey> TuneCache<K> {
175    pub(crate) fn persistent_cache_insert(
176        &mut self,
177        key: K,
178        checksum: String,
179        fastest_index: usize,
180    ) {
181        self.persistent_cache.insert(
182            key,
183            PersistentCacheEntry {
184                checksum,
185                fastest_index,
186            },
187        );
188    }
189
190    /// Load the persistent cache data from disk
191    pub(crate) fn load(&mut self) -> Result<(), io::Error> {
192        let file_path = self.get_persistent_cache_file_path();
193        // note: reading file from memory is faster than using
194        // serde from_reader with a buffered reader
195        // see issue:
196        // https://github.com/serde-rs/json/issues/160
197        match fs::read_to_string(file_path) {
198            Ok(data) => {
199                let data: Vec<(K, PersistentCacheEntry)> = serde_json::from_str(&data)?;
200                for (key, value) in data.into_iter() {
201                    self.persistent_cache.insert(key, value);
202                }
203                Ok(())
204            }
205            Err(e) => {
206                if e.kind() == std::io::ErrorKind::NotFound {
207                    Ok(())
208                } else {
209                    Err(e)
210                }
211            }
212        }?;
213        for (key, entry) in self.persistent_cache.iter() {
214            self.in_memory_cache.insert(
215                key.clone(),
216                CacheEntry::Done {
217                    checksum_matches: None,
218                    fastest_index: entry.fastest_index,
219                },
220            );
221        }
222        Ok(())
223    }
224
225    /// Save the persistent cache on disk
226    pub(crate) fn save(&self) {
227        let file_path = self.get_persistent_cache_file_path();
228        if let Some(parent_dir) = file_path.parent() {
229            if !parent_dir.exists() {
230                fs::create_dir_all(parent_dir).unwrap_or_else(|_| {
231                    panic!(
232                    "Should be able to create directory '{}' for autotune persistent cache file",
233                    parent_dir.to_str().unwrap())
234                });
235            }
236        }
237        let file = File::create(file_path.clone()).unwrap_or_else(|_| {
238            panic!(
239                "Should be able to open autotune persistent cache file '{}'",
240                file_path.to_str().unwrap()
241            )
242        });
243        let data = self.persistent_cache.iter().collect::<Vec<_>>();
244        serde_json::to_writer_pretty(file, &data)
245            .expect("Should be able to write to autotune persistent cache");
246    }
247
248    /// Return the file path for the persistent cache on disk
249    pub fn get_persistent_cache_file_path(&self) -> PathBuf {
250        let name = sanitize_filename::sanitize(&self.name);
251        let device_id = sanitize_filename::sanitize(&self.device_id);
252        get_persistent_cache_file_path(&format!("{name}/{device_id}"))
253    }
254}