use crate::device_context::with_default_device_policy;
use crate::device_future::DeviceFuture;
use crate::device_operation::{DeviceOp, ExecutionContext};
use crate::error::DeviceError;
use anyhow::{Context, Result};
use cuda_core::sys::CUdeviceptr;
use cuda_core::{launch_kernel, DType, Function, LaunchConfig, Stream};
use std::ffi::c_void;
use std::fmt::Debug;
use std::future::IntoFuture;
use std::mem::MaybeUninit;
use std::sync::Arc;
use std::vec::Vec;
#[derive(Debug)]
pub struct AsyncKernelLaunch {
pub func: Arc<Function>,
args: KernelArgStorage,
cfg: Option<LaunchConfig>,
programmatic_dependent_launch: bool,
}
const INLINE_SLOTS: usize = 32;
enum KernelArgStorage {
Inline {
values: [MaybeUninit<u128>; INLINE_SLOTS],
offsets: [u8; INLINE_SLOTS],
values_len: usize,
args_len: usize,
},
Heap {
values: Vec<MaybeUninit<u128>>,
offsets: Vec<usize>,
},
}
impl Default for KernelArgStorage {
fn default() -> Self {
Self::Inline {
values: [const { MaybeUninit::uninit() }; INLINE_SLOTS],
offsets: [0; INLINE_SLOTS],
values_len: 0,
args_len: 0,
}
}
}
impl Debug for KernelArgStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut d = f.debug_struct("KernelArgStorage");
match self {
Self::Inline {
offsets, args_len, ..
} => d
.field("len", args_len)
.field("offsets", &&offsets[..*args_len]),
Self::Heap { offsets, .. } => d.field("len", &offsets.len()).field("offsets", offsets),
};
d.finish()
}
}
impl KernelArgStorage {
fn push<T: Copy + Send>(&mut self, arg: T) {
const SLOT: usize = std::mem::size_of::<u128>();
const {
assert!(std::mem::align_of::<T>() <= SLOT);
}
let slots = std::mem::size_of::<T>().div_ceil(SLOT).max(1);
if let Self::Inline {
values,
offsets,
values_len,
args_len,
} = self
{
if *values_len + slots <= INLINE_SLOTS && *args_len < INLINE_SLOTS {
let offset = *values_len;
for slot in &mut values[offset..offset + slots] {
slot.write(0);
}
unsafe { std::ptr::write(values.as_mut_ptr().add(offset).cast::<T>(), arg) };
offsets[*args_len] = offset as u8;
*values_len += slots;
*args_len += 1;
return;
}
let mut heap_values = Vec::with_capacity(2 * (*values_len + slots));
heap_values.extend_from_slice(&values[..*values_len]);
let mut heap_offsets = Vec::with_capacity(2 * (*args_len + 1));
heap_offsets.extend(offsets[..*args_len].iter().map(|&o| o as usize));
*self = Self::Heap {
values: heap_values,
offsets: heap_offsets,
};
}
let Self::Heap { values, offsets } = self else {
unreachable!("inline arena handled above");
};
let offset = values.len();
values.resize(offset + slots, MaybeUninit::new(0));
unsafe { std::ptr::write(values.as_mut_ptr().add(offset).cast::<T>(), arg) };
offsets.push(offset);
}
fn with_param_ptrs<R>(&mut self, f: impl FnOnce(&mut [*mut c_void]) -> R) -> R {
match self {
Self::Inline {
values,
offsets,
args_len,
..
} => {
let base = values.as_mut_ptr();
let mut ptrs: [*mut c_void; INLINE_SLOTS] = [std::ptr::null_mut(); INLINE_SLOTS];
for (ptr, &offset) in ptrs.iter_mut().zip(&offsets[..*args_len]) {
*ptr = unsafe { base.add(offset as usize) } as *mut c_void;
}
f(&mut ptrs[..*args_len])
}
Self::Heap { values, offsets } => {
let base = values.as_mut_ptr();
let mut ptrs: Vec<*mut c_void> = offsets
.iter()
.map(|&offset| unsafe { base.add(offset) } as *mut c_void)
.collect();
f(&mut ptrs)
}
}
}
}
impl AsyncKernelLaunch {
pub fn new(func: Arc<Function>) -> AsyncKernelLaunch {
AsyncKernelLaunch {
func,
args: KernelArgStorage::default(),
cfg: None,
programmatic_dependent_launch: false,
}
}
#[inline(always)]
pub fn push_arg<T: KernelArgument>(&mut self, arg: T) -> &mut Self {
arg.push_arg(self);
self
}
#[inline(always)]
pub fn push_arg_arc<T: ArcKernelArgument>(&mut self, arg: &Arc<T>) -> &mut Self {
arg.push_arg_arc(self);
self
}
pub unsafe fn push_device_ptr(&mut self, ptr: CUdeviceptr) -> &mut Self {
self.push_arg_raw(ptr)
}
unsafe fn push_arg_raw<T: Copy + Send>(&mut self, arg: T) -> &mut Self {
self.args.push(arg);
self
}
pub fn set_launch_config(&mut self, cfg: LaunchConfig) -> &mut Self {
self.cfg = Some(cfg);
self
}
pub unsafe fn programmatic_dependent_launch(&mut self) -> &mut Self {
self.programmatic_dependent_launch = true;
self
}
unsafe fn launch(mut self, stream: &Arc<Stream>) -> Result<(), DeviceError> {
let cfg = self.cfg.ok_or_else(|| {
DeviceError::Launch("Await called before launching the kernel.".to_string())
})?;
let launch = if self.programmatic_dependent_launch {
let architecture = cuda_core::get_device_sm_name(stream.device().cu_device())?;
let sm: u32 = architecture
.strip_prefix("sm_")
.and_then(|s| s.trim_end_matches(['a', 'f']).parse().ok())
.unwrap_or(0);
if sm < 90 {
return Err(DeviceError::Launch(format!(
"programmatic dependent launch requires sm_90 or newer; target {architecture}"
)));
}
cuda_core::launch_kernel_pdl
} else {
launch_kernel
};
let func = self.func.cu_function();
self.args
.with_param_ptrs(|params| {
launch(
func,
cfg.grid_dim,
cfg.block_dim,
cfg.shared_mem_bytes,
stream.cu_stream(),
params,
)
})
.with_context(|| {
format!(
r#"
Failed to launch kernel.
args: {:#?}
cfg: {:#?}"#,
self.args, cfg
)
})?;
Ok(())
}
}
pub trait ArcKernelArgument {
fn push_arg_arc(self: &Arc<Self>, launcher: &mut AsyncKernelLaunch);
}
pub trait KernelArgument {
fn push_arg(self, launcher: &mut AsyncKernelLaunch);
}
impl<T: DType> KernelArgument for T {
fn push_arg(self, launcher: &mut AsyncKernelLaunch) {
unsafe {
launcher.push_arg_raw(self);
}
}
}
impl DeviceOp for AsyncKernelLaunch {
type Output = ();
unsafe fn execute(
self,
ctx: &ExecutionContext,
) -> Result<<Self as DeviceOp>::Output, DeviceError> {
self.launch(ctx.get_cuda_stream())
}
}
impl IntoFuture for AsyncKernelLaunch {
type Output = Result<(), DeviceError>;
type IntoFuture = DeviceFuture<(), AsyncKernelLaunch>;
fn into_future(self) -> Self::IntoFuture {
match with_default_device_policy(|policy| {
let stream = policy.next_stream()?;
let mut f = DeviceFuture::new();
f.device_operation = Some(self);
f.execution_context = Some(ExecutionContext::new(stream));
Ok(f)
}) {
Ok(Ok(future)) => future,
Ok(Err(e)) => DeviceFuture::failed(e),
Err(e) => DeviceFuture::failed(e),
}
}
}
#[cfg(test)]
mod arg_storage_tests {
use super::*;
fn param<T: Copy>(ptr: *mut c_void) -> T {
unsafe { ptr.cast::<T>().read() }
}
#[test]
fn values_roundtrip_and_survive_arena_growth() {
let mut storage = KernelArgStorage::default();
storage.push(7u8);
storage.push(0x1122_3344_5566_7788u64);
storage.push(-5i32);
assert!(matches!(storage, KernelArgStorage::Inline { .. }));
for i in 0..64u64 {
storage.push(i);
}
assert!(matches!(storage, KernelArgStorage::Heap { .. }));
storage.with_param_ptrs(|ptrs| {
assert_eq!(ptrs.len(), 3 + 64);
assert_eq!(param::<u8>(ptrs[0]), 7);
assert_eq!(param::<u64>(ptrs[1]), 0x1122_3344_5566_7788);
assert_eq!(param::<i32>(ptrs[2]), -5);
for i in 0..64usize {
assert_eq!(param::<u64>(ptrs[3 + i]), i as u64);
}
});
}
#[test]
fn common_parameter_counts_stay_inline() {
let mut storage = KernelArgStorage::default();
for i in 0..INLINE_SLOTS {
storage.push(i as u32);
}
assert!(matches!(storage, KernelArgStorage::Inline { .. }));
storage.with_param_ptrs(|ptrs| {
assert_eq!(ptrs.len(), INLINE_SLOTS);
for (i, &ptr) in ptrs.iter().enumerate() {
assert_eq!(param::<u32>(ptr), i as u32);
}
});
storage.push(1u8);
assert!(matches!(storage, KernelArgStorage::Heap { .. }));
}
#[test]
fn padded_values_survive_the_spill() {
#[derive(Clone, Copy)]
#[repr(C)]
struct Padded {
a: u8,
b: u32,
}
let mut storage = KernelArgStorage::default();
for i in 0..INLINE_SLOTS + 4 {
storage.push(Padded {
a: i as u8,
b: i as u32 * 3,
});
}
assert!(matches!(storage, KernelArgStorage::Heap { .. }));
storage.with_param_ptrs(|ptrs| {
assert_eq!(ptrs.len(), INLINE_SLOTS + 4);
for (i, &ptr) in ptrs.iter().enumerate() {
let value = param::<Padded>(ptr);
assert_eq!((value.a, value.b), (i as u8, i as u32 * 3));
}
});
}
#[test]
fn multi_slot_values_roundtrip_inline_and_on_the_heap() {
let wide = [11u64, 22, 33];
let mut storage = KernelArgStorage::default();
storage.push(wide);
storage.push(7u8);
assert!(matches!(storage, KernelArgStorage::Inline { .. }));
storage.with_param_ptrs(|ptrs| {
assert_eq!(param::<[u64; 3]>(ptrs[0]), wide);
assert_eq!(param::<u8>(ptrs[1]), 7);
});
for _ in 0..INLINE_SLOTS {
storage.push(wide);
}
assert!(matches!(storage, KernelArgStorage::Heap { .. }));
storage.with_param_ptrs(|ptrs| {
assert_eq!(ptrs.len(), 2 + INLINE_SLOTS);
assert_eq!(param::<[u64; 3]>(ptrs[0]), wide);
assert_eq!(param::<[u64; 3]>(ptrs[ptrs.len() - 1]), wide);
});
}
#[test]
fn slots_are_sixteen_byte_aligned() {
let mut storage = KernelArgStorage::default();
storage.push(1u8);
storage.push(2u128);
storage.with_param_ptrs(|ptrs| {
assert!(ptrs.iter().all(|&p| (p as usize).is_multiple_of(16)));
assert_eq!(param::<u128>(ptrs[1]), 2);
});
}
}