1use super::wgsl;
2use crate::{AutoRepresentationRef, WgpuCompiler, WgpuServer};
3use cubecl_core::{
4 CubeDim, ExecutionMode, WgpuCompilationOptions, prelude::Visibility, server::KernelArguments,
5};
6use cubecl_ir::{DeviceProperties, PhysicalDevice};
7use cubecl_server::{
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
24#[cfg(windows)]
25use super::dx12;
26
27#[cfg(target_vendor = "apple")]
28use super::metal_card;
29
30pub enum ModuleSource<'a> {
37 #[cfg(feature = "spirv")]
39 SpirV(&'a cubecl_spirv::SpirvKernel),
40 #[cfg(all(feature = "msl", target_os = "macos"))]
42 Msl(&'a str),
43 Wgsl(&'a str),
45}
46
47impl<'a> ModuleSource<'a> {
48 pub fn resolve(
52 repr: Option<AutoRepresentationRef<'a>>,
53 lang: &str,
54 source: &'a str,
55 ) -> Result<Self, CompilationError> {
56 match repr {
57 #[cfg(feature = "spirv")]
58 Some(AutoRepresentationRef::SpirV(repr)) => Ok(Self::SpirV(repr)),
59 Some(AutoRepresentationRef::Wgsl(_)) => Ok(Self::Wgsl(source)),
60 #[cfg(feature = "msl")]
61 Some(AutoRepresentationRef::Msl(_)) => Self::msl(source),
62 None => match lang {
63 "wgsl" => Ok(Self::Wgsl(source)),
64 "msl" => Self::msl(source),
65 other => Err(CompilationError::Generic {
66 reason: format!(
67 "wgpu has no text passthrough for a precompiled `{other}` kernel"
68 ),
69 backtrace: cubecl_environment::backtrace::BackTrace::capture(),
70 }),
71 },
72 }
73 }
74
75 #[cfg(all(feature = "msl", target_os = "macos"))]
76 fn msl(source: &'a str) -> Result<Self, CompilationError> {
77 Ok(Self::Msl(source))
78 }
79
80 #[cfg(not(all(feature = "msl", target_os = "macos")))]
81 fn msl(_source: &'a str) -> Result<Self, CompilationError> {
82 Err(CompilationError::Generic {
83 reason: "MSL passthrough is only available on macOS".to_string(),
84 backtrace: cubecl_environment::backtrace::BackTrace::capture(),
85 })
86 }
87}
88
89impl<C: WgpuCompiler> WgpuServer<C> {
90 #[allow(
94 clippy::type_complexity,
95 reason = "required because of error propagation"
96 )]
97 #[allow(unused_variables)]
98 pub fn load_cached_pipeline(
99 &mut self,
100 kernel_id: &KernelId,
101 bindings: &KernelArguments,
102 mode: ExecutionMode,
103 ) -> Result<
104 Option<Result<crate::compute::PipelineEntry, (u64, KernelCacheKey)>>,
105 CompilationError,
106 > {
107 #[cfg(not(feature = "spirv"))]
108 let res = Ok(None);
109 #[cfg(feature = "spirv")]
110 let res = if let Some(cache) = self.spirv_cache.as_mut() {
111 let key = (
112 self.utilities.properties_hash,
113 KernelCacheKey::new(kernel_id, self.build_id),
114 );
115 if let Some(entry) = cache.remove(&key) {
116 use crate::ParamsTransfer;
117
118 log::trace!("Using SPIR-V cache");
119
120 let params_transfer = match entry.kernel.immediate_size {
121 Some(_) => ParamsTransfer::Immediate,
122 None => ParamsTransfer::Uniform,
123 };
124 let repr = AutoRepresentationRef::SpirV(&entry.kernel);
125 let module = self.create_module(
126 &entry.entrypoint_name,
127 kernel_id.cube_dim.into(),
128 ModuleSource::SpirV(&entry.kernel),
129 mode,
130 )?;
131 let pipeline =
132 self.create_pipeline(&entry.entrypoint_name, Some(repr), module, bindings)?;
133 let io = entry.kernel.io.clone().map(std::sync::Arc::from);
134 Ok(Some(Ok((
135 pipeline,
136 crate::compute::CompilerInfo::Vulkan { params_transfer },
137 io,
138 ))))
139 } else {
140 Ok(Some(Err(key)))
141 }
142 } else {
143 Ok(None)
144 };
145
146 res
147 }
148
149 pub fn create_module(
150 &self,
151 entrypoint_name: &str,
152 cube_dim: CubeDim,
153 source: ModuleSource<'_>,
154 mode: ExecutionMode,
155 ) -> Result<ShaderModule, CompilationError> {
156 match source {
157 #[cfg(feature = "spirv")]
158 ModuleSource::SpirV(repr) => self
159 .validated(|| unsafe {
160 self.device.create_shader_module_passthrough(
161 wgpu::ShaderModuleDescriptorPassthrough {
162 label: Some(entrypoint_name),
163 spirv: Some(Cow::Borrowed(&repr.assembled_module)),
164 entry_points: Cow::Borrowed(&[wgpu::PassthroughShaderEntryPoint {
165 name: entrypoint_name.into(),
166 workgroup_size: cube_dim.into(),
167 }]),
168 ..Default::default()
169 },
170 )
171 })
172 .map_err(|err| refused("SPIR-V module", entrypoint_name, err)),
173 #[cfg(all(feature = "msl", target_os = "macos"))]
174 ModuleSource::Msl(source) => self
175 .validated(|| unsafe {
176 self.device.create_shader_module_passthrough(
177 wgpu::ShaderModuleDescriptorPassthrough {
178 label: Some(entrypoint_name),
179 msl: Some(Cow::Borrowed(source)),
180 entry_points: Cow::Borrowed(&[wgpu::PassthroughShaderEntryPoint {
181 name: entrypoint_name.into(),
182 workgroup_size: cube_dim.into(),
183 }]),
184 ..Default::default()
185 },
186 )
187 })
188 .map_err(|err| refused("MSL module", entrypoint_name, err)),
189 ModuleSource::Wgsl(source) => {
190 let _ = cube_dim;
191 let checks = wgpu::ShaderRuntimeChecks {
192 bounds_checks: false,
196 force_loop_bounding: mode == ExecutionMode::Checked,
198 ..wgpu::ShaderRuntimeChecks::unchecked()
199 };
200
201 log::trace!("[cubecl-wgpu] compiling WGSL module `{entrypoint_name}`\n{source}");
202
203 let error_scope = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
204
205 let module = unsafe {
208 self.device.create_shader_module_trusted(
209 ShaderModuleDescriptor {
210 label: Some(entrypoint_name),
211 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(source)),
212 },
213 checks,
214 )
215 };
216
217 let err_future = error_scope.pop();
220
221 #[cfg(not(target_family = "wasm"))]
222 if let Some(err) = cubecl_environment::future::block_on(err_future) {
223 log::error!(
224 "[cubecl-wgpu] WGSL compilation failed for kernel `{entrypoint_name}`:\n{err}\n--- shader source ({} bytes) ---\n{source}\n--- end shader ---",
225 source.len()
226 );
227 return Err(CompilationError::Generic {
228 reason: format!(
229 "WGSL compilation failed for kernel `{entrypoint_name}`: {err}"
230 ),
231 backtrace: cubecl_environment::backtrace::BackTrace::capture(),
232 });
233 }
234
235 #[cfg(target_family = "wasm")]
238 {
239 let entrypoint_name = entrypoint_name.to_string();
240 let source = source.to_string();
241 wasm_bindgen_futures::spawn_local(async move {
242 if let Some(err) = err_future.await {
243 log::error!(
244 "[cubecl-wgpu] WGSL compilation failed for kernel `{entrypoint_name}`:\n{err}\n--- shader source ({} bytes) ---\n{source}\n--- end shader ---",
245 source.len()
246 );
247 }
248 });
249 }
250
251 Ok(module)
252 }
253 }
254 }
255
256 #[allow(unused_variables)]
257 pub fn create_pipeline(
258 &self,
259 entrypoint_name: &str,
260 repr: Option<AutoRepresentationRef<'_>>,
261 module: ShaderModule,
262 bindings: &KernelArguments,
263 ) -> Result<Arc<ComputePipeline>, CompilationError> {
264 let bindings_info = match repr {
265 Some(AutoRepresentationRef::Wgsl(repr)) => Some(wgsl::bindings(repr, bindings)),
266 #[cfg(all(feature = "msl", target_os = "macos"))]
267 Some(AutoRepresentationRef::Msl(repr)) => Some(metal::bindings(repr, bindings)),
268 #[cfg(feature = "spirv")]
269 Some(AutoRepresentationRef::SpirV(repr)) => Some(vulkan::bindings(repr, bindings)),
270 _ => None,
271 };
272
273 let create = || {
274 let layout = bindings_info.map(|(bindings, immediate_size)| {
275 if !bindings.is_empty() {
276 let bindings = bindings
277 .into_iter()
278 .map(|visibility| match visibility {
279 Visibility::Uniform => BufferBindingType::Uniform,
280 Visibility::Read => BufferBindingType::Storage { read_only: true },
281 Visibility::ReadWrite => {
282 BufferBindingType::Storage { read_only: false }
283 }
284 })
285 .enumerate()
286 .map(|(i, ty)| BindGroupLayoutEntry {
287 binding: i as u32,
288 visibility: ShaderStages::COMPUTE,
289 ty: BindingType::Buffer {
290 ty,
291 has_dynamic_offset: false,
292 min_binding_size: None,
293 },
294 count: None,
295 })
296 .collect::<Vec<_>>();
297 let layout = self
298 .device
299 .create_bind_group_layout(&BindGroupLayoutDescriptor {
300 label: None,
301 entries: &bindings,
302 });
303 self.device
304 .create_pipeline_layout(&PipelineLayoutDescriptor {
305 label: None,
306 bind_group_layouts: &[Some(&layout)],
307 immediate_size: immediate_size as u32,
308 })
309 } else {
310 self.device
311 .create_pipeline_layout(&PipelineLayoutDescriptor {
312 label: None,
313 bind_group_layouts: &[],
314 immediate_size: immediate_size as u32,
315 })
316 }
317 });
318
319 let pipeline = self
320 .device
321 .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
322 label: Some(entrypoint_name),
323 layout: layout.as_ref(),
324 module: &module,
325 entry_point: Some(entrypoint_name),
326 compilation_options: wgpu::PipelineCompilationOptions {
327 zero_initialize_workgroup_memory: false,
328 ..Default::default()
329 },
330 cache: None,
331 });
332 Arc::new(pipeline)
333 };
334 self.validated(create)
335 .map_err(|err| refused("pipeline", entrypoint_name, err))
336 }
337
338 fn validated<T>(&self, create: impl FnOnce() -> T) -> Result<T, wgpu::Error> {
347 #[cfg(target_family = "wasm")]
348 return Ok(create());
349
350 #[cfg(not(target_family = "wasm"))]
351 {
352 let validation = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
355 let internal = self.device.push_error_scope(wgpu::ErrorFilter::Internal);
356 let created = create();
357 let internal = internal.pop();
358 let validation = validation.pop();
359 let refusal =
360 cubecl_environment::future::block_on(async { internal.await.or(validation.await) });
361 match refusal {
362 Some(err) => Err(err),
363 None => Ok(created),
364 }
365 }
366 }
367}
368
369fn refused(object: &str, entrypoint_name: &str, err: wgpu::Error) -> CompilationError {
371 log::error!(
372 "[cubecl-wgpu] the device refused the {object} of kernel `{entrypoint_name}`: {err}"
373 );
374 CompilationError::Generic {
375 reason: format!("the device refused the {object} of kernel `{entrypoint_name}`: {err}"),
376 backtrace: cubecl_environment::backtrace::BackTrace::capture(),
377 }
378}
379
380pub async fn try_request_device(
382 adapter: &Adapter,
383) -> Result<(Device, Queue), crate::WgpuInitError> {
384 if let Some(result) = request_vulkan_device(adapter).await? {
385 return Ok(result);
386 }
387 if let Some(result) = request_metal_device(adapter).await? {
388 return Ok(result);
389 }
390 wgsl::try_request_device(adapter).await
391}
392
393#[cfg(feature = "spirv")]
394async fn request_vulkan_device(
395 adapter: &Adapter,
396) -> Result<Option<(Device, Queue)>, crate::WgpuInitError> {
397 if is_vulkan(adapter) {
398 vulkan::try_request_vulkan_device(adapter).await
399 } else {
400 Ok(None)
401 }
402}
403
404#[cfg(not(feature = "spirv"))]
405async fn request_vulkan_device(
406 _adapter: &Adapter,
407) -> Result<Option<(Device, Queue)>, crate::WgpuInitError> {
408 Ok(None)
409}
410
411#[cfg(all(feature = "msl", target_os = "macos"))]
412async fn request_metal_device(
413 adapter: &Adapter,
414) -> Result<Option<(Device, Queue)>, crate::WgpuInitError> {
415 if is_metal(adapter) {
416 metal::try_request_metal_device(adapter).await.map(Some)
417 } else {
418 Ok(None)
419 }
420}
421
422#[cfg(not(all(feature = "msl", target_os = "macos")))]
423async fn request_metal_device(
424 _adapter: &Adapter,
425) -> Result<Option<(Device, Queue)>, crate::WgpuInitError> {
426 Ok(None)
427}
428
429pub fn register_features(
430 adapter: &Adapter,
431 props: &mut DeviceProperties,
432 comp_options: &mut WgpuCompilationOptions,
433) {
434 if register_vulkan_features(adapter, props, comp_options) {
435 return;
436 }
437 if register_metal_features(adapter, props, comp_options) {
438 return;
439 }
440 wgsl::register_wgsl_features(adapter, props, comp_options);
441}
442
443#[cfg(feature = "spirv")]
444pub fn register_vulkan_features(
445 adapter: &Adapter,
446 props: &mut DeviceProperties,
447 comp_options: &mut WgpuCompilationOptions,
448) -> bool {
449 if is_vulkan(adapter) {
450 vulkan::register_vulkan_features(adapter, props, comp_options)
451 } else {
452 false
453 }
454}
455
456#[cfg(not(feature = "spirv"))]
457pub fn register_vulkan_features(
458 _adapter: &Adapter,
459 _props: &mut DeviceProperties,
460 _comp_options: &mut WgpuCompilationOptions,
461) -> bool {
462 false
463}
464
465#[cfg(all(feature = "msl", target_os = "macos"))]
466pub fn register_metal_features(
467 adapter: &Adapter,
468 props: &mut DeviceProperties,
469 comp_options: &mut WgpuCompilationOptions,
470) -> bool {
471 if is_metal(adapter) {
472 metal::register_metal_features(adapter, props, comp_options)
473 } else {
474 false
475 }
476}
477
478#[cfg(not(all(feature = "msl", target_os = "macos")))]
479pub fn register_metal_features(
480 _adapter: &Adapter,
481 _props: &mut DeviceProperties,
482 _comp_options: &mut WgpuCompilationOptions,
483) -> bool {
484 false
485}
486
487#[cfg_attr(
489 not(any(feature = "spirv", windows, target_vendor = "apple")),
490 expect(unused_variables)
491)]
492pub fn physical_device(adapter: &Adapter, info: &wgpu::AdapterInfo) -> Option<PhysicalDevice> {
493 if info.device_type == wgpu::DeviceType::Cpu {
494 return None;
495 }
496 let mut physical = PhysicalDevice::default();
497 physical.vendor = (info.vendor != 0).then(|| info.vendor.into());
499 if info.backend == wgpu::Backend::Vulkan {
502 physical.pci_address = info.device_pci_bus_id.parse().ok();
503 }
504 #[cfg(feature = "spirv")]
505 if is_vulkan(adapter) {
506 vulkan::describe_card(adapter, &mut physical);
507 }
508 #[cfg(windows)]
509 dx12::describe_card(adapter, &mut physical);
510 #[cfg(target_vendor = "apple")]
511 metal_card::describe_card(adapter, &mut physical);
512 Some(physical)
513}
514
515#[cfg(feature = "spirv")]
516fn is_vulkan(adapter: &Adapter) -> bool {
517 unsafe { adapter.as_hal::<wgpu::hal::api::Vulkan>().is_some() }
518}
519
520#[cfg(all(feature = "msl", target_os = "macos"))]
521fn is_metal(adapter: &Adapter) -> bool {
522 unsafe { adapter.as_hal::<wgpu::hal::api::Metal>().is_some() }
523}