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}