Skip to main content

cubecl_runtime/
compiler.rs

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