Skip to main content

cubecl_wgpu/backend/
base.rs

1use super::wgsl;
2use crate::WgpuServer;
3use crate::{AutoRepresentationRef, CompilerInfo, WgpuCompiler};
4use cubecl_core::{
5    CubeDim, ExecutionMode, WgpuCompilationOptions, hash::StableHash, server::KernelArguments,
6};
7use cubecl_core::{MemoryConfiguration, prelude::Visibility};
8use cubecl_ir::DeviceProperties;
9use cubecl_runtime::{compiler::CompilationError, id::KernelId};
10use std::{borrow::Cow, sync::Arc};
11use wgpu::{
12    Adapter, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BufferBindingType,
13    ComputePipeline, Device, PipelineLayoutDescriptor, Queue, ShaderModule, ShaderModuleDescriptor,
14    ShaderStages,
15};
16
17#[cfg(feature = "spirv")]
18use super::vulkan;
19
20#[cfg(all(feature = "msl", target_os = "macos"))]
21use super::metal;
22
23impl<C: WgpuCompiler> WgpuServer<C> {
24    /// Loads a cached kernel if present and creates the pipeline for it.
25    /// Returns `None` if the cache isn't enabled, `Some(Ok(pipeline))` if a cache entry was found,
26    /// and `Some(Err(cache_key))` if the cache is enabled but doesn't contain this kernel.
27    #[allow(
28        clippy::type_complexity,
29        reason = "required because of error propagation"
30    )]
31    #[allow(unused_variables)]
32    pub fn load_cached_pipeline(
33        &self,
34        kernel_id: &KernelId,
35        bindings: &KernelArguments,
36        mode: ExecutionMode,
37    ) -> Result<
38        Option<Result<(Arc<ComputePipeline>, CompilerInfo), (u64, StableHash)>>,
39        CompilationError,
40    > {
41        #[cfg(not(feature = "spirv"))]
42        let res = Ok(None);
43        #[cfg(feature = "spirv")]
44        let res = if let Some(cache) = &self.spirv_cache {
45            let key = (self.utilities.properties_hash, kernel_id.stable_hash());
46            if let Some(entry) = cache.get(&key) {
47                use crate::ParamsTransfer;
48
49                log::trace!("Using SPIR-V cache");
50
51                let params_transfer = match entry.kernel.immediate_size {
52                    Some(_) => ParamsTransfer::Immediate,
53                    None => ParamsTransfer::Uniform,
54                };
55                let repr = AutoRepresentationRef::SpirV(&entry.kernel);
56                let module = self.create_module(
57                    &entry.entrypoint_name,
58                    kernel_id.cube_dim,
59                    Some(repr),
60                    "",
61                    mode,
62                )?;
63                let pipeline =
64                    self.create_pipeline(&entry.entrypoint_name, Some(repr), module, bindings);
65                Ok(Some(Ok((
66                    pipeline,
67                    CompilerInfo::Vulkan { params_transfer },
68                ))))
69            } else {
70                Ok(Some(Err(key)))
71            }
72        } else {
73            Ok(None)
74        };
75
76        res
77    }
78
79    pub fn create_module(
80        &self,
81        entrypoint_name: &str,
82        cube_dim: CubeDim,
83        repr: Option<AutoRepresentationRef<'_>>,
84        source: &str,
85        mode: ExecutionMode,
86    ) -> Result<ShaderModule, CompilationError> {
87        match repr {
88            #[cfg(feature = "spirv")]
89            Some(AutoRepresentationRef::SpirV(repr)) => unsafe {
90                Ok(self.device.create_shader_module_passthrough(
91                    wgpu::ShaderModuleDescriptorPassthrough {
92                        label: Some(entrypoint_name),
93                        spirv: Some(Cow::Borrowed(&repr.assembled_module)),
94                        entry_points: Cow::Borrowed(&[wgpu::PassthroughShaderEntryPoint {
95                            name: entrypoint_name.into(),
96                            workgroup_size: cube_dim.into(),
97                        }]),
98                        ..Default::default()
99                    },
100                ))
101            },
102            #[cfg(all(feature = "msl", target_os = "macos"))]
103            Some(AutoRepresentationRef::Msl(_)) => unsafe {
104                Ok(self.device.create_shader_module_passthrough(
105                    wgpu::ShaderModuleDescriptorPassthrough {
106                        label: Some(entrypoint_name),
107                        msl: Some(Cow::Borrowed(source)),
108                        entry_points: Cow::Borrowed(&[wgpu::PassthroughShaderEntryPoint {
109                            name: entrypoint_name.into(),
110                            workgroup_size: cube_dim.into(),
111                        }]),
112                        ..Default::default()
113                    },
114                ))
115            },
116            _ => {
117                let _ = cube_dim;
118                let checks = wgpu::ShaderRuntimeChecks {
119                    // Cube does not need wgpu bounds checks - OOB behaviour is instead
120                    // checked by cube (if enabled).
121                    // This is because the WebGPU specification only makes loose guarantees that Cube can't rely on.
122                    bounds_checks: false,
123                    // Loop bounds are only checked in checked mode.
124                    force_loop_bounding: mode == ExecutionMode::Checked,
125                    ..wgpu::ShaderRuntimeChecks::unchecked()
126                };
127
128                log::trace!("[cubecl-wgpu] compiling WGSL module `{entrypoint_name}`\n{source}");
129
130                let error_scope = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
131
132                // SAFETY: Cube guarantees OOB safety when launching in checked mode. Launching in unchecked mode
133                // is only available through the use of unsafe code.
134                let module = unsafe {
135                    self.device.create_shader_module_trusted(
136                        ShaderModuleDescriptor {
137                            label: Some(entrypoint_name),
138                            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(source)),
139                        },
140                        checks,
141                    )
142                };
143
144                // `pop()` detaches from the LIFO stack immediately; only the
145                // result is async. Safe to interleave with other push/pops.
146                let err_future = error_scope.pop();
147
148                #[cfg(not(target_family = "wasm"))]
149                if let Some(err) = cubecl_common::future::block_on(err_future) {
150                    log::error!(
151                        "[cubecl-wgpu] WGSL compilation failed for kernel `{entrypoint_name}`:\n{err}\n--- shader source ({} bytes) ---\n{source}\n--- end shader ---",
152                        source.len()
153                    );
154                    return Err(CompilationError::Generic {
155                        reason: format!(
156                            "WGSL compilation failed for kernel `{entrypoint_name}`: {err}"
157                        ),
158                        backtrace: cubecl_common::backtrace::BackTrace::capture(),
159                    });
160                }
161
162                // On wasm we can't block; spawn a task that awaits the pop
163                // future and logs.
164                #[cfg(target_family = "wasm")]
165                {
166                    let entrypoint_name = entrypoint_name.to_string();
167                    let source = source.to_string();
168                    wasm_bindgen_futures::spawn_local(async move {
169                        if let Some(err) = err_future.await {
170                            log::error!(
171                                "[cubecl-wgpu] WGSL compilation failed for kernel `{entrypoint_name}`:\n{err}\n--- shader source ({} bytes) ---\n{source}\n--- end shader ---",
172                                source.len()
173                            );
174                        }
175                    });
176                }
177
178                Ok(module)
179            }
180        }
181    }
182
183    #[allow(unused_variables)]
184    pub fn create_pipeline(
185        &self,
186        entrypoint_name: &str,
187        repr: Option<AutoRepresentationRef<'_>>,
188        module: ShaderModule,
189        bindings: &KernelArguments,
190    ) -> Arc<ComputePipeline> {
191        let bindings_info = match repr {
192            Some(AutoRepresentationRef::Wgsl(repr)) => Some(wgsl::bindings(repr, bindings)),
193            #[cfg(all(feature = "msl", target_os = "macos"))]
194            Some(AutoRepresentationRef::Msl(repr)) => Some(metal::bindings(repr, bindings)),
195            #[cfg(feature = "spirv")]
196            Some(AutoRepresentationRef::SpirV(repr)) => Some(vulkan::bindings(repr, bindings)),
197            _ => None,
198        };
199
200        let layout = bindings_info.map(|(bindings, immediate_size)| {
201            if !bindings.is_empty() {
202                let bindings = bindings
203                    .into_iter()
204                    .map(|visibility| match visibility {
205                        Visibility::Uniform => BufferBindingType::Uniform,
206                        Visibility::Read => BufferBindingType::Storage { read_only: true },
207                        Visibility::ReadWrite => BufferBindingType::Storage { read_only: false },
208                    })
209                    .enumerate()
210                    .map(|(i, ty)| BindGroupLayoutEntry {
211                        binding: i as u32,
212                        visibility: ShaderStages::COMPUTE,
213                        ty: BindingType::Buffer {
214                            ty,
215                            has_dynamic_offset: false,
216                            min_binding_size: None,
217                        },
218                        count: None,
219                    })
220                    .collect::<Vec<_>>();
221                let layout = self
222                    .device
223                    .create_bind_group_layout(&BindGroupLayoutDescriptor {
224                        label: None,
225                        entries: &bindings,
226                    });
227                self.device
228                    .create_pipeline_layout(&PipelineLayoutDescriptor {
229                        label: None,
230                        bind_group_layouts: &[Some(&layout)],
231                        immediate_size: immediate_size as u32,
232                    })
233            } else {
234                self.device
235                    .create_pipeline_layout(&PipelineLayoutDescriptor {
236                        label: None,
237                        bind_group_layouts: &[],
238                        immediate_size: immediate_size as u32,
239                    })
240            }
241        });
242
243        let pipeline = self
244            .device
245            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
246                label: Some(entrypoint_name),
247                layout: layout.as_ref(),
248                module: &module,
249                entry_point: Some(entrypoint_name),
250                compilation_options: wgpu::PipelineCompilationOptions {
251                    zero_initialize_workgroup_memory: false,
252                    ..Default::default()
253                },
254                cache: None,
255            });
256        Arc::new(pipeline)
257    }
258}
259
260pub async fn request_device(adapter: &Adapter) -> (Device, Queue) {
261    if let Some(result) = request_vulkan_device(adapter).await {
262        return result;
263    }
264    if let Some(result) = request_metal_device(adapter).await {
265        return result;
266    }
267    wgsl::request_device(adapter).await
268}
269
270#[cfg(feature = "spirv")]
271async fn request_vulkan_device(adapter: &Adapter) -> Option<(Device, Queue)> {
272    if is_vulkan(adapter) {
273        vulkan::request_vulkan_device(adapter).await
274    } else {
275        None
276    }
277}
278
279#[cfg(not(feature = "spirv"))]
280async fn request_vulkan_device(_adapter: &Adapter) -> Option<(Device, Queue)> {
281    None
282}
283
284#[cfg(all(feature = "msl", target_os = "macos"))]
285async fn request_metal_device(adapter: &Adapter) -> Option<(Device, Queue)> {
286    if is_metal(adapter) {
287        Some(metal::request_metal_device(adapter).await)
288    } else {
289        None
290    }
291}
292
293#[cfg(not(all(feature = "msl", target_os = "macos")))]
294async fn request_metal_device(_adapter: &Adapter) -> Option<(Device, Queue)> {
295    None
296}
297
298pub fn register_features(
299    adapter: &Adapter,
300    props: &mut DeviceProperties,
301    comp_options: &mut WgpuCompilationOptions,
302    memory_config: &MemoryConfiguration,
303) {
304    if register_vulkan_features(adapter, props, comp_options, memory_config) {
305        return;
306    }
307    if register_metal_features(adapter, props, comp_options, memory_config) {
308        return;
309    }
310    wgsl::register_wgsl_features(adapter, props, comp_options);
311}
312
313#[cfg(feature = "spirv")]
314pub fn register_vulkan_features(
315    adapter: &Adapter,
316    props: &mut DeviceProperties,
317    comp_options: &mut WgpuCompilationOptions,
318    memory_config: &MemoryConfiguration,
319) -> bool {
320    if is_vulkan(adapter) {
321        vulkan::register_vulkan_features(adapter, props, comp_options, memory_config)
322    } else {
323        false
324    }
325}
326
327#[cfg(not(feature = "spirv"))]
328pub fn register_vulkan_features(
329    _adapter: &Adapter,
330    _props: &mut DeviceProperties,
331    _comp_options: &mut WgpuCompilationOptions,
332    _memory_config: &MemoryConfiguration,
333) -> bool {
334    false
335}
336
337#[cfg(all(feature = "msl", target_os = "macos"))]
338pub fn register_metal_features(
339    adapter: &Adapter,
340    props: &mut DeviceProperties,
341    comp_options: &mut WgpuCompilationOptions,
342    _memory_config: &MemoryConfiguration,
343) -> bool {
344    if is_metal(adapter) {
345        metal::register_metal_features(adapter, props, comp_options);
346        true
347    } else {
348        false
349    }
350}
351
352#[cfg(not(all(feature = "msl", target_os = "macos")))]
353pub fn register_metal_features(
354    _adapter: &Adapter,
355    _props: &mut DeviceProperties,
356    _comp_options: &mut WgpuCompilationOptions,
357    _memory_config: &MemoryConfiguration,
358) -> bool {
359    false
360}
361
362#[cfg(feature = "spirv")]
363fn is_vulkan(adapter: &Adapter) -> bool {
364    unsafe { adapter.as_hal::<wgpu::hal::api::Vulkan>().is_some() }
365}
366
367#[cfg(all(feature = "msl", target_os = "macos"))]
368fn is_metal(adapter: &Adapter) -> bool {
369    unsafe { adapter.as_hal::<wgpu::hal::api::Metal>().is_some() }
370}