Skip to main content

oxicuda_launch/
async_launch.rs

1//! Async kernel launch with completion futures.
2//!
3//! This module provides [`AsyncKernel`] for launching GPU kernels that
4//! return [`Future`]s, enabling integration with Rust's `async`/`await`
5//! ecosystem without depending on any specific async runtime.
6//!
7//! # Architecture
8//!
9//! Since the OxiCUDA driver crate does not expose CUDA callback
10//! registration, completion is detected by **polling**
11//! [`Event::query()`](oxicuda_driver::Event::query). The
12//! [`PollStrategy`] enum controls how aggressively the future polls:
13//!
14//! - [`Spin`](PollStrategy::Spin) — busy-poll with no yielding.
15//! - [`Yield`](PollStrategy::Yield) — call `std::thread::yield_now()`
16//!   between polls.
17//! - [`BackoffMicros`](PollStrategy::BackoffMicros) — sleep a fixed
18//!   number of microseconds between polls.
19//!
20//! # Example
21//!
22//! ```rust,no_run
23//! # use std::sync::Arc;
24//! # use oxicuda_driver::{Module, Stream, Context, Device};
25//! # use oxicuda_launch::{Kernel, LaunchParams, AsyncKernel, PollStrategy, AsyncLaunchConfig};
26//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
27//! # oxicuda_driver::init()?;
28//! # let dev = Device::get(0)?;
29//! # let ctx = Arc::new(Context::new(&dev)?);
30//! # let ptx = "";
31//! # let module = Arc::new(Module::from_ptx(ptx)?);
32//! # let kernel = Kernel::from_module(module, "my_kernel")?;
33//! let async_kernel = AsyncKernel::new(kernel);
34//! let stream = Stream::new(&ctx)?;
35//! let params = LaunchParams::new(4u32, 256u32);
36//!
37//! // Fire-and-await
38//! let completion = async_kernel.launch_async(&params, &stream, &(42u32,))?;
39//! completion.await?;
40//! # Ok(())
41//! # }
42//! ```
43
44use std::future::Future;
45use std::pin::Pin;
46use std::sync::atomic::{AtomicBool, Ordering};
47use std::sync::{Arc, Mutex};
48use std::task::{Context, Poll, Waker};
49use std::time::{Duration, Instant};
50
51use oxicuda_driver::error::{CudaError, CudaResult};
52use oxicuda_driver::event::Event;
53use oxicuda_driver::stream::Stream;
54
55use crate::kernel::{Kernel, KernelArgs};
56use crate::params::LaunchParams;
57
58// ---------------------------------------------------------------------------
59// CompletionStatus
60// ---------------------------------------------------------------------------
61
62/// Status of a GPU kernel completion.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum CompletionStatus {
65    /// The kernel has not yet completed.
66    Pending,
67    /// The kernel has completed successfully.
68    Complete,
69    /// An error occurred while querying completion.
70    Error(String),
71}
72
73impl CompletionStatus {
74    /// Returns `true` if the status is [`Complete`](Self::Complete).
75    #[inline]
76    pub fn is_complete(&self) -> bool {
77        matches!(self, Self::Complete)
78    }
79
80    /// Returns `true` if the status is [`Pending`](Self::Pending).
81    #[inline]
82    pub fn is_pending(&self) -> bool {
83        matches!(self, Self::Pending)
84    }
85
86    /// Returns `true` if the status is [`Error`](Self::Error).
87    #[inline]
88    pub fn is_error(&self) -> bool {
89        matches!(self, Self::Error(_))
90    }
91}
92
93impl std::fmt::Display for CompletionStatus {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        match self {
96            Self::Pending => write!(f, "Pending"),
97            Self::Complete => write!(f, "Complete"),
98            Self::Error(msg) => write!(f, "Error: {msg}"),
99        }
100    }
101}
102
103// ---------------------------------------------------------------------------
104// PollStrategy
105// ---------------------------------------------------------------------------
106
107/// Strategy for polling GPU event completion.
108///
109/// Controls the trade-off between CPU usage and latency when waiting
110/// for a kernel to finish.
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum PollStrategy {
113    /// Busy-poll `event.query()` with no pause between polls.
114    ///
115    /// Lowest latency but highest CPU usage.
116    Spin,
117
118    /// Call [`std::thread::yield_now()`] between polls.
119    ///
120    /// Allows other threads to run but still polls frequently.
121    Yield,
122
123    /// Sleep for the given number of microseconds between polls.
124    ///
125    /// Lower CPU usage at the cost of higher latency.
126    BackoffMicros(u64),
127}
128
129impl Default for PollStrategy {
130    /// Defaults to [`Yield`](PollStrategy::Yield) for a balanced
131    /// trade-off between latency and CPU usage.
132    #[inline]
133    fn default() -> Self {
134        Self::Yield
135    }
136}
137
138// ---------------------------------------------------------------------------
139// AsyncLaunchConfig
140// ---------------------------------------------------------------------------
141
142/// Configuration for async kernel launch behaviour.
143#[derive(Debug, Clone)]
144pub struct AsyncLaunchConfig {
145    /// Strategy for polling event completion.
146    pub poll_strategy: PollStrategy,
147    /// Optional maximum time to wait before the future resolves with
148    /// a timeout error.
149    pub timeout: Option<Duration>,
150}
151
152impl Default for AsyncLaunchConfig {
153    /// Default config: [`PollStrategy::Yield`], no timeout.
154    #[inline]
155    fn default() -> Self {
156        Self {
157            poll_strategy: PollStrategy::Yield,
158            timeout: None,
159        }
160    }
161}
162
163impl AsyncLaunchConfig {
164    /// Creates a new config with the given poll strategy and no timeout.
165    #[inline]
166    pub fn new(poll_strategy: PollStrategy) -> Self {
167        Self {
168            poll_strategy,
169            timeout: None,
170        }
171    }
172
173    /// Sets the timeout duration.
174    #[inline]
175    pub fn with_timeout(mut self, timeout: Duration) -> Self {
176        self.timeout = Some(timeout);
177        self
178    }
179}
180
181// ---------------------------------------------------------------------------
182// LaunchTiming
183// ---------------------------------------------------------------------------
184
185/// Timing information for a completed kernel launch.
186#[derive(Debug, Clone, Copy, PartialEq)]
187pub struct LaunchTiming {
188    /// Elapsed GPU time in microseconds.
189    pub elapsed_us: f64,
190}
191
192impl LaunchTiming {
193    /// Returns the elapsed time in milliseconds.
194    #[inline]
195    pub fn elapsed_ms(&self) -> f64 {
196        self.elapsed_us / 1000.0
197    }
198
199    /// Returns the elapsed time in seconds.
200    #[inline]
201    pub fn elapsed_secs(&self) -> f64 {
202        self.elapsed_us / 1_000_000.0
203    }
204}
205
206impl std::fmt::Display for LaunchTiming {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        if self.elapsed_us < 1000.0 {
209            write!(f, "{:.2} us", self.elapsed_us)
210        } else if self.elapsed_us < 1_000_000.0 {
211            write!(f, "{:.3} ms", self.elapsed_ms())
212        } else {
213            write!(f, "{:.4} s", self.elapsed_secs())
214        }
215    }
216}
217
218// ---------------------------------------------------------------------------
219// PollerShared — background re-waking thread shared by both completion futures
220// ---------------------------------------------------------------------------
221
222/// State shared between a completion future and its background poller
223/// thread.
224///
225/// A single thread (spawned lazily on the first [`Poll::Pending`]) loops
226/// according to the future's [`PollStrategy`], repeatedly re-waking the
227/// most recently stored [`Waker`] until `done` is set — either because the
228/// future resolved, or because it was dropped before resolving.
229///
230/// This replaces a one-shot wake: waking the executor exactly once is not
231/// sufficient for [`PollStrategy::Yield`] or [`PollStrategy::BackoffMicros`]
232/// to ever make further progress, since nothing else would schedule a
233/// second poll and the future would hang forever whenever the GPU work
234/// outlives that single wake.
235struct PollerShared {
236    /// Set once the future has resolved or been dropped, telling the
237    /// background thread to stop looping.
238    done: AtomicBool,
239    /// The waker to re-invoke on each interval; refreshed by the future
240    /// whenever the executor hands it a different one.
241    waker: Mutex<Waker>,
242}
243
244impl PollerShared {
245    /// Creates the shared state, seeded with the waker from the first
246    /// [`Poll::Pending`].
247    fn new(waker: Waker) -> Self {
248        Self {
249            done: AtomicBool::new(false),
250            waker: Mutex::new(waker),
251        }
252    }
253
254    /// Marks the poller as done, so its background thread exits on its
255    /// next loop iteration instead of continuing to re-wake forever.
256    fn mark_done(&self) {
257        self.done.store(true, Ordering::Release);
258    }
259
260    /// Replaces the stored waker if the executor has handed us a
261    /// different one since the last poll (e.g. the task moved to another
262    /// executor thread).
263    fn refresh_waker(&self, cx: &Context<'_>) {
264        let mut stored = self
265            .waker
266            .lock()
267            .unwrap_or_else(|poisoned| poisoned.into_inner());
268        if !stored.will_wake(cx.waker()) {
269            *stored = cx.waker().clone();
270        }
271    }
272}
273
274/// Marks `poller` as done, if a background poller thread was ever spawned
275/// for this future (i.e. it was polled at least once while still pending).
276fn mark_poller_done(poller: &Option<Arc<PollerShared>>) {
277    if let Some(shared) = poller {
278        shared.mark_done();
279    }
280}
281
282/// Spawns the single background thread backing `shared`, looping
283/// according to `strategy` and re-waking `shared`'s stored waker on every
284/// interval until `shared.done` is set.
285///
286/// * [`PollStrategy::Spin`] — no pause between wakes (one busy-looping
287///   thread rather than the previous thread-per-poll spin).
288/// * [`PollStrategy::Yield`] — [`std::thread::yield_now`] between wakes.
289/// * [`PollStrategy::BackoffMicros`] — sleeps the given interval between
290///   wakes.
291fn spawn_poller_thread(shared: Arc<PollerShared>, strategy: PollStrategy) {
292    std::thread::spawn(move || {
293        loop {
294            match strategy {
295                PollStrategy::Spin => {}
296                PollStrategy::Yield => std::thread::yield_now(),
297                PollStrategy::BackoffMicros(us) => std::thread::sleep(Duration::from_micros(us)),
298            }
299            if shared.done.load(Ordering::Acquire) {
300                break;
301            }
302            let waker = shared
303                .waker
304                .lock()
305                .unwrap_or_else(|poisoned| poisoned.into_inner());
306            waker.wake_by_ref();
307        }
308    });
309}
310
311// ---------------------------------------------------------------------------
312// LaunchCompletion
313// ---------------------------------------------------------------------------
314
315/// A [`Future`] that resolves when a GPU kernel finishes execution.
316///
317/// Created by [`AsyncKernel::launch_async`]. The future polls the
318/// underlying CUDA event to detect completion.
319pub struct LaunchCompletion {
320    /// The event recorded after kernel launch.
321    event: Event,
322    /// Poll strategy.
323    strategy: PollStrategy,
324    /// Optional timeout.
325    timeout: Option<Duration>,
326    /// When the future was first polled (lazily initialised).
327    start_time: Option<Instant>,
328    /// Background poller thread's shared state, created on the first
329    /// [`Poll::Pending`].
330    poller: Option<Arc<PollerShared>>,
331}
332
333impl LaunchCompletion {
334    /// Creates a new completion future wrapping the given event.
335    fn new(event: Event, config: &AsyncLaunchConfig) -> Self {
336        Self {
337            event,
338            strategy: config.poll_strategy,
339            timeout: config.timeout,
340            start_time: None,
341            poller: None,
342        }
343    }
344
345    /// Queries the current completion status without consuming the future.
346    pub fn status(&self) -> CompletionStatus {
347        match self.event.query() {
348            Ok(true) => CompletionStatus::Complete,
349            Ok(false) => CompletionStatus::Pending,
350            Err(e) => CompletionStatus::Error(e.to_string()),
351        }
352    }
353
354    /// Checks whether the timeout (if any) has been exceeded.
355    fn check_timeout(&self) -> bool {
356        match (self.timeout, self.start_time) {
357            (Some(timeout), Some(start)) => start.elapsed() >= timeout,
358            _ => false,
359        }
360    }
361
362    /// Ensures a background poller thread is running for this future,
363    /// spawning it on the first [`Poll::Pending`] and refreshing the
364    /// stored waker on every subsequent one.
365    fn ensure_poller(&mut self, cx: &Context<'_>) {
366        match &self.poller {
367            None => {
368                let shared = Arc::new(PollerShared::new(cx.waker().clone()));
369                spawn_poller_thread(Arc::clone(&shared), self.strategy);
370                self.poller = Some(shared);
371            }
372            Some(shared) => shared.refresh_waker(cx),
373        }
374    }
375}
376
377impl Future for LaunchCompletion {
378    type Output = CudaResult<()>;
379
380    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
381        // Initialise start time on first poll.
382        if self.start_time.is_none() {
383            self.start_time = Some(Instant::now());
384        }
385
386        // Check timeout.
387        if self.check_timeout() {
388            mark_poller_done(&self.poller);
389            return Poll::Ready(Err(CudaError::Timeout));
390        }
391
392        // Query the event.
393        match self.event.query() {
394            Ok(true) => {
395                mark_poller_done(&self.poller);
396                Poll::Ready(Ok(()))
397            }
398            Ok(false) => {
399                self.ensure_poller(cx);
400                Poll::Pending
401            }
402            Err(e) => {
403                mark_poller_done(&self.poller);
404                Poll::Ready(Err(e))
405            }
406        }
407    }
408}
409
410impl Drop for LaunchCompletion {
411    /// Signals the background poller thread (if any) to stop, so it does
412    /// not keep looping forever after the future is dropped without
413    /// resolving.
414    fn drop(&mut self) {
415        mark_poller_done(&self.poller);
416    }
417}
418
419// ---------------------------------------------------------------------------
420// TimedLaunchCompletion
421// ---------------------------------------------------------------------------
422
423/// A [`Future`] that resolves to [`LaunchTiming`] when a GPU kernel
424/// finishes, measuring elapsed GPU time via CUDA events.
425pub struct TimedLaunchCompletion {
426    /// Event recorded before the kernel launch.
427    start_event: Event,
428    /// Event recorded after the kernel launch.
429    end_event: Event,
430    /// Poll strategy.
431    strategy: PollStrategy,
432    /// Optional timeout.
433    timeout: Option<Duration>,
434    /// When the future was first polled.
435    start_time: Option<Instant>,
436    /// Background poller thread's shared state, created on the first
437    /// [`Poll::Pending`].
438    poller: Option<Arc<PollerShared>>,
439}
440
441impl TimedLaunchCompletion {
442    /// Creates a new timed completion future.
443    fn new(start_event: Event, end_event: Event, config: &AsyncLaunchConfig) -> Self {
444        Self {
445            start_event,
446            end_event,
447            strategy: config.poll_strategy,
448            timeout: config.timeout,
449            start_time: None,
450            poller: None,
451        }
452    }
453
454    /// Queries the current completion status.
455    pub fn status(&self) -> CompletionStatus {
456        match self.end_event.query() {
457            Ok(true) => CompletionStatus::Complete,
458            Ok(false) => CompletionStatus::Pending,
459            Err(e) => CompletionStatus::Error(e.to_string()),
460        }
461    }
462
463    /// Checks whether the timeout has been exceeded.
464    fn check_timeout(&self) -> bool {
465        match (self.timeout, self.start_time) {
466            (Some(timeout), Some(start)) => start.elapsed() >= timeout,
467            _ => false,
468        }
469    }
470
471    /// Ensures a background poller thread is running for this future,
472    /// spawning it on the first [`Poll::Pending`] and refreshing the
473    /// stored waker on every subsequent one.
474    fn ensure_poller(&mut self, cx: &Context<'_>) {
475        match &self.poller {
476            None => {
477                let shared = Arc::new(PollerShared::new(cx.waker().clone()));
478                spawn_poller_thread(Arc::clone(&shared), self.strategy);
479                self.poller = Some(shared);
480            }
481            Some(shared) => shared.refresh_waker(cx),
482        }
483    }
484}
485
486impl Future for TimedLaunchCompletion {
487    type Output = CudaResult<LaunchTiming>;
488
489    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
490        if self.start_time.is_none() {
491            self.start_time = Some(Instant::now());
492        }
493
494        if self.check_timeout() {
495            mark_poller_done(&self.poller);
496            return Poll::Ready(Err(CudaError::Timeout));
497        }
498
499        match self.end_event.query() {
500            Ok(true) => {
501                mark_poller_done(&self.poller);
502                // Kernel complete — compute elapsed time.
503                match Event::elapsed_time(&self.start_event, &self.end_event) {
504                    Ok(ms) => {
505                        let elapsed_us = f64::from(ms) * 1000.0;
506                        Poll::Ready(Ok(LaunchTiming { elapsed_us }))
507                    }
508                    Err(e) => Poll::Ready(Err(e)),
509                }
510            }
511            Ok(false) => {
512                self.ensure_poller(cx);
513                Poll::Pending
514            }
515            Err(e) => {
516                mark_poller_done(&self.poller);
517                Poll::Ready(Err(e))
518            }
519        }
520    }
521}
522
523impl Drop for TimedLaunchCompletion {
524    /// Signals the background poller thread (if any) to stop, so it does
525    /// not keep looping forever after the future is dropped without
526    /// resolving.
527    fn drop(&mut self) {
528        mark_poller_done(&self.poller);
529    }
530}
531
532// ---------------------------------------------------------------------------
533// AsyncKernel
534// ---------------------------------------------------------------------------
535
536/// A kernel wrapper with async launch capability.
537///
538/// Wraps a [`Kernel`] and provides methods that return [`Future`]s
539/// resolving when the GPU work completes.
540pub struct AsyncKernel {
541    /// The underlying kernel.
542    kernel: Kernel,
543    /// Configuration for async behaviour.
544    config: AsyncLaunchConfig,
545}
546
547impl AsyncKernel {
548    /// Creates a new `AsyncKernel` with default configuration.
549    #[inline]
550    pub fn new(kernel: Kernel) -> Self {
551        Self {
552            kernel,
553            config: AsyncLaunchConfig::default(),
554        }
555    }
556
557    /// Creates a new `AsyncKernel` with the given configuration.
558    #[inline]
559    pub fn with_config(kernel: Kernel, config: AsyncLaunchConfig) -> Self {
560        Self { kernel, config }
561    }
562
563    /// Returns a reference to the underlying [`Kernel`].
564    #[inline]
565    pub fn kernel(&self) -> &Kernel {
566        &self.kernel
567    }
568
569    /// Returns the kernel function name.
570    #[inline]
571    pub fn name(&self) -> &str {
572        self.kernel.name()
573    }
574
575    /// Returns a reference to the current [`AsyncLaunchConfig`].
576    #[inline]
577    pub fn config(&self) -> &AsyncLaunchConfig {
578        &self.config
579    }
580
581    /// Updates the async configuration.
582    #[inline]
583    pub fn set_config(&mut self, config: AsyncLaunchConfig) {
584        self.config = config;
585    }
586
587    /// Launches the kernel and returns a [`LaunchCompletion`] future.
588    ///
589    /// The kernel is launched asynchronously on the given stream, then
590    /// a CUDA event is recorded. The returned future polls that event
591    /// until it completes.
592    ///
593    /// # Errors
594    ///
595    /// Returns a [`CudaError`] if the kernel launch or event operations
596    /// fail. The future itself can also resolve to an error if the event
597    /// query fails later.
598    pub fn launch_async<A: KernelArgs>(
599        &self,
600        params: &LaunchParams,
601        stream: &Stream,
602        args: &A,
603    ) -> CudaResult<LaunchCompletion> {
604        // Launch the kernel.
605        self.kernel.launch(params, stream, args)?;
606
607        // Record an event after the launch.
608        let event = Event::new()?;
609        event.record(stream)?;
610
611        Ok(LaunchCompletion::new(event, &self.config))
612    }
613
614    /// Launches the kernel and returns a [`TimedLaunchCompletion`] future
615    /// that resolves to [`LaunchTiming`] with elapsed GPU time.
616    ///
617    /// Two events are recorded: one before and one after the kernel
618    /// launch. When the future resolves, the elapsed time between the
619    /// two events is computed.
620    ///
621    /// # Errors
622    ///
623    /// Returns a [`CudaError`] if the launch or event operations fail.
624    pub fn launch_and_time_async<A: KernelArgs>(
625        &self,
626        params: &LaunchParams,
627        stream: &Stream,
628        args: &A,
629    ) -> CudaResult<TimedLaunchCompletion> {
630        let start_event = Event::new()?;
631        start_event.record(stream)?;
632
633        self.kernel.launch(params, stream, args)?;
634
635        let end_event = Event::new()?;
636        end_event.record(stream)?;
637
638        Ok(TimedLaunchCompletion::new(
639            start_event,
640            end_event,
641            &self.config,
642        ))
643    }
644}
645
646impl std::fmt::Debug for AsyncKernel {
647    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
648        f.debug_struct("AsyncKernel")
649            .field("kernel", &self.kernel)
650            .field("config", &self.config)
651            .finish()
652    }
653}
654
655impl std::fmt::Display for AsyncKernel {
656    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
657        write!(f, "AsyncKernel({})", self.kernel.name())
658    }
659}
660
661// ---------------------------------------------------------------------------
662// multi_launch_async
663// ---------------------------------------------------------------------------
664
665/// Launches multiple kernels on the same stream and returns a combined
666/// [`LaunchCompletion`] future that resolves when **all** have finished.
667///
668/// A single event is recorded after all kernels have been enqueued,
669/// so the future resolves once the last kernel in the batch completes.
670///
671/// # Parameters
672///
673/// * `launches` — a slice of `(&Kernel, &LaunchParams, param_ptrs)` tuples.
674///   Each entry's `param_ptrs` is the result of calling
675///   [`KernelArgs::as_param_ptrs()`] on the kernel's arguments.
676/// * `stream` — the stream on which to enqueue all kernels.
677/// * `config` — async launch configuration.
678///
679/// # Errors
680///
681/// Returns the first [`CudaError`] encountered during any kernel launch
682/// or event operation.
683pub fn multi_launch_async(
684    launches: &[(&Kernel, &LaunchParams)],
685    args_list: &[&dyn ErasedKernelArgs],
686    stream: &Stream,
687    config: &AsyncLaunchConfig,
688) -> CudaResult<LaunchCompletion> {
689    for (i, (kernel, params)) in launches.iter().enumerate() {
690        let args = args_list.get(i).ok_or(CudaError::InvalidValue)?;
691        kernel.launch_erased(params, stream, *args)?;
692    }
693
694    let event = Event::new()?;
695    event.record(stream)?;
696
697    Ok(LaunchCompletion::new(event, config))
698}
699
700// ---------------------------------------------------------------------------
701// ErasedKernelArgs — object-safe wrapper
702// ---------------------------------------------------------------------------
703
704/// Object-safe trait for kernel arguments, enabling heterogeneous
705/// argument lists in [`multi_launch_async`].
706///
707/// # Safety
708///
709/// Implementors must ensure the returned pointers are valid for the
710/// duration of the kernel launch call.
711pub unsafe trait ErasedKernelArgs {
712    /// Convert arguments to void pointers.
713    fn erased_param_ptrs(&self) -> Vec<*mut std::ffi::c_void>;
714}
715
716/// Blanket implementation: every `KernelArgs` is also `ErasedKernelArgs`.
717///
718/// # Safety
719///
720/// Delegates to the underlying [`KernelArgs::as_param_ptrs`].
721unsafe impl<T: KernelArgs> ErasedKernelArgs for T {
722    #[inline]
723    fn erased_param_ptrs(&self) -> Vec<*mut std::ffi::c_void> {
724        self.as_param_ptrs()
725    }
726}
727
728// ---------------------------------------------------------------------------
729// Kernel::launch_erased — internal helper
730// ---------------------------------------------------------------------------
731
732impl Kernel {
733    /// Launches the kernel with erased (object-safe) arguments.
734    ///
735    /// This is an internal helper for [`multi_launch_async`].
736    pub(crate) fn launch_erased(
737        &self,
738        params: &LaunchParams,
739        stream: &Stream,
740        args: &dyn ErasedKernelArgs,
741    ) -> CudaResult<()> {
742        let driver = oxicuda_driver::loader::try_driver()?;
743        let mut param_ptrs = args.erased_param_ptrs();
744        oxicuda_driver::error::check(unsafe {
745            (driver.cu_launch_kernel)(
746                self.function().raw(),
747                params.grid.x,
748                params.grid.y,
749                params.grid.z,
750                params.block.x,
751                params.block.y,
752                params.block.z,
753                params.shared_mem_bytes,
754                stream.raw(),
755                param_ptrs.as_mut_ptr(),
756                std::ptr::null_mut(),
757            )
758        })
759    }
760}
761
762// ---------------------------------------------------------------------------
763// Tests
764// ---------------------------------------------------------------------------
765
766#[cfg(test)]
767mod tests {
768    use super::*;
769
770    // -- CompletionStatus tests --
771
772    #[test]
773    fn completion_status_is_complete() {
774        let status = CompletionStatus::Complete;
775        assert!(status.is_complete());
776        assert!(!status.is_pending());
777        assert!(!status.is_error());
778    }
779
780    #[test]
781    fn completion_status_is_pending() {
782        let status = CompletionStatus::Pending;
783        assert!(status.is_pending());
784        assert!(!status.is_complete());
785        assert!(!status.is_error());
786    }
787
788    #[test]
789    fn completion_status_is_error() {
790        let status = CompletionStatus::Error("test error".to_string());
791        assert!(status.is_error());
792        assert!(!status.is_complete());
793        assert!(!status.is_pending());
794    }
795
796    #[test]
797    fn completion_status_display() {
798        assert_eq!(CompletionStatus::Pending.to_string(), "Pending");
799        assert_eq!(CompletionStatus::Complete.to_string(), "Complete");
800        assert_eq!(
801            CompletionStatus::Error("oops".to_string()).to_string(),
802            "Error: oops"
803        );
804    }
805
806    #[test]
807    fn completion_status_eq() {
808        assert_eq!(CompletionStatus::Pending, CompletionStatus::Pending);
809        assert_eq!(CompletionStatus::Complete, CompletionStatus::Complete);
810        assert_ne!(CompletionStatus::Pending, CompletionStatus::Complete);
811        assert_eq!(
812            CompletionStatus::Error("a".into()),
813            CompletionStatus::Error("a".into())
814        );
815        assert_ne!(
816            CompletionStatus::Error("a".into()),
817            CompletionStatus::Error("b".into())
818        );
819    }
820
821    // -- PollStrategy tests --
822
823    #[test]
824    fn poll_strategy_default_is_yield() {
825        assert_eq!(PollStrategy::default(), PollStrategy::Yield);
826    }
827
828    #[test]
829    fn poll_strategy_backoff_value() {
830        let strategy = PollStrategy::BackoffMicros(100);
831        if let PollStrategy::BackoffMicros(us) = strategy {
832            assert_eq!(us, 100);
833        } else {
834            panic!("expected BackoffMicros");
835        }
836    }
837
838    // -- AsyncLaunchConfig tests --
839
840    #[test]
841    fn async_launch_config_default() {
842        let config = AsyncLaunchConfig::default();
843        assert_eq!(config.poll_strategy, PollStrategy::Yield);
844        assert!(config.timeout.is_none());
845    }
846
847    #[test]
848    fn async_launch_config_new() {
849        let config = AsyncLaunchConfig::new(PollStrategy::Spin);
850        assert_eq!(config.poll_strategy, PollStrategy::Spin);
851        assert!(config.timeout.is_none());
852    }
853
854    #[test]
855    fn async_launch_config_with_timeout() {
856        let config = AsyncLaunchConfig::new(PollStrategy::BackoffMicros(50))
857            .with_timeout(Duration::from_millis(500));
858        assert_eq!(config.poll_strategy, PollStrategy::BackoffMicros(50));
859        assert_eq!(config.timeout, Some(Duration::from_millis(500)));
860    }
861
862    // -- LaunchTiming tests --
863
864    #[test]
865    fn launch_timing_conversions() {
866        let timing = LaunchTiming {
867            elapsed_us: 1_500_000.0,
868        };
869        assert!((timing.elapsed_ms() - 1500.0).abs() < f64::EPSILON);
870        assert!((timing.elapsed_secs() - 1.5).abs() < f64::EPSILON);
871    }
872
873    #[test]
874    fn launch_timing_display_microseconds() {
875        let timing = LaunchTiming { elapsed_us: 42.5 };
876        let display = timing.to_string();
877        assert!(display.contains("us"), "expected 'us' in: {display}");
878    }
879
880    #[test]
881    fn launch_timing_display_milliseconds() {
882        let timing = LaunchTiming {
883            elapsed_us: 5_000.0,
884        };
885        let display = timing.to_string();
886        assert!(display.contains("ms"), "expected 'ms' in: {display}");
887    }
888
889    #[test]
890    fn launch_timing_display_seconds() {
891        let timing = LaunchTiming {
892            elapsed_us: 2_500_000.0,
893        };
894        let display = timing.to_string();
895        assert!(display.contains("s"), "expected 's' in: {display}");
896        assert!(
897            !display.contains("us"),
898            "should not contain 'us' in: {display}"
899        );
900        assert!(
901            !display.contains("ms"),
902            "should not contain 'ms' in: {display}"
903        );
904    }
905
906    #[test]
907    fn launch_timing_zero() {
908        let timing = LaunchTiming { elapsed_us: 0.0 };
909        assert!(timing.elapsed_ms().abs() < f64::EPSILON);
910        assert!(timing.elapsed_secs().abs() < f64::EPSILON);
911        assert!(timing.to_string().contains("us"));
912    }
913
914    // ---------------------------------------------------------------------------
915    // Quality gate tests (CPU-only)
916    // ---------------------------------------------------------------------------
917
918    #[test]
919    fn async_launch_status_pending_initially() {
920        // CompletionStatus::Pending represents "not yet completed".
921        // Verify initial/constructed status is Pending and passes is_pending().
922        let status = CompletionStatus::Pending;
923        assert!(status.is_pending(), "Newly created status must be Pending");
924        assert!(!status.is_complete());
925        assert!(!status.is_error());
926    }
927
928    #[test]
929    fn async_launch_debug_impl() {
930        // AsyncLaunchConfig implements Debug — verify it does not panic.
931        let config = AsyncLaunchConfig::new(PollStrategy::Yield);
932        let dbg = format!("{config:?}");
933        assert!(
934            dbg.contains("AsyncLaunchConfig"),
935            "Debug output must contain type name, got: {dbg}"
936        );
937        // PollStrategy also implements Debug
938        let strategy_dbg = format!("{:?}", PollStrategy::BackoffMicros(200));
939        assert!(
940            strategy_dbg.contains("BackoffMicros"),
941            "PollStrategy Debug must contain variant name, got: {strategy_dbg}"
942        );
943    }
944
945    #[test]
946    fn async_completion_event_created() {
947        // Creating an AsyncLaunchConfig produces a valid struct with the fields
948        // expected by the async launch machinery.
949        let config = AsyncLaunchConfig {
950            poll_strategy: PollStrategy::Spin,
951            timeout: Some(Duration::from_secs(5)),
952        };
953        assert_eq!(config.poll_strategy, PollStrategy::Spin);
954        assert_eq!(config.timeout, Some(Duration::from_secs(5)));
955
956        // with_timeout builder chain also works
957        let config2 = AsyncLaunchConfig::new(PollStrategy::BackoffMicros(100))
958            .with_timeout(Duration::from_millis(250));
959        assert_eq!(config2.poll_strategy, PollStrategy::BackoffMicros(100));
960        assert_eq!(config2.timeout, Some(Duration::from_millis(250)));
961    }
962
963    // ---------------------------------------------------------------------------
964    // On-device regression tests (F030): the one-shot poller must not hang the
965    // future forever under PollStrategy::Yield/BackoffMicros.
966    // ---------------------------------------------------------------------------
967
968    /// A device-side busy-spin kernel: a single thread repeatedly performs a
969    /// **volatile** global-memory read-modify-write on the same scratch
970    /// address, `iters` times. Used to keep a real CUDA event `Pending` for
971    /// a measurable, tunable amount of wall-clock time so the completion
972    /// futures actually observe multiple poll cycles before resolving.
973    ///
974    /// The loop body must have a genuine, non-eliminable side effect: an
975    /// earlier version that only incremented a register (with no memory
976    /// traffic) let the JIT compiler algebraically collapse the whole loop
977    /// into a single closed-form store, since it could prove no external
978    /// observer could see the intermediate values — making the kernel
979    /// finish instantly instead of taking real time. The `volatile`
980    /// qualifier on the load/store forbids that transformation and forces
981    /// one genuine dependent DRAM round trip per iteration.
982    #[cfg(feature = "gpu-tests")]
983    const BUSY_SPIN_PTX: &str = "\
984.version 7.0
985.target sm_70
986.address_size 64
987.visible .entry busy_spin(
988    .param .u64 scratch_ptr,
989    .param .u32 iters
990)
991{
992    .reg .b32 %r<4>;
993    .reg .b64 %rd<2>;
994    .reg .pred %p<2>;
995    ld.param.u64 %rd0, [scratch_ptr];
996    ld.param.u32 %r0, [iters];
997    mov.u32 %r1, 0;
998$LOOP:
999    setp.ge.u32 %p0, %r1, %r0;
1000    @%p0 bra $DONE;
1001    ld.volatile.global.u32 %r2, [%rd0];
1002    add.u32 %r2, %r2, 1;
1003    st.volatile.global.u32 [%rd0], %r2;
1004    add.u32 %r1, %r1, 1;
1005    bra $LOOP;
1006$DONE:
1007    ret;
1008}
1009";
1010
1011    /// A minimal wake counter: a [`std::task::Wake`] implementation that
1012    /// just counts invocations, used to detect whether the background
1013    /// poller thread wakes the executor repeatedly (fixed behaviour) or
1014    /// only once (the regression this test guards against).
1015    #[cfg(feature = "gpu-tests")]
1016    struct CountingWaker(std::sync::atomic::AtomicUsize);
1017
1018    #[cfg(feature = "gpu-tests")]
1019    impl std::task::Wake for CountingWaker {
1020        fn wake(self: Arc<Self>) {
1021            self.wake_by_ref();
1022        }
1023        fn wake_by_ref(self: &Arc<Self>) {
1024            self.0.fetch_add(1, Ordering::SeqCst);
1025        }
1026    }
1027
1028    /// Regression test for the bug where `LaunchCompletion`'s background
1029    /// poller only woke the executor **once**: under
1030    /// [`PollStrategy::Yield`], a real async executor that parks the task
1031    /// until woken (rather than busy-polling) would then never be told to
1032    /// poll again, and would hang forever whenever the GPU work outlives
1033    /// that single wake. This drives the future with a manual executor
1034    /// that self-polls on a short interval (so it does not itself rely on
1035    /// the wake signal to make progress — that would risk a hang if the
1036    /// fix regressed) while independently counting wake-ups; the fixed
1037    /// implementation must wake the executor *more than once* over the
1038    /// lifetime of a kernel slow enough to take multiple poll cycles.
1039    /// Self-skips if there is no GPU/driver.
1040    #[cfg(feature = "gpu-tests")]
1041    #[test]
1042    fn launch_completion_yield_strategy_wakes_repeatedly_and_completes() {
1043        let Ok(dev) = oxicuda_driver::device::Device::get(0) else {
1044            return;
1045        };
1046        let ctx = match oxicuda_driver::context::Context::new(&dev) {
1047            Ok(c) => Arc::new(c),
1048            Err(_) => return,
1049        };
1050        let stream = match Stream::new(&ctx) {
1051            Ok(s) => s,
1052            Err(_) => return,
1053        };
1054        let module = match oxicuda_driver::module::Module::from_ptx(BUSY_SPIN_PTX) {
1055            Ok(m) => Arc::new(m),
1056            Err(_) => return,
1057        };
1058        let kernel = match Kernel::from_module(module, "busy_spin") {
1059            Ok(k) => k,
1060            Err(_) => return,
1061        };
1062        let scratch = match oxicuda_memory::DeviceBuffer::<u32>::zeroed(1) {
1063            Ok(b) => b,
1064            Err(_) => return,
1065        };
1066
1067        let async_kernel =
1068            AsyncKernel::with_config(kernel, AsyncLaunchConfig::new(PollStrategy::Yield));
1069        // Enough dependent volatile-memory round trips to keep the kernel
1070        // busy for a measurable stretch of wall-clock time, so multiple
1071        // poll cycles happen before it completes.
1072        let iters: u32 = 3_000_000;
1073        let completion = match async_kernel.launch_async(
1074            &LaunchParams::new(1u32, 1u32),
1075            &stream,
1076            &(scratch.as_device_ptr(), iters),
1077        ) {
1078            Ok(c) => c,
1079            Err(_) => return,
1080        };
1081
1082        let counter = Arc::new(CountingWaker(std::sync::atomic::AtomicUsize::new(0)));
1083        let waker = Waker::from(Arc::clone(&counter));
1084        let mut cx = Context::from_waker(&waker);
1085        let mut completion = Box::pin(completion);
1086
1087        let deadline = Instant::now() + Duration::from_secs(30);
1088        let result = loop {
1089            match completion.as_mut().poll(&mut cx) {
1090                Poll::Ready(r) => break r,
1091                Poll::Pending => {
1092                    assert!(
1093                        Instant::now() < deadline,
1094                        "LaunchCompletion under PollStrategy::Yield did not complete \
1095                         within the deadline (regression of the one-shot-waker hang)"
1096                    );
1097                    std::thread::sleep(Duration::from_millis(5));
1098                }
1099            }
1100        };
1101        result.expect("busy-spin kernel launch must succeed");
1102
1103        // The key regression signal: the background poller must wake the
1104        // executor more than once. With the old one-shot poller, `counter`
1105        // would never exceed 1 regardless of how long the kernel runs.
1106        let wakes = counter.0.load(Ordering::SeqCst);
1107        assert!(
1108            wakes > 1,
1109            "background poller must wake the executor more than once under \
1110             PollStrategy::Yield, got {wakes} wake(s)"
1111        );
1112    }
1113
1114    /// Regression test for the `Drop` half of the fix: once a pending
1115    /// completion future is dropped, its background poller thread must
1116    /// stop looping (and thus stop waking) instead of spinning/yielding
1117    /// forever in the background. Self-skips if there is no GPU/driver.
1118    #[cfg(feature = "gpu-tests")]
1119    #[test]
1120    fn launch_completion_drop_stops_background_poller() {
1121        let Ok(dev) = oxicuda_driver::device::Device::get(0) else {
1122            return;
1123        };
1124        let ctx = match oxicuda_driver::context::Context::new(&dev) {
1125            Ok(c) => Arc::new(c),
1126            Err(_) => return,
1127        };
1128        let stream = match Stream::new(&ctx) {
1129            Ok(s) => s,
1130            Err(_) => return,
1131        };
1132        let module = match oxicuda_driver::module::Module::from_ptx(BUSY_SPIN_PTX) {
1133            Ok(m) => Arc::new(m),
1134            Err(_) => return,
1135        };
1136        let kernel = match Kernel::from_module(module, "busy_spin") {
1137            Ok(k) => k,
1138            Err(_) => return,
1139        };
1140        let scratch = match oxicuda_memory::DeviceBuffer::<u32>::zeroed(1) {
1141            Ok(b) => b,
1142            Err(_) => return,
1143        };
1144
1145        let async_kernel =
1146            AsyncKernel::with_config(kernel, AsyncLaunchConfig::new(PollStrategy::Yield));
1147        // A long-running kernel so it is essentially guaranteed to still be
1148        // pending immediately after the first poll.
1149        let iters: u32 = 20_000_000;
1150        let completion = match async_kernel.launch_async(
1151            &LaunchParams::new(1u32, 1u32),
1152            &stream,
1153            &(scratch.as_device_ptr(), iters),
1154        ) {
1155            Ok(c) => c,
1156            Err(_) => return,
1157        };
1158
1159        let counter = Arc::new(CountingWaker(std::sync::atomic::AtomicUsize::new(0)));
1160        let waker = Waker::from(Arc::clone(&counter));
1161        let mut cx = Context::from_waker(&waker);
1162        let mut completion = Box::pin(completion);
1163
1164        match completion.as_mut().poll(&mut cx) {
1165            Poll::Pending => {}
1166            // Finished implausibly fast on this device — nothing to
1167            // observe, skip rather than risk a flaky failure.
1168            Poll::Ready(_) => return,
1169        }
1170
1171        std::thread::sleep(Duration::from_millis(50));
1172        let before_drop = counter.0.load(Ordering::SeqCst);
1173        assert!(
1174            before_drop > 0,
1175            "poller should have woken the executor at least once by now"
1176        );
1177
1178        drop(completion);
1179
1180        // Give the background thread time to observe `done` and exit.
1181        std::thread::sleep(Duration::from_millis(150));
1182        let settled = counter.0.load(Ordering::SeqCst);
1183        std::thread::sleep(Duration::from_millis(150));
1184        let after_settle = counter.0.load(Ordering::SeqCst);
1185
1186        // The completion future's Drop only stops our host-side polling
1187        // machinery — the kernel itself (and its writes to `scratch`) keep
1188        // running on the device. Let it actually finish before `stream`
1189        // and `scratch` are dropped below, regardless of the assertion
1190        // outcome, so we never free device memory (or destroy the stream)
1191        // out from under an in-flight kernel.
1192        stream
1193            .synchronize()
1194            .expect("stream sync after dropped completion");
1195
1196        assert_eq!(
1197            settled, after_settle,
1198            "background poller must stop waking once the future has been dropped"
1199        );
1200    }
1201
1202    /// Smoke test that `TimedLaunchCompletion` — which shares the same
1203    /// `PollerShared`/`spawn_poller_thread` machinery — also resolves
1204    /// under `PollStrategy::Yield` instead of hanging. Self-skips if there
1205    /// is no GPU/driver.
1206    #[cfg(feature = "gpu-tests")]
1207    #[test]
1208    fn timed_launch_completion_yield_strategy_completes() {
1209        let Ok(dev) = oxicuda_driver::device::Device::get(0) else {
1210            return;
1211        };
1212        let ctx = match oxicuda_driver::context::Context::new(&dev) {
1213            Ok(c) => Arc::new(c),
1214            Err(_) => return,
1215        };
1216        let stream = match Stream::new(&ctx) {
1217            Ok(s) => s,
1218            Err(_) => return,
1219        };
1220        let module = match oxicuda_driver::module::Module::from_ptx(BUSY_SPIN_PTX) {
1221            Ok(m) => Arc::new(m),
1222            Err(_) => return,
1223        };
1224        let kernel = match Kernel::from_module(module, "busy_spin") {
1225            Ok(k) => k,
1226            Err(_) => return,
1227        };
1228        let scratch = match oxicuda_memory::DeviceBuffer::<u32>::zeroed(1) {
1229            Ok(b) => b,
1230            Err(_) => return,
1231        };
1232
1233        let async_kernel =
1234            AsyncKernel::with_config(kernel, AsyncLaunchConfig::new(PollStrategy::Yield));
1235        let iters: u32 = 3_000_000;
1236        let completion = match async_kernel.launch_and_time_async(
1237            &LaunchParams::new(1u32, 1u32),
1238            &stream,
1239            &(scratch.as_device_ptr(), iters),
1240        ) {
1241            Ok(c) => c,
1242            Err(_) => return,
1243        };
1244
1245        let counter = Arc::new(CountingWaker(std::sync::atomic::AtomicUsize::new(0)));
1246        let waker = Waker::from(counter);
1247        let mut cx = Context::from_waker(&waker);
1248        let mut completion = Box::pin(completion);
1249
1250        let deadline = Instant::now() + Duration::from_secs(30);
1251        let timing = loop {
1252            match completion.as_mut().poll(&mut cx) {
1253                Poll::Ready(r) => break r,
1254                Poll::Pending => {
1255                    assert!(
1256                        Instant::now() < deadline,
1257                        "TimedLaunchCompletion under PollStrategy::Yield did not \
1258                         complete within the deadline"
1259                    );
1260                    std::thread::sleep(Duration::from_millis(5));
1261                }
1262            }
1263        }
1264        .expect("busy-spin kernel launch must succeed");
1265        assert!(timing.elapsed_us >= 0.0);
1266    }
1267}