Skip to main content

ruda_driver_wgpu/backend/base/
pipeline.rs

1use super::*;
2
3impl WgpuServer {
4    /// Loads a cached kernel if present and creates the pipeline for it.
5    /// Returns `None` if the cache isn't enabled, `Some(Ok(pipeline))` if a cache entry was found,
6    /// and `Some(Err(cache_key))` if the cache is enabled but doesn't contain this kernel.
7    #[allow(
8        clippy::type_complexity,
9        reason = "required because of error propagation"
10    )]
11    #[allow(unused_variables)]
12    pub fn load_cached_pipeline(
13        &self,
14        kernel_id: &KernelId,
15        bindings: &KernelArguments,
16        mode: ExecutionMode,
17    ) -> Result<Option<Result<Arc<ComputePipeline>, (u64, StableHash)>>, CompilationError> {
18        #[cfg(not(feature = "spirv"))]
19        let res = Ok(None);
20        #[cfg(feature = "spirv")]
21        let res = if let Some(cache) = &self.spirv_cache {
22            let key = (self.utilities.properties_hash, kernel_id.stable_hash());
23            if let Some(entry) = cache.get(&key) {
24                log::trace!("Using SPIR-V cache");
25
26                let repr = AutoRepresentationRef::SpirV(&entry.kernel);
27                let module = self.create_module(&entry.entrypoint_name, Some(repr), "", mode)?;
28                let pipeline =
29                    self.create_pipeline(&entry.entrypoint_name, Some(repr), module, bindings);
30                Ok(Some(Ok(pipeline)))
31            } else {
32                Ok(Some(Err(key)))
33            }
34        } else {
35            Ok(None)
36        };
37
38        res
39    }
40
41    pub fn create_module(
42        &self,
43        entrypoint_name: &str,
44        repr: Option<AutoRepresentationRef<'_>>,
45        source: &str,
46        mode: ExecutionMode,
47    ) -> Result<ShaderModule, CompilationError> {
48        match repr {
49            #[cfg(feature = "spirv")]
50            Some(AutoRepresentationRef::SpirV(repr)) => unsafe {
51                Ok(self.device.create_shader_module_passthrough(
52                    wgpu::ShaderModuleDescriptorPassthrough {
53                        label: Some(entrypoint_name),
54                        spirv: Some(Cow::Borrowed(&repr.assembled_module)),
55                        ..Default::default()
56                    },
57                ))
58            },
59            #[cfg(all(feature = "msl", target_os = "macos"))]
60            Some(AutoRepresentationRef::Msl(repr)) => unsafe {
61                Ok(self.device.create_shader_module_passthrough(
62                    wgpu::ShaderModuleDescriptorPassthrough {
63                        label: Some(entrypoint_name),
64                        msl: Some(Cow::Borrowed(source)),
65                        num_workgroups: (repr.ruda_dim.x, repr.ruda_dim.y, repr.ruda_dim.z),
66                        ..Default::default()
67                    },
68                ))
69            },
70            _ => {
71                let checks = wgpu::ShaderRuntimeChecks {
72                    // Ruda does not need wgpu bounds checks - OOB behaviour is instead
73                    // checked by ruda (if enabled).
74                    // This is because the WebGPU specification only makes loose guarantees that Ruda can't rely on.
75                    bounds_checks: false,
76                    // Loop bounds are only checked in checked mode.
77                    force_loop_bounding: mode == ExecutionMode::Checked,
78                    ..wgpu::ShaderRuntimeChecks::unchecked()
79                };
80
81                log::trace!("[ruda-driver-wgpu] compiling WGSL module `{entrypoint_name}`\n{source}");
82
83                let error_scope = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
84
85                // SAFETY: Ruda guarantees OOB safety when launching in checked mode. Launching in unchecked mode
86                // is only available through the use of unsafe code.
87                let module = unsafe {
88                    self.device.create_shader_module_trusted(
89                        ShaderModuleDescriptor {
90                            label: Some(entrypoint_name),
91                            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(source)),
92                        },
93                        checks,
94                    )
95                };
96
97                // `pop()` detaches from the LIFO stack immediately; only the
98                // result is async. Safe to interleave with other push/pops.
99                let err_future = error_scope.pop();
100
101                #[cfg(not(target_family = "wasm"))]
102                if let Some(err) = ruda_core::future::block_on(err_future) {
103                    log::error!(
104                        "[ruda-driver-wgpu] WGSL compilation failed for kernel `{entrypoint_name}`:\n{err}\n--- shader source ({} bytes) ---\n{source}\n--- end shader ---",
105                        source.len()
106                    );
107                    return Err(CompilationError::Generic {
108                        reason: format!(
109                            "WGSL compilation failed for kernel `{entrypoint_name}`: {err}"
110                        ),
111                        backtrace: ruda_core::backtrace::BackTrace::capture(),
112                    });
113                }
114
115                // On wasm we can't block; spawn a task that awaits the pop
116                // future and logs.
117                #[cfg(target_family = "wasm")]
118                {
119                    let entrypoint_name = entrypoint_name.to_string();
120                    let source = source.to_string();
121                    wasm_bindgen_futures::spawn_local(async move {
122                        if let Some(err) = err_future.await {
123                            log::error!(
124                                "[ruda-driver-wgpu] WGSL compilation failed for kernel `{entrypoint_name}`:\n{err}\n--- shader source ({} bytes) ---\n{source}\n--- end shader ---",
125                                source.len()
126                            );
127                        }
128                    });
129                }
130
131                Ok(module)
132            }
133        }
134    }
135
136    #[allow(unused_variables)]
137    pub fn create_pipeline(
138        &self,
139        entrypoint_name: &str,
140        repr: Option<AutoRepresentationRef<'_>>,
141        module: ShaderModule,
142        bindings: &KernelArguments,
143    ) -> Arc<ComputePipeline> {
144        let bindings_info = match repr {
145            Some(AutoRepresentationRef::Wgsl(repr)) => Some(wgsl::bindings(repr, bindings)),
146            #[cfg(all(feature = "msl", target_os = "macos"))]
147            Some(AutoRepresentationRef::Msl(repr)) => Some(cpp_metal::bindings(repr, bindings)),
148            #[cfg(feature = "spirv")]
149            Some(AutoRepresentationRef::SpirV(repr)) => Some(vulkan::bindings(repr, bindings)),
150            _ => None,
151        };
152
153        let layout = bindings_info.map(|bindings| {
154            let (mut bindings, info, uniform_info) = bindings;
155            // When slices are shared, it needs to be read-write if ANY of the slices is read-write,
156            // and since we can't be sure, we'll assume everything is read-write.
157            if !cfg!(exclusive_memory_only) {
158                bindings.fill(ruda::runtime::kernel::Visibility::ReadWrite);
159            }
160
161            let info = info.map(|_| match uniform_info {
162                true => BufferBindingType::Uniform,
163                false => BufferBindingType::Storage { read_only: true },
164            });
165
166            let bindings = bindings
167                .into_iter()
168                .map(|visibility| BufferBindingType::Storage {
169                    read_only: matches!(visibility, ruda::runtime::kernel::Visibility::Read),
170                })
171                .chain(info)
172                .enumerate()
173                .map(|(i, ty)| BindGroupLayoutEntry {
174                    binding: i as u32,
175                    visibility: ShaderStages::COMPUTE,
176                    ty: BindingType::Buffer {
177                        ty,
178                        has_dynamic_offset: false,
179                        min_binding_size: None,
180                    },
181                    count: None,
182                })
183                .collect::<Vec<_>>();
184            let layout = self
185                .device
186                .create_bind_group_layout(&BindGroupLayoutDescriptor {
187                    label: None,
188                    entries: &bindings,
189                });
190            self.device
191                .create_pipeline_layout(&PipelineLayoutDescriptor {
192                    label: None,
193                    bind_group_layouts: &[Some(&layout)],
194                    immediate_size: 0,
195                })
196        });
197
198        let pipeline = self
199            .device
200            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
201                label: Some(entrypoint_name),
202                layout: layout.as_ref(),
203                module: &module,
204                entry_point: Some(entrypoint_name),
205                compilation_options: wgpu::PipelineCompilationOptions {
206                    zero_initialize_workgroup_memory: false,
207                    ..Default::default()
208                },
209                cache: None,
210            });
211        Arc::new(pipeline)
212    }
213}