phase 0+1: project scaffold + xserv-cuda crate
- Cargo workspace with xserv-cuda crate - CUDA FFI bindings (cudart: memory, stream, device, error) - GpuBuffer RAII wrapper with H2D/D2H/D2D copy - CudaStream wrapper with RAII Drop - CachingAllocator with size-bucketed free lists - PinnedBuffer for page-locked host memory - Device info query via cudaDeviceGetAttribute - Vector-add CUDA kernel smoke test - Integration test suite (11 tests) - build.rs: cc crate compiles .cu for SM 12.0 - sync-and-build.sh for remote build on dash5 - Roadmap doc (docs/00-roadmap.md) and Phase 0+1 design doc Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
146
crates/xserv-cuda/src/memory.rs
Normal file
146
crates/xserv-cuda/src/memory.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use crate::error::{self, Result};
|
||||
use crate::ffi;
|
||||
use crate::stream::CudaStream;
|
||||
|
||||
/// RAII wrapper around a GPU memory allocation.
|
||||
pub struct GpuBuffer {
|
||||
ptr: *mut u8,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl GpuBuffer {
|
||||
pub fn alloc(len: usize) -> Result<Self> {
|
||||
assert!(len > 0, "cannot allocate 0 bytes on GPU");
|
||||
let mut ptr = std::ptr::null_mut();
|
||||
error::check(unsafe { ffi::cudaMalloc(&mut ptr, len) })?;
|
||||
Ok(Self { ptr, len })
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
|
||||
pub fn as_ptr(&self) -> *const u8 {
|
||||
self.ptr
|
||||
}
|
||||
|
||||
pub fn as_mut_ptr(&mut self) -> *mut u8 {
|
||||
self.ptr
|
||||
}
|
||||
|
||||
/// Copy data from host (CPU) slice to this GPU buffer.
|
||||
pub fn copy_from_host(&mut self, src: &[u8]) -> Result<()> {
|
||||
assert!(src.len() <= self.len, "source larger than buffer");
|
||||
error::check(unsafe {
|
||||
ffi::cudaMemcpy(self.ptr, src.as_ptr(), src.len(), ffi::CUDA_MEMCPY_H2D)
|
||||
})
|
||||
}
|
||||
|
||||
/// Copy data from this GPU buffer to a host (CPU) slice.
|
||||
pub fn copy_to_host(&self, dst: &mut [u8]) -> Result<()> {
|
||||
assert!(dst.len() <= self.len, "destination larger than buffer");
|
||||
error::check(unsafe {
|
||||
ffi::cudaMemcpy(dst.as_mut_ptr(), self.ptr, dst.len(), ffi::CUDA_MEMCPY_D2H)
|
||||
})
|
||||
}
|
||||
|
||||
/// Async copy from host to device on the given stream.
|
||||
/// Safety: `src` must remain valid until the stream operation completes.
|
||||
pub unsafe fn copy_from_host_async(&mut self, src: &[u8], stream: &CudaStream) -> Result<()> {
|
||||
assert!(src.len() <= self.len);
|
||||
error::check(ffi::cudaMemcpyAsync(
|
||||
self.ptr,
|
||||
src.as_ptr(),
|
||||
src.len(),
|
||||
ffi::CUDA_MEMCPY_H2D,
|
||||
stream.as_raw(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Async copy from device to host on the given stream.
|
||||
/// Safety: `dst` must remain valid until the stream operation completes.
|
||||
pub unsafe fn copy_to_host_async(&self, dst: &mut [u8], stream: &CudaStream) -> Result<()> {
|
||||
assert!(dst.len() <= self.len);
|
||||
error::check(ffi::cudaMemcpyAsync(
|
||||
dst.as_mut_ptr(),
|
||||
self.ptr,
|
||||
dst.len(),
|
||||
ffi::CUDA_MEMCPY_D2H,
|
||||
stream.as_raw(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Copy from another GPU buffer (D2D).
|
||||
pub fn copy_from_device(&mut self, src: &GpuBuffer) -> Result<()> {
|
||||
let n = src.len.min(self.len);
|
||||
error::check(unsafe {
|
||||
ffi::cudaMemcpy(self.ptr, src.ptr, n, ffi::CUDA_MEMCPY_D2D)
|
||||
})
|
||||
}
|
||||
|
||||
/// Fill buffer with zeros.
|
||||
pub fn zero(&mut self) -> Result<()> {
|
||||
error::check(unsafe { ffi::cudaMemset(self.ptr, 0, self.len) })
|
||||
}
|
||||
|
||||
/// Consume the buffer without freeing GPU memory. Returns the raw pointer and length.
|
||||
/// Caller is responsible for eventually calling cudaFree.
|
||||
pub fn into_raw(self) -> (*mut u8, usize) {
|
||||
let ptr = self.ptr;
|
||||
let len = self.len;
|
||||
std::mem::forget(self);
|
||||
(ptr, len)
|
||||
}
|
||||
|
||||
/// Reconstruct a GpuBuffer from a raw pointer + length.
|
||||
/// Safety: ptr must have been allocated with cudaMalloc, len must be correct.
|
||||
pub unsafe fn from_raw(ptr: *mut u8, len: usize) -> Self {
|
||||
Self { ptr, len }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GpuBuffer {
|
||||
fn drop(&mut self) {
|
||||
if !self.ptr.is_null() {
|
||||
unsafe { ffi::cudaFree(self.ptr) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for GpuBuffer {}
|
||||
|
||||
/// Pinned (page-locked) host memory for faster H2D/D2H transfers.
|
||||
pub struct PinnedBuffer {
|
||||
ptr: *mut u8,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl PinnedBuffer {
|
||||
pub fn alloc(len: usize) -> Result<Self> {
|
||||
let mut ptr = std::ptr::null_mut();
|
||||
error::check(unsafe { ffi::cudaMallocHost(&mut ptr, len) })?;
|
||||
Ok(Self { ptr, len })
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
|
||||
}
|
||||
|
||||
pub fn as_mut_slice(&mut self) -> &mut [u8] {
|
||||
unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) }
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PinnedBuffer {
|
||||
fn drop(&mut self) {
|
||||
if !self.ptr.is_null() {
|
||||
unsafe { ffi::cudaFreeHost(self.ptr) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for PinnedBuffer {}
|
||||
Reference in New Issue
Block a user