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.
70pub struct Stream {
71    /// Raw CUDA stream handle.
72    raw: CUstream,
73    /// Keeps the parent context alive for the lifetime of the stream.
74    ctx: Arc<Context>,
75}
76
77// `Stream` is `Send + Sync` by auto-derivation from its fields: a `CUstream`
78// handle and an `Arc<Context>` (and `Context` is itself `Send + Sync`). The
79// CUDA Driver API is thread-safe, so no manual `unsafe impl` is required.
80
81impl Stream {
82    /// Creates a new stream with [`CU_STREAM_NON_BLOCKING`] flag.
83    ///
84    /// Non-blocking streams do not implicitly synchronise with the
85    /// default (NULL) stream, allowing maximum concurrency.
86    ///
87    /// # Errors
88    ///
89    /// Returns a [`CudaError`](crate::error::CudaError) if the driver
90    /// call fails (e.g. invalid context, out of resources).
91    pub fn new(ctx: &Arc<Context>) -> CudaResult<Self> {
92        let api = try_driver()?;
93        // Bind the stream to `ctx` (not merely to whatever context happens to be
94        // current), matching the `Arc<Context>` this stream stores and keeps
95        // alive. See [`create_stream_in_ctx`].
96        let raw = create_stream_in_ctx(api, ctx, |raw| unsafe {
97            (api.cu_stream_create)(raw, CU_STREAM_NON_BLOCKING)
98        })?;
99        Ok(Self {
100            raw,
101            ctx: Arc::clone(ctx),
102        })
103    }
104
105    /// Creates a new stream with the specified priority and
106    /// [`CU_STREAM_NON_BLOCKING`] flag.
107    ///
108    /// Lower numerical values indicate higher priority. The valid range
109    /// can be queried via `cuCtxGetStreamPriorityRange`.
110    ///
111    /// # Errors
112    ///
113    /// Returns a [`CudaError`](crate::error::CudaError) if the priority
114    /// is out of range or the driver call otherwise fails.
115    pub fn with_priority(ctx: &Arc<Context>, priority: i32) -> CudaResult<Self> {
116        let api = try_driver()?;
117        // Bind the stream to `ctx`; see [`Stream::new`] / [`create_stream_in_ctx`].
118        let raw = create_stream_in_ctx(api, ctx, |raw| unsafe {
119            (api.cu_stream_create_with_priority)(raw, CU_STREAM_NON_BLOCKING, priority)
120        })?;
121        Ok(Self {
122            raw,
123            ctx: Arc::clone(ctx),
124        })
125    }
126
127    /// Blocks the calling thread until all previously enqueued commands
128    /// in this stream have completed.
129    ///
130    /// # Errors
131    ///
132    /// Returns a [`CudaError`](crate::error::CudaError) if any enqueued
133    /// operation failed or the driver reports an error.
134    pub fn synchronize(&self) -> CudaResult<()> {
135        let api = try_driver()?;
136        crate::cuda_call!((api.cu_stream_synchronize)(self.raw))
137    }
138
139    /// Makes all future work submitted to this stream wait until
140    /// the given event has been recorded and completed.
141    ///
142    /// This is the primary mechanism for inter-stream synchronisation:
143    /// record an [`Event`] on one stream, then call `wait_event` on
144    /// another stream to establish an ordering dependency.
145    ///
146    /// # Errors
147    ///
148    /// Returns a [`CudaError`](crate::error::CudaError) if the driver
149    /// call fails (e.g. invalid event handle).
150    pub fn wait_event(&self, event: &Event) -> CudaResult<()> {
151        let api = try_driver()?;
152        // flags = 0 is the only documented value.
153        crate::cuda_call!((api.cu_stream_wait_event)(self.raw, event.raw(), 0))
154    }
155
156    /// Returns the raw [`CUstream`] handle.
157    ///
158    /// # Safety (caller)
159    ///
160    /// The caller must not destroy or otherwise invalidate the handle
161    /// while this `Stream` is still alive.
162    #[inline]
163    pub fn raw(&self) -> CUstream {
164        self.raw
165    }
166
167    /// Returns a reference to the parent [`Context`].
168    #[inline]
169    pub fn context(&self) -> &Arc<Context> {
170        &self.ctx
171    }
172}
173
174impl Drop for Stream {
175    fn drop(&mut self) {
176        if let Ok(api) = try_driver() {
177            let rc = unsafe { (api.cu_stream_destroy_v2)(self.raw) };
178            if rc != 0 {
179                tracing::warn!(
180                    cuda_error = rc,
181                    stream = ?self.raw,
182                    "cuStreamDestroy_v2 failed during drop"
183                );
184            }
185        }
186    }
187}
188
189#[cfg(test)]
190mod multi_stream_tests {
191    use super::*;
192    use crate::device::Device;
193    use crate::ffi::CUdeviceptr;
194    use crate::module::Module;
195    use std::ffi::c_void;
196
197    /// Grid-stride in-place doubling kernel, arch-portable (`.target sm_70`).
198    const DOUBLE_PTX: &str = "\
199.version 7.0
200.target sm_70
201.address_size 64
202.visible .entry dbl(
203    .param .u64 ptr,
204    .param .u32 n
205)
206{
207    .reg .b32 %r<8>;
208    .reg .b64 %rd<8>;
209    .reg .f32 %f<2>;
210    .reg .pred %p<2>;
211    ld.param.u64 %rd0, [ptr];
212    ld.param.u32 %r0, [n];
213    mov.u32 %r1, %ctaid.x;
214    mov.u32 %r2, %ntid.x;
215    mov.u32 %r3, %tid.x;
216    mad.lo.u32 %r4, %r1, %r2, %r3;
217    mov.u32 %r5, %nctaid.x;
218    mul.lo.u32 %r6, %r5, %r2;
219$LOOP:
220    setp.ge.u32 %p0, %r4, %r0;
221    @%p0 bra $DONE;
222    mul.wide.u32 %rd1, %r4, 4;
223    add.u64 %rd2, %rd0, %rd1;
224    ld.global.f32 %f0, [%rd2];
225    add.f32 %f0, %f0, %f0;
226    st.global.f32 [%rd2], %f0;
227    add.u32 %r4, %r4, %r6;
228    bra $LOOP;
229$DONE:
230    ret;
231}
232";
233
234    /// Launch the doubling kernel on `dptr` over `stream` (raw FFI).
235    fn launch_double(
236        api: &crate::loader::DriverApi,
237        func: &crate::module::Function,
238        stream: &Stream,
239        dptr: CUdeviceptr,
240        n: usize,
241    ) -> CudaResult<()> {
242        let mut dptr_arg = dptr;
243        let mut n_arg: u32 = n as u32;
244        let mut params: [*mut c_void; 2] = [
245            (&mut dptr_arg as *mut CUdeviceptr).cast(),
246            (&mut n_arg as *mut u32).cast(),
247        ];
248        crate::error::check(unsafe {
249            (api.cu_launch_kernel)(
250                func.raw(),
251                8,
252                1,
253                1,
254                128,
255                1,
256                1,
257                0,
258                stream.raw(),
259                params.as_mut_ptr(),
260                std::ptr::null_mut(),
261            )
262        })
263    }
264
265    /// Real-hardware multi-stream test: run the doubling kernel concurrently on
266    /// two independent streams over two buffers, plus a cross-stream dependency
267    /// (stream B waits on an event recorded on stream A before doubling a buffer
268    /// A already doubled — so it ends up x4). Verifies both the concurrent and
269    /// the ordered results. No-op without a GPU.
270    #[test]
271    fn two_streams_concurrent_and_cross_stream_event() {
272        let Ok(dev) = Device::get(0) else {
273            return;
274        };
275        let ctx = match Context::new(&dev) {
276            Ok(c) => Arc::new(c),
277            Err(_) => return,
278        };
279        let stream_a = match Stream::new(&ctx) {
280            Ok(s) => s,
281            Err(_) => return,
282        };
283        let stream_b = match Stream::new(&ctx) {
284            Ok(s) => s,
285            Err(_) => return,
286        };
287        let api = try_driver().expect("driver present");
288
289        let module = match Module::from_ptx(DOUBLE_PTX) {
290            Ok(m) => m,
291            Err(_) => return,
292        };
293        let func = module.get_function("dbl").expect("dbl");
294
295        const N: usize = 2048;
296        let bytes = N * std::mem::size_of::<f32>();
297        let a_in: Vec<f32> = (0..N).map(|i| i as f32).collect();
298        let b_in: Vec<f32> = (0..N).map(|i| i as f32 + 1000.0).collect();
299
300        let mut da: CUdeviceptr = 0;
301        let mut db: CUdeviceptr = 0;
302        crate::error::check(unsafe { (api.cu_mem_alloc_v2)(&mut da, bytes) }).expect("alloc a");
303        crate::error::check(unsafe { (api.cu_mem_alloc_v2)(&mut db, bytes) }).expect("alloc b");
304
305        let result = (|| -> CudaResult<(Vec<f32>, Vec<f32>)> {
306            crate::error::check(unsafe {
307                (api.cu_memcpy_htod_v2)(da, a_in.as_ptr().cast(), bytes)
308            })?;
309            crate::error::check(unsafe {
310                (api.cu_memcpy_htod_v2)(db, b_in.as_ptr().cast(), bytes)
311            })?;
312
313            // Concurrent: double A on stream A, double B on stream B.
314            launch_double(api, &func, &stream_a, da, N)?;
315            launch_double(api, &func, &stream_b, db, N)?;
316
317            // Cross-stream dependency: record an event on A after its kernel,
318            // make B wait on it, then double A again on B (A -> x4).
319            let evt = Event::new()?;
320            evt.record(&stream_a)?;
321            stream_b.wait_event(&evt)?;
322            launch_double(api, &func, &stream_b, da, N)?;
323
324            stream_a.synchronize()?;
325            stream_b.synchronize()?;
326
327            let mut a_out = vec![0.0f32; N];
328            let mut b_out = vec![0.0f32; N];
329            crate::error::check(unsafe {
330                (api.cu_memcpy_dtoh_v2)(a_out.as_mut_ptr().cast(), da, bytes)
331            })?;
332            crate::error::check(unsafe {
333                (api.cu_memcpy_dtoh_v2)(b_out.as_mut_ptr().cast(), db, bytes)
334            })?;
335            Ok((a_out, b_out))
336        })();
337
338        let _ = unsafe { (api.cu_mem_free_v2)(da) };
339        let _ = unsafe { (api.cu_mem_free_v2)(db) };
340
341        let (a_out, b_out) = result.expect("multi-stream round-trip");
342        // A was doubled twice (stream A, then stream B after the event) => x4.
343        for (i, &v) in a_out.iter().enumerate() {
344            assert!(
345                (v - 4.0 * i as f32).abs() <= 1e-4,
346                "stream A buffer element {i}: got {v}, expected {}",
347                4.0 * i as f32
348            );
349        }
350        // B was doubled once on stream B => x2.
351        for (i, &v) in b_out.iter().enumerate() {
352            let want = 2.0 * (i as f32 + 1000.0);
353            assert!(
354                (v - want).abs() <= 1e-3,
355                "stream B buffer element {i}: got {v}, expected {want}"
356            );
357        }
358    }
359}