Skip to main content

tenferro_gpu/webgpu/
runtime.rs

1use std::fmt;
2
3use cubecl::client::ComputeClient;
4use cubecl::Runtime;
5use cubecl_common::future;
6use cubecl_wgpu::{WgpuDevice, WgpuRuntime};
7use tenferro_tensor::AllocationDomainId;
8
9use super::apple::AppleDomainState;
10
11/// Returns `true` if a WebGPU adapter is available for CubeCL.
12///
13/// Use this in test helpers to skip WebGPU tests on machines without an
14/// adapter.
15pub fn webgpu_available() -> bool {
16    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
17        let device = WgpuDevice::DefaultDevice;
18        let _ = WgpuRuntime::client(&device);
19    }))
20    .is_ok()
21}
22
23/// Opaque identity of one exact WebGPU runtime instance.
24///
25/// The identity follows the executable queue/client resources, including
26/// Apple-backed host-visible Metal runtimes. Cloning preserves identity;
27/// independently initialized runtimes are distinct even for the same device
28/// ordinal. The token intentionally carries no provider or device identifier.
29#[derive(Clone, Debug)]
30pub struct WebGpuRuntimeIdentity {
31    marker: std::sync::Arc<()>,
32}
33
34impl WebGpuRuntimeIdentity {
35    fn fresh() -> Self {
36        Self {
37            marker: std::sync::Arc::new(()),
38        }
39    }
40}
41
42impl PartialEq for WebGpuRuntimeIdentity {
43    fn eq(&self, other: &Self) -> bool {
44        std::sync::Arc::ptr_eq(&self.marker, &other.marker)
45    }
46}
47
48impl Eq for WebGpuRuntimeIdentity {}
49
50/// CubeCL WebGPU runtime wrapper.
51///
52/// # Examples
53///
54/// ```
55/// use tenferro_gpu::webgpu::WebGpuRuntime;
56///
57/// let _ctor: fn(usize) -> tenferro_tensor::Result<WebGpuRuntime> = WebGpuRuntime::new;
58/// let _sync: fn(&WebGpuRuntime) -> tenferro_tensor::Result<()> =
59///     WebGpuRuntime::synchronize;
60/// ```
61#[derive(Clone)]
62pub struct WebGpuRuntime {
63    client: ComputeClient<WgpuRuntime>,
64    device_ordinal: usize,
65    pub(super) apple_domain: Option<std::sync::Arc<AppleDomainState>>,
66    identity: WebGpuRuntimeIdentity,
67    allocation_domain: AllocationDomainId,
68}
69
70impl fmt::Debug for WebGpuRuntime {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        f.debug_struct("WebGpuRuntime")
73            .field("device_ordinal", &self.device_ordinal)
74            .finish_non_exhaustive()
75    }
76}
77
78impl WebGpuRuntime {
79    /// Initialize the CubeCL WebGPU runtime for a discrete GPU ordinal.
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// use tenferro_gpu::webgpu::WebGpuRuntime;
85    ///
86    /// let _ctor: fn(usize) -> tenferro_tensor::Result<WebGpuRuntime> = WebGpuRuntime::new;
87    /// ```
88    ///
89    /// # Errors
90    ///
91    /// Returns [`crate::Error::RuntimeState`] when the requested adapter is
92    /// unavailable or CubeCL initialization panics while selecting it.
93    pub fn new(device_ordinal: usize) -> crate::Result<Self> {
94        Self::from_device(WgpuDevice::DiscreteGpu(device_ordinal), device_ordinal)
95    }
96
97    /// Initialize the CubeCL WebGPU runtime using CubeCL's default adapter selection.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use tenferro_gpu::webgpu::WebGpuRuntime;
103    ///
104    /// let _ctor: fn() -> tenferro_tensor::Result<WebGpuRuntime> = WebGpuRuntime::new_default;
105    /// ```
106    ///
107    /// # Errors
108    ///
109    /// Returns [`crate::Error::RuntimeState`] when default adapter selection is
110    /// unavailable or CubeCL initialization panics.
111    pub fn new_default() -> crate::Result<Self> {
112        Self::from_device(WgpuDevice::DefaultDevice, 0)
113    }
114
115    fn from_device(device: WgpuDevice, device_ordinal: usize) -> crate::Result<Self> {
116        let client = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
117            WgpuRuntime::client(&device)
118        }))
119        .map_err(|payload| {
120            crate::Error::runtime_state(
121                "webgpu_runtime_init",
122                format!("failed to initialize CubeCL WebGPU runtime: {payload:?}"),
123            )
124        })?;
125        Ok(Self {
126            client,
127            device_ordinal,
128            apple_domain: None,
129            identity: WebGpuRuntimeIdentity::fresh(),
130            allocation_domain: AllocationDomainId::fresh(),
131        })
132    }
133
134    pub(super) fn from_apple_client(
135        client: ComputeClient<WgpuRuntime>,
136        device_ordinal: usize,
137        domain: std::sync::Arc<AppleDomainState>,
138    ) -> Self {
139        let allocation_domain = domain.id;
140        Self {
141            client,
142            device_ordinal,
143            apple_domain: Some(domain),
144            identity: WebGpuRuntimeIdentity::fresh(),
145            allocation_domain,
146        }
147    }
148
149    pub(super) fn allocation_domain(&self) -> Option<&std::sync::Arc<AppleDomainState>> {
150        self.apple_domain.as_ref()
151    }
152
153    pub(super) fn allocation_domain_id(&self) -> AllocationDomainId {
154        self.allocation_domain
155    }
156
157    pub(super) fn record_upload(&self, bytes: usize) {
158        if let Some(domain) = &self.apple_domain {
159            domain.record_upload(bytes);
160        }
161    }
162
163    pub(super) fn record_download(&self, bytes: usize) {
164        if let Some(domain) = &self.apple_domain {
165            domain.record_download(bytes);
166        }
167    }
168
169    pub(crate) fn client(&self) -> &ComputeClient<WgpuRuntime> {
170        &self.client
171    }
172
173    /// Return the WebGPU device ordinal requested at construction.
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// use tenferro_gpu::webgpu::WebGpuRuntime;
179    ///
180    /// let _device_ordinal: fn(&WebGpuRuntime) -> usize = WebGpuRuntime::device_ordinal;
181    /// ```
182    pub fn device_ordinal(&self) -> usize {
183        self.device_ordinal
184    }
185
186    /// Return the opaque identity of this exact executable runtime instance.
187    ///
188    /// # Examples
189    ///
190    /// ```
191    /// use tenferro_gpu::webgpu::WebGpuRuntime;
192    ///
193    /// let _identity: fn(&WebGpuRuntime) -> tenferro_gpu::webgpu::WebGpuRuntimeIdentity =
194    ///     WebGpuRuntime::runtime_identity;
195    /// ```
196    pub fn runtime_identity(&self) -> WebGpuRuntimeIdentity {
197        self.identity.clone()
198    }
199
200    /// Block the current thread until work submitted to the WebGPU queue completes.
201    ///
202    /// # Examples
203    ///
204    /// ```
205    /// use tenferro_gpu::webgpu::WebGpuRuntime;
206    ///
207    /// let _sync: fn(&WebGpuRuntime) -> tenferro_tensor::Result<()> =
208    ///     WebGpuRuntime::synchronize;
209    /// ```
210    ///
211    /// # Errors
212    ///
213    /// Returns [`crate::Error::BackendSource`] when CubeCL queue synchronization fails.
214    pub fn synchronize(&self) -> crate::Result<()> {
215        const OP: &str = "webgpu_runtime_synchronize";
216        future::block_on(self.client.sync()).map_err(|err| crate::Error::backend_source(OP, err))
217    }
218}
219
220#[cfg(test)]
221mod identity_tests {
222    use super::{webgpu_available, WebGpuRuntimeIdentity};
223    use crate::webgpu::WebGpuBackend;
224
225    #[test]
226    fn webgpu_runtime_identity_is_clone_stable_and_instance_scoped() {
227        let first = WebGpuRuntimeIdentity::fresh();
228        let clone = first.clone();
229        let independent = WebGpuRuntimeIdentity::fresh();
230
231        assert_eq!(first, clone);
232        assert_ne!(first, independent);
233    }
234
235    #[test]
236    fn webgpu_backend_identity_tracks_the_exact_runtime_when_hardware_is_available() {
237        if !webgpu_available() {
238            return;
239        }
240
241        let first = WebGpuBackend::new_default().expect("WebGPU backend should initialize");
242        let clone = first.clone();
243        let independent =
244            WebGpuBackend::new_default().expect("second WebGPU backend should initialize");
245
246        assert_eq!(first.runtime_identity(), clone.runtime_identity());
247        assert_ne!(first.runtime_identity(), independent.runtime_identity());
248        assert_eq!(
249            first.runtime_identity(),
250            WebGpuBackend::from_runtime(first.runtime().clone()).runtime_identity()
251        );
252    }
253}