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