mod module;
mod nvrtc;
use std::fmt;
use std::marker::PhantomData;
use std::rc::Rc;
use tenferro_tensor::{TensorRank, TypedTensor};
use super::runtime::CudaRuntime;
use super::{CudaExtensionCache, CudaExtensionCacheGuard, CudaRuntimeIdentity};
pub use nvrtc::NvrtcOptions;
#[allow(dead_code)]
pub struct Module {
inner: Rc<ModuleInner>,
runtime: CudaRuntime,
_not_send_sync: PhantomData<Rc<()>>,
}
pub(crate) struct ModuleInner {
handle: cudarc::driver::sys::CUmodule,
context: cudarc::driver::sys::CUcontext,
}
impl Drop for ModuleInner {
fn drop(&mut self) {
unsafe {
let was_current = cudarc::driver::result::ctx::get_current().ok();
let _ = cudarc::driver::result::ctx::set_current(self.context);
let _ = cudarc::driver::result::module::unload(self.handle);
if let Some(previous) = was_current.flatten() {
if previous != self.context {
let _ = cudarc::driver::result::ctx::set_current(previous);
}
}
}
}
}
impl Module {
pub(crate) fn from_handle(
op: &'static str,
handle: cudarc::driver::sys::CUmodule,
runtime: CudaRuntime,
) -> crate::Result<Self> {
if handle.is_null() {
return Err(crate::Error::backend_source(
op,
std::io::Error::other("driver returned a null module handle"),
));
}
let context = runtime.primary_context();
Ok(Self {
inner: Rc::new(ModuleInner { handle, context }),
runtime,
_not_send_sync: PhantomData,
})
}
pub fn function(&self, name: &str) -> crate::Result<Function> {
module::module_function(&self.inner, name, "module.function")
}
#[allow(dead_code)]
pub(crate) fn handle(&self) -> cudarc::driver::sys::CUmodule {
self.inner.handle
}
}
#[allow(dead_code)]
pub struct Function {
handle: cudarc::driver::sys::CUfunction,
module: Rc<ModuleInner>,
}
impl Function {
pub(crate) fn handle(&self) -> cudarc::driver::sys::CUfunction {
self.handle
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LaunchConfig {
pub grid: [u32; 3],
pub block: [u32; 3],
pub shared_mem_bytes: u32,
}
impl LaunchConfig {
pub fn flat(threads: u32, block: u32, shared_mem_bytes: u32) -> crate::Result<Self> {
if block == 0 {
return Err(crate::Error::invalid_argument(
"launch_config.flat",
"block",
"block size must be non-zero",
));
}
let grids = threads.div_ceil(block);
Ok(Self {
grid: [grids.max(1), 1, 1],
block: [block, 1, 1],
shared_mem_bytes,
})
}
}
pub struct Session<'s> {
runtime: CudaRuntime,
cache: &'s CudaExtensionCache,
stream: u64,
_not_send_sync: PhantomData<Rc<()>>,
_scope: PhantomData<&'s ()>,
}
impl<'s> Session<'s> {
pub(crate) unsafe fn new(
runtime: CudaRuntime,
cache: &'s CudaExtensionCache,
stream: u64,
) -> Self {
Self {
runtime,
cache,
stream,
_not_send_sync: PhantomData,
_scope: PhantomData,
}
}
pub fn runtime_identity(&self) -> CudaRuntimeIdentity {
self.runtime.runtime_identity()
}
pub fn synchronize(&self) -> crate::Result<()> {
self.runtime.synchronize()
}
pub fn stream(&self) -> StreamRef<'s> {
StreamRef {
raw: self.stream,
_scope: PhantomData,
_not_send_sync: PhantomData,
}
}
pub fn tensor<'a, T>(
&'a self,
tensor: &'a TypedTensor<T, impl TensorRank>,
) -> crate::Result<TensorRef<'a, T>>
where
T: 'static,
{
super::dispatch::ensure_resident_on_runtime(&self.runtime, tensor, "raw.tensor")?;
let prepared = super::dispatch::cubecl_buffer(tensor, "raw.tensor")?;
let byte_len = prepared.byte_len;
let resource = self
.runtime
.client()
.get_resource(prepared.handle().clone())
.map_err(|err| crate::Error::backend_source("raw.tensor", err))?;
let base =
super::interop::cuda_device_ptr_from_addr(resource.resource().ptr, "raw.tensor")?;
Ok(TensorRef {
base,
byte_len,
_scope: PhantomData,
_dtype: PhantomData,
_not_send_sync: PhantomData,
})
}
pub fn tensor_mut<'a, T>(
&'a self,
tensor: &'a mut TypedTensor<T, impl TensorRank>,
) -> crate::Result<TensorMut<'a, T>>
where
T: 'static,
{
super::dispatch::ensure_resident_on_runtime(&self.runtime, tensor, "raw.tensor_mut")?;
let prepared = super::dispatch::cubecl_buffer(tensor, "raw.tensor_mut")?;
let byte_len = prepared.byte_len;
let resource = self
.runtime
.client()
.get_resource(prepared.handle().clone())
.map_err(|err| crate::Error::backend_source("raw.tensor_mut", err))?;
let base =
super::interop::cuda_device_ptr_from_addr(resource.resource().ptr, "raw.tensor_mut")?;
Ok(TensorMut {
base,
byte_len,
_scope: PhantomData,
_dtype: PhantomData,
_not_send_sync: PhantomData,
})
}
pub fn alloc_output<T>(&self, shape: &[usize]) -> crate::Result<TypedTensor<T>>
where
T: cubecl::prelude::CubeElement
+ tenferro_tensor::TensorScalar
+ Clone
+ Send
+ Sync
+ 'static,
{
super::dispatch::alloc_output(&self.runtime, shape)
}
pub fn retain_tensor<T>(
&self,
tensor: &TypedTensor<T, impl TensorRank>,
op: &'static str,
) -> crate::Result<DeviceBytes<'s>>
where
T: 'static,
{
let inner = super::interop::retain_tensor_bytes(&self.runtime, tensor, op)?;
Ok(DeviceBytes {
inner,
_scope: PhantomData,
_not_send_sync: PhantomData,
})
}
pub fn alloc_bytes(&self, nbytes: usize, op: &'static str) -> crate::Result<DeviceBytes<'s>> {
let inner = super::interop::alloc_device_bytes(&self.runtime, nbytes, op)?;
Ok(DeviceBytes {
inner,
_scope: PhantomData,
_not_send_sync: PhantomData,
})
}
pub unsafe fn copy_bytes(
&self,
dst: *mut std::ffi::c_void,
src: *const std::ffi::c_void,
nbytes: usize,
op: &'static str,
) -> crate::Result<()> {
if nbytes == 0 {
return Ok(());
}
let stream = self.stream as *mut cudarc::runtime::sys::CUstream_st;
cudarc::runtime::result::memcpy_dtod_async(dst, src, nbytes, stream)
.map_err(|err| crate::Error::backend_source(op, err))
}
pub fn upload_bytes(&self, bytes: &[u8], op: &'static str) -> crate::Result<DeviceBytes<'s>> {
let inner = super::interop::upload_device_bytes(&self.runtime, bytes, op)?;
Ok(DeviceBytes {
inner,
_scope: PhantomData,
_not_send_sync: PhantomData,
})
}
pub fn download_tensor<T>(
&self,
tensor: &TypedTensor<T, impl TensorRank>,
op: &'static str,
) -> crate::Result<TypedTensor<T>>
where
T: cubecl::prelude::CubeElement
+ tenferro_tensor::TensorScalar
+ Clone
+ Send
+ Sync
+ 'static,
{
super::interop::download_typed_tensor(&self.runtime, tensor, op)
}
pub fn load_ptx(&self, ptx: &std::ffi::CStr) -> crate::Result<Module> {
module::load_module_data(ptx.as_ptr().cast(), self.runtime.clone(), "raw.load_ptx")
}
pub fn load_cubin(&self, cubin: &[u8]) -> crate::Result<Module> {
module::load_module_data(
cubin.as_ptr().cast(),
self.runtime.clone(),
"raw.load_cubin",
)
}
pub fn compile_nvrtc(&self, src: &str, opts: &NvrtcOptions) -> crate::Result<Module> {
let ptx = nvrtc::compile_nvrtc(src, opts)?;
let ptx_src = ptx.to_src();
let cstr = std::ffi::CString::new(ptx_src).map_err(|_| {
crate::Error::invalid_argument("raw.compile_nvrtc", "ptx", "PTX contains NUL")
})?;
self.load_ptx(&cstr)
}
pub unsafe fn launch(
&self,
function: &Function,
config: LaunchConfig,
args: &[KernelArg<'_>],
) -> crate::Result<()> {
module::validate_launch_config(&config)?;
let mut scalar_storage: Vec<Box<[u8]>> = Vec::with_capacity(args.len());
let mut ptr_storage: Vec<Box<*mut std::ffi::c_void>> = Vec::with_capacity(args.len());
let mut kernel_params: Vec<*mut std::ffi::c_void> = Vec::with_capacity(args.len());
for arg in args {
match arg {
KernelArg::Scalar(bytes) => {
scalar_storage.push(bytes.clone().into_boxed_slice());
let slot = &scalar_storage[scalar_storage.len() - 1];
let last_ptr = slot.as_ptr() as *const std::ffi::c_void;
kernel_params.push(last_ptr as *mut std::ffi::c_void);
}
KernelArg::DevicePtr(ptr, _) => {
let mut slot_box = Box::new(*ptr);
let slot = (&mut *slot_box) as *mut *mut std::ffi::c_void;
kernel_params.push(slot as *mut std::ffi::c_void);
ptr_storage.push(slot_box);
}
}
}
let stream = self.stream as *mut cudarc::driver::sys::CUstream_st;
cudarc::driver::result::launch_kernel(
function.handle(),
(config.grid[0], config.grid[1], config.grid[2]),
(config.block[0], config.block[1], config.block[2]),
config.shared_mem_bytes,
stream,
&mut kernel_params,
)
.map_err(|err| crate::Error::backend_source("raw.launch", err))
}
pub fn resource<T>(
&self,
init: impl FnOnce() -> crate::Result<T>,
) -> crate::Result<CudaResourceGuard<'_, T>>
where
T: Send + 'static,
{
let guard = self.cache.get_or_try_init(init)?;
Ok(CudaResourceGuard { inner: guard })
}
}
pub struct StreamRef<'s> {
raw: u64,
_scope: PhantomData<&'s ()>,
_not_send_sync: PhantomData<Rc<()>>,
}
impl<'s> StreamRef<'s> {
pub unsafe fn raw_handle(&self) -> u64 {
self.raw
}
}
impl fmt::Debug for StreamRef<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StreamRef").finish_non_exhaustive()
}
}
pub struct TensorRef<'s, T> {
base: *mut std::ffi::c_void,
byte_len: usize,
_scope: PhantomData<&'s ()>,
_dtype: PhantomData<T>,
_not_send_sync: PhantomData<Rc<()>>,
}
impl<'s, T> TensorRef<'s, T> {
pub fn byte_len(&self) -> usize {
self.byte_len
}
pub unsafe fn raw_ptr(&self) -> *mut std::ffi::c_void {
self.base
}
}
impl<T> fmt::Debug for TensorRef<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TensorRef")
.field("byte_len", &self.byte_len)
.finish_non_exhaustive()
}
}
pub struct TensorMut<'s, T> {
base: *mut std::ffi::c_void,
byte_len: usize,
_scope: PhantomData<&'s ()>,
_dtype: PhantomData<T>,
_not_send_sync: PhantomData<Rc<()>>,
}
impl<'s, T> TensorMut<'s, T> {
pub fn byte_len(&self) -> usize {
self.byte_len
}
pub unsafe fn raw_ptr(&self) -> *mut std::ffi::c_void {
self.base
}
}
#[derive(Clone)]
pub enum KernelArg<'a> {
Scalar(Vec<u8>),
DevicePtr(*mut std::ffi::c_void, PhantomData<&'a ()>),
}
impl<'a> KernelArg<'a> {
pub fn scalar(bytes: &[u8]) -> Self {
Self::Scalar(bytes.to_vec())
}
pub fn u32(value: u32) -> Self {
Self::scalar(&value.to_ne_bytes())
}
pub fn i32(value: i32) -> Self {
Self::scalar(&value.to_ne_bytes())
}
pub fn f32(value: f32) -> Self {
Self::scalar(&value.to_ne_bytes())
}
pub fn u64(value: u64) -> Self {
Self::scalar(&value.to_ne_bytes())
}
pub fn i64(value: i64) -> Self {
Self::scalar(&value.to_ne_bytes())
}
pub fn f64(value: f64) -> Self {
Self::scalar(&value.to_ne_bytes())
}
pub fn tensor<T>(reference: &TensorRef<'a, T>) -> Self {
Self::DevicePtr(unsafe { reference.raw_ptr() }, PhantomData)
}
pub fn tensor_mut<T>(reference: &TensorMut<'a, T>) -> Self {
Self::DevicePtr(unsafe { reference.raw_ptr() }, PhantomData)
}
pub fn output<T>(reference: &TensorMut<'a, T>) -> Self {
Self::tensor_mut(reference)
}
pub fn workspace(bytes: &DeviceBytes<'a>) -> Self {
let mut ptr = std::ptr::null_mut::<std::ffi::c_void>();
bytes.with_ptr(|p| ptr = p);
Self::DevicePtr(ptr, PhantomData)
}
}
pub struct DeviceBytes<'s> {
inner: super::interop::DeviceByteBuffer,
_scope: PhantomData<&'s ()>,
_not_send_sync: PhantomData<Rc<()>>,
}
impl<'s> DeviceBytes<'s> {
pub fn with_ptr(&self, f: impl FnOnce(*mut std::ffi::c_void)) {
self.inner.with_ptr(f)
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
impl fmt::Debug for DeviceBytes<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DeviceBytes")
.field("is_empty", &self.is_empty())
.finish_non_exhaustive()
}
}
pub struct CudaResourceGuard<'cache, T> {
inner: CudaExtensionCacheGuard<'cache, T>,
}
impl<T: 'static> std::ops::Deref for CudaResourceGuard<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T: 'static> fmt::Debug for CudaResourceGuard<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CudaResourceGuard")
.field("value_type", &std::any::type_name::<T>())
.finish_non_exhaustive()
}
}