use std::ffi::c_void;
use oxicuda_driver::error::{CudaError, CudaResult};
use oxicuda_driver::loader::try_driver;
use oxicuda_driver::stream::Stream;
use crate::device_buffer::DeviceBuffer;
use crate::host_buffer::PinnedBuffer;
pub unsafe trait StagingPod: Copy {}
macro_rules! impl_staging_pod {
($($t:ty),* $(,)?) => {
$(
unsafe impl StagingPod for $t {}
)*
};
}
impl_staging_pod!(
u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, usize, isize, f32, f64
);
#[cfg(feature = "half")]
unsafe impl StagingPod for half::f16 {}
#[cfg(feature = "half")]
unsafe impl StagingPod for half::bf16 {}
pub const DEFAULT_AUTO_STAGE_MAX_BYTES: usize = 512 * 1024;
const CAPACITY_GRANULARITY: usize = 64 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct StagingStats {
pub allocations: u64,
pub staged_transfers: u64,
pub direct_transfers: u64,
pub bytes_uploaded: u64,
pub bytes_downloaded: u64,
}
pub struct StagingBuffer {
pinned: Option<PinnedBuffer<u8>>,
auto_stage_max_bytes: usize,
stats: StagingStats,
}
impl Default for StagingBuffer {
fn default() -> Self {
Self::new()
}
}
impl StagingBuffer {
#[must_use]
pub const fn new() -> Self {
Self {
pinned: None,
auto_stage_max_bytes: DEFAULT_AUTO_STAGE_MAX_BYTES,
stats: StagingStats {
allocations: 0,
staged_transfers: 0,
direct_transfers: 0,
bytes_uploaded: 0,
bytes_downloaded: 0,
},
}
}
pub fn with_capacity(bytes: usize) -> CudaResult<Self> {
let mut this = Self::new();
this.reserve(bytes)?;
Ok(this)
}
#[inline]
#[must_use]
pub fn capacity(&self) -> usize {
self.pinned.as_ref().map_or(0, PinnedBuffer::len)
}
#[inline]
#[must_use]
pub fn stats(&self) -> StagingStats {
self.stats
}
#[inline]
#[must_use]
pub fn auto_stage_max_bytes(&self) -> usize {
self.auto_stage_max_bytes
}
#[inline]
pub fn set_auto_stage_max_bytes(&mut self, bytes: usize) {
self.auto_stage_max_bytes = bytes;
}
pub fn reserve(&mut self, bytes: usize) -> CudaResult<()> {
if bytes == 0 {
return Err(CudaError::InvalidValue);
}
if self.capacity() >= bytes {
return Ok(());
}
let rounded = bytes
.checked_next_multiple_of(CAPACITY_GRANULARITY)
.ok_or(CudaError::InvalidValue)?;
self.pinned = None;
self.pinned = Some(PinnedBuffer::<u8>::alloc(rounded)?);
self.stats.allocations += 1;
Ok(())
}
pub fn shrink_to_fit(&mut self) {
self.pinned = None;
}
pub fn upload_with<T, F>(
&mut self,
dst: &mut DeviceBuffer<T>,
n: usize,
stream: &Stream,
fill: F,
) -> CudaResult<()>
where
T: StagingPod,
F: FnOnce(&mut [T]),
{
let byte_size = Self::check_extent::<T>(n, dst.len())?;
self.reserve(byte_size)?;
fill(self.typed_mut::<T>(n)?);
self.enqueue_htod(dst.as_device_ptr(), byte_size, stream)?;
stream.synchronize()?;
self.stats.staged_transfers += 1;
self.stats.bytes_uploaded += byte_size as u64;
Ok(())
}
pub fn download_into<T: StagingPod>(
&mut self,
src: &DeviceBuffer<T>,
n: usize,
stream: &Stream,
) -> CudaResult<&[T]> {
let byte_size = Self::check_extent::<T>(n, src.len())?;
self.reserve(byte_size)?;
self.enqueue_dtoh(src.as_device_ptr(), byte_size, stream)?;
stream.synchronize()?;
self.stats.staged_transfers += 1;
self.stats.bytes_downloaded += byte_size as u64;
Ok(self.typed_mut::<T>(n)?)
}
pub fn upload<T: StagingPod>(
&mut self,
dst: &mut DeviceBuffer<T>,
src: &[T],
stream: &Stream,
) -> CudaResult<()> {
let byte_size = Self::check_extent::<T>(src.len(), dst.len())?;
if byte_size > self.auto_stage_max_bytes {
stream.synchronize()?;
dst.copy_from_host(src)?;
self.stats.direct_transfers += 1;
self.stats.bytes_uploaded += byte_size as u64;
return Ok(());
}
let n = src.len();
self.upload_with(dst, n, stream, |staged: &mut [T]| {
staged.copy_from_slice(src);
})
}
pub fn download<T: StagingPod>(
&mut self,
dst: &mut [T],
src: &DeviceBuffer<T>,
stream: &Stream,
) -> CudaResult<()> {
let byte_size = Self::check_extent::<T>(dst.len(), src.len())?;
if byte_size > self.auto_stage_max_bytes {
stream.synchronize()?;
src.copy_to_host(dst)?;
self.stats.direct_transfers += 1;
self.stats.bytes_downloaded += byte_size as u64;
return Ok(());
}
let n = dst.len();
let staged = self.download_into(src, n, stream)?;
dst.copy_from_slice(staged);
Ok(())
}
fn check_extent<T>(n: usize, device_len: usize) -> CudaResult<usize> {
if n == 0 || n != device_len {
return Err(CudaError::InvalidValue);
}
n.checked_mul(std::mem::size_of::<T>())
.ok_or(CudaError::InvalidValue)
}
fn typed_mut<T: StagingPod>(&mut self, n: usize) -> CudaResult<&mut [T]> {
let buf = self.pinned.as_mut().ok_or(CudaError::InvalidValue)?;
let byte_size = n
.checked_mul(std::mem::size_of::<T>())
.ok_or(CudaError::InvalidValue)?;
if byte_size > buf.len() {
return Err(CudaError::InvalidValue);
}
let ptr = buf.as_mut_ptr();
if !ptr.cast::<T>().is_aligned() {
return Err(CudaError::InvalidValue);
}
Ok(unsafe { std::slice::from_raw_parts_mut(ptr.cast::<T>(), n) })
}
fn enqueue_htod(
&self,
dst_ptr: oxicuda_driver::ffi::CUdeviceptr,
byte_size: usize,
stream: &Stream,
) -> CudaResult<()> {
let buf = self.pinned.as_ref().ok_or(CudaError::InvalidValue)?;
if byte_size > buf.len() {
return Err(CudaError::InvalidValue);
}
let api = try_driver()?;
let rc = unsafe {
(api.cu_memcpy_htod_async_v2)(
dst_ptr,
buf.as_ptr().cast::<c_void>(),
byte_size,
stream.raw(),
)
};
oxicuda_driver::check(rc)
}
fn enqueue_dtoh(
&mut self,
src_ptr: oxicuda_driver::ffi::CUdeviceptr,
byte_size: usize,
stream: &Stream,
) -> CudaResult<()> {
let buf = self.pinned.as_mut().ok_or(CudaError::InvalidValue)?;
if byte_size > buf.len() {
return Err(CudaError::InvalidValue);
}
let api = try_driver()?;
let rc = unsafe {
(api.cu_memcpy_dtoh_async_v2)(
buf.as_mut_ptr().cast::<c_void>(),
src_ptr,
byte_size,
stream.raw(),
)
};
oxicuda_driver::check(rc)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_pins_nothing() {
let staging = StagingBuffer::new();
assert_eq!(staging.capacity(), 0);
assert_eq!(staging.stats().allocations, 0);
assert_eq!(staging.auto_stage_max_bytes(), DEFAULT_AUTO_STAGE_MAX_BYTES);
}
#[test]
fn default_matches_new() {
assert_eq!(
StagingBuffer::default().auto_stage_max_bytes(),
StagingBuffer::new().auto_stage_max_bytes()
);
assert_eq!(StagingBuffer::default().capacity(), 0);
}
#[test]
fn threshold_is_overridable() {
let mut staging = StagingBuffer::new();
staging.set_auto_stage_max_bytes(0);
assert_eq!(staging.auto_stage_max_bytes(), 0);
staging.set_auto_stage_max_bytes(usize::MAX);
assert_eq!(staging.auto_stage_max_bytes(), usize::MAX);
}
#[test]
fn reserve_rejects_zero() {
let mut staging = StagingBuffer::new();
assert_eq!(staging.reserve(0), Err(CudaError::InvalidValue));
}
#[test]
fn check_extent_rejects_mismatch_and_zero() {
assert_eq!(
StagingBuffer::check_extent::<f32>(4, 5),
Err(CudaError::InvalidValue)
);
assert_eq!(
StagingBuffer::check_extent::<f32>(0, 0),
Err(CudaError::InvalidValue)
);
assert_eq!(StagingBuffer::check_extent::<f32>(4, 4), Ok(16));
}
#[test]
fn check_extent_rejects_byte_overflow() {
assert_eq!(
StagingBuffer::check_extent::<u64>(usize::MAX, usize::MAX),
Err(CudaError::InvalidValue)
);
}
#[test]
fn capacity_granularity_is_a_power_of_two_page_multiple() {
assert!(CAPACITY_GRANULARITY.is_power_of_two());
assert_eq!(CAPACITY_GRANULARITY % 4096, 0);
}
#[test]
fn stats_start_zeroed() {
assert_eq!(StagingBuffer::new().stats(), StagingStats::default());
}
#[cfg(feature = "gpu-tests")]
mod gpu_tests {
use super::*;
use std::sync::Arc;
fn fixture() -> Option<(Arc<oxicuda_driver::Context>, Stream)> {
oxicuda_driver::init().ok()?;
if oxicuda_driver::Device::count().ok()? == 0 {
return None;
}
let dev = oxicuda_driver::Device::get(0).ok()?;
let ctx = Arc::new(oxicuda_driver::Context::new(&dev).ok()?);
let stream = Stream::new(&ctx).ok()?;
Some((ctx, stream))
}
#[test]
fn upload_with_download_into_round_trip() {
let Some((_ctx, stream)) = fixture() else {
eprintln!("skipping: no CUDA driver/device");
return;
};
let n = 128 * 128 * 3;
let mut staging = StagingBuffer::new();
let mut d = DeviceBuffer::<f32>::alloc(n).expect("alloc");
staging
.upload_with(&mut d, n, &stream, |dst: &mut [f32]| {
for (i, v) in dst.iter_mut().enumerate() {
*v = (i % 977) as f32 * 0.25;
}
})
.expect("upload_with");
let got = staging
.download_into(&d, n, &stream)
.expect("download_into");
for (i, &v) in got.iter().enumerate() {
assert_eq!(v, (i % 977) as f32 * 0.25, "element {i}");
}
}
#[test]
fn repeated_transfers_reuse_one_allocation() {
let Some((_ctx, stream)) = fixture() else {
eprintln!("skipping: no CUDA driver/device");
return;
};
let n = 4096;
let mut staging = StagingBuffer::new();
let mut d = DeviceBuffer::<f32>::alloc(n).expect("alloc");
let src = vec![1.5f32; n];
let mut dst = vec![0.0f32; n];
for _ in 0..64 {
staging.upload(&mut d, &src, &stream).expect("upload");
staging.download(&mut dst, &d, &stream).expect("download");
}
assert_eq!(dst, src);
let stats = staging.stats();
assert_eq!(
stats.allocations, 1,
"staging buffer re-pinned host memory instead of reusing it"
);
assert_eq!(stats.staged_transfers, 128);
assert_eq!(stats.direct_transfers, 0);
}
#[test]
fn growth_is_high_water_marked() {
let Some((_ctx, stream)) = fixture() else {
eprintln!("skipping: no CUDA driver/device");
return;
};
let mut staging = StagingBuffer::new();
let small = 1024usize;
let large = 64 * 1024usize;
let mut d_small = DeviceBuffer::<f32>::alloc(small).expect("alloc small");
let mut d_large = DeviceBuffer::<f32>::alloc(large).expect("alloc large");
staging
.upload(&mut d_small, &vec![1.0f32; small], &stream)
.expect("small");
assert_eq!(staging.stats().allocations, 1);
let cap_after_small = staging.capacity();
staging
.upload(&mut d_large, &vec![2.0f32; large], &stream)
.expect("large");
assert_eq!(staging.stats().allocations, 2, "growth should re-pin once");
assert!(staging.capacity() > cap_after_small);
let cap_after_large = staging.capacity();
staging
.upload(&mut d_small, &vec![3.0f32; small], &stream)
.expect("small again");
assert_eq!(staging.stats().allocations, 2, "shrinking must not re-pin");
assert_eq!(staging.capacity(), cap_after_large);
}
#[test]
fn oversized_transfers_bypass_staging_and_stay_correct() {
let Some((_ctx, stream)) = fixture() else {
eprintln!("skipping: no CUDA driver/device");
return;
};
let n = 256 * 1024; let mut staging = StagingBuffer::new();
let mut d = DeviceBuffer::<f32>::alloc(n).expect("alloc");
let src: Vec<f32> = (0..n).map(|i| (i % 613) as f32).collect();
let mut dst = vec![0.0f32; n];
staging.upload(&mut d, &src, &stream).expect("upload");
staging.download(&mut dst, &d, &stream).expect("download");
assert_eq!(dst, src);
let stats = staging.stats();
assert_eq!(stats.direct_transfers, 2, "should have bypassed staging");
assert_eq!(stats.staged_transfers, 0);
assert_eq!(
stats.allocations, 0,
"bypassed transfers must not pin host memory"
);
assert_eq!(stats.bytes_uploaded, (n * 4) as u64);
assert_eq!(stats.bytes_downloaded, (n * 4) as u64);
staging.set_auto_stage_max_bytes(usize::MAX);
dst.fill(0.0);
staging
.upload(&mut d, &src, &stream)
.expect("upload staged");
staging
.download(&mut dst, &d, &stream)
.expect("download staged");
assert_eq!(dst, src);
assert_eq!(staging.stats().staged_transfers, 2);
}
#[test]
fn length_mismatch_is_rejected() {
let Some((_ctx, stream)) = fixture() else {
eprintln!("skipping: no CUDA driver/device");
return;
};
let mut staging = StagingBuffer::new();
let mut d = DeviceBuffer::<f32>::alloc(64).expect("alloc");
let src = vec![0.0f32; 32];
assert_eq!(
staging.upload(&mut d, &src, &stream),
Err(CudaError::InvalidValue)
);
let mut dst = vec![0.0f32; 32];
assert_eq!(
staging.download(&mut dst, &d, &stream),
Err(CudaError::InvalidValue)
);
assert_eq!(
staging.upload_with(&mut d, 32, &stream, |_: &mut [f32]| {}),
Err(CudaError::InvalidValue)
);
}
#[test]
fn with_capacity_preallocates() {
let Some((_ctx, stream)) = fixture() else {
eprintln!("skipping: no CUDA driver/device");
return;
};
let n = 8192usize;
let mut staging = StagingBuffer::with_capacity(n * 4).expect("with_capacity");
assert!(staging.capacity() >= n * 4);
assert_eq!(staging.stats().allocations, 1);
let mut d = DeviceBuffer::<f32>::alloc(n).expect("alloc");
for _ in 0..16 {
staging
.upload(&mut d, &vec![7.0f32; n], &stream)
.expect("upload");
}
assert_eq!(
staging.stats().allocations,
1,
"pre-sized buffer must never re-pin in the hot loop"
);
}
}
}