use std::marker::PhantomData;
use std::mem::MaybeUninit;
use std::num::Wrapping;
use std::sync::Arc;
use cuda_bindings::CUdeviceptr;
use crate::error::DriverError;
use crate::simt::context::CudaContext;
use crate::simt::pinned_host_buffer::PinnedHostBuffer;
use crate::simt::stream::CudaStream;
pub unsafe trait DeviceCopy: Copy {}
macro_rules! impl_device_copy {
($($ty:ty),+ $(,)?) => {
$(
unsafe impl DeviceCopy for $ty {}
)+
};
}
impl_device_copy!(
(),
i8,
i16,
i32,
i64,
i128,
isize,
u8,
u16,
u32,
u64,
u128,
usize,
f32,
f64
);
unsafe impl<T: DeviceCopy, const N: usize> DeviceCopy for [T; N] {}
unsafe impl<T: ?Sized> DeviceCopy for *const T {}
unsafe impl<T: ?Sized> DeviceCopy for *mut T {}
unsafe impl<T: ?Sized> DeviceCopy for PhantomData<T> {}
unsafe impl<T: DeviceCopy> DeviceCopy for MaybeUninit<T> {}
unsafe impl<T: DeviceCopy> DeviceCopy for Wrapping<T> {}
macro_rules! impl_device_copy_tuple {
($($name:ident),+ $(,)?) => {
unsafe impl<$($name: DeviceCopy),+> DeviceCopy for ($($name,)+) {}
};
}
impl_device_copy_tuple!(A);
impl_device_copy_tuple!(A, B);
impl_device_copy_tuple!(A, B, C);
impl_device_copy_tuple!(A, B, C, D);
impl_device_copy_tuple!(A, B, C, D, E);
impl_device_copy_tuple!(A, B, C, D, E, F);
impl_device_copy_tuple!(A, B, C, D, E, F, G);
impl_device_copy_tuple!(A, B, C, D, E, F, G, H);
#[cfg(feature = "f16")]
unsafe impl DeviceCopy for f16 {}
unsafe impl DeviceCopy for half::bf16 {}
unsafe impl DeviceCopy for half::f16 {}
pub struct DeviceBuffer<T> {
ptr: CUdeviceptr,
len: usize,
num_bytes: usize,
ctx: Arc<CudaContext>,
dealloc_stream: Option<Arc<CudaStream>>,
_marker: PhantomData<T>,
}
unsafe impl<T: Send> Send for DeviceBuffer<T> {}
unsafe impl<T: Send + Sync> Sync for DeviceBuffer<T> {}
impl<T> Drop for DeviceBuffer<T> {
fn drop(&mut self) {
if self.ptr != 0 {
self.ctx.record_err(self.ctx.bind_to_thread());
let result = match &self.dealloc_stream {
Some(stream) => match self.ctx.synchronize() {
Ok(()) => unsafe {
crate::simt::memory::free_async(self.ptr, stream.cu_stream())
},
Err(error) => Err(error),
},
None => unsafe { crate::simt::memory::free_sync(self.ptr) },
};
self.ctx.record_err(result);
}
}
}
impl<T> DeviceBuffer<T> {
#[inline]
pub fn cu_deviceptr(&self) -> CUdeviceptr {
self.ptr
}
#[inline]
pub fn len(&self) -> usize {
self.len
}
#[inline]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[inline]
pub fn num_bytes(&self) -> usize {
self.num_bytes
}
#[inline]
pub fn context(&self) -> &Arc<CudaContext> {
&self.ctx
}
pub unsafe fn from_raw_parts(ptr: CUdeviceptr, len: usize, ctx: Arc<CudaContext>) -> Self {
unsafe { Self::from_raw_parts_with_dealloc_stream(ptr, len, ctx, None) }
}
unsafe fn from_raw_parts_with_dealloc_stream(
ptr: CUdeviceptr,
len: usize,
ctx: Arc<CudaContext>,
dealloc_stream: Option<Arc<CudaStream>>,
) -> Self {
let num_bytes =
allocation_size::<T>(len).expect("DeviceBuffer::from_raw_parts byte size overflow");
Self {
ptr,
len,
num_bytes,
ctx,
dealloc_stream,
_marker: PhantomData,
}
}
pub fn into_raw_parts(self) -> (CUdeviceptr, usize, Arc<CudaContext>) {
let (ptr, len, ctx, _dealloc_stream) = self.into_all_raw_parts();
(ptr, len, ctx)
}
fn into_all_raw_parts(
self,
) -> (
CUdeviceptr,
usize,
Arc<CudaContext>,
Option<Arc<CudaStream>>,
) {
let this = std::mem::ManuallyDrop::new(self);
let ptr = this.ptr;
let len = this.len;
let ctx = unsafe { std::ptr::read(&this.ctx) };
let dealloc_stream = unsafe { std::ptr::read(&this.dealloc_stream) };
(ptr, len, ctx, dealloc_stream)
}
pub fn cast_elem<A>(self) -> DeviceBuffer<A> {
assert_eq!(
std::mem::size_of::<A>(),
std::mem::size_of::<T>(),
"cast_elem requires the same element size"
);
assert_eq!(
std::mem::align_of::<A>(),
std::mem::align_of::<T>(),
"cast_elem requires the same element alignment"
);
let (ptr, len, ctx, dealloc_stream) = self.into_all_raw_parts();
unsafe {
DeviceBuffer::<A>::from_raw_parts_with_dealloc_stream(ptr, len, ctx, dealloc_stream)
}
}
pub fn cast_chunks<A>(self) -> Result<DeviceBuffer<A>, Self> {
let bytes = self.len.saturating_mul(std::mem::size_of::<T>());
let Some(new_len) = chunk_cast_len(
bytes,
self.ptr as usize,
std::mem::size_of::<A>(),
std::mem::align_of::<A>(),
) else {
return Err(self);
};
let (ptr, _len, ctx, dealloc_stream) = self.into_all_raw_parts();
Ok(unsafe {
DeviceBuffer::<A>::from_raw_parts_with_dealloc_stream(ptr, new_len, ctx, dealloc_stream)
})
}
#[must_use]
pub fn can_cast_chunks<A>(&self) -> bool {
let bytes = self.len.saturating_mul(std::mem::size_of::<T>());
chunk_cast_len(
bytes,
self.ptr as usize,
std::mem::size_of::<A>(),
std::mem::align_of::<A>(),
)
.is_some()
}
}
impl<T: DeviceCopy> DeviceBuffer<T> {
pub fn from_host(stream: &CudaStream, data: &[T]) -> Result<Self, DriverError> {
let ctx = stream.context().clone();
let len = data.len();
let num_bytes = allocation_size::<T>(len)?;
if num_bytes == 0 {
return Ok(unsafe { Self::from_raw_parts(0, len, ctx) });
}
let ptr = unsafe { crate::simt::memory::malloc_sync(num_bytes)? };
let buf = unsafe { Self::from_raw_parts(ptr, len, ctx) };
let enqueue_result = unsafe {
crate::simt::memory::memcpy_htod_async(
buf.ptr,
data.as_ptr(),
num_bytes,
stream.cu_stream(),
)
};
let sync_result = stream.synchronize();
enqueue_result?;
sync_result?;
Ok(buf)
}
pub unsafe fn from_host_async_unchecked(
stream: &CudaStream,
data: &[T],
) -> Result<Self, DriverError> {
let ctx = stream.context().clone();
let len = data.len();
let num_bytes = std::mem::size_of_val(data);
if num_bytes == 0 {
return Ok(unsafe { Self::from_raw_parts(0, len, ctx) });
}
let ptr = unsafe { crate::simt::memory::malloc_sync(num_bytes)? };
let buf = unsafe { Self::from_raw_parts(ptr, len, ctx) };
unsafe {
crate::simt::memory::memcpy_htod_async(
buf.ptr,
data.as_ptr(),
num_bytes,
stream.cu_stream(),
)?;
}
Ok(buf)
}
pub unsafe fn from_pinned_host(
stream: &CudaStream,
data: &PinnedHostBuffer<T>,
) -> Result<Self, DriverError> {
debug_assert!(
Arc::ptr_eq(data.context(), stream.context()),
"pinned host buffer and stream must belong to the same CUDA context"
);
unsafe { Self::from_host_async_unchecked(stream, data.as_slice()) }
}
pub fn zeroed(stream: &CudaStream, len: usize) -> Result<Self, DriverError> {
let ctx = stream.context().clone();
let num_bytes = allocation_size::<T>(len)?;
if num_bytes == 0 {
return Ok(unsafe { Self::from_raw_parts(0, len, ctx) });
}
let ptr = unsafe { crate::simt::memory::malloc_sync(num_bytes)? };
let buf = unsafe { Self::from_raw_parts(ptr, len, ctx) };
unsafe {
crate::simt::memory::memset_d8_async(buf.ptr, 0, num_bytes, stream.cu_stream())?;
}
Ok(buf)
}
pub fn to_host_vec(&self, stream: &CudaStream) -> Result<Vec<T>, DriverError> {
let mut host = Vec::with_capacity(self.len);
unsafe {
crate::simt::memory::memcpy_dtoh_async(
host.as_mut_ptr(),
self.ptr,
self.num_bytes(),
stream.cu_stream(),
)?;
}
stream.synchronize()?;
unsafe { host.set_len(self.len) };
Ok(host)
}
pub fn copy_to_host(&self, stream: &CudaStream, dst: &mut [T]) -> Result<(), DriverError> {
assert!(
dst.len() >= self.len,
"destination slice too small: {} < {}",
dst.len(),
self.len
);
unsafe {
crate::simt::memory::memcpy_dtoh_async(
dst.as_mut_ptr(),
self.ptr,
self.num_bytes(),
stream.cu_stream(),
)?;
}
stream.synchronize()
}
pub fn copy_to_pinned_host(
&self,
stream: &CudaStream,
dst: &mut PinnedHostBuffer<T>,
) -> Result<(), DriverError> {
unsafe { self.copy_to_pinned_host_async(stream, dst)? };
stream.synchronize()
}
pub unsafe fn copy_to_pinned_host_async(
&self,
stream: &CudaStream,
dst: &mut PinnedHostBuffer<T>,
) -> Result<(), DriverError> {
debug_assert!(
Arc::ptr_eq(dst.context(), stream.context()),
"pinned host buffer and stream must belong to the same CUDA context"
);
assert!(
dst.len() >= self.len,
"destination pinned host buffer too small: {} < {}",
dst.len(),
self.len
);
unsafe {
crate::simt::memory::memcpy_dtoh_async(
dst.as_mut_ptr(),
self.ptr,
self.num_bytes(),
stream.cu_stream(),
)
}
}
pub unsafe fn copy_from_pinned_host_async(
&mut self,
stream: &CudaStream,
src: &PinnedHostBuffer<T>,
) -> Result<(), DriverError> {
debug_assert!(
Arc::ptr_eq(src.context(), stream.context()),
"pinned host buffer and stream must belong to the same CUDA context"
);
assert!(
src.len() <= self.len,
"source pinned host buffer too large: {} > {}",
src.len(),
self.len
);
let num_bytes = src.num_bytes();
unsafe {
crate::simt::memory::memcpy_htod_async(
self.ptr,
src.as_ptr(),
num_bytes,
stream.cu_stream(),
)
}
}
pub unsafe fn uninitialized_async(
stream: &Arc<CudaStream>,
len: usize,
) -> Result<Self, DriverError> {
let ctx = stream.context().clone();
let num_bytes = allocation_size::<T>(len)?;
if num_bytes == 0 {
return Ok(unsafe { Self::from_raw_parts(0, len, ctx) });
}
let ptr = unsafe { crate::simt::memory::malloc_async(stream.cu_stream(), num_bytes)? };
Ok(Self {
ptr,
len,
num_bytes,
ctx,
dealloc_stream: Some(stream.clone()),
_marker: PhantomData,
})
}
pub fn copy_from_device_async(
&mut self,
other: &DeviceBuffer<T>,
stream: &CudaStream,
) -> Result<(), DriverError> {
assert_eq!(
self.len, other.len,
"device-to-device copy length mismatch: dst {} != src {}",
self.len, other.len
);
if self.num_bytes() == 0 {
return Ok(());
}
unsafe {
crate::simt::memory::memcpy_dtod_async(
self.ptr,
other.ptr,
self.num_bytes(),
stream.cu_stream(),
)
}
}
pub fn copy_from_host(&mut self, stream: &CudaStream, src: &[T]) -> Result<(), DriverError> {
let enqueue_result = unsafe { self.copy_from_host_async_unchecked(stream, src) };
let sync_result = if self.num_bytes() == 0 {
Ok(())
} else {
stream.synchronize()
};
enqueue_result?;
sync_result
}
pub unsafe fn copy_from_host_async_unchecked(
&mut self,
stream: &CudaStream,
src: &[T],
) -> Result<(), DriverError> {
assert_eq!(
self.len,
src.len(),
"host-to-device copy length mismatch: dst {} != src {}",
self.len,
src.len()
);
if self.num_bytes() == 0 {
return Ok(());
}
unsafe {
crate::simt::memory::memcpy_htod_async(
self.ptr,
src.as_ptr(),
self.num_bytes(),
stream.cu_stream(),
)
}
}
pub unsafe fn drop_async(mut self, stream: &CudaStream) -> Result<(), DriverError> {
if self.ctx.as_ref() != stream.context().as_ref() {
return Err(DriverError(
cuda_bindings::cudaError_enum_CUDA_ERROR_INVALID_CONTEXT,
));
}
if self.ptr == 0 {
return Ok(());
}
self.ctx.bind_to_thread()?;
if let Some(allocation_stream) = &self.dealloc_stream {
if allocation_stream.as_ref() != stream {
stream.join(allocation_stream)?;
}
}
let ptr = self.ptr;
self.ptr = 0;
unsafe { crate::simt::memory::free_async(ptr, stream.cu_stream()) }
}
pub fn zero_async(&mut self, stream: &CudaStream) -> Result<(), DriverError> {
if self.num_bytes() == 0 {
return Ok(());
}
unsafe {
crate::simt::memory::memset_d8_async(self.ptr, 0, self.num_bytes(), stream.cu_stream())
}
}
}
fn allocation_size<T>(len: usize) -> Result<usize, DriverError> {
len.checked_mul(std::mem::size_of::<T>()).ok_or(DriverError(
cuda_bindings::cudaError_enum_CUDA_ERROR_INVALID_VALUE,
))
}
fn chunk_cast_len(bytes: usize, addr: usize, elem_size: usize, align: usize) -> Option<usize> {
if elem_size == 0 || align == 0 {
return None;
}
if !bytes.is_multiple_of(elem_size) {
return None;
}
if !addr.is_multiple_of(align) {
return None;
}
Some(bytes / elem_size)
}
#[cfg(test)]
mod chunk_cast_tests {
use super::chunk_cast_len;
#[test]
fn accepts_an_aligned_allocation_that_divides() {
assert_eq!(chunk_cast_len(4096, 0x1000, 16, 16), Some(256));
assert_eq!(chunk_cast_len(4096, 0x1000, 8, 8), Some(512));
assert_eq!(chunk_cast_len(4096, 0x1000, 4, 4), Some(1024));
}
#[test]
fn refuses_a_byte_extent_that_does_not_divide() {
assert_eq!(
chunk_cast_len(12, 0x1000, 16, 16),
None,
"3 f32 into a quad"
);
assert_eq!(chunk_cast_len(4100, 0x1000, 16, 16), None);
assert_eq!(chunk_cast_len(4088, 0x1000, 16, 16), None);
}
#[test]
fn refuses_a_misaligned_base() {
assert_eq!(chunk_cast_len(4096, 0x1004, 16, 16), None, "4-byte offset");
assert_eq!(chunk_cast_len(4096, 0x1008, 16, 16), None, "8-byte offset");
assert_eq!(chunk_cast_len(4096, 0x1008, 8, 8), Some(512));
assert_eq!(chunk_cast_len(4096, 0x1004, 4, 4), Some(1024));
}
#[test]
fn a_cuda_allocation_alignment_satisfies_every_vector_type() {
for base in [0usize, 256, 512, 4096, 1 << 20] {
for align in [4usize, 8, 16] {
assert!(
chunk_cast_len(4096, base, align, align).is_some(),
"base {base:#x} should satisfy align {align}"
);
}
}
}
#[test]
fn rejects_degenerate_parameters() {
assert_eq!(chunk_cast_len(4096, 0x1000, 0, 16), None, "zero-sized");
assert_eq!(chunk_cast_len(4096, 0x1000, 16, 0), None);
assert_eq!(chunk_cast_len(0, 0x1000, 16, 16), Some(0));
}
}