Skip to main content

cubecl_common/device/handle/
base.rs

1use crate::device::{DeviceId, DeviceService, ServerUtilitiesHandle};
2use alloc::boxed::Box;
3use alloc::string::String;
4use core::any::Any;
5
6/// An error returned by a blocking device call.
7///
8/// When a task panics on a device's runner thread, the panic payload is
9/// captured and carried here instead of being logged and dropped.
10/// A `CallError` without a payload means the runner channel disconnected
11/// before the task could produce a result.
12pub struct CallError {
13    /// The captured panic payload, or `None` if the call failed because the
14    /// runner channel disconnected
15    payload: Option<Box<dyn Any + Send>>,
16}
17
18impl CallError {
19    // Only the `channel` backend (active when `multi_threading` is set) calls these
20    // constructors.
21    /// Builds an error from a panic payload captured by `catch_unwind`.
22    #[cfg_attr(not(multi_threading), allow(dead_code))]
23    pub(crate) fn from_panic(payload: Box<dyn Any + Send>) -> Self {
24        Self {
25            payload: Some(payload),
26        }
27    }
28
29    /// Builds an error for a runner channel that disconnected before producing
30    /// a result (no panic payload available).
31    #[cfg_attr(not(multi_threading), allow(dead_code))]
32    pub(crate) fn disconnected() -> Self {
33        Self { payload: None }
34    }
35
36    /// Returns the panic message if the failure was a panic whose payload is a
37    /// string (the common case for `panic!`, indexing, `unwrap`, ...).
38    pub fn message(&self) -> Option<&str> {
39        let payload = self.payload.as_ref()?;
40        if let Some(s) = payload.downcast_ref::<&'static str>() {
41            Some(*s)
42        } else if let Some(s) = payload.downcast_ref::<String>() {
43            Some(s.as_str())
44        } else {
45            None
46        }
47    }
48
49    /// Consumes the error and returns the captured panic payload, if any.
50    ///
51    /// The payload can be handed to [`std::panic::resume_unwind`] to re-raise
52    /// the original panic on the caller's thread.
53    pub fn into_panic(self) -> Option<Box<dyn Any + Send>> {
54        self.payload
55    }
56
57    /// Re-raises the failure on the current thread, diverging.
58    #[track_caller]
59    pub fn resume(self) -> ! {
60        match self.into_panic() {
61            #[cfg(feature = "std")]
62            Some(payload) => std::panic::resume_unwind(payload),
63            // std::panic::resume_unwind doesn't exist in a no_std build
64            #[cfg(not(feature = "std"))]
65            Some(_payload) => {
66                panic!("a device task panicked but its payload cannot be re-raised without `std`")
67            }
68            None => panic!("device runner channel disconnected before producing a result"),
69        }
70    }
71}
72
73/// Extension trait for re-raising a [`CallError`] on the caller's thread.
74pub trait CallResultExt<R> {
75    /// Returns the success value, or re-raises the original panic captured in the
76    /// [`CallError`] via [`CallError::resume`].
77    ///
78    /// Use this instead of [`Result::unwrap`] to panics with the original payload itself.
79    fn unwrap_or_resume(self) -> R;
80}
81
82impl<R> CallResultExt<R> for Result<R, CallError> {
83    #[track_caller]
84    fn unwrap_or_resume(self) -> R {
85        match self {
86            Ok(value) => value,
87            Err(err) => err.resume(),
88        }
89    }
90}
91
92impl core::fmt::Debug for CallError {
93    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
94        match self.message() {
95            Some(message) => write!(
96                f,
97                "CallError(task panicked on device runner thread: {message})"
98            ),
99            None if self.payload.is_some() => f.write_str(
100                "CallError(task panicked on device runner thread with a non-string payload)",
101            ),
102            None => f.write_str(
103                "CallError(device runner channel disconnected before producing a result)",
104            ),
105        }
106    }
107}
108
109#[derive(new, Clone, Debug)]
110/// Error when creating a [`DeviceService`].
111pub struct ServiceCreationError {
112    #[allow(dead_code)] // Debug uses it.
113    reason: alloc::string::String,
114}
115
116/// How a service's state is reached: the transport under a [`DeviceHandle`].
117///
118/// A transport holds no service type. It hands a task the boxed state as
119/// `&mut dyn Any`; the handle above it knows what that is.
120pub trait DeviceHandleSpec: Sized + Clone {
121    /// If functions block the current thread even if they are non-blocking.
122    const BLOCKING: bool;
123
124    /// Registers `service` for the device and returns a transport to it.
125    ///
126    /// If a runner thread for this `device_id` does not exist, it will be spawned.
127    fn insert<S: DeviceService>(
128        device_id: DeviceId,
129        service: S,
130    ) -> Result<Self, ServiceCreationError>;
131
132    /// Creates or retrieves a transport for the given device ID.
133    ///
134    /// If a runner thread for this `device_id` does not exist, it will be spawned.
135    fn new<S: DeviceService>(device_id: DeviceId) -> Self;
136
137    /// Retrieves the device ID for this handle.
138    fn device_id(&self) -> DeviceId;
139
140    /// Retrieves the server utilities for this thread.
141    fn utilities(&self) -> ServerUtilitiesHandle;
142
143    /// Doesn't flush the service state, but flushes any task enqueued in the communication
144    /// channel.
145    ///
146    /// # Notes
147    ///
148    /// This is a no-op for blocking handles.
149    fn flush_queue(&self);
150
151    fn submit_blocking<'a, R: Send, T: FnOnce(&mut dyn Any) -> R + Send + 'a>(
152        &self,
153        task: T,
154    ) -> Result<R, CallError>;
155
156    fn submit<T: FnOnce(&mut dyn Any) + Send + 'static>(&self, task: T);
157
158    fn exclusive<R: Send, T: FnOnce() -> R + Send>(&self, task: T) -> Result<R, CallError>;
159
160    fn shutdown(device_id: DeviceId) {
161        let _ = device_id;
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::CallError;
168    use alloc::boxed::Box;
169    use core::any::Any;
170
171    /// D.1 — A disconnected error carries neither a message nor a payload, which is
172    /// what distinguishes it from a non-string panic (both have `message() == None`).
173    #[test]
174    fn test_disconnected_has_no_message_or_payload() {
175        let err = CallError::disconnected();
176        assert_eq!(err.message(), None);
177        assert!(err.into_panic().is_none());
178    }
179
180    /// D.2 — Debug of a string payload includes the panic message.
181    #[test]
182    fn test_debug_string_payload_includes_message() {
183        let err = CallError::from_panic(Box::new("oops") as Box<dyn Any + Send>);
184        let debug = alloc::format!("{err:?}");
185        assert!(debug.contains("task panicked on device runner thread"));
186        assert!(debug.contains("oops"));
187    }
188
189    /// D.3 — Debug of a non-string payload reports it as such.
190    #[test]
191    fn test_debug_non_string_payload() {
192        let err = CallError::from_panic(Box::new(123u8) as Box<dyn Any + Send>);
193        let debug = alloc::format!("{err:?}");
194        assert!(debug.contains("non-string payload"), "got: {debug}");
195    }
196
197    /// D.4 — Debug of a disconnected error mentions the disconnection.
198    #[test]
199    fn test_debug_disconnected() {
200        let err = CallError::disconnected();
201        let debug = alloc::format!("{err:?}");
202        assert!(debug.contains("disconnected"), "got: {debug}");
203    }
204}