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::WgpuCompilationOptions;
11use cubecl_core::device::{DeviceId, ServerUtilitiesHandle};
12use cubecl_core::ir::TargetProperties;
13use cubecl_core::server::ServerUtilities;
14use cubecl_core::zspace::{Shape, Strides};
15use cubecl_environment::{future, sync::Mutex};
16use cubecl_ir::{DeviceIdentity, DeviceProperties, HardwareProperties, MemoryDeviceProperties};
17use cubecl_monitoring::{DeviceUtilization, UtilizationUnavailable, gpu_utilization::CardCounters};
18use cubecl_server::allocator::ContiguousMemoryLayoutPolicy;
19#[cfg(not(feature = "vulkan-validate"))]
20use cubecl_server::logging::ProfileLevel;
21pub use cubecl_server::memory_management::MemoryConfiguration;
22use cubecl_server::runtime::Runtime;
23use cubecl_server::{client::Client, logging::ServerLogger};
24use wgpu::{InstanceFlags, RequestAdapterOptions};
25
26/// Runtime that uses the [wgpu] crate.
27///
28/// The default [`AutoCompiler`] selects a native shader compiler when supported and falls back to
29/// WGSL otherwise. Supply an explicit compiler type when fallback is not desired.
30/// With `msl` enabled, [`AutoCompiler`] requires native MSL support for devices pinned to Metal;
31/// an automatic device can still fall back to WGSL. An explicit [`crate::WgslCompiler`] continues
32/// to use WGSL on Metal without requiring native MSL support.
33///
34/// For advanced configuration, use [`init_setup`] to pass runtime options or select a specific
35/// graphics API.
36#[derive(Debug)]
37pub struct WgpuRuntime<Compiler = AutoCompiler> {
38    _p: PhantomData<Compiler>,
39}
40
41impl<C> Clone for WgpuRuntime<C> {
42    fn clone(&self) -> Self {
43        Self { _p: self._p }
44    }
45}
46
47impl<C: WgpuCompiler> DeviceService for WgpuServer<C> {
48    fn init(device_id: cubecl_common::device::DeviceId) -> Self {
49        let device = WgpuDevice::from_id(device_id);
50        let setup =
51            future::block_on(try_create_setup(&device)).expect("Unable to acquire wgpu device");
52        try_create_server(setup, RuntimeOptions::default(), device_id, device.backend)
53            .expect("Unable to initialize wgpu runtime")
54    }
55
56    fn utilities(&self) -> ServerUtilitiesHandle {
57        self.utilities.clone() as ServerUtilitiesHandle
58    }
59}
60
61impl<C: WgpuCompiler> Runtime for WgpuRuntime<C> {
62    type Server = WgpuServer<C>;
63    type Device = WgpuDevice;
64
65    fn can_read_tensor(shape: &Shape, strides: &Strides) -> bool {
66        if shape.is_empty() {
67            return true;
68        }
69
70        for (&expected, &stride) in contiguous_strides(shape).iter().zip(strides.iter()) {
71            if expected != stride {
72                return false;
73            }
74        }
75
76        true
77    }
78
79    fn target_properties() -> TargetProperties {
80        TargetProperties {
81            // Values are irrelevant, since no wgsl backends currently support manual mma
82            mma: Default::default(),
83        }
84    }
85
86    fn enumerate_devices(type_id: u16) -> Vec<DeviceId> {
87        // The devices of that type on `Auto`, whose id is index zero with no
88        // graphics API in the top bits.
89        Self::enumerate_devices_like(DeviceId::new(type_id, 0))
90    }
91
92    fn is_available() -> bool {
93        // A software rasterizer — lavapipe, llvmpipe, WARP — enumerates as
94        // `WgpuDeviceKind::Cpu`. It runs, but a machine with nothing else is
95        // better served by a native CPU runtime, so wgpu does not claim it;
96        // a caller who wants it still names it.
97        let gpu = [
98            WgpuDeviceKind::DiscreteGpu(0),
99            WgpuDeviceKind::IntegratedGpu(0),
100            WgpuDeviceKind::VirtualGpu(0),
101            WgpuDeviceKind::Other(0),
102        ]
103        .map(|kind| WgpuDevice::new(kind).to_id().type_id);
104
105        Self::enumerate_all_devices()
106            .iter()
107            .any(|device| gpu.contains(&device.type_id))
108    }
109
110    /// Each adapter once, on whichever graphics API [`WgpuBackend::Auto`]
111    /// settles on — which is what a client created lazily resolves these ids
112    /// against. The same adapter pinned to another API is a device a caller
113    /// names, not one more to count.
114    fn enumerate_all_devices() -> Vec<DeviceId> {
115        adapters_on(WgpuBackend::Auto)
116    }
117
118    fn enumerate_devices_like(device_id: DeviceId) -> Vec<DeviceId> {
119        let device = WgpuDevice::from_id(device_id);
120        let reachable = adapters_on(device.backend);
121
122        match device.kind {
123            // One per graphics API, standing for whichever adapter it lands
124            // on: its own only peer, there as soon as the API has anything.
125            // Listing the adapters beside it would hand one of them a second
126            // client under another id.
127            WgpuDeviceKind::DefaultDevice if !reachable.is_empty() => alloc::vec![device_id],
128            _ => reachable
129                .into_iter()
130                .filter(|id| id.type_id == device_id.type_id)
131                .collect(),
132        }
133    }
134
135    /// The browser hands out one adapter without saying what it is, so a kind
136    /// there is only a power preference — and `request_adapter` honors the
137    /// low-power one as well as the rest.
138    #[cfg(target_family = "wasm")]
139    fn find_device(device_id: DeviceId) -> Result<(), usize> {
140        let device = WgpuDevice::from_id(device_id);
141        let peers = Self::enumerate_devices_like(device_id);
142
143        let low_power = device.kind == WgpuDeviceKind::IntegratedGpu(0)
144            && !adapters_on(device.backend).is_empty();
145
146        match peers.contains(&device_id) || low_power {
147            true => Ok(()),
148            false => Err(peers.len()),
149        }
150    }
151
152    fn utilization(device: &Self::Device) -> Result<DeviceUtilization, UtilizationUnavailable> {
153        CardCounters::read(Self::client(device).properties().identity.physical.as_ref())
154    }
155}
156
157/// The ids of the adapters `backend` reaches, pinned the way it is.
158///
159/// Those of the one graphics API it settles on, so each id resolves in
160/// `WgpuServer::init` to the adapter it was listed for. A device brought up on
161/// another API through [`init_setup`] is reached through the client that call
162/// hands back, not here.
163fn adapters_on(backend: WgpuBackend) -> Vec<DeviceId> {
164    // WebGPU only supports a single device currently, and only the browser's
165    // own API reaches it.
166    #[cfg(target_family = "wasm")]
167    let ids = match backend {
168        WgpuBackend::Auto | WgpuBackend::WebGpu => vec![DeviceId::new(0, 0)],
169        _ => Vec::new(),
170    };
171
172    #[cfg(not(target_family = "wasm"))]
173    let ids = settle(backend)
174        .map(|(_, adapters)| adapter_device_ids(adapters))
175        .unwrap_or_default();
176
177    ids.into_iter()
178        .map(|id| WgpuDevice::from_id(id).on(backend).to_id())
179        .collect()
180}
181
182/// The graphics API `backend` settles on, and the adapters this machine has
183/// there: the first of its candidates with a GPU, or failing that, the first
184/// with anything at all.
185///
186/// A software rasterizer is not reason enough to stop. Where Vulkan offers
187/// only lavapipe and `OpenGL` the real GPU — a VM passing it through, say —
188/// stopping at Vulkan leaves that GPU unreachable through `Auto`, and wgpu
189/// declining a machine it could have served.
190#[cfg(not(target_family = "wasm"))]
191fn settle(backend: WgpuBackend) -> Option<(wgpu::Backend, Vec<wgpu::AdapterInfo>)> {
192    let mut software_only = None;
193
194    for api in backend_candidates(backend) {
195        let adapters = probe(api);
196
197        if adapters
198            .iter()
199            .any(|adapter| adapter.device_type != wgpu::DeviceType::Cpu)
200        {
201            return Some((api, adapters));
202        }
203
204        if software_only.is_none() && !adapters.is_empty() {
205            software_only = Some((api, adapters));
206        }
207    }
208
209    software_only
210}
211
212/// The `wgpu` backends to try for a [`WgpuBackend`], best first.
213///
214/// A pinned one is the only candidate — that is what pinning it means.
215fn backend_candidates(backend: WgpuBackend) -> alloc::vec::Vec<wgpu::Backend> {
216    match backend {
217        WgpuBackend::Auto => AutoGraphicsApi::chain(),
218        WgpuBackend::Vulkan => alloc::vec![wgpu::Backend::Vulkan],
219        WgpuBackend::Metal => alloc::vec![wgpu::Backend::Metal],
220        WgpuBackend::Dx12 => alloc::vec![wgpu::Backend::Dx12],
221        WgpuBackend::Gl => alloc::vec![wgpu::Backend::Gl],
222        WgpuBackend::WebGpu => alloc::vec![wgpu::Backend::BrowserWebGpu],
223    }
224}
225
226/// The instance each backend's devices come up on, kept so a device going away never destroys
227/// one. Locked whenever an instance is created or dropped: the Vulkan loader crashes when
228/// instances come and go on several threads at once.
229static INSTANCES: Mutex<Vec<(wgpu::Backend, wgpu::Instance)>> = Mutex::new(Vec::new());
230
231/// The adapters `backend` has on this machine, asked through an instance limited to it.
232#[cfg(not(target_family = "wasm"))]
233fn probe(backend: wgpu::Backend) -> Vec<wgpu::AdapterInfo> {
234    let _instances = INSTANCES.lock();
235    let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
236        backends: backend.into(),
237        ..wgpu::InstanceDescriptor::new_without_display_handle()
238    });
239    future::block_on(instance.enumerate_adapters(backend.into()))
240        .iter()
241        .map(wgpu::Adapter::get_info)
242        .collect()
243}
244
245/// The graphics API a device on `backend` comes up on.
246///
247/// The one pinned, where one is. Otherwise the first of the chain
248/// this machine has an adapter for — which is what makes `Auto` mean Vulkan
249/// wherever Vulkan exists, and the next thing where it does not.
250pub(crate) fn resolve_backend(backend: WgpuBackend) -> wgpu::Backend {
251    #[cfg(not(target_family = "wasm"))]
252    if let Some((api, _)) = settle(backend) {
253        return api;
254    }
255
256    // Nothing answered: hand back the first anyway, so the failure is the
257    // setup's own rather than a silent fallback to some other API.
258    backend_candidates(backend)[0]
259}
260
261/// The `DeviceId` addressing each adapter, in enumeration order.
262///
263/// Every device type counts from zero on its own: `WgpuDevice::DiscreteGpu(n)`
264/// is the nth *discrete* adapter, not the nth adapter overall, so a single
265/// counter over the mixed list hands out ids for devices that do not exist.
266/// `Cpu` carries no index in `WgpuDevice`, so it stays at zero.
267#[cfg(not(target_family = "wasm"))]
268fn adapter_device_ids(adapters: Vec<wgpu::AdapterInfo>) -> Vec<DeviceId> {
269    let mut next = [0u16; 7];
270
271    adapters
272        .into_iter()
273        .map(|adapter| {
274            let type_id = match adapter.device_type {
275                wgpu::DeviceType::DiscreteGpu => 0,
276                wgpu::DeviceType::IntegratedGpu => 1,
277                wgpu::DeviceType::VirtualGpu => 2,
278                wgpu::DeviceType::Cpu => 3,
279                wgpu::DeviceType::Other => 6,
280            };
281
282            // Only the indexed kinds have a counter; the rest are always zero.
283            let index = match next.get_mut(type_id as usize).filter(|_| type_id != 3) {
284                Some(next) => {
285                    let index = *next;
286                    *next += 1;
287                    index
288                }
289                None => 0,
290            };
291
292            DeviceId::new(type_id, index)
293        })
294        .collect()
295}
296
297/// A recoverable failure while acquiring or registering a wgpu runtime.
298#[derive(Debug, thiserror::Error)]
299#[non_exhaustive]
300pub enum WgpuInitError {
301    /// Options, environment defaults, or supplied setup handles are invalid.
302    #[error("Invalid wgpu configuration: {message}")]
303    InvalidConfiguration {
304        /// The invalid configuration and why it was rejected.
305        message: String,
306    },
307    /// The requested API is not compiled for this platform.
308    #[error("Graphics API {api:?} is unavailable in this build")]
309    UnsupportedGraphicsApi {
310        /// The requested API.
311        api: WgpuBackend,
312    },
313    /// No adapter matches the requested selector.
314    #[error("No adapter available for {device:?}: {message}")]
315    AdapterUnavailable {
316        /// The requested selector, including its graphics API.
317        device: WgpuDevice,
318        /// Details from adapter selection.
319        message: String,
320    },
321    /// The adapter refused to create a device.
322    #[error("Unable to request a wgpu device: {message}")]
323    RequestDevice {
324        /// Details from the graphics API.
325        message: String,
326    },
327    /// A runtime could not be registered.
328    #[error("Unable to register wgpu runtime: {message}")]
329    Registration {
330        /// Registration failure details.
331        message: String,
332    },
333}
334
335/// The values that control how a WGPU Runtime will perform its calculations.
336pub struct RuntimeOptions {
337    /// Control the amount of compute tasks to be aggregated into a single GPU command.
338    pub tasks_max: usize,
339    /// Configures the memory management.
340    pub memory_config: MemoryConfiguration,
341}
342
343impl Default for RuntimeOptions {
344    fn default() -> Self {
345        Self::try_default().expect("Invalid wgpu runtime defaults")
346    }
347}
348
349impl RuntimeOptions {
350    /// Resolve environment defaults without panicking on invalid configuration.
351    pub fn try_default() -> Result<Self, WgpuInitError> {
352        Self::with_tasks_max(None)
353    }
354
355    /// Resolve defaults, letting an explicit task count override the environment.
356    pub fn with_tasks_max(tasks_max: Option<usize>) -> Result<Self, WgpuInitError> {
357        let tasks_max = match tasks_max {
358            Some(value) => value,
359            None => match std::env::var("CUBECL_WGPU_MAX_TASKS") {
360                Ok(value) => value
361                    .parse()
362                    .map_err(|_| WgpuInitError::InvalidConfiguration {
363                        message: "CUBECL_WGPU_MAX_TASKS must be a positive integer".into(),
364                    })?,
365                Err(std::env::VarError::NotPresent) => 32,
366                Err(err) => {
367                    return Err(WgpuInitError::InvalidConfiguration {
368                        message: err.to_string(),
369                    });
370                }
371            },
372        };
373        let options = Self {
374            tasks_max,
375            memory_config: MemoryConfiguration::default(),
376        };
377        options.validate()?;
378        Ok(options)
379    }
380
381    fn validate(&self) -> Result<(), WgpuInitError> {
382        if self.tasks_max == 0 {
383            return Err(WgpuInitError::InvalidConfiguration {
384                message: "tasks_max must be greater than zero".into(),
385            });
386        }
387        Ok(())
388    }
389}
390
391/// A complete setup used to run wgpu.
392///
393/// These can either be created with [`init_setup`] or [`init_setup_async`].
394#[derive(Clone, Debug)]
395pub struct WgpuSetup {
396    /// The underlying wgpu instance.
397    pub instance: wgpu::Instance,
398    /// The selected 'adapter'. This corresponds to a physical device.
399    pub adapter: wgpu::Adapter,
400    /// The logical wgpu device the runtime will use.
401    pub device: wgpu::Device,
402    /// The queue Burn commands will be submitted to.
403    pub queue: wgpu::Queue,
404    /// The backend used by the setup.
405    pub backend: wgpu::Backend,
406}
407
408/// Create a [`WgpuDevice`] on an existing [`WgpuSetup`].
409/// Useful when you want to share a device between `CubeCL` and other wgpu-dependent libraries.
410///
411/// Uses [`AutoCompiler`] with WGSL fallback. Use [`init_device_with_api`] to request a graphics
412/// API explicitly, including native MSL on Metal when `msl` is enabled.
413///
414/// # Panics
415///
416/// If configuration is invalid, the setup is incompatible, or registration fails.
417/// Use [`try_init_device`] to handle initialization errors. Register each setup once,
418/// then clone the returned device to share its runtime; repeated handles are not detected.
419pub fn init_device(setup: WgpuSetup, options: RuntimeOptions) -> WgpuDevice {
420    try_init_device(setup, options).expect("Unable to register existing wgpu setup")
421}
422
423/// Import a setup with an explicitly selected graphics API.
424///
425/// With `msl` enabled, importing through [`crate::Metal`] requires native MSL support.
426/// [`AutoGraphicsApi`] permits WGSL fallback, as [`init_device`] does.
427///
428/// Like [`init_device`], each call generates a unique device ID; do not import the same setup twice.
429///
430/// # Panics
431///
432/// If configuration is invalid, the requested API differs from the setup's backend,
433/// the selected compiler is unavailable, or registration fails. Use
434/// [`try_init_device_with_api`] to handle these errors.
435pub fn init_device_with_api<G: GraphicsApi>(
436    setup: WgpuSetup,
437    options: RuntimeOptions,
438) -> WgpuDevice {
439    try_init_device_with_api::<G>(setup, options).expect("Unable to register existing wgpu setup")
440}
441
442/// Like [`init_setup_async`], but synchronous.
443/// On wasm, it is necessary to use [`init_setup_async`] instead.
444///
445/// A device brought up on a `G` other than [`AutoGraphicsApi`] is reached
446/// through the client this initializes, and is not among the devices
447/// [`Runtime::enumerate_devices`] lists: those ids index the auto backend's
448/// adapters, which is what a client created lazily resolves them against.
449///
450/// # Panics
451///
452/// Where `device` pins a graphics API and `G` names another: see
453/// [`init_setup_async`].
454/// With `msl` enabled, an explicitly selected Metal API also panics if native MSL is unavailable.
455pub fn init_setup<G: GraphicsApi>(device: &WgpuDevice, options: RuntimeOptions) -> WgpuSetup {
456    cfg_if::cfg_if! {
457        if #[cfg(target_family = "wasm")] {
458            let _ = (device, options);
459            panic!("Creating a wgpu setup synchronously is unsupported on wasm. Use init_async instead");
460        } else {
461            future::block_on(init_setup_async::<G>(device, options))
462        }
463    }
464}
465
466/// Initialize a client on the given device with the given options.
467/// This function is useful to configure the runtime options
468/// or to pick a different graphics API.
469///
470/// A device pinned to a graphics API comes up on that API: through
471/// [`AutoGraphicsApi`], or through the `G` naming the same one.
472///
473/// # Panics
474///
475/// Where `device` pins a graphics API and `G` names another. The client is
476/// registered under the device's id, and a pinned id promises its API to
477/// every caller who reaches for that client afterwards.
478/// With `msl` enabled, an explicitly selected Metal API also panics if native MSL is unavailable.
479pub async fn init_setup_async<G: GraphicsApi>(
480    device: &WgpuDevice,
481    options: RuntimeOptions,
482) -> WgpuSetup {
483    try_init_setup_async::<G>(device, options)
484        .await
485        .expect("Unable to initialize wgpu setup")
486}
487
488/// Register an existing setup without changing its enabled GPU capabilities.
489///
490/// Uses [`AutoCompiler`] with WGSL fallback. Use [`try_init_device_with_api`] to require
491/// an explicit graphics API and its compiler support. The supplied device must enable
492/// the features required by the selected compiler, including native extensions when
493/// using SPIR-V or MSL. Registration does not
494/// validate every native requirement. Each call allocates a new runtime identity;
495/// repeated registration of the same handles is not detected. Register once, then
496/// clone the returned device.
497pub fn try_init_device(
498    setup: WgpuSetup,
499    options: RuntimeOptions,
500) -> Result<WgpuDevice, WgpuInitError> {
501    try_init_device_with_api::<AutoGraphicsApi>(setup, options)
502}
503
504/// Register an existing setup with an explicitly selected graphics API.
505///
506/// The requested API must match the setup's backend. With `msl` enabled, an explicit
507/// [`crate::Metal`] request requires native MSL support; [`AutoGraphicsApi`] permits
508/// WGSL fallback. Invalid configuration, unavailable compilers, and registration
509/// failures are returned as errors. Register once, then clone the returned device.
510pub fn try_init_device_with_api<G: GraphicsApi>(
511    setup: WgpuSetup,
512    options: RuntimeOptions,
513) -> Result<WgpuDevice, WgpuInitError> {
514    use core::sync::atomic::{AtomicU32, Ordering};
515    static COUNTER: AtomicU32 = AtomicU32::new(0);
516    options.validate()?;
517    let requested_backend = G::backend_for(&WgpuDevice::default());
518    if requested_backend != WgpuBackend::Auto
519        && !backend_candidates(requested_backend).contains(&setup.backend)
520    {
521        return Err(WgpuInitError::InvalidConfiguration {
522            message: format!(
523                "the imported setup uses {:?}, but {requested_backend:?} was requested",
524                setup.backend
525            ),
526        });
527    }
528    if setup.backend != setup.adapter.get_info().backend
529        || setup.backend != setup.device.adapter_info().backend
530    {
531        return Err(WgpuInitError::InvalidConfiguration {
532            message: "setup backend does not match its adapter and device".into(),
533        });
534    }
535    let limits = setup.device.limits();
536    if limits.max_compute_invocations_per_workgroup == 0
537        || limits.max_storage_buffers_per_shader_stage < 2
538    {
539        return Err(WgpuInitError::InvalidConfiguration {
540            message: "compute support and at least two storage bindings are required".into(),
541        });
542    }
543    // External DeviceId indices are u16. Never wrap an ID onto a live runtime.
544    let id = COUNTER
545        .try_update(Ordering::Relaxed, Ordering::Relaxed, |id| {
546            (id <= u16::MAX as u32).then_some(id + 1)
547        })
548        .map_err(|_| WgpuInitError::Registration {
549            message: "external wgpu device IDs exhausted".into(),
550        })?;
551    let device = WgpuDevice::new(WgpuDeviceKind::Existing(id));
552    let server =
553        try_create_server::<AutoCompiler>(setup, options, device.to_id(), requested_backend)?;
554    Client::try_init(device.to_id(), server).map_err(|err| WgpuInitError::Registration {
555        message: format!("{device:?}: {err:?}"),
556    })?;
557    Ok(device)
558}
559
560/// Fallible synchronous initialization. Use [`try_init_setup_async`] on wasm.
561#[cfg(not(target_family = "wasm"))]
562pub fn try_init_setup<G: GraphicsApi>(
563    device: &WgpuDevice,
564    options: RuntimeOptions,
565) -> Result<WgpuSetup, WgpuInitError> {
566    future::block_on(try_init_setup_async::<G>(device, options))
567}
568
569/// Initialize the selected runtime, returning acquisition or registration failures.
570///
571/// An initialized selector cannot be configured again. Registration is atomic in
572/// `CubeCL`'s service registry, but concurrent calls may each acquire a device before
573/// one registers successfully. Failed or cancelled acquisition leaves no registration.
574/// With `msl` enabled, explicitly selecting Metal requires native MSL support;
575/// automatic selection permits WGSL fallback. Compiler validation failures are returned
576/// as [`WgpuInitError::InvalidConfiguration`] before registration.
577pub async fn try_init_setup_async<G: GraphicsApi>(
578    device: &WgpuDevice,
579    options: RuntimeOptions,
580) -> Result<WgpuSetup, WgpuInitError> {
581    options.validate()?;
582    if matches!(device.kind, WgpuDeviceKind::Existing(_)) {
583        return Err(WgpuInitError::InvalidConfiguration {
584            message:
585                "an Existing ID is not an adapter selector; register a setup or reuse its device"
586                    .into(),
587        });
588    }
589    // Preserve Auto until acquisition resolves it, so automatic Metal selection
590    // can fall back to WGSL while an explicit Metal request requires native MSL.
591    let requested_backend = G::backend_for(device);
592    if device.backend != WgpuBackend::Auto && device.backend != requested_backend {
593        return Err(WgpuInitError::InvalidConfiguration {
594            message: format!("{device:?} cannot be initialized on {requested_backend:?}"),
595        });
596    }
597    let mut selected = device.clone();
598    selected.backend = requested_backend;
599    let setup = try_create_setup(&selected).await?;
600    let server = try_create_server::<AutoCompiler>(
601        setup.clone(),
602        options,
603        device.to_id(),
604        requested_backend,
605    )?;
606    Client::try_init(device.to_id(), server).map_err(|err| WgpuInitError::Registration {
607        message: format!("{device:?}: {err:?}"),
608    })?;
609    Ok(setup)
610}
611
612/// The runtime name for `backend`, naming the compiler that serves it.
613///
614/// Read from the same options [`AutoCompiler`] picks its compiler from, so a device that fell
615/// back to WGSL says so.
616fn runtime_name(backend: wgpu::Backend, options: &WgpuCompilationOptions) -> &'static str {
617    match backend {
618        wgpu::Backend::Vulkan if options.supports_vulkan_compiler => "wgpu<spirv>",
619        wgpu::Backend::Metal if options.supports_msl_compiler => "wgpu<msl>",
620        _ => "wgpu<wgsl>",
621    }
622}
623
624fn try_create_server<C: WgpuCompiler>(
625    setup: WgpuSetup,
626    options: RuntimeOptions,
627    device_id: DeviceId,
628    requested_backend: WgpuBackend,
629) -> Result<WgpuServer<C>, WgpuInitError> {
630    let limits = setup.device.limits();
631    let adapter_limits = setup.adapter.limits();
632    let mut adapter_info = setup.adapter.get_info();
633
634    // Workaround: WebGPU reports some "fake" subgroup info atm, as it's not really supported yet.
635    // However, some algorithms do rely on having this information eg. cubecl-reduce uses max subgroup size _even_ when
636    // subgroups aren't used. For now, just override with the maximum range of subgroups possible.
637    if adapter_info.subgroup_min_size == 0 && adapter_info.subgroup_max_size == 0 {
638        // There is in theory nothing limiting the size to go below 8 but in practice 8 is the minimum found anywhere.
639        adapter_info.subgroup_min_size = 8;
640        // This is a hard limit of GPU APIs (subgroup ballot returns 4 * 32 bits).
641        adapter_info.subgroup_max_size = 128;
642    }
643
644    // WebGPU states no capacity. `register_features` fills it in where the
645    // backend's own API states one and this build enabled that backend:
646    // `spirv` for Vulkan, `msl` for Metal.
647    let mem_props = MemoryDeviceProperties::new(
648        limits
649            .max_storage_buffer_binding_size
650            .min(limits.max_buffer_size),
651        limits
652            .min_uniform_buffer_offset_alignment
653            .max(limits.min_storage_buffer_offset_alignment) as u64,
654    );
655    let max_count = adapter_limits.max_compute_workgroups_per_dimension;
656    let hardware_props = HardwareProperties {
657        load_width: 128,
658        vector_register_count: None,
659        // On Apple Silicon, the plane size is 32,
660        // though the minimum and maximum differ.
661        // https://github.com/gpuweb/gpuweb/issues/3950
662        #[cfg(apple_silicon)]
663        plane_size_min: 32,
664        #[cfg(not(apple_silicon))]
665        plane_size_min: adapter_info.subgroup_min_size,
666        #[cfg(apple_silicon)]
667        plane_size_max: 32,
668        #[cfg(not(apple_silicon))]
669        plane_size_max: adapter_info.subgroup_max_size,
670        // wgpu uses an additional buffer for variable-length buffers,
671        // so we have to use one buffer less on our side to make room for that wgpu internal buffer.
672        // See: https://github.com/gfx-rs/wgpu/blob/a9638c8e3ac09ce4f27ac171f8175671e30365fd/wgpu-hal/src/metal/device.rs#L799
673        max_bindings: limits
674            .max_storage_buffers_per_shader_stage
675            .saturating_sub(1),
676        max_shared_memory_size: limits.max_compute_workgroup_storage_size as usize,
677        max_cube_count: (max_count, max_count, max_count),
678        max_units_per_cube: adapter_limits.max_compute_invocations_per_workgroup,
679        max_cube_dim: (
680            adapter_limits.max_compute_workgroup_size_x,
681            adapter_limits.max_compute_workgroup_size_y,
682            adapter_limits.max_compute_workgroup_size_z,
683        ),
684        num_streaming_multiprocessors: None,
685        num_tensor_cores: None,
686        min_tensor_cores_dim: None,
687        num_cpu_cores: None, // TODO: Check if device is CPU.
688        last_level_cache_size: None,
689        max_vector_size: 4,
690        // Init later if extension is enabled
691        cube_mma_reserved_shared_memory: 0,
692    };
693
694    let mut compilation_options = Default::default();
695
696    let features = setup.adapter.features();
697
698    let time_measurement = if features.contains(wgpu::Features::TIMESTAMP_QUERY) {
699        TimingMethod::Device
700    } else {
701        TimingMethod::System
702    };
703
704    // The adapter's vendor/device pair, which is what `WgpuServer` keys its
705    // SPIR-V store on. Reported unconditionally, even in a WGSL-only build that
706    // persists no compiled code: measurement caches (autotune, throughput) are
707    // namespaced by neither vendor nor device, so this string is the only thing
708    // that can tell one adapter's measurements from another's.
709    let fingerprint = format!("spirv_{}_{}", adapter_info.vendor, adapter_info.device);
710
711    let mut device_props = DeviceProperties::new(
712        Default::default(),
713        mem_props,
714        hardware_props,
715        time_measurement,
716        DeviceIdentity {
717            name: adapter_info.name.clone(),
718            fingerprint,
719            physical: backend::physical_device(&setup.adapter, &adapter_info),
720        },
721    );
722
723    #[cfg(not(all(target_os = "macos", feature = "msl")))]
724    {
725        if features.contains(wgpu::Features::SUBGROUP)
726            && setup.adapter.get_info().device_type != wgpu::DeviceType::Cpu
727        {
728            use cubecl_ir::features::Plane;
729
730            device_props.features.plane.insert(Plane::Ops);
731        }
732    }
733
734    backend::register_features(&setup.adapter, &mut device_props, &mut compilation_options);
735
736    #[cfg(any(feature = "spirv", feature = "msl"))]
737    if compilation_options.supports_vulkan_compiler || compilation_options.supports_msl_compiler {
738        device_props
739            .features
740            .plane
741            .insert(cubecl_ir::features::Plane::NonUniformControlFlow);
742    }
743
744    C::validate_runtime(setup.backend, &compilation_options, requested_backend).map_err(|err| {
745        WgpuInitError::InvalidConfiguration {
746            message: format!("failed to initialize the wgpu compiler: {err}"),
747        }
748    })?;
749
750    let logger = alloc::sync::Arc::new(ServerLogger::default());
751    let name = runtime_name(setup.backend, &compilation_options);
752
753    let allocator = ContiguousMemoryLayoutPolicy::new(device_props.memory.alignment as usize);
754    let memory_properties = device_props.memory.clone();
755    let (utilities, captures) = ServerUtilities::init(
756        ServiceId::of::<WgpuServer<C>>(device_id),
757        name,
758        device_props,
759        WgpuRuntime::<C>::target_properties(),
760        logger,
761        allocator,
762    );
763    Ok(WgpuServer::new(
764        memory_properties,
765        options.memory_config,
766        compilation_options,
767        setup.device.clone(),
768        setup.queue,
769        options.tasks_max,
770        setup.backend,
771        time_measurement,
772        utilities,
773        captures,
774    ))
775}
776
777/// Acquire an adapter and device without blocking the caller's executor.
778async fn try_create_setup(device: &WgpuDevice) -> Result<WgpuSetup, WgpuInitError> {
779    // Validate overrides before touching the graphics APIs. They choose hardware, not
780    // the graphics API or the registration key the caller selected.
781    let override_device = match device.kind {
782        WgpuDeviceKind::DefaultDevice => try_device_override()?.map(|kind| WgpuDevice {
783            kind,
784            backend: device.backend,
785        }),
786        _ => None,
787    };
788    let device = override_device.as_ref().unwrap_or(device);
789    match device.kind {
790        WgpuDeviceKind::DiscreteGpu(index)
791        | WgpuDeviceKind::IntegratedGpu(index)
792        | WgpuDeviceKind::VirtualGpu(index)
793        | WgpuDeviceKind::Other(index)
794            if index > WgpuDeviceKind::MAX_INDEX =>
795        {
796            return Err(WgpuInitError::InvalidConfiguration {
797                message: format!(
798                    "adapter index {index} exceeds {}",
799                    WgpuDeviceKind::MAX_INDEX
800                ),
801            });
802        }
803        WgpuDeviceKind::Existing(_) => {
804            return Err(WgpuInitError::InvalidConfiguration {
805                message: "an Existing ID cannot acquire an adapter".into(),
806            });
807        }
808        _ => {}
809    }
810    let candidates = backend_candidates(device.backend);
811    if !candidates
812        .iter()
813        .any(|api| wgpu::Instance::enabled_backend_features().contains((*api).into()))
814    {
815        return Err(WgpuInitError::UnsupportedGraphicsApi {
816            api: device.backend,
817        });
818    }
819    let mut backend = None;
820    #[cfg(not(target_family = "wasm"))]
821    for api in candidates {
822        if !wgpu::Instance::enabled_backend_features().contains(api.into()) {
823            continue;
824        }
825        let adapters = probe(api);
826        if adapters
827            .iter()
828            .any(|adapter| adapter.device_type != wgpu::DeviceType::Cpu)
829        {
830            backend = Some(api);
831            break;
832        }
833        if backend.is_none() && !adapters.is_empty() {
834            backend = Some(api);
835        }
836    }
837    #[cfg(target_family = "wasm")]
838    for api in candidates {
839        if api == wgpu::Backend::BrowserWebGpu
840            && wgpu::Instance::enabled_backend_features().contains(api.into())
841        {
842            backend = Some(api);
843            break;
844        }
845    }
846    let backend = backend.ok_or_else(|| WgpuInitError::AdapterUnavailable {
847        device: device.clone(),
848        message: "no adapter found on the requested graphics API".into(),
849    })?;
850    let (instance, adapter) = request_adapter(device, backend).await?;
851    let (device, queue) = backend::try_request_device(&adapter).await?;
852    Ok(WgpuSetup {
853        instance,
854        adapter,
855        device,
856        queue,
857        backend,
858    })
859}
860
861async fn request_adapter(
862    device: &WgpuDevice,
863    backend: wgpu::Backend,
864) -> Result<(wgpu::Instance, wgpu::Adapter), WgpuInitError> {
865    #[cfg(not(feature = "vulkan-validate"))]
866    let instance_flags = {
867        let debug = ServerLogger::default();
868        // Debug/validation layers cost real per-dispatch CPU time, so only
869        // source-level compilation logging (`full`) opts into them — `basic`
870        // is passive name-only logging and must not change how kernels run.
871        match (debug.profile_level(), debug.compilation_source_activated()) {
872            (Some(ProfileLevel::Full), _) => InstanceFlags::advanced_debugging(),
873            (_, true) => InstanceFlags::debugging(),
874            (_, false) => InstanceFlags::default(),
875        }
876    };
877    #[cfg(feature = "vulkan-validate")]
878    let instance_flags = InstanceFlags::advanced_debugging();
879    log::debug!("{instance_flags:?}");
880    let instance = device_instance(backend, instance_flags);
881
882    let adapter = match device.kind {
883        #[cfg(not(target_family = "wasm"))]
884        WgpuDeviceKind::DiscreteGpu(num) => {
885            select_from_adapter_list(
886                num,
887                "No Discrete GPU device found",
888                &instance,
889                device,
890                backend,
891            )
892            .await
893        }
894        #[cfg(not(target_family = "wasm"))]
895        WgpuDeviceKind::IntegratedGpu(num) => {
896            select_from_adapter_list(
897                num,
898                "No Integrated GPU device found",
899                &instance,
900                device,
901                backend,
902            )
903            .await
904        }
905        #[cfg(not(target_family = "wasm"))]
906        WgpuDeviceKind::VirtualGpu(num) => {
907            select_from_adapter_list(
908                num,
909                "No Virtual GPU device found",
910                &instance,
911                device,
912                backend,
913            )
914            .await
915        }
916        #[cfg(not(target_family = "wasm"))]
917        WgpuDeviceKind::Other(num) => {
918            select_from_adapter_list(num, "No Other device found", &instance, device, backend).await
919        }
920        #[cfg(not(target_family = "wasm"))]
921        WgpuDeviceKind::Cpu => {
922            select_from_adapter_list(0, "No CPU device found", &instance, device, backend).await
923        }
924        #[cfg(target_family = "wasm")]
925        WgpuDeviceKind::IntegratedGpu(_) => {
926            request_adapter_with_preference(&instance, wgpu::PowerPreference::LowPower).await
927        }
928        WgpuDeviceKind::Existing(_) => {
929            unreachable!("Cannot select an adapter for an existing device.")
930        }
931        _ => {
932            request_adapter_with_preference(&instance, wgpu::PowerPreference::HighPerformance).await
933        }
934    }
935    .map_err(|message| WgpuInitError::AdapterUnavailable {
936        device: device.clone(),
937        message,
938    })?;
939
940    Ok((instance, adapter))
941}
942
943/// The instance devices on `backend` come up on, created with the first device's `flags`.
944fn device_instance(backend: wgpu::Backend, flags: InstanceFlags) -> wgpu::Instance {
945    let mut instances = INSTANCES.lock();
946    if let Some((_, instance)) = instances.iter().find(|(api, _)| *api == backend) {
947        return instance.clone();
948    }
949    let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
950        backends: backend.into(),
951        flags,
952        ..wgpu::InstanceDescriptor::new_without_display_handle()
953    });
954    instances.push((backend, instance.clone()));
955    instance
956}
957
958async fn request_adapter_with_preference(
959    instance: &wgpu::Instance,
960    power_preference: wgpu::PowerPreference,
961) -> Result<wgpu::Adapter, String> {
962    instance
963        .request_adapter(&RequestAdapterOptions {
964            power_preference,
965            force_fallback_adapter: false,
966            compatible_surface: None,
967            ..RequestAdapterOptions::default()
968        })
969        .await
970        .map_err(|err| err.to_string())
971}
972
973#[cfg(not(target_family = "wasm"))]
974async fn select_from_adapter_list(
975    num: usize,
976    error: &str,
977    instance: &wgpu::Instance,
978    device: &WgpuDevice,
979    backend: wgpu::Backend,
980) -> Result<wgpu::Adapter, String> {
981    // A kind is what the graphics API reports, and nothing stands in for it:
982    // `OpenGL` calling a GPU `Other` makes it `Other(n)` there, not a discrete
983    // GPU by another name. Anything looser selects adapters `find_device` says
984    // the machine does not have, and two ids end up on one adapter.
985    let adapters = instance.enumerate_adapters(backend.into()).await;
986    let found = adapters
987        .iter()
988        .map(|adapter| adapter.get_info())
989        .collect::<Vec<_>>();
990
991    let is_same_type = |adapter: &wgpu::Adapter| {
992        let device_type = adapter.get_info().device_type;
993
994        match device.kind {
995            WgpuDeviceKind::DiscreteGpu(_) => device_type == wgpu::DeviceType::DiscreteGpu,
996            WgpuDeviceKind::IntegratedGpu(_) => device_type == wgpu::DeviceType::IntegratedGpu,
997            WgpuDeviceKind::VirtualGpu(_) => device_type == wgpu::DeviceType::VirtualGpu,
998            WgpuDeviceKind::Cpu => device_type == wgpu::DeviceType::Cpu,
999            WgpuDeviceKind::Other(_) => device_type == wgpu::DeviceType::Other,
1000            WgpuDeviceKind::DefaultDevice => true,
1001            WgpuDeviceKind::Existing(_) => {
1002                unreachable!("Cannot select an adapter for an existing device.")
1003            }
1004        }
1005    };
1006
1007    adapters
1008        .into_iter()
1009        .filter(is_same_type)
1010        .nth(num)
1011        .ok_or_else(|| format!("{error}, adapters {found:?}"))
1012}
1013
1014fn try_device_override() -> Result<Option<WgpuDeviceKind>, WgpuInitError> {
1015    let value = match std::env::var("CUBECL_WGPU_DEFAULT_DEVICE") {
1016        Ok(value) => value,
1017        Err(std::env::VarError::NotPresent) => return Ok(None),
1018        Err(err) => {
1019            return Err(WgpuInitError::InvalidConfiguration {
1020                message: err.to_string(),
1021            });
1022        }
1023    };
1024    let parsed = if value == "Cpu" {
1025        Some(WgpuDeviceKind::Cpu)
1026    } else {
1027        [
1028            (
1029                "DiscreteGpu(",
1030                WgpuDeviceKind::DiscreteGpu as fn(usize) -> WgpuDeviceKind,
1031            ),
1032            ("IntegratedGpu(", WgpuDeviceKind::IntegratedGpu),
1033            ("VirtualGpu(", WgpuDeviceKind::VirtualGpu),
1034            ("Other(", WgpuDeviceKind::Other),
1035        ]
1036        .into_iter()
1037        .find_map(|(prefix, kind)| {
1038            value
1039                .strip_prefix(prefix)?
1040                .strip_suffix(')')?
1041                .parse()
1042                .ok()
1043                .map(kind)
1044        })
1045    };
1046    parsed
1047        .map(Some)
1048        .ok_or_else(|| WgpuInitError::InvalidConfiguration {
1049            message: format!("Invalid CUBECL_WGPU_DEFAULT_DEVICE: {value}"),
1050        })
1051}
1052
1053#[cfg(all(test, not(target_family = "wasm")))]
1054mod device_tests {
1055    use super::*;
1056
1057    #[test]
1058    #[ignore = "requires a native Vulkan adapter (software adapters are supported)"]
1059    fn fallible_import_preserves_api_selection_and_unique_ids() {
1060        let selector = WgpuDevice::default().on(WgpuBackend::Vulkan);
1061        let setup = future::block_on(try_create_setup(&selector)).unwrap();
1062
1063        assert!(matches!(
1064            try_init_device_with_api::<crate::Metal>(setup.clone(), RuntimeOptions::default()),
1065            Err(WgpuInitError::InvalidConfiguration { .. })
1066        ));
1067
1068        let automatic = try_init_device(setup, RuntimeOptions::default()).unwrap();
1069        let setup = future::block_on(try_create_setup(&selector)).unwrap();
1070        let explicit = init_device_with_api::<crate::Vulkan>(setup, RuntimeOptions::default());
1071        assert_ne!(automatic.to_id(), explicit.to_id());
1072    }
1073
1074    #[cfg(feature = "msl")]
1075    #[test]
1076    #[ignore = "requires a native Vulkan adapter (software adapters are supported)"]
1077    fn compiler_validation_returns_an_initialization_error() {
1078        let selector = WgpuDevice::default().on(WgpuBackend::Vulkan);
1079        let setup = future::block_on(try_create_setup(&selector)).unwrap();
1080
1081        // An explicit MSL compiler still rejects Vulkan, through a recoverable error.
1082        assert!(matches!(
1083            try_create_server::<crate::MslCompiler>(
1084                setup.clone(),
1085                RuntimeOptions::default(),
1086                selector.to_id(),
1087                WgpuBackend::Auto,
1088            ),
1089            Err(WgpuInitError::InvalidConfiguration { message })
1090                if message.contains("MslCompiler") && message.contains("Metal")
1091        ));
1092
1093        // AutoCompiler must also receive the original API request for validation.
1094        assert!(matches!(
1095            try_create_server::<AutoCompiler>(
1096                setup,
1097                RuntimeOptions::default(),
1098                selector.to_id(),
1099                WgpuBackend::Metal,
1100            ),
1101            Err(WgpuInitError::InvalidConfiguration { .. })
1102        ));
1103
1104        // Failed validation left no registration behind; a supported request succeeds.
1105        let setup =
1106            try_init_setup::<AutoGraphicsApi>(&selector, RuntimeOptions::default()).unwrap();
1107        assert_eq!(setup.backend, wgpu::Backend::Vulkan);
1108        assert!(matches!(
1109            try_init_setup::<AutoGraphicsApi>(&selector, RuntimeOptions::default()),
1110            Err(WgpuInitError::Registration { .. })
1111        ));
1112    }
1113
1114    const PINNED: [WgpuBackend; 4] = [
1115        WgpuBackend::Vulkan,
1116        WgpuBackend::Metal,
1117        WgpuBackend::Dx12,
1118        WgpuBackend::Gl,
1119    ];
1120
1121    /// One adapter is one device. Listing it again for every graphics API
1122    /// that reaches it makes a single GPU look like several, and whatever
1123    /// counts devices — a collective, a transfer between two of them — runs
1124    /// on hardware that is not there.
1125    #[test]
1126    fn each_adapter_is_listed_once() {
1127        let ids = <WgpuRuntime>::enumerate_all_devices();
1128
1129        let adapters = settle(WgpuBackend::Auto).map_or(0, |(_, adapters)| adapters.len());
1130
1131        assert_eq!(ids.len(), adapters);
1132        for id in ids {
1133            assert_eq!(WgpuDevice::from_id(id).backend, WgpuBackend::Auto);
1134        }
1135    }
1136
1137    /// A device pinned to an API is found where that API has it, whatever
1138    /// the API `Auto` settles on has — the kinds differ from one API to the
1139    /// next, `OpenGL` calling a GPU what Vulkan calls discrete.
1140    #[test]
1141    fn a_device_is_found_on_the_api_it_names() {
1142        for backend in PINNED {
1143            let reachable = adapters_on(backend);
1144
1145            for id in reachable.iter() {
1146                assert_eq!(
1147                    <WgpuRuntime>::find_device(*id),
1148                    Ok(()),
1149                    "{id} on {backend:?}"
1150                );
1151            }
1152
1153            let default = WgpuDevice::new(WgpuDeviceKind::DefaultDevice).on(backend);
1154            assert_eq!(
1155                <WgpuRuntime>::find_device(default.to_id()).is_ok(),
1156                !reachable.is_empty(),
1157                "the default device on {backend:?}"
1158            );
1159        }
1160    }
1161
1162    /// The default device stands for whichever adapter it lands on, so it is
1163    /// its own only peer: the adapters listed beside it would give one of them
1164    /// a second client, and leaving it out drops the caller from its own list.
1165    #[test]
1166    fn the_default_device_is_its_own_only_peer() {
1167        for backend in PINNED.into_iter().chain([WgpuBackend::Auto]) {
1168            let default = WgpuDevice::new(WgpuDeviceKind::DefaultDevice)
1169                .on(backend)
1170                .to_id();
1171
1172            let expected = match adapters_on(backend).is_empty() {
1173                true => Vec::new(),
1174                false => alloc::vec![default],
1175            };
1176
1177            assert_eq!(<WgpuRuntime>::enumerate_devices_like(default), expected);
1178        }
1179    }
1180
1181    /// Setting a pinned device up through `AutoGraphicsApi` keeps its pin —
1182    /// the natural call, `init_setup` being the only way to pass options.
1183    #[test]
1184    fn auto_defers_to_the_api_a_device_pins() {
1185        for (backend, api) in [
1186            (WgpuBackend::Vulkan, wgpu::Backend::Vulkan),
1187            (WgpuBackend::Metal, wgpu::Backend::Metal),
1188            (WgpuBackend::Dx12, wgpu::Backend::Dx12),
1189            (WgpuBackend::Gl, wgpu::Backend::Gl),
1190        ] {
1191            let device = WgpuDevice::new(WgpuDeviceKind::DefaultDevice).on(backend);
1192
1193            assert_eq!(AutoGraphicsApi::backend_for(&device), backend);
1194            assert_eq!(resolve_backend(AutoGraphicsApi::backend_for(&device)), api);
1195        }
1196    }
1197
1198    /// Naming one API for a device pinned to another is refused before any
1199    /// adapter is asked for: the client lands under the pinned id, and would
1200    /// hand every later caller the wrong API.
1201    #[test]
1202    fn a_setup_on_another_api_than_the_pinned_one_is_refused() {
1203        let device = WgpuDevice::new(WgpuDeviceKind::DefaultDevice).on(WgpuBackend::Gl);
1204
1205        assert!(matches!(
1206            try_init_setup::<crate::Vulkan>(&device, RuntimeOptions::default()),
1207            Err(WgpuInitError::InvalidConfiguration { .. })
1208        ));
1209    }
1210
1211    /// A pinned device's peers are those of its own API. The same adapters on
1212    /// `Auto` are other devices, with other clients.
1213    #[test]
1214    fn a_pinned_device_is_enumerated_with_its_own_api() {
1215        for backend in PINNED {
1216            for id in adapters_on(backend) {
1217                let peers = <WgpuRuntime>::enumerate_devices_like(id);
1218
1219                assert!(peers.contains(&id), "{id} among {peers:?}");
1220                for peer in peers {
1221                    assert_eq!(WgpuDevice::from_id(peer).backend, backend);
1222                }
1223            }
1224        }
1225    }
1226
1227    /// And one that API does not have is a miss, reported against what it
1228    /// has of that kind.
1229    #[test]
1230    fn an_index_past_the_end_is_not_found_on_any_api() {
1231        for backend in PINNED.into_iter().chain([WgpuBackend::Auto]) {
1232            let device = WgpuDevice::new(WgpuDeviceKind::DiscreteGpu(4242)).on(backend);
1233
1234            let discrete = adapters_on(backend)
1235                .into_iter()
1236                .filter(|id| id.type_id == device.to_id().type_id)
1237                .count();
1238
1239            assert_eq!(<WgpuRuntime>::find_device(device.to_id()), Err(discrete));
1240        }
1241    }
1242}