#![cfg(feature = "gpu-tests")]
use std::ffi::c_void;
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
use std::time::{Duration, Instant};
use oxicuda_driver::ffi::{CUdeviceptr, CUstream};
use oxicuda_driver::loader::try_driver;
use oxicuda_driver::{Context, Device, Function, Module, Stream};
use oxicuda_memory::DeviceBuffer;
const SPIN_PTX: &str = r"
.version 6.0
.target sm_52
.address_size 64
.visible .entry spin_ns(
.param .u64 spin_dur,
.param .u64 spin_out
)
{
.reg .b64 %rd<8>;
.reg .pred %p<2>;
ld.param.u64 %rd1, [spin_dur];
ld.param.u64 %rd2, [spin_out];
mov.u64 %rd3, %globaltimer;
add.s64 %rd4, %rd3, %rd1;
$SPIN:
mov.u64 %rd5, %globaltimer;
setp.lt.u64 %p0, %rd5, %rd4;
@%p0 bra $SPIN;
st.global.u64 [%rd2], %rd5;
ret;
}
";
const CHECK_PTX: &str = r"
.version 6.0
.target sm_52
.address_size 64
.visible .entry count_mismatch(
.param .u64 cm_data,
.param .u32 cm_n,
.param .u64 cm_out,
.param .f32 cm_expect
)
{
.reg .b32 %r<12>;
.reg .b64 %rd<8>;
.reg .f32 %f<4>;
.reg .pred %p<4>;
ld.param.u64 %rd1, [cm_data];
ld.param.u32 %r1, [cm_n];
ld.param.u64 %rd2, [cm_out];
ld.param.f32 %f1, [cm_expect];
mov.u32 %r2, %ctaid.x;
mov.u32 %r3, %ntid.x;
mov.u32 %r4, %tid.x;
mad.lo.u32 %r5, %r2, %r3, %r4;
mov.u32 %r6, %nctaid.x;
mul.lo.u32 %r7, %r6, %r3;
$LOOP:
setp.ge.u32 %p1, %r5, %r1;
@%p1 bra $DONE;
mul.wide.u32 %rd3, %r5, 4;
add.s64 %rd4, %rd1, %rd3;
ld.global.f32 %f2, [%rd4];
setp.eq.f32 %p2, %f2, %f1;
@%p2 bra $NEXT;
mov.u32 %r8, 1;
atom.global.add.u32 %r9, [%rd2], %r8;
$NEXT:
add.s32 %r5, %r5, %r7;
bra $LOOP;
$DONE:
ret;
}
";
static CONTEXT: OnceLock<Option<Arc<Context>>> = OnceLock::new();
static GPU_SERIAL: Mutex<()> = Mutex::new(());
fn serial() -> MutexGuard<'static, ()> {
GPU_SERIAL.lock().unwrap_or_else(|e| e.into_inner())
}
fn context() -> Option<&'static Arc<Context>> {
CONTEXT
.get_or_init(|| {
oxicuda_driver::init().ok()?;
if Device::count().ok()? == 0 {
return None;
}
let dev = Device::get(0).ok()?;
Some(Arc::new(Context::new(&dev).ok()?))
})
.as_ref()
}
macro_rules! gpu_or_skip {
() => {
match context() {
Some(ctx) => {
ctx.set_current().expect("make shared context current");
ctx
}
None => {
eprintln!("skipping: no CUDA driver/device");
return;
}
}
};
}
fn load(ptx: &str, entry: &str) -> (Module, Function) {
let module = Module::from_ptx(ptx).expect("ptxas rejected the test kernel");
let func = module.get_function(entry).expect("entry point not found");
(module, func)
}
fn launch_spin(func: &Function, stream: CUstream, duration: Duration, out: CUdeviceptr) {
let api = try_driver().expect("driver present");
let mut ns_arg: u64 = duration.as_nanos() as u64;
let mut out_arg: CUdeviceptr = out;
let mut params: [*mut c_void; 2] = [
std::ptr::from_mut(&mut ns_arg).cast(),
std::ptr::from_mut(&mut out_arg).cast(),
];
let rc = unsafe {
(api.cu_launch_kernel)(
func.raw(),
1,
1,
1,
1,
1,
1,
0,
stream,
params.as_mut_ptr(),
std::ptr::null_mut(),
)
};
oxicuda_driver::check(rc).expect("spin kernel launch");
}
fn launch_check(
func: &Function,
stream: CUstream,
data: CUdeviceptr,
n: u32,
out: CUdeviceptr,
expect: f32,
) {
let api = try_driver().expect("driver present");
let mut data_arg = data;
let mut n_arg = n;
let mut out_arg = out;
let mut expect_arg = expect;
let mut params: [*mut c_void; 4] = [
std::ptr::from_mut(&mut data_arg).cast(),
std::ptr::from_mut(&mut n_arg).cast(),
std::ptr::from_mut(&mut out_arg).cast(),
std::ptr::from_mut(&mut expect_arg).cast(),
];
let rc = unsafe {
(api.cu_launch_kernel)(
func.raw(),
256,
1,
1,
256,
1,
1,
0,
stream,
params.as_mut_ptr(),
std::ptr::null_mut(),
)
};
oxicuda_driver::check(rc).expect("check kernel launch");
}
fn sync_raw(stream: CUstream) {
let api = try_driver().expect("driver present");
oxicuda_driver::check(unsafe { (api.cu_stream_synchronize)(stream) }).expect("stream sync");
}
const SPIN: Duration = Duration::from_millis(400);
#[test]
fn zeroed_still_waits_for_the_legacy_stream() {
let _ctx = gpu_or_skip!();
let _guard = serial();
let (_module, spin) = load(SPIN_PTX, "spin_ns");
let stamp = DeviceBuffer::<u64>::zeroed(1).expect("stamp buffer");
launch_spin(&spin, CUstream::default(), SPIN, stamp.as_device_ptr());
let started = Instant::now();
let buf = DeviceBuffer::<f32>::zeroed(1024).expect("zeroed");
let elapsed = started.elapsed();
eprintln!("zeroed() with {SPIN:?} queued on the legacy stream: {elapsed:?}");
assert!(
elapsed >= SPIN.mul_f64(0.8),
"zeroed() returned after {elapsed:?} while {SPIN:?} of work was still \
queued on the legacy default stream -- the memset it issues is queued \
behind that work on that same stream, so returning early means the \
zero-fill had NOT landed and any consumer stream could race it"
);
let mut host = vec![1.0f32; 1024];
buf.copy_to_host(&mut host).expect("readback");
assert!(host.iter().all(|&v| v == 0.0), "buffer was not zero-filled");
}
#[test]
fn zeroed_no_longer_waits_for_unrelated_streams() {
let ctx = gpu_or_skip!();
let _guard = serial();
let other = Stream::new(ctx).expect("non-blocking stream");
let (_module, spin) = load(SPIN_PTX, "spin_ns");
let stamp = DeviceBuffer::<u64>::zeroed(1).expect("stamp buffer");
launch_spin(&spin, other.raw(), SPIN, stamp.as_device_ptr());
let started = Instant::now();
let buf = DeviceBuffer::<f32>::zeroed(1024).expect("zeroed");
let elapsed = started.elapsed();
eprintln!("zeroed() with {SPIN:?} queued on an UNRELATED stream: {elapsed:?}");
assert!(
elapsed < SPIN.mul_f64(0.5),
"zeroed() blocked for {elapsed:?} while {SPIN:?} of unrelated work ran \
on an independent non-blocking stream -- that is the whole-context \
barrier `cuCtxSynchronize` imposed; the legacy-stream-scoped \
`cuStreamSynchronize(NULL)` must not wait for it"
);
let mut host = vec![1.0f32; 1024];
buf.copy_to_host(&mut host).expect("readback");
assert!(host.iter().all(|&v| v == 0.0), "buffer was not zero-filled");
other.synchronize().expect("drain spin");
}
#[test]
fn copy_from_host_result_is_visible_to_a_non_blocking_stream() {
let ctx = gpu_or_skip!();
let _guard = serial();
let consumer = Stream::new(ctx).expect("non-blocking stream");
let (_module, check) = load(CHECK_PTX, "count_mismatch");
const N: usize = 4 * 1024 * 1024; const SENTINEL: f32 = 3.25;
let mut buf = DeviceBuffer::<f32>::zeroed(N).expect("zeroed");
let mismatches = DeviceBuffer::<u32>::zeroed(1).expect("counter");
let host = vec![SENTINEL; N];
buf.copy_from_host(&host).expect("copy_from_host");
launch_check(
&check,
consumer.raw(),
buf.as_device_ptr(),
N as u32,
mismatches.as_device_ptr(),
SENTINEL,
);
consumer.synchronize().expect("drain consumer");
let mut count = [0u32; 1];
mismatches.copy_to_host(&mut count).expect("read counter");
assert_eq!(
count[0], 0,
"a kernel on a non-blocking stream saw {} of {N} elements still holding \
pre-upload data -- copy_from_host returned before its DMA landed",
count[0]
);
}
#[test]
fn zeroed_result_is_visible_to_a_non_blocking_stream() {
let ctx = gpu_or_skip!();
let _guard = serial();
let consumer = Stream::new(ctx).expect("non-blocking stream");
let (_module, check) = load(CHECK_PTX, "count_mismatch");
const N: usize = 4 * 1024 * 1024;
{
let mut dirty = DeviceBuffer::<f32>::alloc(N).expect("dirty alloc");
dirty.copy_from_host(&vec![7.5f32; N]).expect("dirty fill");
}
let buf = DeviceBuffer::<f32>::zeroed(N).expect("zeroed");
let mismatches = DeviceBuffer::<u32>::zeroed(1).expect("counter");
launch_check(
&check,
consumer.raw(),
buf.as_device_ptr(),
N as u32,
mismatches.as_device_ptr(),
0.0,
);
consumer.synchronize().expect("drain consumer");
let mut count = [0u32; 1];
mismatches.copy_to_host(&mut count).expect("read counter");
assert_eq!(
count[0], 0,
"a kernel on a non-blocking stream saw {} of {N} elements not yet zeroed \
-- zeroed() returned before its memset landed",
count[0]
);
}
#[test]
fn spin_kernel_actually_spins() {
let ctx = gpu_or_skip!();
let _guard = serial();
let stream = Stream::new(ctx).expect("stream");
let (_module, spin) = load(SPIN_PTX, "spin_ns");
let stamp = DeviceBuffer::<u64>::zeroed(1).expect("stamp buffer");
let started = Instant::now();
launch_spin(&spin, stream.raw(), SPIN, stamp.as_device_ptr());
stream.synchronize().expect("drain spin");
let elapsed = started.elapsed();
assert!(
elapsed >= SPIN.mul_f64(0.8),
"spin kernel returned after {elapsed:?}, expected ~{SPIN:?} -- the \
timing tests that rely on it would be vacuous"
);
let mut stamped = [0u64; 1];
stamp.copy_to_host(&mut stamped).expect("read stamp");
assert_ne!(stamped[0], 0, "spin kernel never reached its final store");
}
#[test]
fn null_handle_synchronizes_the_legacy_stream() {
let _ctx = gpu_or_skip!();
let _guard = serial();
let (_module, spin) = load(SPIN_PTX, "spin_ns");
let stamp = DeviceBuffer::<u64>::zeroed(1).expect("stamp buffer");
let started = Instant::now();
launch_spin(&spin, CUstream::default(), SPIN, stamp.as_device_ptr());
sync_raw(CUstream::default());
let elapsed = started.elapsed();
assert!(
elapsed >= SPIN.mul_f64(0.8),
"cuStreamSynchronize(NULL) returned after {elapsed:?} without waiting \
for work launched on the NULL stream -- the legacy-stream \
interpretation this optimisation relies on would not hold"
);
let mut stamped = [0u64; 1];
stamp.copy_to_host(&mut stamped).expect("read stamp");
assert_ne!(stamped[0], 0, "spin kernel did not complete");
}