Skip to main content

cubecl_common/device/handle/
mod.rs

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