Skip to main content

oxicuda_driver/
event.rs

1//! CUDA event management for timing and synchronisation.
2//!
3//! Events can be recorded on a stream and used to measure elapsed time
4//! between GPU operations or to synchronise streams.
5//!
6//! # Timing example
7//!
8//! ```rust,no_run
9//! # use std::sync::Arc;
10//! # use oxicuda_driver::event::Event;
11//! # use oxicuda_driver::stream::Stream;
12//! # use oxicuda_driver::context::Context;
13//! # fn main() -> Result<(), oxicuda_driver::error::CudaError> {
14//! # let ctx: Arc<Context> = unimplemented!();
15//! let stream = Stream::new(&ctx)?;
16//! let start = Event::new()?;
17//! let end = Event::new()?;
18//!
19//! start.record(&stream)?;
20//! // ... enqueue GPU work on `stream` ...
21//! end.record(&stream)?;
22//! end.synchronize()?;
23//!
24//! let ms = Event::elapsed_time(&start, &end)?;
25//! println!("kernel took {ms:.3} ms");
26//! # Ok(())
27//! # }
28//! ```
29
30use crate::error::CudaResult;
31use crate::ffi::{CU_EVENT_DEFAULT, CUevent};
32use crate::loader::try_driver;
33use crate::stream::Stream;
34
35/// A CUDA event for timing and synchronisation.
36///
37/// Events are lightweight markers that can be recorded into a
38/// [`Stream`]. They support two primary use-cases:
39///
40/// 1. **Timing** — measure elapsed GPU time between two recorded events
41///    via [`Event::elapsed_time`].
42/// 2. **Synchronisation** — make one stream wait for work recorded in
43///    another stream via [`Stream::wait_event`].
44pub struct Event {
45    /// Raw CUDA event handle.
46    raw: CUevent,
47    /// The context that owned this event at creation, used to skip the driver
48    /// destroy if that context was torn down first (avoids a use-after-free).
49    /// `None` when no tracked context was current — see
50    /// [`crate::context::current_ctx_owner`].
51    owner: crate::context::CtxOwner,
52}
53
54// `Event` is `Send + Sync` by auto-derivation: its only field is a `CUevent`
55// handle (a plain driver-side identifier). The CUDA Driver API is thread-safe,
56// so no manual `unsafe impl` is required.
57
58impl Event {
59    /// Creates a new event with [`CU_EVENT_DEFAULT`] flags.
60    ///
61    /// Default events record timing data. Use [`Event::with_flags`] to
62    /// create events with different characteristics (e.g. disable timing
63    /// for lower overhead).
64    ///
65    /// # Errors
66    ///
67    /// Returns a [`CudaError`](crate::error::CudaError) if the driver
68    /// call fails.
69    pub fn new() -> CudaResult<Self> {
70        Self::with_flags(CU_EVENT_DEFAULT)
71    }
72
73    /// Creates a new event with the specified flags.
74    ///
75    /// Common flag values (from [`crate::ffi`]):
76    ///
77    /// | Constant                  | Value | Description                    |
78    /// |---------------------------|-------|--------------------------------|
79    /// | `CU_EVENT_DEFAULT`        | 0     | Default (records timing)       |
80    /// | `CU_EVENT_BLOCKING_SYNC`  | 1     | Use blocking synchronisation   |
81    /// | `CU_EVENT_DISABLE_TIMING` | 2     | Disable timing (lower overhead)|
82    /// | `CU_EVENT_INTERPROCESS`   | 4     | Usable across processes        |
83    ///
84    /// Flags can be combined with bitwise OR.
85    ///
86    /// # Errors
87    ///
88    /// Returns a [`CudaError`](crate::error::CudaError) if the flags
89    /// are invalid or the driver call otherwise fails.
90    pub fn with_flags(flags: u32) -> CudaResult<Self> {
91        let api = try_driver()?;
92        let mut raw = CUevent::default();
93        crate::cuda_call!((api.cu_event_create)(&mut raw, flags))?;
94        Ok(Self {
95            raw,
96            owner: crate::context::current_ctx_owner(),
97        })
98    }
99
100    /// Records this event on the given stream.
101    ///
102    /// The event captures the point in the stream's command queue at
103    /// which it was recorded. Subsequent calls to [`Event::synchronize`]
104    /// or [`Event::elapsed_time`] reference this recorded point.
105    ///
106    /// # Errors
107    ///
108    /// Returns a [`CudaError`](crate::error::CudaError) if the stream
109    /// or event handle is invalid.
110    pub fn record(&self, stream: &Stream) -> CudaResult<()> {
111        let api = try_driver()?;
112        crate::cuda_call!((api.cu_event_record)(self.raw, stream.raw()))
113    }
114
115    /// Queries whether this event has completed.
116    ///
117    /// Returns `Ok(true)` if the event (and all preceding work in its
118    /// stream) has completed, `Ok(false)` if it is still pending.
119    ///
120    /// # Errors
121    ///
122    /// Returns a [`CudaError`](crate::error::CudaError) if the event
123    /// was not recorded or an unexpected driver error occurs (errors
124    /// other than `NotReady`).
125    pub fn query(&self) -> CudaResult<bool> {
126        let api = try_driver()?;
127        let rc = unsafe { (api.cu_event_query)(self.raw) };
128        if rc == 0 {
129            Ok(true)
130        } else if rc == crate::ffi::CUDA_ERROR_NOT_READY {
131            Ok(false)
132        } else {
133            Err(crate::error::CudaError::from_raw(rc))
134        }
135    }
136
137    /// Blocks the calling thread until this event has been recorded
138    /// and all preceding work in its stream has completed.
139    ///
140    /// # Errors
141    ///
142    /// Returns a [`CudaError`](crate::error::CudaError) if the event
143    /// was not recorded or the driver reports an error.
144    pub fn synchronize(&self) -> CudaResult<()> {
145        let api = try_driver()?;
146        crate::cuda_call!((api.cu_event_synchronize)(self.raw))
147    }
148
149    /// Computes the elapsed time in milliseconds between two recorded
150    /// events.
151    ///
152    /// Both `start` and `end` must have been previously recorded on a
153    /// stream, and `end` must have completed (e.g. via
154    /// [`Event::synchronize`]).
155    ///
156    /// # Errors
157    ///
158    /// Returns a [`CudaError`](crate::error::CudaError) if either event
159    /// has not been recorded, or if timing data is not available (e.g.
160    /// the events were created with `CU_EVENT_DISABLE_TIMING`).
161    pub fn elapsed_time(start: &Event, end: &Event) -> CudaResult<f32> {
162        let api = try_driver()?;
163        let mut ms: f32 = 0.0;
164        crate::cuda_call!((api.cu_event_elapsed_time)(&mut ms, start.raw, end.raw))?;
165        Ok(ms)
166    }
167
168    /// Returns the raw [`CUevent`] handle.
169    ///
170    /// # Safety (caller)
171    ///
172    /// The caller must not destroy or otherwise invalidate the handle
173    /// while this `Event` is still alive.
174    #[inline]
175    pub fn raw(&self) -> CUevent {
176        self.raw
177    }
178}
179
180impl Drop for Event {
181    fn drop(&mut self) {
182        // Hold the registry lock across the destroy, and skip it entirely if
183        // the owning context was already torn down (its `cuCtxDestroy` already
184        // freed this event — calling `cuEventDestroy` again would be a
185        // use-after-free).
186        let map = crate::context::lock_live_ctxs();
187        if !crate::context::owner_is_live(&map, self.owner) {
188            return;
189        }
190        if let Ok(api) = try_driver() {
191            let rc = unsafe { (api.cu_event_destroy_v2)(self.raw) };
192            if rc != 0 {
193                tracing::warn!(
194                    cuda_error = rc,
195                    event = ?self.raw,
196                    "cuEventDestroy_v2 failed during drop"
197                );
198            }
199        }
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::context::Context;
207    use crate::device::Device;
208    use crate::ffi::CUdeviceptr;
209
210    /// Real-hardware event timing: record two timing events around a real
211    /// stream operation and assert `cuEventElapsedTime` returns a finite,
212    /// non-negative duration. No-op when no GPU is present.
213    #[test]
214    fn event_elapsed_time_on_real_device() {
215        let Ok(dev) = Device::get(0) else {
216            return;
217        };
218        let ctx = match Context::new(&dev) {
219            Ok(c) => std::sync::Arc::new(c),
220            Err(_) => return,
221        };
222        let stream = match Stream::new(&ctx) {
223            Ok(s) => s,
224            Err(_) => return,
225        };
226        let api = try_driver().expect("driver present");
227
228        let start = Event::new().expect("start event");
229        let end = Event::new().expect("end event");
230
231        // A real device allocation gives the timed stream op something to do.
232        const N: usize = 1 << 16;
233        let bytes = N * std::mem::size_of::<u32>();
234        let mut dptr: CUdeviceptr = 0;
235        crate::error::check(unsafe { (api.cu_mem_alloc_v2)(&mut dptr, bytes) }).expect("alloc");
236
237        let timed = || -> CudaResult<f32> {
238            start.record(&stream)?;
239            // Prefer the async memset so the work is enqueued on the timed
240            // stream between the two events.
241            if let Some(memset_async) = api.cu_memset_d32_async {
242                crate::error::check(unsafe { memset_async(dptr, 0x7, N, stream.raw()) })?;
243            } else {
244                crate::error::check(unsafe { (api.cu_memset_d32_v2)(dptr, 0x7, N) })?;
245            }
246            end.record(&stream)?;
247            end.synchronize()?;
248            Event::elapsed_time(&start, &end)
249        };
250
251        let result = timed();
252        let _ = unsafe { (api.cu_mem_free_v2)(dptr) };
253
254        let ms = result.expect("elapsed time");
255        assert!(ms.is_finite(), "elapsed time must be finite, got {ms}");
256        assert!(ms >= 0.0, "elapsed time must be non-negative, got {ms}");
257    }
258}