Skip to main content

cubecl_runtime/
compiler.rs

1use crate::{
2    id::KernelId,
3    kernel::{CompiledKernel, KernelDefinition, KernelMetadata},
4};
5use alloc::string::{String, ToString};
6use core::hash::Hash;
7use cubecl_common::hash::{StableHash, StableHasher};
8use cubecl_environment::backtrace::BackTrace;
9use cubecl_environment::collections::HashMap;
10#[cfg(std_io)]
11use cubecl_environment::persistence::{CacheOption, Namespace, StoreOptions};
12use cubecl_environment::persistence::{Store, StoreKey, StoreValue};
13use thiserror::Error;
14
15/// Platform-specific build identifier, changes on rebuild
16pub type BuildId = Option<&'static [u8]>;
17
18/// Pre-hashed build ID
19pub fn build_id_hash() -> StableHash {
20    StableHasher::hash_one(&buildid::build_id())
21}
22
23/// A store for `backend`'s compiled artifacts, or `None` when compilation
24/// caching is disabled or the target has nowhere durable to put them.
25///
26/// `fingerprint` names what the artifacts were built for — an architecture, a
27/// device — and becomes part of the namespace. Compiled code is not portable
28/// across those, so this is what keeps a bundle shipped between machines from
29/// serving the wrong binary. It needs no sanitizing: a namespace is a database
30/// column, never a path.
31pub fn compilation_store<K: StoreKey, V: StoreValue>(
32    backend: &'static str,
33    fingerprint: impl AsRef<str>,
34) -> Option<Store<K, V>> {
35    #[cfg(std_io)]
36    {
37        use crate::config::RuntimeConfig;
38
39        if !crate::config::CubeClRuntimeConfig::get().compilation.cache {
40            return None;
41        }
42
43        Some(Store::new(
44            StoreOptions::new()
45                .storage(Namespace::scoped(backend, fingerprint))
46                .cache(CacheOption::Lazy),
47        ))
48    }
49
50    // No file system to persist to; the caller keeps its in-memory map.
51    #[cfg(not(std_io))]
52    {
53        let _ = (backend, fingerprint);
54        None
55    }
56}
57
58/// Records a freshly compiled artifact, logging rather than failing.
59///
60/// A refused write is routine, not exceptional: another process sharing the
61/// environment may have written the key first, or the backing store may have
62/// declined it. The artifact was just compiled either way, so the whole cost
63/// is compiling it again next run.
64pub fn store_compiled<K: StoreKey, V: StoreValue>(store: &mut Store<K, V>, key: K, value: V) {
65    if let Err(err) = store.insert(key, value) {
66        log::warn!("Unable to cache the compiled kernel: {}", err.reason());
67    }
68}
69
70/// Kernel trait with the `ComputeShader` that will be compiled and cached based on the
71/// provided id.
72pub trait CubeTask<C: Compiler>: KernelMetadata + Send + Sync {
73    /// Expand the kernel into its [definition](KernelDefinition).
74    ///
75    /// Kept separate from [`CubeTask::compile`] so a server can hash the definition to key the
76    /// compilation cache, then hand the same definition back on a miss instead of expanding twice.
77    fn define(&self) -> KernelDefinition;
78
79    /// Compile a kernel definition and return the compiled form with an optional non-text
80    /// representation.
81    fn compile(
82        &self,
83        definition: KernelDefinition,
84        compiler: &mut C,
85        compilation_options: &C::CompilationOptions,
86    ) -> Result<CompiledKernel<C>, CompilationError>;
87}
88
89/// Key for an entry in the persistent compilation cache.
90///
91/// The [id](KernelId) alone doesn't describe what a kernel does: it covers the kernel type, its
92/// comptime arguments and its launch settings, but nothing of the body. Pairing it with a hash of
93/// the expanded IR is what lets a cached artifact be invalidated when the code behind it changes.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
95pub struct KernelCacheKey {
96    /// Hash of the [kernel id](KernelId).
97    pub id: StableHash,
98    /// Hash of the [build id](buildid::build_id).
99    pub build_id: StableHash,
100}
101
102impl KernelCacheKey {
103    /// Create a key from a kernel id and the current build ID.
104    pub fn new(id: &KernelId, build_id: StableHash) -> Self {
105        Self {
106            id: id.stable_hash(),
107            build_id,
108        }
109    }
110}
111
112/// A server's in-memory compilation cache: the compiled artifacts it memoizes
113/// — pipelines, loaded modules — in front of a persistent [`compilation_store`].
114///
115/// Entries are dropped when the environment switches, because the map is bound
116/// to an environment exactly as the store it mirrors is. One served after a
117/// switch would describe the environment that is gone, and, worse, would never
118/// be written to the new environment's store, so a bundle exported from that
119/// environment would silently be missing that kernel. This is the same contract
120/// [`Store`] applies to itself, for the state a store cannot see — see
121/// [`cubecl_environment::environment::generation`].
122///
123/// Every accessor resets before it answers, so a backend has nothing to
124/// remember beyond using this in place of a plain map.
125#[derive(Debug)]
126pub struct CompilationCache<K, V> {
127    entries: HashMap<K, V>,
128    /// The generation the entries were built under, or `None` when the cache
129    /// mirrors no store and so is unbound.
130    generation: Option<u32>,
131}
132
133impl<K: Eq + Hash, V> CompilationCache<K, V> {
134    /// An empty cache in front of `store`, bound to the active environment
135    /// exactly when that store exists.
136    ///
137    /// Unbound otherwise: with nothing persisted, a switch changes nothing
138    /// about what the cache holds, so resetting it would only buy a redundant
139    /// compilation — the same reason the autotune cache survives a switch when
140    /// its persistent cache is off.
141    pub fn mirroring<SK: StoreKey, SV: StoreValue>(store: &Option<Store<SK, SV>>) -> Self {
142        Self {
143            entries: HashMap::new(),
144            generation: store
145                .is_some()
146                .then(cubecl_environment::environment::generation),
147        }
148    }
149
150    /// An empty cache that no environment switch ever resets, for a backend
151    /// with no persistent store to mirror.
152    pub fn unbound() -> Self {
153        Self {
154            entries: HashMap::new(),
155            generation: None,
156        }
157    }
158
159    /// The artifact compiled for `key`, if it is still valid.
160    pub fn get(&mut self, key: &K) -> Option<&V> {
161        self.reset_if_switched();
162        self.entries.get(key)
163    }
164
165    /// Whether an artifact for `key` is cached and still valid.
166    pub fn contains(&mut self, key: &K) -> bool {
167        self.reset_if_switched();
168        self.entries.contains_key(key)
169    }
170
171    /// Records a freshly compiled artifact.
172    pub fn insert(&mut self, key: K, value: V) {
173        self.reset_if_switched();
174        self.entries.insert(key, value);
175    }
176
177    /// Drops every entry when the environment switched since the last access,
178    /// adopting the new generation so one switch costs one reset.
179    fn reset_if_switched(&mut self) {
180        let Some(generation) = self.generation else {
181            return;
182        };
183
184        let current = cubecl_environment::environment::generation();
185        if current == generation {
186            return;
187        }
188
189        log::debug!("Environment switched, dropping the in-memory compilation cache");
190        self.generation = Some(current);
191        self.entries.clear();
192    }
193}
194
195/// JIT compilation error.
196#[derive(Error, Clone)]
197#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
198pub enum CompilationError {
199    /// An instruction isn't supported.
200    #[error(
201        "An unsupported instruction caused the compilation to fail\nCaused by:\n  {reason}\nBacktrace:\n{backtrace}"
202    )]
203    UnsupportedInstruction {
204        /// The caused of the error.
205        reason: String,
206        /// The backtrace for this error.
207        #[cfg_attr(std_io, serde(skip))]
208        backtrace: BackTrace,
209    },
210
211    /// A generic compilation error.
212    #[error(
213        "An error caused the compilation to fail\nCaused by:\n  {reason}\nBacktrace:\n{backtrace}"
214    )]
215    Generic {
216        /// The error context.
217        reason: String,
218        /// The backtrace for this error.
219        #[cfg_attr(std_io, serde(skip))]
220        backtrace: BackTrace,
221    },
222    /// A generic compilation error.
223    #[error(
224        "A validation error caused the compilation to fail\nCaused by:\n  {reason}\nBacktrace:\n{backtrace}"
225    )]
226    Validation {
227        /// The error context.
228        reason: String,
229        /// The backtrace for this error.
230        #[cfg_attr(std_io, serde(skip))]
231        backtrace: BackTrace,
232    },
233}
234
235impl core::fmt::Debug for CompilationError {
236    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
237        write!(f, "{self}")
238    }
239}
240
241impl From<pliron::result::Error> for CompilationError {
242    fn from(value: pliron::result::Error) -> Self {
243        CompilationError::Validation {
244            reason: value.to_string(),
245            backtrace: BackTrace::capture(),
246        }
247    }
248}
249
250/// Compiles the representation into its own representation that can be formatted into tokens.
251pub trait Compiler: Sync + Send + 'static + Clone + core::fmt::Debug {
252    /// The representation for the compiled code.
253    type Representation: core::fmt::Display;
254    /// The compilation options used to configure the compiler
255    type CompilationOptions: Send + Default + core::fmt::Debug;
256
257    /// Compiles the [kernel definition](KernelDefinition) into the compiler's representation.
258    fn compile(
259        &mut self,
260        kernel: KernelDefinition,
261        compilation_options: &Self::CompilationOptions,
262    ) -> Result<Self::Representation, CompilationError>;
263
264    /// The default extension for the runtime's kernel/shader code.
265    /// Might change based on which compiler is used.
266    fn extension(&self) -> &'static str;
267}