Skip to main content

cubecl_wgpu/backend/
base.rs

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