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