use std::{
collections::HashMap,
sync::{Arc, LazyLock, Mutex},
};
use cuda_core::{
simt::memory::{memcpy_dtod_async, memcpy_dtoh_async, memcpy_htod_async},
sys::CUdeviceptr,
CudaContext, CudaEvent, CudaStream, DeviceBuffer, DriverError, LaunchConfig1D, LaunchConfig2D,
PinnedHostBuffer,
};
use cuda_device::atomic::DeviceAtomicU64;
use crate::{
cuda_error,
cuda_oxide_kernels::{kernels, BLOCK_SIZE},
error::LaError,
};
type DeviceEntry = (Arc<CudaContext>, Arc<kernels::LoadedModule>, u32);
static DEVICES: LazyLock<Mutex<HashMap<usize, DeviceEntry>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub(crate) struct ReduceScratch {
pub(crate) out: DeviceBuffer<DeviceAtomicU64>,
pub(crate) partials: DeviceBuffer<u8>,
pub(crate) readback: PinnedHostBuffer<u64>,
pub(crate) done: CudaEvent,
}
impl ReduceScratch {
fn new(stream: &CudaStream, nbatch: usize, target_blocks: u32) -> Result<Self, LaError> {
let fail = |e| cuda_error!(Other, format!("Failed to allocate scratch: {}", e));
let partials: DeviceBuffer<u8> = DeviceBuffer::zeroed(
stream,
nbatch.max(target_blocks as usize) * size_of::<f64>(),
)
.map_err(fail)?;
assert_eq!(
partials.cu_deviceptr() % align_of::<f64>() as u64,
0,
"reduction scratch is not aligned for f64"
);
Ok(Self {
out: DeviceBuffer::<u64>::zeroed(stream, 1)
.map_err(fail)?
.cast_elem(),
partials,
readback: PinnedHostBuffer::zeroed(stream.context(), 1).map_err(fail)?,
done: stream.context().new_event(None).map_err(fail)?,
})
}
}
impl std::fmt::Debug for ReduceScratch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReduceScratch")
.field("partials", &self.partials.len())
.finish()
}
}
#[derive(Clone, Debug)]
pub struct OxideContext {
pub(crate) stream: Arc<CudaStream>,
pub(crate) module: Arc<kernels::LoadedModule>,
pub(crate) target_blocks: u32,
pub(crate) scratch: Arc<Mutex<ReduceScratch>>,
nbatch: usize,
}
impl OxideContext {
fn get_device_and_module(ordinal: usize) -> Result<DeviceEntry, LaError> {
let mut devices = DEVICES.lock().unwrap();
let (context, module, sms) = match devices.get(&ordinal) {
Some(entry) => entry.clone(),
None => {
let context = CudaContext::new(ordinal)
.map_err(|e| cuda_error!(CudaInitializationError, e.to_string()))?;
let module = unsafe { kernels::load(&context) }.map_err(|e| {
cuda_error!(Other, format!("Failed to load kernel module: {}", e))
})?;
let sms = context
.multiprocessor_count()
.map_err(|e| cuda_error!(CudaInitializationError, e.to_string()))?;
let entry = (context, Arc::new(module), sms);
devices.insert(ordinal, entry.clone());
entry
}
};
context
.bind_to_thread()
.map_err(|e| cuda_error!(CudaInitializationError, e.to_string()))?;
Ok((context, module, sms))
}
pub fn new(ordinal: usize) -> Result<Self, LaError> {
let (context, module, sms) = Self::get_device_and_module(ordinal)?;
let stream = context.default_stream();
let target_blocks = sms * 4;
let scratch = ReduceScratch::new(&stream, 1, target_blocks)?;
Ok(Self {
stream,
module,
target_blocks,
scratch: Arc::new(Mutex::new(scratch)),
nbatch: 1,
})
}
pub fn with_nbatch(self, nbatch: usize) -> Self {
self.clone_with_nbatch_inner(nbatch)
.expect("Failed to allocate reduction scratch")
}
fn clone_with_nbatch_inner(&self, nbatch: usize) -> Result<Self, LaError> {
assert!(nbatch > 0, "nbatch must be > 0");
let scratch = ReduceScratch::new(&self.stream, nbatch, self.target_blocks)?;
Ok(Self {
stream: self.stream.clone(),
module: self.module.clone(),
target_blocks: self.target_blocks,
scratch: Arc::new(Mutex::new(scratch)),
nbatch,
})
}
pub(crate) fn config_1d(n: u32) -> LaunchConfig1D {
Self::config_1d_blocks(n.div_ceil(BLOCK_SIZE))
}
pub(crate) fn config_1d_blocks(nblocks: u32) -> LaunchConfig1D {
LaunchConfig1D::new(nblocks, BLOCK_SIZE, 0)
}
pub(crate) fn config_2d(nstates: u32, nbatch: u32) -> LaunchConfig2D {
LaunchConfig2D::new((nstates.div_ceil(BLOCK_SIZE), nbatch), (BLOCK_SIZE, 1), 0)
}
}
impl Default for OxideContext {
fn default() -> Self {
Self::new(0).unwrap()
}
}
impl crate::Context for OxideContext {
fn nbatch(&self) -> usize {
self.nbatch
}
fn clone_with_nbatch(&self, nbatch: usize) -> Result<Self, LaError> {
self.clone_with_nbatch_inner(nbatch)
}
fn synchronize(&self) {
self.stream
.synchronize()
.expect("Failed to synchronize stream");
}
}
fn offset_ptr<T>(buf: &DeviceBuffer<T>, offset: usize, len: usize) -> CUdeviceptr {
assert!(
offset + len <= buf.len(),
"range {offset}..{} out of bounds for buffer of {}",
offset + len,
buf.len()
);
buf.cu_deviceptr() + (offset * std::mem::size_of::<T>()) as u64
}
pub(crate) fn read_at<T>(
stream: &CudaStream,
buf: &DeviceBuffer<T>,
offset: usize,
dst: &mut [T],
) -> Result<(), DriverError> {
let src = offset_ptr(buf, offset, dst.len());
unsafe {
memcpy_dtoh_async(
dst.as_mut_ptr(),
src,
std::mem::size_of_val(dst),
stream.cu_stream(),
)?;
}
stream.synchronize()
}
pub(crate) fn write_at<T>(
stream: &CudaStream,
buf: &DeviceBuffer<T>,
offset: usize,
src: &[T],
) -> Result<(), DriverError> {
let dst = offset_ptr(buf, offset, src.len());
unsafe {
memcpy_htod_async(
dst,
src.as_ptr(),
std::mem::size_of_val(src),
stream.cu_stream(),
)?;
}
stream.synchronize()
}
pub(crate) fn copy_at<T>(
stream: &CudaStream,
dst: &DeviceBuffer<T>,
dst_offset: usize,
src: &DeviceBuffer<T>,
src_offset: usize,
len: usize,
) -> Result<(), DriverError> {
if len == 0 {
return Ok(());
}
let dst = offset_ptr(dst, dst_offset, len);
let src = offset_ptr(src, src_offset, len);
unsafe { memcpy_dtod_async(dst, src, len * std::mem::size_of::<T>(), stream.cu_stream()) }
}