1use 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#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum CompletionStatus {
65 Pending,
67 Complete,
69 Error(String),
71}
72
73impl CompletionStatus {
74 #[inline]
76 pub fn is_complete(&self) -> bool {
77 matches!(self, Self::Complete)
78 }
79
80 #[inline]
82 pub fn is_pending(&self) -> bool {
83 matches!(self, Self::Pending)
84 }
85
86 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum PollStrategy {
113 Spin,
117
118 Yield,
122
123 BackoffMicros(u64),
127}
128
129impl Default for PollStrategy {
130 #[inline]
133 fn default() -> Self {
134 Self::Yield
135 }
136}
137
138#[derive(Debug, Clone)]
144pub struct AsyncLaunchConfig {
145 pub poll_strategy: PollStrategy,
147 pub timeout: Option<Duration>,
150}
151
152impl Default for AsyncLaunchConfig {
153 #[inline]
155 fn default() -> Self {
156 Self {
157 poll_strategy: PollStrategy::Yield,
158 timeout: None,
159 }
160 }
161}
162
163impl AsyncLaunchConfig {
164 #[inline]
166 pub fn new(poll_strategy: PollStrategy) -> Self {
167 Self {
168 poll_strategy,
169 timeout: None,
170 }
171 }
172
173 #[inline]
175 pub fn with_timeout(mut self, timeout: Duration) -> Self {
176 self.timeout = Some(timeout);
177 self
178 }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq)]
187pub struct LaunchTiming {
188 pub elapsed_us: f64,
190}
191
192impl LaunchTiming {
193 #[inline]
195 pub fn elapsed_ms(&self) -> f64 {
196 self.elapsed_us / 1000.0
197 }
198
199 #[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
218struct PollerShared {
236 done: AtomicBool,
239 waker: Mutex<Waker>,
242}
243
244impl PollerShared {
245 fn new(waker: Waker) -> Self {
248 Self {
249 done: AtomicBool::new(false),
250 waker: Mutex::new(waker),
251 }
252 }
253
254 fn mark_done(&self) {
257 self.done.store(true, Ordering::Release);
258 }
259
260 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
274fn mark_poller_done(poller: &Option<Arc<PollerShared>>) {
277 if let Some(shared) = poller {
278 shared.mark_done();
279 }
280}
281
282fn 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
311pub struct LaunchCompletion {
320 event: Event,
322 strategy: PollStrategy,
324 timeout: Option<Duration>,
326 start_time: Option<Instant>,
328 poller: Option<Arc<PollerShared>>,
331}
332
333impl LaunchCompletion {
334 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 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 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 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 if self.start_time.is_none() {
383 self.start_time = Some(Instant::now());
384 }
385
386 if self.check_timeout() {
388 mark_poller_done(&self.poller);
389 return Poll::Ready(Err(CudaError::Timeout));
390 }
391
392 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 fn drop(&mut self) {
415 mark_poller_done(&self.poller);
416 }
417}
418
419pub struct TimedLaunchCompletion {
426 start_event: Event,
428 end_event: Event,
430 strategy: PollStrategy,
432 timeout: Option<Duration>,
434 start_time: Option<Instant>,
436 poller: Option<Arc<PollerShared>>,
439}
440
441impl TimedLaunchCompletion {
442 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 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 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 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 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 fn drop(&mut self) {
528 mark_poller_done(&self.poller);
529 }
530}
531
532pub struct AsyncKernel {
541 kernel: Kernel,
543 config: AsyncLaunchConfig,
545}
546
547impl AsyncKernel {
548 #[inline]
550 pub fn new(kernel: Kernel) -> Self {
551 Self {
552 kernel,
553 config: AsyncLaunchConfig::default(),
554 }
555 }
556
557 #[inline]
559 pub fn with_config(kernel: Kernel, config: AsyncLaunchConfig) -> Self {
560 Self { kernel, config }
561 }
562
563 #[inline]
565 pub fn kernel(&self) -> &Kernel {
566 &self.kernel
567 }
568
569 #[inline]
571 pub fn name(&self) -> &str {
572 self.kernel.name()
573 }
574
575 #[inline]
577 pub fn config(&self) -> &AsyncLaunchConfig {
578 &self.config
579 }
580
581 #[inline]
583 pub fn set_config(&mut self, config: AsyncLaunchConfig) {
584 self.config = config;
585 }
586
587 pub fn launch_async<A: KernelArgs>(
599 &self,
600 params: &LaunchParams,
601 stream: &Stream,
602 args: &A,
603 ) -> CudaResult<LaunchCompletion> {
604 self.kernel.launch(params, stream, args)?;
606
607 let event = Event::new()?;
609 event.record(stream)?;
610
611 Ok(LaunchCompletion::new(event, &self.config))
612 }
613
614 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
661pub 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
700pub unsafe trait ErasedKernelArgs {
712 fn erased_param_ptrs(&self) -> Vec<*mut std::ffi::c_void>;
714}
715
716unsafe 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
728impl Kernel {
733 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#[cfg(test)]
767mod tests {
768 use super::*;
769
770 #[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 #[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 #[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 #[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 #[test]
919 fn async_launch_status_pending_initially() {
920 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 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 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 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 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 #[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 #[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 #[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 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 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 #[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 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 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 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 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 #[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}