Skip to main content

oxicuda_memory/
copy.rs

1//! Explicit memory copy operations between host and device.
2//!
3//! This module provides freestanding functions for copying data between
4//! host memory, device memory, and pinned host memory.  Each function
5//! validates that the source and destination have matching lengths before
6//! issuing the underlying CUDA driver call.
7//!
8//! For simple cases, the methods on [`DeviceBuffer`]
9//! (e.g. [`DeviceBuffer::copy_from_host`]) are more
10//! ergonomic.  These freestanding functions are useful when you want to be
11//! explicit about the direction of the transfer or when working with
12//! [`PinnedBuffer`] for async operations.
13//!
14//! # Length validation
15//!
16//! All functions return [`CudaError::InvalidValue`] if the element counts
17//! of source and destination do not match.
18
19use std::ffi::c_void;
20
21use oxicuda_driver::error::{CudaError, CudaResult};
22use oxicuda_driver::loader::try_driver;
23use oxicuda_driver::stream::Stream;
24
25use crate::device_buffer::DeviceBuffer;
26use crate::host_buffer::PinnedBuffer;
27
28// ---------------------------------------------------------------------------
29// Synchronous copies
30// ---------------------------------------------------------------------------
31
32/// Copies data from a host slice into a device buffer (host-to-device).
33///
34/// This is a synchronous operation: it blocks the calling thread until the
35/// transfer completes.
36///
37/// # Errors
38///
39/// * [`CudaError::InvalidValue`] if `src.len() != dst.len()`.
40/// * Other driver errors from `cuMemcpyHtoD_v2`.
41pub fn copy_htod<T: Copy>(dst: &mut DeviceBuffer<T>, src: &[T]) -> CudaResult<()> {
42    if src.len() != dst.len() {
43        return Err(CudaError::InvalidValue);
44    }
45    let byte_size = dst.byte_size();
46    let api = try_driver()?;
47    // SAFETY: `src` is a valid host slice, `dst` owns a valid device allocation,
48    // and the byte counts match.
49    let rc = unsafe {
50        (api.cu_memcpy_htod_v2)(
51            dst.as_device_ptr(),
52            src.as_ptr().cast::<c_void>(),
53            byte_size,
54        )
55    };
56    oxicuda_driver::check(rc)
57}
58
59/// Copies data from a device buffer into a host slice (device-to-host).
60///
61/// This is a synchronous operation: it blocks the calling thread until the
62/// transfer completes.
63///
64/// # Errors
65///
66/// * [`CudaError::InvalidValue`] if `dst.len() != src.len()`.
67/// * Other driver errors from `cuMemcpyDtoH_v2`.
68pub fn copy_dtoh<T: Copy>(dst: &mut [T], src: &DeviceBuffer<T>) -> CudaResult<()> {
69    if dst.len() != src.len() {
70        return Err(CudaError::InvalidValue);
71    }
72    let byte_size = src.byte_size();
73    let api = try_driver()?;
74    // SAFETY: `dst` is a valid host slice, `src` owns a valid device allocation,
75    // and the byte counts match.
76    let rc = unsafe {
77        (api.cu_memcpy_dtoh_v2)(
78            dst.as_mut_ptr().cast::<c_void>(),
79            src.as_device_ptr(),
80            byte_size,
81        )
82    };
83    oxicuda_driver::check(rc)
84}
85
86/// Copies data from one device buffer to another (device-to-device).
87///
88/// This is a synchronous operation that blocks until the copy completes.
89///
90/// # Errors
91///
92/// * [`CudaError::InvalidValue`] if `dst.len() != src.len()`.
93/// * Other driver errors from `cuMemcpyDtoD_v2`.
94pub fn copy_dtod<T: Copy>(dst: &mut DeviceBuffer<T>, src: &DeviceBuffer<T>) -> CudaResult<()> {
95    if dst.len() != src.len() {
96        return Err(CudaError::InvalidValue);
97    }
98    let byte_size = src.byte_size();
99    let api = try_driver()?;
100    // SAFETY: both buffers own valid device allocations of the same size.
101    let rc =
102        unsafe { (api.cu_memcpy_dtod_v2)(dst.as_device_ptr(), src.as_device_ptr(), byte_size) };
103    oxicuda_driver::check(rc)
104}
105
106// ---------------------------------------------------------------------------
107// Asynchronous copies
108// ---------------------------------------------------------------------------
109
110// ---------------------------------------------------------------------------
111// Asynchronous copies (raw slice variants)
112// ---------------------------------------------------------------------------
113
114/// Asynchronously copies data from a host slice into a device buffer.
115///
116/// The copy is enqueued on `stream` and may not be complete when this
117/// function returns.  The caller must ensure that `src` remains valid
118/// (i.e., is not moved or dropped) until the stream has been synchronised.
119/// For guaranteed correctness with DMA, prefer using a [`PinnedBuffer`]
120/// as the source.
121///
122/// # Errors
123///
124/// * [`CudaError::InvalidValue`] if `src.len() != dst.len()`.
125/// * Other driver errors from `cuMemcpyHtoDAsync_v2`.
126pub fn copy_htod_async_raw<T: Copy>(
127    dst: &mut DeviceBuffer<T>,
128    src: &[T],
129    stream: &Stream,
130) -> CudaResult<()> {
131    if src.len() != dst.len() {
132        return Err(CudaError::InvalidValue);
133    }
134    let byte_size = dst.byte_size();
135    let api = try_driver()?;
136    let rc = unsafe {
137        (api.cu_memcpy_htod_async_v2)(
138            dst.as_device_ptr(),
139            src.as_ptr().cast::<c_void>(),
140            byte_size,
141            stream.raw(),
142        )
143    };
144    oxicuda_driver::check(rc)
145}
146
147/// Asynchronously copies data from a device buffer into a host slice.
148///
149/// The copy is enqueued on `stream` and may not be complete when this
150/// function returns.  The caller must ensure that `dst` remains valid
151/// and is not read until the stream has been synchronised.
152///
153/// # Errors
154///
155/// * [`CudaError::InvalidValue`] if `dst.len() != src.len()`.
156/// * Other driver errors from `cuMemcpyDtoHAsync_v2`.
157pub fn copy_dtoh_async_raw<T: Copy>(
158    dst: &mut [T],
159    src: &DeviceBuffer<T>,
160    stream: &Stream,
161) -> CudaResult<()> {
162    if dst.len() != src.len() {
163        return Err(CudaError::InvalidValue);
164    }
165    let byte_size = src.byte_size();
166    let api = try_driver()?;
167    let rc = unsafe {
168        (api.cu_memcpy_dtoh_async_v2)(
169            dst.as_mut_ptr().cast::<c_void>(),
170            src.as_device_ptr(),
171            byte_size,
172            stream.raw(),
173        )
174    };
175    oxicuda_driver::check(rc)
176}
177
178/// Asynchronously copies data from one device buffer to another.
179///
180/// Both buffers must have the same length.  The copy is enqueued on
181/// `stream` via `cuMemcpyDtoDAsync_v2` and may not be complete when this
182/// function returns; the caller must synchronise `stream` (or otherwise
183/// order subsequent accesses) before reading `dst` or reusing `src`.
184///
185/// # Errors
186///
187/// * [`CudaError::InvalidValue`] if `dst.len() != src.len()`.
188/// * [`CudaError::NotSupported`] if the loaded driver predates CUDA 4.0 and
189///   does not export `cuMemcpyDtoDAsync_v2`.
190/// * Other driver errors from `cuMemcpyDtoDAsync_v2`.
191pub fn copy_dtod_async<T: Copy>(
192    dst: &mut DeviceBuffer<T>,
193    src: &DeviceBuffer<T>,
194    stream: &Stream,
195) -> CudaResult<()> {
196    if dst.len() != src.len() {
197        return Err(CudaError::InvalidValue);
198    }
199    let byte_size = src.byte_size();
200    oxicuda_driver::memory_info::memcpy_device_to_device_async(
201        dst.as_device_ptr(),
202        src.as_device_ptr(),
203        byte_size,
204        stream,
205    )
206}
207
208// ---------------------------------------------------------------------------
209// Asynchronous copies (pinned buffer variants)
210// ---------------------------------------------------------------------------
211
212/// Asynchronously copies data from a pinned host buffer into a device buffer.
213///
214/// The copy is enqueued on `stream` and may not be complete when this
215/// function returns.  The caller must not modify `src` or read `dst` until
216/// the stream has been synchronised.
217///
218/// Using a [`PinnedBuffer`] as the source guarantees that the host memory
219/// is page-locked, which is required for correct async DMA transfers.
220///
221/// # Errors
222///
223/// * [`CudaError::InvalidValue`] if `src.len() != dst.len()`.
224/// * Other driver errors from `cuMemcpyHtoDAsync_v2`.
225pub fn copy_htod_async<T: Copy>(
226    dst: &mut DeviceBuffer<T>,
227    src: &PinnedBuffer<T>,
228    stream: &Stream,
229) -> CudaResult<()> {
230    if src.len() != dst.len() {
231        return Err(CudaError::InvalidValue);
232    }
233    let byte_size = dst.byte_size();
234    let api = try_driver()?;
235    // SAFETY: `src` is pinned host memory, `dst` is a valid device allocation,
236    // byte counts match, and the stream will order the transfer.
237    let rc = unsafe {
238        (api.cu_memcpy_htod_async_v2)(
239            dst.as_device_ptr(),
240            src.as_ptr().cast::<c_void>(),
241            byte_size,
242            stream.raw(),
243        )
244    };
245    oxicuda_driver::check(rc)
246}
247
248/// Asynchronously copies data from a device buffer into a pinned host buffer.
249///
250/// The copy is enqueued on `stream` and may not be complete when this
251/// function returns.  The caller must not read `dst` until the stream
252/// has been synchronised.
253///
254/// Using a [`PinnedBuffer`] as the destination guarantees that the host
255/// memory is page-locked, which is required for correct async DMA transfers.
256///
257/// # Errors
258///
259/// * [`CudaError::InvalidValue`] if `dst.len() != src.len()`.
260/// * Other driver errors from `cuMemcpyDtoHAsync_v2`.
261pub fn copy_dtoh_async<T: Copy>(
262    dst: &mut PinnedBuffer<T>,
263    src: &DeviceBuffer<T>,
264    stream: &Stream,
265) -> CudaResult<()> {
266    if dst.len() != src.len() {
267        return Err(CudaError::InvalidValue);
268    }
269    let byte_size = src.byte_size();
270    let api = try_driver()?;
271    // SAFETY: `dst` is pinned host memory, `src` is a valid device allocation,
272    // byte counts match, and the stream will order the transfer.
273    let rc = unsafe {
274        (api.cu_memcpy_dtoh_async_v2)(
275            dst.as_mut_ptr().cast::<c_void>(),
276            src.as_device_ptr(),
277            byte_size,
278            stream.raw(),
279        )
280    };
281    oxicuda_driver::check(rc)
282}
283
284// ---------------------------------------------------------------------------
285// Asynchronous sub-region copies (pinned buffer staging)
286// ---------------------------------------------------------------------------
287
288/// Asynchronously copies a contiguous sub-region of a device buffer into a
289/// pinned host buffer.
290///
291/// Exactly `count` elements starting at element index `src_offset` within
292/// `src` are copied into `dst[0..count]`.  The pinned buffer must be large
293/// enough to receive `count` elements.
294///
295/// This is the device→host leg of a host-staged inter-device transfer: the
296/// caller stages a slab slice into pinned memory here, then pushes it onto a
297/// different device with [`copy_htod_region_async`].
298///
299/// The copy is enqueued on `stream`; the caller must synchronise the stream
300/// before reading `dst`.
301///
302/// # Errors
303///
304/// * [`CudaError::InvalidValue`] if `src_offset + count` exceeds `src.len()`,
305///   if `count` exceeds `dst.len()`, or on offset overflow.
306/// * Other driver errors from `cuMemcpyDtoHAsync_v2`.
307pub fn copy_dtoh_region_async<T: Copy>(
308    dst: &mut PinnedBuffer<T>,
309    src: &DeviceBuffer<T>,
310    src_offset: usize,
311    count: usize,
312    stream: &Stream,
313) -> CudaResult<()> {
314    let elem_size = std::mem::size_of::<T>();
315    let src_end = src_offset
316        .checked_add(count)
317        .ok_or(CudaError::InvalidValue)?;
318    if src_end > src.len() || count > dst.len() {
319        return Err(CudaError::InvalidValue);
320    }
321    if count == 0 {
322        return Ok(());
323    }
324    let byte_count = count
325        .checked_mul(elem_size)
326        .ok_or(CudaError::InvalidValue)?;
327    let src_byte_offset = src_offset
328        .checked_mul(elem_size)
329        .ok_or(CudaError::InvalidValue)? as u64;
330    let api = try_driver()?;
331    // SAFETY: `dst` is pinned host memory with room for `count` elements,
332    // the source sub-range lies within `src`, and byte counts match.
333    let rc = unsafe {
334        (api.cu_memcpy_dtoh_async_v2)(
335            dst.as_mut_ptr().cast::<c_void>(),
336            src.as_device_ptr() + src_byte_offset,
337            byte_count,
338            stream.raw(),
339        )
340    };
341    oxicuda_driver::check(rc)
342}
343
344/// Asynchronously copies from a pinned host buffer into a contiguous
345/// sub-region of a device buffer.
346///
347/// The first `count` elements of `src` are written into `dst` starting at
348/// element index `dst_offset`.
349///
350/// This is the host→device leg of a host-staged inter-device transfer; see
351/// [`copy_dtoh_region_async`] for the device→host leg.
352///
353/// The copy is enqueued on `stream`; the caller must synchronise the stream
354/// before reusing `src`.
355///
356/// # Errors
357///
358/// * [`CudaError::InvalidValue`] if `dst_offset + count` exceeds `dst.len()`,
359///   if `count` exceeds `src.len()`, or on offset overflow.
360/// * Other driver errors from `cuMemcpyHtoDAsync_v2`.
361pub fn copy_htod_region_async<T: Copy>(
362    dst: &mut DeviceBuffer<T>,
363    dst_offset: usize,
364    src: &PinnedBuffer<T>,
365    count: usize,
366    stream: &Stream,
367) -> CudaResult<()> {
368    let elem_size = std::mem::size_of::<T>();
369    let dst_end = dst_offset
370        .checked_add(count)
371        .ok_or(CudaError::InvalidValue)?;
372    if dst_end > dst.len() || count > src.len() {
373        return Err(CudaError::InvalidValue);
374    }
375    if count == 0 {
376        return Ok(());
377    }
378    let byte_count = count
379        .checked_mul(elem_size)
380        .ok_or(CudaError::InvalidValue)?;
381    let dst_byte_offset = dst_offset
382        .checked_mul(elem_size)
383        .ok_or(CudaError::InvalidValue)? as u64;
384    let api = try_driver()?;
385    // SAFETY: `src` is pinned host memory holding at least `count` elements,
386    // the destination sub-range lies within `dst`, and byte counts match.
387    let rc = unsafe {
388        (api.cu_memcpy_htod_async_v2)(
389            dst.as_device_ptr() + dst_byte_offset,
390            src.as_ptr().cast::<c_void>(),
391            byte_count,
392            stream.raw(),
393        )
394    };
395    oxicuda_driver::check(rc)
396}
397
398// ---------------------------------------------------------------------------
399// Tests
400// ---------------------------------------------------------------------------
401
402#[cfg(test)]
403mod tests {
404    #[test]
405    fn copy_htod_signature_compiles() {
406        let _f: fn(&mut super::DeviceBuffer<f32>, &[f32]) -> super::CudaResult<()> =
407            super::copy_htod;
408        let _f2: fn(&mut [f32], &super::DeviceBuffer<f32>) -> super::CudaResult<()> =
409            super::copy_dtoh;
410    }
411
412    #[test]
413    fn copy_dtod_signature_compiles() {
414        let _f: fn(
415            &mut super::DeviceBuffer<f32>,
416            &super::DeviceBuffer<f32>,
417        ) -> super::CudaResult<()> = super::copy_dtod;
418    }
419
420    #[test]
421    fn async_raw_htod_signature_compiles() {
422        let _f: fn(
423            &mut super::DeviceBuffer<f32>,
424            &[f32],
425            &oxicuda_driver::stream::Stream,
426        ) -> super::CudaResult<()> = super::copy_htod_async_raw;
427    }
428
429    #[test]
430    fn async_raw_dtoh_signature_compiles() {
431        let _f: fn(
432            &mut [f32],
433            &super::DeviceBuffer<f32>,
434            &oxicuda_driver::stream::Stream,
435        ) -> super::CudaResult<()> = super::copy_dtoh_async_raw;
436    }
437
438    #[test]
439    fn async_dtod_signature_compiles() {
440        let _f: fn(
441            &mut super::DeviceBuffer<f32>,
442            &super::DeviceBuffer<f32>,
443            &oxicuda_driver::stream::Stream,
444        ) -> super::CudaResult<()> = super::copy_dtod_async;
445    }
446
447    #[test]
448    fn async_pinned_htod_signature_compiles() {
449        let _f: fn(
450            &mut super::DeviceBuffer<f32>,
451            &super::PinnedBuffer<f32>,
452            &oxicuda_driver::stream::Stream,
453        ) -> super::CudaResult<()> = super::copy_htod_async;
454    }
455
456    #[test]
457    fn region_dtoh_signature_compiles() {
458        type RegionDtohFn = fn(
459            &mut super::PinnedBuffer<f32>,
460            &super::DeviceBuffer<f32>,
461            usize,
462            usize,
463            &oxicuda_driver::stream::Stream,
464        ) -> super::CudaResult<()>;
465        let _f: RegionDtohFn = super::copy_dtoh_region_async;
466    }
467
468    #[test]
469    fn region_htod_signature_compiles() {
470        type RegionHtodFn = fn(
471            &mut super::DeviceBuffer<f32>,
472            usize,
473            &super::PinnedBuffer<f32>,
474            usize,
475            &oxicuda_driver::stream::Stream,
476        ) -> super::CudaResult<()>;
477        let _f: RegionHtodFn = super::copy_htod_region_async;
478    }
479
480    /// Regression test for F035: `copy_dtod_async` must actually enqueue a
481    /// real `cuMemcpyDtoDAsync_v2` on the given stream (rather than silently
482    /// falling back to a synchronous legacy-stream copy) and produce
483    /// correct data once the stream is synchronised.
484    #[cfg(feature = "gpu-tests")]
485    #[test]
486    fn copy_dtod_async_round_trips_on_device() {
487        if oxicuda_driver::init().is_err() {
488            eprintln!("skipping: CUDA init failed");
489            return;
490        }
491        let Ok(dev) = oxicuda_driver::device::Device::get(0) else {
492            eprintln!("skipping: no CUDA device");
493            return;
494        };
495        let Ok(ctx) = oxicuda_driver::context::Context::new(&dev) else {
496            eprintln!("skipping: context creation failed");
497            return;
498        };
499        let ctx = std::sync::Arc::new(ctx);
500        let Ok(stream) = oxicuda_driver::stream::Stream::new(&ctx) else {
501            eprintln!("skipping: stream creation failed");
502            return;
503        };
504
505        let host_src: Vec<f32> = (0..1024).map(|i| i as f32 * 0.5).collect();
506        let Ok(src) = super::DeviceBuffer::<f32>::from_host(&host_src) else {
507            eprintln!("skipping: src alloc failed");
508            return;
509        };
510        let Ok(mut dst) = super::DeviceBuffer::<f32>::from_host(&vec![0.0f32; 1024]) else {
511            eprintln!("skipping: dst alloc failed");
512            return;
513        };
514
515        match super::copy_dtod_async(&mut dst, &src, &stream) {
516            Ok(()) => {}
517            Err(oxicuda_driver::error::CudaError::NotSupported) => {
518                eprintln!("skipping: driver lacks cuMemcpyDtoDAsync_v2");
519                return;
520            }
521            Err(e) => panic!("copy_dtod_async failed: {e:?}"),
522        }
523        stream.synchronize().expect("stream sync failed");
524
525        let mut host_out = vec![0.0f32; 1024];
526        dst.copy_to_host(&mut host_out).expect("copy back failed");
527        assert_eq!(host_out, host_src);
528    }
529}