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::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_environment::future;
15use cubecl_ir::{DeviceIdentity, DeviceProperties, HardwareProperties, MemoryDeviceProperties};
16use cubecl_runtime::allocator::ContiguousMemoryLayoutPolicy;
17#[cfg(not(feature = "vulkan-validate"))]
18use cubecl_runtime::logging::ProfileLevel;
19pub use cubecl_runtime::memory_management::MemoryConfiguration;
20use cubecl_runtime::{client::ComputeClient, logging::ServerLogger};
21use wgpu::{InstanceFlags, RequestAdapterOptions};
22
23/// Runtime that uses the [wgpu] crate with the wgsl compiler. This is used in the Wgpu backend.
24/// For advanced configuration, use [`init_setup`] to pass in runtime options or to select a
25/// specific graphics API.
26#[derive(Debug)]
27pub struct WgpuRuntime<Compiler = AutoCompiler> {
28    _p: PhantomData<Compiler>,
29}
30
31impl<C> Clone for WgpuRuntime<C> {
32    fn clone(&self) -> Self {
33        Self { _p: self._p }
34    }
35}
36
37impl<C: WgpuCompiler> DeviceService for WgpuServer<C> {
38    fn init(device_id: cubecl_common::device::DeviceId) -> Self {
39        let device = WgpuDevice::from_id(device_id);
40        let setup = future::block_on(create_setup_for_device(&device, AutoGraphicsApi::backend()));
41        create_server(setup, RuntimeOptions::default())
42    }
43
44    fn utilities(&self) -> ServerUtilitiesHandle {
45        self.utilities.clone() as ServerUtilitiesHandle
46    }
47}
48
49impl<C: WgpuCompiler> Runtime for WgpuRuntime<C> {
50    type Compiler = C;
51    type Server = WgpuServer<C>;
52    type Device = WgpuDevice;
53
54    fn client(device: &Self::Device) -> ComputeClient<Self> {
55        ComputeClient::load(device)
56    }
57
58    fn name(client: &ComputeClient<Self>) -> &'static str {
59        match client.info() {
60            wgpu::Backend::Vulkan => {
61                #[cfg(feature = "spirv")]
62                return "wgpu<spirv>";
63
64                #[cfg(not(feature = "spirv"))]
65                return "wgpu<wgsl>";
66            }
67            wgpu::Backend::Metal => {
68                #[cfg(feature = "msl")]
69                return "wgpu<msl>";
70
71                #[cfg(not(feature = "msl"))]
72                return "wgpu<wgsl>";
73            }
74            _ => "wgpu<wgsl>",
75        }
76    }
77
78    fn max_cube_count() -> (u32, u32, u32) {
79        let max_dim = u16::MAX as u32;
80        (max_dim, max_dim, max_dim)
81    }
82
83    fn can_read_tensor(shape: &Shape, strides: &Strides) -> bool {
84        if shape.is_empty() {
85            return true;
86        }
87
88        for (&expected, &stride) in contiguous_strides(shape).iter().zip(strides.iter()) {
89            if expected != stride {
90                return false;
91            }
92        }
93
94        true
95    }
96
97    fn target_properties() -> TargetProperties {
98        TargetProperties {
99            // Values are irrelevant, since no wgsl backends currently support manual mma
100            mma: Default::default(),
101        }
102    }
103
104    fn enumerate_devices(type_id: u16, info: &wgpu::Backend) -> Vec<DeviceId> {
105        #[cfg(target_family = "wasm")]
106        {
107            let _ = type_id;
108            let _ = info;
109            // WebGPU only supports a single device currently.
110            vec![DeviceId::new(0, 0)]
111        }
112
113        #[cfg(not(target_family = "wasm"))]
114        {
115            let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
116                backends: wgpu::Backends::all(),
117                ..wgpu::InstanceDescriptor::new_without_display_handle()
118            });
119
120            let adapters = enumerate_all_adapters(instance, *info);
121            adapters
122                .into_iter()
123                .filter(|adapter| {
124                    // Default doesn't filter device types.
125                    if type_id == 4 {
126                        return true;
127                    }
128
129                    let device_type = adapter.get_info().device_type;
130
131                    let adapter_type_id = match device_type {
132                        wgpu::DeviceType::Other => 4,
133                        wgpu::DeviceType::IntegratedGpu => 1,
134                        wgpu::DeviceType::DiscreteGpu => 0,
135                        wgpu::DeviceType::VirtualGpu => 2,
136                        wgpu::DeviceType::Cpu => 3,
137                    };
138
139                    adapter_type_id == type_id
140                })
141                .enumerate()
142                .map(|(index, adapter)| match adapter.get_info().device_type {
143                    wgpu::DeviceType::DiscreteGpu => DeviceId::new(0, index as u16),
144                    wgpu::DeviceType::IntegratedGpu => DeviceId::new(1, index as u16),
145                    wgpu::DeviceType::VirtualGpu => DeviceId::new(2, index as u16),
146                    wgpu::DeviceType::Cpu => DeviceId::new(3, 0),
147                    wgpu::DeviceType::Other => DeviceId::new(4, 0),
148                })
149                .collect()
150        }
151    }
152
153    fn enumerate_all_devices(info: &wgpu::Backend) -> Vec<DeviceId> {
154        #[cfg(target_family = "wasm")]
155        {
156            let _ = info;
157            // WebGPU only supports a single device currently.
158            vec![DeviceId::new(0, 0)]
159        }
160
161        #[cfg(not(target_family = "wasm"))]
162        {
163            let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
164                backends: wgpu::Backends::all(),
165                ..wgpu::InstanceDescriptor::new_without_display_handle()
166            });
167            let adapters = enumerate_all_adapters(instance, *info);
168            adapters
169                .into_iter()
170                .enumerate()
171                .map(|(index, adapter)| match adapter.get_info().device_type {
172                    wgpu::DeviceType::DiscreteGpu => DeviceId::new(0, index as u16),
173                    wgpu::DeviceType::IntegratedGpu => DeviceId::new(1, index as u16),
174                    wgpu::DeviceType::VirtualGpu => DeviceId::new(2, index as u16),
175                    wgpu::DeviceType::Cpu => DeviceId::new(3, 0),
176                    wgpu::DeviceType::Other => DeviceId::new(4, 0),
177                })
178                .collect()
179        }
180    }
181}
182
183#[cfg(not(target_family = "wasm"))]
184fn enumerate_all_adapters(instance: wgpu::Instance, backend: wgpu::Backend) -> Vec<wgpu::Adapter> {
185    // `enumerate_adapters` is now async & available on WebGPU
186    cubecl_environment::future::block_on(instance.enumerate_adapters(backend.into()))
187}
188
189/// The values that control how a WGPU Runtime will perform its calculations.
190pub struct RuntimeOptions {
191    /// Control the amount of compute tasks to be aggregated into a single GPU command.
192    pub tasks_max: usize,
193    /// Configures the memory management.
194    pub memory_config: MemoryConfiguration,
195}
196
197impl Default for RuntimeOptions {
198    fn default() -> Self {
199        #[cfg(test)]
200        const DEFAULT_MAX_TASKS: usize = 32;
201        #[cfg(not(test))]
202        const DEFAULT_MAX_TASKS: usize = 32;
203
204        let tasks_max = match std::env::var("CUBECL_WGPU_MAX_TASKS") {
205            Ok(value) => value
206                .parse::<usize>()
207                .expect("CUBECL_WGPU_MAX_TASKS should be a positive integer."),
208            Err(_) => DEFAULT_MAX_TASKS,
209        };
210
211        Self {
212            tasks_max,
213            memory_config: MemoryConfiguration::default(),
214        }
215    }
216}
217
218/// A complete setup used to run wgpu.
219///
220/// These can either be created with [`init_setup`] or [`init_setup_async`].
221#[derive(Clone, Debug)]
222pub struct WgpuSetup {
223    /// The underlying wgpu instance.
224    pub instance: wgpu::Instance,
225    /// The selected 'adapter'. This corresponds to a physical device.
226    pub adapter: wgpu::Adapter,
227    /// The wgpu device Burn will use. Nb: There can only be one device per adapter.
228    pub device: wgpu::Device,
229    /// The queue Burn commands will be submitted to.
230    pub queue: wgpu::Queue,
231    /// The backend used by the setup.
232    pub backend: wgpu::Backend,
233}
234
235/// Create a [`WgpuDevice`] on an existing [`WgpuSetup`].
236/// Useful when you want to share a device between `CubeCL` and other wgpu-dependent libraries.
237///
238/// # Note
239///
240/// Please **do not** to call on the same [`setup`](WgpuSetup) more than once.
241///
242/// This function generates a new, globally unique ID for the device every time it is called,
243/// even if called on the same device multiple times.
244pub fn init_device(setup: WgpuSetup, options: RuntimeOptions) -> WgpuDevice {
245    use core::sync::atomic::{AtomicU32, Ordering};
246
247    static COUNTER: AtomicU32 = AtomicU32::new(0);
248
249    let device_id = COUNTER.fetch_add(1, Ordering::Relaxed);
250    if device_id == u32::MAX {
251        core::panic!("Memory ID overflowed");
252    }
253
254    let device_id = WgpuDevice::Existing(device_id);
255    let server = create_server(setup, options);
256    let _ = ComputeClient::<WgpuRuntime>::init(&device_id, server);
257    device_id
258}
259
260/// Like [`init_setup_async`], but synchronous.
261/// On wasm, it is necessary to use [`init_setup_async`] instead.
262pub fn init_setup<G: GraphicsApi>(device: &WgpuDevice, options: RuntimeOptions) -> WgpuSetup {
263    cfg_if::cfg_if! {
264        if #[cfg(target_family = "wasm")] {
265            let _ = (device, options);
266            panic!("Creating a wgpu setup synchronously is unsupported on wasm. Use init_async instead");
267        } else {
268            future::block_on(init_setup_async::<G>(device, options))
269        }
270    }
271}
272
273/// Initialize a client on the given device with the given options.
274/// This function is useful to configure the runtime options
275/// or to pick a different graphics API.
276pub async fn init_setup_async<G: GraphicsApi>(
277    device: &WgpuDevice,
278    options: RuntimeOptions,
279) -> WgpuSetup {
280    let setup = create_setup_for_device(device, G::backend()).await;
281    let return_setup = setup.clone();
282    let server = create_server(setup, options);
283    let _ = ComputeClient::<WgpuRuntime>::init(device, server);
284    return_setup
285}
286
287pub(crate) fn create_server<C: WgpuCompiler>(
288    setup: WgpuSetup,
289    options: RuntimeOptions,
290) -> WgpuServer<C> {
291    let limits = setup.device.limits();
292    let adapter_limits = setup.adapter.limits();
293    let mut adapter_info = setup.adapter.get_info();
294
295    // Workaround: WebGPU reports some "fake" subgroup info atm, as it's not really supported yet.
296    // However, some algorithms do rely on having this information eg. cubecl-reduce uses max subgroup size _even_ when
297    // subgroups aren't used. For now, just override with the maximum range of subgroups possible.
298    if adapter_info.subgroup_min_size == 0 && adapter_info.subgroup_max_size == 0 {
299        // There is in theory nothing limiting the size to go below 8 but in practice 8 is the minimum found anywhere.
300        adapter_info.subgroup_min_size = 8;
301        // This is a hard limit of GPU APIs (subgroup ballot returns 4 * 32 bits).
302        adapter_info.subgroup_max_size = 128;
303    }
304
305    let mem_props = MemoryDeviceProperties {
306        max_page_size: limits.max_storage_buffer_binding_size,
307        alignment: limits.min_uniform_buffer_offset_alignment as u64,
308    };
309    let max_count = adapter_limits.max_compute_workgroups_per_dimension;
310    let hardware_props = HardwareProperties {
311        load_width: 128,
312        // On Apple Silicon, the plane size is 32,
313        // though the minimum and maximum differ.
314        // https://github.com/gpuweb/gpuweb/issues/3950
315        #[cfg(apple_silicon)]
316        plane_size_min: 32,
317        #[cfg(not(apple_silicon))]
318        plane_size_min: adapter_info.subgroup_min_size,
319        #[cfg(apple_silicon)]
320        plane_size_max: 32,
321        #[cfg(not(apple_silicon))]
322        plane_size_max: adapter_info.subgroup_max_size,
323        // wgpu uses an additional buffer for variable-length buffers,
324        // so we have to use one buffer less on our side to make room for that wgpu internal buffer.
325        // See: https://github.com/gfx-rs/wgpu/blob/a9638c8e3ac09ce4f27ac171f8175671e30365fd/wgpu-hal/src/metal/device.rs#L799
326        max_bindings: limits
327            .max_storage_buffers_per_shader_stage
328            .saturating_sub(1),
329        max_shared_memory_size: limits.max_compute_workgroup_storage_size as usize,
330        max_cube_count: (max_count, max_count, max_count),
331        max_units_per_cube: adapter_limits.max_compute_invocations_per_workgroup,
332        max_cube_dim: (
333            adapter_limits.max_compute_workgroup_size_x,
334            adapter_limits.max_compute_workgroup_size_y,
335            adapter_limits.max_compute_workgroup_size_z,
336        ),
337        num_streaming_multiprocessors: None,
338        num_tensor_cores: None,
339        min_tensor_cores_dim: None,
340        num_cpu_cores: None, // TODO: Check if device is CPU.
341        max_vector_size: 4,
342        // Init later if extension is enabled
343        cube_mma_reserved_shared_memory: 0,
344    };
345
346    let mut compilation_options = Default::default();
347
348    let features = setup.adapter.features();
349
350    let time_measurement = if features.contains(wgpu::Features::TIMESTAMP_QUERY) {
351        TimingMethod::Device
352    } else {
353        TimingMethod::System
354    };
355
356    // The adapter's vendor/device pair, which is what `WgpuServer` keys its
357    // SPIR-V store on. Reported unconditionally, even in a WGSL-only build that
358    // persists no compiled code: measurement caches (autotune, throughput) are
359    // namespaced by neither vendor nor device, so this string is the only thing
360    // that can tell one adapter's measurements from another's.
361    let fingerprint = format!("spirv_{}_{}", adapter_info.vendor, adapter_info.device);
362
363    let mut device_props = DeviceProperties::new(
364        Default::default(),
365        mem_props,
366        hardware_props,
367        time_measurement,
368        DeviceIdentity {
369            name: adapter_info.name.clone(),
370            fingerprint,
371        },
372    );
373
374    #[cfg(not(all(target_os = "macos", feature = "msl")))]
375    {
376        if features.contains(wgpu::Features::SUBGROUP)
377            && setup.adapter.get_info().device_type != wgpu::DeviceType::Cpu
378        {
379            use cubecl_ir::features::Plane;
380
381            device_props.features.plane.insert(Plane::Ops);
382        }
383    }
384
385    #[cfg(any(feature = "spirv", feature = "msl"))]
386    device_props
387        .features
388        .plane
389        .insert(cubecl_ir::features::Plane::NonUniformControlFlow);
390
391    backend::register_features(
392        &setup.adapter,
393        &mut device_props,
394        &mut compilation_options,
395        &options.memory_config,
396    );
397
398    let logger = alloc::sync::Arc::new(ServerLogger::default());
399
400    let allocator = ContiguousMemoryLayoutPolicy::new(device_props.memory.alignment as usize);
401    WgpuServer::new(
402        device_props.memory.clone(),
403        options.memory_config,
404        compilation_options,
405        setup.device.clone(),
406        setup.queue,
407        options.tasks_max,
408        setup.backend,
409        time_measurement,
410        ServerUtilities::new(device_props, logger, setup.backend, allocator),
411    )
412}
413
414/// Select the wgpu device and queue based on the provided [device](WgpuDevice) and
415/// [backend](wgpu::Backend).
416pub(crate) async fn create_setup_for_device(
417    device: &WgpuDevice,
418    backend: wgpu::Backend,
419) -> WgpuSetup {
420    let (instance, adapter) = request_adapter(device, backend).await;
421    let (device, queue) = backend::request_device(&adapter).await;
422
423    log::info!(
424        "Created wgpu compute server on device {:?}",
425        adapter.get_info()
426    );
427
428    WgpuSetup {
429        instance,
430        adapter,
431        device,
432        queue,
433        backend,
434    }
435}
436
437async fn request_adapter(
438    device: &WgpuDevice,
439    backend: wgpu::Backend,
440) -> (wgpu::Instance, wgpu::Adapter) {
441    #[cfg(not(feature = "vulkan-validate"))]
442    let instance_flags = {
443        let debug = ServerLogger::default();
444        // Debug/validation layers cost real per-dispatch CPU time, so only
445        // source-level compilation logging (`full`) opts into them — `basic`
446        // is passive name-only logging and must not change how kernels run.
447        match (debug.profile_level(), debug.compilation_source_activated()) {
448            (Some(ProfileLevel::Full), _) => InstanceFlags::advanced_debugging(),
449            (_, true) => InstanceFlags::debugging(),
450            (_, false) => InstanceFlags::default(),
451        }
452    };
453    #[cfg(feature = "vulkan-validate")]
454    let instance_flags = InstanceFlags::advanced_debugging();
455    log::debug!("{instance_flags:?}");
456    let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
457        backends: backend.into(),
458        flags: instance_flags,
459        ..wgpu::InstanceDescriptor::new_without_display_handle()
460    });
461
462    #[allow(deprecated)]
463    let override_device = if matches!(
464        device,
465        WgpuDevice::DefaultDevice | WgpuDevice::BestAvailable
466    ) {
467        get_device_override()
468    } else {
469        None
470    };
471
472    let device = override_device.unwrap_or_else(|| device.clone());
473
474    let adapter = match device {
475        #[cfg(not(target_family = "wasm"))]
476        WgpuDevice::DiscreteGpu(num) => {
477            select_from_adapter_list(
478                num,
479                "No Discrete GPU device found",
480                &instance,
481                &device,
482                backend,
483            )
484            .await
485        }
486        #[cfg(not(target_family = "wasm"))]
487        WgpuDevice::IntegratedGpu(num) => {
488            select_from_adapter_list(
489                num,
490                "No Integrated GPU device found",
491                &instance,
492                &device,
493                backend,
494            )
495            .await
496        }
497        #[cfg(not(target_family = "wasm"))]
498        WgpuDevice::VirtualGpu(num) => {
499            select_from_adapter_list(
500                num,
501                "No Virtual GPU device found",
502                &instance,
503                &device,
504                backend,
505            )
506            .await
507        }
508        #[cfg(not(target_family = "wasm"))]
509        WgpuDevice::Cpu => {
510            select_from_adapter_list(0, "No CPU device found", &instance, &device, backend).await
511        }
512        #[cfg(target_family = "wasm")]
513        WgpuDevice::IntegratedGpu(_) => {
514            request_adapter_with_preference(&instance, wgpu::PowerPreference::LowPower).await
515        }
516        WgpuDevice::Existing(_) => {
517            unreachable!("Cannot select an adapter for an existing device.")
518        }
519        _ => {
520            request_adapter_with_preference(&instance, wgpu::PowerPreference::HighPerformance).await
521        }
522    };
523
524    (instance, adapter)
525}
526
527async fn request_adapter_with_preference(
528    instance: &wgpu::Instance,
529    power_preference: wgpu::PowerPreference,
530) -> wgpu::Adapter {
531    instance
532        .request_adapter(&RequestAdapterOptions {
533            power_preference,
534            force_fallback_adapter: false,
535            compatible_surface: None,
536            ..RequestAdapterOptions::default()
537        })
538        .await
539        .expect("No possible adapter available for backend. Falling back to first available.")
540}
541
542#[cfg(not(target_family = "wasm"))]
543async fn select_from_adapter_list(
544    num: usize,
545    error: &str,
546    instance: &wgpu::Instance,
547    device: &WgpuDevice,
548    backend: wgpu::Backend,
549) -> wgpu::Adapter {
550    let mut adapters_other = Vec::new();
551    let mut adapters = Vec::new();
552
553    instance
554        .enumerate_adapters(backend.into())
555        .await
556        .into_iter()
557        .for_each(|adapter| {
558            let device_type = adapter.get_info().device_type;
559
560            if let wgpu::DeviceType::Other = device_type {
561                adapters_other.push(adapter);
562                return;
563            }
564
565            let is_same_type = match device {
566                WgpuDevice::DiscreteGpu(_) => device_type == wgpu::DeviceType::DiscreteGpu,
567                WgpuDevice::IntegratedGpu(_) => device_type == wgpu::DeviceType::IntegratedGpu,
568                WgpuDevice::VirtualGpu(_) => device_type == wgpu::DeviceType::VirtualGpu,
569                WgpuDevice::Cpu => device_type == wgpu::DeviceType::Cpu,
570                #[allow(deprecated)]
571                WgpuDevice::DefaultDevice | WgpuDevice::BestAvailable => true,
572                WgpuDevice::Existing(_) => {
573                    unreachable!("Cannot select an adapter for an existing device.")
574                }
575            };
576
577            if is_same_type {
578                adapters.push(adapter);
579            }
580        });
581
582    if adapters.len() <= num {
583        if adapters_other.len() <= num {
584            panic!(
585                "{}, adapters {:?}, other adapters {:?}",
586                error,
587                adapters
588                    .into_iter()
589                    .map(|adapter| adapter.get_info())
590                    .collect::<Vec<_>>(),
591                adapters_other
592                    .into_iter()
593                    .map(|adapter| adapter.get_info())
594                    .collect::<Vec<_>>(),
595            );
596        }
597
598        return adapters_other.remove(num);
599    }
600
601    adapters.remove(num)
602}
603
604fn get_device_override() -> Option<WgpuDevice> {
605    // If BestAvailable, check if we should instead construct as
606    // if a specific device was specified.
607    std::env::var("CUBECL_WGPU_DEFAULT_DEVICE")
608        .ok()
609        .and_then(|var| {
610            let override_device = if let Some(inner) = var.strip_prefix("DiscreteGpu(") {
611                inner
612                    .strip_suffix(")")
613                    .and_then(|s| s.parse().ok())
614                    .map(WgpuDevice::DiscreteGpu)
615            } else if let Some(inner) = var.strip_prefix("IntegratedGpu(") {
616                inner
617                    .strip_suffix(")")
618                    .and_then(|s| s.parse().ok())
619                    .map(WgpuDevice::IntegratedGpu)
620            } else if let Some(inner) = var.strip_prefix("VirtualGpu(") {
621                inner
622                    .strip_suffix(")")
623                    .and_then(|s| s.parse().ok())
624                    .map(WgpuDevice::VirtualGpu)
625            } else if var == "Cpu" {
626                Some(WgpuDevice::Cpu)
627            } else {
628                None
629            };
630
631            if override_device.is_none() {
632                log::warn!("Unknown CUBECL_WGPU_DEVICE override {var}");
633            }
634            override_device
635        })
636}