flodl/tensor/
cuda_stream.rs1use 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
31pub struct GpuStream {
35 ptr: *mut c_void,
36 device_index: i32,
37}
38
39unsafe impl Send for GpuStream {}
41
42impl GpuStream {
43 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 pub fn synchronize(&self) -> Result<()> {
61 let err = unsafe { ffi::flodl_gpu_stream_synchronize(self.ptr) };
62 check_err(err)
63 }
64
65 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 pub fn is_complete(&self) -> bool {
74 unsafe { ffi::flodl_gpu_stream_query(self.ptr) != 0 }
75 }
76
77 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 pub fn device(&self) -> Device {
97 Device::CUDA(self.device_index as u8)
98 }
99
100 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
115pub struct StreamGuard {
137 prev: *mut std::ffi::c_void,
140 device_index: i32,
141}
142
143impl StreamGuard {
144 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 let _a = Tensor::randn(&[32, 32], opts).unwrap();
208 }
209 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 let gpu = Tensor::full(&[128], 42.0, opts).unwrap();
227
228 let copy_stream = GpuStream::new(test_device(), false).unwrap();
230
231 let ready = GpuEvent::new(GpuEventFlags::DisableTiming).unwrap();
233 ready.record().unwrap();
234
235 copy_stream.wait_event(&ready).unwrap();
237 let cpu_copy = {
238 let _guard = StreamGuard::new(©_stream);
239 gpu.to_device_async(Device::CPU).unwrap()
240 };
241
242 let done = GpuEvent::new(GpuEventFlags::DisableTiming).unwrap();
244 done.record_on(©_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 let result = {
268 let _guard = StreamGuard::new(&stream_a);
269 Tensor::full(&[64], 7.0, opts).unwrap()
270 };
271
272 let event = GpuEvent::new(GpuEventFlags::DisableTiming).unwrap();
274 event.record_on(&stream_a).unwrap();
275
276 stream_b.wait_event(&event).unwrap();
278 let doubled = {
279 let _guard = StreamGuard::new(&stream_b);
280 result.add(&result).unwrap()
281 };
282
283 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}