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}