Skip to main content

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}