use cuda_async::device_context::{global_policy, init_device_contexts, load_module_from_ptx};
use cuda_async::device_operation::{value, DeviceOp, ExecutionContext};
use cuda_async::error::DeviceError;
use cuda_async::launch::AsyncKernelLaunch;
use cuda_core::{Function, LaunchConfig};
use std::future::{Future, IntoFuture};
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use std::time::{Duration, Instant};
const TRAP_PTX: &str = r#"
.version 7.0
.target sm_50
.address_size 64
.visible .entry fault_kernel()
{
trap;
}
"#;
fn on_fresh_thread<F: FnOnce() + Send + 'static>(f: F) {
std::thread::spawn(f).join().expect("test thread panicked");
}
fn noop_waker() -> Waker {
fn noop(_: *const ()) {}
fn clone(p: *const ()) -> RawWaker {
RawWaker::new(p, &VTABLE)
}
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, noop, noop, noop);
unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) }
}
fn alloc_device(bytes: usize) -> u64 {
cuda_async::device_context::with_device(0, |device| device.bind_to_thread())
.expect("device context")
.expect("bind_to_thread failed");
let mut dptr = std::mem::MaybeUninit::uninit();
let code = unsafe { cuda_bindings::cuMemAlloc_v2(dptr.as_mut_ptr(), bytes) };
assert_eq!(code, 0, "cuMemAlloc failed: {code}");
unsafe { dptr.assume_init() }
}
fn block_on_with_deadline<F: Future + Unpin>(mut future: F, deadline: Duration) -> F::Output {
let start = Instant::now();
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
loop {
match Pin::new(&mut future).poll(&mut cx) {
Poll::Ready(out) => return out,
Poll::Pending => {
assert!(
start.elapsed() < deadline,
"future did not resolve within {deadline:?}"
);
std::thread::sleep(Duration::from_millis(1));
}
}
}
}
struct FaultingOp {
dptr: u64,
bytes: usize,
passes: usize,
trap: Arc<Function>,
}
impl DeviceOp for FaultingOp {
type Output = ();
unsafe fn execute(self, context: &ExecutionContext) -> Result<(), DeviceError> {
let stream = context.get_cuda_stream().cu_stream();
for _ in 0..self.passes {
let code = cuda_bindings::cuMemsetD8Async(self.dptr, 0x11, self.bytes, stream);
if code != cuda_bindings::cudaError_enum_CUDA_SUCCESS {
return Err(DeviceError::Internal(format!(
"cuMemsetD8Async failed: {code}"
)));
}
}
let mut launch = AsyncKernelLaunch::new(self.trap);
launch.set_launch_config(LaunchConfig {
grid_dim: (1, 1, 1),
block_dim: (1, 1, 1),
shared_mem_bytes: 0,
});
launch.execute(context)
}
}
impl IntoFuture for FaultingOp {
type Output = Result<(), DeviceError>;
type IntoFuture = cuda_async::device_future::DeviceFuture<(), FaultingOp>;
fn into_future(self) -> Self::IntoFuture {
let policy = global_policy(0).expect("global policy");
match self.schedule(&policy) {
Ok(future) => future,
Err(error) => cuda_async::device_future::DeviceFuture::failed(error),
}
}
}
#[test]
fn device_fault_resolves_the_awaiting_future_with_the_driver_error() {
on_fresh_thread(|| {
init_device_contexts(0, 1).expect("init failed (requires GPU)");
let bytes = 64 << 20;
let dptr = alloc_device(bytes);
let module = load_module_from_ptx(TRAP_PTX, 0).expect("PTX JIT failed");
let trap = Arc::new(module.load_function("fault_kernel").expect("fault_kernel"));
let op = FaultingOp {
dptr,
bytes,
passes: 32,
trap,
};
let started = Instant::now();
let result = block_on_with_deadline(op.into_future(), Duration::from_secs(30));
assert!(
matches!(result, Err(DeviceError::Driver(_))),
"a faulted stream must surface the driver error, got {result:?} after {:?}",
started.elapsed()
);
let again = block_on_with_deadline(value(7).into_future(), Duration::from_secs(30));
assert!(
matches!(again, Err(DeviceError::Driver(_))),
"a dead context must fail fast with the driver error, got {again:?}"
);
let synced = value(8).sync();
assert!(
matches!(synced, Err(DeviceError::Driver(_))),
"sync on a dead context must fail, got {synced:?}"
);
});
}