Skip to main content

cubecl_wgpu/compiler/
base.rs

1use cubecl_server::kernel::BufferIOAttr;
2use cubecl_server::kernel::CompiledKernel;
3use std::fmt::Display;
4
5use cubecl_core::{
6    Compiler, WgpuCompilationOptions,
7    prelude::{CubeKernel, KernelDefinition},
8    server::{LaunchError, ResourceLimitError},
9};
10#[cfg(feature = "msl")]
11use cubecl_cpp::shared::MslComputeKernel;
12use cubecl_environment::backtrace::BackTrace;
13use cubecl_ir::DeviceProperties;
14use cubecl_server::compiler::CompilationError;
15use derive_more::derive::From;
16use thiserror::Error;
17
18#[cfg(feature = "spirv")]
19use crate::ParamsTransfer;
20use crate::{CompilerInfo, WgpuBackend, WgpuServer};
21
22use super::wgsl;
23
24#[cfg(feature = "msl")]
25pub use cubecl_cpp::MslCompiler;
26#[cfg(feature = "spirv")]
27pub use cubecl_spirv::SpirvCompiler;
28pub use wgsl::WgslCompiler;
29
30/// Compiler that dispatches to the most appropriate shader language backend for the active
31/// `wgpu` backend.
32///
33/// The variant is selected at runtime by [`WgpuCompiler::init`] based on the `wgpu::Backend`
34/// in use and the enabled cargo features (`spirv`, `msl`).
35#[allow(clippy::large_enum_variant)]
36#[derive(Debug, Clone)]
37pub enum AutoCompiler {
38    /// WGSL backend, available on every `wgpu` backend.
39    Wgsl(WgslCompiler),
40    /// SPIR-V backend used on Vulkan when the device supports the required features.
41    #[cfg(feature = "spirv")]
42    SpirV(cubecl_spirv::SpirvCompiler),
43    /// Metal Shading Language backend used on the Metal backend.
44    #[cfg(feature = "msl")]
45    Msl(MslCompiler),
46}
47
48/// Owned compiled kernel representation matching the variants of [`AutoCompiler`].
49#[derive(From)]
50#[allow(clippy::large_enum_variant)]
51pub enum AutoRepresentation {
52    /// WGSL compute shader source.
53    Wgsl(wgsl::ComputeShader),
54    /// Compiled SPIR-V kernel.
55    #[cfg(feature = "spirv")]
56    SpirV(cubecl_spirv::SpirvKernel),
57    /// Compiled Metal Shading Language kernel.
58    #[cfg(feature = "msl")]
59    Msl(MslComputeKernel),
60}
61
62/// Borrowed counterpart of [`AutoRepresentation`], useful when only read access is needed.
63#[derive(From, Clone, Copy)]
64#[allow(clippy::large_enum_variant)]
65pub enum AutoRepresentationRef<'a> {
66    /// Borrowed WGSL compute shader.
67    Wgsl(&'a wgsl::ComputeShader),
68    /// Borrowed SPIR-V kernel.
69    #[cfg(feature = "spirv")]
70    SpirV(&'a cubecl_spirv::SpirvKernel),
71    /// Borrowed Metal Shading Language kernel.
72    #[cfg(feature = "msl")]
73    Msl(&'a MslComputeKernel),
74}
75
76#[cfg(feature = "spirv")]
77impl AutoRepresentation {
78    /// Returns the SPIR-V kernel if this representation is the SPIR-V variant.
79    pub fn as_spirv(&self) -> Option<&cubecl_spirv::SpirvKernel> {
80        match self {
81            AutoRepresentation::SpirV(repr) => Some(repr),
82            _ => None,
83        }
84    }
85}
86
87#[cfg(feature = "msl")]
88impl AutoRepresentation {
89    /// Returns the MSL kernel if this representation is the MSL variant.
90    pub fn as_msl(&self) -> Option<&MslComputeKernel> {
91        match self {
92            AutoRepresentation::Msl(repr) => Some(repr),
93            _ => None,
94        }
95    }
96}
97
98impl AutoRepresentation {
99    /// Borrow this representation as an [`AutoRepresentationRef`].
100    pub fn as_ref(&self) -> AutoRepresentationRef<'_> {
101        match self {
102            AutoRepresentation::Wgsl(compute_shader) => AutoRepresentationRef::Wgsl(compute_shader),
103            #[cfg(feature = "spirv")]
104            AutoRepresentation::SpirV(spirv_kernel) => AutoRepresentationRef::SpirV(spirv_kernel),
105            #[cfg(feature = "msl")]
106            AutoRepresentation::Msl(compute_shader) => AutoRepresentationRef::Msl(compute_shader),
107        }
108    }
109}
110
111impl Display for AutoRepresentation {
112    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> std::fmt::Result {
113        match self {
114            AutoRepresentation::Wgsl(compute_shader) => compute_shader.fmt(f),
115            #[cfg(feature = "spirv")]
116            AutoRepresentation::SpirV(spirv_kernel) => spirv_kernel.fmt(f),
117            #[cfg(feature = "msl")]
118            AutoRepresentation::Msl(compute_shader) => compute_shader.fmt(f),
119        }
120    }
121}
122
123impl Compiler for AutoCompiler {
124    type Representation = AutoRepresentation;
125
126    type CompilationOptions = WgpuCompilationOptions;
127
128    fn compile(
129        &mut self,
130        kernel: KernelDefinition,
131        compilation_options: &Self::CompilationOptions,
132    ) -> Result<Self::Representation, CompilationError> {
133        let kernel = match self {
134            AutoCompiler::Wgsl(wgsl_compiler) => {
135                Compiler::compile(wgsl_compiler, kernel, compilation_options)?.into()
136            }
137            #[cfg(feature = "spirv")]
138            AutoCompiler::SpirV(spirv_compiler) => {
139                Compiler::compile(spirv_compiler, kernel, compilation_options)?.into()
140            }
141            #[cfg(feature = "msl")]
142            AutoCompiler::Msl(msl_compiler) => {
143                // override compilation options with cpp compiler options for metal
144                use cubecl_cpp;
145                let compilation_options = cubecl_cpp::shared::CompilationOptions::default();
146                Compiler::compile(msl_compiler, kernel, &compilation_options)?.into()
147            }
148        };
149
150        Ok(kernel)
151    }
152
153    fn buffer_io(repr: &Self::Representation) -> Option<Vec<BufferIOAttr>> {
154        match repr {
155            AutoRepresentation::Wgsl(shader) => WgslCompiler::buffer_io(shader),
156            #[cfg(feature = "spirv")]
157            AutoRepresentation::SpirV(kernel) => cubecl_spirv::SpirvCompiler::buffer_io(kernel),
158            #[cfg(feature = "msl")]
159            AutoRepresentation::Msl(kernel) => MslCompiler::buffer_io(kernel),
160        }
161    }
162
163    fn extension(&self) -> &'static str {
164        match self {
165            AutoCompiler::Wgsl(_) => "wgsl",
166            #[cfg(feature = "spirv")]
167            AutoCompiler::SpirV(_) => "spv",
168            #[cfg(feature = "msl")]
169            AutoCompiler::Msl(_) => "msl",
170        }
171    }
172
173    fn lang_tag(&self) -> &'static str {
174        match self {
175            AutoCompiler::Wgsl(compiler) => compiler.lang_tag(),
176            #[cfg(feature = "spirv")]
177            AutoCompiler::SpirV(compiler) => compiler.lang_tag(),
178            #[cfg(feature = "msl")]
179            AutoCompiler::Msl(compiler) => compiler.lang_tag(),
180        }
181    }
182}
183
184impl WgpuCompiler for AutoCompiler {
185    #[cfg(feature = "msl")]
186    fn validate_runtime(
187        backend: wgpu::Backend,
188        options: &WgpuCompilationOptions,
189        requested_backend: WgpuBackend,
190    ) -> Result<(), WgpuCompilerInitError> {
191        if requested_backend == WgpuBackend::Metal {
192            MslCompiler::validate_runtime(backend, options, requested_backend)?;
193        }
194
195        Ok(())
196    }
197
198    fn init(backend: wgpu::Backend, options: &WgpuCompilationOptions) -> Self {
199        let _ = options; // Unused without `spirv` feature
200        match backend {
201            #[cfg(feature = "spirv")]
202            wgpu::Backend::Vulkan if options.supports_vulkan_compiler => {
203                AutoCompiler::SpirV(Default::default())
204            }
205            #[cfg(feature = "msl")]
206            wgpu::Backend::Metal if options.supports_msl_compiler => {
207                AutoCompiler::Msl(Default::default())
208            }
209            _ => AutoCompiler::Wgsl(Default::default()),
210        }
211    }
212
213    fn compile_kernel(
214        &mut self,
215        server: &mut WgpuServer<AutoCompiler>,
216        kernel: Box<dyn CubeKernel>,
217        definition: KernelDefinition,
218    ) -> Result<CompiledKernel<Self>, CompilationError> {
219        match self {
220            AutoCompiler::Wgsl(_) => {
221                CompiledKernel::compile(&*kernel, definition, self, &server.compilation_options)
222            }
223            #[cfg(feature = "spirv")]
224            AutoCompiler::SpirV(_) => crate::vulkan::compile(self, server, kernel, definition),
225            #[cfg(feature = "msl")]
226            AutoCompiler::Msl(_) => {
227                CompiledKernel::compile(&*kernel, definition, self, &server.compilation_options)
228            }
229        }
230    }
231
232    fn validate_ir(
233        &self,
234        repr: &Option<Self::Representation>,
235        props: &DeviceProperties,
236    ) -> Result<(), LaunchError> {
237        let shared_bytes = repr.as_ref().map(|repr| match repr {
238            AutoRepresentation::Wgsl(repr) => repr.shared_memory_size,
239            #[cfg(feature = "msl")]
240            AutoRepresentation::Msl(repr) => repr.shared_memory_size,
241            #[cfg(feature = "spirv")]
242            AutoRepresentation::SpirV(repr) => repr.shared_size,
243        });
244        check_shared_memory(shared_bytes, props)
245    }
246
247    fn normalize_repr(
248        &self,
249        repr: Option<Self::Representation>,
250    ) -> (CompilerInfo, Option<AutoRepresentation>) {
251        let compiler_info = match &repr {
252            #[cfg(feature = "spirv")]
253            Some(AutoRepresentation::SpirV(repr)) => CompilerInfo::Vulkan {
254                params_transfer: match repr.immediate_size {
255                    Some(_) => ParamsTransfer::Immediate,
256                    None => ParamsTransfer::Uniform,
257                },
258            },
259            #[cfg(feature = "msl")]
260            Some(AutoRepresentation::Msl(_)) => CompilerInfo::Metal,
261            Some(AutoRepresentation::Wgsl(_)) => CompilerInfo::WGSL,
262            None => CompilerInfo::None,
263        };
264
265        (compiler_info, repr)
266    }
267}
268
269impl WgpuCompiler for WgslCompiler {
270    fn init(_backend: wgpu::Backend, _options: &WgpuCompilationOptions) -> Self {
271        Self
272    }
273
274    fn compile_kernel(
275        &mut self,
276        server: &mut WgpuServer<Self>,
277        kernel: Box<dyn CubeKernel>,
278        definition: KernelDefinition,
279    ) -> Result<CompiledKernel<Self>, CompilationError> {
280        CompiledKernel::compile(&*kernel, definition, self, &server.compilation_options)
281    }
282
283    fn validate_ir(
284        &self,
285        repr: &Option<Self::Representation>,
286        props: &DeviceProperties,
287    ) -> Result<(), LaunchError> {
288        let shared_bytes = repr.as_ref().map(|repr| repr.shared_memory_size);
289        check_shared_memory(shared_bytes, props)
290    }
291
292    fn normalize_repr(
293        &self,
294        repr: Option<Self::Representation>,
295    ) -> (CompilerInfo, Option<AutoRepresentation>) {
296        (CompilerInfo::WGSL, repr.map(|r| r.into()))
297    }
298}
299
300#[cfg(feature = "msl")]
301impl WgpuCompiler for MslCompiler {
302    fn validate_runtime(
303        backend: wgpu::Backend,
304        options: &WgpuCompilationOptions,
305        _requested_backend: WgpuBackend,
306    ) -> Result<(), WgpuCompilerInitError> {
307        if backend != wgpu::Backend::Metal {
308            return Err(WgpuCompilerInitError::BackendMismatch {
309                compiler: "MslCompiler",
310                expected: wgpu::Backend::Metal,
311                actual: backend,
312            });
313        }
314
315        if !options.supports_msl_compiler {
316            return Err(WgpuCompilerInitError::Unavailable {
317                compiler: "MslCompiler",
318                backend,
319            });
320        }
321
322        Ok(())
323    }
324
325    fn init(_backend: wgpu::Backend, _options: &WgpuCompilationOptions) -> Self {
326        Self::default()
327    }
328
329    fn compile_kernel(
330        &mut self,
331        _server: &mut WgpuServer<Self>,
332        kernel: Box<dyn CubeKernel>,
333        definition: KernelDefinition,
334    ) -> Result<CompiledKernel<Self>, CompilationError> {
335        // The MSL compiler uses its own CompilationOptions, not WgpuCompilationOptions.
336        let compilation_options = cubecl_cpp::shared::CompilationOptions::default();
337        CompiledKernel::compile(&*kernel, definition, self, &compilation_options)
338    }
339
340    fn validate_ir(
341        &self,
342        repr: &Option<Self::Representation>,
343        props: &DeviceProperties,
344    ) -> Result<(), LaunchError> {
345        let shared_bytes = repr.as_ref().map(|repr| repr.shared_memory_size);
346        check_shared_memory(shared_bytes, props)
347    }
348
349    fn normalize_repr(
350        &self,
351        repr: Option<Self::Representation>,
352    ) -> (CompilerInfo, Option<AutoRepresentation>) {
353        (CompilerInfo::Metal, repr.map(|r| r.into()))
354    }
355}
356
357#[cfg(feature = "spirv")]
358impl WgpuCompiler for cubecl_spirv::SpirvCompiler {
359    fn init(_backend: wgpu::Backend, _options: &WgpuCompilationOptions) -> Self {
360        Self
361    }
362
363    fn compile_kernel(
364        &mut self,
365        server: &mut WgpuServer<Self>,
366        kernel: Box<dyn CubeKernel>,
367        definition: KernelDefinition,
368    ) -> Result<CompiledKernel<Self>, CompilationError> {
369        crate::vulkan::compile(self, server, kernel, definition)
370    }
371
372    fn validate_ir(
373        &self,
374        repr: &Option<Self::Representation>,
375        props: &DeviceProperties,
376    ) -> Result<(), LaunchError> {
377        let shared_bytes = repr.as_ref().map(|repr| repr.shared_size);
378        check_shared_memory(shared_bytes, props)
379    }
380
381    fn normalize_repr(
382        &self,
383        repr: Option<Self::Representation>,
384    ) -> (CompilerInfo, Option<AutoRepresentation>) {
385        let params_transfer = match repr.as_ref().and_then(|r| r.immediate_size) {
386            Some(_) => ParamsTransfer::Immediate,
387            None => ParamsTransfer::Uniform,
388        };
389        (
390            CompilerInfo::Vulkan { params_transfer },
391            repr.map(|r| r.into()),
392        )
393    }
394}
395
396fn check_shared_memory(
397    shared_bytes: Option<usize>,
398    props: &DeviceProperties,
399) -> Result<(), LaunchError> {
400    let max_smem = props.hardware.max_shared_memory_size;
401    if let Some(shared_bytes) = shared_bytes
402        && shared_bytes > max_smem
403    {
404        return Err(ResourceLimitError::SharedMemory {
405            requested: shared_bytes,
406            max: max_smem,
407            backtrace: BackTrace::capture(),
408        }
409        .into());
410    }
411    Ok(())
412}
413
414/// An explicitly selected wgpu compiler cannot serve the selected graphics backend or device.
415#[non_exhaustive]
416#[derive(Debug, Error, PartialEq, Eq)]
417pub enum WgpuCompilerInitError {
418    /// The compiler only emits shaders for a different graphics backend.
419    #[error("`{compiler}` requires the {expected:?} wgpu backend, but {actual:?} was selected")]
420    BackendMismatch {
421        compiler: &'static str,
422        expected: wgpu::Backend,
423        actual: wgpu::Backend,
424    },
425    /// The selected device does not meet the compiler's requirements.
426    #[error(
427        "`{compiler}` was requested explicitly, but it is unavailable on the selected {backend:?} device; use `AutoCompiler` with an automatic device to allow WGSL fallback"
428    )]
429    Unavailable {
430        compiler: &'static str,
431        backend: wgpu::Backend,
432    },
433}
434
435/// Extension trait implemented by every compiler usable with the `wgpu` runtime.
436///
437/// The base [`Compiler`] trait already exposes a `compile` method that turns a
438/// [`KernelDefinition`] into a backend representation. [`WgpuCompiler`] sits one level
439/// higher: it owns the wgpu-specific lifecycle around a [`CubeKernel`](cubecl_server::kernel::CubeKernel)
440/// kernel — initializing the compiler for a given `wgpu::Backend`, compiling a kernel using
441/// the server's [`WgpuCompilationOptions`], validating the resulting IR against the device,
442/// and projecting the typed representation into the runtime-erased [`AutoRepresentation`].
443pub trait WgpuCompiler: Compiler {
444    /// Check that this compiler can serve the selected graphics backend and device.
445    ///
446    /// Explicit compilers should return an error instead of silently selecting another shader
447    /// language. `requested_backend` preserves the unresolved selection; [`WgpuBackend::Auto`]
448    /// allows automatic API selection. With `msl` enabled, [`AutoCompiler`] requires native MSL
449    /// for an explicit Metal request; otherwise it permits WGSL fallback. Explicit compilers apply
450    /// their own requirements.
451    fn validate_runtime(
452        _backend: wgpu::Backend,
453        _options: &WgpuCompilationOptions,
454        _requested_backend: WgpuBackend,
455    ) -> Result<(), WgpuCompilerInitError> {
456        Ok(())
457    }
458
459    /// Build the compiler instance appropriate for the given `wgpu` backend.
460    ///
461    /// `options` is consulted to decide between alternative implementations (for example, to
462    /// opt into the SPIR-V compiler on Vulkan when the device advertises the required
463    /// features).
464    fn init(backend: wgpu::Backend, options: &WgpuCompilationOptions) -> Self;
465
466    /// Validate that the compiled representation fits within the device's resource limits.
467    ///
468    /// Today this checks shared memory usage; additional checks may be added without
469    /// breaking the contract.
470    fn validate_ir(
471        &self,
472        repr: &Option<Self::Representation>,
473        props: &DeviceProperties,
474    ) -> Result<(), LaunchError>;
475
476    /// Compile a runtime kernel into a [`CompiledKernel`] ready for pipeline creation.
477    ///
478    /// Distinct from [`Compiler::compile`], which only translates a [`KernelDefinition`].
479    /// This entry point operates on a full server-level kernel and pulls compilation
480    /// options from `server`, so its signature cannot collide with the base trait method.
481    fn compile_kernel(
482        &mut self,
483        server: &mut WgpuServer<Self>,
484        kernel: Box<dyn CubeKernel>,
485        definition: KernelDefinition,
486    ) -> Result<CompiledKernel<Self>, CompilationError>;
487
488    /// Normalize the backend-specific representation into the [`AutoRepresentation`] shared
489    /// by every wgpu compiler, and report the [`CompilerInfo`] derived from it.
490    ///
491    /// The [`CompilerInfo`] tells the server which parameter-passing strategy to use for
492    /// the resulting pipeline.
493    fn normalize_repr(
494        &self,
495        repr: Option<Self::Representation>,
496    ) -> (CompilerInfo, Option<AutoRepresentation>);
497}
498
499#[cfg(all(test, feature = "msl"))]
500mod tests {
501    use super::*;
502
503    #[test]
504    fn auto_compiler_falls_back_to_wgsl_when_msl_is_unavailable() {
505        let options = WgpuCompilationOptions::default();
506        assert!(
507            AutoCompiler::validate_runtime(wgpu::Backend::Metal, &options, WgpuBackend::Auto)
508                .is_ok()
509        );
510        let compiler = AutoCompiler::init(wgpu::Backend::Metal, &options);
511
512        assert!(matches!(compiler, AutoCompiler::Wgsl(_)));
513    }
514
515    #[test]
516    fn explicit_msl_compiler_rejects_wgsl_fallback() {
517        for requested_backend in [WgpuBackend::Auto, WgpuBackend::Metal] {
518            let result = MslCompiler::validate_runtime(
519                wgpu::Backend::Metal,
520                &WgpuCompilationOptions::default(),
521                requested_backend,
522            );
523
524            assert_eq!(
525                result,
526                Err(WgpuCompilerInitError::Unavailable {
527                    compiler: "MslCompiler",
528                    backend: wgpu::Backend::Metal,
529                })
530            );
531        }
532    }
533
534    #[test]
535    fn explicit_msl_compiler_rejects_non_metal_backend() {
536        let options = WgpuCompilationOptions {
537            supports_msl_compiler: true,
538            ..Default::default()
539        };
540        let result =
541            MslCompiler::validate_runtime(wgpu::Backend::Vulkan, &options, WgpuBackend::Auto);
542
543        assert_eq!(
544            result,
545            Err(WgpuCompilerInitError::BackendMismatch {
546                compiler: "MslCompiler",
547                expected: wgpu::Backend::Metal,
548                actual: wgpu::Backend::Vulkan,
549            })
550        );
551    }
552
553    #[test]
554    fn explicit_msl_compiler_accepts_supported_metal_device() {
555        let options = WgpuCompilationOptions {
556            supports_msl_compiler: true,
557            ..Default::default()
558        };
559
560        assert!(
561            MslCompiler::validate_runtime(wgpu::Backend::Metal, &options, WgpuBackend::Auto)
562                .is_ok()
563        );
564    }
565
566    #[test]
567    fn explicit_wgsl_on_metal_does_not_require_native_msl() {
568        for supports_msl_compiler in [false, true] {
569            let options = WgpuCompilationOptions {
570                supports_msl_compiler,
571                ..Default::default()
572            };
573            assert!(
574                WgslCompiler::validate_runtime(wgpu::Backend::Metal, &options, WgpuBackend::Metal)
575                    .is_ok()
576            );
577        }
578    }
579
580    #[test]
581    fn auto_compiler_on_explicit_metal_requires_native_msl() {
582        let options = WgpuCompilationOptions::default();
583        assert_eq!(
584            AutoCompiler::validate_runtime(wgpu::Backend::Metal, &options, WgpuBackend::Metal),
585            Err(WgpuCompilerInitError::Unavailable {
586                compiler: "MslCompiler",
587                backend: wgpu::Backend::Metal,
588            })
589        );
590        let supported = WgpuCompilationOptions {
591            supports_msl_compiler: true,
592            ..options
593        };
594        assert!(
595            AutoCompiler::validate_runtime(wgpu::Backend::Metal, &supported, WgpuBackend::Metal)
596                .is_ok()
597        );
598    }
599}