use std::fmt;
use std::ptr::NonNull;
use std::sync::Arc;
use tensor_wasm_tenant::TenantContext;
#[derive(Debug, thiserror::Error)]
pub enum UnifiedError {
#[error("allocation failed: {0}")]
Allocation(String),
#[error("CUDA call failed: {0}")]
Cuda(String),
#[error("cannot allocate a zero-byte buffer")]
ZeroSize,
#[error("requested {requested} bytes exceeds hard cap {limit}")]
TooLarge {
requested: u64,
limit: u64,
},
#[error("{feature:?} not supported by backing {backing:?}")]
NotSupported {
feature: &'static str,
backing: &'static str,
},
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UvmAdvice {
SetReadMostly,
UnsetReadMostly,
SetPreferredLocation(u32),
UnsetPreferredLocation,
SetAccessedBy(u32),
UnsetAccessedBy(u32),
}
pub trait UnifiedBacking: Send + Sync {
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn as_slice(&self) -> &[u8];
fn as_mut_slice(&mut self) -> &mut [u8];
fn apply_advice(&self, hint: UvmAdvice) -> Result<(), UnifiedError>;
fn prefetch_to_device(&self, device_ord: u32) -> Result<(), UnifiedError>;
fn prefetch_to_host(&self) -> Result<(), UnifiedError>;
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DeviceId(pub u32);
impl fmt::Display for DeviceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "cuda:{}", self.0)
}
}
pub struct UnifiedBuffer {
ptr: NonNull<u8>,
size: usize,
device_id: DeviceId,
#[allow(dead_code)]
backing: Backing,
host_addressable: bool,
host_zeroize_on_drop: bool,
host_capacity: usize,
tenant_ctx: Option<Arc<TenantContext>>,
}
unsafe impl Send for UnifiedBuffer {}
unsafe impl Sync for UnifiedBuffer {}
#[cfg(feature = "unified-memory")]
mod backing {
use super::*;
pub(super) const IS_UVM_BACKED: bool = true;
pub(super) const IS_HOST_BACKED: bool = false;
#[deny(missing_docs)]
#[allow(dead_code)]
pub(super) enum Backing {
Cuda(cust::memory::UnifiedBuffer<u8>),
#[cfg(feature = "gpu-mem-pool")]
TenantPool(super::TenantPoolBacking),
}
fn ensure_cuda_init() -> Result<(), UnifiedError> {
use std::sync::OnceLock;
static CTX: OnceLock<Result<cust::context::Context, String>> = OnceLock::new();
let r =
CTX.get_or_init(|| cust::quick_init().map_err(|e| format!("cust::quick_init: {e:?}")));
match r {
Ok(_) => Ok(()),
Err(msg) => Err(UnifiedError::Cuda(msg.clone())),
}
}
impl Backing {
pub(super) fn allocate(
size: usize,
init_zero_bytes: usize,
) -> Result<(NonNull<u8>, Self), UnifiedError> {
ensure_cuda_init()?;
let mut buf = unsafe { cust::memory::UnifiedBuffer::<u8>::uninitialized(size) }
.map_err(|e| UnifiedError::Cuda(format!("{e:?}")))?;
let init = init_zero_bytes.min(size);
if init > 0 {
buf.as_mut_slice()[..init].fill(0);
}
let ptr = NonNull::new(buf.as_unified_ptr().as_raw_mut() as *mut u8)
.ok_or_else(|| UnifiedError::Allocation("cust returned null".into()))?;
Ok((ptr, Backing::Cuda(buf)))
}
}
}
#[cfg(all(not(feature = "unified-memory"), feature = "cudarc-backend"))]
mod backing {
use super::*;
use crate::cudarc_backend::CudarcUnifiedBuffer;
pub(super) const IS_UVM_BACKED: bool = true;
pub(super) const IS_HOST_BACKED: bool = false;
#[deny(missing_docs)]
#[allow(dead_code)]
pub(super) enum Backing {
Cudarc(CudarcUnifiedBuffer),
#[cfg(feature = "gpu-mem-pool")]
TenantPool(super::TenantPoolBacking),
}
impl Backing {
pub(super) fn allocate(
size: usize,
init_zero_bytes: usize,
) -> Result<(NonNull<u8>, Self), UnifiedError> {
let mut buf = CudarcUnifiedBuffer::new(size)?;
let init = init_zero_bytes.min(size);
if init > 0 {
buf.as_mut_slice()[..init].fill(0);
}
let ptr = NonNull::new(buf.as_ptr() as *mut u8)
.ok_or_else(|| UnifiedError::Allocation("cudarc returned null".into()))?;
Ok((ptr, Backing::Cudarc(buf)))
}
}
}
#[cfg(all(not(feature = "unified-memory"), not(feature = "cudarc-backend")))]
mod backing {
use super::*;
pub(super) const IS_UVM_BACKED: bool = false;
pub(super) const IS_HOST_BACKED: bool = true;
#[deny(missing_docs)]
#[allow(dead_code)]
pub(super) enum Backing {
Host(Box<[u8]>),
}
impl Backing {
pub(super) fn allocate(
size: usize,
_init_zero_bytes: usize,
) -> Result<(NonNull<u8>, Self), UnifiedError> {
let mut boxed: Box<[u8]> = vec![0u8; size].into_boxed_slice();
let ptr = NonNull::new(boxed.as_mut_ptr())
.ok_or_else(|| UnifiedError::Allocation("Box returned null".into()))?;
Ok((ptr, Backing::Host(boxed)))
}
}
}
use backing::{Backing, IS_HOST_BACKED, IS_UVM_BACKED};
#[cfg(feature = "gpu-mem-pool")]
pub(crate) struct TenantPoolBacking {
ptr: NonNull<u8>,
size: usize,
pool: Arc<crate::cuda_mem_pool::TenantMemPool>,
}
#[cfg(feature = "gpu-mem-pool")]
impl fmt::Debug for TenantPoolBacking {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TenantPoolBacking")
.field("ptr", &"<redacted>")
.field("pool_cap_bytes", &self.pool.cap_bytes())
.finish()
}
}
#[cfg(feature = "gpu-mem-pool")]
impl Drop for TenantPoolBacking {
fn drop(&mut self) {
if let Err(e) = self.pool.deallocate(self.ptr) {
tracing::error!(
target: "tensor_wasm_mem::cuda_mem_pool",
error = ?e,
pool_cap_bytes = self.pool.cap_bytes(),
"cuMemFreeAsync failed in TenantPoolBacking::drop -- \
allocation leaked, bounded by pool cap",
);
}
self.pool.release_bytes(self.size);
}
}
#[cfg(feature = "gpu-mem-pool")]
unsafe impl Send for TenantPoolBacking {}
#[cfg(feature = "gpu-mem-pool")]
unsafe impl Sync for TenantPoolBacking {}
impl UnifiedBuffer {
pub fn new(size: usize) -> Result<Self, UnifiedError> {
Self::new_on(size, DeviceId::default())
}
pub fn new_on(size: usize, device_id: DeviceId) -> Result<Self, UnifiedError> {
Self::new_with_visible_window_on(size, size, device_id)
}
pub fn new_with_visible_window_on(
size: usize,
visible_bytes: usize,
device_id: DeviceId,
) -> Result<Self, UnifiedError> {
if size == 0 {
return Err(UnifiedError::ZeroSize);
}
let (ptr, backing) = Backing::allocate(size, visible_bytes)?;
Ok(Self {
ptr,
size,
device_id,
backing,
host_addressable: true,
host_zeroize_on_drop: IS_HOST_BACKED,
host_capacity: size,
tenant_ctx: None,
})
}
pub fn new_host_with_capacity_on(
size: usize,
capacity: usize,
device_id: DeviceId,
) -> Result<Self, UnifiedError> {
if size == 0 {
return Err(UnifiedError::ZeroSize);
}
let capacity = capacity.max(size);
let (ptr, backing) = Backing::allocate(capacity, capacity)?;
Ok(Self {
ptr,
size,
device_id,
backing,
host_addressable: true,
host_zeroize_on_drop: IS_HOST_BACKED,
host_capacity: capacity,
tenant_ctx: None,
})
}
pub fn new_on_with_tenant_context(
size: usize,
device_id: DeviceId,
tenant_ctx: Arc<TenantContext>,
) -> Result<Self, tensor_wasm_core::error::TensorWasmError> {
Self::new_with_visible_window_on_with_tenant_context(size, size, device_id, tenant_ctx)
}
pub fn new_with_visible_window_on_with_tenant_context(
size: usize,
visible_bytes: usize,
device_id: DeviceId,
tenant_ctx: Arc<TenantContext>,
) -> Result<Self, tensor_wasm_core::error::TensorWasmError> {
if size == 0 {
return Err(UnifiedError::ZeroSize.into());
}
tenant_ctx.consume_gpu_bytes(size as u64)?;
match Backing::allocate(size, visible_bytes) {
Ok((ptr, backing)) => Ok(Self {
ptr,
size,
device_id,
backing,
host_addressable: true,
host_zeroize_on_drop: IS_HOST_BACKED,
host_capacity: size,
tenant_ctx: Some(tenant_ctx),
}),
Err(e) => {
tenant_ctx.release_gpu_bytes(size as u64);
Err(e.into())
}
}
}
#[cfg(feature = "gpu-mem-pool")]
pub fn new_in_tenant_pool(
pool: Arc<crate::cuda_mem_pool::TenantMemPool>,
size: usize,
device_id: DeviceId,
) -> Result<Self, UnifiedError> {
if size == 0 {
return Err(UnifiedError::ZeroSize);
}
let ptr = pool.allocate(size)?;
let tp = TenantPoolBacking {
ptr,
size,
pool: Arc::clone(&pool),
};
Ok(Self {
ptr,
size,
device_id,
backing: Backing::TenantPool(tp),
host_addressable: false,
host_zeroize_on_drop: false,
host_capacity: size,
tenant_ctx: None,
})
}
pub fn len(&self) -> usize {
self.size
}
pub fn capacity(&self) -> usize {
self.host_capacity
}
pub fn is_empty(&self) -> bool {
self.size == 0
}
pub fn as_ptr(&self) -> *const u8 {
self.ptr.as_ptr() as *const u8
}
pub fn as_mut_ptr(&mut self) -> *mut u8 {
self.ptr.as_ptr()
}
pub fn as_slice(&self) -> &[u8] {
assert!(
self.host_addressable,
"UnifiedBuffer::as_slice on a device-only buffer (audit H4): \
this allocation came from new_in_tenant_pool \
(cuMemAllocFromPoolAsync) and is NOT host-dereferenceable; \
copy it to host memory via a stream instead"
);
unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.size) }
}
pub fn as_mut_slice(&mut self) -> &mut [u8] {
assert!(
self.host_addressable,
"UnifiedBuffer::as_mut_slice on a device-only buffer (audit H4): \
this allocation came from new_in_tenant_pool \
(cuMemAllocFromPoolAsync) and is NOT host-dereferenceable; \
copy it to/from host memory via a stream instead"
);
unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.size) }
}
pub fn device_id(&self) -> DeviceId {
self.device_id
}
fn opaque_handle(&self) -> u32 {
let bits = self.ptr.as_ptr() as usize as u64;
let mixed = bits.wrapping_mul(0x9E37_79B9_7F4A_7C15);
((mixed >> 32) ^ (mixed & 0xFFFF_FFFF)) as u32
}
pub fn try_grow_in_place(&mut self, new_size: usize) -> Result<(), UnifiedError> {
if IS_UVM_BACKED {
return Err(UnifiedError::Cuda(
"in-place grow not yet wired -- see UnifiedBuffer::try_grow_in_place doc + \
RFC 0001 v0.4 follow-up. Until then TensorWasmLinearMemory uses the B5 \
option-(a) preallocate-at-max strategy."
.into(),
));
}
if !self.host_addressable {
return Err(UnifiedError::NotSupported {
feature: "try_grow_in_place",
backing: "host-box",
});
}
if new_size > self.host_capacity {
return Err(UnifiedError::NotSupported {
feature: "try_grow_in_place",
backing: "host-box",
});
}
let old_size = self.size;
self.size = new_size;
if new_size > old_size {
self.as_mut_slice()[old_size..new_size].fill(0);
}
Ok(())
}
pub const fn supports_in_place_grow() -> bool {
!IS_UVM_BACKED
}
pub fn is_uvm_backed(&self) -> bool {
IS_UVM_BACKED
}
pub fn prefetch_to_device(&self) -> Result<(), UnifiedError> {
Ok(())
}
pub fn prefetch_to_host(&self) -> Result<(), UnifiedError> {
Ok(())
}
}
impl UnifiedBacking for UnifiedBuffer {
fn len(&self) -> usize {
UnifiedBuffer::len(self)
}
fn as_slice(&self) -> &[u8] {
UnifiedBuffer::as_slice(self)
}
fn as_mut_slice(&mut self) -> &mut [u8] {
UnifiedBuffer::as_mut_slice(self)
}
fn apply_advice(&self, hint: UvmAdvice) -> Result<(), UnifiedError> {
#[cfg(feature = "unified-memory")]
{
let advice = match hint {
UvmAdvice::SetReadMostly => crate::advise::Advice::ReadMostly,
UvmAdvice::UnsetReadMostly => {
return Err(UnifiedError::NotSupported {
feature: "apply_advice(UnsetReadMostly)",
backing: "cust",
});
}
UvmAdvice::SetPreferredLocation(d) => {
crate::advise::Advice::PreferredLocation(DeviceId(d))
}
UvmAdvice::UnsetPreferredLocation => crate::advise::Advice::UnsetPreferredLocation,
UvmAdvice::SetAccessedBy(d) => crate::advise::Advice::AccessedBy(DeviceId(d)),
UvmAdvice::UnsetAccessedBy(d) => {
crate::advise::Advice::UnsetAccessedBy(DeviceId(d))
}
};
crate::advise::apply(self, advice)
}
#[cfg(not(feature = "unified-memory"))]
{
let _ = hint;
Ok(())
}
}
fn prefetch_to_device(&self, device_ord: u32) -> Result<(), UnifiedError> {
if device_ord == self.device_id().0 {
UnifiedBuffer::prefetch_to_device(self)
} else {
Err(UnifiedError::NotSupported {
feature: "prefetch_to_device(non-owning-ordinal)",
backing: "cust",
})
}
}
fn prefetch_to_host(&self) -> Result<(), UnifiedError> {
UnifiedBuffer::prefetch_to_host(self)
}
}
impl fmt::Debug for UnifiedBuffer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UnifiedBuffer")
.field("ptr", &"<redacted>")
.field("handle", &format_args!("{:#010x}", self.opaque_handle()))
.field("size", &self.size)
.field("device_id", &self.device_id)
.field("host_addressable", &self.host_addressable)
.finish()
}
}
impl Drop for UnifiedBuffer {
fn drop(&mut self) {
if let Some(ctx) = self.tenant_ctx.as_ref() {
ctx.release_gpu_bytes(self.size as u64);
}
if self.host_zeroize_on_drop && self.host_capacity > 0 {
unsafe {
std::ptr::write_bytes(self.ptr.as_ptr(), 0u8, self.host_capacity);
let _ = std::ptr::read_volatile(self.ptr.as_ptr());
}
}
}
}
impl From<UnifiedError> for tensor_wasm_core::error::TensorWasmError {
fn from(e: UnifiedError) -> Self {
match e {
UnifiedError::ZeroSize => tensor_wasm_core::error::TensorWasmError::Serialization(
"unified buffer: zero-byte allocation rejected".into(),
),
UnifiedError::Allocation(msg) => {
tensor_wasm_core::error::TensorWasmError::Serialization(
format!("unified buffer allocation failed: {msg}").into(),
)
}
UnifiedError::Cuda(msg) => {
tensor_wasm_core::error::TensorWasmError::CudaError(msg.into())
}
UnifiedError::TooLarge { requested, limit } => {
tensor_wasm_core::error::TensorWasmError::MemoryExhausted { requested, limit }
}
UnifiedError::NotSupported { feature, backing } => {
tensor_wasm_core::error::TensorWasmError::Serialization(
format!("unified backing {backing:?} does not support feature {feature:?}")
.into(),
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn allocate_and_round_trip() {
let mut b = UnifiedBuffer::new(64).expect("alloc");
assert_eq!(b.len(), 64);
assert!(!b.is_empty());
b.as_mut_slice().copy_from_slice(&[7u8; 64]);
assert!(b.as_slice().iter().all(|&v| v == 7));
}
#[test]
fn zero_size_rejected() {
let err = UnifiedBuffer::new(0).expect_err("zero should be rejected");
assert!(matches!(err, UnifiedError::ZeroSize));
}
#[test]
fn try_grow_in_place_classifies_per_backing() {
assert_eq!(UnifiedBuffer::supports_in_place_grow(), !IS_UVM_BACKED);
let mut b = UnifiedBuffer::new(64).expect("alloc");
let err = b.try_grow_in_place(128).expect_err("over-cap must error");
if IS_UVM_BACKED {
let msg = format!("{err}");
assert!(
msg.contains("in-place grow not yet wired"),
"sentinel string changed; v0.4 caller match site must be updated: {msg}",
);
} else {
assert!(
matches!(
err,
UnifiedError::NotSupported {
feature: "try_grow_in_place",
backing: "host-box",
}
),
"host over-cap grow must return typed NotSupported, got {err:?}",
);
}
}
#[cfg(all(not(feature = "unified-memory"), not(feature = "cudarc-backend")))]
mod host_in_place_grow {
use super::*;
#[test]
fn host_grow_into_reserved_capacity_succeeds_and_zero_fills() {
let mut b = UnifiedBuffer::new_host_with_capacity_on(64, 256, DeviceId::default())
.expect("alloc with capacity");
assert_eq!(b.len(), 64);
assert_eq!(b.capacity(), 256);
b.as_mut_slice().fill(0xAB);
b.try_grow_in_place(200)
.expect("in-place grow within capacity");
assert_eq!(b.len(), 200);
assert_eq!(b.capacity(), 256, "capacity unchanged by in-place grow");
let s = b.as_slice();
assert!(
s[..64].iter().all(|&x| x == 0xAB),
"existing bytes clobbered"
);
assert!(
s[64..200].iter().all(|&x| x == 0),
"grown region not zeroed"
);
}
#[test]
fn host_grow_beyond_capacity_returns_not_supported() {
let mut b = UnifiedBuffer::new_host_with_capacity_on(64, 128, DeviceId::default())
.expect("alloc with capacity");
let err = b
.try_grow_in_place(129)
.expect_err("over-capacity must fall back");
assert!(
matches!(
err,
UnifiedError::NotSupported {
feature: "try_grow_in_place",
backing: "host-box",
}
),
"expected typed realloc-fallback signal, got {err:?}",
);
assert_eq!(b.len(), 64);
}
#[test]
fn host_regrow_after_shrink_rezeros_exposed_region() {
let mut b = UnifiedBuffer::new_host_with_capacity_on(8, 64, DeviceId::default())
.expect("alloc with capacity");
b.try_grow_in_place(64).expect("grow to cap");
b.as_mut_slice().fill(0xFF);
b.try_grow_in_place(8).expect("shrink");
assert_eq!(b.len(), 8);
b.try_grow_in_place(64).expect("re-grow to cap");
assert!(
b.as_slice()[8..64].iter().all(|&x| x == 0),
"re-grown region must be re-zeroed",
);
}
#[test]
fn host_exactly_sized_buffer_has_no_spare_capacity() {
let mut b = UnifiedBuffer::new(64).expect("alloc");
assert_eq!(b.capacity(), b.len());
assert!(b.try_grow_in_place(65).is_err());
b.try_grow_in_place(64)
.expect("grow to current len is a no-op");
assert_eq!(b.len(), 64);
}
}
#[test]
fn device_id_default_is_zero() {
let b = UnifiedBuffer::new(8).unwrap();
assert_eq!(b.device_id(), DeviceId(0));
}
#[test]
fn device_id_display_format() {
assert_eq!(DeviceId(3).to_string(), "cuda:3");
}
#[test]
fn prefetch_no_op_without_cuda() {
let b = UnifiedBuffer::new(32).unwrap();
b.prefetch_to_device().expect("no-op should succeed");
b.prefetch_to_host().expect("no-op should succeed");
}
#[test]
fn pointers_are_non_null_and_consistent() {
let mut b = UnifiedBuffer::new(16).unwrap();
let p1 = b.as_ptr();
let p2 = b.as_mut_ptr() as *const u8;
assert_eq!(p1, p2);
assert!(!p1.is_null());
}
#[test]
fn from_unified_error_to_tensor_wasm_error_zero_size() {
let e = UnifiedError::ZeroSize;
let b: tensor_wasm_core::error::TensorWasmError = e.into();
assert!(matches!(
b,
tensor_wasm_core::error::TensorWasmError::Serialization(_)
));
assert!(b.inner().unwrap_or("").contains("zero-byte"));
}
#[test]
fn from_unified_error_to_tensor_wasm_error_cuda() {
let e = UnifiedError::Cuda("ctx not current".into());
let b: tensor_wasm_core::error::TensorWasmError = e.into();
match b {
tensor_wasm_core::error::TensorWasmError::CudaError(s) => {
assert_eq!(&*s, "ctx not current")
}
other => panic!("expected CudaError, got {other:?}"),
}
}
#[test]
fn from_unified_error_too_large_maps_to_memory_exhausted_with_figures() {
let e = UnifiedError::TooLarge {
requested: 4096,
limit: 1024,
};
let b: tensor_wasm_core::error::TensorWasmError = e.into();
match b {
tensor_wasm_core::error::TensorWasmError::MemoryExhausted { requested, limit } => {
assert_eq!(requested, 4096);
assert_eq!(limit, 1024);
}
other => panic!("expected MemoryExhausted, got {other:?}"),
}
}
#[test]
#[cfg(feature = "unified-memory")]
fn is_uvm_backed_true_under_feature() {
let b = UnifiedBuffer::new(64).expect("alloc under feature");
assert!(
b.is_uvm_backed(),
"unified-memory build must use UVM backing"
);
}
#[test]
#[cfg(all(not(feature = "unified-memory"), not(feature = "cudarc-backend")))]
fn is_uvm_backed_false_without_feature() {
let b = UnifiedBuffer::new(64).expect("alloc without feature");
assert!(!b.is_uvm_backed(), "no-feature build must use heap backing");
}
#[test]
#[cfg(all(not(feature = "unified-memory"), feature = "cudarc-backend"))]
fn is_uvm_backed_true_under_cudarc_backend() {
let b = UnifiedBuffer::new(64).expect("alloc under cudarc-backend");
assert!(
b.is_uvm_backed(),
"cudarc-backend build must use UVM backing"
);
}
#[test]
fn backing_aliasing_sealed_allocate_use_drop_round_trip() {
let mut b = UnifiedBuffer::new(128).expect("alloc");
{
let s = b.as_mut_slice();
for (i, byte) in s.iter_mut().enumerate() {
*byte = (i & 0xFF) as u8;
}
}
{
let s = b.as_slice();
for (i, byte) in s.iter().enumerate() {
assert_eq!(
*byte,
(i & 0xFF) as u8,
"byte {i} mismatch — aliasing regression?"
);
}
}
drop(b);
}
#[test]
fn debug_redacts_raw_pointer_address() {
let b = UnifiedBuffer::new(32).expect("alloc");
let dbg = format!("{b:?}");
assert!(dbg.contains("<redacted>"), "ptr not redacted: {dbg}");
let raw = format!("{:p}", b.as_ptr());
assert!(
!dbg.contains(&raw),
"Debug leaked the raw pointer address {raw}: {dbg}"
);
}
#[test]
#[cfg(all(not(feature = "unified-memory"), not(feature = "cudarc-backend")))]
fn host_backed_buffer_is_zeroized_on_drop() {
let mut b = UnifiedBuffer::new(256).expect("alloc");
b.as_mut_slice().fill(0xAB);
assert!(
b.host_zeroize_on_drop,
"host build must flag drop-time zeroization"
);
drop(b);
}
#[test]
fn host_addressable_buffers_expose_slices() {
let mut b = UnifiedBuffer::new(16).expect("alloc");
assert!(b.host_addressable);
b.as_mut_slice().fill(1);
assert!(b.as_slice().iter().all(|&v| v == 1));
}
#[test]
fn from_unified_error_allocation_maps_to_serialization() {
let e = UnifiedError::Allocation("minimum 1024 > maximum 512".into());
let b: tensor_wasm_core::error::TensorWasmError = e.into();
assert!(matches!(
b,
tensor_wasm_core::error::TensorWasmError::Serialization(_)
));
assert!(b.inner().unwrap_or("").contains("minimum 1024"));
}
}