Skip to main content

cubecl_wgpu/backend/
base.rs

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