use cuda_async::device_context::{global_policy, init_device_contexts};
use cuda_async::device_operation::{DeviceOp, ExecutionContext};
use cuda_async::error::DeviceError;
use futures::task::ArcWake;
use std::future::{Future, IntoFuture};
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
fn on_fresh_thread<F: FnOnce() + Send + 'static>(f: F) {
std::thread::spawn(f).join().expect("test thread panicked");
}
fn force_notification_path() {
#[allow(unused_unsafe)]
unsafe {
std::env::set_var("CUDA_ASYNC_SPIN_BUDGET_US", "0")
};
}
struct MemsetOp {
dptr: u64,
bytes: usize,
passes: usize,
value: u8,
}
impl DeviceOp for MemsetOp {
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, self.value, self.bytes, stream);
if code != cuda_bindings::cudaError_enum_CUDA_SUCCESS {
return Err(DeviceError::Internal(format!(
"cuMemsetD8Async failed: {code}"
)));
}
}
Ok(())
}
}
impl IntoFuture for MemsetOp {
type Output = Result<(), DeviceError>;
type IntoFuture = cuda_async::device_future::DeviceFuture<(), MemsetOp>;
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),
}
}
}
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 read_device(dptr: u64, bytes: usize) -> Vec<u8> {
let mut host = vec![0u8; bytes];
let code = unsafe { cuda_bindings::cuMemcpyDtoH_v2(host.as_mut_ptr() as *mut _, dptr, bytes) };
assert_eq!(code, 0, "cuMemcpyDtoH failed: {code}");
host
}
fn slow_op(dptr: u64, bytes: usize, value: u8) -> MemsetOp {
MemsetOp {
dptr,
bytes,
passes: 16,
value,
}
}
struct FlagWaker {
woken: AtomicBool,
thread: std::thread::Thread,
}
impl ArcWake for FlagWaker {
fn wake_by_ref(arc_self: &Arc<Self>) {
arc_self.woken.store(true, Ordering::SeqCst);
arc_self.thread.unpark();
}
}
fn flag_waker() -> (Arc<FlagWaker>, std::task::Waker) {
let state = Arc::new(FlagWaker {
woken: AtomicBool::new(false),
thread: std::thread::current(),
});
let waker = futures::task::waker(state.clone());
(state, waker)
}
fn block_on_with_deadline<F: Future + Unpin>(mut future: F, deadline: Duration) -> F::Output {
let start = Instant::now();
let (_state, waker) = flag_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 complete within {deadline:?} (missed wake?)"
);
std::thread::park_timeout(Duration::from_millis(1));
}
}
}
}
const DEADLINE: Duration = Duration::from_secs(20);
const BUF: usize = 64 << 20;
#[test]
fn notification_path_completes_and_data_is_visible() {
on_fresh_thread(|| {
force_notification_path();
init_device_contexts(0, 1).expect("init failed (requires GPU)");
let dptr = alloc_device(BUF);
for round in 0..8u8 {
let value = 0xA0 + round;
block_on_with_deadline(slow_op(dptr, BUF, value).into_future(), DEADLINE)
.expect("op failed");
let host = read_device(dptr, 4096);
assert!(
host.iter().all(|&b| b == value),
"round {round}: data not visible after await"
);
}
});
}
#[test]
fn spurious_wake_repolls_to_pending_then_completes() {
on_fresh_thread(|| {
force_notification_path();
init_device_contexts(0, 1).expect("init failed (requires GPU)");
let dptr = alloc_device(BUF);
let mut future = slow_op(dptr, BUF, 0x11).into_future();
let (_state, waker) = flag_waker();
let mut cx = Context::from_waker(&waker);
let first = Pin::new(&mut future).poll(&mut cx);
assert!(first.is_pending(), "slow op resolved on first poll");
for _ in 0..4 {
waker.wake_by_ref();
let _ = Pin::new(&mut future).poll(&mut cx);
}
block_on_with_deadline(future, DEADLINE).expect("op failed after spurious wakes");
});
}
#[test]
fn completion_wakes_the_latest_registered_waker() {
on_fresh_thread(|| {
force_notification_path();
init_device_contexts(0, 1).expect("init failed (requires GPU)");
let dptr = alloc_device(BUF);
let mut future = slow_op(dptr, BUF, 0x22).into_future();
let (state_a, waker_a) = flag_waker();
let mut cx_a = Context::from_waker(&waker_a);
assert!(Pin::new(&mut future).poll(&mut cx_a).is_pending());
let (state_b, waker_b) = flag_waker();
let mut cx_b = Context::from_waker(&waker_b);
if Pin::new(&mut future).poll(&mut cx_b).is_pending() {
let start = Instant::now();
while !state_b.woken.load(Ordering::SeqCst) {
assert!(start.elapsed() < DEADLINE, "waker B was never woken");
std::thread::park_timeout(Duration::from_millis(1));
}
assert!(
!state_a.woken.load(Ordering::SeqCst),
"stale waker A was woken after replacement"
);
match Pin::new(&mut future).poll(&mut cx_b) {
Poll::Ready(result) => result.expect("op failed"),
Poll::Pending => panic!("woken but still pending"),
}
}
});
}
#[test]
fn drop_mid_flight_recycles_and_later_pipelines_work() {
on_fresh_thread(|| {
force_notification_path();
init_device_contexts(0, 1).expect("init failed (requires GPU)");
let dptr = alloc_device(BUF);
for _ in 0..32 {
let mut future = slow_op(dptr, BUF, 0x33).into_future();
let (_state, waker) = flag_waker();
let mut cx = Context::from_waker(&waker);
assert!(Pin::new(&mut future).poll(&mut cx).is_pending());
drop(future);
}
std::thread::sleep(Duration::from_millis(200));
for round in 0..8u8 {
let value = 0x40 + round;
block_on_with_deadline(slow_op(dptr, BUF, value).into_future(), DEADLINE)
.expect("op after cancellations failed");
let host = read_device(dptr, 4096);
assert!(host.iter().all(|&b| b == value));
}
});
}
#[test]
fn sequential_pingpong_park_unpark() {
on_fresh_thread(|| {
force_notification_path();
init_device_contexts(0, 1).expect("init failed (requires GPU)");
let small = 8 << 20;
let dptr = alloc_device(small);
for i in 0..200u32 {
let value = (i % 251) as u8;
let op = MemsetOp {
dptr,
bytes: small,
passes: 2,
value,
};
block_on_with_deadline(op.into_future(), DEADLINE).expect("ping-pong op failed");
if i % 10 == 0 {
std::thread::sleep(Duration::from_millis(2));
}
}
let host = read_device(dptr, 4096);
assert!(host.iter().all(|&b| b == (199 % 251) as u8));
});
}
#[test]
fn concurrent_submitters_share_one_reactor() {
on_fresh_thread(|| {
force_notification_path();
init_device_contexts(0, 1).expect("init failed (requires GPU)");
const THREADS: usize = 8;
const OPS_PER_THREAD: usize = 40;
let buf = 8 << 20;
let workers: Vec<_> = (0..THREADS)
.map(|t| {
std::thread::spawn(move || {
init_device_contexts(0, 1).expect("per-thread init failed");
let dptr = alloc_device(buf);
let value = (t as u8).wrapping_add(1);
for _ in 0..OPS_PER_THREAD {
let op = MemsetOp {
dptr,
bytes: buf,
passes: 2,
value,
};
block_on_with_deadline(op.into_future(), DEADLINE)
.expect("concurrent op failed");
}
let host = read_device(dptr, 4096);
assert!(
host.iter().all(|&b| b == value),
"thread {t}: data corrupted (slot mix-up?)"
);
})
})
.collect();
for (t, w) in workers.into_iter().enumerate() {
w.join()
.unwrap_or_else(|_| panic!("worker thread {t} panicked"));
}
});
}