Skip to main content

cubecl_runtime/
kernel.rs

1use alloc::{
2    boxed::Box,
3    string::{String, ToString},
4    vec::Vec,
5};
6use core::{
7    fmt::Display,
8    hash::Hash,
9    marker::PhantomData,
10    sync::atomic::{AtomicI8, Ordering},
11};
12
13use cubecl_common::{
14    format::format_str,
15    hash::{StableHash, StableHasher},
16};
17use cubecl_ir::{Id, Scope, StorageType, Value};
18use serde::{Deserialize, Serialize};
19
20use crate::{
21    compiler::{CompilationError, Compiler, CubeTask},
22    config::{CubeClRuntimeConfig, RuntimeConfig, compilation::CompilationLogLevel},
23    id::KernelId,
24    server::{CubeDim, ExecutionMode},
25};
26
27/// Implement this trait to create a [kernel definition](KernelDefinition).
28pub trait KernelMetadata: Send + Sync + 'static {
29    /// Name of the kernel for debugging.
30    fn name(&self) -> &'static str {
31        core::any::type_name::<Self>()
32    }
33
34    /// Identifier for the kernel, used for caching kernel compilation.
35    fn id(&self) -> KernelId;
36
37    /// Type of addresses in this kernel
38    fn address_type(&self) -> StorageType;
39}
40
41#[derive(Debug, Clone)]
42#[allow(missing_docs)]
43pub struct KernelDefinition {
44    pub buffers: Vec<KernelArg>,
45    pub tensor_maps: Vec<KernelArg>,
46    pub scalars: Vec<ScalarKernelArg>,
47    pub cube_dim: CubeDim,
48    pub body: Scope,
49    pub options: KernelOptions,
50}
51
52impl KernelDefinition {
53    /// Returns the total number of global buffers (including tensor maps)
54    pub fn num_global_buffers(&self) -> usize {
55        self.buffers.len() + self.tensor_maps.len()
56    }
57
58    /// Hash the content of the kernel in a stable way that can be used between runs.
59    ///
60    /// Two kernels with the same hash expand to the same IR, so a compiled artifact keyed on it
61    /// stays valid exactly as long as the code producing it is unchanged. This is what makes the
62    /// persistent compilation cache pick up edits to a kernel body, or to any `#[cube]` function it
63    /// reaches, without relying on a version bump.
64    ///
65    /// Debug information is deliberately left out: it holds absolute source paths, which would make
66    /// the hash differ between machines and checkouts, and it never changes what the compiler emits
67    /// beyond [`KernelOptions::debug_symbols`], which is hashed.
68    pub fn stable_hash(&self) -> StableHash {
69        let mut hasher = StableHasher::new();
70
71        self.buffers.hash(&mut hasher);
72        self.tensor_maps.hash(&mut hasher);
73        self.scalars.hash(&mut hasher);
74        self.cube_dim.hash(&mut hasher);
75        self.options.hash(&mut hasher);
76        self.body.hash(&mut hasher);
77
78        // `Scope` skips the global state when hashing, so outlined functions have to be hashed
79        // here. They aren't reachable from the body instructions, which only reference them by id,
80        // meaning a change confined to one of them would otherwise go unnoticed. The map is ordered
81        // by id, so the traversal is deterministic.
82        let state = self.body.state();
83        for (id, function) in state.functions.iter() {
84            id.hash(&mut hasher);
85            function.hash(&mut hasher);
86        }
87
88        hasher.finalize()
89    }
90}
91
92#[derive(Default, Clone, Debug, Hash, PartialEq, Eq)]
93/// Options for a specific kernel compilation
94pub struct KernelOptions {
95    /// The name of the kernel
96    pub kernel_name: String,
97    /// Whether to include debug symbols
98    pub debug_symbols: bool,
99    /// CUDA Cluster dim, if any
100    pub cluster_dim: Option<CubeDim>,
101}
102
103#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
104/// Global argument of a kernel.
105pub struct KernelArg {
106    /// The kernel id.
107    pub id: Id,
108    /// The value the argument is bound to.
109    pub value: Value,
110    /// Whether the argument has metadata.
111    pub has_extended_meta: bool,
112}
113
114#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
115#[allow(missing_docs)]
116pub struct ScalarKernelArg {
117    pub ty: StorageType,
118    pub count: usize,
119}
120
121#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Hash)]
122#[allow(missing_docs)]
123pub enum Visibility {
124    Uniform,
125    Read,
126    ReadWrite,
127}
128
129/// A kernel, compiled in the target language
130pub struct CompiledKernel<C: Compiler> {
131    /// The name of the kernel entrypoint.
132    /// For example
133    ///
134    /// ```text
135    /// #[cube(launch)]
136    /// fn gelu_array<F: Float, R: Runtime>() {}
137    /// ```
138    ///
139    /// would have the entrypoint name "`gelu_array`".
140    pub entrypoint_name: String,
141
142    /// A fully qualified debug name of the kernel.
143    ///
144    /// For example
145    ///
146    /// ```text
147    /// #[cube(launch)]
148    /// fn gelu_array<F: Float, R: Runtime>() {}
149    /// ```
150    ///
151    /// would have a debug name such as
152    ///
153    /// ```text
154    /// gelu::gelu_array::GeluArray<
155    ///    cubecl_core::frontend::element::float::F32,
156    ///    cubecl_cuda::runtime::CudaRuntime,
157    /// >
158    /// ```
159    pub debug_name: Option<&'static str>,
160
161    /// Source code of the kernel
162    pub source: String,
163    /// In-memory representation of the kernel
164    pub repr: Option<C::Representation>,
165    /// Size of a cube for the compiled kernel
166    pub cube_dim: CubeDim,
167    /// Extra debugging information about the compiled kernel.
168    pub debug_info: Option<DebugInformation>,
169}
170
171/// Extra debugging information about the compiled kernel.
172#[derive(new)]
173pub struct DebugInformation {
174    /// The language tag of the source..
175    pub lang_tag: &'static str,
176    /// The compilation id.
177    pub id: KernelId,
178}
179
180/// Kernel that can be defined
181pub trait CubeKernel: KernelMetadata {
182    /// Define the kernel for compilation
183    fn define(&self) -> KernelDefinition;
184}
185
186/// Wraps a [`CubeKernel`] to allow it be compiled.
187pub struct KernelTask<C: Compiler, K: CubeKernel> {
188    kernel_definition: K,
189    _compiler: PhantomData<C>,
190}
191
192/// Generic [`CubeTask`] for compiling kernels
193pub struct CubeTaskKernel<C: Compiler> {
194    /// The inner compilation task being wrapped
195    pub task: Box<dyn CubeTask<C>>,
196}
197
198impl<C: Compiler, K: CubeKernel> KernelTask<C, K> {
199    /// Create a new kernel task
200    pub fn new(kernel_definition: K) -> Self {
201        Self {
202            kernel_definition,
203            _compiler: PhantomData,
204        }
205    }
206}
207
208impl<C: Compiler, K: CubeKernel> CubeTask<C> for KernelTask<C, K> {
209    fn define(&self) -> KernelDefinition {
210        self.kernel_definition.define()
211    }
212
213    fn compile(
214        &self,
215        gpu_ir: KernelDefinition,
216        compiler: &mut C,
217        compilation_options: &C::CompilationOptions,
218        mode: ExecutionMode,
219        addr_type: StorageType,
220    ) -> Result<CompiledKernel<C>, CompilationError> {
221        let entrypoint_name = gpu_ir.options.kernel_name.clone();
222        let cube_dim = gpu_ir.cube_dim;
223        let lower_level_ir = compiler.compile(gpu_ir, compilation_options, mode, addr_type)?;
224
225        Ok(CompiledKernel {
226            entrypoint_name,
227            debug_name: Some(core::any::type_name::<K>()),
228            source: lower_level_ir.to_string(),
229            repr: Some(lower_level_ir),
230            cube_dim,
231            debug_info: None,
232        })
233    }
234}
235
236impl<C: Compiler, K: CubeKernel> KernelMetadata for KernelTask<C, K> {
237    // Forward ID to underlying kernel definition.
238    fn id(&self) -> KernelId {
239        self.kernel_definition.id()
240    }
241
242    // Forward name to underlying kernel definition.
243    fn name(&self) -> &'static str {
244        self.kernel_definition.name()
245    }
246
247    fn address_type(&self) -> StorageType {
248        self.kernel_definition.address_type()
249    }
250}
251
252impl<C: Compiler> KernelMetadata for Box<dyn CubeTask<C>> {
253    // Deref and use existing ID.
254    fn id(&self) -> KernelId {
255        self.as_ref().id()
256    }
257
258    // Deref and use existing name.
259    fn name(&self) -> &'static str {
260        self.as_ref().name()
261    }
262
263    fn address_type(&self) -> StorageType {
264        self.as_ref().address_type()
265    }
266}
267
268static COMPILATION_LEVEL: AtomicI8 = AtomicI8::new(-1);
269
270fn compilation_level() -> u8 {
271    let compilation_level = COMPILATION_LEVEL.load(Ordering::Relaxed);
272    if compilation_level == -1 {
273        let val = match CubeClRuntimeConfig::get().compilation.logger.level {
274            CompilationLogLevel::Full => 2,
275            CompilationLogLevel::Disabled => 0,
276            CompilationLogLevel::Basic => 1,
277        };
278
279        COMPILATION_LEVEL.store(val, Ordering::Relaxed);
280        val as u8
281    } else {
282        compilation_level as u8
283    }
284}
285
286impl<C: Compiler> Display for CompiledKernel<C> {
287    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
288        match compilation_level() {
289            2 => self.format_full(f),
290            _ => self.format_basic(f),
291        }
292    }
293}
294
295impl<C: Compiler> CompiledKernel<C> {
296    fn format_basic(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
297        f.write_str("[Compiling kernel]")?;
298        if let Some(name) = self.debug_name {
299            if name.len() <= 32 {
300                f.write_fmt(format_args!(" {name}"))?;
301            } else {
302                f.write_fmt(format_args!(" {}", name.split('<').next().unwrap_or("")))?;
303            }
304        }
305
306        Ok(())
307    }
308
309    fn format_full(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
310        f.write_str("[START_KERNEL_COMPILATION]")?;
311
312        if let Some(name) = self.debug_name {
313            if name.len() <= 32 {
314                f.write_fmt(format_args!("\nname: {name}"))?;
315            } else {
316                let name = format_str(name, &[('<', '>')], false);
317                f.write_fmt(format_args!("\nname: {name}"))?;
318            }
319        }
320
321        if let Some(info) = &self.debug_info {
322            f.write_fmt(format_args!("\nid: {:#?}", info.id))?;
323        }
324
325        f.write_fmt(format_args!(
326            "
327source:
328```{}
329{}
330```
331[END_KERNEL_COMPILATION]
332",
333            self.debug_info
334                .as_ref()
335                .map(|info| info.lang_tag)
336                .unwrap_or(""),
337            self.source
338        ))
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use cubecl_ir::{ElemType, Instruction, Operation, Type};
346
347    fn definition(body: Scope) -> KernelDefinition {
348        KernelDefinition {
349            buffers: Vec::new(),
350            tensor_maps: Vec::new(),
351            scalars: Vec::new(),
352            cube_dim: CubeDim::new_single(),
353            body,
354            options: KernelOptions::default(),
355        }
356    }
357
358    /// A scope holding a single `Copy` on a freshly declared local.
359    fn scope_with_copy() -> Scope {
360        let scope = Scope::root(false);
361        let local = scope.create_local_mut(Type::scalar(ElemType::Bool));
362        scope.register(Instruction::new(Operation::Copy(local), local));
363        scope
364    }
365
366    #[test]
367    fn hash_is_stable_across_calls() {
368        let definition = definition(scope_with_copy());
369
370        assert_eq!(definition.stable_hash(), definition.stable_hash());
371    }
372
373    #[test]
374    fn equivalent_definitions_hash_equal() {
375        let lhs = definition(scope_with_copy());
376        let rhs = definition(scope_with_copy());
377
378        assert_eq!(lhs.stable_hash(), rhs.stable_hash());
379    }
380
381    #[test]
382    fn body_change_changes_hash() {
383        let lhs = definition(scope_with_copy());
384
385        let scope = Scope::root(false);
386        let local = scope.create_local_mut(Type::scalar(ElemType::Bool));
387        // Same shape as `scope_with_copy`, different operation.
388        scope.register(Instruction::new(
389            Operation::ConstructAggregate(alloc::vec![local]),
390            local,
391        ));
392        let rhs = definition(scope);
393
394        assert_ne!(lhs.stable_hash(), rhs.stable_hash());
395    }
396
397    #[test]
398    fn cube_dim_change_changes_hash() {
399        let lhs = definition(scope_with_copy());
400        let mut rhs = definition(scope_with_copy());
401        rhs.cube_dim = CubeDim::new_2d(2, 2);
402
403        assert_ne!(lhs.stable_hash(), rhs.stable_hash());
404    }
405
406    /// The body only references an outlined function by id, and `Scope`'s own `Hash` skips the
407    /// global state holding it. Without hashing that map, editing a `#[cube]` helper that got
408    /// outlined would leave the key untouched and the stale artifact would be served.
409    #[test]
410    fn outlined_function_change_changes_hash() {
411        // The kernel body is empty either way; only the outlined function differs.
412        fn with_function(extra_instruction: bool) -> KernelDefinition {
413            let outlined = Scope::root(false);
414            let local = outlined.create_local_mut(Type::scalar(ElemType::Bool));
415            outlined.register(Instruction::new(Operation::Copy(local), local));
416            if extra_instruction {
417                outlined.register(Instruction::new(
418                    Operation::ConstructAggregate(alloc::vec![local]),
419                    local,
420                ));
421            }
422
423            let definition = definition(Scope::root(false));
424            definition.body.create_function(Vec::new(), outlined);
425            definition
426        }
427
428        let lhs = with_function(false);
429        let rhs = with_function(true);
430
431        // The bodies are indistinguishable on their own; only hashing the outlined function
432        // separates the two definitions.
433        assert_eq!(
434            StableHasher::hash_one(&lhs.body),
435            StableHasher::hash_one(&rhs.body)
436        );
437        assert_ne!(lhs.stable_hash(), rhs.stable_hash());
438    }
439}