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}