use std::sync::{Arc, OnceLock};
use onnx_runtime_ep_api::{DeviceBuffer, ExecutionProvider};
use onnx_runtime_ep_cpu::CpuExecutionProvider;
use onnx_runtime_ir::{DataType, DeviceId, TensorLayout};
use crate::error::{Result, SessionError};
pub(crate) fn shared_cpu_ep() -> Arc<CpuExecutionProvider> {
static EP: OnceLock<Arc<CpuExecutionProvider>> = OnceLock::new();
EP.get_or_init(|| {
let mut ep = CpuExecutionProvider::new();
let _ = ep.initialize(&Default::default());
Arc::new(ep)
})
.clone()
}
pub fn cpu_allocator() -> Arc<dyn ExecutionProvider> {
shared_cpu_ep()
}
pub(crate) fn host_bytes(buffer: &DeviceBuffer) -> &[u8] {
assert!(
buffer.device().is_host_accessible(),
"host_bytes on non-host device {:?}",
buffer.device()
);
if buffer.is_empty() {
return &[];
}
unsafe { std::slice::from_raw_parts(buffer.as_ptr() as *const u8, buffer.len()) }
}
pub(crate) fn write_host(buffer: &mut DeviceBuffer, src: &[u8]) -> Result<()> {
assert!(
buffer.device().is_host_accessible(),
"write_host on non-host device {:?}",
buffer.device()
);
if src.len() > buffer.len() {
return Err(SessionError::Internal(format!(
"write_host: source {} bytes exceeds buffer {} bytes",
src.len(),
buffer.len()
)));
}
if src.is_empty() {
return Ok(());
}
let dst = buffer.as_mut_ptr() as *mut u8;
unsafe {
std::ptr::copy_nonoverlapping(src.as_ptr(), dst, src.len());
}
Ok(())
}
pub struct Tensor {
pub dtype: DataType,
pub shape: Vec<usize>,
pub layout: TensorLayout,
device: DeviceId,
buffer: Option<DeviceBuffer>,
allocator: Arc<dyn ExecutionProvider>,
import_guard: Option<Box<dyn core::any::Any + Send + Sync>>,
}
impl Tensor {
pub(crate) fn from_raw_in(
allocator: Arc<dyn ExecutionProvider>,
dtype: DataType,
shape: Vec<usize>,
bytes: &[u8],
) -> Result<Self> {
let numel: usize = shape.iter().product();
let expected = dtype.storage_bytes(numel);
if bytes.len() != expected {
return Err(SessionError::Internal(format!(
"Tensor::from_raw_in: {} bytes for shape {shape:?} dtype {dtype:?}, expected {expected}",
bytes.len()
)));
}
let layout = TensorLayout::contiguous();
let align = layout.alignment;
let mut buffer = allocator.allocate(expected.max(1), align)?;
write_host(&mut buffer, bytes)?;
Ok(Self {
dtype,
shape,
layout,
device: buffer.device(),
buffer: Some(buffer),
allocator,
import_guard: None,
})
}
pub fn from_raw(dtype: DataType, shape: Vec<usize>, bytes: &[u8]) -> Result<Self> {
Self::from_raw_in(shared_cpu_ep(), dtype, shape, bytes)
}
pub fn from_f32(shape: &[usize], data: &[f32]) -> Result<Self> {
let mut bytes = Vec::with_capacity(data.len() * 4);
for v in data {
bytes.extend_from_slice(&v.to_le_bytes());
}
Self::from_raw(DataType::Float32, shape.to_vec(), &bytes)
}
pub fn from_i64(shape: &[usize], data: &[i64]) -> Result<Self> {
let mut bytes = Vec::with_capacity(data.len() * 8);
for v in data {
bytes.extend_from_slice(&v.to_le_bytes());
}
Self::from_raw(DataType::Int64, shape.to_vec(), &bytes)
}
pub fn device(&self) -> DeviceId {
self.device
}
pub fn from_borrowed_parts_with_guard(
allocator: Arc<dyn ExecutionProvider>,
dtype: DataType,
shape: Vec<usize>,
layout: TensorLayout,
buffer: DeviceBuffer,
guard: Box<dyn core::any::Any + Send + Sync>,
) -> Self {
debug_assert!(
buffer.is_borrowed(),
"from_borrowed_parts_with_guard requires a borrowed DeviceBuffer; \
an owned buffer would be freed twice (EP deallocate + guard)"
);
Self {
dtype,
shape,
layout,
device: buffer.device(),
buffer: Some(buffer),
allocator,
import_guard: Some(guard),
}
}
pub fn numel(&self) -> usize {
self.shape.iter().product()
}
pub fn device_ptr(&self) -> *const std::ffi::c_void {
if self.numel() == 0 {
std::ptr::null()
} else {
self.buffer().as_ptr()
}
}
pub fn sync(&self) -> Result<()> {
self.allocator.sync()?;
Ok(())
}
fn buffer(&self) -> &DeviceBuffer {
self.buffer
.as_ref()
.expect("Tensor buffer taken only in Drop")
}
pub fn as_bytes(&self) -> &[u8] {
let n = self.dtype.storage_bytes(self.numel());
&host_bytes(self.buffer())[..n]
}
pub(crate) fn overwrite_bytes(&mut self, bytes: &[u8]) -> Result<()> {
let expected = self.dtype.storage_bytes(self.numel());
if bytes.len() != expected {
return Err(SessionError::Internal(format!(
"Tensor::overwrite_bytes: got {} bytes for shape {:?} dtype {:?}, expected {expected}",
bytes.len(), self.shape, self.dtype
)));
}
let buffer = self.buffer.as_mut().expect("Tensor buffer taken only in Drop");
write_host(buffer, bytes)
}
pub fn to_vec_f32(&self) -> Vec<f32> {
assert_eq!(self.dtype, DataType::Float32, "to_vec_f32 on non-f32 tensor");
self.as_bytes()
.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect()
}
pub fn to_vec_i64(&self) -> Vec<i64> {
assert_eq!(self.dtype, DataType::Int64, "to_vec_i64 on non-i64 tensor");
self.as_bytes()
.chunks_exact(8)
.map(|c| i64::from_le_bytes(c.try_into().unwrap()))
.collect()
}
}
impl Clone for Tensor {
fn clone(&self) -> Self {
Self::from_raw_in(
self.allocator.clone(),
self.dtype,
self.shape.clone(),
self.as_bytes(),
)
.expect("Tensor::clone: re-allocation of identical bytes")
}
}
impl std::fmt::Debug for Tensor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Tensor")
.field("dtype", &self.dtype)
.field("shape", &self.shape)
.field("device", &self.device)
.finish()
}
}
impl Drop for Tensor {
fn drop(&mut self) {
if let Some(buffer) = self.buffer.take() {
let _ = self.allocator.deallocate(buffer);
}
let _ = self.import_guard.take();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::raw::c_void;
use std::sync::atomic::{AtomicUsize, Ordering};
struct CountingGuard(Arc<AtomicUsize>);
impl Drop for CountingGuard {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
#[test]
fn borrowed_guard_ctor_runs_guard_exactly_once_on_drop() {
let drops = Arc::new(AtomicUsize::new(0));
let mut backing = [1.0f32, 2.0, 3.0, 4.0];
let ptr = backing.as_mut_ptr() as *mut c_void;
let buffer = unsafe {
DeviceBuffer::from_borrowed_parts(ptr, DeviceId::cpu(), backing.len() * 4, 4)
};
assert!(buffer.is_borrowed());
let guard = Box::new(CountingGuard(drops.clone()));
let tensor = Tensor::from_borrowed_parts_with_guard(
shared_cpu_ep(),
DataType::Float32,
vec![4],
TensorLayout::contiguous(),
buffer,
guard,
);
assert_eq!(tensor.as_bytes().len(), 16);
assert_eq!(tensor.to_vec_f32(), vec![1.0, 2.0, 3.0, 4.0]);
assert_eq!(drops.load(Ordering::SeqCst), 0, "guard alive while tensor is");
drop(tensor);
assert_eq!(drops.load(Ordering::SeqCst), 1, "guard runs exactly once on drop");
}
}