use std::sync::Arc;
use tensor_wasm_core::types::TenantId;
use tensor_wasm_jit::cache::{CacheKey, KernelCache};
use wasmtime::{Caller, Linker};
use crate::instance::InstanceState;
pub trait TenantContext {
fn tenant_id(&self) -> TenantId;
}
impl TenantContext for InstanceState {
fn tenant_id(&self) -> TenantId {
self.tenant_id
}
}
impl TenantContext for () {
fn tenant_id(&self) -> TenantId {
TenantId(0)
}
}
pub const DEFAULT_DISPATCH_SM_VERSION: u32 = 80;
pub const DISPATCH_OK: i32 = 0;
pub const DISPATCH_CACHE_MISS: i32 = -1;
pub const DISPATCH_BAD_SCRATCH: i32 = -2;
pub const SCRATCH_ARENA_BYTES: u32 = 64 * 1024;
#[derive(Debug, Default)]
pub struct ArenaState {
pub(crate) bump_cursor: Option<u32>,
pub(crate) arena_floor: u32,
pub(crate) live: Vec<(u32, u32)>,
}
impl ArenaState {
pub fn bump_cursor(&self) -> Option<u32> {
self.bump_cursor
}
pub fn live_count(&self) -> usize {
self.live.len()
}
}
pub trait JitArenaProvider {
fn jit_arena_mut(&mut self) -> &mut ArenaState;
}
pub fn add_jit_dispatch_to_linker<T>(
linker: &mut Linker<T>,
cache: Arc<KernelCache>,
) -> wasmtime::Result<()>
where
T: JitArenaProvider + TenantContext + 'static,
{
add_jit_dispatch_to_linker_with(
linker,
cache,
"tensor-wasm:jit/host",
"dispatch",
"alloc",
"free",
DEFAULT_DISPATCH_SM_VERSION,
)
}
#[allow(clippy::too_many_arguments)]
pub fn add_jit_dispatch_to_linker_with<T>(
linker: &mut Linker<T>,
cache: Arc<KernelCache>,
host_module: &str,
dispatch_fn: &str,
alloc_fn: &str,
free_fn: &str,
sm_version: u32,
) -> wasmtime::Result<()>
where
T: JitArenaProvider + TenantContext + 'static,
{
linker.func_wrap(
host_module,
alloc_fn,
move |mut caller: Caller<'_, T>, size: i32| -> i32 {
if size <= 0 {
return -1;
}
let size_u = size as u32;
let memory = match caller.get_export("memory").and_then(|e| e.into_memory()) {
Some(m) => m,
None => {
tracing::warn!(
target: "tensor_wasm_exec::jit_dispatch",
"alloc: caller has no exported `memory`"
);
return -1;
}
};
let cursor_opt = caller.data_mut().jit_arena_mut().bump_cursor;
let mem_len = memory.data(&caller).len() as u64;
let cursor = match cursor_opt {
Some(c) => c,
None => {
if mem_len < SCRATCH_ARENA_BYTES as u64 {
let pages_needed = (SCRATCH_ARENA_BYTES as u64)
.div_ceil(65536)
.saturating_sub(mem_len / 65536);
if pages_needed > 0 && memory.grow(&mut caller, pages_needed).is_err() {
return -1;
}
}
let new_len = memory.data(&caller).len() as u64;
let top = u32::try_from(new_len).unwrap_or(u32::MAX);
let floor = top.saturating_sub(SCRATCH_ARENA_BYTES);
let arena = caller.data_mut().jit_arena_mut();
arena.arena_floor = floor;
arena.bump_cursor = Some(top);
top
}
};
let aligned_size = (size_u + 7) & !7;
let Some(ptr) = cursor.checked_sub(aligned_size) else {
tracing::warn!(
target: "tensor_wasm_exec::jit_dispatch",
requested = size,
"alloc: scratch arena exhausted (cursor underflow)"
);
return -1;
};
let st = caller.data_mut().jit_arena_mut();
if ptr < st.arena_floor {
tracing::warn!(
target: "tensor_wasm_exec::jit_dispatch",
requested = size,
arena_floor = st.arena_floor,
cursor = cursor,
"alloc: scratch arena exhausted (would collide with guest data)"
);
return -1;
}
st.bump_cursor = Some(ptr);
st.live.push((ptr, aligned_size));
ptr as i32
},
)?;
linker.func_wrap(
host_module,
free_fn,
move |mut caller: Caller<'_, T>, ptr: i32, size: i32| {
if ptr <= 0 || size <= 0 {
return;
}
let st = caller.data_mut().jit_arena_mut();
match st.live.last().copied() {
Some((top_ptr, top_size)) if top_ptr == ptr as u32 => {
st.live.pop();
st.bump_cursor = Some(top_ptr + top_size);
}
_ => {
tracing::warn!(
target: "tensor_wasm_exec::jit_dispatch",
ptr = ptr,
size = size,
"free: out-of-order free; slot leaked until arena reset"
);
}
}
},
)?;
let cache_disp = cache;
linker.func_wrap(
host_module,
dispatch_fn,
move |mut caller: Caller<'_, T>,
fingerprint_lo: i64,
fingerprint_hi: i64,
scratch_ptr: i32,
args_len: i32,
results_len: i32|
-> i32 {
let lo = (fingerprint_lo as u64) & 0xFFFF_FFFF;
let hi = (fingerprint_hi as u64) & 0xFFFF_FFFF;
let fp = lo | (hi << 32);
let tenant_id = caller.data().tenant_id();
let key = CacheKey::for_tenant(tenant_id, fp, sm_version);
let cached = match cache_disp.get(&key) {
Some(k) => k,
None => {
tracing::warn!(
target: "tensor_wasm_exec::jit_dispatch",
fingerprint = fp,
tenant = %tenant_id,
"JIT dispatch cache miss"
);
return DISPATCH_CACHE_MISS;
}
};
tracing::trace!(
target: "tensor_wasm_exec::jit_dispatch",
fingerprint = fp,
tenant = %tenant_id,
args_len, results_len,
"JIT dispatch cache hit"
);
if scratch_ptr < 0 || args_len < 0 || results_len < 0 {
return DISPATCH_BAD_SCRATCH;
}
let memory = match caller.get_export("memory").and_then(|e| e.into_memory()) {
Some(m) => m,
None if args_len == 0 && results_len == 0 => {
return DISPATCH_OK;
}
None => {
tracing::warn!(
target: "tensor_wasm_exec::jit_dispatch",
"dispatch: caller has no exported memory but args/results > 0"
);
return DISPATCH_BAD_SCRATCH;
}
};
let mem = memory.data_mut(&mut caller);
let scratch = scratch_ptr as usize;
let alen = args_len as usize;
let rlen = results_len as usize;
let end = match scratch.checked_add(alen).and_then(|x| x.checked_add(rlen)) {
Some(e) => e,
None => return DISPATCH_BAD_SCRATCH,
};
if end > mem.len() {
tracing::warn!(
target: "tensor_wasm_exec::jit_dispatch",
scratch_ptr, args_len, results_len, mem_len = mem.len(),
"dispatch: scratch region exceeds linear memory"
);
return DISPATCH_BAD_SCRATCH;
}
let _ = &cached;
#[cfg(feature = "cuda")]
{
let _ = (&mut *mem, scratch, alen, rlen, &cached);
DISPATCH_OK
}
#[cfg(not(feature = "cuda"))]
{
let _ = (mem, scratch, alen, rlen);
tracing::warn!(
target: "tensor_wasm_exec::jit_dispatch",
fingerprint = fp,
tenant = %tenant_id,
"JIT dispatch cache hit on a no-CUDA build: no kernel to run, \
signalling deopt (cache-miss code) instead of echoing input"
);
DISPATCH_CACHE_MISS
}
},
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use tensor_wasm_jit::cache::{CachedKernel, CompiledHandle, KernelCache};
use tensor_wasm_jit::ptx_emit::EmittedPtx;
use wasmtime::{Config, Engine, Module, Store};
#[derive(Default)]
struct TestState {
arena: ArenaState,
}
impl JitArenaProvider for TestState {
fn jit_arena_mut(&mut self) -> &mut ArenaState {
&mut self.arena
}
}
impl TenantContext for TestState {
fn tenant_id(&self) -> TenantId {
TenantId(0)
}
}
fn driver_wat() -> &'static str {
r#"
(module
(import "tensor-wasm:jit/host" "dispatch"
(func $dispatch (param i64 i64 i32 i32 i32) (result i32)))
(import "tensor-wasm:jit/host" "alloc"
(func $alloc (param i32) (result i32)))
(import "tensor-wasm:jit/host" "free"
(func $free (param i32 i32)))
(memory (export "memory") 1)
(func (export "call_dispatch") (param $lo i64) (param $hi i64) (result i32)
(call $dispatch
(local.get $lo)
(local.get $hi)
(i32.const 0)
(i32.const 0)
(i32.const 0)))
)
"#
}
fn make_engine() -> Engine {
let mut cfg = Config::new();
cfg.wasm_simd(true);
Engine::new(&cfg).expect("engine")
}
fn make_cache_with(fp: u64, sm_version: u32) -> Arc<KernelCache> {
let cache = Arc::new(KernelCache::new());
cache.put(
CacheKey::for_tenant(TenantId(0), fp, sm_version),
CachedKernel::new(
fp,
Arc::new(EmittedPtx {
text: "// stub".into(),
launch_geometry: (1, 1),
}),
CompiledHandle::default(),
),
);
cache
}
#[cfg(feature = "cuda")]
const HIT_CODE: i32 = DISPATCH_OK;
#[cfg(not(feature = "cuda"))]
const HIT_CODE: i32 = DISPATCH_CACHE_MISS;
#[test]
fn cache_hit_returns_expected_code() {
let engine = make_engine();
let fp: u64 = 0xDEAD_BEEF_CAFE_BABE;
let cache = make_cache_with(fp, DEFAULT_DISPATCH_SM_VERSION);
let mut linker: Linker<TestState> = Linker::new(&engine);
add_jit_dispatch_to_linker(&mut linker, cache).expect("register dispatch");
let mut store = Store::new(&engine, TestState::default());
let wasm = wat::parse_str(driver_wat()).expect("wat");
let module = Module::new(&engine, &wasm).expect("module");
let instance = linker
.instantiate(&mut store, &module)
.expect("instantiate");
let call = instance
.get_typed_func::<(i64, i64), i32>(&mut store, "call_dispatch")
.expect("typed func");
let lo = (fp & 0xFFFF_FFFF) as i64;
let hi = (fp >> 32) as i64;
let ret = call.call(&mut store, (lo, hi)).expect("call");
assert_eq!(ret, HIT_CODE);
}
#[test]
fn cache_miss_returns_minus_one() {
let engine = make_engine();
let cache = Arc::new(KernelCache::new());
let mut linker: Linker<TestState> = Linker::new(&engine);
add_jit_dispatch_to_linker(&mut linker, cache).expect("register dispatch");
let mut store = Store::new(&engine, TestState::default());
let wasm = wat::parse_str(driver_wat()).expect("wat");
let module = Module::new(&engine, &wasm).expect("module");
let instance = linker
.instantiate(&mut store, &module)
.expect("instantiate");
let call = instance
.get_typed_func::<(i64, i64), i32>(&mut store, "call_dispatch")
.expect("typed func");
let ret = call.call(&mut store, (0, 0)).expect("call");
assert_eq!(ret, DISPATCH_CACHE_MISS);
}
#[test]
fn custom_module_and_fn_name_round_trip() {
let engine = make_engine();
let fp: u64 = 42;
let cache = make_cache_with(fp, 89);
let mut linker: Linker<TestState> = Linker::new(&engine);
add_jit_dispatch_to_linker_with(
&mut linker,
cache,
"custom:host",
"go",
"give",
"take",
89,
)
.expect("register custom");
let wat = r#"
(module
(import "custom:host" "go"
(func $g (param i64 i64 i32 i32 i32) (result i32)))
(import "custom:host" "give"
(func $a (param i32) (result i32)))
(import "custom:host" "take"
(func $f (param i32 i32)))
(memory (export "memory") 1)
(func (export "drive") (param i64 i64) (result i32)
(call $g (local.get 0) (local.get 1)
(i32.const 0) (i32.const 0) (i32.const 0)))
)
"#;
let mut store = Store::new(&engine, TestState::default());
let wasm = wat::parse_str(wat).expect("wat");
let module = Module::new(&engine, &wasm).expect("module");
let instance = linker
.instantiate(&mut store, &module)
.expect("instantiate");
let call = instance
.get_typed_func::<(i64, i64), i32>(&mut store, "drive")
.expect("typed func");
let lo = (fp & 0xFFFF_FFFF) as i64;
let hi = (fp >> 32) as i64;
let ret = call.call(&mut store, (lo, hi)).expect("call");
assert_eq!(ret, HIT_CODE);
}
#[test]
fn fingerprint_with_high_bit_round_trips() {
let engine = make_engine();
let fp: u64 = 0xFFFF_FFFF_FFFF_FFFF;
let cache = make_cache_with(fp, DEFAULT_DISPATCH_SM_VERSION);
let mut linker: Linker<TestState> = Linker::new(&engine);
add_jit_dispatch_to_linker(&mut linker, cache).expect("register");
let mut store = Store::new(&engine, TestState::default());
let wasm = wat::parse_str(driver_wat()).expect("wat");
let module = Module::new(&engine, &wasm).expect("module");
let instance = linker
.instantiate(&mut store, &module)
.expect("instantiate");
let call = instance
.get_typed_func::<(i64, i64), i32>(&mut store, "call_dispatch")
.expect("typed func");
let lo = (fp & 0xFFFF_FFFF) as i64;
let hi = (fp >> 32) as i64;
let ret = call.call(&mut store, (lo, hi)).expect("call");
assert_eq!(ret, HIT_CODE);
}
#[test]
fn end_to_end_add_returns_sum() {
use tensor_wasm_jit::cache::{CacheKey, CachedKernel, CompiledHandle, KernelCache};
use tensor_wasm_jit::detector::DetectorConfig;
use tensor_wasm_jit::ptx_emit::EmittedPtx;
use tensor_wasm_jit::rewrite::{rewrite_wasm, RewriteOptions};
let hot_add = r#"
(module
(memory (export "memory") 1)
(func (export "add") (param $a i32) (param $b i32) (result i32)
(local $v v128)
(loop $L
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
(local.set $v (i32x4.add (local.get $v) (local.get $v)))
)
(i32.add (local.get $a) (local.get $b))
)
)
"#;
let wasm = wat::parse_str(hot_add).expect("wat");
let cache = Arc::new(KernelCache::new());
let opts = RewriteOptions {
detector: DetectorConfig {
v128_ratio_threshold: 0.05,
min_trip_count: 64,
},
..RewriteOptions::default()
};
let outcome = rewrite_wasm(&wasm, &opts, &cache).expect("rewrite");
assert_eq!(
outcome.offloaded_functions.len(),
1,
"the hot add function must be swapped"
);
let fp = outcome.offloaded_functions[0].fingerprint;
cache.put(
CacheKey::for_tenant(TenantId(0), fp, DEFAULT_DISPATCH_SM_VERSION),
CachedKernel::new(
fp,
Arc::new(EmittedPtx {
text: "// stub for e2e".into(),
launch_geometry: (1, 1),
}),
CompiledHandle::default(),
),
);
let engine = make_engine();
let mut linker: Linker<TestState> = Linker::new(&engine);
linker
.func_wrap(
"tensor-wasm:jit/host",
"alloc",
move |mut caller: Caller<'_, TestState>, size: i32| -> i32 {
if size <= 0 {
return -1;
}
let memory = caller
.get_export("memory")
.and_then(|e| e.into_memory())
.expect("memory exported");
let cursor_opt = caller.data_mut().jit_arena_mut().bump_cursor;
let mem_len = memory.data(&caller).len() as u64;
let cursor = match cursor_opt {
Some(c) => c,
None => {
if mem_len < SCRATCH_ARENA_BYTES as u64 {
let pages = (SCRATCH_ARENA_BYTES as u64)
.div_ceil(65536)
.saturating_sub(mem_len / 65536);
if pages > 0 {
memory.grow(&mut caller, pages).expect("grow");
}
}
let new_len = memory.data(&caller).len() as u64;
let top = u32::try_from(new_len).unwrap_or(u32::MAX);
caller.data_mut().jit_arena_mut().bump_cursor = Some(top);
top
}
};
let aligned = (size as u32 + 7) & !7;
let ptr = cursor.checked_sub(aligned).expect("arena room");
let st = caller.data_mut().jit_arena_mut();
st.bump_cursor = Some(ptr);
st.live.push((ptr, aligned));
ptr as i32
},
)
.expect("alloc");
linker
.func_wrap(
"tensor-wasm:jit/host",
"free",
move |mut caller: Caller<'_, TestState>, ptr: i32, _size: i32| {
let st = caller.data_mut().jit_arena_mut();
if let Some((top_ptr, top_size)) = st.live.last().copied() {
if top_ptr == ptr as u32 {
st.live.pop();
st.bump_cursor = Some(top_ptr + top_size);
}
}
},
)
.expect("free");
linker
.func_wrap(
"tensor-wasm:jit/host",
"dispatch",
|mut caller: Caller<'_, TestState>,
_fp_lo: i64,
_fp_hi: i64,
scratch_ptr: i32,
args_len: i32,
_results_len: i32|
-> i32 {
let memory = caller
.get_export("memory")
.and_then(|e| e.into_memory())
.expect("memory");
let mem = memory.data_mut(&mut caller);
let sp = scratch_ptr as usize;
let a = i32::from_le_bytes([mem[sp], mem[sp + 1], mem[sp + 2], mem[sp + 3]]);
let b =
i32::from_le_bytes([mem[sp + 4], mem[sp + 5], mem[sp + 6], mem[sp + 7]]);
let sum = a.wrapping_add(b).to_le_bytes();
let r = sp + args_len as usize;
mem[r..r + 4].copy_from_slice(&sum);
DISPATCH_OK
},
)
.expect("dispatch");
let mut store = Store::new(&engine, TestState::default());
let module = Module::new(&engine, &outcome.rewritten_wasm).expect("module");
let instance = linker
.instantiate(&mut store, &module)
.expect("instantiate");
let add = instance
.get_typed_func::<(i32, i32), i32>(&mut store, "add")
.expect("typed");
let r = add.call(&mut store, (2, 3)).expect("call add");
assert_eq!(
r, 5,
"end-to-end add(2,3) must marshall args, dispatch, and load result"
);
}
#[test]
fn alloc_does_not_overwrite_guest_static_data() {
let engine = make_engine();
let cache = Arc::new(KernelCache::new());
let mut linker: Linker<TestState> = Linker::new(&engine);
add_jit_dispatch_to_linker(&mut linker, cache).expect("register");
let wat = r#"
(module
(import "tensor-wasm:jit/host" "alloc"
(func $a (param i32) (result i32)))
(memory (export "memory") 2)
(data (i32.const 1024) "\AB")
(func (export "alloc_one") (param i32) (result i32)
(call $a (local.get 0)))
(func (export "sentinel") (result i32)
(i32.load8_u (i32.const 1024))))
"#;
let mut store = Store::new(&engine, TestState::default());
let wasm = wat::parse_str(wat).expect("wat");
let module = Module::new(&engine, &wasm).expect("module");
let instance = linker
.instantiate(&mut store, &module)
.expect("instantiate");
let alloc_one = instance
.get_typed_func::<i32, i32>(&mut store, "alloc_one")
.expect("typed func alloc_one");
let sentinel = instance
.get_typed_func::<(), i32>(&mut store, "sentinel")
.expect("typed func sentinel");
let p = alloc_one.call(&mut store, 64).expect("alloc 64");
assert!(p > 0, "alloc must succeed, got {p}");
assert!(
(p as u32) >= 65536,
"ptr {p} must land in the upper page (>= 64 KiB), above guest static data",
);
let s = sentinel.call(&mut store, ()).expect("sentinel load");
assert_eq!(
s, 0xAB,
"guest static-data byte must survive JIT scratch alloc"
);
let too_big = alloc_one
.call(&mut store, SCRATCH_ARENA_BYTES as i32)
.expect("alloc oversize");
assert_eq!(
too_big, -1,
"alloc that would overflow into guest data must return -1"
);
let s2 = sentinel.call(&mut store, ()).expect("sentinel load 2");
assert_eq!(s2, 0xAB, "guest static data must survive a refused alloc");
}
#[test]
fn alloc_free_round_trip() {
let engine = make_engine();
let cache = Arc::new(KernelCache::new());
let mut linker: Linker<TestState> = Linker::new(&engine);
add_jit_dispatch_to_linker(&mut linker, cache).expect("register");
let wat = r#"
(module
(import "tensor-wasm:jit/host" "dispatch"
(func $d (param i64 i64 i32 i32 i32) (result i32)))
(import "tensor-wasm:jit/host" "alloc"
(func $a (param i32) (result i32)))
(import "tensor-wasm:jit/host" "free"
(func $f (param i32 i32)))
(memory (export "memory") 1)
(func (export "two_alloc") (result i32 i32)
(call $a (i32.const 32))
(call $a (i32.const 32))))
"#;
let mut store = Store::new(&engine, TestState::default());
let wasm = wat::parse_str(wat).expect("wat");
let module = Module::new(&engine, &wasm).expect("module");
let instance = linker
.instantiate(&mut store, &module)
.expect("instantiate");
let call = instance
.get_typed_func::<(), (i32, i32)>(&mut store, "two_alloc")
.expect("typed func");
let (p1, p2) = call.call(&mut store, ()).expect("call");
assert!(p1 > 0, "first alloc must succeed, got {p1}");
assert!(p2 > 0, "second alloc must succeed, got {p2}");
assert_ne!(p1, p2, "successive allocs must hand out distinct pointers");
}
}