Skip to main content

cubecl_wgpu/
runtime.rs

1use std::marker::PhantomData;
2
3use crate::WgpuCompiler;
4use crate::{
5    AutoCompiler, AutoGraphicsApi, GraphicsApi, WgpuBackend, WgpuDevice, WgpuDeviceKind, backend,
6    compute::WgpuServer, contiguous_strides,
7};
8use cubecl_common::device::{Device, DeviceService, ServiceId};
9use cubecl_common::profile::TimingMethod;
10use cubecl_core::device::{DeviceId, ServerUtilitiesHandle};
11use cubecl_core::ir::TargetProperties;
12use cubecl_core::server::ServerUtilities;
13use cubecl_core::zspace::{Shape, Strides};
14use cubecl_environment::future;
15use cubecl_ir::{DeviceIdentity, DeviceProperties, HardwareProperties, MemoryDeviceProperties};
16use cubecl_server::allocator::ContiguousMemoryLayoutPolicy;
17#[cfg(not(feature = "vulkan-validate"))]
18use cubecl_server::logging::ProfileLevel;
19pub use cubecl_server::memory_management::MemoryConfiguration;
20use cubecl_server::runtime::Runtime;
21use cubecl_server::{client::Client, logging::ServerLogger};
22use wgpu::{InstanceFlags, RequestAdapterOptions};
23
24/// Runtime that uses the [wgpu] crate with the wgsl compiler. This is used in the Wgpu backend.
25/// For advanced configuration, use [`init_setup`] to pass in runtime options or to select a
26/// specific graphics API.
27#[derive(Debug)]
28pub struct WgpuRuntime<Compiler = AutoCompiler> {
29    _p: PhantomData<Compiler>,
30}
31
32impl<C> Clone for WgpuRuntime<C> {
33    fn clone(&self) -> Self {
34        Self { _p: self._p }
35    }
36}
37
38impl<C: WgpuCompiler> DeviceService for WgpuServer<C> {
39    fn init(device_id: cubecl_common::device::DeviceId) -> Self {
40        let device = WgpuDevice::from_id(device_id);
41        let setup = future::block_on(create_setup_for_device(
42            &device,
43            resolve_backend(device.backend),
44        ));
45        create_server(setup, RuntimeOptions::default(), device_id)
46    }
47
48    fn utilities(&self) -> ServerUtilitiesHandle {
49        self.utilities.clone() as ServerUtilitiesHandle
50    }
51}
52
53impl<C: WgpuCompiler> Runtime for WgpuRuntime<C> {
54    type Server = WgpuServer<C>;
55    type Device = WgpuDevice;
56
57    fn can_read_tensor(shape: &Shape, strides: &Strides) -> bool {
58        if shape.is_empty() {
59            return true;
60        }
61
62        for (&expected, &stride) in contiguous_strides(shape).iter().zip(strides.iter()) {
63            if expected != stride {
64                return false;
65            }
66        }
67
68        true
69    }
70
71    fn target_properties() -> TargetProperties {
72        TargetProperties {
73            // Values are irrelevant, since no wgsl backends currently support manual mma
74            mma: Default::default(),
75        }
76    }
77
78    fn enumerate_devices(type_id: u16) -> Vec<DeviceId> {
79        // The devices of that type on `Auto`, whose id is index zero with no
80        // graphics API in the top bits.
81        Self::enumerate_devices_like(DeviceId::new(type_id, 0))
82    }
83
84    fn is_available() -> bool {
85        // A software rasterizer — lavapipe, llvmpipe, WARP — enumerates as
86        // `WgpuDeviceKind::Cpu`. It runs, but a machine with nothing else is
87        // better served by a native CPU runtime, so wgpu does not claim it;
88        // a caller who wants it still names it.
89        let gpu = [
90            WgpuDeviceKind::DiscreteGpu(0),
91            WgpuDeviceKind::IntegratedGpu(0),
92            WgpuDeviceKind::VirtualGpu(0),
93            WgpuDeviceKind::Other(0),
94        ]
95        .map(|kind| WgpuDevice::new(kind).to_id().type_id);
96
97        Self::enumerate_all_devices()
98            .iter()
99            .any(|device| gpu.contains(&device.type_id))
100    }
101
102    /// Each adapter once, on whichever graphics API [`WgpuBackend::Auto`]
103    /// settles on — which is what a client created lazily resolves these ids
104    /// against. The same adapter pinned to another API is a device a caller
105    /// names, not one more to count.
106    fn enumerate_all_devices() -> Vec<DeviceId> {
107        adapters_on(WgpuBackend::Auto)
108    }
109
110    fn enumerate_devices_like(device_id: DeviceId) -> Vec<DeviceId> {
111        let device = WgpuDevice::from_id(device_id);
112        let reachable = adapters_on(device.backend);
113
114        match device.kind {
115            // One per graphics API, standing for whichever adapter it lands
116            // on: its own only peer, there as soon as the API has anything.
117            // Listing the adapters beside it would hand one of them a second
118            // client under another id.
119            WgpuDeviceKind::DefaultDevice if !reachable.is_empty() => alloc::vec![device_id],
120            _ => reachable
121                .into_iter()
122                .filter(|id| id.type_id == device_id.type_id)
123                .collect(),
124        }
125    }
126
127    /// The browser hands out one adapter without saying what it is, so a kind
128    /// there is only a power preference — and `request_adapter` honors the
129    /// low-power one as well as the rest.
130    #[cfg(target_family = "wasm")]
131    fn find_device(device_id: DeviceId) -> Result<(), usize> {
132        let device = WgpuDevice::from_id(device_id);
133        let peers = Self::enumerate_devices_like(device_id);
134
135        let low_power = device.kind == WgpuDeviceKind::IntegratedGpu(0)
136            && !adapters_on(device.backend).is_empty();
137
138        match peers.contains(&device_id) || low_power {
139            true => Ok(()),
140            false => Err(peers.len()),
141        }
142    }
143}
144
145/// The ids of the adapters `backend` reaches, pinned the way it is.
146///
147/// Those of the one graphics API it settles on, so each id resolves in
148/// `WgpuServer::init` to the adapter it was listed for. A device brought up on
149/// another API through [`init_setup`] is reached through the client that call
150/// hands back, not here.
151fn adapters_on(backend: WgpuBackend) -> Vec<DeviceId> {
152    // WebGPU only supports a single device currently, and only the browser's
153    // own API reaches it.
154    #[cfg(target_family = "wasm")]
155    let ids = match backend {
156        WgpuBackend::Auto | WgpuBackend::WebGpu => vec![DeviceId::new(0, 0)],
157        _ => Vec::new(),
158    };
159
160    #[cfg(not(target_family = "wasm"))]
161    let ids = settle(backend)
162        .map(|(_, adapters)| adapter_device_ids(adapters))
163        .unwrap_or_default();
164
165    ids.into_iter()
166        .map(|id| WgpuDevice::from_id(id).on(backend).to_id())
167        .collect()
168}
169
170/// The graphics API `backend` settles on, and the adapters this machine has
171/// there: the first of its candidates with a GPU, or failing that, the first
172/// with anything at all.
173///
174/// A software rasterizer is not reason enough to stop. Where Vulkan offers
175/// only lavapipe and `OpenGL` the real GPU — a VM passing it through, say —
176/// stopping at Vulkan leaves that GPU unreachable through `Auto`, and wgpu
177/// declining a machine it could have served.
178#[cfg(not(target_family = "wasm"))]
179fn settle(backend: WgpuBackend) -> Option<(wgpu::Backend, Vec<wgpu::Adapter>)> {
180    let mut software_only = None;
181
182    for api in backend_candidates(backend) {
183        let adapters = enumerate_all_adapters(instance_for(api), api);
184
185        if adapters
186            .iter()
187            .any(|adapter| adapter.get_info().device_type != wgpu::DeviceType::Cpu)
188        {
189            return Some((api, adapters));
190        }
191
192        if software_only.is_none() && !adapters.is_empty() {
193            software_only = Some((api, adapters));
194        }
195    }
196
197    software_only
198}
199
200/// The `wgpu` backends to try for a [`WgpuBackend`], best first.
201///
202/// A pinned one is the only candidate — that is what pinning it means.
203fn backend_candidates(backend: WgpuBackend) -> alloc::vec::Vec<wgpu::Backend> {
204    match backend {
205        WgpuBackend::Auto => AutoGraphicsApi::chain(),
206        WgpuBackend::Vulkan => alloc::vec![wgpu::Backend::Vulkan],
207        WgpuBackend::Metal => alloc::vec![wgpu::Backend::Metal],
208        WgpuBackend::Dx12 => alloc::vec![wgpu::Backend::Dx12],
209        WgpuBackend::Gl => alloc::vec![wgpu::Backend::Gl],
210        WgpuBackend::WebGpu => alloc::vec![wgpu::Backend::BrowserWebGpu],
211    }
212}
213
214/// An instance limited to one graphics API, for asking what it has.
215#[cfg(not(target_family = "wasm"))]
216fn instance_for(backend: wgpu::Backend) -> wgpu::Instance {
217    wgpu::Instance::new(wgpu::InstanceDescriptor {
218        backends: backend.into(),
219        ..wgpu::InstanceDescriptor::new_without_display_handle()
220    })
221}
222
223/// The graphics API a device on `backend` comes up on.
224///
225/// The one pinned, where one is. Otherwise the first of the chain
226/// this machine has an adapter for — which is what makes `Auto` mean Vulkan
227/// wherever Vulkan exists, and the next thing where it does not.
228pub(crate) fn resolve_backend(backend: WgpuBackend) -> wgpu::Backend {
229    #[cfg(not(target_family = "wasm"))]
230    if let Some((api, _)) = settle(backend) {
231        return api;
232    }
233
234    // Nothing answered: hand back the first anyway, so the failure is the
235    // setup's own rather than a silent fallback to some other API.
236    backend_candidates(backend)[0]
237}
238
239/// The `DeviceId` addressing each adapter, in enumeration order.
240///
241/// Every device type counts from zero on its own: `WgpuDevice::DiscreteGpu(n)`
242/// is the nth *discrete* adapter, not the nth adapter overall, so a single
243/// counter over the mixed list hands out ids for devices that do not exist.
244/// `Cpu` carries no index in `WgpuDevice`, so it stays at zero.
245#[cfg(not(target_family = "wasm"))]
246fn adapter_device_ids(adapters: Vec<wgpu::Adapter>) -> Vec<DeviceId> {
247    let mut next = [0u16; 7];
248
249    adapters
250        .into_iter()
251        .map(|adapter| {
252            let type_id = match adapter.get_info().device_type {
253                wgpu::DeviceType::DiscreteGpu => 0,
254                wgpu::DeviceType::IntegratedGpu => 1,
255                wgpu::DeviceType::VirtualGpu => 2,
256                wgpu::DeviceType::Cpu => 3,
257                wgpu::DeviceType::Other => 6,
258            };
259
260            // Only the indexed kinds have a counter; the rest are always zero.
261            let index = match next.get_mut(type_id as usize).filter(|_| type_id != 3) {
262                Some(next) => {
263                    let index = *next;
264                    *next += 1;
265                    index
266                }
267                None => 0,
268            };
269
270            DeviceId::new(type_id, index)
271        })
272        .collect()
273}
274
275#[cfg(not(target_family = "wasm"))]
276fn enumerate_all_adapters(instance: wgpu::Instance, backend: wgpu::Backend) -> Vec<wgpu::Adapter> {
277    // `enumerate_adapters` is now async & available on WebGPU
278    cubecl_environment::future::block_on(instance.enumerate_adapters(backend.into()))
279}
280
281/// The values that control how a WGPU Runtime will perform its calculations.
282pub struct RuntimeOptions {
283    /// Control the amount of compute tasks to be aggregated into a single GPU command.
284    pub tasks_max: usize,
285    /// Configures the memory management.
286    pub memory_config: MemoryConfiguration,
287}
288
289impl Default for RuntimeOptions {
290    fn default() -> Self {
291        #[cfg(test)]
292        const DEFAULT_MAX_TASKS: usize = 32;
293        #[cfg(not(test))]
294        const DEFAULT_MAX_TASKS: usize = 32;
295
296        let tasks_max = match std::env::var("CUBECL_WGPU_MAX_TASKS") {
297            Ok(value) => value
298                .parse::<usize>()
299                .expect("CUBECL_WGPU_MAX_TASKS should be a positive integer."),
300            Err(_) => DEFAULT_MAX_TASKS,
301        };
302
303        Self {
304            tasks_max,
305            memory_config: MemoryConfiguration::default(),
306        }
307    }
308}
309
310/// A complete setup used to run wgpu.
311///
312/// These can either be created with [`init_setup`] or [`init_setup_async`].
313#[derive(Clone, Debug)]
314pub struct WgpuSetup {
315    /// The underlying wgpu instance.
316    pub instance: wgpu::Instance,
317    /// The selected 'adapter'. This corresponds to a physical device.
318    pub adapter: wgpu::Adapter,
319    /// The wgpu device Burn will use. Nb: There can only be one device per adapter.
320    pub device: wgpu::Device,
321    /// The queue Burn commands will be submitted to.
322    pub queue: wgpu::Queue,
323    /// The backend used by the setup.
324    pub backend: wgpu::Backend,
325}
326
327/// Create a [`WgpuDevice`] on an existing [`WgpuSetup`].
328/// Useful when you want to share a device between `CubeCL` and other wgpu-dependent libraries.
329///
330/// # Note
331///
332/// Please **do not** to call on the same [`setup`](WgpuSetup) more than once.
333///
334/// This function generates a new, globally unique ID for the device every time it is called,
335/// even if called on the same device multiple times.
336pub fn init_device(setup: WgpuSetup, options: RuntimeOptions) -> WgpuDevice {
337    use core::sync::atomic::{AtomicU32, Ordering};
338
339    static COUNTER: AtomicU32 = AtomicU32::new(0);
340
341    let device_id = COUNTER.fetch_add(1, Ordering::Relaxed);
342    if device_id == u32::MAX {
343        core::panic!("Memory ID overflowed");
344    }
345
346    let device_id = WgpuDevice::new(WgpuDeviceKind::Existing(device_id));
347    let server = create_server::<AutoCompiler>(setup, options, device_id.to_id());
348    let _ = Client::init(device_id.to_id(), server);
349    device_id
350}
351
352/// Like [`init_setup_async`], but synchronous.
353/// On wasm, it is necessary to use [`init_setup_async`] instead.
354///
355/// A device brought up on a `G` other than [`AutoGraphicsApi`] is reached
356/// through the client this initializes, and is not among the devices
357/// [`Runtime::enumerate_devices`] lists: those ids index the auto backend's
358/// adapters, which is what a client created lazily resolves them against.
359///
360/// # Panics
361///
362/// Where `device` pins a graphics API and `G` names another: see
363/// [`init_setup_async`].
364pub fn init_setup<G: GraphicsApi>(device: &WgpuDevice, options: RuntimeOptions) -> WgpuSetup {
365    cfg_if::cfg_if! {
366        if #[cfg(target_family = "wasm")] {
367            let _ = (device, options);
368            panic!("Creating a wgpu setup synchronously is unsupported on wasm. Use init_async instead");
369        } else {
370            future::block_on(init_setup_async::<G>(device, options))
371        }
372    }
373}
374
375/// Initialize a client on the given device with the given options.
376/// This function is useful to configure the runtime options
377/// or to pick a different graphics API.
378///
379/// A device pinned to a graphics API comes up on that API: through
380/// [`AutoGraphicsApi`], or through the `G` naming the same one.
381///
382/// # Panics
383///
384/// Where `device` pins a graphics API and `G` names another. The client is
385/// registered under the device's id, and a pinned id promises its API to
386/// every caller who reaches for that client afterwards.
387pub async fn init_setup_async<G: GraphicsApi>(
388    device: &WgpuDevice,
389    options: RuntimeOptions,
390) -> WgpuSetup {
391    let backend = G::backend_for(device);
392
393    if device.backend != WgpuBackend::Auto {
394        let pinned = resolve_backend(device.backend);
395        assert_eq!(
396            backend, pinned,
397            "{device:?} is pinned to {pinned:?}, and cannot be set up on {backend:?}"
398        );
399    }
400
401    let setup = create_setup_for_device(device, backend).await;
402    let return_setup = setup.clone();
403    let server = create_server::<AutoCompiler>(setup, options, device.to_id());
404    let _ = Client::init(device.to_id(), server);
405    return_setup
406}
407
408/// The runtime name for `backend`, naming the compiler that serves it.
409fn runtime_name(backend: wgpu::Backend) -> &'static str {
410    match backend {
411        wgpu::Backend::Vulkan => {
412            #[cfg(feature = "spirv")]
413            return "wgpu<spirv>";
414
415            #[cfg(not(feature = "spirv"))]
416            return "wgpu<wgsl>";
417        }
418        wgpu::Backend::Metal => {
419            #[cfg(feature = "msl")]
420            return "wgpu<msl>";
421
422            #[cfg(not(feature = "msl"))]
423            return "wgpu<wgsl>";
424        }
425        _ => "wgpu<wgsl>",
426    }
427}
428
429pub(crate) fn create_server<C: WgpuCompiler>(
430    setup: WgpuSetup,
431    options: RuntimeOptions,
432    device_id: DeviceId,
433) -> WgpuServer<C> {
434    let limits = setup.device.limits();
435    let adapter_limits = setup.adapter.limits();
436    let mut adapter_info = setup.adapter.get_info();
437
438    // Workaround: WebGPU reports some "fake" subgroup info atm, as it's not really supported yet.
439    // However, some algorithms do rely on having this information eg. cubecl-reduce uses max subgroup size _even_ when
440    // subgroups aren't used. For now, just override with the maximum range of subgroups possible.
441    if adapter_info.subgroup_min_size == 0 && adapter_info.subgroup_max_size == 0 {
442        // There is in theory nothing limiting the size to go below 8 but in practice 8 is the minimum found anywhere.
443        adapter_info.subgroup_min_size = 8;
444        // This is a hard limit of GPU APIs (subgroup ballot returns 4 * 32 bits).
445        adapter_info.subgroup_max_size = 128;
446    }
447
448    // WebGPU states no capacity. `register_features` fills it in where the
449    // backend's own API states one and this build enabled that backend:
450    // `spirv` for Vulkan, `msl` for Metal.
451    let mem_props = MemoryDeviceProperties::new(
452        limits.max_storage_buffer_binding_size,
453        limits.min_uniform_buffer_offset_alignment as u64,
454    );
455    let max_count = adapter_limits.max_compute_workgroups_per_dimension;
456    let hardware_props = HardwareProperties {
457        load_width: 128,
458        // On Apple Silicon, the plane size is 32,
459        // though the minimum and maximum differ.
460        // https://github.com/gpuweb/gpuweb/issues/3950
461        #[cfg(apple_silicon)]
462        plane_size_min: 32,
463        #[cfg(not(apple_silicon))]
464        plane_size_min: adapter_info.subgroup_min_size,
465        #[cfg(apple_silicon)]
466        plane_size_max: 32,
467        #[cfg(not(apple_silicon))]
468        plane_size_max: adapter_info.subgroup_max_size,
469        // wgpu uses an additional buffer for variable-length buffers,
470        // so we have to use one buffer less on our side to make room for that wgpu internal buffer.
471        // See: https://github.com/gfx-rs/wgpu/blob/a9638c8e3ac09ce4f27ac171f8175671e30365fd/wgpu-hal/src/metal/device.rs#L799
472        max_bindings: limits
473            .max_storage_buffers_per_shader_stage
474            .saturating_sub(1),
475        max_shared_memory_size: limits.max_compute_workgroup_storage_size as usize,
476        max_cube_count: (max_count, max_count, max_count),
477        max_units_per_cube: adapter_limits.max_compute_invocations_per_workgroup,
478        max_cube_dim: (
479            adapter_limits.max_compute_workgroup_size_x,
480            adapter_limits.max_compute_workgroup_size_y,
481            adapter_limits.max_compute_workgroup_size_z,
482        ),
483        num_streaming_multiprocessors: None,
484        num_tensor_cores: None,
485        min_tensor_cores_dim: None,
486        num_cpu_cores: None, // TODO: Check if device is CPU.
487        last_level_cache_size: None,
488        max_vector_size: 4,
489        // Init later if extension is enabled
490        cube_mma_reserved_shared_memory: 0,
491    };
492
493    let mut compilation_options = Default::default();
494
495    let features = setup.adapter.features();
496
497    let time_measurement = if features.contains(wgpu::Features::TIMESTAMP_QUERY) {
498        TimingMethod::Device
499    } else {
500        TimingMethod::System
501    };
502
503    // The adapter's vendor/device pair, which is what `WgpuServer` keys its
504    // SPIR-V store on. Reported unconditionally, even in a WGSL-only build that
505    // persists no compiled code: measurement caches (autotune, throughput) are
506    // namespaced by neither vendor nor device, so this string is the only thing
507    // that can tell one adapter's measurements from another's.
508    let fingerprint = format!("spirv_{}_{}", adapter_info.vendor, adapter_info.device);
509
510    let mut device_props = DeviceProperties::new(
511        Default::default(),
512        mem_props,
513        hardware_props,
514        time_measurement,
515        DeviceIdentity {
516            name: adapter_info.name.clone(),
517            fingerprint,
518            physical: backend::physical_device(&setup.adapter, &adapter_info),
519        },
520    );
521
522    #[cfg(not(all(target_os = "macos", feature = "msl")))]
523    {
524        if features.contains(wgpu::Features::SUBGROUP)
525            && setup.adapter.get_info().device_type != wgpu::DeviceType::Cpu
526        {
527            use cubecl_ir::features::Plane;
528
529            device_props.features.plane.insert(Plane::Ops);
530        }
531    }
532
533    #[cfg(any(feature = "spirv", feature = "msl"))]
534    device_props
535        .features
536        .plane
537        .insert(cubecl_ir::features::Plane::NonUniformControlFlow);
538
539    backend::register_features(
540        &setup.adapter,
541        &mut device_props,
542        &mut compilation_options,
543        &options.memory_config,
544    );
545
546    let logger = alloc::sync::Arc::new(ServerLogger::default());
547
548    let allocator = ContiguousMemoryLayoutPolicy::new(device_props.memory.alignment as usize);
549    WgpuServer::new(
550        device_props.memory.clone(),
551        options.memory_config,
552        compilation_options,
553        setup.device.clone(),
554        setup.queue,
555        options.tasks_max,
556        setup.backend,
557        time_measurement,
558        ServerUtilities::new(
559            ServiceId::of::<WgpuServer<C>>(device_id),
560            runtime_name(setup.backend),
561            device_props,
562            WgpuRuntime::<C>::target_properties(),
563            logger,
564            allocator,
565        ),
566    )
567}
568
569/// Select the wgpu device and queue based on the provided [device](WgpuDevice) and
570/// [backend](wgpu::Backend).
571pub(crate) async fn create_setup_for_device(
572    device: &WgpuDevice,
573    backend: wgpu::Backend,
574) -> WgpuSetup {
575    let (instance, adapter) = request_adapter(device, backend).await;
576    let (device, queue) = backend::request_device(&adapter).await;
577
578    log::info!(
579        "Created wgpu compute server on device {:?}",
580        adapter.get_info()
581    );
582
583    WgpuSetup {
584        instance,
585        adapter,
586        device,
587        queue,
588        backend,
589    }
590}
591
592async fn request_adapter(
593    device: &WgpuDevice,
594    backend: wgpu::Backend,
595) -> (wgpu::Instance, wgpu::Adapter) {
596    #[cfg(not(feature = "vulkan-validate"))]
597    let instance_flags = {
598        let debug = ServerLogger::default();
599        // Debug/validation layers cost real per-dispatch CPU time, so only
600        // source-level compilation logging (`full`) opts into them — `basic`
601        // is passive name-only logging and must not change how kernels run.
602        match (debug.profile_level(), debug.compilation_source_activated()) {
603            (Some(ProfileLevel::Full), _) => InstanceFlags::advanced_debugging(),
604            (_, true) => InstanceFlags::debugging(),
605            (_, false) => InstanceFlags::default(),
606        }
607    };
608    #[cfg(feature = "vulkan-validate")]
609    let instance_flags = InstanceFlags::advanced_debugging();
610    log::debug!("{instance_flags:?}");
611    let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
612        backends: backend.into(),
613        flags: instance_flags,
614        ..wgpu::InstanceDescriptor::new_without_display_handle()
615    });
616
617    // The variable names a device, not a graphics API, so a caller who pinned
618    // one keeps it.
619    let override_device = match device.kind {
620        WgpuDeviceKind::DefaultDevice => get_device_override().map(|kind| WgpuDevice {
621            kind,
622            backend: device.backend,
623        }),
624        _ => None,
625    };
626
627    let device = override_device.unwrap_or_else(|| device.clone());
628
629    let adapter = match device.kind {
630        #[cfg(not(target_family = "wasm"))]
631        WgpuDeviceKind::DiscreteGpu(num) => {
632            select_from_adapter_list(
633                num,
634                "No Discrete GPU device found",
635                &instance,
636                &device,
637                backend,
638            )
639            .await
640        }
641        #[cfg(not(target_family = "wasm"))]
642        WgpuDeviceKind::IntegratedGpu(num) => {
643            select_from_adapter_list(
644                num,
645                "No Integrated GPU device found",
646                &instance,
647                &device,
648                backend,
649            )
650            .await
651        }
652        #[cfg(not(target_family = "wasm"))]
653        WgpuDeviceKind::VirtualGpu(num) => {
654            select_from_adapter_list(
655                num,
656                "No Virtual GPU device found",
657                &instance,
658                &device,
659                backend,
660            )
661            .await
662        }
663        #[cfg(not(target_family = "wasm"))]
664        WgpuDeviceKind::Other(num) => {
665            select_from_adapter_list(num, "No Other device found", &instance, &device, backend)
666                .await
667        }
668        #[cfg(not(target_family = "wasm"))]
669        WgpuDeviceKind::Cpu => {
670            select_from_adapter_list(0, "No CPU device found", &instance, &device, backend).await
671        }
672        #[cfg(target_family = "wasm")]
673        WgpuDeviceKind::IntegratedGpu(_) => {
674            request_adapter_with_preference(&instance, wgpu::PowerPreference::LowPower).await
675        }
676        WgpuDeviceKind::Existing(_) => {
677            unreachable!("Cannot select an adapter for an existing device.")
678        }
679        _ => {
680            request_adapter_with_preference(&instance, wgpu::PowerPreference::HighPerformance).await
681        }
682    };
683
684    (instance, adapter)
685}
686
687async fn request_adapter_with_preference(
688    instance: &wgpu::Instance,
689    power_preference: wgpu::PowerPreference,
690) -> wgpu::Adapter {
691    instance
692        .request_adapter(&RequestAdapterOptions {
693            power_preference,
694            force_fallback_adapter: false,
695            compatible_surface: None,
696            ..RequestAdapterOptions::default()
697        })
698        .await
699        .expect("No possible adapter available for backend. Falling back to first available.")
700}
701
702#[cfg(not(target_family = "wasm"))]
703async fn select_from_adapter_list(
704    num: usize,
705    error: &str,
706    instance: &wgpu::Instance,
707    device: &WgpuDevice,
708    backend: wgpu::Backend,
709) -> wgpu::Adapter {
710    // A kind is what the graphics API reports, and nothing stands in for it:
711    // `OpenGL` calling a GPU `Other` makes it `Other(n)` there, not a discrete
712    // GPU by another name. Anything looser selects adapters `find_device` says
713    // the machine does not have, and two ids end up on one adapter.
714    let adapters = instance.enumerate_adapters(backend.into()).await;
715    let found = adapters
716        .iter()
717        .map(|adapter| adapter.get_info())
718        .collect::<Vec<_>>();
719
720    let is_same_type = |adapter: &wgpu::Adapter| {
721        let device_type = adapter.get_info().device_type;
722
723        match device.kind {
724            WgpuDeviceKind::DiscreteGpu(_) => device_type == wgpu::DeviceType::DiscreteGpu,
725            WgpuDeviceKind::IntegratedGpu(_) => device_type == wgpu::DeviceType::IntegratedGpu,
726            WgpuDeviceKind::VirtualGpu(_) => device_type == wgpu::DeviceType::VirtualGpu,
727            WgpuDeviceKind::Cpu => device_type == wgpu::DeviceType::Cpu,
728            WgpuDeviceKind::Other(_) => device_type == wgpu::DeviceType::Other,
729            WgpuDeviceKind::DefaultDevice => true,
730            WgpuDeviceKind::Existing(_) => {
731                unreachable!("Cannot select an adapter for an existing device.")
732            }
733        }
734    };
735
736    adapters
737        .into_iter()
738        .filter(is_same_type)
739        .nth(num)
740        .unwrap_or_else(|| panic!("{error}, adapters {found:?}"))
741}
742
743fn get_device_override() -> Option<WgpuDeviceKind> {
744    // If BestAvailable, check if we should instead construct as
745    // if a specific device was specified.
746    std::env::var("CUBECL_WGPU_DEFAULT_DEVICE")
747        .ok()
748        .and_then(|var| {
749            let override_device = if let Some(inner) = var.strip_prefix("DiscreteGpu(") {
750                inner
751                    .strip_suffix(")")
752                    .and_then(|s| s.parse().ok())
753                    .map(WgpuDeviceKind::DiscreteGpu)
754            } else if let Some(inner) = var.strip_prefix("IntegratedGpu(") {
755                inner
756                    .strip_suffix(")")
757                    .and_then(|s| s.parse().ok())
758                    .map(WgpuDeviceKind::IntegratedGpu)
759            } else if let Some(inner) = var.strip_prefix("VirtualGpu(") {
760                inner
761                    .strip_suffix(")")
762                    .and_then(|s| s.parse().ok())
763                    .map(WgpuDeviceKind::VirtualGpu)
764            } else if var == "Cpu" {
765                Some(WgpuDeviceKind::Cpu)
766            } else {
767                None
768            };
769
770            if override_device.is_none() {
771                log::warn!("Unknown CUBECL_WGPU_DEVICE override {var}");
772            }
773            override_device
774        })
775}
776
777#[cfg(all(test, not(target_family = "wasm")))]
778mod device_tests {
779    use super::*;
780
781    const PINNED: [WgpuBackend; 4] = [
782        WgpuBackend::Vulkan,
783        WgpuBackend::Metal,
784        WgpuBackend::Dx12,
785        WgpuBackend::Gl,
786    ];
787
788    /// One adapter is one device. Listing it again for every graphics API
789    /// that reaches it makes a single GPU look like several, and whatever
790    /// counts devices — a collective, a transfer between two of them — runs
791    /// on hardware that is not there.
792    #[test]
793    fn each_adapter_is_listed_once() {
794        let ids = <WgpuRuntime>::enumerate_all_devices();
795
796        let adapters = settle(WgpuBackend::Auto).map_or(0, |(_, adapters)| adapters.len());
797
798        assert_eq!(ids.len(), adapters);
799        for id in ids {
800            assert_eq!(WgpuDevice::from_id(id).backend, WgpuBackend::Auto);
801        }
802    }
803
804    /// A device pinned to an API is found where that API has it, whatever
805    /// the API `Auto` settles on has — the kinds differ from one API to the
806    /// next, `OpenGL` calling a GPU what Vulkan calls discrete.
807    #[test]
808    fn a_device_is_found_on_the_api_it_names() {
809        for backend in PINNED {
810            let reachable = adapters_on(backend);
811
812            for id in reachable.iter() {
813                assert_eq!(
814                    <WgpuRuntime>::find_device(*id),
815                    Ok(()),
816                    "{id} on {backend:?}"
817                );
818            }
819
820            let default = WgpuDevice::new(WgpuDeviceKind::DefaultDevice).on(backend);
821            assert_eq!(
822                <WgpuRuntime>::find_device(default.to_id()).is_ok(),
823                !reachable.is_empty(),
824                "the default device on {backend:?}"
825            );
826        }
827    }
828
829    /// The default device stands for whichever adapter it lands on, so it is
830    /// its own only peer: the adapters listed beside it would give one of them
831    /// a second client, and leaving it out drops the caller from its own list.
832    #[test]
833    fn the_default_device_is_its_own_only_peer() {
834        for backend in PINNED.into_iter().chain([WgpuBackend::Auto]) {
835            let default = WgpuDevice::new(WgpuDeviceKind::DefaultDevice)
836                .on(backend)
837                .to_id();
838
839            let expected = match adapters_on(backend).is_empty() {
840                true => Vec::new(),
841                false => alloc::vec![default],
842            };
843
844            assert_eq!(<WgpuRuntime>::enumerate_devices_like(default), expected);
845        }
846    }
847
848    /// Setting a pinned device up through `AutoGraphicsApi` keeps its pin —
849    /// the natural call, `init_setup` being the only way to pass options.
850    #[test]
851    fn auto_defers_to_the_api_a_device_pins() {
852        for (backend, api) in [
853            (WgpuBackend::Vulkan, wgpu::Backend::Vulkan),
854            (WgpuBackend::Metal, wgpu::Backend::Metal),
855            (WgpuBackend::Dx12, wgpu::Backend::Dx12),
856            (WgpuBackend::Gl, wgpu::Backend::Gl),
857        ] {
858            let device = WgpuDevice::new(WgpuDeviceKind::DefaultDevice).on(backend);
859
860            assert_eq!(AutoGraphicsApi::backend_for(&device), api);
861        }
862    }
863
864    /// Naming one API for a device pinned to another is refused before any
865    /// adapter is asked for: the client lands under the pinned id, and would
866    /// hand every later caller the wrong API.
867    #[test]
868    #[should_panic(expected = "is pinned to Gl")]
869    fn a_setup_on_another_api_than_the_pinned_one_is_refused() {
870        let device = WgpuDevice::new(WgpuDeviceKind::DefaultDevice).on(WgpuBackend::Gl);
871
872        init_setup::<crate::Vulkan>(&device, RuntimeOptions::default());
873    }
874
875    /// A pinned device's peers are those of its own API. The same adapters on
876    /// `Auto` are other devices, with other clients.
877    #[test]
878    fn a_pinned_device_is_enumerated_with_its_own_api() {
879        for backend in PINNED {
880            for id in adapters_on(backend) {
881                let peers = <WgpuRuntime>::enumerate_devices_like(id);
882
883                assert!(peers.contains(&id), "{id} among {peers:?}");
884                for peer in peers {
885                    assert_eq!(WgpuDevice::from_id(peer).backend, backend);
886                }
887            }
888        }
889    }
890
891    /// And one that API does not have is a miss, reported against what it
892    /// has of that kind.
893    #[test]
894    fn an_index_past_the_end_is_not_found_on_any_api() {
895        for backend in PINNED.into_iter().chain([WgpuBackend::Auto]) {
896            let device = WgpuDevice::new(WgpuDeviceKind::DiscreteGpu(4242)).on(backend);
897
898            let discrete = adapters_on(backend)
899                .into_iter()
900                .filter(|id| id.type_id == device.to_id().type_id)
901                .count();
902
903            assert_eq!(<WgpuRuntime>::find_device(device.to_id()), Err(discrete));
904        }
905    }
906}