Skip to main content

cubecl_common/device/handle/
mod.rs

1mod base;
2
3pub use base::*;
4
5use crate::device::{DeviceId, DeviceService, ServerUtilitiesHandle, ServiceId};
6use core::any::Any;
7
8#[cfg(feature = "std")]
9#[allow(dead_code)]
10mod channel;
11
12#[allow(dead_code)]
13mod mutex;
14
15#[cfg(feature = "std")]
16#[allow(dead_code)]
17mod reentrant;
18
19#[cfg(all(feature = "std", multi_threading))]
20type Inner = channel::ChannelDeviceHandle;
21// type Inner = mutex::MutexDeviceHandle;
22#[cfg(all(feature = "std", not(multi_threading)))]
23type Inner = reentrant::ReentrantMutexDeviceHandle;
24#[cfg(all(not(feature = "std"), not(multi_threading)))]
25type Inner = mutex::MutexDeviceHandle;
26
27/// A handle to one service, reached as `S`.
28///
29/// `S` is the concrete service for a handle built with [`insert`](Self::insert)
30/// or [`new`](Self::new), and a trait object for one built with
31/// [`seen_as`](Self::seen_as). Either way the service lives where the
32/// transport `I` put it; the handle only knows how to see it as `S`.
33pub struct DeviceHandle<S: ?Sized, I: DeviceHandleSpec = Inner> {
34    handle: I,
35    service: ServiceId,
36    cast: fn(&mut dyn Any) -> &mut S,
37}
38
39impl<S: ?Sized, I: DeviceHandleSpec> Clone for DeviceHandle<S, I> {
40    fn clone(&self) -> Self {
41        Self {
42            handle: self.handle.clone(),
43            service: self.service,
44            cast: self.cast,
45        }
46    }
47}
48
49/// The state the transport holds is `S`, or the registry is broken.
50fn downcast<S: 'static>(state: &mut dyn Any) -> &mut S {
51    state
52        .downcast_mut::<S>()
53        .expect("State type mismatch in the device registry")
54}
55
56#[allow(missing_docs)]
57impl<S: DeviceService, I: DeviceHandleSpec> DeviceHandle<S, I> {
58    pub fn insert(device_id: DeviceId, service: S) -> Result<Self, ServiceCreationError> {
59        Ok(Self {
60            handle: I::insert::<S>(device_id, service)?,
61            service: ServiceId::of::<S>(device_id),
62            cast: downcast::<S>,
63        })
64    }
65
66    pub fn new(device_id: DeviceId) -> Self {
67        Self {
68            handle: I::new::<S>(device_id),
69            service: ServiceId::of::<S>(device_id),
70            cast: downcast::<S>,
71        }
72    }
73}
74
75#[allow(missing_docs)]
76impl<S: ?Sized + 'static, I: DeviceHandleSpec> DeviceHandle<S, I> {
77    pub const fn is_blocking() -> bool {
78        I::BLOCKING
79    }
80
81    /// The same service, seen as `T`: `cast` turns the state this handle
82    /// already reaches into a `T`, once per task, on the thread that runs it.
83    pub fn seen_as<T: ?Sized>(self, cast: fn(&mut dyn Any) -> &mut T) -> DeviceHandle<T, I> {
84        DeviceHandle {
85            handle: self.handle,
86            service: self.service,
87            cast,
88        }
89    }
90
91    pub fn device_id(&self) -> DeviceId {
92        self.handle.device_id()
93    }
94
95    /// The service this handle reaches: its device and its concrete type,
96    /// whatever it is seen as.
97    pub fn service_id(&self) -> ServiceId {
98        self.service
99    }
100
101    pub fn utilities(&self) -> ServerUtilitiesHandle {
102        self.handle.utilities()
103    }
104
105    pub fn submit_blocking<'a, R: Send, T: FnOnce(&mut S) -> R + Send + 'a>(
106        &self,
107        task: T,
108    ) -> Result<R, CallError> {
109        let cast = self.cast;
110        self.handle.submit_blocking(move |state| task(cast(state)))
111    }
112
113    pub fn submit<T: FnOnce(&mut S) + Send + 'static>(&self, task: T) {
114        let cast = self.cast;
115        self.handle.submit(move |state| task(cast(state)))
116    }
117
118    pub fn flush_queue(&self) {
119        self.handle.flush_queue();
120    }
121
122    pub fn exclusive<R: Send, T: FnOnce() -> R + Send>(&self, task: T) -> Result<R, CallError> {
123        self.handle.exclusive(task)
124    }
125
126    /// Stops the background runner threads for `device_id`, blocking until they
127    /// exit. Queued tasks run before the threads stop. Live handles keep their
128    /// runner alive, so all handles for the device should be dropped first.
129    ///
130    /// Only meaningful for handle implementations with background threads; a
131    /// no-op otherwise.
132    ///
133    /// # Scope
134    ///
135    /// **This is device-wide, and `S` is ignored.** It shuts down every
136    /// [`DeviceService`] registered on `device_id`, across both service stages, not
137    /// only `S`. The type parameter selects the handle implementation to dispatch
138    /// through, nothing more.
139    ///
140    /// [`DeviceId`] is not unique across runtimes either: `type_id` is assigned per
141    /// runtime, so distinct backends can hand out the same id. Shutting down a
142    /// device from one runtime can therefore tear down another runtime's services
143    /// on the colliding id, and block while that runtime's handles are still live.
144    pub fn shutdown(device_id: DeviceId) {
145        I::shutdown(device_id)
146    }
147}
148
149/// Shuts a device's runner down when dropped. Use [`DeviceFixture`] rather than
150/// this directly: the guard alone still requires getting the device id and the
151/// declaration order right.
152#[cfg(test)]
153struct ShutdownGuard {
154    device_id: DeviceId,
155    shutdown: fn(DeviceId),
156}
157
158#[cfg(test)]
159impl Drop for ShutdownGuard {
160    fn drop(&mut self) {
161        (self.shutdown)(self.device_id);
162    }
163}
164
165/// Hands out a device id no other test is using.
166///
167/// The channel implementation keys global registries by device id, and the whole
168/// crate's tests share one binary, so a hardcoded id collides with whatever test
169/// happens to run alongside: one test would shut down another's live runner.
170#[cfg(test)]
171fn next_test_device_id() -> DeviceId {
172    use core::sync::atomic::{AtomicU16, Ordering};
173
174    static NEXT: AtomicU16 = AtomicU16::new(0);
175
176    DeviceId {
177        type_id: 0,
178        index_id: NEXT.fetch_add(1, Ordering::Relaxed),
179    }
180}
181
182/// A handle on a device of its own, whose runner is shut down when the fixture drops.
183///
184/// This is the only correct way to spell the pattern, so it is the only one tests
185/// should use. The three hazards are all handled structurally: the device id comes
186/// from [`next_test_device_id`] so it cannot collide, the guard is created with it
187/// so it cannot be forgotten, and `handle` is declared before `_guard` so it drops
188/// first, meaning the shutdown never waits on a handle the fixture itself owns.
189///
190/// Handles a test creates on top of this one (a second service on the same device,
191/// clones) must be locals declared *after* the fixture, which drop in reverse order
192/// and so are gone before the shutdown runs.
193#[cfg(test)]
194pub(crate) struct DeviceFixture<H> {
195    handle: H,
196    _guard: ShutdownGuard,
197    device_id: DeviceId,
198}
199
200#[cfg(test)]
201impl<H> DeviceFixture<H> {
202    pub(crate) fn new(build: fn(DeviceId) -> H, shutdown: fn(DeviceId)) -> Self {
203        let device_id = next_test_device_id();
204
205        Self {
206            handle: build(device_id),
207            _guard: ShutdownGuard {
208                device_id,
209                shutdown,
210            },
211            device_id,
212        }
213    }
214
215    pub(crate) fn device_id(&self) -> DeviceId {
216        self.device_id
217    }
218}
219
220#[cfg(test)]
221impl<H> core::ops::Deref for DeviceFixture<H> {
222    type Target = H;
223
224    fn deref(&self) -> &Self::Target {
225        &self.handle
226    }
227}
228
229#[cfg(test)]
230mod tests_channel {
231    type DeviceHandle<S> = super::DeviceHandle<S, channel::ChannelDeviceHandle>;
232
233    include!("./tests.rs");
234    include!("./tests_recursive.rs");
235}
236
237#[cfg(test)]
238mod tests_mutex {
239    type DeviceHandle<S> = super::DeviceHandle<S, mutex::MutexDeviceHandle>;
240
241    include!("./tests.rs");
242}
243
244#[cfg(test)]
245mod tests_reentrant {
246    type DeviceHandle<S> = super::DeviceHandle<S, reentrant::ReentrantMutexDeviceHandle>;
247
248    include!("./tests.rs");
249    include!("./tests_recursive.rs");
250}