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}