Skip to main content

cubecl_wgpu/
runtime.rs

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/// Runtime that uses the [wgpu] crate with the wgsl compiler. This is used in the Wgpu backend.
23/// For advanced configuration, use [`init_setup`] to pass in runtime options or to select a
24/// specific graphics API.
25#[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            // Values are irrelevant, since no wgsl backends currently support manual mma
99            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            // WebGPU only supports a single device currently.
109            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                    // Default doesn't filter device types.
124                    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            // WebGPU only supports a single device currently.
157            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    // `enumerate_adapters` is now async & available on WebGPU
185    cubecl_common::future::block_on(instance.enumerate_adapters(backend.into()))
186}
187
188/// The values that control how a WGPU Runtime will perform its calculations.
189pub struct RuntimeOptions {
190    /// Control the amount of compute tasks to be aggregated into a single GPU command.
191    pub tasks_max: usize,
192    /// Configures the memory management.
193    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/// A complete setup used to run wgpu.
218///
219/// These can either be created with [`init_setup`] or [`init_setup_async`].
220#[derive(Clone, Debug)]
221pub struct WgpuSetup {
222    /// The underlying wgpu instance.
223    pub instance: wgpu::Instance,
224    /// The selected 'adapter'. This corresponds to a physical device.
225    pub adapter: wgpu::Adapter,
226    /// The wgpu device Burn will use. Nb: There can only be one device per adapter.
227    pub device: wgpu::Device,
228    /// The queue Burn commands will be submitted to.
229    pub queue: wgpu::Queue,
230    /// The backend used by the setup.
231    pub backend: wgpu::Backend,
232}
233
234/// Create a [`WgpuDevice`] on an existing [`WgpuSetup`].
235/// Useful when you want to share a device between `CubeCL` and other wgpu-dependent libraries.
236///
237/// # Note
238///
239/// Please **do not** to call on the same [`setup`](WgpuSetup) more than once.
240///
241/// This function generates a new, globally unique ID for the device every time it is called,
242/// even if called on the same device multiple times.
243pub 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
259/// Like [`init_setup_async`], but synchronous.
260/// On wasm, it is necessary to use [`init_setup_async`] instead.
261pub 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
272/// Initialize a client on the given device with the given options.
273/// This function is useful to configure the runtime options
274/// or to pick a different graphics API.
275pub 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    // Workaround: WebGPU reports some "fake" subgroup info atm, as it's not really supported yet.
295    // However, some algorithms do rely on having this information eg. cubecl-reduce uses max subgroup size _even_ when
296    // subgroups aren't used. For now, just override with the maximum range of subgroups possible.
297    if adapter_info.subgroup_min_size == 0 && adapter_info.subgroup_max_size == 0 {
298        // There is in theory nothing limiting the size to go below 8 but in practice 8 is the minimum found anywhere.
299        adapter_info.subgroup_min_size = 8;
300        // This is a hard limit of GPU APIs (subgroup ballot returns 4 * 32 bits).
301        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        // On Apple Silicon, the plane size is 32,
312        // though the minimum and maximum differ.
313        // https://github.com/gpuweb/gpuweb/issues/3950
314        #[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        // wgpu uses an additional buffer for variable-length buffers,
323        // so we have to use one buffer less on our side to make room for that wgpu internal buffer.
324        // See: https://github.com/gfx-rs/wgpu/blob/a9638c8e3ac09ce4f27ac171f8175671e30365fd/wgpu-hal/src/metal/device.rs#L799
325        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, // TODO: Check if device is CPU.
340        max_vector_size: 4,
341        // Init later if extension is enabled
342        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
402/// Select the wgpu device and queue based on the provided [device](WgpuDevice) and
403/// [backend](wgpu::Backend).
404pub(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        // Debug/validation layers cost real per-dispatch CPU time, so only
433        // source-level compilation logging (`full`) opts into them — `basic`
434        // is passive name-only logging and must not change how kernels run.
435        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    // If BestAvailable, check if we should instead construct as
594    // if a specific device was specified.
595    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}