use cuda_async::cuda_graph::CudaGraph;
use cuda_async::device_future::DeviceFuture;
use cuda_async::device_operation::{value, DeviceOp, ExecutionContext, Value};
use cuda_async::error::DeviceError;
use std::future::IntoFuture;
fn has_gpu() -> bool {
cuda_core::Device::device_count()
.map(|n| n > 0)
.unwrap_or(false)
}
fn on_fresh_thread<F: FnOnce() + Send + 'static>(f: F) {
std::thread::spawn(f).join().expect("test thread panicked");
}
#[test]
fn scope_empty_closure() {
if !has_gpu() {
return;
}
on_fresh_thread(|| {
let device = cuda_core::Device::new(0).unwrap();
let stream = device.new_stream().unwrap();
let graph = CudaGraph::scope(&stream, |_s| Ok(())).unwrap();
graph.launch().sync_on(&stream).unwrap();
});
}
#[test]
fn scope_records_value_ops() {
if !has_gpu() {
return;
}
on_fresh_thread(|| {
let device = cuda_core::Device::new(0).unwrap();
let stream = device.new_stream().unwrap();
let mut recorded = Vec::new();
let graph = CudaGraph::scope(&stream, |s| {
let a = s.record(value(42))?;
let b = s.record(value("hello"))?;
recorded.push(a);
recorded.push(b.len() as i32);
Ok(())
})
.unwrap();
assert_eq!(recorded, vec![42, 5]);
graph.launch().sync_on(&stream).unwrap();
});
}
#[test]
fn scope_error_propagation() {
if !has_gpu() {
return;
}
on_fresh_thread(|| {
let device = cuda_core::Device::new(0).unwrap();
let stream = device.new_stream().unwrap();
let result = CudaGraph::scope(&stream, |_s| {
Err(DeviceError::Internal("test error".into()))
});
assert!(result.is_err());
match result {
Err(DeviceError::Internal(msg)) => {
assert!(
msg.contains("test error"),
"Expected test error, got: {msg}"
);
}
Err(e) => panic!("Expected Internal error, got: {e}"),
Ok(_) => panic!("Expected error, got Ok"),
}
});
}
#[test]
fn scope_panic_safety() {
if !has_gpu() {
return;
}
let result = std::thread::spawn(|| {
let device = cuda_core::Device::new(0).unwrap();
let stream = device.new_stream().unwrap();
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
CudaGraph::scope(&stream, |_s| {
panic!("intentional panic in scope");
})
}));
unsafe { stream.synchronize() }.unwrap();
})
.join();
assert!(
result.is_ok(),
"Thread should not panic after scope cleanup"
);
}
#[test]
fn scope_multiple_launches() {
if !has_gpu() {
return;
}
on_fresh_thread(|| {
let device = cuda_core::Device::new(0).unwrap();
let stream = device.new_stream().unwrap();
let graph = CudaGraph::scope(&stream, |_s| Ok(())).unwrap();
for _ in 0..10 {
graph.launch().sync_on(&stream).unwrap();
}
});
}
#[test]
fn scope_nested_execution_rejected() {
if !has_gpu() {
return;
}
on_fresh_thread(|| {
let device = cuda_core::Device::new(0).unwrap();
let stream = device.new_stream().unwrap();
let other_stream = device.new_stream().unwrap();
let result = CudaGraph::scope(&stream, |_s| {
let _ = value(42).sync_on(&stream)?;
Ok(())
});
assert!(result.is_err(), "nested sync_on should fail");
let result = CudaGraph::scope(&stream, |_s| {
let _ = value(42).sync_on(&other_stream)?;
Ok(())
});
assert!(result.is_err(), "nested sync_on (other stream) should fail");
let result = CudaGraph::scope(&stream, |_s| {
value(42).sync()?;
Ok(())
});
assert!(result.is_err(), "nested sync should fail");
});
}
#[test]
fn launch_outlives_its_graph() {
if !has_gpu() {
return;
}
on_fresh_thread(|| {
let device = cuda_core::Device::new(0).unwrap();
let stream = device.new_stream().unwrap();
let graph = CudaGraph::scope(&stream, |s| {
s.record(value(1))?;
Ok(())
})
.unwrap();
let launch = graph.launch();
drop(graph);
launch
.sync_on(&stream)
.expect("launch must keep the instantiated graph alive");
let graph = CudaGraph::capture(stream.clone(), value(3)).unwrap();
let a = graph.launch();
let b = graph.launch();
drop(graph);
a.sync_on(&stream).unwrap();
b.sync().unwrap();
});
}
#[test]
fn capture_rejects_nested_execution() {
if !has_gpu() {
return;
}
on_fresh_thread(|| {
let device = cuda_core::Device::new(0).unwrap();
let stream = device.new_stream().unwrap();
let mut graph = CudaGraph::capture(
stream.clone(),
value(1).then(|x| {
let nested = value(0).sync();
assert!(
matches!(&nested, Err(DeviceError::Internal(m)) if m.contains("non-reentrant")),
"nested execution inside capture must hit the lock, got {nested:?}"
);
value(x)
}),
)
.expect("capture failed");
assert_eq!(graph.take_output(), Some(1));
});
}
#[test]
fn capture_panic_ends_capture_and_releases_lock() {
if !has_gpu() {
return;
}
on_fresh_thread(|| {
let device = cuda_core::Device::new(0).unwrap();
let stream = device.new_stream().unwrap();
let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
CudaGraph::capture(
stream.clone(),
value(()).then(|()| -> Value<()> { panic!("intentional panic in capture") }),
)
}));
assert!(panicked.is_err(), "the panic must propagate");
unsafe { stream.synchronize() }.expect("stream must not be left in capture mode");
assert_eq!(value(2).sync_on(&stream).expect("lock must be free"), 2);
let mut graph = CudaGraph::capture(stream.clone(), value(4)).expect("recapture");
assert_eq!(graph.take_output(), Some(4));
});
}
struct FailingOp;
impl DeviceOp for FailingOp {
type Output = ();
unsafe fn execute(self, _context: &ExecutionContext) -> Result<(), DeviceError> {
Err(DeviceError::Internal("failing op".into()))
}
}
impl IntoFuture for FailingOp {
type Output = Result<(), DeviceError>;
type IntoFuture = DeviceFuture<(), FailingOp>;
fn into_future(self) -> Self::IntoFuture {
DeviceFuture::failed(DeviceError::Internal("not used".into()))
}
}
#[test]
fn capture_error_ends_capture_and_releases_lock() {
if !has_gpu() {
return;
}
on_fresh_thread(|| {
let device = cuda_core::Device::new(0).unwrap();
let stream = device.new_stream().unwrap();
let err = match CudaGraph::capture(stream.clone(), FailingOp) {
Err(err) => err,
Ok(_) => panic!("capture of a failing op must fail"),
};
assert!(
matches!(&err, DeviceError::Internal(m) if m == "failing op"),
"op error must propagate unchanged, got {err:?}"
);
unsafe { stream.synchronize() }.expect("stream must not be left in capture mode");
assert_eq!(value(2).sync_on(&stream).expect("lock must be free"), 2);
CudaGraph::capture(stream.clone(), value(4)).expect("recapture");
});
}
#[test]
fn graph_combinators_capture_and_replay() {
if !has_gpu() {
return;
}
on_fresh_thread(|| {
let device = cuda_core::Device::new(0).unwrap();
let stream = device.new_stream().unwrap();
let mut graph = value(5).graph_on(stream.clone()).expect("graph_on");
assert_eq!(graph.take_output(), Some(5));
graph.launch().sync_on(&stream).unwrap();
let mut graph = value(6).graph().expect("graph");
assert_eq!(graph.take_output(), Some(6));
graph.launch().sync().unwrap();
let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
value(())
.then(|()| -> Value<()> { panic!("intentional panic in graph_on") })
.graph_on(stream.clone())
}));
assert!(panicked.is_err());
unsafe { stream.synchronize() }.expect("stream must not be left in capture mode");
assert_eq!(value(7).sync_on(&stream).expect("lock must be free"), 7);
});
}
#[test]
fn update_runs_unit_graph_nodes() {
if !has_gpu() {
return;
}
on_fresh_thread(|| {
let device = cuda_core::Device::new(0).unwrap();
let stream = device.new_stream().unwrap();
let graph = CudaGraph::scope(&stream, |_s| Ok(())).unwrap();
graph.update(value(())).expect("unit GraphNode");
graph.launch().sync_on(graph.stream()).unwrap();
let nested = value(())
.then(|()| {
let r = graph.update(value(()));
assert!(
matches!(&r, Err(DeviceError::Internal(m)) if m.contains("non-reentrant")),
"update inside an executing op must hit the lock, got {r:?}"
);
value(())
})
.sync_on(&stream);
assert!(nested.is_ok());
});
}