use std::ops::Range;
use std::sync::Arc;
use wasmtime::{LinearMemory, MemoryCreator, MemoryType};
use crate::pool::UnifiedMemoryPool;
use crate::unified::{DeviceId, UnifiedBuffer, UnifiedError};
pub const DEFAULT_MAX_BYTES: usize = 256 * 1024 * 1024;
pub const HARD_MAX_LINEAR_MEMORY_BYTES: usize = 4 * 1024 * 1024 * 1024;
fn effective_max_bytes(maximum_bytes: Option<usize>) -> usize {
match maximum_bytes {
None => DEFAULT_MAX_BYTES,
Some(m) if m == HARD_MAX_LINEAR_MEMORY_BYTES => DEFAULT_MAX_BYTES,
Some(m) => m,
}
}
#[derive(Debug)]
pub struct TensorWasmLinearMemory {
buffer: UnifiedBuffer,
current_size: usize,
maximum_size: usize,
}
impl TensorWasmLinearMemory {
pub fn new(minimum_bytes: usize, maximum_bytes: Option<usize>) -> Result<Self, UnifiedError> {
Self::new_on(minimum_bytes, maximum_bytes, DeviceId::default())
}
pub fn new_on(
minimum_bytes: usize,
maximum_bytes: Option<usize>,
device_id: DeviceId,
) -> Result<Self, UnifiedError> {
let max = effective_max_bytes(maximum_bytes);
if max > HARD_MAX_LINEAR_MEMORY_BYTES {
return Err(UnifiedError::TooLarge {
requested: max as u64,
limit: HARD_MAX_LINEAR_MEMORY_BYTES as u64,
});
}
if minimum_bytes > HARD_MAX_LINEAR_MEMORY_BYTES {
return Err(UnifiedError::TooLarge {
requested: minimum_bytes as u64,
limit: HARD_MAX_LINEAR_MEMORY_BYTES as u64,
});
}
if minimum_bytes > max {
return Err(UnifiedError::Allocation(format!(
"minimum {minimum_bytes} > maximum {max}"
)));
}
let cap = max.max(1);
let buffer = UnifiedBuffer::new_with_visible_window_on(cap, minimum_bytes, device_id)?;
Ok(Self {
buffer,
current_size: minimum_bytes,
maximum_size: max,
})
}
pub fn new_on_with_tenant_context(
minimum_bytes: usize,
maximum_bytes: Option<usize>,
device_id: DeviceId,
tenant_ctx: Arc<tensor_wasm_tenant::TenantContext>,
) -> Result<Self, tensor_wasm_core::error::TensorWasmError> {
let max = effective_max_bytes(maximum_bytes);
if max > HARD_MAX_LINEAR_MEMORY_BYTES {
return Err(UnifiedError::TooLarge {
requested: max as u64,
limit: HARD_MAX_LINEAR_MEMORY_BYTES as u64,
}
.into());
}
if minimum_bytes > HARD_MAX_LINEAR_MEMORY_BYTES {
return Err(UnifiedError::TooLarge {
requested: minimum_bytes as u64,
limit: HARD_MAX_LINEAR_MEMORY_BYTES as u64,
}
.into());
}
if minimum_bytes > max {
return Err(UnifiedError::Allocation(format!(
"minimum {minimum_bytes} > maximum {max}"
))
.into());
}
let cap = max.max(1);
let buffer = UnifiedBuffer::new_with_visible_window_on_with_tenant_context(
cap,
minimum_bytes,
device_id,
tenant_ctx,
)?;
Ok(Self {
buffer,
current_size: minimum_bytes,
maximum_size: max,
})
}
pub fn current_size(&self) -> usize {
self.current_size
}
pub fn capacity(&self) -> usize {
self.maximum_size
}
pub fn is_uvm_backed(&self) -> bool {
self.buffer.is_uvm_backed()
}
#[cfg(test)]
pub(crate) fn as_slice(&self) -> &[u8] {
let size = self.current_size;
unsafe { std::slice::from_raw_parts(self.buffer.as_ptr(), size) }
}
}
unsafe impl LinearMemory for TensorWasmLinearMemory {
fn byte_size(&self) -> usize {
self.current_size()
}
fn byte_capacity(&self) -> usize {
self.maximum_size
}
fn grow_to(&mut self, new_size: usize) -> wasmtime::Result<()> {
if new_size > self.maximum_size {
return Err(wasmtime::Error::msg(format!(
"memory.grow requested {new_size} > maximum {}",
self.maximum_size
)));
}
if new_size < self.current_size {
return Err(wasmtime::Error::msg(format!(
"memory.grow cannot shrink ({new_size} < current {})",
self.current_size
)));
}
let old_size = self.current_size;
if new_size > old_size {
self.buffer.as_mut_slice()[old_size..new_size].fill(0);
}
self.current_size = new_size;
Ok(())
}
fn as_ptr(&self) -> *mut u8 {
self.buffer.as_ptr() as *mut u8
}
}
impl TensorWasmLinearMemory {
#[allow(dead_code)]
pub(crate) fn wasm_accessible(&self) -> Range<usize> {
let base = self.buffer.as_ptr() as usize;
base..(base + self.maximum_size)
}
}
struct PooledLinearMemory {
pool_keepalive: Arc<UnifiedMemoryPool>,
pool_offset: usize,
base_ptr: *mut u8,
size: usize,
current_size: usize,
max_size: usize,
}
impl Drop for PooledLinearMemory {
fn drop(&mut self) {
self.pool_keepalive.release(self.pool_offset, self.size);
}
}
impl std::fmt::Debug for PooledLinearMemory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PooledLinearMemory")
.field("base_ptr", &self.base_ptr)
.field("size", &self.size)
.field("current_size", &self.current_size)
.field("max_size", &self.max_size)
.finish()
}
}
unsafe impl Send for PooledLinearMemory {}
unsafe impl Sync for PooledLinearMemory {}
unsafe impl LinearMemory for PooledLinearMemory {
fn byte_size(&self) -> usize {
self.current_size
}
fn byte_capacity(&self) -> usize {
self.max_size
}
fn grow_to(&mut self, new_size: usize) -> wasmtime::Result<()> {
if new_size > self.max_size {
return Err(wasmtime::Error::msg(format!(
"memory.grow requested {new_size} > maximum {}",
self.max_size
)));
}
if new_size < self.current_size {
return Err(wasmtime::Error::msg(format!(
"memory.grow cannot shrink ({new_size} < current {})",
self.current_size
)));
}
let old_size = self.current_size;
if new_size > old_size {
unsafe {
std::ptr::write_bytes(self.base_ptr.add(old_size), 0u8, new_size - old_size);
}
}
self.current_size = new_size;
Ok(())
}
fn as_ptr(&self) -> *mut u8 {
self.base_ptr
}
}
impl PooledLinearMemory {
#[allow(dead_code)]
pub(crate) fn wasm_accessible(&self) -> Range<usize> {
let base = self.base_ptr as usize;
base..(base + self.max_size)
}
}
#[derive(Debug, Clone)]
pub struct MemoryCreatorConfig {
pub max_linear_memory_bytes: usize,
}
impl Default for MemoryCreatorConfig {
fn default() -> Self {
Self {
max_linear_memory_bytes: HARD_MAX_LINEAR_MEMORY_BYTES,
}
}
}
impl MemoryCreatorConfig {
pub fn effective_max_linear_memory_bytes(&self) -> usize {
self.max_linear_memory_bytes
.min(HARD_MAX_LINEAR_MEMORY_BYTES)
}
}
#[derive(Clone)]
pub struct TensorWasmMemoryCreator {
inner: Arc<MemoryCreatorState>,
}
struct MemoryCreatorState {
device_id: DeviceId,
pool: Option<Arc<UnifiedMemoryPool>>,
config: MemoryCreatorConfig,
tenant_ctx: Option<Arc<tensor_wasm_tenant::TenantContext>>,
}
impl Default for TensorWasmMemoryCreator {
fn default() -> Self {
Self::new(DeviceId::default())
}
}
impl TensorWasmMemoryCreator {
pub fn new(device_id: DeviceId) -> Self {
Self::with_config(device_id, MemoryCreatorConfig::default())
}
pub fn with_config(device_id: DeviceId, config: MemoryCreatorConfig) -> Self {
Self {
inner: Arc::new(MemoryCreatorState {
device_id,
pool: None,
config,
tenant_ctx: None,
}),
}
}
pub fn with_tenant_context(
device_id: DeviceId,
tenant_ctx: Arc<tensor_wasm_tenant::TenantContext>,
) -> Self {
Self {
inner: Arc::new(MemoryCreatorState {
device_id,
pool: None,
config: MemoryCreatorConfig::default(),
tenant_ctx: Some(tenant_ctx),
}),
}
}
pub fn with_pool_and_tenant_context(
device_id: DeviceId,
pool: Arc<UnifiedMemoryPool>,
tenant_ctx: Arc<tensor_wasm_tenant::TenantContext>,
) -> Self {
Self {
inner: Arc::new(MemoryCreatorState {
device_id,
pool: Some(pool),
config: MemoryCreatorConfig::default(),
tenant_ctx: Some(tenant_ctx),
}),
}
}
pub fn with_pool(device_id: DeviceId, pool: Arc<UnifiedMemoryPool>) -> Self {
Self::with_pool_and_config(device_id, pool, MemoryCreatorConfig::default())
}
pub fn with_pool_and_config(
device_id: DeviceId,
pool: Arc<UnifiedMemoryPool>,
config: MemoryCreatorConfig,
) -> Self {
Self {
inner: Arc::new(MemoryCreatorState {
device_id,
pool: Some(pool),
config,
tenant_ctx: None,
}),
}
}
pub fn device_id(&self) -> DeviceId {
self.inner.device_id
}
pub fn pool(&self) -> Option<&Arc<UnifiedMemoryPool>> {
self.inner.pool.as_ref()
}
pub fn config(&self) -> &MemoryCreatorConfig {
&self.inner.config
}
}
unsafe impl MemoryCreator for TensorWasmMemoryCreator {
fn new_memory(
&self,
_ty: MemoryType,
minimum: usize,
maximum: Option<usize>,
reserved_size_in_bytes: Option<usize>,
guard_size_in_bytes: usize,
) -> Result<Box<dyn LinearMemory>, String> {
let max = effective_max_bytes(maximum);
if max > HARD_MAX_LINEAR_MEMORY_BYTES {
return Err(format!(
"module-declared memory maximum {max} bytes exceeds hard cap {HARD_MAX_LINEAR_MEMORY_BYTES} bytes",
));
}
if minimum > HARD_MAX_LINEAR_MEMORY_BYTES {
return Err(format!(
"module-declared memory minimum {minimum} bytes exceeds hard cap {HARD_MAX_LINEAR_MEMORY_BYTES} bytes",
));
}
if minimum > max {
return Err(format!("minimum {minimum} > maximum {max}"));
}
if guard_size_in_bytes > 0 {
return Err(format!(
"TensorWasmMemoryCreator cannot honour guard_size_in_bytes = {guard_size_in_bytes}: \
managed-memory backings are incompatible with host page guards. \
Set `Config::memory_guard_size(0)` and \
`Config::guard_before_linear_memory(false)`, or use the PoolingMpk backend."
));
}
if let Some(reserved) = reserved_size_in_bytes {
if reserved > max {
return Err(format!(
"TensorWasmMemoryCreator cannot reserve {reserved} bytes: backing capacity is {max}"
));
}
}
if let Some(pool) = self.inner.pool.as_ref() {
const WASM_PAGE: usize = 65_536;
let carve_size = max.max(1);
match pool.allocate(carve_size, WASM_PAGE) {
Ok(mut alloc) => {
let pool_offset = alloc.offset();
let slice = alloc.as_mut_slice();
let base_ptr = slice.as_mut_ptr();
let size = slice.len();
std::mem::forget(alloc);
return Ok(Box::new(PooledLinearMemory {
pool_keepalive: Arc::clone(pool),
pool_offset,
base_ptr,
size,
current_size: minimum,
max_size: size,
}));
}
Err(e) => {
tracing::debug!(
error = %e,
requested = carve_size,
remaining = pool.remaining(),
"pool exhausted; falling back to fresh UnifiedBuffer"
);
let pool_device = pool.device_id();
if pool_device != self.inner.device_id {
tracing::warn!(
pool_device_id = %pool_device,
creator_device_id = %self.inner.device_id,
"fallback UnifiedBuffer will use creator's device, \
which differs from the exhausted pool's device"
);
}
}
}
}
let mem = if let Some(tenant_ctx) = self.inner.tenant_ctx.as_ref() {
TensorWasmLinearMemory::new_on_with_tenant_context(
minimum,
maximum,
self.inner.device_id,
Arc::clone(tenant_ctx),
)
.map_err(|e| e.to_string())?
} else {
TensorWasmLinearMemory::new_on(minimum, maximum, self.inner.device_id)
.map_err(|e| e.to_string())?
};
Ok(Box::new(mem))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn construct_and_query_size() {
let mem = TensorWasmLinearMemory::new(64 * 1024, Some(1024 * 1024)).unwrap();
assert_eq!(mem.byte_size(), 64 * 1024);
assert_eq!(mem.byte_capacity(), 1024 * 1024);
assert_eq!(mem.capacity(), 1024 * 1024);
}
#[test]
fn grow_increases_visible_size() {
let mut mem = TensorWasmLinearMemory::new(64 * 1024, Some(256 * 1024)).unwrap();
mem.grow_to(128 * 1024).expect("grow");
assert_eq!(mem.byte_size(), 128 * 1024);
}
#[test]
fn grow_beyond_maximum_rejected() {
let mut mem = TensorWasmLinearMemory::new(64 * 1024, Some(128 * 1024)).unwrap();
let err = mem.grow_to(256 * 1024).unwrap_err();
assert!(err.to_string().contains("maximum"));
}
#[test]
fn shrink_rejected() {
let mut mem = TensorWasmLinearMemory::new(128 * 1024, Some(1024 * 1024)).unwrap();
let err = mem.grow_to(64 * 1024).unwrap_err();
assert!(err.to_string().contains("shrink"));
}
#[test]
fn ptr_stable_across_grow() {
let mut mem = TensorWasmLinearMemory::new(64 * 1024, Some(1024 * 1024)).unwrap();
let p1 = mem.as_ptr();
mem.grow_to(256 * 1024).unwrap();
let p2 = mem.as_ptr();
assert_eq!(p1, p2, "as_ptr must be stable across grow_to");
}
#[test]
fn wasm_accessible_covers_capacity() {
let mem = TensorWasmLinearMemory::new(64 * 1024, Some(1024 * 1024)).unwrap();
let r = mem.wasm_accessible();
assert_eq!(r.end - r.start, 1024 * 1024);
}
#[test]
fn minimum_exceeds_maximum_rejected() {
let err = TensorWasmLinearMemory::new(1024 * 1024, Some(512 * 1024)).unwrap_err();
assert!(matches!(err, UnifiedError::Allocation(_)));
}
#[test]
fn maximum_above_hard_cap_rejected_without_allocating() {
let big = HARD_MAX_LINEAR_MEMORY_BYTES + 1;
let err = TensorWasmLinearMemory::new(64 * 1024, Some(big)).unwrap_err();
match err {
UnifiedError::TooLarge { requested, limit } => {
assert_eq!(requested, big as u64);
assert_eq!(limit, HARD_MAX_LINEAR_MEMORY_BYTES as u64);
}
other => panic!("expected TooLarge, got {other:?}"),
}
}
#[test]
fn minimum_above_hard_cap_rejected() {
let big = HARD_MAX_LINEAR_MEMORY_BYTES + 1;
let err = TensorWasmLinearMemory::new(big, Some(big + 1)).unwrap_err();
assert!(matches!(err, UnifiedError::TooLarge { .. }));
}
#[test]
fn maximum_exactly_at_hard_cap_admitted_but_not_allocated_in_test() {
let at_cap = HARD_MAX_LINEAR_MEMORY_BYTES;
let result = TensorWasmLinearMemory::new(0, Some(at_cap));
match result {
Ok(_) => { }
Err(UnifiedError::Allocation(_)) | Err(UnifiedError::Cuda(_)) => {
}
Err(other) => panic!(
"request at exactly the hard cap must not be rejected as TooLarge: got {other:?}"
),
}
}
#[test]
fn creator_rejects_module_max_above_hard_cap() {
use wasmtime::MemoryCreator;
let creator = TensorWasmMemoryCreator::default();
let mt = wasmtime::MemoryType::new(1, None);
let big = HARD_MAX_LINEAR_MEMORY_BYTES + 1;
let err = match creator.new_memory(mt, 64 * 1024, Some(big), None, 0) {
Ok(_) => panic!("oversized module max must be refused"),
Err(e) => e,
};
assert!(
err.contains("hard cap"),
"error must mention the hard cap; got: {err}"
);
}
#[test]
fn creator_default_device_zero() {
let c = TensorWasmMemoryCreator::default();
assert_eq!(c.device_id(), DeviceId(0));
}
#[test]
fn creator_default_pool_is_none() {
let c = TensorWasmMemoryCreator::default();
assert!(c.pool().is_none());
}
#[test]
fn creator_with_pool_round_trip() {
let pool = std::sync::Arc::new(crate::pool::UnifiedMemoryPool::new(64 * 1024).unwrap());
let creator = TensorWasmMemoryCreator::with_pool(DeviceId(2), pool.clone());
assert_eq!(creator.device_id(), DeviceId(2));
assert!(creator.pool().is_some());
assert!(std::sync::Arc::ptr_eq(creator.pool().unwrap(), &pool));
}
#[test]
fn as_slice_reflects_written_bytes_and_current_size() {
let mut mem = TensorWasmLinearMemory::new(64 * 1024, Some(256 * 1024)).unwrap();
mem.grow_to(128 * 1024).expect("grow");
unsafe {
let p = mem.as_ptr();
*p.add(0) = 0xDE;
*p.add(1) = 0xAD;
*p.add(64 * 1024) = 0xBE;
*p.add(128 * 1024 - 1) = 0xEF;
}
let s = mem.as_slice();
assert_eq!(
s.len(),
128 * 1024,
"slice tracks current_size, not capacity"
);
assert_eq!(s[0], 0xDE);
assert_eq!(s[1], 0xAD);
assert_eq!(s[64 * 1024], 0xBE);
assert_eq!(s[128 * 1024 - 1], 0xEF);
}
#[test]
fn is_uvm_backed_matches_feature_flag() {
let mem = TensorWasmLinearMemory::new(64 * 1024, Some(1024 * 1024)).unwrap();
#[cfg(feature = "unified-memory")]
assert!(
mem.is_uvm_backed(),
"with --features unified-memory the wasm linear memory MUST be UVM-backed"
);
#[cfg(all(not(feature = "unified-memory"), feature = "cudarc-backend"))]
assert!(
mem.is_uvm_backed(),
"with --features cudarc-backend the wasm linear memory MUST be UVM-backed"
);
#[cfg(all(not(feature = "unified-memory"), not(feature = "cudarc-backend")))]
assert!(
!mem.is_uvm_backed(),
"without any CUDA backing feature the linear memory must be the heap Box<[u8]>"
);
}
#[test]
fn as_ptr_returns_non_null_inside_backing_region() {
let mem = TensorWasmLinearMemory::new(64 * 1024, Some(1024 * 1024)).unwrap();
let p = LinearMemory::as_ptr(&mem);
assert!(!p.is_null(), "as_ptr must be non-null");
let r = mem.wasm_accessible();
let p_addr = p as usize;
assert!(
r.contains(&p_addr),
"as_ptr ({p_addr:#x}) must land inside wasm_accessible ({:#x}..{:#x})",
r.start,
r.end,
);
}
#[test]
fn grow_up_to_preallocated_cap_succeeds_beyond_fails() {
const MAX: usize = 256 * 1024;
let mut mem = TensorWasmLinearMemory::new(64 * 1024, Some(MAX)).unwrap();
mem.grow_to(MAX).expect("grow up to cap must succeed");
assert_eq!(mem.byte_size(), MAX);
let err = mem.grow_to(MAX + 1).expect_err("grow past cap must fail");
assert!(err.to_string().contains("maximum"));
}
#[test]
fn grow_to_zero_fills_newly_exposed_bytes() {
const MIN: usize = 64 * 1024;
const MAX: usize = 4 * 64 * 1024; const POISON_START: usize = MIN + 4096;
const POISON_END: usize = MIN + 8192;
const GROW_TO: usize = MAX;
let mut mem = TensorWasmLinearMemory::new(MIN, Some(MAX)).unwrap();
unsafe {
let p = LinearMemory::as_ptr(&mem);
for off in POISON_START..POISON_END {
*p.add(off) = 0xCD;
}
}
mem.grow_to(GROW_TO).expect("grow must succeed");
assert_eq!(mem.byte_size(), GROW_TO);
let s = mem.as_slice();
assert_eq!(s.len(), GROW_TO);
let leaked: Vec<usize> = s[MIN..GROW_TO]
.iter()
.enumerate()
.filter_map(|(i, &v)| if v != 0 { Some(MIN + i) } else { None })
.collect();
assert!(
leaked.is_empty(),
"grow_to leaked {} non-zero bytes in newly-exposed range \
[{MIN}, {GROW_TO}); first leak at offset {:?} (value would be 0xCD if poison)",
leaked.len(),
leaked.first(),
);
}
#[test]
fn pooled_grow_to_zero_fills_newly_exposed_bytes() {
use std::sync::Arc;
const MIN: usize = 64 * 1024;
const MAX: usize = 4 * 64 * 1024;
const POISON_START: usize = MIN + 4096;
const POISON_END: usize = MIN + 8192;
const GROW_TO: usize = MAX;
let pool = Arc::new(crate::pool::UnifiedMemoryPool::new(8 * 1024 * 1024).unwrap());
let creator = TensorWasmMemoryCreator::with_pool(DeviceId::default(), pool.clone());
let mt = wasmtime::MemoryType::new(1, Some(4));
use wasmtime::MemoryCreator;
let mut mem = creator
.new_memory(mt, MIN, Some(MAX), None, 0)
.expect("new_memory");
unsafe {
let p = mem.as_ptr();
for off in POISON_START..POISON_END {
*p.add(off) = 0xCD;
}
}
mem.grow_to(GROW_TO).expect("grow must succeed");
assert_eq!(mem.byte_size(), GROW_TO);
let s = unsafe { std::slice::from_raw_parts(mem.as_ptr() as *const u8, GROW_TO) };
let leaked: Vec<usize> = s[MIN..GROW_TO]
.iter()
.enumerate()
.filter_map(|(i, &v)| if v != 0 { Some(MIN + i) } else { None })
.collect();
assert!(
leaked.is_empty(),
"pooled grow_to leaked {} non-zero bytes in [{MIN}, {GROW_TO}); first at {:?}",
leaked.len(),
leaked.first(),
);
}
#[test]
fn creator_with_pool_carves_from_slab() {
use std::sync::Arc;
let pool = Arc::new(crate::pool::UnifiedMemoryPool::new(8 * 1024 * 1024).unwrap());
let creator = TensorWasmMemoryCreator::with_pool(DeviceId::default(), pool.clone());
let mt = wasmtime::MemoryType::new(1, Some(2));
use wasmtime::MemoryCreator;
let mem = creator
.new_memory(mt, 64 * 1024, Some(128 * 1024), None, 0)
.expect("new_memory");
assert!(mem.byte_size() == 64 * 1024);
assert!(mem.byte_capacity() == 128 * 1024);
assert_eq!(pool.live_allocations(), 1);
drop(mem);
assert_eq!(pool.live_allocations(), 0);
}
}