oxicuda_memory/device_buffer.rs
1//! Type-safe device (GPU VRAM) memory buffer.
2//!
3//! [`DeviceBuffer<T>`] owns a contiguous allocation of `T` elements in device
4//! memory. It supports synchronous and asynchronous copies to/from host
5//! memory, device-to-device copies, and zero-initialisation via `cuMemsetD8`.
6//!
7//! The buffer is parameterised over `T: Copy` so that only plain-old-data
8//! types can be stored — no heap pointers that would be meaningless on the
9//! GPU.
10//!
11//! # Ownership
12//!
13//! The allocation is freed automatically when the buffer is dropped. If
14//! `cuMemFree_v2` fails during [`Drop`], the error is logged via
15//! [`tracing::warn`] rather than panicking.
16//!
17//! # Example
18//!
19//! ```rust,no_run
20//! # use oxicuda_memory::DeviceBuffer;
21//! let mut buf = DeviceBuffer::<f32>::alloc(1024)?;
22//! let host_data = vec![1.0_f32; 1024];
23//! buf.copy_from_host(&host_data)?;
24//!
25//! let mut result = vec![0.0_f32; 1024];
26//! buf.copy_to_host(&mut result)?;
27//! assert_eq!(result, host_data);
28//! # Ok::<(), oxicuda_driver::error::CudaError>(())
29//! ```
30
31use std::ffi::c_void;
32use std::marker::PhantomData;
33
34use oxicuda_driver::error::{CudaError, CudaResult};
35use oxicuda_driver::ffi::{CUdeviceptr, CUstream};
36use oxicuda_driver::loader::{DriverApi, try_driver};
37use oxicuda_driver::stream::Stream;
38
39// ---------------------------------------------------------------------------
40// Legacy-default-stream synchronisation
41// ---------------------------------------------------------------------------
42
43/// Blocks until the **legacy default stream** has drained, and *only* that
44/// stream.
45///
46/// # Why this exists (and why it is not `cuCtxSynchronize`)
47///
48/// The non-async driver transfer primitives this module uses — `cuMemsetD8_v2`
49/// (see [`DeviceBuffer::zeroed`]) and `cuMemcpyHtoD_v2` from pageable host
50/// memory (see [`DeviceBuffer::copy_from_host`]) — are documented as
51/// *asynchronous with respect to the host*: they enqueue onto the legacy
52/// default stream (the `NULL` stream) and return before the device-side work
53/// has landed. Every OxiCUDA [`Stream`] is created `CU_STREAM_NON_BLOCKING`
54/// (see [`Stream::new`]), which by definition opts **out** of the legacy
55/// stream's implicit ordering, so a consumer stream can observe the buffer
56/// before the memset/DMA completes — a real data race, not a theoretical one.
57/// Something must block until the legacy stream has drained.
58///
59/// `cuCtxSynchronize()` closes that race, but far too widely: it is documented
60/// as blocking "until the device has completed all preceding requested tasks"
61/// in the *current context* — i.e. every stream in the context, including the
62/// non-blocking ones that have nothing to do with this buffer. In a
63/// multi-stream pipeline (independent models on independent streams) that turns
64/// every `zeroed` / `copy_from_host` into a full device barrier and serialises
65/// unrelated work.
66///
67/// `cuStreamSynchronize(hStream)` is documented as waiting "until the device
68/// has completed all operations in the stream specified by `hStream`" — one
69/// stream's queue, not the context's. Passing a `NULL` handle
70/// ([`CUstream::default`], a null pointer) selects the default stream, which
71/// for a driver-API symbol resolved by name (`cuStreamSynchronize`, never the
72/// `_ptsz` per-thread-default alias the CUDA *Runtime* substitutes under
73/// `--default-stream per-thread`) is the legacy default stream — precisely the
74/// stream the memset / DMA above was enqueued on.
75///
76/// That makes this strictly narrower than `cuCtxSynchronize` while closing the
77/// exact same gap. The legacy stream's *implicit synchronisation* rule (an
78/// operation **enqueued into** it first waits for all preceding operations in
79/// the context's *blocking* streams) does not widen this call: that rule
80/// governs enqueued operations, not a host-side wait — and in any case OxiCUDA
81/// creates no blocking streams at all, so the set of streams it could pull in
82/// is empty.
83///
84/// Proven on-device by `tests/legacy_stream_sync_gpu.rs`: the legacy stream is
85/// still awaited (so the race stays closed), and an unrelated non-blocking
86/// stream no longer is (the actual speedup).
87#[inline]
88fn sync_legacy_stream(api: &DriverApi) -> CudaResult<()> {
89 // SAFETY: `cu_stream_synchronize` was resolved from the loaded driver, and
90 // a `NULL` stream handle is the driver API's legacy default stream — always
91 // a valid argument, no allocation of ours is referenced.
92 oxicuda_driver::check(unsafe { (api.cu_stream_synchronize)(CUstream::default()) })
93}
94
95// ---------------------------------------------------------------------------
96// DeviceBuffer<T>
97// ---------------------------------------------------------------------------
98
99/// A contiguous buffer of `T` elements allocated in GPU device memory.
100///
101/// The buffer owns the underlying `CUdeviceptr` allocation and frees it on
102/// drop. All copy operations validate that source and destination lengths
103/// match, returning [`CudaError::InvalidValue`] on mismatch.
104pub struct DeviceBuffer<T: Copy> {
105 /// Raw CUDA device pointer to the start of the allocation.
106 ptr: CUdeviceptr,
107 /// Number of `T` elements (not bytes).
108 len: usize,
109 /// Whether this buffer owns its allocation and must free it on drop.
110 ///
111 /// `true` for buffers created via [`DeviceBuffer::alloc`],
112 /// [`DeviceBuffer::zeroed`], or [`DeviceBuffer::from_host`]; `false` for
113 /// non-owning views created via [`DeviceBuffer::from_raw`], which borrow an
114 /// externally-owned device pointer and must NOT free it on drop.
115 owned: bool,
116 /// Marker to tie the generic parameter `T` to this struct.
117 _phantom: PhantomData<T>,
118}
119
120// SAFETY: Device memory is not bound to a specific host thread. The raw
121// pointer is a `u64` handle managed by the CUDA driver, which is thread-safe
122// for memory operations when properly synchronised.
123unsafe impl<T: Copy + Send> Send for DeviceBuffer<T> {}
124unsafe impl<T: Copy + Sync> Sync for DeviceBuffer<T> {}
125
126impl<T: Copy> DeviceBuffer<T> {
127 /// Allocates a device buffer capable of holding `n` elements of type `T`.
128 ///
129 /// # Errors
130 ///
131 /// * [`CudaError::InvalidValue`] if `n` is zero.
132 /// * [`CudaError::OutOfMemory`] if the GPU cannot satisfy the request.
133 /// * Other driver errors propagated from `cuMemAlloc_v2`.
134 pub fn alloc(n: usize) -> CudaResult<Self> {
135 if n == 0 {
136 return Err(CudaError::InvalidValue);
137 }
138 let byte_size = n
139 .checked_mul(std::mem::size_of::<T>())
140 .ok_or(CudaError::InvalidValue)?;
141 let api = try_driver()?;
142 let mut ptr: CUdeviceptr = 0;
143 // SAFETY: `cu_mem_alloc_v2` writes a valid device pointer on success.
144 let rc = unsafe { (api.cu_mem_alloc_v2)(&mut ptr, byte_size) };
145 oxicuda_driver::check(rc)?;
146 Ok(Self {
147 ptr,
148 len: n,
149 owned: true,
150 _phantom: PhantomData,
151 })
152 }
153
154 /// Allocates a device buffer of `n` elements and zero-initialises every byte.
155 ///
156 /// This is equivalent to [`alloc`](Self::alloc) followed by a
157 /// `cuMemsetD8_v2` call that writes `0` to every byte.
158 ///
159 /// The zero-fill is **fully completed on the device before this function
160 /// returns**: `cuMemsetD8_v2` is issued on the legacy default stream and is
161 /// asynchronous with respect to the host for device memory, so the returned
162 /// buffer would otherwise not be guaranteed zeroed relative to work later
163 /// submitted on a `CU_STREAM_NON_BLOCKING` stream (which does *not*
164 /// implicitly synchronise with the default stream). Draining the legacy
165 /// stream after the memset makes the "every byte is 0" postcondition hold
166 /// for any consumer stream, closing a data race where a kernel on a
167 /// non-blocking stream could read/overwrite this buffer concurrently with
168 /// the pending zero-fill.
169 ///
170 /// The wait is scoped to the legacy default stream alone (see
171 /// `sync_legacy_stream`), so unrelated work in flight on other streams is
172 /// **not** waited on.
173 ///
174 /// # Errors
175 ///
176 /// Same as [`alloc`](Self::alloc), plus any error from `cuMemsetD8_v2` or
177 /// the legacy-stream synchronise.
178 pub fn zeroed(n: usize) -> CudaResult<Self> {
179 let buf = Self::alloc(n)?;
180 let api = try_driver()?;
181 // SAFETY: the buffer was just allocated with the correct byte size.
182 let rc = unsafe { (api.cu_memset_d8_v2)(buf.ptr, 0, buf.byte_size()) };
183 oxicuda_driver::check(rc)?;
184 // The non-async memset runs on the legacy default stream and is host
185 // asynchronous for device memory; block until it has actually landed so
186 // the buffer is zeroed with respect to every stream, not just the
187 // default one. Waits on the legacy stream specifically -- the one the
188 // memset was enqueued on -- not the whole context.
189 sync_legacy_stream(api)?;
190 Ok(buf)
191 }
192
193 /// Allocates a device buffer and copies the contents of `data` into it.
194 ///
195 /// The resulting buffer has the same length as the input slice.
196 ///
197 /// # Errors
198 ///
199 /// * [`CudaError::InvalidValue`] if `data` is empty.
200 /// * Other driver errors from allocation or the host-to-device copy.
201 pub fn from_host(data: &[T]) -> CudaResult<Self> {
202 let mut buf = Self::alloc(data.len())?;
203 buf.copy_from_host(data)?;
204 Ok(buf)
205 }
206
207 /// Wraps an externally-owned device pointer in a non-owning
208 /// [`DeviceBuffer`] view **without allocating**.
209 ///
210 /// The returned buffer points at the *existing* allocation described by
211 /// `ptr` and `len`, and exposes the full [`DeviceBuffer`] API (copies,
212 /// slicing, [`as_device_ptr`](Self::as_device_ptr), and use as a matrix
213 /// operand in `oxicuda-blas`) over that memory. Because the view does not
214 /// own the allocation, its [`Drop`] is a no-op: it will **not** call
215 /// `cuMemFree_v2`. Ownership and the lifetime of the underlying memory
216 /// remain entirely with the original owner (e.g. another CUDA library,
217 /// `cudarc`, or a foreign allocator).
218 ///
219 /// This enables zero-copy interop: a consumer that already holds a
220 /// resident device allocation can wrap it here and run OxiCUDA operations
221 /// in place, with no host round-trip and no extra device allocation.
222 ///
223 /// # Safety
224 ///
225 /// The caller must guarantee all of the following:
226 ///
227 /// * `ptr` is a valid CUDA device pointer into an allocation of at least
228 /// `len * size_of::<T>()` bytes, correctly aligned for `T`, and
229 /// associated with the CUDA context that subsequent OxiCUDA operations
230 /// run under.
231 /// * The pointed-to memory contains a valid, initialised `[T; len]` (or is
232 /// only used as a write target before being read).
233 /// * The underlying allocation **outlives** this `DeviceBuffer` view: the
234 /// original owner must not free, reallocate, or invalidate `ptr` while
235 /// this view (or any [`DeviceSlice`] borrowed from it) is alive.
236 /// * No other live `DeviceBuffer` owns the same `ptr` (to avoid a
237 /// double-free) and aliasing rules are respected when the view is used
238 /// mutably (e.g. as a [`MatrixDescMut`](../oxicuda_blas/struct.MatrixDescMut.html)
239 /// output operand).
240 ///
241 /// A zero `len` is permitted (unlike [`alloc`](Self::alloc)) since no
242 /// allocation is performed; a `ptr` of `0` is also permitted for a
243 /// zero-length view, but pointer/length validity is the caller's
244 /// responsibility.
245 ///
246 /// # Example
247 ///
248 /// ```rust,no_run
249 /// # use oxicuda_memory::DeviceBuffer;
250 /// # use oxicuda_driver::ffi::CUdeviceptr;
251 /// // `raw` is a device pointer owned elsewhere (e.g. obtained from another
252 /// // CUDA library) pointing at `n` resident `f32` elements.
253 /// # let raw: CUdeviceptr = 0;
254 /// # let n: usize = 1024;
255 /// // SAFETY: `raw` is valid for `n` f32s and outlives `view`.
256 /// let view = unsafe { DeviceBuffer::<f32>::from_raw(raw, n) };
257 /// // `view` can now be used with oxicuda-blas / copies; dropping it does
258 /// // NOT free `raw`.
259 /// assert_eq!(view.len(), n);
260 /// ```
261 #[must_use]
262 pub unsafe fn from_raw(ptr: CUdeviceptr, len: usize) -> Self {
263 Self {
264 ptr,
265 len,
266 owned: false,
267 _phantom: PhantomData,
268 }
269 }
270
271 /// Copies data from a host slice into this device buffer (synchronous).
272 ///
273 /// The slice length must exactly match the buffer length.
274 ///
275 /// The upload is **fully landed on the device before this function
276 /// returns**, and the wait is scoped to the legacy default stream alone
277 /// (see `sync_legacy_stream`) rather than to the whole context.
278 ///
279 /// # Performance
280 ///
281 /// `src` is ordinary pageable host memory, so the driver must stage it
282 /// through an internal DMA buffer. For a hot path that uploads the same
283 /// tensor shape every frame, a reusable page-locked staging buffer
284 /// ([`crate::StagingBuffer`]) is measurably faster.
285 ///
286 /// # Errors
287 ///
288 /// * [`CudaError::InvalidValue`] if `src.len() != self.len()`.
289 /// * Other driver errors from `cuMemcpyHtoD_v2`.
290 pub fn copy_from_host(&mut self, src: &[T]) -> CudaResult<()> {
291 if src.len() != self.len {
292 return Err(CudaError::InvalidValue);
293 }
294 let api = try_driver()?;
295 // SAFETY: `src` is a valid host slice with the correct byte count.
296 let rc = unsafe {
297 (api.cu_memcpy_htod_v2)(self.ptr, src.as_ptr().cast::<c_void>(), self.byte_size())
298 };
299 oxicuda_driver::check(rc)?;
300 // `cuMemcpyHtoD_v2` is only "synchronous" in the sense that it returns
301 // once `src` (pageable memory) has been staged into the driver's DMA
302 // buffer -- the transfer to device memory itself completes later, on the
303 // legacy default stream. Every OxiCUDA `Stream` is created with
304 // `CU_STREAM_NON_BLOCKING`, which by definition does *not* implicitly
305 // synchronise with the default stream, so a kernel or copy issued on one
306 // can observe this buffer before the upload lands and silently read
307 // zeros. Block until the DMA has completed, mirroring `zeroed` -- on the
308 // legacy stream specifically, not the whole context.
309 sync_legacy_stream(api)
310 }
311
312 /// Copies this device buffer's contents into a host slice (synchronous).
313 ///
314 /// The slice length must exactly match the buffer length.
315 ///
316 /// # Errors
317 ///
318 /// * [`CudaError::InvalidValue`] if `dst.len() != self.len()`.
319 /// * Other driver errors from `cuMemcpyDtoH_v2`.
320 pub fn copy_to_host(&self, dst: &mut [T]) -> CudaResult<()> {
321 if dst.len() != self.len {
322 return Err(CudaError::InvalidValue);
323 }
324 let api = try_driver()?;
325 // SAFETY: `dst` is a valid host slice with the correct byte count.
326 let rc = unsafe {
327 (api.cu_memcpy_dtoh_v2)(
328 dst.as_mut_ptr().cast::<c_void>(),
329 self.ptr,
330 self.byte_size(),
331 )
332 };
333 oxicuda_driver::check(rc)
334 }
335
336 /// Copies the entire contents of another device buffer into this one.
337 ///
338 /// Both buffers must have the same length.
339 ///
340 /// # Errors
341 ///
342 /// * [`CudaError::InvalidValue`] if `src.len() != self.len()`.
343 /// * Other driver errors from `cuMemcpyDtoD_v2`.
344 pub fn copy_from_device(&mut self, src: &DeviceBuffer<T>) -> CudaResult<()> {
345 if src.len != self.len {
346 return Err(CudaError::InvalidValue);
347 }
348 let api = try_driver()?;
349 // SAFETY: both pointers are valid device allocations of the same size.
350 let rc = unsafe { (api.cu_memcpy_dtod_v2)(self.ptr, src.ptr, self.byte_size()) };
351 oxicuda_driver::check(rc)
352 }
353
354 /// Asynchronously copies data from a host slice into this device buffer.
355 ///
356 /// The copy is enqueued on `stream` and may not be complete when this
357 /// function returns. The caller must ensure that `src` remains valid
358 /// (i.e., is not moved or dropped) until the stream has been
359 /// synchronised. For guaranteed correctness, prefer using a
360 /// [`PinnedBuffer`](crate::PinnedBuffer) as the source.
361 ///
362 /// # Errors
363 ///
364 /// * [`CudaError::InvalidValue`] if `src.len() != self.len()`.
365 /// * Other driver errors from `cuMemcpyHtoDAsync_v2`.
366 pub fn copy_from_host_async(&mut self, src: &[T], stream: &Stream) -> CudaResult<()> {
367 if src.len() != self.len {
368 return Err(CudaError::InvalidValue);
369 }
370 let api = try_driver()?;
371 // SAFETY: the caller is responsible for keeping `src` alive until
372 // the stream completes.
373 let rc = unsafe {
374 (api.cu_memcpy_htod_async_v2)(
375 self.ptr,
376 src.as_ptr().cast::<c_void>(),
377 self.byte_size(),
378 stream.raw(),
379 )
380 };
381 oxicuda_driver::check(rc)
382 }
383
384 /// Asynchronously copies this device buffer's contents into a host slice.
385 ///
386 /// The copy is enqueued on `stream` and may not be complete when this
387 /// function returns. The caller must ensure that `dst` remains valid
388 /// and is not read until the stream has been synchronised. For
389 /// guaranteed correctness, prefer using a
390 /// [`PinnedBuffer`](crate::PinnedBuffer) as the destination.
391 ///
392 /// # Errors
393 ///
394 /// * [`CudaError::InvalidValue`] if `dst.len() != self.len()`.
395 /// * Other driver errors from `cuMemcpyDtoHAsync_v2`.
396 pub fn copy_to_host_async(&self, dst: &mut [T], stream: &Stream) -> CudaResult<()> {
397 if dst.len() != self.len {
398 return Err(CudaError::InvalidValue);
399 }
400 let api = try_driver()?;
401 // SAFETY: the caller is responsible for keeping `dst` alive until
402 // the stream completes.
403 let rc = unsafe {
404 (api.cu_memcpy_dtoh_async_v2)(
405 dst.as_mut_ptr().cast::<c_void>(),
406 self.ptr,
407 self.byte_size(),
408 stream.raw(),
409 )
410 };
411 oxicuda_driver::check(rc)
412 }
413
414 /// Returns the number of `T` elements in this buffer.
415 #[inline]
416 pub fn len(&self) -> usize {
417 self.len
418 }
419
420 /// Returns `true` if the buffer contains zero elements.
421 ///
422 /// In practice this is always `false` because [`alloc`](Self::alloc)
423 /// rejects zero-length allocations.
424 #[inline]
425 pub fn is_empty(&self) -> bool {
426 self.len == 0
427 }
428
429 /// Returns the total size of the allocation in bytes.
430 #[inline]
431 pub fn byte_size(&self) -> usize {
432 self.len * std::mem::size_of::<T>()
433 }
434
435 /// Returns the raw [`CUdeviceptr`] handle for this buffer.
436 ///
437 /// This is useful when passing the pointer to kernel launch parameters
438 /// or other low-level driver calls.
439 #[inline]
440 pub fn as_device_ptr(&self) -> CUdeviceptr {
441 self.ptr
442 }
443
444 /// Returns a borrowed [`DeviceSlice`] referencing a sub-range of this
445 /// buffer starting at element `offset` and spanning `len` elements.
446 ///
447 /// # Errors
448 ///
449 /// Returns [`CudaError::InvalidValue`] if the requested range exceeds
450 /// the buffer bounds (i.e., `offset + len > self.len()`).
451 pub fn slice(&self, offset: usize, len: usize) -> CudaResult<DeviceSlice<'_, T>> {
452 let end = offset.checked_add(len).ok_or(CudaError::InvalidValue)?;
453 if end > self.len {
454 return Err(CudaError::InvalidValue);
455 }
456 let byte_offset = offset
457 .checked_mul(std::mem::size_of::<T>())
458 .ok_or(CudaError::InvalidValue)?;
459 Ok(DeviceSlice {
460 ptr: self.ptr + byte_offset as u64,
461 len,
462 _phantom: PhantomData,
463 })
464 }
465}
466
467impl<T: Copy> Drop for DeviceBuffer<T> {
468 fn drop(&mut self) {
469 // Non-owning views (created via `from_raw`) borrow an externally-owned
470 // allocation and must never free it.
471 if !self.owned {
472 return;
473 }
474 if let Ok(api) = try_driver() {
475 // SAFETY: `self.ptr` was allocated by `cu_mem_alloc_v2` and has
476 // not yet been freed.
477 let rc = unsafe { (api.cu_mem_free_v2)(self.ptr) };
478 if rc != 0 {
479 tracing::warn!(
480 cuda_error = rc,
481 ptr = self.ptr,
482 len = self.len,
483 "cuMemFree_v2 failed during DeviceBuffer drop"
484 );
485 }
486 }
487 }
488}
489
490// ---------------------------------------------------------------------------
491// DeviceSlice<'a, T>
492// ---------------------------------------------------------------------------
493
494/// A borrowed, non-owning view into a sub-range of a [`DeviceBuffer`].
495///
496/// A `DeviceSlice` does not own the memory it points to — it borrows from
497/// the parent [`DeviceBuffer`] and is lifetime-bound to it. This is useful
498/// for passing sub-regions of a buffer to kernels or copy operations without
499/// extra allocations.
500///
501/// `DeviceSlice` does **not** implement [`Drop`]; the parent buffer is
502/// responsible for freeing the allocation.
503pub struct DeviceSlice<'a, T: Copy> {
504 /// Raw device pointer to the start of this slice within the parent buffer.
505 ptr: CUdeviceptr,
506 /// Number of `T` elements in this slice.
507 len: usize,
508 /// Ties the lifetime to the parent buffer and the element type.
509 _phantom: PhantomData<&'a T>,
510}
511
512impl<T: Copy> DeviceSlice<'_, T> {
513 /// Returns the number of `T` elements in this slice.
514 #[inline]
515 pub fn len(&self) -> usize {
516 self.len
517 }
518
519 /// Returns `true` if the slice contains zero elements.
520 #[inline]
521 pub fn is_empty(&self) -> bool {
522 self.len == 0
523 }
524
525 /// Returns the total size of this slice in bytes.
526 #[inline]
527 pub fn byte_size(&self) -> usize {
528 self.len * std::mem::size_of::<T>()
529 }
530
531 /// Returns the raw [`CUdeviceptr`] handle for the start of this slice.
532 #[inline]
533 pub fn as_device_ptr(&self) -> CUdeviceptr {
534 self.ptr
535 }
536}
537
538// ---------------------------------------------------------------------------
539// Tests
540// ---------------------------------------------------------------------------
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 /// A `from_raw` view must be marked non-owning so that `Drop` skips the
547 /// `cuMemFree_v2` call. We construct over a dummy sentinel pointer; because
548 /// the view is non-owning, dropping it performs no driver call and is safe
549 /// even without a CUDA device present.
550 #[test]
551 fn from_raw_is_non_owning() {
552 let sentinel: CUdeviceptr = 0xDEAD_BEEF;
553 // SAFETY: this view is never dereferenced; we only inspect metadata and
554 // rely on the non-owning Drop being a no-op.
555 let view = unsafe { DeviceBuffer::<f32>::from_raw(sentinel, 16) };
556 assert!(!view.owned, "from_raw must produce a non-owning buffer");
557 assert_eq!(view.len(), 16);
558 assert_eq!(view.as_device_ptr(), sentinel);
559 assert_eq!(view.byte_size(), 16 * std::mem::size_of::<f32>());
560 // Dropping a non-owning view must NOT touch the driver / free memory.
561 // Reaching the end of scope here exercises that path without a GPU.
562 drop(view);
563 }
564
565 /// A zero-length `from_raw` view is permitted (no allocation occurs) and is
566 /// reported as empty.
567 #[test]
568 fn from_raw_zero_len_is_empty() {
569 // SAFETY: zero-length, pointer never dereferenced; Drop is a no-op.
570 let view = unsafe { DeviceBuffer::<u8>::from_raw(0, 0) };
571 assert!(!view.owned);
572 assert!(view.is_empty());
573 assert_eq!(view.len(), 0);
574 assert_eq!(view.byte_size(), 0);
575 }
576
577 /// Two non-owning views may share the same pointer without risking a
578 /// double-free, because neither frees on drop. This models a consumer
579 /// re-wrapping the same resident allocation.
580 #[test]
581 fn from_raw_aliasing_views_do_not_double_free() {
582 let ptr: CUdeviceptr = 0x1000;
583 // SAFETY: non-owning aliases, never dereferenced; both Drops are no-ops.
584 let a = unsafe { DeviceBuffer::<f64>::from_raw(ptr, 8) };
585 let b = unsafe { DeviceBuffer::<f64>::from_raw(ptr, 8) };
586 assert!(!a.owned);
587 assert!(!b.owned);
588 assert_eq!(a.as_device_ptr(), b.as_device_ptr());
589 drop(a);
590 drop(b);
591 }
592
593 /// A real owning allocation created via `alloc` is marked `owned` so that
594 /// its memory is freed on drop. This requires a CUDA device, so it is gated
595 /// behind a runtime driver check and skipped (passing) when no GPU/driver
596 /// is available — keeping the test green on macOS while still proving the
597 /// owned-flag wiring on real hardware.
598 #[test]
599 fn alloc_is_owning_when_driver_available() {
600 match DeviceBuffer::<f32>::alloc(32) {
601 Ok(buf) => {
602 assert!(buf.owned, "alloc must produce an owning buffer");
603 assert_eq!(buf.len(), 32);
604 // `buf` is dropped here and frees its allocation via the driver.
605 }
606 Err(_) => {
607 // No CUDA driver/device on this host (e.g. macOS CI): the
608 // owned-flag logic is covered by the non-GPU tests above.
609 }
610 }
611 }
612}