use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Instant;
use tensor_wasm_core::types::{InstanceId, KernelId};
use tracing::{info, info_span, warn, Instrument};
use wasmtime::{Caller, Linker};
use crate::abi::{
AbiError, FN_ALLOC, FN_FREE, FN_LAST_ERROR_COPY, FN_LAST_ERROR_LEN, FN_LAUNCH, FN_LOAD_PTX,
FN_MEMCPY_D2H, FN_MEMCPY_H2D, FN_SYNC, MAX_BLOCK_DIM, MAX_GRID_DIM, MAX_PTX_BYTES,
MAX_THREADS_PER_BLOCK, MODULE,
};
use crate::async_dispatch::BackPressure;
use crate::device_mem::{DeviceMemEntry, DeviceMemRegistry, MAX_DEVICE_ALLOC_BYTES};
use crate::kernel_args::{parse_argv, LoweredArg, LoweredArgSnapshot};
use crate::registry::{KernelEntry, KernelRegistry};
use crate::scheduler::SchedulerContext;
pub const MAX_RECORDED_ERROR_BYTES: usize = 512;
pub const MAX_ENTRY_NAME_BYTES: usize = 256;
pub const MAX_DYNAMIC_SHARED_MEM_BYTES: i32 = 228 * 1024;
pub struct WasiCudaContext {
pub instance_id: InstanceId,
pub registry: Arc<KernelRegistry>,
pub last_error: Mutex<Option<String>>,
pub back_pressure: Arc<BackPressure>,
pub last_lowered_args: Mutex<Vec<LoweredArg>>,
pub(crate) wasi_cuda_enabled: AtomicBool,
pub bp_deadline: Mutex<Option<Instant>>,
pub device_mem: Arc<DeviceMemRegistry>,
pub(crate) kernels_launched: AtomicU64,
pub(crate) back_pressure_rejections: AtomicU64,
}
impl WasiCudaContext {
pub fn new(instance_id: InstanceId) -> Self {
Self {
instance_id,
registry: Arc::new(KernelRegistry::new()),
last_error: Mutex::new(None),
back_pressure: Arc::new(BackPressure::new()),
last_lowered_args: Mutex::new(Vec::new()),
wasi_cuda_enabled: AtomicBool::new(false),
bp_deadline: Mutex::new(None),
device_mem: Arc::new(DeviceMemRegistry::new()),
kernels_launched: AtomicU64::new(0),
back_pressure_rejections: AtomicU64::new(0),
}
}
pub fn with_back_pressure(instance_id: InstanceId, bp: Arc<BackPressure>) -> Self {
Self {
instance_id,
registry: Arc::new(KernelRegistry::new()),
last_error: Mutex::new(None),
back_pressure: bp,
last_lowered_args: Mutex::new(Vec::new()),
wasi_cuda_enabled: AtomicBool::new(false),
bp_deadline: Mutex::new(None),
device_mem: Arc::new(DeviceMemRegistry::new()),
kernels_launched: AtomicU64::new(0),
back_pressure_rejections: AtomicU64::new(0),
}
}
pub fn back_pressure(&self) -> &Arc<BackPressure> {
&self.back_pressure
}
pub fn device_mem(&self) -> &Arc<DeviceMemRegistry> {
&self.device_mem
}
pub fn metrics_snapshot(&self) -> InstanceMetricsSnapshot {
InstanceMetricsSnapshot {
kernels_launched: self.kernels_launched.load(Ordering::Relaxed),
bytes_pinned: self.registry.total_ptx_bytes(),
back_pressure_rejections: self.back_pressure_rejections.load(Ordering::Relaxed),
yield_count: 0,
device_bytes_allocated: self.device_mem.total_device_bytes(),
}
}
pub fn metrics_snapshot_with_scheduler(
&self,
scheduler: &SchedulerContext,
) -> InstanceMetricsSnapshot {
let mut snap = self.metrics_snapshot();
snap.yield_count = scheduler.yield_count();
snap
}
fn record_kernel_launched(&self) {
self.kernels_launched.fetch_add(1, Ordering::Relaxed);
}
fn record_back_pressure_rejection(&self) {
self.back_pressure_rejections
.fetch_add(1, Ordering::Relaxed);
}
pub fn set_bp_deadline(&self, deadline: Option<Instant>) {
let mut guard = self.bp_deadline.lock().unwrap_or_else(|e| e.into_inner());
*guard = deadline;
}
pub fn bp_deadline(&self) -> Option<Instant> {
*self.bp_deadline.lock().unwrap_or_else(|e| e.into_inner())
}
pub fn deadline_aware_back_pressure(&self) -> BackPressure {
let bp = (*self.back_pressure).clone();
bp.with_deadline_hint(self.bp_deadline())
}
pub fn enable_wasi_cuda(&mut self) {
self.wasi_cuda_enabled.store(true, Ordering::Release);
}
pub fn disable_wasi_cuda(&mut self) {
self.wasi_cuda_enabled.store(false, Ordering::Release);
if let Ok(mut guard) = self.last_error.lock() {
*guard = None;
}
}
pub fn wasi_cuda_enabled(&self) -> bool {
self.wasi_cuda_enabled.load(Ordering::Acquire)
}
fn record_error(&self, msg: impl Into<String>) {
let mut msg = msg.into();
if msg.len() > MAX_RECORDED_ERROR_BYTES {
let cutoff = (0..=MAX_RECORDED_ERROR_BYTES)
.rev()
.find(|i| msg.is_char_boundary(*i))
.unwrap_or(0);
msg.truncate(cutoff);
msg.push('\u{2026}');
}
warn!(target: "tensor_wasm_wasi_gpu::host", instance = %self.instance_id, %msg, "wasi-cuda error");
*self.last_error.lock().unwrap_or_else(|e| e.into_inner()) = Some(msg);
}
#[doc(hidden)]
pub fn record_error_for_test(&self, msg: impl Into<String>) {
self.record_error(msg);
}
pub fn last_error(&self) -> Option<String> {
self.last_error
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
pub fn last_lowered_args(&self) -> Vec<LoweredArgSnapshot> {
self.last_lowered_args
.lock()
.unwrap_or_else(|e| e.into_inner())
.iter()
.map(LoweredArgSnapshot::from)
.collect()
}
#[cfg(test)]
#[allow(dead_code)]
pub(crate) fn last_lowered_args_internal(&self) -> Vec<LoweredArg> {
self.last_lowered_args
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct InstanceMetricsSnapshot {
pub kernels_launched: u64,
pub bytes_pinned: u64,
pub back_pressure_rejections: u64,
pub yield_count: u32,
pub device_bytes_allocated: u64,
}
pub trait HasWasiCuda {
fn wasi_cuda(&self) -> &WasiCudaContext;
}
pub fn add_to_linker<T: HasWasiCuda + Send + 'static>(
linker: &mut Linker<T>,
) -> wasmtime::Result<()> {
linker.func_wrap(
MODULE,
FN_LOAD_PTX,
|mut caller: Caller<'_, T>,
ptx_ptr: i32,
ptx_len: i32,
entry_ptr: i32,
entry_len: i32|
-> i64 {
if !caller
.data()
.wasi_cuda()
.wasi_cuda_enabled
.load(Ordering::Acquire)
{
return AbiError::NotAvailable.code() as i64;
}
match load_ptx_impl(&mut caller, ptx_ptr, ptx_len, entry_ptr, entry_len) {
Ok(k) => k.0 as i64,
Err(e) => e.code() as i64,
}
},
)?;
linker.func_wrap_async(
MODULE,
FN_LAUNCH,
|mut caller: Caller<'_, T>,
(
kernel_id,
grid_x,
grid_y,
grid_z,
block_x,
block_y,
block_z,
shared_mem,
args_ptr,
args_len,
): (i64, i32, i32, i32, i32, i32, i32, i32, i32, i32)|
-> Box<dyn std::future::Future<Output = i32> + Send + '_> {
Box::new(async move {
if !caller
.data()
.wasi_cuda()
.wasi_cuda_enabled
.load(Ordering::Acquire)
{
return AbiError::NotAvailable.code();
}
launch_impl_async(
&mut caller,
kernel_id,
grid_x,
grid_y,
grid_z,
block_x,
block_y,
block_z,
shared_mem,
args_ptr,
args_len,
)
.await
.map_or_else(|e| e.code(), |_| 0)
})
},
)?;
linker.func_wrap(MODULE, FN_SYNC, |caller: Caller<'_, T>| -> i32 {
if !caller
.data()
.wasi_cuda()
.wasi_cuda_enabled
.load(Ordering::Acquire)
{
return AbiError::NotAvailable.code();
}
sync_impl(&caller).map_or_else(|e| e.code(), |_| 0)
})?;
linker.func_wrap(
MODULE,
FN_ALLOC,
|mut caller: Caller<'_, T>, size_lo: i32, size_hi: i32| -> i64 {
if !caller
.data()
.wasi_cuda()
.wasi_cuda_enabled
.load(Ordering::Acquire)
{
return AbiError::NotAvailable.code() as i64;
}
match alloc_impl(&mut caller, join_u64(size_lo, size_hi)) {
Ok(handle) => handle as i64,
Err(e) => e.code() as i64,
}
},
)?;
linker.func_wrap(
MODULE,
FN_FREE,
|mut caller: Caller<'_, T>, handle_lo: i32, handle_hi: i32| -> i32 {
if !caller
.data()
.wasi_cuda()
.wasi_cuda_enabled
.load(Ordering::Acquire)
{
return AbiError::NotAvailable.code();
}
free_impl(&mut caller, join_u64(handle_lo, handle_hi)).map_or_else(|e| e.code(), |_| 0)
},
)?;
linker.func_wrap(
MODULE,
FN_MEMCPY_H2D,
|mut caller: Caller<'_, T>,
handle_lo: i32,
handle_hi: i32,
src_ptr: i32,
len: i32|
-> i32 {
if !caller
.data()
.wasi_cuda()
.wasi_cuda_enabled
.load(Ordering::Acquire)
{
return AbiError::NotAvailable.code();
}
memcpy_h2d_impl(&mut caller, join_u64(handle_lo, handle_hi), src_ptr, len)
.map_or_else(|e| e.code(), |_| 0)
},
)?;
linker.func_wrap(
MODULE,
FN_MEMCPY_D2H,
|mut caller: Caller<'_, T>,
dst_ptr: i32,
handle_lo: i32,
handle_hi: i32,
len: i32|
-> i32 {
if !caller
.data()
.wasi_cuda()
.wasi_cuda_enabled
.load(Ordering::Acquire)
{
return AbiError::NotAvailable.code();
}
memcpy_d2h_impl(&mut caller, dst_ptr, join_u64(handle_lo, handle_hi), len)
.map_or_else(|e| e.code(), |_| 0)
},
)?;
linker.func_wrap(MODULE, FN_LAST_ERROR_LEN, |caller: Caller<'_, T>| -> i32 {
if !caller
.data()
.wasi_cuda()
.wasi_cuda_enabled
.load(Ordering::Acquire)
{
return AbiError::NotAvailable.code();
}
caller
.data()
.wasi_cuda()
.last_error()
.map(|s| s.len() as i32)
.unwrap_or(0)
})?;
linker.func_wrap(
MODULE,
FN_LAST_ERROR_COPY,
|mut caller: Caller<'_, T>, dst_ptr: i32, dst_len: i32| -> i32 {
if !caller
.data()
.wasi_cuda()
.wasi_cuda_enabled
.load(Ordering::Acquire)
{
return AbiError::NotAvailable.code();
}
if dst_ptr < 0 || dst_len < 0 {
return AbiError::InvalidPointer.code();
}
if dst_len == 0 {
return 0;
}
let msg = match caller.data().wasi_cuda().last_error() {
Some(s) => s,
None => return 0,
};
let bytes = msg.as_bytes();
let to_copy = std::cmp::min(bytes.len(), dst_len as usize);
let memory = match caller.get_export("memory").and_then(|e| e.into_memory()) {
Some(m) => m,
None => return AbiError::InvalidPointer.code(),
};
let mem_len = memory.data(&caller).len();
let start = dst_ptr as usize;
let end = match start.checked_add(to_copy) {
Some(e) => e,
None => return AbiError::InvalidPointer.code(),
};
if end > mem_len {
return AbiError::InvalidPointer.code();
}
let buf = bytes[..to_copy].to_vec();
if memory.write(&mut caller, dst_ptr as usize, &buf).is_err() {
return AbiError::InvalidPointer.code();
}
to_copy as i32
},
)?;
Ok(())
}
fn read_bytes<T>(caller: &mut Caller<'_, T>, ptr: i32, len: i32) -> Result<Vec<u8>, AbiError> {
if len < 0 || ptr < 0 {
return Err(AbiError::InvalidPointer);
}
let memory = caller
.get_export("memory")
.and_then(|e| e.into_memory())
.ok_or(AbiError::InvalidPointer)?;
let data = memory.data(&caller);
let start = ptr as usize;
let end = start
.checked_add(len as usize)
.ok_or(AbiError::InvalidPointer)?;
if end > data.len() {
return Err(AbiError::InvalidPointer);
}
Ok(data[start..end].to_vec())
}
fn join_u64(lo: i32, hi: i32) -> u64 {
((hi as u32 as u64) << 32) | (lo as u32 as u64)
}
fn checked_guest_region<T>(
caller: &mut Caller<'_, T>,
ptr: i32,
len: u32,
) -> Result<(usize, usize), AbiError> {
let memory = caller
.get_export("memory")
.and_then(|e| e.into_memory())
.ok_or(AbiError::InvalidPointer)?;
let mem_len = memory.data(&caller).len();
let start = ptr as u32 as usize;
let end = start
.checked_add(len as usize)
.ok_or(AbiError::InvalidPointer)?;
if end > mem_len {
return Err(AbiError::InvalidPointer);
}
Ok((start, end))
}
fn alloc_impl<T: HasWasiCuda>(caller: &mut Caller<'_, T>, size: u64) -> Result<u64, AbiError> {
let _span = info_span!(
"wasi_cuda.alloc",
instance = %caller.data().wasi_cuda().instance_id,
size = size,
)
.entered();
if size == 0 {
caller
.data()
.wasi_cuda()
.record_error("alloc: size must be > 0");
return Err(AbiError::InvalidArgs);
}
if size > MAX_DEVICE_ALLOC_BYTES {
caller.data().wasi_cuda().record_error(format!(
"alloc: size {size} exceeds MAX_DEVICE_ALLOC_BYTES {MAX_DEVICE_ALLOC_BYTES}"
));
return Err(AbiError::QuotaExceeded);
}
let owner = caller.data().wasi_cuda().instance_id;
let device_mem = caller.data().wasi_cuda().device_mem.clone();
#[cfg(not(feature = "cuda"))]
{
let _handle = device_mem.insert(DeviceMemEntry { owner, size })?;
caller.data().wasi_cuda().record_error(format!(
"alloc: CUDA not available on this host (requested {size} bytes; \
handle tracked in registry)"
));
Err(AbiError::NotAvailable)
}
#[cfg(feature = "cuda")]
{
if let Err(e) = crate::cuda_ctx::ensure_current_context() {
caller
.data()
.wasi_cuda()
.record_error(format!("alloc: CUDA context bind failed: {e}"));
return Err(AbiError::LaunchFailed);
}
use cust::sys as cuda_sys;
let mut device_ptr: cuda_sys::CUdeviceptr = 0;
let status = unsafe { cuda_sys::cuMemAlloc_v2(&mut device_ptr, size as usize) };
if status != cuda_sys::CUresult::CUDA_SUCCESS {
caller
.data()
.wasi_cuda()
.record_error(format!("alloc: cuMemAlloc failed with status {status:?}"));
return Err(AbiError::LaunchFailed);
}
let handle = match device_mem.insert(DeviceMemEntry {
owner,
size,
device_ptr,
}) {
Ok(h) => h,
Err(e) => {
unsafe {
let _ = cuda_sys::cuMemFree_v2(device_ptr);
}
return Err(e);
}
};
Ok(handle)
}
}
fn free_impl<T: HasWasiCuda>(caller: &mut Caller<'_, T>, handle: u64) -> Result<(), AbiError> {
let _span = info_span!(
"wasi_cuda.free",
instance = %caller.data().wasi_cuda().instance_id,
handle = handle,
)
.entered();
let owner = caller.data().wasi_cuda().instance_id;
let device_mem = caller.data().wasi_cuda().device_mem.clone();
let entry = match device_mem.free(handle, owner) {
Ok(e) => e,
Err(e) => {
caller
.data()
.wasi_cuda()
.record_error(format!("free: handle {handle} {}", e.name()));
return Err(e);
}
};
let _ = &entry;
#[cfg(feature = "cuda")]
{
use cust::sys as cuda_sys;
if let Err(e) = crate::cuda_ctx::ensure_current_context() {
caller
.data()
.wasi_cuda()
.record_error(format!("free: CUDA context bind failed: {e}"));
} else {
let status = unsafe { cuda_sys::cuMemFree_v2(entry.device_ptr) };
if status != cuda_sys::CUresult::CUDA_SUCCESS {
caller
.data()
.wasi_cuda()
.record_error(format!("free: cuMemFree failed with status {status:?}"));
}
}
}
Ok(())
}
fn memcpy_h2d_impl<T: HasWasiCuda>(
caller: &mut Caller<'_, T>,
handle: u64,
src_ptr: i32,
len: i32,
) -> Result<(), AbiError> {
let _span = info_span!(
"wasi_cuda.memcpy_h2d",
instance = %caller.data().wasi_cuda().instance_id,
handle = handle,
)
.entered();
let owner = caller.data().wasi_cuda().instance_id;
let device_mem = caller.data().wasi_cuda().device_mem.clone();
let dev = match device_mem.lookup(handle, owner) {
Ok(d) => d,
Err(e) => {
caller
.data()
.wasi_cuda()
.record_error(format!("memcpy_h2d: handle {handle} {}", e.name()));
return Err(e);
}
};
let len_u32 = len as u32;
if (len_u32 as u64) > dev.size {
caller.data().wasi_cuda().record_error(format!(
"memcpy_h2d: len {len_u32} exceeds device buffer size {}",
dev.size
));
return Err(AbiError::InvalidArgs);
}
let (start, end) = match checked_guest_region(caller, src_ptr, len_u32) {
Ok(r) => r,
Err(e) => {
caller.data().wasi_cuda().record_error(format!(
"memcpy_h2d: source region [{src_ptr}, +{len_u32}) out of bounds"
));
return Err(e);
}
};
let _ = (start, end);
#[cfg(feature = "cuda")]
{
if let Err(e) = crate::cuda_ctx::ensure_current_context() {
caller
.data()
.wasi_cuda()
.record_error(format!("memcpy_h2d: CUDA context bind failed: {e}"));
return Err(AbiError::LaunchFailed);
}
use cust::sys as cuda_sys;
let memory = caller
.get_export("memory")
.and_then(|e| e.into_memory())
.ok_or(AbiError::InvalidPointer)?;
let src = &memory.data(&caller)[start..end];
let status = unsafe {
cuda_sys::cuMemcpyHtoD_v2(
dev.device_ptr,
src.as_ptr() as *const std::ffi::c_void,
len_u32 as usize,
)
};
if status != cuda_sys::CUresult::CUDA_SUCCESS {
caller
.data()
.wasi_cuda()
.record_error(format!("memcpy_h2d: cuMemcpyHtoD failed: {status:?}"));
return Err(AbiError::LaunchFailed);
}
return Ok(());
}
#[cfg(not(feature = "cuda"))]
{
caller
.data()
.wasi_cuda()
.record_error("memcpy_h2d: CUDA not available on this host");
Err(AbiError::NotAvailable)
}
}
fn memcpy_d2h_impl<T: HasWasiCuda>(
caller: &mut Caller<'_, T>,
dst_ptr: i32,
handle: u64,
len: i32,
) -> Result<(), AbiError> {
let _span = info_span!(
"wasi_cuda.memcpy_d2h",
instance = %caller.data().wasi_cuda().instance_id,
handle = handle,
)
.entered();
let owner = caller.data().wasi_cuda().instance_id;
let device_mem = caller.data().wasi_cuda().device_mem.clone();
let dev = match device_mem.lookup(handle, owner) {
Ok(d) => d,
Err(e) => {
caller
.data()
.wasi_cuda()
.record_error(format!("memcpy_d2h: handle {handle} {}", e.name()));
return Err(e);
}
};
let len_u32 = len as u32;
if (len_u32 as u64) > dev.size {
caller.data().wasi_cuda().record_error(format!(
"memcpy_d2h: len {len_u32} exceeds device buffer size {}",
dev.size
));
return Err(AbiError::InvalidArgs);
}
let (start, end) = match checked_guest_region(caller, dst_ptr, len_u32) {
Ok(r) => r,
Err(e) => {
caller.data().wasi_cuda().record_error(format!(
"memcpy_d2h: dest region [{dst_ptr}, +{len_u32}) out of bounds"
));
return Err(e);
}
};
let _ = (start, end);
#[cfg(feature = "cuda")]
{
if let Err(e) = crate::cuda_ctx::ensure_current_context() {
caller
.data()
.wasi_cuda()
.record_error(format!("memcpy_d2h: CUDA context bind failed: {e}"));
return Err(AbiError::LaunchFailed);
}
use cust::sys as cuda_sys;
let memory = caller
.get_export("memory")
.and_then(|e| e.into_memory())
.ok_or(AbiError::InvalidPointer)?;
let dst = &mut memory.data_mut(&mut *caller)[start..end];
let status = unsafe {
cuda_sys::cuMemcpyDtoH_v2(
dst.as_mut_ptr() as *mut std::ffi::c_void,
dev.device_ptr,
len_u32 as usize,
)
};
if status != cuda_sys::CUresult::CUDA_SUCCESS {
caller
.data()
.wasi_cuda()
.record_error(format!("memcpy_d2h: cuMemcpyDtoH failed: {status:?}"));
return Err(AbiError::LaunchFailed);
}
return Ok(());
}
#[cfg(not(feature = "cuda"))]
{
caller
.data()
.wasi_cuda()
.record_error("memcpy_d2h: CUDA not available on this host");
Err(AbiError::NotAvailable)
}
}
fn load_ptx_impl<T: HasWasiCuda>(
caller: &mut Caller<'_, T>,
ptx_ptr: i32,
ptx_len: i32,
entry_ptr: i32,
entry_len: i32,
) -> Result<KernelId, AbiError> {
let _span = info_span!(
"wasi_cuda.load_ptx",
instance = %caller.data().wasi_cuda().instance_id,
ptx_bytes = ptx_len as u64,
entry_bytes = entry_len as u64,
)
.entered();
if ptx_len < 0 {
caller
.data()
.wasi_cuda()
.record_error(format!("load_ptx: negative ptx_len ({ptx_len})"));
return Err(AbiError::InvalidPointer);
}
if (ptx_len as usize) > MAX_PTX_BYTES {
caller.data().wasi_cuda().record_error(format!(
"load_ptx: ptx_len {ptx_len} exceeds MAX_PTX_BYTES {MAX_PTX_BYTES}"
));
return Err(AbiError::QuotaExceeded);
}
if entry_len < 0 || (entry_len as usize) > MAX_ENTRY_NAME_BYTES {
caller.data().wasi_cuda().record_error(format!(
"load_ptx: entry_len {entry_len} exceeds MAX_ENTRY_NAME_BYTES {MAX_ENTRY_NAME_BYTES}"
));
return Err(AbiError::QuotaExceeded);
}
let ptx = read_bytes(caller, ptx_ptr, ptx_len)?;
let entry_bytes = read_bytes(caller, entry_ptr, entry_len)?;
let entry = String::from_utf8(entry_bytes).map_err(|_| {
caller
.data()
.wasi_cuda()
.record_error("load_ptx: entry name is not valid UTF-8");
AbiError::InvalidArgs
})?;
#[cfg(not(feature = "cuda"))]
{
if ptx.is_empty() {
caller
.data()
.wasi_cuda()
.record_error("load_ptx: PTX bytes empty");
return Err(AbiError::MalformedPtx);
}
let ptx_str = match std::str::from_utf8(&ptx) {
Ok(s) => s,
Err(_) => {
caller
.data()
.wasi_cuda()
.record_error("load_ptx: PTX bytes are not valid UTF-8");
return Err(AbiError::MalformedPtx);
}
};
for directive in [".version", ".target", ".entry"] {
if !ptx_str.contains(directive) {
caller.data().wasi_cuda().record_error(format!(
"load_ptx: PTX missing required directive {directive}"
));
return Err(AbiError::MalformedPtx);
}
}
let owner = caller.data().wasi_cuda().instance_id;
let entry_record = KernelEntry {
owner,
entry: entry.clone(),
ptx_bytes_len: ptx.len(),
};
let registry = caller.data().wasi_cuda().registry.clone();
let id = registry.register(entry_record)?;
info!(target: "tensor_wasm_wasi_gpu::host", instance = %owner, kernel = %id, entry, "PTX registered (stub: cuda feature off)");
Ok(id)
}
#[cfg(feature = "cuda")]
{
use cust::module::Module;
let ptx_str = std::str::from_utf8(&ptx).map_err(|_| {
caller
.data()
.wasi_cuda()
.record_error("load_ptx: PTX bytes are not valid UTF-8");
AbiError::MalformedPtx
})?;
if ptx_str.as_bytes().contains(&0u8) {
caller
.data()
.wasi_cuda()
.record_error("load_ptx: PTX bytes contain an interior NUL");
return Err(AbiError::MalformedPtx);
}
crate::cuda_ctx::ensure_current_context().map_err(|e| {
caller
.data()
.wasi_cuda()
.record_error(format!("load_ptx: CUDA context bind failed: {e}"));
AbiError::NotAvailable
})?;
let module = Module::from_ptx(ptx_str, &[]).map_err(|e| {
caller
.data()
.wasi_cuda()
.record_error(format!("load_ptx: cust compile failed: {e:?}"));
AbiError::MalformedPtx
})?;
let owner = caller.data().wasi_cuda().instance_id;
let entry_record = KernelEntry {
owner,
entry: entry.clone(),
ptx_bytes_len: ptx.len(),
module: Some(Arc::new(module)),
};
let registry = caller.data().wasi_cuda().registry.clone();
let id = registry.register(entry_record)?;
info!(target: "tensor_wasm_wasi_gpu::host", instance = %owner, kernel = %id, entry, "PTX compiled and registered via cust");
Ok(id)
}
}
#[allow(clippy::too_many_arguments)]
fn validate_launch_args<T: HasWasiCuda>(
caller: &mut Caller<'_, T>,
kernel_id: i64,
grid_x: i32,
grid_y: i32,
grid_z: i32,
block_x: i32,
block_y: i32,
block_z: i32,
shared_mem: i32,
args_ptr: i32,
args_len: i32,
) -> Result<KernelId, AbiError> {
if args_len < 0 || args_ptr < 0 {
caller.data().wasi_cuda().record_error(format!(
"launch: negative args_ptr ({args_ptr}) or args_len ({args_len})"
));
return Err(AbiError::InvalidPointer);
}
if args_len > 0 {
let memory = caller
.get_export("memory")
.and_then(|e| e.into_memory())
.ok_or_else(|| {
caller
.data()
.wasi_cuda()
.record_error("launch: caller has no exported memory but args_len > 0");
AbiError::InvalidPointer
})?;
let mem_len = memory.data(&caller).len();
let start = args_ptr as usize;
let end = start.checked_add(args_len as usize).ok_or_else(|| {
caller.data().wasi_cuda().record_error(format!(
"launch: args_ptr + args_len overflows usize ({args_ptr} + {args_len})"
));
AbiError::InvalidPointer
})?;
if end > mem_len {
caller.data().wasi_cuda().record_error(format!(
"launch: args region [{start}, {end}) exceeds Wasm memory len {mem_len}"
));
return Err(AbiError::InvalidPointer);
}
}
if kernel_id < 0 {
return Err(AbiError::InvalidKernel);
}
if block_x <= 0 || block_y <= 0 || block_z <= 0 {
caller.data().wasi_cuda().record_error(format!(
"launch: block dim must be >= 1 (got {block_x}, {block_y}, {block_z})"
));
return Err(AbiError::InvalidDimensions);
}
if grid_x <= 0 || grid_y <= 0 || grid_z <= 0 {
caller.data().wasi_cuda().record_error(format!(
"launch: grid dim must be >= 1 (got {grid_x}, {grid_y}, {grid_z})"
));
return Err(AbiError::InvalidDimensions);
}
if shared_mem < 0 {
caller.data().wasi_cuda().record_error(format!(
"launch: shared_mem must be >= 0 (got {shared_mem})"
));
return Err(AbiError::InvalidDimensions);
}
if shared_mem > MAX_DYNAMIC_SHARED_MEM_BYTES {
caller.data().wasi_cuda().record_error(format!(
"launch: shared_mem {shared_mem} exceeds MAX_DYNAMIC_SHARED_MEM_BYTES={MAX_DYNAMIC_SHARED_MEM_BYTES}"
));
return Err(AbiError::InvalidDimensions);
}
if (block_x as u32) > MAX_BLOCK_DIM
|| (block_y as u32) > MAX_BLOCK_DIM
|| (block_z as u32) > MAX_BLOCK_DIM
{
caller.data().wasi_cuda().record_error(format!(
"launch: block dim exceeds MAX_BLOCK_DIM={MAX_BLOCK_DIM} (got {block_x}, {block_y}, {block_z})"
));
return Err(AbiError::InvalidDimensions);
}
let threads_per_block = (block_x as u64)
.checked_mul(block_y as u64)
.and_then(|v| v.checked_mul(block_z as u64))
.ok_or_else(|| {
caller
.data()
.wasi_cuda()
.record_error("launch: block dim product overflows u64");
AbiError::InvalidDimensions
})?;
if threads_per_block > MAX_THREADS_PER_BLOCK as u64 {
caller.data().wasi_cuda().record_error(format!(
"launch: threads-per-block {threads_per_block} exceeds MAX_THREADS_PER_BLOCK={MAX_THREADS_PER_BLOCK}"
));
return Err(AbiError::InvalidDimensions);
}
if (grid_x as u32) > MAX_GRID_DIM
|| (grid_y as u32) > MAX_GRID_DIM
|| (grid_z as u32) > MAX_GRID_DIM
{
caller.data().wasi_cuda().record_error(format!(
"launch: grid dim exceeds MAX_GRID_DIM={MAX_GRID_DIM} (got {grid_x}, {grid_y}, {grid_z})"
));
return Err(AbiError::InvalidDimensions);
}
Ok(KernelId(kernel_id as u64))
}
#[allow(clippy::too_many_arguments)]
async fn launch_impl_async<T: HasWasiCuda>(
caller: &mut Caller<'_, T>,
kernel_id: i64,
grid_x: i32,
grid_y: i32,
grid_z: i32,
block_x: i32,
block_y: i32,
block_z: i32,
shared_mem: i32,
args_ptr: i32,
args_len: i32,
) -> Result<(), AbiError> {
let launch_span = info_span!(
"wasi_cuda.launch",
instance = %caller.data().wasi_cuda().instance_id,
kernel = kernel_id,
grid_x = grid_x, grid_y = grid_y, grid_z = grid_z,
block_x = block_x, block_y = block_y, block_z = block_z,
shared_mem = shared_mem,
);
launch_impl_async_inner(
caller, kernel_id, grid_x, grid_y, grid_z, block_x, block_y, block_z, shared_mem, args_ptr,
args_len,
)
.instrument(launch_span)
.await
}
#[allow(clippy::too_many_arguments)]
async fn launch_impl_async_inner<T: HasWasiCuda>(
caller: &mut Caller<'_, T>,
kernel_id: i64,
grid_x: i32,
grid_y: i32,
grid_z: i32,
block_x: i32,
block_y: i32,
block_z: i32,
shared_mem: i32,
args_ptr: i32,
args_len: i32,
) -> Result<(), AbiError> {
let kid = validate_launch_args(
caller, kernel_id, grid_x, grid_y, grid_z, block_x, block_y, block_z, shared_mem, args_ptr,
args_len,
)?;
let owner = caller.data().wasi_cuda().instance_id;
let registry = caller.data().wasi_cuda().registry.clone();
let bp = caller.data().wasi_cuda().deadline_aware_back_pressure();
let _permit = match bp.acquire_borrowed().await {
Ok(p) => p,
Err(e) => {
caller.data().wasi_cuda().record_back_pressure_rejection();
return Err(e);
}
};
let lowered_args: Vec<LoweredArg> = if args_len > 0 {
let mem = caller
.get_export("memory")
.and_then(|e| e.into_memory())
.ok_or(AbiError::InvalidPointer)?;
let mem_data = mem.data(&caller);
let start = args_ptr as usize;
let end = start + args_len as usize;
let argv_slice = &mem_data[start..end];
match parse_argv(argv_slice, mem_data) {
Ok(v) => v,
Err(e) => {
caller.data().wasi_cuda().record_error(format!(
"launch: kernel argv parse failed ({}); args_len={args_len}",
e.name()
));
return Err(e);
}
}
} else {
Vec::new()
};
*caller
.data()
.wasi_cuda()
.last_lowered_args
.lock()
.unwrap_or_else(|e| e.into_inner()) = lowered_args.clone();
let handle = registry.lookup(kid, owner)?;
#[cfg(feature = "cuda")]
{
use cust::event::{Event, EventFlags};
use cust::stream::{Stream, StreamFlags};
use crate::kernel_args::build_kernel_param_storage;
crate::cuda_ctx::ensure_current_context().map_err(|e| {
caller
.data()
.wasi_cuda()
.record_error(format!("launch: CUDA context bind failed: {e}"));
AbiError::LaunchFailed
})?;
let module = handle.module.clone().ok_or_else(|| {
caller
.data()
.wasi_cuda()
.record_error("launch: kernel entry has no compiled module");
AbiError::InvalidKernel
})?;
let func = module.get_function(&handle.entry).map_err(|e| {
caller.data().wasi_cuda().record_error(format!(
"launch: get_function({}) failed: {e:?}",
handle.entry
));
AbiError::LaunchFailed
})?;
let stream = Stream::new(StreamFlags::NON_BLOCKING, None).map_err(|e| {
caller
.data()
.wasi_cuda()
.record_error(format!("launch: Stream::new failed: {e:?}"));
AbiError::LaunchFailed
})?;
let mut storage = build_kernel_param_storage(&lowered_args);
let param_count = storage.len();
let kernel_params_ptr: *mut *mut std::ffi::c_void = if param_count == 0 {
std::ptr::null_mut()
} else {
storage.as_ptr()
};
use cust::sys as cuda_sys;
for arg in &lowered_args {
if let LoweredArg::Ptr { host_ptr, .. } = arg {
let mut is_managed: std::os::raw::c_int = 0;
let res = unsafe {
cuda_sys::cuPointerGetAttribute(
&mut is_managed as *mut std::os::raw::c_int as *mut std::ffi::c_void,
cuda_sys::CUpointer_attribute_enum::CU_POINTER_ATTRIBUTE_IS_MANAGED,
*host_ptr as cuda_sys::CUdeviceptr,
)
};
if res != cuda_sys::CUresult::CUDA_SUCCESS || is_managed == 0 {
caller.data().wasi_cuda().record_error(format!(
"launch: kernel pointer argument is not GPU-addressable \
(cuPointerGetAttribute IS_MANAGED -> {res:?}, \
is_managed={is_managed}); refusing to launch so an invalid \
device pointer cannot raise a sticky CUDA_ERROR_ILLEGAL_ADDRESS \
that would poison the shared CUDA context for all tenants. Back \
guest linear memory with the unified-memory MemoryCreator \
(enable the `unified-memory` feature on tensor-wasm-mem)."
));
return Err(AbiError::LaunchFailed);
}
}
}
let launch_status = unsafe {
cuda_sys::cuLaunchKernel(
func.to_raw(),
grid_x as u32,
grid_y as u32,
grid_z as u32,
block_x as u32,
block_y as u32,
block_z as u32,
shared_mem as u32,
stream.as_inner(),
kernel_params_ptr,
std::ptr::null_mut(),
)
};
if launch_status != cuda_sys::CUresult::CUDA_SUCCESS {
caller.data().wasi_cuda().record_error(format!(
"launch: cuLaunchKernel failed with status {launch_status:?}; \
param_count={param_count}"
));
return Err(AbiError::LaunchFailed);
}
let event = Event::new(EventFlags::DEFAULT).map_err(|e| {
caller
.data()
.wasi_cuda()
.record_error(format!("launch: Event::new failed: {e:?}"));
AbiError::LaunchFailed
})?;
event.record(&stream).map_err(|e| {
caller
.data()
.wasi_cuda()
.record_error(format!("launch: event.record failed: {e:?}"));
AbiError::LaunchFailed
})?;
let handle_for_keepalive = handle.clone();
let result = tokio::task::spawn_blocking(move || -> Result<(), String> {
let _keep_event = event;
let _keep_module = handle_for_keepalive;
let _keep_storage = storage;
crate::cuda_ctx::ensure_current_context()?;
stream
.synchronize()
.map_err(|e| format!("stream synchronize failed: {e:?}"))
})
.await
.map_err(|_| {
AbiError::Internal
})?;
result.map_err(|e| {
caller
.data()
.wasi_cuda()
.record_error(format!("launch: {e}"));
AbiError::LaunchFailed
})?;
*caller
.data()
.wasi_cuda()
.last_lowered_args
.lock()
.unwrap_or_else(|e| e.into_inner()) = lowered_args;
caller.data().wasi_cuda().record_kernel_launched();
drop(handle);
Ok(())
}
#[cfg(not(feature = "cuda"))]
{
let _ = handle; let parsed_count = lowered_args.len();
*caller
.data()
.wasi_cuda()
.last_lowered_args
.lock()
.unwrap_or_else(|e| e.into_inner()) = lowered_args;
caller.data().wasi_cuda().record_kernel_launched();
caller.data().wasi_cuda().record_error(format!(
"launch: CUDA not available on this host (argv parsed: {parsed_count} args)"
));
Err(AbiError::NotAvailable)
}
}
fn sync_impl<T: HasWasiCuda>(_caller: &Caller<'_, T>) -> Result<(), AbiError> {
let _span = info_span!(
"wasi_cuda.sync",
instance = %_caller.data().wasi_cuda().instance_id,
)
.entered();
#[cfg(feature = "cuda")]
{
use cust::context::CurrentContext;
if let Err(e) = crate::cuda_ctx::ensure_current_context() {
_caller
.data()
.wasi_cuda()
.record_error(format!("sync: CUDA context bind failed: {e}"));
return Err(AbiError::LaunchFailed);
}
match CurrentContext::synchronize() {
Ok(()) => Ok(()),
Err(e) => {
_caller
.data()
.wasi_cuda()
.record_error(format!("sync: CurrentContext::synchronize failed: {e:?}"));
Err(AbiError::LaunchFailed)
}
}
}
#[cfg(not(feature = "cuda"))]
{
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::abi::FN_LAST_ERROR_PTR;
struct Dummy(WasiCudaContext);
impl HasWasiCuda for Dummy {
fn wasi_cuda(&self) -> &WasiCudaContext {
&self.0
}
}
#[test]
fn record_and_read_error() {
let ctx = WasiCudaContext::new(InstanceId(42));
ctx.record_error("oh no");
assert_eq!(ctx.last_error().as_deref(), Some("oh no"));
}
#[test]
fn poisoned_lock_does_not_panic_last_lowered_args() {
use std::sync::Arc;
let ctx = Arc::new(WasiCudaContext::new(InstanceId(7)));
let poisoner = {
let ctx = Arc::clone(&ctx);
std::thread::spawn(move || {
let _guard = ctx.last_lowered_args.lock().unwrap();
panic!("poison the lock on purpose");
})
};
assert!(poisoner.join().is_err());
assert!(ctx.last_lowered_args.is_poisoned());
let snapshot = ctx.last_lowered_args();
assert!(snapshot.is_empty());
}
#[test]
fn add_to_linker_compiles() {
let config = wasmtime::Config::new();
let engine = wasmtime::Engine::new(&config).unwrap();
let mut linker: Linker<Dummy> = Linker::new(&engine);
add_to_linker(&mut linker).expect("add_to_linker");
}
#[tokio::test]
async fn add_to_linker_does_not_register_last_error_ptr() {
let config = wasmtime::Config::new();
let engine = wasmtime::Engine::new(&config).unwrap();
let mut linker: Linker<Dummy> = Linker::new(&engine);
add_to_linker(&mut linker).expect("add_to_linker");
let wat = format!(
r#"
(module
(import "{MODULE}" "{fn_name}" (func (result i32)))
)
"#,
fn_name = FN_LAST_ERROR_PTR
);
let bytes = wat::parse_str(&wat).unwrap();
let module = wasmtime::Module::new(&engine, &bytes).expect("compile");
let mut store = wasmtime::Store::new(&engine, Dummy(WasiCudaContext::new(InstanceId(101))));
let result = linker.instantiate_async(&mut store, &module).await;
assert!(
result.is_err(),
"instantiation must fail because FN_LAST_ERROR_PTR is not registered"
);
}
#[test]
fn shared_back_pressure_constructor() {
let bp = Arc::new(crate::async_dispatch::BackPressure::with_cap(8));
let a = WasiCudaContext::with_back_pressure(InstanceId(1), bp.clone());
let b = WasiCudaContext::with_back_pressure(InstanceId(2), bp.clone());
assert_eq!(a.back_pressure().max_concurrent(), 8);
assert_eq!(b.back_pressure().max_concurrent(), 8);
assert!(Arc::ptr_eq(a.back_pressure(), b.back_pressure()));
}
#[tokio::test]
async fn launch_with_inbounds_unknown_tag_returns_invalid_args() {
let config = wasmtime::Config::new();
let engine = wasmtime::Engine::new(&config).unwrap();
let mut linker: Linker<Dummy> = Linker::new(&engine);
add_to_linker(&mut linker).expect("add_to_linker");
let wat = format!(
r#"
(module
(import "{m}" "{fn_name}"
(func $launch (param i64 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(func (export "try_launch") (result i32)
(call $launch
(i64.const 0) ;; kernel_id
(i32.const 1) (i32.const 1) (i32.const 1) ;; grid
(i32.const 1) (i32.const 1) (i32.const 1) ;; block
(i32.const 0) ;; shared_mem
(i32.const 0) ;; args_ptr — inside the one-page region
(i32.const 4) ;; args_len — 4 bytes, in-bounds
))
)
"#,
m = MODULE,
fn_name = FN_LAUNCH,
);
let bytes = wat::parse_str(&wat).unwrap();
let module = wasmtime::Module::new(&engine, &bytes).expect("compile");
let mut ctx = WasiCudaContext::new(InstanceId(202));
ctx.enable_wasi_cuda();
let mut store = wasmtime::Store::new(&engine, Dummy(ctx));
let instance = linker
.instantiate_async(&mut store, &module)
.await
.expect("instantiate");
let try_launch = instance
.get_typed_func::<(), i32>(&mut store, "try_launch")
.expect("typed func");
let rc = try_launch.call_async(&mut store, ()).await.expect("call");
assert_eq!(
rc,
AbiError::InvalidArgs.code(),
"unknown leading tag byte must return InvalidArgs ({}), \
not KernelArgsUnsupported ({}) or InvalidPointer ({})",
AbiError::InvalidArgs.code(),
AbiError::KernelArgsUnsupported.code(),
AbiError::InvalidPointer.code(),
);
let last = store.data().wasi_cuda().last_error().unwrap_or_default();
assert!(
last.contains("kernel argv parse failed"),
"expected argv-parse error, got: {last}"
);
}
#[tokio::test]
async fn launch_with_oob_args_returns_invalid_pointer() {
let config = wasmtime::Config::new();
let engine = wasmtime::Engine::new(&config).unwrap();
let mut linker: Linker<Dummy> = Linker::new(&engine);
add_to_linker(&mut linker).expect("add_to_linker");
let wat = format!(
r#"
(module
(import "{m}" "{fn_name}"
(func $launch (param i64 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(func (export "try_launch") (result i32)
(call $launch
(i64.const 0)
(i32.const 1) (i32.const 1) (i32.const 1)
(i32.const 1) (i32.const 1) (i32.const 1)
(i32.const 0)
(i32.const 70000) ;; args_ptr — past end of single page
(i32.const 4) ;; args_len — would overshoot
))
)
"#,
m = MODULE,
fn_name = FN_LAUNCH,
);
let bytes = wat::parse_str(&wat).unwrap();
let module = wasmtime::Module::new(&engine, &bytes).expect("compile");
let mut ctx = WasiCudaContext::new(InstanceId(203));
ctx.enable_wasi_cuda();
let mut store = wasmtime::Store::new(&engine, Dummy(ctx));
let instance = linker
.instantiate_async(&mut store, &module)
.await
.expect("instantiate");
let try_launch = instance
.get_typed_func::<(), i32>(&mut store, "try_launch")
.expect("typed func");
let rc = try_launch.call_async(&mut store, ()).await.expect("call");
assert_eq!(
rc,
AbiError::InvalidPointer.code(),
"OOB args region must return InvalidPointer (memory fault), \
not KernelArgsUnsupported — the bounds-check must run first"
);
}
#[tokio::test]
async fn launch_with_oversized_argv_returns_kernel_args_unsupported() {
use crate::kernel_args::MAX_KERNEL_ARGS_BYTES;
let config = wasmtime::Config::new();
let engine = wasmtime::Engine::new(&config).unwrap();
let mut linker: Linker<Dummy> = Linker::new(&engine);
add_to_linker(&mut linker).expect("add_to_linker");
let oversized = (MAX_KERNEL_ARGS_BYTES + 1) as i32;
let wat = format!(
r#"
(module
(import "{m}" "{fn_name}"
(func $launch (param i64 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "memory") 16)
(func (export "try_launch") (result i32)
(call $launch
(i64.const 0)
(i32.const 1) (i32.const 1) (i32.const 1)
(i32.const 1) (i32.const 1) (i32.const 1)
(i32.const 0)
(i32.const 0)
(i32.const {oversized})))
)
"#,
m = MODULE,
fn_name = FN_LAUNCH,
);
let bytes = wat::parse_str(&wat).unwrap();
let module = wasmtime::Module::new(&engine, &bytes).expect("compile");
let mut ctx = WasiCudaContext::new(InstanceId(220));
ctx.enable_wasi_cuda();
let mut store = wasmtime::Store::new(&engine, Dummy(ctx));
let instance = linker
.instantiate_async(&mut store, &module)
.await
.expect("instantiate");
let try_launch = instance
.get_typed_func::<(), i32>(&mut store, "try_launch")
.expect("typed func");
let rc = try_launch.call_async(&mut store, ()).await.expect("call");
assert_eq!(
rc,
AbiError::KernelArgsUnsupported.code(),
"argv buffer past sanity cap must return KernelArgsUnsupported"
);
}
#[tokio::test]
async fn launch_without_capability_returns_not_available() {
let config = wasmtime::Config::new();
let engine = wasmtime::Engine::new(&config).unwrap();
let mut linker: Linker<Dummy> = Linker::new(&engine);
add_to_linker(&mut linker).expect("add_to_linker");
let wat = format!(
r#"
(module
(import "{m}" "{fn_launch}"
(func $launch (param i64 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(import "{m}" "{fn_last_err_len}"
(func $last_error_len (result i32)))
(memory (export "memory") 1)
(func (export "try_launch") (result i32)
(call $launch
(i64.const 1)
(i32.const 1) (i32.const 1) (i32.const 1)
(i32.const 1) (i32.const 1) (i32.const 1)
(i32.const 0) (i32.const 0) (i32.const 0)))
(func (export "probe_last_error_len") (result i32)
(call $last_error_len))
)
"#,
m = MODULE,
fn_launch = FN_LAUNCH,
fn_last_err_len = FN_LAST_ERROR_LEN,
);
let bytes = wat::parse_str(&wat).unwrap();
let module = wasmtime::Module::new(&engine, &bytes).expect("compile");
let ctx = WasiCudaContext::new(InstanceId(901));
assert!(
!ctx.wasi_cuda_enabled(),
"freshly-constructed context must default to disabled"
);
let mut store = wasmtime::Store::new(&engine, Dummy(ctx));
let instance = linker
.instantiate_async(&mut store, &module)
.await
.expect("instantiate");
let try_launch = instance
.get_typed_func::<(), i32>(&mut store, "try_launch")
.expect("typed func");
let rc = try_launch.call_async(&mut store, ()).await.expect("call");
assert_eq!(
rc,
AbiError::NotAvailable.code(),
"ungranted wasi-cuda capability must surface as NotAvailable, \
got {rc}"
);
assert!(
store.data().wasi_cuda().last_error().is_none(),
"disabled-capability path must NOT record_error, but found: {:?}",
store.data().wasi_cuda().last_error()
);
let probe = instance
.get_typed_func::<(), i32>(&mut store, "probe_last_error_len")
.expect("typed func");
let len_rc = probe.call_async(&mut store, ()).await.expect("call");
assert_eq!(
len_rc,
AbiError::NotAvailable.code(),
"last_error_len on a disabled-capability context must return \
the NotAvailable sentinel ({}), got {len_rc}",
AbiError::NotAvailable.code()
);
}
#[tokio::test]
async fn launch_with_capability_passes_gate() {
let config = wasmtime::Config::new();
let engine = wasmtime::Engine::new(&config).unwrap();
let mut linker: Linker<Dummy> = Linker::new(&engine);
add_to_linker(&mut linker).expect("add_to_linker");
let wat = format!(
r#"
(module
(import "{m}" "{fn_name}"
(func $launch (param i64 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(func (export "try_launch") (result i32)
(call $launch
(i64.const 999)
(i32.const 1) (i32.const 1) (i32.const 1)
(i32.const 1) (i32.const 1) (i32.const 1)
(i32.const 0) (i32.const 0) (i32.const 0)))
)
"#,
m = MODULE,
fn_name = FN_LAUNCH,
);
let bytes = wat::parse_str(&wat).unwrap();
let module = wasmtime::Module::new(&engine, &bytes).expect("compile");
let mut ctx = WasiCudaContext::new(InstanceId(902));
ctx.enable_wasi_cuda();
assert!(
ctx.wasi_cuda_enabled(),
"enable_wasi_cuda() must flip the flag"
);
let mut store = wasmtime::Store::new(&engine, Dummy(ctx));
let instance = linker
.instantiate_async(&mut store, &module)
.await
.expect("instantiate");
let try_launch = instance
.get_typed_func::<(), i32>(&mut store, "try_launch")
.expect("typed func");
let rc = try_launch.call_async(&mut store, ()).await.expect("call");
assert_eq!(
rc,
AbiError::InvalidKernel.code(),
"with capability granted, an unknown kernel id must reach the \
registry lookup and return InvalidKernel; got {rc}"
);
}
#[test]
fn registry_lookup_handle_outlives_remove() {
let reg = KernelRegistry::new();
let id = reg
.register(KernelEntry {
owner: InstanceId(7),
entry: "k".into(),
ptx_bytes_len: 16,
#[cfg(feature = "cuda")]
module: None,
})
.unwrap();
let handle = reg.lookup(id, InstanceId(7)).unwrap();
assert!(reg.remove(id).is_some());
assert_eq!(handle.entry, "k");
}
#[tokio::test]
async fn launch_with_oversize_shared_mem_returns_invalid_dimensions() {
let config = wasmtime::Config::new();
let engine = wasmtime::Engine::new(&config).unwrap();
let mut linker: Linker<Dummy> = Linker::new(&engine);
add_to_linker(&mut linker).expect("add_to_linker");
let oversize = MAX_DYNAMIC_SHARED_MEM_BYTES + 1;
let wat = format!(
r#"
(module
(import "{m}" "{fn_name}"
(func $launch (param i64 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(func (export "try_launch") (result i32)
(call $launch
(i64.const 0)
(i32.const 1) (i32.const 1) (i32.const 1)
(i32.const 1) (i32.const 1) (i32.const 1)
(i32.const {oversize}) ;; shared_mem — above the host cap
(i32.const 0) (i32.const 0)))
)
"#,
m = MODULE,
fn_name = FN_LAUNCH,
);
let bytes = wat::parse_str(&wat).unwrap();
let module = wasmtime::Module::new(&engine, &bytes).expect("compile");
let mut ctx = WasiCudaContext::new(InstanceId(230));
ctx.enable_wasi_cuda();
let mut store = wasmtime::Store::new(&engine, Dummy(ctx));
let instance = linker
.instantiate_async(&mut store, &module)
.await
.expect("instantiate");
let try_launch = instance
.get_typed_func::<(), i32>(&mut store, "try_launch")
.expect("typed func");
let rc = try_launch.call_async(&mut store, ()).await.expect("call");
assert_eq!(
rc,
AbiError::InvalidDimensions.code(),
"shared_mem past the host cap must return InvalidDimensions, got {rc}"
);
let last = store.data().wasi_cuda().last_error().unwrap_or_default();
assert!(
last.contains("MAX_DYNAMIC_SHARED_MEM_BYTES"),
"expected shared-mem cap error, got: {last}"
);
}
#[tokio::test]
async fn load_ptx_negative_ptx_len_returns_invalid_pointer() {
let config = wasmtime::Config::new();
let engine = wasmtime::Engine::new(&config).unwrap();
let mut linker: Linker<Dummy> = Linker::new(&engine);
add_to_linker(&mut linker).expect("add_to_linker");
let wat = format!(
r#"
(module
(import "{m}" "{fn_name}"
(func $load_ptx (param i32 i32 i32 i32) (result i64)))
(memory (export "memory") 1)
(func (export "try_load") (result i64)
(call $load_ptx
(i32.const 0) ;; ptx_ptr
(i32.const -1) ;; ptx_len — negative
(i32.const 0) ;; entry_ptr
(i32.const 4))) ;; entry_len
)
"#,
m = MODULE,
fn_name = FN_LOAD_PTX,
);
let bytes = wat::parse_str(&wat).unwrap();
let module = wasmtime::Module::new(&engine, &bytes).expect("compile");
let mut ctx = WasiCudaContext::new(InstanceId(231));
ctx.enable_wasi_cuda();
let mut store = wasmtime::Store::new(&engine, Dummy(ctx));
let instance = linker
.instantiate_async(&mut store, &module)
.await
.expect("instantiate");
let try_load = instance
.get_typed_func::<(), i64>(&mut store, "try_load")
.expect("typed func");
let rc = try_load.call_async(&mut store, ()).await.expect("call");
assert_eq!(
rc,
AbiError::InvalidPointer.code() as i64,
"negative ptx_len must return InvalidPointer ({}), not QuotaExceeded ({})",
AbiError::InvalidPointer.code(),
AbiError::QuotaExceeded.code(),
);
let last = store.data().wasi_cuda().last_error().unwrap_or_default();
assert!(
last.contains("negative ptx_len"),
"expected negative-ptx_len error, got: {last}"
);
}
fn device_mem_wat() -> String {
format!(
r#"
(module
(import "{m}" "{fn_alloc}"
(func $alloc (param i32 i32) (result i64)))
(import "{m}" "{fn_free}"
(func $free (param i32 i32) (result i32)))
(import "{m}" "{fn_h2d}"
(func $h2d (param i32 i32 i32 i32) (result i32)))
(import "{m}" "{fn_d2h}"
(func $d2h (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
;; alloc(size_lo, size_hi) -> i64
(func (export "do_alloc") (param i32 i32) (result i64)
(call $alloc (local.get 0) (local.get 1)))
;; free(handle_lo, handle_hi) -> i32
(func (export "do_free") (param i32 i32) (result i32)
(call $free (local.get 0) (local.get 1)))
;; memcpy_h2d(handle_lo, handle_hi, src_ptr, len) -> i32
(func (export "do_h2d") (param i32 i32 i32 i32) (result i32)
(call $h2d (local.get 0) (local.get 1) (local.get 2) (local.get 3)))
;; memcpy_d2h(dst_ptr, handle_lo, handle_hi, len) -> i32
(func (export "do_d2h") (param i32 i32 i32 i32) (result i32)
(call $d2h (local.get 0) (local.get 1) (local.get 2) (local.get 3)))
)
"#,
m = MODULE,
fn_alloc = FN_ALLOC,
fn_free = FN_FREE,
fn_h2d = FN_MEMCPY_H2D,
fn_d2h = FN_MEMCPY_D2H,
)
}
#[tokio::test]
async fn memcpy_h2d_oob_source_returns_invalid_pointer() {
let config = wasmtime::Config::new();
let engine = wasmtime::Engine::new(&config).unwrap();
let mut linker: Linker<Dummy> = Linker::new(&engine);
add_to_linker(&mut linker).expect("add_to_linker");
let mut ctx = WasiCudaContext::new(InstanceId(300));
ctx.enable_wasi_cuda();
let handle = ctx
.device_mem()
.insert(crate::device_mem::DeviceMemEntry {
owner: InstanceId(300),
size: 1024 * 1024,
#[cfg(feature = "cuda")]
device_ptr: 0,
})
.expect("insert");
let module = wasmtime::Module::new(&engine, wat::parse_str(device_mem_wat()).unwrap())
.expect("compile");
let mut store = wasmtime::Store::new(&engine, Dummy(ctx));
let instance = linker
.instantiate_async(&mut store, &module)
.await
.expect("instantiate");
let h2d = instance
.get_typed_func::<(i32, i32, i32, i32), i32>(&mut store, "do_h2d")
.expect("typed func");
let rc = h2d
.call_async(&mut store, (handle as i32, 0, 70000, 16))
.await
.expect("call");
assert_eq!(
rc,
AbiError::InvalidPointer.code(),
"OOB source region must return InvalidPointer, got {rc}"
);
}
#[tokio::test]
async fn memcpy_h2d_oversize_len_returns_invalid_args() {
let config = wasmtime::Config::new();
let engine = wasmtime::Engine::new(&config).unwrap();
let mut linker: Linker<Dummy> = Linker::new(&engine);
add_to_linker(&mut linker).expect("add_to_linker");
let mut ctx = WasiCudaContext::new(InstanceId(301));
ctx.enable_wasi_cuda();
let handle = ctx
.device_mem()
.insert(crate::device_mem::DeviceMemEntry {
owner: InstanceId(301),
size: 8,
#[cfg(feature = "cuda")]
device_ptr: 0,
})
.expect("insert");
let module = wasmtime::Module::new(&engine, wat::parse_str(device_mem_wat()).unwrap())
.expect("compile");
let mut store = wasmtime::Store::new(&engine, Dummy(ctx));
let instance = linker
.instantiate_async(&mut store, &module)
.await
.expect("instantiate");
let h2d = instance
.get_typed_func::<(i32, i32, i32, i32), i32>(&mut store, "do_h2d")
.expect("typed func");
let rc = h2d
.call_async(&mut store, (handle as i32, 0, 0, 16))
.await
.expect("call");
assert_eq!(
rc,
AbiError::InvalidArgs.code(),
"len > buffer size must return InvalidArgs, got {rc}"
);
}
#[tokio::test]
async fn device_mem_cross_owner_handle_rejected() {
let config = wasmtime::Config::new();
let engine = wasmtime::Engine::new(&config).unwrap();
let mut linker: Linker<Dummy> = Linker::new(&engine);
add_to_linker(&mut linker).expect("add_to_linker");
let mut ctx = WasiCudaContext::new(InstanceId(302));
ctx.enable_wasi_cuda();
let foreign = ctx
.device_mem()
.insert(crate::device_mem::DeviceMemEntry {
owner: InstanceId(999),
size: 4096,
#[cfg(feature = "cuda")]
device_ptr: 0,
})
.expect("insert");
let module = wasmtime::Module::new(&engine, wat::parse_str(device_mem_wat()).unwrap())
.expect("compile");
let mut store = wasmtime::Store::new(&engine, Dummy(ctx));
let instance = linker
.instantiate_async(&mut store, &module)
.await
.expect("instantiate");
let free = instance
.get_typed_func::<(i32, i32), i32>(&mut store, "do_free")
.expect("typed func");
let rc = free
.call_async(&mut store, (foreign as i32, 0))
.await
.expect("call");
assert_eq!(
rc,
AbiError::InvalidHandle.code(),
"cross-owner free must return InvalidHandle, got {rc}"
);
let h2d = instance
.get_typed_func::<(i32, i32, i32, i32), i32>(&mut store, "do_h2d")
.expect("typed func");
let rc = h2d
.call_async(&mut store, (foreign as i32, 0, 0, 16))
.await
.expect("call");
assert_eq!(
rc,
AbiError::InvalidHandle.code(),
"cross-owner memcpy_h2d must return InvalidHandle, got {rc}"
);
let d2h = instance
.get_typed_func::<(i32, i32, i32, i32), i32>(&mut store, "do_d2h")
.expect("typed func");
let rc = d2h
.call_async(&mut store, (0, foreign as i32, 0, 16))
.await
.expect("call");
assert_eq!(
rc,
AbiError::InvalidHandle.code(),
"cross-owner memcpy_d2h must return InvalidHandle, got {rc}"
);
assert!(store
.data()
.wasi_cuda()
.device_mem()
.lookup(foreign, InstanceId(999))
.is_ok());
}
#[tokio::test]
async fn alloc_rejects_zero_and_oversize() {
let config = wasmtime::Config::new();
let engine = wasmtime::Engine::new(&config).unwrap();
let mut linker: Linker<Dummy> = Linker::new(&engine);
add_to_linker(&mut linker).expect("add_to_linker");
let mut ctx = WasiCudaContext::new(InstanceId(303));
ctx.enable_wasi_cuda();
let module = wasmtime::Module::new(&engine, wat::parse_str(device_mem_wat()).unwrap())
.expect("compile");
let mut store = wasmtime::Store::new(&engine, Dummy(ctx));
let instance = linker
.instantiate_async(&mut store, &module)
.await
.expect("instantiate");
let alloc = instance
.get_typed_func::<(i32, i32), i64>(&mut store, "do_alloc")
.expect("typed func");
let rc = alloc.call_async(&mut store, (0, 0)).await.expect("call");
assert_eq!(
rc,
AbiError::InvalidArgs.code() as i64,
"zero-size alloc must return InvalidArgs, got {rc}"
);
let oversize = crate::device_mem::MAX_DEVICE_ALLOC_BYTES + 1;
let lo = (oversize & 0xffff_ffff) as i32;
let hi = (oversize >> 32) as i32;
let rc = alloc.call_async(&mut store, (lo, hi)).await.expect("call");
assert_eq!(
rc,
AbiError::QuotaExceeded.code() as i64,
"oversize alloc must return QuotaExceeded, got {rc}"
);
}
#[tokio::test]
async fn alloc_tracks_handle_then_free_lifecycle() {
let config = wasmtime::Config::new();
let engine = wasmtime::Engine::new(&config).unwrap();
let mut linker: Linker<Dummy> = Linker::new(&engine);
add_to_linker(&mut linker).expect("add_to_linker");
let mut ctx = WasiCudaContext::new(InstanceId(304));
ctx.enable_wasi_cuda();
let module = wasmtime::Module::new(&engine, wat::parse_str(device_mem_wat()).unwrap())
.expect("compile");
let mut store = wasmtime::Store::new(&engine, Dummy(ctx));
let instance = linker
.instantiate_async(&mut store, &module)
.await
.expect("instantiate");
let alloc = instance
.get_typed_func::<(i32, i32), i64>(&mut store, "do_alloc")
.expect("typed func");
let free = instance
.get_typed_func::<(i32, i32), i32>(&mut store, "do_free")
.expect("typed func");
let rc = alloc.call_async(&mut store, (4096, 0)).await.expect("call");
#[cfg(not(feature = "cuda"))]
assert_eq!(
rc,
AbiError::NotAvailable.code() as i64,
"no-CUDA alloc must return NotAvailable, got {rc}"
);
#[cfg(feature = "cuda")]
{
assert_eq!(
rc, 1,
"first allocation's handle id is 1 (registry hands out \
sequential ids from 1); a negative rc here means the device \
alloc failed (e.g. no current CUDA context), got {rc}"
);
}
assert_eq!(store.data().wasi_cuda().device_mem().len(), 1);
assert_eq!(
store.data().wasi_cuda().device_mem().total_device_bytes(),
4096
);
let handle: u64 = 1;
let rc = free
.call_async(&mut store, (handle as i32, 0))
.await
.expect("call");
assert_eq!(rc, 0, "free of a tracked handle must succeed, got {rc}");
assert!(store.data().wasi_cuda().device_mem().is_empty());
assert_eq!(
store.data().wasi_cuda().device_mem().total_device_bytes(),
0
);
let rc = free
.call_async(&mut store, (handle as i32, 0))
.await
.expect("call");
assert_eq!(
rc,
AbiError::InvalidHandle.code(),
"double-free must return InvalidHandle, got {rc}"
);
}
#[test]
fn device_mem_aggregate_cap_via_registry() {
use crate::device_mem::{
DeviceMemBudget, DeviceMemEntry, DeviceMemRegistry, MAX_DEVICE_ALLOC_BYTES,
MAX_TOTAL_DEVICE_BYTES,
};
let reg = DeviceMemRegistry::with_budget(Arc::new(DeviceMemBudget::new()));
let per = MAX_DEVICE_ALLOC_BYTES;
let count = (MAX_TOTAL_DEVICE_BYTES / per) as usize;
for _ in 0..count {
reg.insert(DeviceMemEntry {
owner: InstanceId(305),
size: per,
#[cfg(feature = "cuda")]
device_ptr: 0,
})
.expect("under cap");
}
assert_eq!(
reg.insert(DeviceMemEntry {
owner: InstanceId(305),
size: per,
#[cfg(feature = "cuda")]
device_ptr: 0,
})
.unwrap_err(),
AbiError::QuotaExceeded
);
}
#[test]
fn metrics_snapshot_zero_on_fresh_context() {
let ctx = WasiCudaContext::new(InstanceId(400));
let snap = ctx.metrics_snapshot();
assert_eq!(snap, InstanceMetricsSnapshot::default());
assert_eq!(snap.kernels_launched, 0);
assert_eq!(snap.bytes_pinned, 0);
assert_eq!(snap.back_pressure_rejections, 0);
assert_eq!(snap.yield_count, 0);
assert_eq!(snap.device_bytes_allocated, 0);
}
#[test]
fn metrics_snapshot_reflects_activity() {
let ctx = WasiCudaContext::new(InstanceId(401));
ctx.registry
.register(KernelEntry {
owner: InstanceId(401),
entry: "k".into(),
ptx_bytes_len: 2048,
#[cfg(feature = "cuda")]
module: None,
})
.expect("register");
ctx.device_mem()
.insert(crate::device_mem::DeviceMemEntry {
owner: InstanceId(401),
size: 65536,
#[cfg(feature = "cuda")]
device_ptr: 0,
})
.expect("insert");
ctx.record_kernel_launched();
ctx.record_kernel_launched();
ctx.record_back_pressure_rejection();
let snap = ctx.metrics_snapshot();
assert_eq!(snap.kernels_launched, 2);
assert_eq!(snap.bytes_pinned, 2048);
assert_eq!(snap.back_pressure_rejections, 1);
assert_eq!(snap.device_bytes_allocated, 65536);
assert_eq!(snap.yield_count, 0, "no scheduler folded in yet");
let sched = SchedulerContext::unbounded();
sched.yield_now();
sched.yield_now();
sched.yield_now();
let snap = ctx.metrics_snapshot_with_scheduler(&sched);
assert_eq!(snap.yield_count, 3);
assert_eq!(snap.kernels_launched, 2);
assert_eq!(snap.device_bytes_allocated, 65536);
}
}