Skip to main content

flodl/tensor/
cuda_stream.rs

1//! CUDA stream for async GPU operations.
2//!
3//! Streams represent ordered queues of GPU work. Operations on different
4//! streams can execute concurrently. The default stream serializes all work.
5//!
6//! Common use: run GPU-to-CPU tensor copies on a non-default stream so
7//! they overlap with training on the default stream.
8//!
9//! CUDA only. Returns error on CPU builds.
10//!
11//! # Usage
12//!
13//! ```ignore
14//! let copy_stream = GpuStream::new(Device::CUDA(0), false)?;
15//! {
16//!     let _guard = StreamGuard::new(&copy_stream);
17//!     // All CUDA ops here run on copy_stream instead of the default stream.
18//!     let cpu_copy = gpu_tensor.to_device_async(Device::CPU)?;
19//! }
20//! // Default stream restored automatically.
21//! ```
22
23use std::ffi::c_void;
24use std::ptr;
25
26use flodl_sys as ffi;
27
28use super::cuda_event::GpuEvent;
29use crate::tensor::{Device, Result, TensorError, check_err};
30
31/// A CUDA stream obtained from the libtorch stream pool.
32///
33/// RAII: the stream is returned to the pool on drop.
34pub struct GpuStream {
35    ptr: *mut c_void,
36    device_index: i32,
37}
38
39// cudaStream_t is a device-global handle safe to reference from any thread.
40unsafe impl Send for GpuStream {}
41
42impl GpuStream {
43    /// Create a new CUDA stream from the pool on the given device.
44    ///
45    /// `high_priority`: if true, uses a high-priority stream that preempts
46    /// normal-priority work at SM boundaries.
47    pub fn new(device: Device, high_priority: bool) -> Result<Self> {
48        let device_index = match device {
49            Device::CUDA(idx) => idx as i32,
50            Device::CPU => return Err(TensorError::new("GpuStream requires a CUDA device")),
51        };
52        let mut ptr: *mut c_void = ptr::null_mut();
53        let err =
54            unsafe { ffi::flodl_gpu_stream_new(device_index, high_priority as i32, &mut ptr) };
55        check_err(err)?;
56        Ok(GpuStream { ptr, device_index })
57    }
58
59    /// Block the CPU thread until all work on this stream completes.
60    pub fn synchronize(&self) -> Result<()> {
61        let err = unsafe { ffi::flodl_gpu_stream_synchronize(self.ptr) };
62        check_err(err)
63    }
64
65    /// Make this stream wait for a recorded event before executing any
66    /// further work. Does not block the CPU.
67    pub fn wait_event(&self, event: &GpuEvent) -> Result<()> {
68        let err = unsafe { ffi::flodl_gpu_stream_wait_event(self.ptr, event.as_ptr()) };
69        check_err(err)
70    }
71
72    /// Non-blocking check: has all work on this stream completed?
73    pub fn is_complete(&self) -> bool {
74        unsafe { ffi::flodl_gpu_stream_query(self.ptr) != 0 }
75    }
76
77    /// The calling thread's current stream on `device` (the default
78    /// stream unless a [`StreamGuard`] is active). The returned value
79    /// owns only the wrapper — dropping it never destroys the
80    /// underlying stream.
81    pub fn current(device: Device) -> Result<Self> {
82        let device_index = match device {
83            Device::CUDA(idx) => idx as i32,
84            Device::CPU => return Err(TensorError::new("GpuStream requires a CUDA device")),
85        };
86        let ptr = unsafe { ffi::flodl_gpu_stream_get_current(device_index) };
87        if ptr.is_null() {
88            return Err(TensorError::new(
89                "cuda_stream_get_current returned null (CUDA build required)",
90            ));
91        }
92        Ok(GpuStream { ptr, device_index })
93    }
94
95    /// The device this stream belongs to.
96    pub fn device(&self) -> Device {
97        Device::CUDA(self.device_index as u8)
98    }
99
100    /// Raw pointer for cross-module use (e.g., event.record_on).
101    pub(crate) fn as_ptr(&self) -> *mut c_void {
102        self.ptr
103    }
104}
105
106impl Drop for GpuStream {
107    fn drop(&mut self) {
108        if !self.ptr.is_null() {
109            unsafe { ffi::flodl_gpu_stream_delete(self.ptr) };
110            self.ptr = ptr::null_mut();
111        }
112    }
113}
114
115/// RAII guard that sets a stream as the current CUDA stream and
116/// restores the **previous** stream on drop.
117///
118/// Nestable: inner guards restore the outer guard's stream, not the
119/// default stream. This is critical for DDP where `sync_now_nccl`
120/// temporarily switches to `comm_stream` inside a `compute_stream` guard.
121///
122/// ```ignore
123/// let compute = GpuStream::new(Device::CUDA(0), false)?;
124/// let comm = GpuStream::new(Device::CUDA(0), false)?;
125/// {
126///     let _outer = StreamGuard::new(&compute);
127///     // All CUDA ops on compute_stream.
128///     {
129///         let _inner = StreamGuard::new(&comm);
130///         // CUDA ops on comm_stream.
131///     }
132///     // Restored to compute_stream (not default).
133/// }
134/// // Restored to whatever was current before _outer.
135/// ```
136pub struct StreamGuard {
137    /// Previous stream pointer, restored on drop. Owned (heap-allocated by
138    /// `flodl_gpu_stream_get_current`), freed via `flodl_gpu_stream_delete`.
139    prev: *mut std::ffi::c_void,
140    device_index: i32,
141}
142
143impl StreamGuard {
144    /// Set `stream` as the current CUDA stream. The previous stream
145    /// is saved and restored when this guard is dropped.
146    pub fn new(stream: &GpuStream) -> Self {
147        let prev = unsafe { ffi::flodl_gpu_stream_get_current(stream.device_index) };
148        unsafe { ffi::flodl_gpu_stream_set_current(stream.ptr) };
149        StreamGuard {
150            prev,
151            device_index: stream.device_index,
152        }
153    }
154}
155
156impl Drop for StreamGuard {
157    fn drop(&mut self) {
158        if !self.prev.is_null() {
159            unsafe { ffi::flodl_gpu_stream_set_current(self.prev) };
160            unsafe { ffi::flodl_gpu_stream_delete(self.prev) };
161        } else {
162            unsafe { ffi::flodl_gpu_stream_restore_default(self.device_index) };
163        }
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::super::cuda_event::GpuEventFlags;
170    use super::*;
171    use crate::tensor::{Tensor, test_device, test_opts};
172
173    use std::sync::Mutex;
174    static STREAM_LOCK: Mutex<()> = Mutex::new(());
175
176    #[test]
177    fn test_cuda_stream_requires_cuda_device() {
178        let result = GpuStream::new(Device::CPU, false);
179        assert!(result.is_err(), "GpuStream::new(CPU) should fail");
180    }
181
182    #[test]
183    fn test_cuda_stream_create_synchronize() {
184        if !test_device().is_cuda() {
185            return;
186        }
187        let _lock = STREAM_LOCK.lock().unwrap_or_else(|e| e.into_inner());
188
189        let stream = GpuStream::new(test_device(), false).unwrap();
190        assert_eq!(stream.device(), test_device());
191        stream.synchronize().unwrap();
192        assert!(stream.is_complete(), "empty stream should be complete");
193    }
194
195    #[test]
196    fn test_stream_guard_restores_default() {
197        if !test_device().is_cuda() {
198            return;
199        }
200        let _lock = STREAM_LOCK.lock().unwrap_or_else(|e| e.into_inner());
201        let opts = test_opts();
202
203        let stream = GpuStream::new(test_device(), false).unwrap();
204        {
205            let _guard = StreamGuard::new(&stream);
206            // Ops run on the non-default stream
207            let _a = Tensor::randn(&[32, 32], opts).unwrap();
208        }
209        // Guard dropped — default stream restored.
210        // Verify we can still do GPU ops normally.
211        let b = Tensor::ones(&[4], opts).unwrap();
212        let c = b.add(&b).unwrap();
213        let vals = c.to_f32_vec().unwrap();
214        assert!(vals.iter().all(|&v| (v - 2.0).abs() < 1e-5));
215    }
216
217    #[test]
218    fn test_async_copy_on_stream() {
219        if !test_device().is_cuda() {
220            return;
221        }
222        let _lock = STREAM_LOCK.lock().unwrap_or_else(|e| e.into_inner());
223        let opts = test_opts();
224
225        // Create a GPU tensor with known values
226        let gpu = Tensor::full(&[128], 42.0, opts).unwrap();
227
228        // Create a non-default copy stream
229        let copy_stream = GpuStream::new(test_device(), false).unwrap();
230
231        // Record event on default stream to capture when gpu tensor is ready
232        let ready = GpuEvent::new(GpuEventFlags::DisableTiming).unwrap();
233        ready.record().unwrap();
234
235        // Copy stream waits for the event, then copies
236        copy_stream.wait_event(&ready).unwrap();
237        let cpu_copy = {
238            let _guard = StreamGuard::new(&copy_stream);
239            gpu.to_device_async(Device::CPU).unwrap()
240        };
241
242        // Record completion on copy stream
243        let done = GpuEvent::new(GpuEventFlags::DisableTiming).unwrap();
244        done.record_on(&copy_stream).unwrap();
245        done.synchronize().unwrap();
246
247        let vals = cpu_copy.to_f32_vec().unwrap();
248        assert_eq!(vals.len(), 128);
249        assert!(
250            vals.iter().all(|&v| (v - 42.0).abs() < 1e-5),
251            "async copy should preserve values"
252        );
253    }
254
255    #[test]
256    fn test_cross_stream_wait_event() {
257        if !test_device().is_cuda() {
258            return;
259        }
260        let _lock = STREAM_LOCK.lock().unwrap_or_else(|e| e.into_inner());
261        let opts = test_opts();
262
263        let stream_a = GpuStream::new(test_device(), false).unwrap();
264        let stream_b = GpuStream::new(test_device(), false).unwrap();
265
266        // On stream A: create a tensor
267        let result = {
268            let _guard = StreamGuard::new(&stream_a);
269            Tensor::full(&[64], 7.0, opts).unwrap()
270        };
271
272        // Record event on stream A
273        let event = GpuEvent::new(GpuEventFlags::DisableTiming).unwrap();
274        event.record_on(&stream_a).unwrap();
275
276        // Stream B waits for stream A, then reads the tensor
277        stream_b.wait_event(&event).unwrap();
278        let doubled = {
279            let _guard = StreamGuard::new(&stream_b);
280            result.add(&result).unwrap()
281        };
282
283        // Wait for stream B to finish
284        stream_b.synchronize().unwrap();
285
286        let vals = doubled.to_f32_vec().unwrap();
287        assert!(
288            vals.iter().all(|&v| (v - 14.0).abs() < 1e-5),
289            "cross-stream result should be 14.0"
290        );
291    }
292}