oxicuda_driver/stream.rs
1//! CUDA stream management.
2//!
3//! Streams are command queues on the GPU. Commands within a stream
4//! execute in order. Different streams can execute concurrently.
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! # use std::sync::Arc;
10//! # use oxicuda_driver::context::Context;
11//! # use oxicuda_driver::stream::Stream;
12//! # fn main() -> Result<(), oxicuda_driver::error::CudaError> {
13//! // Assuming `ctx` is an Arc<Context> obtained from Context::new(...)
14//! # let ctx: Arc<Context> = unimplemented!();
15//! let stream = Stream::new(&ctx)?;
16//! // ... enqueue work on the stream ...
17//! stream.synchronize()?;
18//! # Ok(())
19//! # }
20//! ```
21
22use std::sync::Arc;
23
24use crate::context::Context;
25use crate::error::CudaResult;
26use crate::event::Event;
27use crate::ffi::{CU_STREAM_NON_BLOCKING, CUcontext, CUstream};
28use crate::loader::try_driver;
29
30/// Creates a raw non-blocking stream **in `ctx`**, restoring the thread's
31/// previously-current context afterward.
32///
33/// `cuStreamCreate` targets whichever context is current on the calling
34/// thread, so a [`Stream`] that stores an [`Arc<Context>`] must make that
35/// context current for the duration of the create call — otherwise the stream
36/// would silently belong to some unrelated context that merely happened to be
37/// current, and work later enqueued on it would run in the wrong context
38/// (device pointers from `ctx` would be invalid there). The previous current
39/// context is captured and restored so this is transparent to the caller.
40fn create_stream_in_ctx(
41 api: &crate::loader::DriverApi,
42 ctx: &Context,
43 create: impl FnOnce(&mut CUstream) -> u32,
44) -> CudaResult<CUstream> {
45 // Capture the thread's current context (null if none) so we can restore it.
46 let mut prev = CUcontext::default();
47 // SAFETY: `cu_ctx_get_current` was resolved from the driver and `prev` is a
48 // valid out-pointer. A non-zero rc leaves `prev` null, so we restore to the
49 // "no context" state, which is the correct fallback.
50 let _ = unsafe { (api.cu_ctx_get_current)(&mut prev) };
51 // Bind `ctx` for the duration of the create call.
52 crate::cuda_call!((api.cu_ctx_set_current)(ctx.raw()))?;
53 let mut raw = CUstream::default();
54 let rc = create(&mut raw);
55 // Restore the previous context regardless of whether create succeeded.
56 // SAFETY: `prev` is either a context that was current a moment ago or null.
57 let _ = unsafe { (api.cu_ctx_set_current)(prev) };
58 crate::error::check(rc)?;
59 Ok(raw)
60}
61
62/// A CUDA stream (GPU command queue).
63///
64/// Streams provide ordered, asynchronous execution of GPU commands.
65/// Commands enqueued on the same stream execute sequentially, while
66/// commands on different streams may execute concurrently.
67///
68/// The stream holds an [`Arc<Context>`] to ensure the parent context
69/// outlives the stream.
70///
71/// # A `Stream` is a shared handle, not a unique owner
72///
73/// Cloning yields a second handle to the **same** queue — [`Stream::raw`]
74/// returns the same `CUstream` — and the queue is destroyed once the last
75/// handle drops. That is what lets two subsystems which each want to hold
76/// "their" stream be collapsed onto one queue: `oxicuda-dnn`'s `DnnHandle` and
77/// the `BlasHandle` nested inside it now share one, so a convolution's output
78/// is ordered before a GEMM that reads it by stream semantics alone — no
79/// event choreography, no host rendezvous, and a capture of the pair is a
80/// linear chain rather than a fork/join.
81///
82/// `Clone` is written out rather than derived so the doc comment can say what
83/// it means: this is another reference to one queue, not a copy of it.
84pub struct Stream {
85 /// The driver-owned queue, destroyed when the last handle drops.
86 inner: Arc<StreamInner>,
87}
88
89impl Clone for Stream {
90 /// Another handle to the same queue. See the type docs.
91 fn clone(&self) -> Self {
92 Self {
93 inner: Arc::clone(&self.inner),
94 }
95 }
96}
97
98/// The driver-owned half of a [`Stream`]: destroyed exactly once, when the
99/// last handle to the queue drops.
100struct StreamInner {
101 /// Raw CUDA stream handle.
102 raw: CUstream,
103 /// Keeps the parent context alive for the lifetime of the stream.
104 ctx: Arc<Context>,
105}
106
107// `Stream` is `Send + Sync` by auto-derivation from its fields: an
108// `Arc<StreamInner>` over a `CUstream` handle and an `Arc<Context>` (and
109// `Context` is itself `Send + Sync`). The CUDA Driver API is thread-safe, so
110// no manual `unsafe impl` is required.
111
112impl Stream {
113 /// Creates a new stream with [`CU_STREAM_NON_BLOCKING`] flag.
114 ///
115 /// Non-blocking streams do not implicitly synchronise with the
116 /// default (NULL) stream, allowing maximum concurrency.
117 ///
118 /// # Errors
119 ///
120 /// Returns a [`CudaError`](crate::error::CudaError) if the driver
121 /// call fails (e.g. invalid context, out of resources).
122 pub fn new(ctx: &Arc<Context>) -> CudaResult<Self> {
123 let api = try_driver()?;
124 // Bind the stream to `ctx` (not merely to whatever context happens to be
125 // current), matching the `Arc<Context>` this stream stores and keeps
126 // alive. See [`create_stream_in_ctx`].
127 let raw = create_stream_in_ctx(api, ctx, |raw| unsafe {
128 (api.cu_stream_create)(raw, CU_STREAM_NON_BLOCKING)
129 })?;
130 Ok(Self {
131 inner: Arc::new(StreamInner {
132 raw,
133 ctx: Arc::clone(ctx),
134 }),
135 })
136 }
137
138 /// Creates a new stream with the specified priority and
139 /// [`CU_STREAM_NON_BLOCKING`] flag.
140 ///
141 /// Lower numerical values indicate higher priority. The valid range
142 /// can be queried via `cuCtxGetStreamPriorityRange`.
143 ///
144 /// # Errors
145 ///
146 /// Returns a [`CudaError`](crate::error::CudaError) if the priority
147 /// is out of range or the driver call otherwise fails.
148 pub fn with_priority(ctx: &Arc<Context>, priority: i32) -> CudaResult<Self> {
149 let api = try_driver()?;
150 // Bind the stream to `ctx`; see [`Stream::new`] / [`create_stream_in_ctx`].
151 let raw = create_stream_in_ctx(api, ctx, |raw| unsafe {
152 (api.cu_stream_create_with_priority)(raw, CU_STREAM_NON_BLOCKING, priority)
153 })?;
154 Ok(Self {
155 inner: Arc::new(StreamInner {
156 raw,
157 ctx: Arc::clone(ctx),
158 }),
159 })
160 }
161
162 /// Blocks the calling thread until all previously enqueued commands
163 /// in this stream have completed.
164 ///
165 /// # Errors
166 ///
167 /// Returns a [`CudaError`](crate::error::CudaError) if any enqueued
168 /// operation failed or the driver reports an error.
169 pub fn synchronize(&self) -> CudaResult<()> {
170 let api = try_driver()?;
171 crate::cuda_call!((api.cu_stream_synchronize)(self.inner.raw))
172 }
173
174 /// Makes all future work submitted to this stream wait until
175 /// the given event has been recorded and completed.
176 ///
177 /// This is the primary mechanism for inter-stream synchronisation:
178 /// record an [`Event`] on one stream, then call `wait_event` on
179 /// another stream to establish an ordering dependency.
180 ///
181 /// # Errors
182 ///
183 /// Returns a [`CudaError`](crate::error::CudaError) if the driver
184 /// call fails (e.g. invalid event handle).
185 pub fn wait_event(&self, event: &Event) -> CudaResult<()> {
186 let api = try_driver()?;
187 // flags = 0 is the only documented value.
188 crate::cuda_call!((api.cu_stream_wait_event)(self.inner.raw, event.raw(), 0))
189 }
190
191 /// Returns the raw [`CUstream`] handle.
192 ///
193 /// # Safety (caller)
194 ///
195 /// The caller must not destroy or otherwise invalidate the handle
196 /// while this `Stream` is still alive.
197 #[inline]
198 pub fn raw(&self) -> CUstream {
199 self.inner.raw
200 }
201
202 /// Whether `self` and `other` are handles to the **same** driver queue.
203 ///
204 /// The question a caller asks before deciding that stream order alone
205 /// sequences two pieces of work: on one queue it does, on two it does not
206 /// and an event is required. Compares the driver handle rather than the
207 /// `Arc`, so a queue reached through two independently-built handles (were
208 /// that ever possible) still answers truthfully.
209 #[inline]
210 #[must_use]
211 pub fn is_same_queue(&self, other: &Self) -> bool {
212 self.inner.raw == other.inner.raw
213 }
214
215 /// Returns a reference to the parent [`Context`].
216 #[inline]
217 pub fn context(&self) -> &Arc<Context> {
218 &self.inner.ctx
219 }
220}
221
222impl Drop for StreamInner {
223 fn drop(&mut self) {
224 if let Ok(api) = try_driver() {
225 let rc = unsafe { (api.cu_stream_destroy_v2)(self.raw) };
226 if rc != 0 {
227 tracing::warn!(
228 cuda_error = rc,
229 stream = ?self.raw,
230 "cuStreamDestroy_v2 failed during drop"
231 );
232 }
233 }
234 }
235}
236
237#[cfg(test)]
238mod multi_stream_tests {
239 use super::*;
240 use crate::device::Device;
241 use crate::ffi::CUdeviceptr;
242 use crate::module::Module;
243 use std::ffi::c_void;
244
245 /// Grid-stride in-place doubling kernel, arch-portable (`.target sm_70`).
246 const DOUBLE_PTX: &str = "\
247.version 7.0
248.target sm_70
249.address_size 64
250.visible .entry dbl(
251 .param .u64 ptr,
252 .param .u32 n
253)
254{
255 .reg .b32 %r<8>;
256 .reg .b64 %rd<8>;
257 .reg .f32 %f<2>;
258 .reg .pred %p<2>;
259 ld.param.u64 %rd0, [ptr];
260 ld.param.u32 %r0, [n];
261 mov.u32 %r1, %ctaid.x;
262 mov.u32 %r2, %ntid.x;
263 mov.u32 %r3, %tid.x;
264 mad.lo.u32 %r4, %r1, %r2, %r3;
265 mov.u32 %r5, %nctaid.x;
266 mul.lo.u32 %r6, %r5, %r2;
267$LOOP:
268 setp.ge.u32 %p0, %r4, %r0;
269 @%p0 bra $DONE;
270 mul.wide.u32 %rd1, %r4, 4;
271 add.u64 %rd2, %rd0, %rd1;
272 ld.global.f32 %f0, [%rd2];
273 add.f32 %f0, %f0, %f0;
274 st.global.f32 [%rd2], %f0;
275 add.u32 %r4, %r4, %r6;
276 bra $LOOP;
277$DONE:
278 ret;
279}
280";
281
282 /// Launch the doubling kernel on `dptr` over `stream` (raw FFI).
283 fn launch_double(
284 api: &crate::loader::DriverApi,
285 func: &crate::module::Function,
286 stream: &Stream,
287 dptr: CUdeviceptr,
288 n: usize,
289 ) -> CudaResult<()> {
290 let mut dptr_arg = dptr;
291 let mut n_arg: u32 = n as u32;
292 let mut params: [*mut c_void; 2] = [
293 (&mut dptr_arg as *mut CUdeviceptr).cast(),
294 (&mut n_arg as *mut u32).cast(),
295 ];
296 crate::error::check(unsafe {
297 (api.cu_launch_kernel)(
298 func.raw(),
299 8,
300 1,
301 1,
302 128,
303 1,
304 1,
305 0,
306 stream.raw(),
307 params.as_mut_ptr(),
308 std::ptr::null_mut(),
309 )
310 })
311 }
312
313 /// Real-hardware multi-stream test: run the doubling kernel concurrently on
314 /// two independent streams over two buffers, plus a cross-stream dependency
315 /// (stream B waits on an event recorded on stream A before doubling a buffer
316 /// A already doubled — so it ends up x4). Verifies both the concurrent and
317 /// the ordered results. No-op without a GPU.
318 #[test]
319 fn two_streams_concurrent_and_cross_stream_event() {
320 let Ok(dev) = Device::get(0) else {
321 return;
322 };
323 let ctx = match Context::new(&dev) {
324 Ok(c) => Arc::new(c),
325 Err(_) => return,
326 };
327 let stream_a = match Stream::new(&ctx) {
328 Ok(s) => s,
329 Err(_) => return,
330 };
331 let stream_b = match Stream::new(&ctx) {
332 Ok(s) => s,
333 Err(_) => return,
334 };
335 let api = try_driver().expect("driver present");
336
337 let module = match Module::from_ptx(DOUBLE_PTX) {
338 Ok(m) => m,
339 Err(_) => return,
340 };
341 let func = module.get_function("dbl").expect("dbl");
342
343 const N: usize = 2048;
344 let bytes = N * std::mem::size_of::<f32>();
345 let a_in: Vec<f32> = (0..N).map(|i| i as f32).collect();
346 let b_in: Vec<f32> = (0..N).map(|i| i as f32 + 1000.0).collect();
347
348 let mut da: CUdeviceptr = 0;
349 let mut db: CUdeviceptr = 0;
350 crate::error::check(unsafe { (api.cu_mem_alloc_v2)(&mut da, bytes) }).expect("alloc a");
351 crate::error::check(unsafe { (api.cu_mem_alloc_v2)(&mut db, bytes) }).expect("alloc b");
352
353 let result = (|| -> CudaResult<(Vec<f32>, Vec<f32>)> {
354 crate::error::check(unsafe {
355 (api.cu_memcpy_htod_v2)(da, a_in.as_ptr().cast(), bytes)
356 })?;
357 crate::error::check(unsafe {
358 (api.cu_memcpy_htod_v2)(db, b_in.as_ptr().cast(), bytes)
359 })?;
360
361 // Concurrent: double A on stream A, double B on stream B.
362 launch_double(api, &func, &stream_a, da, N)?;
363 launch_double(api, &func, &stream_b, db, N)?;
364
365 // Cross-stream dependency: record an event on A after its kernel,
366 // make B wait on it, then double A again on B (A -> x4).
367 let evt = Event::new()?;
368 evt.record(&stream_a)?;
369 stream_b.wait_event(&evt)?;
370 launch_double(api, &func, &stream_b, da, N)?;
371
372 stream_a.synchronize()?;
373 stream_b.synchronize()?;
374
375 let mut a_out = vec![0.0f32; N];
376 let mut b_out = vec![0.0f32; N];
377 crate::error::check(unsafe {
378 (api.cu_memcpy_dtoh_v2)(a_out.as_mut_ptr().cast(), da, bytes)
379 })?;
380 crate::error::check(unsafe {
381 (api.cu_memcpy_dtoh_v2)(b_out.as_mut_ptr().cast(), db, bytes)
382 })?;
383 Ok((a_out, b_out))
384 })();
385
386 let _ = unsafe { (api.cu_mem_free_v2)(da) };
387 let _ = unsafe { (api.cu_mem_free_v2)(db) };
388
389 let (a_out, b_out) = result.expect("multi-stream round-trip");
390 // A was doubled twice (stream A, then stream B after the event) => x4.
391 for (i, &v) in a_out.iter().enumerate() {
392 assert!(
393 (v - 4.0 * i as f32).abs() <= 1e-4,
394 "stream A buffer element {i}: got {v}, expected {}",
395 4.0 * i as f32
396 );
397 }
398 // B was doubled once on stream B => x2.
399 for (i, &v) in b_out.iter().enumerate() {
400 let want = 2.0 * (i as f32 + 1000.0);
401 assert!(
402 (v - want).abs() <= 1e-3,
403 "stream B buffer element {i}: got {v}, expected {want}"
404 );
405 }
406 }
407}