1use std::marker::PhantomData;
2
3use crate::WgpuCompiler;
4use crate::{
5 AutoCompiler, AutoGraphicsApi, GraphicsApi, WgpuDevice, backend, compute::WgpuServer,
6 contiguous_strides,
7};
8use cubecl_common::device::{Device, DeviceService};
9use cubecl_common::{future, profile::TimingMethod};
10use cubecl_core::device::{DeviceId, ServerUtilitiesHandle};
11use cubecl_core::server::ServerUtilities;
12use cubecl_core::zspace::{Shape, Strides};
13use cubecl_core::{Runtime, ir::TargetProperties};
14use cubecl_ir::{DeviceProperties, HardwareProperties, MemoryDeviceProperties};
15use cubecl_runtime::allocator::ContiguousMemoryLayoutPolicy;
16#[cfg(not(feature = "vulkan-validate"))]
17use cubecl_runtime::logging::ProfileLevel;
18pub use cubecl_runtime::memory_management::MemoryConfiguration;
19use cubecl_runtime::{client::ComputeClient, logging::ServerLogger};
20use wgpu::{InstanceFlags, RequestAdapterOptions};
21
22#[derive(Debug)]
26pub struct WgpuRuntime<Compiler = AutoCompiler> {
27 _p: PhantomData<Compiler>,
28}
29
30impl<C> Clone for WgpuRuntime<C> {
31 fn clone(&self) -> Self {
32 Self { _p: self._p }
33 }
34}
35
36impl<C: WgpuCompiler> DeviceService for WgpuServer<C> {
37 fn init(device_id: cubecl_common::device::DeviceId) -> Self {
38 let device = WgpuDevice::from_id(device_id);
39 let setup = future::block_on(create_setup_for_device(&device, AutoGraphicsApi::backend()));
40 create_server(setup, RuntimeOptions::default())
41 }
42
43 fn utilities(&self) -> ServerUtilitiesHandle {
44 self.utilities.clone() as ServerUtilitiesHandle
45 }
46}
47
48impl<C: WgpuCompiler> Runtime for WgpuRuntime<C> {
49 type Compiler = C;
50 type Server = WgpuServer<C>;
51 type Device = WgpuDevice;
52
53 fn client(device: &Self::Device) -> ComputeClient<Self> {
54 ComputeClient::load(device)
55 }
56
57 fn name(client: &ComputeClient<Self>) -> &'static str {
58 match client.info() {
59 wgpu::Backend::Vulkan => {
60 #[cfg(feature = "spirv")]
61 return "wgpu<spirv>";
62
63 #[cfg(not(feature = "spirv"))]
64 return "wgpu<wgsl>";
65 }
66 wgpu::Backend::Metal => {
67 #[cfg(feature = "msl")]
68 return "wgpu<msl>";
69
70 #[cfg(not(feature = "msl"))]
71 return "wgpu<wgsl>";
72 }
73 _ => "wgpu<wgsl>",
74 }
75 }
76
77 fn max_cube_count() -> (u32, u32, u32) {
78 let max_dim = u16::MAX as u32;
79 (max_dim, max_dim, max_dim)
80 }
81
82 fn can_read_tensor(shape: &Shape, strides: &Strides) -> bool {
83 if shape.is_empty() {
84 return true;
85 }
86
87 for (&expected, &stride) in contiguous_strides(shape).iter().zip(strides.iter()) {
88 if expected != stride {
89 return false;
90 }
91 }
92
93 true
94 }
95
96 fn target_properties() -> TargetProperties {
97 TargetProperties {
98 mma: Default::default(),
100 }
101 }
102
103 fn enumerate_devices(type_id: u16, info: &wgpu::Backend) -> Vec<DeviceId> {
104 #[cfg(target_family = "wasm")]
105 {
106 let _ = type_id;
107 let _ = info;
108 vec![DeviceId::new(0, 0)]
110 }
111
112 #[cfg(not(target_family = "wasm"))]
113 {
114 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
115 backends: wgpu::Backends::all(),
116 ..wgpu::InstanceDescriptor::new_without_display_handle()
117 });
118
119 let adapters = enumerate_all_adapters(instance, *info);
120 adapters
121 .into_iter()
122 .filter(|adapter| {
123 if type_id == 4 {
125 return true;
126 }
127
128 let device_type = adapter.get_info().device_type;
129
130 let adapter_type_id = match device_type {
131 wgpu::DeviceType::Other => 4,
132 wgpu::DeviceType::IntegratedGpu => 1,
133 wgpu::DeviceType::DiscreteGpu => 0,
134 wgpu::DeviceType::VirtualGpu => 2,
135 wgpu::DeviceType::Cpu => 3,
136 };
137
138 adapter_type_id == type_id
139 })
140 .enumerate()
141 .map(|(index, adapter)| match adapter.get_info().device_type {
142 wgpu::DeviceType::DiscreteGpu => DeviceId::new(0, index as u16),
143 wgpu::DeviceType::IntegratedGpu => DeviceId::new(1, index as u16),
144 wgpu::DeviceType::VirtualGpu => DeviceId::new(2, index as u16),
145 wgpu::DeviceType::Cpu => DeviceId::new(3, 0),
146 wgpu::DeviceType::Other => DeviceId::new(4, 0),
147 })
148 .collect()
149 }
150 }
151
152 fn enumerate_all_devices(info: &wgpu::Backend) -> Vec<DeviceId> {
153 #[cfg(target_family = "wasm")]
154 {
155 let _ = info;
156 vec![DeviceId::new(0, 0)]
158 }
159
160 #[cfg(not(target_family = "wasm"))]
161 {
162 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
163 backends: wgpu::Backends::all(),
164 ..wgpu::InstanceDescriptor::new_without_display_handle()
165 });
166 let adapters = enumerate_all_adapters(instance, *info);
167 adapters
168 .into_iter()
169 .enumerate()
170 .map(|(index, adapter)| match adapter.get_info().device_type {
171 wgpu::DeviceType::DiscreteGpu => DeviceId::new(0, index as u16),
172 wgpu::DeviceType::IntegratedGpu => DeviceId::new(1, index as u16),
173 wgpu::DeviceType::VirtualGpu => DeviceId::new(2, index as u16),
174 wgpu::DeviceType::Cpu => DeviceId::new(3, 0),
175 wgpu::DeviceType::Other => DeviceId::new(4, 0),
176 })
177 .collect()
178 }
179 }
180}
181
182#[cfg(not(target_family = "wasm"))]
183fn enumerate_all_adapters(instance: wgpu::Instance, backend: wgpu::Backend) -> Vec<wgpu::Adapter> {
184 cubecl_common::future::block_on(instance.enumerate_adapters(backend.into()))
186}
187
188pub struct RuntimeOptions {
190 pub tasks_max: usize,
192 pub memory_config: MemoryConfiguration,
194}
195
196impl Default for RuntimeOptions {
197 fn default() -> Self {
198 #[cfg(test)]
199 const DEFAULT_MAX_TASKS: usize = 32;
200 #[cfg(not(test))]
201 const DEFAULT_MAX_TASKS: usize = 32;
202
203 let tasks_max = match std::env::var("CUBECL_WGPU_MAX_TASKS") {
204 Ok(value) => value
205 .parse::<usize>()
206 .expect("CUBECL_WGPU_MAX_TASKS should be a positive integer."),
207 Err(_) => DEFAULT_MAX_TASKS,
208 };
209
210 Self {
211 tasks_max,
212 memory_config: MemoryConfiguration::default(),
213 }
214 }
215}
216
217#[derive(Clone, Debug)]
221pub struct WgpuSetup {
222 pub instance: wgpu::Instance,
224 pub adapter: wgpu::Adapter,
226 pub device: wgpu::Device,
228 pub queue: wgpu::Queue,
230 pub backend: wgpu::Backend,
232}
233
234pub fn init_device(setup: WgpuSetup, options: RuntimeOptions) -> WgpuDevice {
244 use core::sync::atomic::{AtomicU32, Ordering};
245
246 static COUNTER: AtomicU32 = AtomicU32::new(0);
247
248 let device_id = COUNTER.fetch_add(1, Ordering::Relaxed);
249 if device_id == u32::MAX {
250 core::panic!("Memory ID overflowed");
251 }
252
253 let device_id = WgpuDevice::Existing(device_id);
254 let server = create_server(setup, options);
255 let _ = ComputeClient::<WgpuRuntime>::init(&device_id, server);
256 device_id
257}
258
259pub fn init_setup<G: GraphicsApi>(device: &WgpuDevice, options: RuntimeOptions) -> WgpuSetup {
262 cfg_if::cfg_if! {
263 if #[cfg(target_family = "wasm")] {
264 let _ = (device, options);
265 panic!("Creating a wgpu setup synchronously is unsupported on wasm. Use init_async instead");
266 } else {
267 future::block_on(init_setup_async::<G>(device, options))
268 }
269 }
270}
271
272pub async fn init_setup_async<G: GraphicsApi>(
276 device: &WgpuDevice,
277 options: RuntimeOptions,
278) -> WgpuSetup {
279 let setup = create_setup_for_device(device, G::backend()).await;
280 let return_setup = setup.clone();
281 let server = create_server(setup, options);
282 let _ = ComputeClient::<WgpuRuntime>::init(device, server);
283 return_setup
284}
285
286pub(crate) fn create_server<C: WgpuCompiler>(
287 setup: WgpuSetup,
288 options: RuntimeOptions,
289) -> WgpuServer<C> {
290 let limits = setup.device.limits();
291 let adapter_limits = setup.adapter.limits();
292 let mut adapter_info = setup.adapter.get_info();
293
294 if adapter_info.subgroup_min_size == 0 && adapter_info.subgroup_max_size == 0 {
298 adapter_info.subgroup_min_size = 8;
300 adapter_info.subgroup_max_size = 128;
302 }
303
304 let mem_props = MemoryDeviceProperties {
305 max_page_size: limits.max_storage_buffer_binding_size,
306 alignment: limits.min_uniform_buffer_offset_alignment as u64,
307 };
308 let max_count = adapter_limits.max_compute_workgroups_per_dimension;
309 let hardware_props = HardwareProperties {
310 load_width: 128,
311 #[cfg(apple_silicon)]
315 plane_size_min: 32,
316 #[cfg(not(apple_silicon))]
317 plane_size_min: adapter_info.subgroup_min_size,
318 #[cfg(apple_silicon)]
319 plane_size_max: 32,
320 #[cfg(not(apple_silicon))]
321 plane_size_max: adapter_info.subgroup_max_size,
322 max_bindings: limits
326 .max_storage_buffers_per_shader_stage
327 .saturating_sub(1),
328 max_shared_memory_size: limits.max_compute_workgroup_storage_size as usize,
329 max_cube_count: (max_count, max_count, max_count),
330 max_units_per_cube: adapter_limits.max_compute_invocations_per_workgroup,
331 max_cube_dim: (
332 adapter_limits.max_compute_workgroup_size_x,
333 adapter_limits.max_compute_workgroup_size_y,
334 adapter_limits.max_compute_workgroup_size_z,
335 ),
336 num_streaming_multiprocessors: None,
337 num_tensor_cores: None,
338 min_tensor_cores_dim: None,
339 num_cpu_cores: None, max_vector_size: 4,
341 cube_mma_reserved_shared_memory: 0,
343 };
344
345 let mut compilation_options = Default::default();
346
347 let features = setup.adapter.features();
348
349 let time_measurement = if features.contains(wgpu::Features::TIMESTAMP_QUERY) {
350 TimingMethod::Device
351 } else {
352 TimingMethod::System
353 };
354
355 let mut device_props = DeviceProperties::new(
356 Default::default(),
357 mem_props,
358 hardware_props,
359 time_measurement,
360 );
361
362 #[cfg(not(all(target_os = "macos", feature = "msl")))]
363 {
364 if features.contains(wgpu::Features::SUBGROUP)
365 && setup.adapter.get_info().device_type != wgpu::DeviceType::Cpu
366 {
367 use cubecl_ir::features::Plane;
368
369 device_props.features.plane.insert(Plane::Ops);
370 }
371 }
372
373 #[cfg(any(feature = "spirv", feature = "msl"))]
374 device_props
375 .features
376 .plane
377 .insert(cubecl_ir::features::Plane::NonUniformControlFlow);
378
379 backend::register_features(
380 &setup.adapter,
381 &mut device_props,
382 &mut compilation_options,
383 &options.memory_config,
384 );
385
386 let logger = alloc::sync::Arc::new(ServerLogger::default());
387
388 let allocator = ContiguousMemoryLayoutPolicy::new(device_props.memory.alignment as usize);
389 WgpuServer::new(
390 device_props.memory.clone(),
391 options.memory_config,
392 compilation_options,
393 setup.device.clone(),
394 setup.queue,
395 options.tasks_max,
396 setup.backend,
397 time_measurement,
398 ServerUtilities::new(device_props, logger, setup.backend, allocator),
399 )
400}
401
402pub(crate) async fn create_setup_for_device(
405 device: &WgpuDevice,
406 backend: wgpu::Backend,
407) -> WgpuSetup {
408 let (instance, adapter) = request_adapter(device, backend).await;
409 let (device, queue) = backend::request_device(&adapter).await;
410
411 log::info!(
412 "Created wgpu compute server on device {:?}",
413 adapter.get_info()
414 );
415
416 WgpuSetup {
417 instance,
418 adapter,
419 device,
420 queue,
421 backend,
422 }
423}
424
425async fn request_adapter(
426 device: &WgpuDevice,
427 backend: wgpu::Backend,
428) -> (wgpu::Instance, wgpu::Adapter) {
429 #[cfg(not(feature = "vulkan-validate"))]
430 let instance_flags = {
431 let debug = ServerLogger::default();
432 match (debug.profile_level(), debug.compilation_source_activated()) {
436 (Some(ProfileLevel::Full), _) => InstanceFlags::advanced_debugging(),
437 (_, true) => InstanceFlags::debugging(),
438 (_, false) => InstanceFlags::default(),
439 }
440 };
441 #[cfg(feature = "vulkan-validate")]
442 let instance_flags = InstanceFlags::advanced_debugging();
443 log::debug!("{instance_flags:?}");
444 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
445 backends: backend.into(),
446 flags: instance_flags,
447 ..wgpu::InstanceDescriptor::new_without_display_handle()
448 });
449
450 #[allow(deprecated)]
451 let override_device = if matches!(
452 device,
453 WgpuDevice::DefaultDevice | WgpuDevice::BestAvailable
454 ) {
455 get_device_override()
456 } else {
457 None
458 };
459
460 let device = override_device.unwrap_or_else(|| device.clone());
461
462 let adapter = match device {
463 #[cfg(not(target_family = "wasm"))]
464 WgpuDevice::DiscreteGpu(num) => {
465 select_from_adapter_list(
466 num,
467 "No Discrete GPU device found",
468 &instance,
469 &device,
470 backend,
471 )
472 .await
473 }
474 #[cfg(not(target_family = "wasm"))]
475 WgpuDevice::IntegratedGpu(num) => {
476 select_from_adapter_list(
477 num,
478 "No Integrated GPU device found",
479 &instance,
480 &device,
481 backend,
482 )
483 .await
484 }
485 #[cfg(not(target_family = "wasm"))]
486 WgpuDevice::VirtualGpu(num) => {
487 select_from_adapter_list(
488 num,
489 "No Virtual GPU device found",
490 &instance,
491 &device,
492 backend,
493 )
494 .await
495 }
496 #[cfg(not(target_family = "wasm"))]
497 WgpuDevice::Cpu => {
498 select_from_adapter_list(0, "No CPU device found", &instance, &device, backend).await
499 }
500 #[cfg(target_family = "wasm")]
501 WgpuDevice::IntegratedGpu(_) => {
502 request_adapter_with_preference(&instance, wgpu::PowerPreference::LowPower).await
503 }
504 WgpuDevice::Existing(_) => {
505 unreachable!("Cannot select an adapter for an existing device.")
506 }
507 _ => {
508 request_adapter_with_preference(&instance, wgpu::PowerPreference::HighPerformance).await
509 }
510 };
511
512 (instance, adapter)
513}
514
515async fn request_adapter_with_preference(
516 instance: &wgpu::Instance,
517 power_preference: wgpu::PowerPreference,
518) -> wgpu::Adapter {
519 instance
520 .request_adapter(&RequestAdapterOptions {
521 power_preference,
522 force_fallback_adapter: false,
523 compatible_surface: None,
524 ..RequestAdapterOptions::default()
525 })
526 .await
527 .expect("No possible adapter available for backend. Falling back to first available.")
528}
529
530#[cfg(not(target_family = "wasm"))]
531async fn select_from_adapter_list(
532 num: usize,
533 error: &str,
534 instance: &wgpu::Instance,
535 device: &WgpuDevice,
536 backend: wgpu::Backend,
537) -> wgpu::Adapter {
538 let mut adapters_other = Vec::new();
539 let mut adapters = Vec::new();
540
541 instance
542 .enumerate_adapters(backend.into())
543 .await
544 .into_iter()
545 .for_each(|adapter| {
546 let device_type = adapter.get_info().device_type;
547
548 if let wgpu::DeviceType::Other = device_type {
549 adapters_other.push(adapter);
550 return;
551 }
552
553 let is_same_type = match device {
554 WgpuDevice::DiscreteGpu(_) => device_type == wgpu::DeviceType::DiscreteGpu,
555 WgpuDevice::IntegratedGpu(_) => device_type == wgpu::DeviceType::IntegratedGpu,
556 WgpuDevice::VirtualGpu(_) => device_type == wgpu::DeviceType::VirtualGpu,
557 WgpuDevice::Cpu => device_type == wgpu::DeviceType::Cpu,
558 #[allow(deprecated)]
559 WgpuDevice::DefaultDevice | WgpuDevice::BestAvailable => true,
560 WgpuDevice::Existing(_) => {
561 unreachable!("Cannot select an adapter for an existing device.")
562 }
563 };
564
565 if is_same_type {
566 adapters.push(adapter);
567 }
568 });
569
570 if adapters.len() <= num {
571 if adapters_other.len() <= num {
572 panic!(
573 "{}, adapters {:?}, other adapters {:?}",
574 error,
575 adapters
576 .into_iter()
577 .map(|adapter| adapter.get_info())
578 .collect::<Vec<_>>(),
579 adapters_other
580 .into_iter()
581 .map(|adapter| adapter.get_info())
582 .collect::<Vec<_>>(),
583 );
584 }
585
586 return adapters_other.remove(num);
587 }
588
589 adapters.remove(num)
590}
591
592fn get_device_override() -> Option<WgpuDevice> {
593 std::env::var("CUBECL_WGPU_DEFAULT_DEVICE")
596 .ok()
597 .and_then(|var| {
598 let override_device = if let Some(inner) = var.strip_prefix("DiscreteGpu(") {
599 inner
600 .strip_suffix(")")
601 .and_then(|s| s.parse().ok())
602 .map(WgpuDevice::DiscreteGpu)
603 } else if let Some(inner) = var.strip_prefix("IntegratedGpu(") {
604 inner
605 .strip_suffix(")")
606 .and_then(|s| s.parse().ok())
607 .map(WgpuDevice::IntegratedGpu)
608 } else if let Some(inner) = var.strip_prefix("VirtualGpu(") {
609 inner
610 .strip_suffix(")")
611 .and_then(|s| s.parse().ok())
612 .map(WgpuDevice::VirtualGpu)
613 } else if var == "Cpu" {
614 Some(WgpuDevice::Cpu)
615 } else {
616 None
617 };
618
619 if override_device.is_none() {
620 log::warn!("Unknown CUBECL_WGPU_DEVICE override {var}");
621 }
622 override_device
623 })
624}