1use crate::id::KernelId;
4use cubecl_common::hash::{StableHash, StableHasher};
5#[cfg(compilation_cache)]
6use cubecl_environment::persistence::{CacheOption, Namespace, StoreOptions};
7use cubecl_environment::persistence::{Store, StoreKey, StoreValue};
8
9pub type BuildId = Option<&'static [u8]>;
11
12pub fn build_id_hash() -> StableHash {
14 StableHasher::hash_one(&buildid::build_id())
15}
16
17pub fn compilation_store<K: StoreKey, V: StoreValue>(
26 backend: &'static str,
27 fingerprint: impl AsRef<str>,
28) -> Option<Store<K, V>> {
29 #[cfg(compilation_cache)]
30 {
31 use crate::config::RuntimeConfig;
32
33 if !crate::config::CubeClRuntimeConfig::get().compilation.cache {
34 return None;
35 }
36
37 Some(Store::new(
38 StoreOptions::new()
39 .storage(Namespace::scoped(backend, fingerprint))
40 .cache(CacheOption::Lazy),
41 ))
42 }
43
44 #[cfg(not(compilation_cache))]
46 {
47 let _ = (backend, fingerprint);
48 None
49 }
50}
51
52pub fn store_compiled<K: StoreKey, V: StoreValue>(
60 store: &mut Store<K, V>,
61 key: K,
62 value: V,
63) -> bool {
64 match store.insert(key, value) {
65 Ok(()) => true,
66 Err(err) => {
67 log::warn!("Unable to cache the compiled kernel: {}", err.reason());
68 false
69 }
70 }
71}
72
73#[derive(
79 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
80)]
81pub struct KernelCacheKey {
82 pub id: StableHash,
84 pub build_id: StableHash,
86}
87
88impl KernelCacheKey {
89 pub fn new(id: &KernelId, build_id: StableHash) -> Self {
91 Self {
92 id: id.stable_hash(),
93 build_id,
94 }
95 }
96
97 pub fn from_artifact_id<V: core::hash::Hash>(
99 id: &super::ArtifactId<V>,
100 build_id: StableHash,
101 ) -> Self {
102 use core::hash::Hash;
103 let mut hasher = StableHasher::new();
104 id.kernel.stable_hash().hash(&mut hasher);
105 id.variant.hash(&mut hasher);
106 Self {
107 id: hasher.finalize(),
108 build_id,
109 }
110 }
111}
112
113#[derive(Debug, Clone, Copy)]
116pub struct StoreNames {
117 pub kernels: &'static str,
119 pub sources: &'static str,
121}
122
123#[derive(Debug)]
130pub struct ArtifactStore<A> {
131 by_kernel: Option<Store<KernelCacheKey, A>>,
132 by_source: Option<Store<StableHash, KernelCacheKey>>,
134 build_id: StableHash,
135}
136
137impl<A: StoreValue> ArtifactStore<A> {
138 pub fn new(backend: &'static str, fingerprint: impl AsRef<str>) -> Self {
141 Self {
142 by_kernel: compilation_store(backend, fingerprint),
143 by_source: None,
144 build_id: build_id_hash(),
145 }
146 }
147
148 pub fn with_sources(names: StoreNames, fingerprint: impl AsRef<str>) -> Self {
151 let by_kernel = compilation_store(names.kernels, fingerprint.as_ref());
152 let by_source = by_kernel
153 .is_some()
154 .then(|| compilation_store(names.sources, fingerprint))
155 .flatten();
156 Self {
157 by_kernel,
158 by_source,
159 build_id: build_id_hash(),
160 }
161 }
162
163 pub fn persists(&self) -> bool {
166 self.by_kernel.is_some()
167 }
168
169 pub fn is_enabled(&self) -> bool {
171 self.by_kernel.is_some()
172 }
173
174 pub fn take(&mut self, kernel: &KernelId) -> Option<A> {
176 let key = KernelCacheKey::new(kernel, self.build_id);
177 let artifact = self.by_kernel.as_mut()?.remove(&key)?;
178 log::trace!("Using the compilation store");
179 Some(artifact)
180 }
181
182 pub fn take_variant<V: core::hash::Hash>(&mut self, id: &super::ArtifactId<V>) -> Option<A> {
184 let key = KernelCacheKey::from_artifact_id(id, self.build_id);
185 let artifact = self.by_kernel.as_mut()?.remove(&key)?;
186 log::trace!("Using the compilation store for variant");
187 Some(artifact)
188 }
189
190 pub fn store_variant<V: core::hash::Hash>(
192 &mut self,
193 id: &super::ArtifactId<V>,
194 artifact: A,
195 ) -> bool {
196 let Some(by_kernel) = self.by_kernel.as_mut() else {
197 return false;
198 };
199 let key = KernelCacheKey::from_artifact_id(id, self.build_id);
200 store_compiled(by_kernel, key, artifact)
201 }
202
203 pub fn purge(&mut self, kernel: &KernelId) {
205 if let Some(by_kernel) = self.by_kernel.as_mut() {
206 let key = KernelCacheKey::new(kernel, self.build_id);
207 by_kernel.purge_key(&key);
208 }
209 }
210
211 pub fn purge_variant<V: core::hash::Hash>(&mut self, id: &super::ArtifactId<V>) {
213 if let Some(by_kernel) = self.by_kernel.as_mut() {
214 let key = KernelCacheKey::from_artifact_id(id, self.build_id);
215 by_kernel.purge_key(&key);
216 }
217 }
218
219 pub fn take_by_source(&mut self, source: &str) -> Option<A> {
228 let by_kernel = self.by_kernel.as_mut()?;
229 let by_source = self.by_source.as_mut()?;
230 let hash = StableHasher::hash_one(&source);
231 let key = *by_source.get_mut(&hash)?;
234
235 let artifact = if key.build_id == self.build_id {
236 by_kernel.get_mut(&key)?.clone()
237 } else {
238 by_source.purge_key(&hash);
239 by_kernel.purge_key(&key)?
240 };
241 log::trace!("Using the compilation store, by source");
242 Some(artifact)
243 }
244
245 pub fn keep(&mut self, kernel: &KernelId, artifact: A, source: Option<&str>) -> bool {
249 let Some(by_kernel) = self.by_kernel.as_mut() else {
250 return false;
251 };
252 let key = KernelCacheKey::new(kernel, self.build_id);
253 let stored = store_compiled(by_kernel, key, artifact);
254 if let Some(source) = source
255 && let Some(by_source) = self.by_source.as_mut()
256 {
257 store_compiled(by_source, StableHasher::hash_one(&source), key);
258 }
259 stored
260 }
261}
262
263#[cfg(all(test, compilation_cache))]
264mod tests {
265 use super::*;
266 use alloc::vec;
269
270 struct First;
271 struct Second;
272 struct Third;
273
274 fn store(root: &std::path::Path) -> ArtifactStore<u32> {
277 fn lazy<K: StoreKey, V: StoreValue>(namespace: &str) -> Store<K, V> {
278 Store::new(
279 StoreOptions::new()
280 .storage(Namespace::new(namespace))
281 .cache(CacheOption::Lazy),
282 )
283 }
284
285 cubecl_environment::environment::set_root(root);
286 ArtifactStore {
287 by_kernel: Some(lazy("kernels")),
288 by_source: Some(lazy("sources")),
289 build_id: build_id_hash(),
290 }
291 }
292
293 #[test]
296 #[serial_test::serial(records)]
297 fn kernels_sharing_a_source_each_keep_it() {
298 let root = tempfile::tempdir().unwrap();
299 let mut store = store(root.path());
300 let kernels = [
301 KernelId::new::<First>(),
302 KernelId::new::<Second>(),
303 KernelId::new::<Third>(),
304 ];
305 store.keep(&kernels[0], 7, Some("shared"));
306 for kernel in &kernels[1..] {
307 let artifact = store.take_by_source("shared").expect("kept by source");
308 store.keep(kernel, artifact, Some("shared"));
309 }
310 for kernel in &kernels {
311 assert_eq!(store.take(kernel), Some(7));
312 }
313 }
314
315 #[test]
318 #[serial_test::serial(records)]
319 fn an_earlier_build_is_moved() {
320 let root = tempfile::tempdir().unwrap();
321 let mut store = store(root.path());
322 let current = store.build_id;
323 let kernel = KernelId::new::<First>();
324 store.build_id = StableHasher::hash_one(&"an earlier build");
325 store.keep(&kernel, 7, Some("shared"));
326
327 store.build_id = current;
328 assert_eq!(store.take_by_source("shared"), Some(7));
329 store.build_id = StableHasher::hash_one(&"an earlier build");
330 assert_eq!(store.take(&kernel), None, "moved out");
331 }
332}