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