Skip to main content

cubecl_server/compiler/
store.rs

1//! Where a backend keeps the artifacts it compiled, between runs.
2
3use 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
9/// Platform-specific build identifier, changes on rebuild
10pub type BuildId = Option<&'static [u8]>;
11
12/// Pre-hashed build ID
13pub fn build_id_hash() -> StableHash {
14    StableHasher::hash_one(&buildid::build_id())
15}
16
17/// A store for `backend`'s compiled artifacts, or `None` when compilation
18/// caching is disabled or the target has nowhere durable to put them.
19///
20/// `fingerprint` names what the artifacts were built for — an architecture, a
21/// device — and becomes part of the namespace. Compiled code is not portable
22/// across those, so this is what keeps a bundle shipped between machines from
23/// serving the wrong binary. It needs no sanitizing: a namespace is a database
24/// column, never a path.
25pub 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    // No file system to persist to; the caller keeps its in-memory map.
45    #[cfg(not(compilation_cache))]
46    {
47        let _ = (backend, fingerprint);
48        None
49    }
50}
51
52/// Stores a freshly compiled artifact, logging rather than failing, and says
53/// whether the store took it.
54///
55/// A refused write is routine, not exceptional: another process sharing the
56/// environment may have written the key first, or the backing store may have
57/// declined it. The artifact was just compiled either way, so the whole cost
58/// is compiling it again next run.
59pub 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/// Key for an entry in the persistent compilation cache.
74///
75/// The [id](KernelId) alone doesn't describe what a kernel does: it covers the kernel type, its
76/// comptime arguments and its launch settings, but nothing of the body. Pairing it with a hash of
77/// the expanded IR is what lets a cached artifact be invalidated when the code behind it changes.
78#[derive(
79    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
80)]
81pub struct KernelCacheKey {
82    /// Hash of the [kernel id](KernelId).
83    pub id: StableHash,
84    /// Hash of the [build id](buildid::build_id).
85    pub build_id: StableHash,
86}
87
88impl KernelCacheKey {
89    /// Create a key from a kernel id and the current build ID.
90    pub fn new(id: &KernelId, build_id: StableHash) -> Self {
91        Self {
92            id: id.stable_hash(),
93            build_id,
94        }
95    }
96
97    /// Create a key from an artifact id (kernel and variant) and the current build ID.
98    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/// What an [`ArtifactStore`] kept by kernel and by source names its two maps,
114/// as [`compilation_store`] takes a backend's name.
115#[derive(Debug, Clone, Copy)]
116pub struct StoreNames {
117    /// The map from kernel to artifact.
118    pub kernels: &'static str,
119    /// The map from source to the kernel that first finalized it.
120    pub sources: &'static str,
121}
122
123/// The compiled artifacts of one backend and device, kept by kernel and, for a
124/// backend that opts in, by the source each was finalized from — see
125/// [`ArtifactCompiler::source`](super::ArtifactCompiler::source).
126///
127/// Holds nothing when compilation caching is disabled or the target has
128/// nowhere durable to put it: every take then misses and every keep refuses.
129#[derive(Debug)]
130pub struct ArtifactStore<A> {
131    by_kernel: Option<Store<KernelCacheKey, A>>,
132    /// Maps a source's hash to the key of the kernel that first finalized it.
133    by_source: Option<Store<StableHash, KernelCacheKey>>,
134    build_id: StableHash,
135}
136
137impl<A: StoreValue> ArtifactStore<A> {
138    /// The artifacts `backend` compiled for `fingerprint`, kept by kernel
139    /// only. See [`compilation_store`] for what the two name.
140    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    /// The artifacts compiled for `fingerprint`, kept by kernel and by
149    /// source, each map under its name in `names`.
150    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    /// Whether anything is kept between runs, which is what binds a backend's
164    /// loaded kernels to the active environment.
165    pub fn persists(&self) -> bool {
166        self.by_kernel.is_some()
167    }
168
169    /// Whether the compilation store is enabled for this backend.
170    pub fn is_enabled(&self) -> bool {
171        self.by_kernel.is_some()
172    }
173
174    /// Takes the artifact kept for `kernel` out of the store, to be loaded.
175    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    /// Takes the artifact kept for `id` (kernel and variant) out of the store, to be loaded.
183    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    /// Stores `artifact` under `id` (kernel and variant).
191    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    /// Purges a corrupted or invalid entry for `kernel`.
204    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    /// Purges a corrupted or invalid entry for `id` (kernel and variant).
212    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    /// The artifact another kernel finalized from `source`, for
220    /// [`keep`](Self::keep) to put under the kernel asking.
221    ///
222    /// A kernel of this build keeps its own entry: it is as live as the one
223    /// asking, and two kernel ids of one build that expand to one source —
224    /// several of them in one batch — each keep theirs. One of an earlier
225    /// build is moved, deleted under its key: nothing will ask for that key
226    /// again.
227    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        // `get_mut`, not `get`: a lazy store reads through to its storage
232        // only there, and the compilation stores are lazy.
233        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    /// Keeps `artifact` under `kernel` and, when given, under the `source` it
246    /// was finalized from. Whether the store took it, as [`store_compiled`]
247    /// answers: `false` with nothing to keep it in.
248    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    // `serial_test`'s macro expands to `vec!`, which a `no_std` crate has to
267    // bring in itself.
268    use alloc::vec;
269
270    struct First;
271    struct Second;
272    struct Third;
273
274    /// Stores as [`compilation_store`] opens them: lazy, over storage in an
275    /// environment rooted at `root`, which only holds what it is written.
276    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    /// Kernels of one build that expand to one source each keep the artifact:
294    /// taking it for one leaves it to the others.
295    #[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    /// A kernel of an earlier build gives its artifact up: nothing asks for
316    /// its key again.
317    #[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}