cubecl_common/device/handle/
base.rs1use crate::device::{DeviceId, DeviceService, ServerUtilitiesHandle};
2use alloc::boxed::Box;
3use alloc::string::String;
4use core::any::Any;
5#[cfg(feature = "std")]
6use cubecl_environment::future::channel::oneshot;
7
8pub struct CallError {
13 failure: Failure,
14}
15
16enum Failure {
18 Panicked(Box<dyn Any + Send>),
20 Closed,
23 Disconnected,
25}
26
27impl CallError {
28 #[cfg_attr(not(multi_threading), allow(dead_code))]
32 pub(crate) fn from_panic(payload: Box<dyn Any + Send>) -> Self {
33 Self {
34 failure: Failure::Panicked(payload),
35 }
36 }
37
38 #[cfg_attr(not(multi_threading), allow(dead_code))]
41 pub(crate) fn disconnected() -> Self {
42 Self {
43 failure: Failure::Disconnected,
44 }
45 }
46
47 #[cfg_attr(not(multi_threading), allow(dead_code))]
49 pub(crate) fn closed() -> Self {
50 Self {
51 failure: Failure::Closed,
52 }
53 }
54
55 pub fn is_closed(&self) -> bool {
58 matches!(self.failure, Failure::Closed)
59 }
60
61 pub fn message(&self) -> Option<&str> {
64 let Failure::Panicked(payload) = &self.failure else {
65 return None;
66 };
67 if let Some(s) = payload.downcast_ref::<&'static str>() {
68 Some(*s)
69 } else if let Some(s) = payload.downcast_ref::<String>() {
70 Some(s.as_str())
71 } else {
72 None
73 }
74 }
75
76 pub fn into_panic(self) -> Option<Box<dyn Any + Send>> {
81 match self.failure {
82 Failure::Panicked(payload) => Some(payload),
83 Failure::Closed | Failure::Disconnected => None,
84 }
85 }
86
87 #[track_caller]
89 pub fn resume(self) -> ! {
90 match self.failure {
91 #[cfg(feature = "std")]
92 Failure::Panicked(payload) => std::panic::resume_unwind(payload),
93 #[cfg(not(feature = "std"))]
95 Failure::Panicked(_payload) => {
96 panic!("a device task panicked but its payload cannot be re-raised without `std`")
97 }
98 Failure::Closed => panic!("the device service was closed, as the process exits"),
99 Failure::Disconnected => {
100 panic!("device runner channel disconnected before producing a result")
101 }
102 }
103 }
104}
105
106pub trait CallResultExt<R> {
108 fn unwrap_or_resume(self) -> R;
113}
114
115impl<R> CallResultExt<R> for Result<R, CallError> {
116 #[track_caller]
117 fn unwrap_or_resume(self) -> R {
118 match self {
119 Ok(value) => value,
120 Err(err) => err.resume(),
121 }
122 }
123}
124
125#[must_use = "the call's result is lost unless it is joined"]
127pub struct SpawnedCall<R> {
128 state: SpawnedState<R>,
129}
130
131enum SpawnedState<R> {
132 Done(Result<R, CallError>),
134 #[cfg(feature = "std")]
135 Running(oneshot::Receiver<Result<R, CallError>>),
136}
137
138impl<R> SpawnedCall<R> {
139 pub(crate) fn done(result: Result<R, CallError>) -> Self {
140 Self {
141 state: SpawnedState::Done(result),
142 }
143 }
144
145 #[cfg(feature = "std")]
146 pub(crate) fn running(receiver: oneshot::Receiver<Result<R, CallError>>) -> Self {
147 Self {
148 state: SpawnedState::Running(receiver),
149 }
150 }
151
152 pub fn join(self) -> Result<R, CallError> {
155 match self.state {
156 SpawnedState::Done(result) => result,
157 #[cfg(feature = "std")]
158 SpawnedState::Running(receiver) => receiver
159 .recv()
160 .unwrap_or_else(|_| Err(CallError::disconnected())),
161 }
162 }
163}
164
165impl core::fmt::Debug for CallError {
166 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
167 match (&self.failure, self.message()) {
168 (_, Some(message)) => write!(
169 f,
170 "CallError(task panicked on device runner thread: {message})"
171 ),
172 (Failure::Panicked(_), None) => f.write_str(
173 "CallError(task panicked on device runner thread with a non-string payload)",
174 ),
175 (Failure::Closed, None) => {
176 f.write_str("CallError(the device service was closed, as the process exits)")
177 }
178 (Failure::Disconnected, None) => f.write_str(
179 "CallError(device runner channel disconnected before producing a result)",
180 ),
181 }
182 }
183}
184
185#[derive(new, Clone, Debug)]
186pub struct ServiceCreationError {
188 #[allow(dead_code)] reason: alloc::string::String,
190}
191
192#[derive(new, Clone, Debug)]
193pub struct ServiceCloseError {
195 #[allow(dead_code)] reason: alloc::string::String,
197}
198
199pub trait DeviceHandleSpec: Sized + Clone {
204 const BLOCKING: bool;
206
207 fn insert<S: DeviceService>(
211 device_id: DeviceId,
212 service: S,
213 ) -> Result<Self, ServiceCreationError>;
214
215 fn new<S: DeviceService>(device_id: DeviceId) -> Self;
219
220 fn device_id(&self) -> DeviceId;
222
223 fn utilities(&self) -> ServerUtilitiesHandle;
225
226 fn flush_queue(&self);
233
234 fn submit_blocking<'a, R: Send, T: FnOnce(&mut dyn Any) -> R + Send + 'a>(
235 &self,
236 task: T,
237 ) -> Result<R, CallError>;
238
239 fn submit<T: FnOnce(&mut dyn Any) + Send + 'static>(&self, task: T);
240
241 fn spawn<R: Send + 'static, T: FnOnce(&mut dyn Any) -> R + Send + 'static>(
245 &self,
246 task: T,
247 ) -> SpawnedCall<R>;
248
249 fn exclusive<R: Send, T: FnOnce() -> R + Send>(&self, task: T) -> Result<R, CallError>;
250
251 fn shutdown(device_id: DeviceId) {
252 let _ = device_id;
253 }
254
255 fn close<S: DeviceService>(device_id: DeviceId) -> Result<(), ServiceCloseError> {
261 let _ = device_id;
262 Ok(())
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::CallError;
269 use alloc::boxed::Box;
270 use core::any::Any;
271
272 #[test]
275 fn test_disconnected_has_no_message_or_payload() {
276 let err = CallError::disconnected();
277 assert_eq!(err.message(), None);
278 assert!(err.into_panic().is_none());
279 }
280
281 #[test]
284 fn test_closed_is_neither_a_panic_nor_a_disconnect() {
285 let err = CallError::closed();
286 assert!(err.is_closed());
287 assert!(!CallError::disconnected().is_closed());
288 assert!(alloc::format!("{err:?}").contains("closed"));
289 assert!(err.into_panic().is_none());
290 }
291
292 #[test]
294 fn test_debug_string_payload_includes_message() {
295 let err = CallError::from_panic(Box::new("oops") as Box<dyn Any + Send>);
296 let debug = alloc::format!("{err:?}");
297 assert!(debug.contains("task panicked on device runner thread"));
298 assert!(debug.contains("oops"));
299 }
300
301 #[test]
303 fn test_debug_non_string_payload() {
304 let err = CallError::from_panic(Box::new(123u8) as Box<dyn Any + Send>);
305 let debug = alloc::format!("{err:?}");
306 assert!(debug.contains("non-string payload"), "got: {debug}");
307 }
308
309 #[test]
311 fn test_debug_disconnected() {
312 let err = CallError::disconnected();
313 let debug = alloc::format!("{err:?}");
314 assert!(debug.contains("disconnected"), "got: {debug}");
315 }
316}