use core::fmt::Debug;
#[cfg(not(target_family = "wasm"))]
use core::time::Duration;
#[cfg(not(target_family = "wasm"))]
use cubecl_environment::backtrace::BackTrace;
use cubecl_environment::future::channel::{Sender, WeakSender};
use cubecl_server::driver::DevicePoison;
use cubecl_server::server::ServerError;
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError};
#[cfg(not(target_family = "wasm"))]
const WAIT_SLICE: Duration = Duration::from_secs(1);
#[cfg(not(target_family = "wasm"))]
const PROBE_AFTER: Duration = Duration::from_secs(5);
#[derive(Clone, Debug, Default)]
pub struct PoisonWatch {
shared: Arc<Shared>,
}
#[derive(Debug, Default)]
struct Shared {
reason: OnceLock<String>,
waiters: Mutex<Vec<Box<dyn Waiter>>>,
}
impl PoisonWatch {
pub fn watch(device: &wgpu::Device) -> Self {
let poison = Self::default();
let lost = poison.clone();
device.set_device_lost_callback(move |kind, message| {
lost.poison(format!("{kind:?}: {message}"));
log::error!("the wgpu device was lost ({kind:?}): {message}");
});
let watched = poison.clone();
device.on_uncaptured_error(Arc::new(move |error| {
if watched.is_poisoned() {
log::debug!("error on a poisoned wgpu device: {error}");
} else {
panic!("wgpu error: {error}");
}
}));
poison
}
pub fn is_poisoned(&self) -> bool {
self.shared.reason.get().is_some()
}
pub fn check(&self) -> Result<(), ServerError> {
match self.shared.reason.get() {
Some(reason) => Err(DevicePoison::new(reason.clone()).into()),
None => Ok(()),
}
}
pub fn wake_on_loss<T: Send + 'static>(&self, sender: &Sender<T>) {
let mut waiters = self.waiters();
if self.is_poisoned() {
sender.close();
return;
}
if waiters.len() == waiters.capacity() {
waiters.retain(|waiter| waiter.is_pending());
let pending = waiters.len();
waiters.reserve(pending);
}
waiters.push(Box::new(sender.downgrade()));
}
#[cfg(not(target_family = "wasm"))]
pub fn wait_unless_lost(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
submission: Option<wgpu::SubmissionIndex>,
) -> Result<(), ServerError> {
let poll_slice = |submission: &Option<wgpu::SubmissionIndex>| {
device.poll(wgpu::PollType::Wait {
submission_index: submission.clone(),
timeout: Some(WAIT_SLICE),
})
};
self.check()?;
let mut submission = submission;
let mut polled = poll_slice(&submission);
let mut stalled = Duration::ZERO;
while let Err(wgpu::PollError::Timeout) = polled {
self.check()?;
submission.get_or_insert_with(|| queue.submit(core::iter::empty()));
stalled += WAIT_SLICE;
if stalled >= PROBE_AFTER {
queue.submit(core::iter::empty());
stalled = Duration::ZERO;
}
polled = poll_slice(&submission);
}
match polled {
Ok(_) => self.check(),
Err(err) => Err(ServerError::Generic {
reason: format!("wgpu: waiting on the device failed ({err})"),
backtrace: BackTrace::capture(),
}),
}
}
#[cfg(target_family = "wasm")]
pub fn wait_unless_lost(
&self,
_device: &wgpu::Device,
_queue: &wgpu::Queue,
_submission: Option<wgpu::SubmissionIndex>,
) -> Result<(), ServerError> {
self.check()
}
fn poison(&self, reason: String) {
let _ = self.shared.reason.set(reason);
for waiter in self.waiters().drain(..) {
waiter.close();
}
}
fn waiters(&self) -> MutexGuard<'_, Vec<Box<dyn Waiter>>> {
self.shared
.waiters
.lock()
.unwrap_or_else(PoisonError::into_inner)
}
}
trait Waiter: Send + Debug {
fn is_pending(&self) -> bool;
fn close(&self);
}
impl<T: Send> Waiter for WeakSender<T> {
fn is_pending(&self) -> bool {
self.upgrade().is_some()
}
fn close(&self) {
if let Some(sender) = self.upgrade() {
sender.close();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use cubecl_environment::future::{block_on, channel::bounded};
#[test]
fn a_wait_pending_when_the_device_is_lost_wakes_poisoned() {
let watch = PoisonWatch::default();
let (sender, receiver) = bounded::<()>(1);
watch.wake_on_loss(&sender);
watch.poison("lost".into());
assert!(block_on(receiver.recv()).is_err());
assert!(watch.check().is_err());
}
#[test]
fn a_wait_started_after_the_loss_wakes_at_once() {
let watch = PoisonWatch::default();
watch.poison("lost".into());
let (sender, receiver) = bounded::<()>(1);
watch.wake_on_loss(&sender);
assert!(block_on(receiver.recv()).is_err());
}
#[test]
fn a_sender_dropped_unsent_still_closes_its_channel() {
let watch = PoisonWatch::default();
let (sender, receiver) = bounded::<()>(1);
watch.wake_on_loss(&sender);
drop(sender);
assert!(block_on(receiver.recv()).is_err());
assert!(watch.check().is_ok());
}
}