use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
use std::time::{Duration, Instant};
use oxicuda_driver::error::{CudaError, CudaResult};
use oxicuda_driver::event::Event;
use oxicuda_driver::stream::Stream;
use crate::kernel::{Kernel, KernelArgs};
use crate::params::LaunchParams;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompletionStatus {
Pending,
Complete,
Error(String),
}
impl CompletionStatus {
#[inline]
pub fn is_complete(&self) -> bool {
matches!(self, Self::Complete)
}
#[inline]
pub fn is_pending(&self) -> bool {
matches!(self, Self::Pending)
}
#[inline]
pub fn is_error(&self) -> bool {
matches!(self, Self::Error(_))
}
}
impl std::fmt::Display for CompletionStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pending => write!(f, "Pending"),
Self::Complete => write!(f, "Complete"),
Self::Error(msg) => write!(f, "Error: {msg}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PollStrategy {
Spin,
Yield,
BackoffMicros(u64),
}
impl Default for PollStrategy {
#[inline]
fn default() -> Self {
Self::Yield
}
}
#[derive(Debug, Clone)]
pub struct AsyncLaunchConfig {
pub poll_strategy: PollStrategy,
pub timeout: Option<Duration>,
}
impl Default for AsyncLaunchConfig {
#[inline]
fn default() -> Self {
Self {
poll_strategy: PollStrategy::Yield,
timeout: None,
}
}
}
impl AsyncLaunchConfig {
#[inline]
pub fn new(poll_strategy: PollStrategy) -> Self {
Self {
poll_strategy,
timeout: None,
}
}
#[inline]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LaunchTiming {
pub elapsed_us: f64,
}
impl LaunchTiming {
#[inline]
pub fn elapsed_ms(&self) -> f64 {
self.elapsed_us / 1000.0
}
#[inline]
pub fn elapsed_secs(&self) -> f64 {
self.elapsed_us / 1_000_000.0
}
}
impl std::fmt::Display for LaunchTiming {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.elapsed_us < 1000.0 {
write!(f, "{:.2} us", self.elapsed_us)
} else if self.elapsed_us < 1_000_000.0 {
write!(f, "{:.3} ms", self.elapsed_ms())
} else {
write!(f, "{:.4} s", self.elapsed_secs())
}
}
}
struct PollerShared {
done: AtomicBool,
waker: Mutex<Waker>,
}
impl PollerShared {
fn new(waker: Waker) -> Self {
Self {
done: AtomicBool::new(false),
waker: Mutex::new(waker),
}
}
fn mark_done(&self) {
self.done.store(true, Ordering::Release);
}
fn refresh_waker(&self, cx: &Context<'_>) {
let mut stored = self
.waker
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if !stored.will_wake(cx.waker()) {
*stored = cx.waker().clone();
}
}
}
fn mark_poller_done(poller: &Option<Arc<PollerShared>>) {
if let Some(shared) = poller {
shared.mark_done();
}
}
fn spawn_poller_thread(shared: Arc<PollerShared>, strategy: PollStrategy) {
std::thread::spawn(move || {
loop {
match strategy {
PollStrategy::Spin => {}
PollStrategy::Yield => std::thread::yield_now(),
PollStrategy::BackoffMicros(us) => std::thread::sleep(Duration::from_micros(us)),
}
if shared.done.load(Ordering::Acquire) {
break;
}
let waker = shared
.waker
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
waker.wake_by_ref();
}
});
}
pub struct LaunchCompletion {
event: Event,
strategy: PollStrategy,
timeout: Option<Duration>,
start_time: Option<Instant>,
poller: Option<Arc<PollerShared>>,
}
impl LaunchCompletion {
fn new(event: Event, config: &AsyncLaunchConfig) -> Self {
Self {
event,
strategy: config.poll_strategy,
timeout: config.timeout,
start_time: None,
poller: None,
}
}
pub fn status(&self) -> CompletionStatus {
match self.event.query() {
Ok(true) => CompletionStatus::Complete,
Ok(false) => CompletionStatus::Pending,
Err(e) => CompletionStatus::Error(e.to_string()),
}
}
fn check_timeout(&self) -> bool {
match (self.timeout, self.start_time) {
(Some(timeout), Some(start)) => start.elapsed() >= timeout,
_ => false,
}
}
fn ensure_poller(&mut self, cx: &Context<'_>) {
match &self.poller {
None => {
let shared = Arc::new(PollerShared::new(cx.waker().clone()));
spawn_poller_thread(Arc::clone(&shared), self.strategy);
self.poller = Some(shared);
}
Some(shared) => shared.refresh_waker(cx),
}
}
}
impl Future for LaunchCompletion {
type Output = CudaResult<()>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.start_time.is_none() {
self.start_time = Some(Instant::now());
}
if self.check_timeout() {
mark_poller_done(&self.poller);
return Poll::Ready(Err(CudaError::Timeout));
}
match self.event.query() {
Ok(true) => {
mark_poller_done(&self.poller);
Poll::Ready(Ok(()))
}
Ok(false) => {
self.ensure_poller(cx);
Poll::Pending
}
Err(e) => {
mark_poller_done(&self.poller);
Poll::Ready(Err(e))
}
}
}
}
impl Drop for LaunchCompletion {
fn drop(&mut self) {
mark_poller_done(&self.poller);
}
}
pub struct TimedLaunchCompletion {
start_event: Event,
end_event: Event,
strategy: PollStrategy,
timeout: Option<Duration>,
start_time: Option<Instant>,
poller: Option<Arc<PollerShared>>,
}
impl TimedLaunchCompletion {
fn new(start_event: Event, end_event: Event, config: &AsyncLaunchConfig) -> Self {
Self {
start_event,
end_event,
strategy: config.poll_strategy,
timeout: config.timeout,
start_time: None,
poller: None,
}
}
pub fn status(&self) -> CompletionStatus {
match self.end_event.query() {
Ok(true) => CompletionStatus::Complete,
Ok(false) => CompletionStatus::Pending,
Err(e) => CompletionStatus::Error(e.to_string()),
}
}
fn check_timeout(&self) -> bool {
match (self.timeout, self.start_time) {
(Some(timeout), Some(start)) => start.elapsed() >= timeout,
_ => false,
}
}
fn ensure_poller(&mut self, cx: &Context<'_>) {
match &self.poller {
None => {
let shared = Arc::new(PollerShared::new(cx.waker().clone()));
spawn_poller_thread(Arc::clone(&shared), self.strategy);
self.poller = Some(shared);
}
Some(shared) => shared.refresh_waker(cx),
}
}
}
impl Future for TimedLaunchCompletion {
type Output = CudaResult<LaunchTiming>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.start_time.is_none() {
self.start_time = Some(Instant::now());
}
if self.check_timeout() {
mark_poller_done(&self.poller);
return Poll::Ready(Err(CudaError::Timeout));
}
match self.end_event.query() {
Ok(true) => {
mark_poller_done(&self.poller);
match Event::elapsed_time(&self.start_event, &self.end_event) {
Ok(ms) => {
let elapsed_us = f64::from(ms) * 1000.0;
Poll::Ready(Ok(LaunchTiming { elapsed_us }))
}
Err(e) => Poll::Ready(Err(e)),
}
}
Ok(false) => {
self.ensure_poller(cx);
Poll::Pending
}
Err(e) => {
mark_poller_done(&self.poller);
Poll::Ready(Err(e))
}
}
}
}
impl Drop for TimedLaunchCompletion {
fn drop(&mut self) {
mark_poller_done(&self.poller);
}
}
pub struct AsyncKernel {
kernel: Kernel,
config: AsyncLaunchConfig,
}
impl AsyncKernel {
#[inline]
pub fn new(kernel: Kernel) -> Self {
Self {
kernel,
config: AsyncLaunchConfig::default(),
}
}
#[inline]
pub fn with_config(kernel: Kernel, config: AsyncLaunchConfig) -> Self {
Self { kernel, config }
}
#[inline]
pub fn kernel(&self) -> &Kernel {
&self.kernel
}
#[inline]
pub fn name(&self) -> &str {
self.kernel.name()
}
#[inline]
pub fn config(&self) -> &AsyncLaunchConfig {
&self.config
}
#[inline]
pub fn set_config(&mut self, config: AsyncLaunchConfig) {
self.config = config;
}
pub fn launch_async<A: KernelArgs>(
&self,
params: &LaunchParams,
stream: &Stream,
args: &A,
) -> CudaResult<LaunchCompletion> {
self.kernel.launch(params, stream, args)?;
let event = Event::new()?;
event.record(stream)?;
Ok(LaunchCompletion::new(event, &self.config))
}
pub fn launch_and_time_async<A: KernelArgs>(
&self,
params: &LaunchParams,
stream: &Stream,
args: &A,
) -> CudaResult<TimedLaunchCompletion> {
let start_event = Event::new()?;
start_event.record(stream)?;
self.kernel.launch(params, stream, args)?;
let end_event = Event::new()?;
end_event.record(stream)?;
Ok(TimedLaunchCompletion::new(
start_event,
end_event,
&self.config,
))
}
}
impl std::fmt::Debug for AsyncKernel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AsyncKernel")
.field("kernel", &self.kernel)
.field("config", &self.config)
.finish()
}
}
impl std::fmt::Display for AsyncKernel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "AsyncKernel({})", self.kernel.name())
}
}
pub fn multi_launch_async(
launches: &[(&Kernel, &LaunchParams)],
args_list: &[&dyn ErasedKernelArgs],
stream: &Stream,
config: &AsyncLaunchConfig,
) -> CudaResult<LaunchCompletion> {
for (i, (kernel, params)) in launches.iter().enumerate() {
let args = args_list.get(i).ok_or(CudaError::InvalidValue)?;
kernel.launch_erased(params, stream, *args)?;
}
let event = Event::new()?;
event.record(stream)?;
Ok(LaunchCompletion::new(event, config))
}
pub unsafe trait ErasedKernelArgs {
fn erased_param_ptrs(&self) -> Vec<*mut std::ffi::c_void>;
}
unsafe impl<T: KernelArgs> ErasedKernelArgs for T {
#[inline]
fn erased_param_ptrs(&self) -> Vec<*mut std::ffi::c_void> {
self.as_param_ptrs()
}
}
impl Kernel {
pub(crate) fn launch_erased(
&self,
params: &LaunchParams,
stream: &Stream,
args: &dyn ErasedKernelArgs,
) -> CudaResult<()> {
let driver = oxicuda_driver::loader::try_driver()?;
let mut param_ptrs = args.erased_param_ptrs();
oxicuda_driver::error::check(unsafe {
(driver.cu_launch_kernel)(
self.function().raw(),
params.grid.x,
params.grid.y,
params.grid.z,
params.block.x,
params.block.y,
params.block.z,
params.shared_mem_bytes,
stream.raw(),
param_ptrs.as_mut_ptr(),
std::ptr::null_mut(),
)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn completion_status_is_complete() {
let status = CompletionStatus::Complete;
assert!(status.is_complete());
assert!(!status.is_pending());
assert!(!status.is_error());
}
#[test]
fn completion_status_is_pending() {
let status = CompletionStatus::Pending;
assert!(status.is_pending());
assert!(!status.is_complete());
assert!(!status.is_error());
}
#[test]
fn completion_status_is_error() {
let status = CompletionStatus::Error("test error".to_string());
assert!(status.is_error());
assert!(!status.is_complete());
assert!(!status.is_pending());
}
#[test]
fn completion_status_display() {
assert_eq!(CompletionStatus::Pending.to_string(), "Pending");
assert_eq!(CompletionStatus::Complete.to_string(), "Complete");
assert_eq!(
CompletionStatus::Error("oops".to_string()).to_string(),
"Error: oops"
);
}
#[test]
fn completion_status_eq() {
assert_eq!(CompletionStatus::Pending, CompletionStatus::Pending);
assert_eq!(CompletionStatus::Complete, CompletionStatus::Complete);
assert_ne!(CompletionStatus::Pending, CompletionStatus::Complete);
assert_eq!(
CompletionStatus::Error("a".into()),
CompletionStatus::Error("a".into())
);
assert_ne!(
CompletionStatus::Error("a".into()),
CompletionStatus::Error("b".into())
);
}
#[test]
fn poll_strategy_default_is_yield() {
assert_eq!(PollStrategy::default(), PollStrategy::Yield);
}
#[test]
fn poll_strategy_backoff_value() {
let strategy = PollStrategy::BackoffMicros(100);
if let PollStrategy::BackoffMicros(us) = strategy {
assert_eq!(us, 100);
} else {
panic!("expected BackoffMicros");
}
}
#[test]
fn async_launch_config_default() {
let config = AsyncLaunchConfig::default();
assert_eq!(config.poll_strategy, PollStrategy::Yield);
assert!(config.timeout.is_none());
}
#[test]
fn async_launch_config_new() {
let config = AsyncLaunchConfig::new(PollStrategy::Spin);
assert_eq!(config.poll_strategy, PollStrategy::Spin);
assert!(config.timeout.is_none());
}
#[test]
fn async_launch_config_with_timeout() {
let config = AsyncLaunchConfig::new(PollStrategy::BackoffMicros(50))
.with_timeout(Duration::from_millis(500));
assert_eq!(config.poll_strategy, PollStrategy::BackoffMicros(50));
assert_eq!(config.timeout, Some(Duration::from_millis(500)));
}
#[test]
fn launch_timing_conversions() {
let timing = LaunchTiming {
elapsed_us: 1_500_000.0,
};
assert!((timing.elapsed_ms() - 1500.0).abs() < f64::EPSILON);
assert!((timing.elapsed_secs() - 1.5).abs() < f64::EPSILON);
}
#[test]
fn launch_timing_display_microseconds() {
let timing = LaunchTiming { elapsed_us: 42.5 };
let display = timing.to_string();
assert!(display.contains("us"), "expected 'us' in: {display}");
}
#[test]
fn launch_timing_display_milliseconds() {
let timing = LaunchTiming {
elapsed_us: 5_000.0,
};
let display = timing.to_string();
assert!(display.contains("ms"), "expected 'ms' in: {display}");
}
#[test]
fn launch_timing_display_seconds() {
let timing = LaunchTiming {
elapsed_us: 2_500_000.0,
};
let display = timing.to_string();
assert!(display.contains("s"), "expected 's' in: {display}");
assert!(
!display.contains("us"),
"should not contain 'us' in: {display}"
);
assert!(
!display.contains("ms"),
"should not contain 'ms' in: {display}"
);
}
#[test]
fn launch_timing_zero() {
let timing = LaunchTiming { elapsed_us: 0.0 };
assert!(timing.elapsed_ms().abs() < f64::EPSILON);
assert!(timing.elapsed_secs().abs() < f64::EPSILON);
assert!(timing.to_string().contains("us"));
}
#[test]
fn async_launch_status_pending_initially() {
let status = CompletionStatus::Pending;
assert!(status.is_pending(), "Newly created status must be Pending");
assert!(!status.is_complete());
assert!(!status.is_error());
}
#[test]
fn async_launch_debug_impl() {
let config = AsyncLaunchConfig::new(PollStrategy::Yield);
let dbg = format!("{config:?}");
assert!(
dbg.contains("AsyncLaunchConfig"),
"Debug output must contain type name, got: {dbg}"
);
let strategy_dbg = format!("{:?}", PollStrategy::BackoffMicros(200));
assert!(
strategy_dbg.contains("BackoffMicros"),
"PollStrategy Debug must contain variant name, got: {strategy_dbg}"
);
}
#[test]
fn async_completion_event_created() {
let config = AsyncLaunchConfig {
poll_strategy: PollStrategy::Spin,
timeout: Some(Duration::from_secs(5)),
};
assert_eq!(config.poll_strategy, PollStrategy::Spin);
assert_eq!(config.timeout, Some(Duration::from_secs(5)));
let config2 = AsyncLaunchConfig::new(PollStrategy::BackoffMicros(100))
.with_timeout(Duration::from_millis(250));
assert_eq!(config2.poll_strategy, PollStrategy::BackoffMicros(100));
assert_eq!(config2.timeout, Some(Duration::from_millis(250)));
}
#[cfg(feature = "gpu-tests")]
const BUSY_SPIN_PTX: &str = "\
.version 7.0
.target sm_70
.address_size 64
.visible .entry busy_spin(
.param .u64 scratch_ptr,
.param .u32 iters
)
{
.reg .b32 %r<4>;
.reg .b64 %rd<2>;
.reg .pred %p<2>;
ld.param.u64 %rd0, [scratch_ptr];
ld.param.u32 %r0, [iters];
mov.u32 %r1, 0;
$LOOP:
setp.ge.u32 %p0, %r1, %r0;
@%p0 bra $DONE;
ld.volatile.global.u32 %r2, [%rd0];
add.u32 %r2, %r2, 1;
st.volatile.global.u32 [%rd0], %r2;
add.u32 %r1, %r1, 1;
bra $LOOP;
$DONE:
ret;
}
";
#[cfg(feature = "gpu-tests")]
struct CountingWaker(std::sync::atomic::AtomicUsize);
#[cfg(feature = "gpu-tests")]
impl std::task::Wake for CountingWaker {
fn wake(self: Arc<Self>) {
self.wake_by_ref();
}
fn wake_by_ref(self: &Arc<Self>) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
#[cfg(feature = "gpu-tests")]
#[test]
fn launch_completion_yield_strategy_wakes_repeatedly_and_completes() {
let Ok(dev) = oxicuda_driver::device::Device::get(0) else {
return;
};
let ctx = match oxicuda_driver::context::Context::new(&dev) {
Ok(c) => Arc::new(c),
Err(_) => return,
};
let stream = match Stream::new(&ctx) {
Ok(s) => s,
Err(_) => return,
};
let module = match oxicuda_driver::module::Module::from_ptx(BUSY_SPIN_PTX) {
Ok(m) => Arc::new(m),
Err(_) => return,
};
let kernel = match Kernel::from_module(module, "busy_spin") {
Ok(k) => k,
Err(_) => return,
};
let scratch = match oxicuda_memory::DeviceBuffer::<u32>::zeroed(1) {
Ok(b) => b,
Err(_) => return,
};
let async_kernel =
AsyncKernel::with_config(kernel, AsyncLaunchConfig::new(PollStrategy::Yield));
let iters: u32 = 3_000_000;
let completion = match async_kernel.launch_async(
&LaunchParams::new(1u32, 1u32),
&stream,
&(scratch.as_device_ptr(), iters),
) {
Ok(c) => c,
Err(_) => return,
};
let counter = Arc::new(CountingWaker(std::sync::atomic::AtomicUsize::new(0)));
let waker = Waker::from(Arc::clone(&counter));
let mut cx = Context::from_waker(&waker);
let mut completion = Box::pin(completion);
let deadline = Instant::now() + Duration::from_secs(30);
let result = loop {
match completion.as_mut().poll(&mut cx) {
Poll::Ready(r) => break r,
Poll::Pending => {
assert!(
Instant::now() < deadline,
"LaunchCompletion under PollStrategy::Yield did not complete \
within the deadline (regression of the one-shot-waker hang)"
);
std::thread::sleep(Duration::from_millis(5));
}
}
};
result.expect("busy-spin kernel launch must succeed");
let wakes = counter.0.load(Ordering::SeqCst);
assert!(
wakes > 1,
"background poller must wake the executor more than once under \
PollStrategy::Yield, got {wakes} wake(s)"
);
}
#[cfg(feature = "gpu-tests")]
#[test]
fn launch_completion_drop_stops_background_poller() {
let Ok(dev) = oxicuda_driver::device::Device::get(0) else {
return;
};
let ctx = match oxicuda_driver::context::Context::new(&dev) {
Ok(c) => Arc::new(c),
Err(_) => return,
};
let stream = match Stream::new(&ctx) {
Ok(s) => s,
Err(_) => return,
};
let module = match oxicuda_driver::module::Module::from_ptx(BUSY_SPIN_PTX) {
Ok(m) => Arc::new(m),
Err(_) => return,
};
let kernel = match Kernel::from_module(module, "busy_spin") {
Ok(k) => k,
Err(_) => return,
};
let scratch = match oxicuda_memory::DeviceBuffer::<u32>::zeroed(1) {
Ok(b) => b,
Err(_) => return,
};
let async_kernel =
AsyncKernel::with_config(kernel, AsyncLaunchConfig::new(PollStrategy::Yield));
let iters: u32 = 20_000_000;
let completion = match async_kernel.launch_async(
&LaunchParams::new(1u32, 1u32),
&stream,
&(scratch.as_device_ptr(), iters),
) {
Ok(c) => c,
Err(_) => return,
};
let counter = Arc::new(CountingWaker(std::sync::atomic::AtomicUsize::new(0)));
let waker = Waker::from(Arc::clone(&counter));
let mut cx = Context::from_waker(&waker);
let mut completion = Box::pin(completion);
match completion.as_mut().poll(&mut cx) {
Poll::Pending => {}
Poll::Ready(_) => return,
}
std::thread::sleep(Duration::from_millis(50));
let before_drop = counter.0.load(Ordering::SeqCst);
assert!(
before_drop > 0,
"poller should have woken the executor at least once by now"
);
drop(completion);
std::thread::sleep(Duration::from_millis(150));
let settled = counter.0.load(Ordering::SeqCst);
std::thread::sleep(Duration::from_millis(150));
let after_settle = counter.0.load(Ordering::SeqCst);
stream
.synchronize()
.expect("stream sync after dropped completion");
assert_eq!(
settled, after_settle,
"background poller must stop waking once the future has been dropped"
);
}
#[cfg(feature = "gpu-tests")]
#[test]
fn timed_launch_completion_yield_strategy_completes() {
let Ok(dev) = oxicuda_driver::device::Device::get(0) else {
return;
};
let ctx = match oxicuda_driver::context::Context::new(&dev) {
Ok(c) => Arc::new(c),
Err(_) => return,
};
let stream = match Stream::new(&ctx) {
Ok(s) => s,
Err(_) => return,
};
let module = match oxicuda_driver::module::Module::from_ptx(BUSY_SPIN_PTX) {
Ok(m) => Arc::new(m),
Err(_) => return,
};
let kernel = match Kernel::from_module(module, "busy_spin") {
Ok(k) => k,
Err(_) => return,
};
let scratch = match oxicuda_memory::DeviceBuffer::<u32>::zeroed(1) {
Ok(b) => b,
Err(_) => return,
};
let async_kernel =
AsyncKernel::with_config(kernel, AsyncLaunchConfig::new(PollStrategy::Yield));
let iters: u32 = 3_000_000;
let completion = match async_kernel.launch_and_time_async(
&LaunchParams::new(1u32, 1u32),
&stream,
&(scratch.as_device_ptr(), iters),
) {
Ok(c) => c,
Err(_) => return,
};
let counter = Arc::new(CountingWaker(std::sync::atomic::AtomicUsize::new(0)));
let waker = Waker::from(counter);
let mut cx = Context::from_waker(&waker);
let mut completion = Box::pin(completion);
let deadline = Instant::now() + Duration::from_secs(30);
let timing = loop {
match completion.as_mut().poll(&mut cx) {
Poll::Ready(r) => break r,
Poll::Pending => {
assert!(
Instant::now() < deadline,
"TimedLaunchCompletion under PollStrategy::Yield did not \
complete within the deadline"
);
std::thread::sleep(Duration::from_millis(5));
}
}
}
.expect("busy-spin kernel launch must succeed");
assert!(timing.elapsed_us >= 0.0);
}
}