Skip to main content

cubecl_wgpu/backend/
base.rs

1use super::wgsl;
2use crate::{AutoRepresentationRef, WgpuCompiler, WgpuServer};
3use cubecl_core::{
4    CubeDim, ExecutionMode, WgpuCompilationOptions, prelude::Visibility, server::KernelArguments,
5};
6use cubecl_ir::{DeviceProperties, PhysicalDevice};
7use cubecl_server::{
8    compiler::{CompilationError, KernelCacheKey},
9    id::KernelId,
10};
11use std::{borrow::Cow, sync::Arc};
12use wgpu::{
13    Adapter, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BufferBindingType,
14    ComputePipeline, Device, PipelineLayoutDescriptor, Queue, ShaderModule, ShaderModuleDescriptor,
15    ShaderStages,
16};
17
18#[cfg(feature = "spirv")]
19use super::vulkan;
20
21#[cfg(all(feature = "msl", target_os = "macos"))]
22use super::metal;
23
24#[cfg(windows)]
25use super::dx12;
26
27#[cfg(target_vendor = "apple")]
28use super::metal_card;
29
30/// What a shader module is built from: the compiler's representation and the
31/// source text, reconciled.
32///
33/// A compiled kernel carries both, and the representation says which the
34/// device wants. A precompiled kernel carries only text, so the language it
35/// was tagged with decides instead.
36pub enum ModuleSource<'a> {
37    /// An assembled SPIR-V module, handed to the driver as is.
38    #[cfg(feature = "spirv")]
39    SpirV(&'a cubecl_spirv::SpirvKernel),
40    /// Metal Shading Language text, handed to the driver as is.
41    #[cfg(all(feature = "msl", target_os = "macos"))]
42    Msl(&'a str),
43    /// WGSL text, for naga to compile.
44    Wgsl(&'a str),
45}
46
47impl<'a> ModuleSource<'a> {
48    /// Pairs `repr` with `source`, or, when there is no representation, reads
49    /// the language off `lang`, the tag the precompiled kernel was accepted
50    /// under.
51    pub fn resolve(
52        repr: Option<AutoRepresentationRef<'a>>,
53        lang: &str,
54        source: &'a str,
55    ) -> Result<Self, CompilationError> {
56        match repr {
57            #[cfg(feature = "spirv")]
58            Some(AutoRepresentationRef::SpirV(repr)) => Ok(Self::SpirV(repr)),
59            Some(AutoRepresentationRef::Wgsl(_)) => Ok(Self::Wgsl(source)),
60            #[cfg(feature = "msl")]
61            Some(AutoRepresentationRef::Msl(_)) => Self::msl(source),
62            None => match lang {
63                "wgsl" => Ok(Self::Wgsl(source)),
64                "msl" => Self::msl(source),
65                other => Err(CompilationError::Generic {
66                    reason: format!(
67                        "wgpu has no text passthrough for a precompiled `{other}` kernel"
68                    ),
69                    backtrace: cubecl_environment::backtrace::BackTrace::capture(),
70                }),
71            },
72        }
73    }
74
75    #[cfg(all(feature = "msl", target_os = "macos"))]
76    fn msl(source: &'a str) -> Result<Self, CompilationError> {
77        Ok(Self::Msl(source))
78    }
79
80    #[cfg(not(all(feature = "msl", target_os = "macos")))]
81    fn msl(_source: &'a str) -> Result<Self, CompilationError> {
82        Err(CompilationError::Generic {
83            reason: "MSL passthrough is only available on macOS".to_string(),
84            backtrace: cubecl_environment::backtrace::BackTrace::capture(),
85        })
86    }
87}
88
89impl<C: WgpuCompiler> WgpuServer<C> {
90    /// Loads a cached kernel if present and creates the pipeline for it.
91    /// Returns `None` if the cache isn't enabled, `Some(Ok(pipeline))` if a cache entry was found,
92    /// and `Some(Err(cache_key))` if the cache is enabled but doesn't contain this kernel.
93    #[allow(
94        clippy::type_complexity,
95        reason = "required because of error propagation"
96    )]
97    #[allow(unused_variables)]
98    pub fn load_cached_pipeline(
99        &mut self,
100        kernel_id: &KernelId,
101        bindings: &KernelArguments,
102        mode: ExecutionMode,
103    ) -> Result<
104        Option<Result<crate::compute::PipelineEntry, (u64, KernelCacheKey)>>,
105        CompilationError,
106    > {
107        #[cfg(not(feature = "spirv"))]
108        let res = Ok(None);
109        #[cfg(feature = "spirv")]
110        let res = if let Some(cache) = self.spirv_cache.as_mut() {
111            let key = (
112                self.utilities.properties_hash,
113                KernelCacheKey::new(kernel_id, self.build_id),
114            );
115            if let Some(entry) = cache.remove(&key) {
116                use crate::ParamsTransfer;
117
118                log::trace!("Using SPIR-V cache");
119
120                let params_transfer = match entry.kernel.immediate_size {
121                    Some(_) => ParamsTransfer::Immediate,
122                    None => ParamsTransfer::Uniform,
123                };
124                let repr = AutoRepresentationRef::SpirV(&entry.kernel);
125                let module = self.create_module(
126                    &entry.entrypoint_name,
127                    kernel_id.cube_dim.into(),
128                    ModuleSource::SpirV(&entry.kernel),
129                    mode,
130                )?;
131                let pipeline =
132                    self.create_pipeline(&entry.entrypoint_name, Some(repr), module, bindings)?;
133                let io = entry.kernel.io.clone().map(std::sync::Arc::from);
134                Ok(Some(Ok((
135                    pipeline,
136                    crate::compute::CompilerInfo::Vulkan { params_transfer },
137                    io,
138                ))))
139            } else {
140                Ok(Some(Err(key)))
141            }
142        } else {
143            Ok(None)
144        };
145
146        res
147    }
148
149    pub fn create_module(
150        &self,
151        entrypoint_name: &str,
152        cube_dim: CubeDim,
153        source: ModuleSource<'_>,
154        mode: ExecutionMode,
155    ) -> Result<ShaderModule, CompilationError> {
156        match source {
157            #[cfg(feature = "spirv")]
158            ModuleSource::SpirV(repr) => self
159                .validated(|| unsafe {
160                    self.device.create_shader_module_passthrough(
161                        wgpu::ShaderModuleDescriptorPassthrough {
162                            label: Some(entrypoint_name),
163                            spirv: Some(Cow::Borrowed(&repr.assembled_module)),
164                            entry_points: Cow::Borrowed(&[wgpu::PassthroughShaderEntryPoint {
165                                name: entrypoint_name.into(),
166                                workgroup_size: cube_dim.into(),
167                            }]),
168                            ..Default::default()
169                        },
170                    )
171                })
172                .map_err(|err| refused("SPIR-V module", entrypoint_name, err)),
173            #[cfg(all(feature = "msl", target_os = "macos"))]
174            ModuleSource::Msl(source) => self
175                .validated(|| unsafe {
176                    self.device.create_shader_module_passthrough(
177                        wgpu::ShaderModuleDescriptorPassthrough {
178                            label: Some(entrypoint_name),
179                            msl: Some(Cow::Borrowed(source)),
180                            entry_points: Cow::Borrowed(&[wgpu::PassthroughShaderEntryPoint {
181                                name: entrypoint_name.into(),
182                                workgroup_size: cube_dim.into(),
183                            }]),
184                            ..Default::default()
185                        },
186                    )
187                })
188                .map_err(|err| refused("MSL module", entrypoint_name, err)),
189            ModuleSource::Wgsl(source) => {
190                let _ = cube_dim;
191                let checks = wgpu::ShaderRuntimeChecks {
192                    // Cube does not need wgpu bounds checks - OOB behaviour is instead
193                    // checked by cube (if enabled).
194                    // This is because the WebGPU specification only makes loose guarantees that Cube can't rely on.
195                    bounds_checks: false,
196                    // Loop bounds are only checked in checked mode.
197                    force_loop_bounding: mode == ExecutionMode::Checked,
198                    ..wgpu::ShaderRuntimeChecks::unchecked()
199                };
200
201                log::trace!("[cubecl-wgpu] compiling WGSL module `{entrypoint_name}`\n{source}");
202
203                let error_scope = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
204
205                // SAFETY: Cube guarantees OOB safety when launching in checked mode. Launching in unchecked mode
206                // is only available through the use of unsafe code.
207                let module = unsafe {
208                    self.device.create_shader_module_trusted(
209                        ShaderModuleDescriptor {
210                            label: Some(entrypoint_name),
211                            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(source)),
212                        },
213                        checks,
214                    )
215                };
216
217                // `pop()` detaches from the LIFO stack immediately; only the
218                // result is async. Safe to interleave with other push/pops.
219                let err_future = error_scope.pop();
220
221                #[cfg(not(target_family = "wasm"))]
222                if let Some(err) = cubecl_environment::future::block_on(err_future) {
223                    log::error!(
224                        "[cubecl-wgpu] WGSL compilation failed for kernel `{entrypoint_name}`:\n{err}\n--- shader source ({} bytes) ---\n{source}\n--- end shader ---",
225                        source.len()
226                    );
227                    return Err(CompilationError::Generic {
228                        reason: format!(
229                            "WGSL compilation failed for kernel `{entrypoint_name}`: {err}"
230                        ),
231                        backtrace: cubecl_environment::backtrace::BackTrace::capture(),
232                    });
233                }
234
235                // On wasm we can't block; spawn a task that awaits the pop
236                // future and logs.
237                #[cfg(target_family = "wasm")]
238                {
239                    let entrypoint_name = entrypoint_name.to_string();
240                    let source = source.to_string();
241                    wasm_bindgen_futures::spawn_local(async move {
242                        if let Some(err) = err_future.await {
243                            log::error!(
244                                "[cubecl-wgpu] WGSL compilation failed for kernel `{entrypoint_name}`:\n{err}\n--- shader source ({} bytes) ---\n{source}\n--- end shader ---",
245                                source.len()
246                            );
247                        }
248                    });
249                }
250
251                Ok(module)
252            }
253        }
254    }
255
256    #[allow(unused_variables)]
257    pub fn create_pipeline(
258        &self,
259        entrypoint_name: &str,
260        repr: Option<AutoRepresentationRef<'_>>,
261        module: ShaderModule,
262        bindings: &KernelArguments,
263    ) -> Result<Arc<ComputePipeline>, CompilationError> {
264        let bindings_info = match repr {
265            Some(AutoRepresentationRef::Wgsl(repr)) => Some(wgsl::bindings(repr, bindings)),
266            #[cfg(all(feature = "msl", target_os = "macos"))]
267            Some(AutoRepresentationRef::Msl(repr)) => Some(metal::bindings(repr, bindings)),
268            #[cfg(feature = "spirv")]
269            Some(AutoRepresentationRef::SpirV(repr)) => Some(vulkan::bindings(repr, bindings)),
270            _ => None,
271        };
272
273        let create = || {
274            let layout = bindings_info.map(|(bindings, immediate_size)| {
275                if !bindings.is_empty() {
276                    let bindings = bindings
277                        .into_iter()
278                        .map(|visibility| match visibility {
279                            Visibility::Uniform => BufferBindingType::Uniform,
280                            Visibility::Read => BufferBindingType::Storage { read_only: true },
281                            Visibility::ReadWrite => {
282                                BufferBindingType::Storage { read_only: false }
283                            }
284                        })
285                        .enumerate()
286                        .map(|(i, ty)| BindGroupLayoutEntry {
287                            binding: i as u32,
288                            visibility: ShaderStages::COMPUTE,
289                            ty: BindingType::Buffer {
290                                ty,
291                                has_dynamic_offset: false,
292                                min_binding_size: None,
293                            },
294                            count: None,
295                        })
296                        .collect::<Vec<_>>();
297                    let layout = self
298                        .device
299                        .create_bind_group_layout(&BindGroupLayoutDescriptor {
300                            label: None,
301                            entries: &bindings,
302                        });
303                    self.device
304                        .create_pipeline_layout(&PipelineLayoutDescriptor {
305                            label: None,
306                            bind_group_layouts: &[Some(&layout)],
307                            immediate_size: immediate_size as u32,
308                        })
309                } else {
310                    self.device
311                        .create_pipeline_layout(&PipelineLayoutDescriptor {
312                            label: None,
313                            bind_group_layouts: &[],
314                            immediate_size: immediate_size as u32,
315                        })
316                }
317            });
318
319            let pipeline = self
320                .device
321                .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
322                    label: Some(entrypoint_name),
323                    layout: layout.as_ref(),
324                    module: &module,
325                    entry_point: Some(entrypoint_name),
326                    compilation_options: wgpu::PipelineCompilationOptions {
327                        zero_initialize_workgroup_memory: false,
328                        ..Default::default()
329                    },
330                    cache: None,
331                });
332            Arc::new(pipeline)
333        };
334        self.validated(create)
335            .map_err(|err| refused("pipeline", entrypoint_name, err))
336    }
337
338    /// Creates a device object under validation and internal error scopes, and returns what the
339    /// device reported against it.
340    ///
341    /// wgpu reports a creation it refuses as an uncaptured device error, which panics the thread
342    /// polling the device and leaves a read of the launch's outputs returning whatever they held.
343    /// The scopes make the refusal the launch's error, which the outputs then carry. Blocks until
344    /// the device has validated the creation; on wasm, which cannot block, the creation is
345    /// unchecked.
346    fn validated<T>(&self, create: impl FnOnce() -> T) -> Result<T, wgpu::Error> {
347        #[cfg(target_family = "wasm")]
348        return Ok(create());
349
350        #[cfg(not(target_family = "wasm"))]
351        {
352            // A passthrough module the backend fails to compile is reported as internal, a
353            // layout past the device's limits as validation. Both are refusals.
354            let validation = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
355            let internal = self.device.push_error_scope(wgpu::ErrorFilter::Internal);
356            let created = create();
357            let internal = internal.pop();
358            let validation = validation.pop();
359            let refusal =
360                cubecl_environment::future::block_on(async { internal.await.or(validation.await) });
361            match refusal {
362                Some(err) => Err(err),
363                None => Ok(created),
364            }
365        }
366    }
367}
368
369/// The error a launch returns when the device refuses to create one of its objects.
370fn refused(object: &str, entrypoint_name: &str, err: wgpu::Error) -> CompilationError {
371    log::error!(
372        "[cubecl-wgpu] the device refused the {object} of kernel `{entrypoint_name}`: {err}"
373    );
374    CompilationError::Generic {
375        reason: format!("the device refused the {object} of kernel `{entrypoint_name}`: {err}"),
376        backtrace: cubecl_environment::backtrace::BackTrace::capture(),
377    }
378}
379
380/// Request a device, returning device creation failures.
381pub async fn try_request_device(
382    adapter: &Adapter,
383) -> Result<(Device, Queue), crate::WgpuInitError> {
384    if let Some(result) = request_vulkan_device(adapter).await? {
385        return Ok(result);
386    }
387    if let Some(result) = request_metal_device(adapter).await? {
388        return Ok(result);
389    }
390    wgsl::try_request_device(adapter).await
391}
392
393#[cfg(feature = "spirv")]
394async fn request_vulkan_device(
395    adapter: &Adapter,
396) -> Result<Option<(Device, Queue)>, crate::WgpuInitError> {
397    if is_vulkan(adapter) {
398        vulkan::try_request_vulkan_device(adapter).await
399    } else {
400        Ok(None)
401    }
402}
403
404#[cfg(not(feature = "spirv"))]
405async fn request_vulkan_device(
406    _adapter: &Adapter,
407) -> Result<Option<(Device, Queue)>, crate::WgpuInitError> {
408    Ok(None)
409}
410
411#[cfg(all(feature = "msl", target_os = "macos"))]
412async fn request_metal_device(
413    adapter: &Adapter,
414) -> Result<Option<(Device, Queue)>, crate::WgpuInitError> {
415    if is_metal(adapter) {
416        metal::try_request_metal_device(adapter).await.map(Some)
417    } else {
418        Ok(None)
419    }
420}
421
422#[cfg(not(all(feature = "msl", target_os = "macos")))]
423async fn request_metal_device(
424    _adapter: &Adapter,
425) -> Result<Option<(Device, Queue)>, crate::WgpuInitError> {
426    Ok(None)
427}
428
429pub fn register_features(
430    adapter: &Adapter,
431    props: &mut DeviceProperties,
432    comp_options: &mut WgpuCompilationOptions,
433) {
434    if register_vulkan_features(adapter, props, comp_options) {
435        return;
436    }
437    if register_metal_features(adapter, props, comp_options) {
438        return;
439    }
440    wgsl::register_wgsl_features(adapter, props, comp_options);
441}
442
443#[cfg(feature = "spirv")]
444pub fn register_vulkan_features(
445    adapter: &Adapter,
446    props: &mut DeviceProperties,
447    comp_options: &mut WgpuCompilationOptions,
448) -> bool {
449    if is_vulkan(adapter) {
450        vulkan::register_vulkan_features(adapter, props, comp_options)
451    } else {
452        false
453    }
454}
455
456#[cfg(not(feature = "spirv"))]
457pub fn register_vulkan_features(
458    _adapter: &Adapter,
459    _props: &mut DeviceProperties,
460    _comp_options: &mut WgpuCompilationOptions,
461) -> bool {
462    false
463}
464
465#[cfg(all(feature = "msl", target_os = "macos"))]
466pub fn register_metal_features(
467    adapter: &Adapter,
468    props: &mut DeviceProperties,
469    comp_options: &mut WgpuCompilationOptions,
470) -> bool {
471    if is_metal(adapter) {
472        metal::register_metal_features(adapter, props, comp_options)
473    } else {
474        false
475    }
476}
477
478#[cfg(not(all(feature = "msl", target_os = "macos")))]
479pub fn register_metal_features(
480    _adapter: &Adapter,
481    _props: &mut DeviceProperties,
482    _comp_options: &mut WgpuCompilationOptions,
483) -> bool {
484    false
485}
486
487/// The card behind `adapter`, `None` for a software adapter, which is no card at all.
488#[cfg_attr(
489    not(any(feature = "spirv", windows, target_vendor = "apple")),
490    expect(unused_variables)
491)]
492pub fn physical_device(adapter: &Adapter, info: &wgpu::AdapterInfo) -> Option<PhysicalDevice> {
493    if info.device_type == wgpu::DeviceType::Cpu {
494        return None;
495    }
496    let mut physical = PhysicalDevice::default();
497    // Metal and WebGPU report a zero vendor rather than none.
498    physical.vendor = (info.vendor != 0).then(|| info.vendor.into());
499    // wgpu gives a DX12 adapter the address of the first card with its vendor and device id, so
500    // identical cards would share one.
501    if info.backend == wgpu::Backend::Vulkan {
502        physical.pci_address = info.device_pci_bus_id.parse().ok();
503    }
504    #[cfg(feature = "spirv")]
505    if is_vulkan(adapter) {
506        vulkan::describe_card(adapter, &mut physical);
507    }
508    #[cfg(windows)]
509    dx12::describe_card(adapter, &mut physical);
510    #[cfg(target_vendor = "apple")]
511    metal_card::describe_card(adapter, &mut physical);
512    Some(physical)
513}
514
515#[cfg(feature = "spirv")]
516fn is_vulkan(adapter: &Adapter) -> bool {
517    unsafe { adapter.as_hal::<wgpu::hal::api::Vulkan>().is_some() }
518}
519
520#[cfg(all(feature = "msl", target_os = "macos"))]
521fn is_metal(adapter: &Adapter) -> bool {
522    unsafe { adapter.as_hal::<wgpu::hal::api::Metal>().is_some() }
523}