use std::sync::{Arc, OnceLock};
use onnx_runtime_ep_api::{
DeviceBuffer, DeviceGraphToken, DeviceValidationRegistration, DeviceValidationToken,
ExecutionProvider,
};
use onnx_runtime_ep_cpu::CpuExecutionProvider;
use onnx_runtime_ir::{DataType, DeviceId, TensorLayout, checked_expected_bytes, read_vec_le};
use crate::error::{Result, SessionError};
use crate::sequence::{SequenceError, SequenceResult, clone_shape};
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) struct SharedTensorBuffer {
buffer: Option<DeviceBuffer>,
allocator: Arc<dyn ExecutionProvider>,
import_guard: Option<Box<dyn core::any::Any + Send + Sync>>,
}
impl SharedTensorBuffer {
pub(crate) fn new(allocator: Arc<dyn ExecutionProvider>, buffer: DeviceBuffer) -> Arc<Self> {
Arc::new(Self {
buffer: Some(buffer),
allocator,
import_guard: None,
})
}
fn with_guard(
allocator: Arc<dyn ExecutionProvider>,
buffer: DeviceBuffer,
import_guard: Option<Box<dyn core::any::Any + Send + Sync>>,
) -> Arc<Self> {
Arc::new(Self {
buffer: Some(buffer),
allocator,
import_guard,
})
}
pub(crate) fn allocate_cpu(bytes: usize) -> Result<Arc<Self>> {
let allocator: Arc<dyn ExecutionProvider> = shared_cpu_ep();
let buffer = allocator.allocate(bytes.max(1), TensorLayout::contiguous().alignment)?;
Ok(Self::new(allocator, buffer))
}
pub(crate) fn buffer(&self) -> &DeviceBuffer {
self.buffer
.as_ref()
.expect("SharedTensorBuffer buffer taken only in Drop")
}
pub(crate) fn buffer_mut(&mut self) -> &mut DeviceBuffer {
self.buffer
.as_mut()
.expect("SharedTensorBuffer buffer taken only in Drop")
}
pub(crate) fn allocator(&self) -> &Arc<dyn ExecutionProvider> {
&self.allocator
}
pub(crate) fn alias(&self) -> DeviceBuffer {
let buffer = self.buffer();
unsafe {
DeviceBuffer::from_borrowed_parts(
buffer.as_ptr() as *mut std::ffi::c_void,
buffer.device(),
buffer.len(),
buffer.alignment(),
)
}
}
pub(crate) fn into_buffer(mut self) -> DeviceBuffer {
debug_assert!(
self.import_guard.is_none(),
"executor-promoted buffers never carry a foreign import guard"
);
self.buffer
.take()
.expect("SharedTensorBuffer buffer taken only by into_buffer or Drop")
}
}
impl std::fmt::Debug for SharedTensorBuffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SharedTensorBuffer")
.field("device", &self.buffer().device())
.field("len", &self.buffer().len())
.field("ptr", &self.buffer().as_ptr())
.finish()
}
}
impl Drop for SharedTensorBuffer {
fn drop(&mut self) {
if let Some(buffer) = self.buffer.take() {
let _ = self.allocator.deallocate(buffer);
}
let _ = self.import_guard.take();
}
}
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 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>>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct DeviceBindingTransferStats {
pub host_upload_calls: u64,
pub host_upload_bytes: u64,
pub host_download_calls: u64,
pub host_download_bytes: u64,
}
pub(crate) struct DeviceBindingSpec {
pub(crate) input_name: String,
pub(crate) bind_input: bool,
pub(crate) output_name: Option<String>,
pub(crate) dtype: DataType,
pub(crate) physical_shape: Vec<usize>,
pub(crate) logical_shape: Vec<usize>,
pub(crate) expose_logical_input_shape: bool,
pub(crate) decode_freeze_safe_mask: bool,
pub(crate) fixed_physical_strides: bool,
pub(crate) allocation_bytes: Option<usize>,
pub(crate) committed_ranges: Option<Vec<std::ops::Range<usize>>>,
}
pub struct ExternalMemorySpec {
pub input_name: String,
pub bind_input: bool,
pub output_name: Option<String>,
pub dtype: DataType,
pub physical_shape: Vec<usize>,
pub logical_shape: Vec<usize>,
pub ptr: *mut core::ffi::c_void,
pub len_bytes: usize,
}
impl ExternalMemorySpec {
pub fn input(
input_name: impl Into<String>,
output_name: Option<impl Into<String>>,
dtype: DataType,
physical_shape: Vec<usize>,
logical_shape: Vec<usize>,
ptr: *mut core::ffi::c_void,
len_bytes: usize,
) -> Self {
Self {
input_name: input_name.into(),
bind_input: true,
output_name: output_name.map(Into::into),
dtype,
physical_shape,
logical_shape,
ptr,
len_bytes,
}
}
pub fn output(
output_name: impl Into<String>,
dtype: DataType,
physical_shape: Vec<usize>,
logical_shape: Vec<usize>,
ptr: *mut core::ffi::c_void,
len_bytes: usize,
) -> Self {
Self {
input_name: String::new(),
bind_input: false,
output_name: Some(output_name.into()),
dtype,
physical_shape,
logical_shape,
ptr,
len_bytes,
}
}
}
pub struct DeviceIoBinding {
input_name: String,
bind_input: bool,
output_name: Option<String>,
pub dtype: DataType,
physical_shape: Vec<usize>,
logical_shape: Vec<usize>,
expose_logical_input_shape: bool,
decode_freeze_safe_mask: bool,
fixed_physical_strides: bool,
buffer: Option<DeviceBuffer>,
allocator: Arc<dyn ExecutionProvider>,
transfer_stats: DeviceBindingTransferStats,
device_graph_token: Option<DeviceGraphToken>,
validation_registration: Option<DeviceValidationRegistration>,
device_validation: Option<DeviceValidationToken>,
}
impl DeviceIoBinding {
pub(crate) fn allocate(
allocator: Arc<dyn ExecutionProvider>,
spec: DeviceBindingSpec,
) -> Result<Self> {
let DeviceBindingSpec {
input_name,
bind_input,
output_name,
dtype,
physical_shape,
logical_shape,
expose_logical_input_shape,
decode_freeze_safe_mask,
fixed_physical_strides,
allocation_bytes,
committed_ranges,
} = spec;
validate_logical_shape(&physical_shape, &logical_shape)?;
let bytes = checked_expected_bytes(dtype, &physical_shape)
.ok_or_else(|| SessionError::ShapeOverflow {
value: format!("device binding '{input_name}'"),
dims: physical_shape.clone(),
})?
.max(1);
let allocation_bytes = allocation_bytes.unwrap_or(bytes).max(1);
if allocation_bytes < bytes {
return Err(SessionError::ExternalBuffer {
binding: input_name,
reason: format!(
"allocation is {allocation_bytes} bytes but physical shape {physical_shape:?} needs {bytes}"
),
});
}
let allocator_for_buffer = allocator.clone();
let default_range;
let ranges = match committed_ranges.as_deref() {
Some(ranges) => ranges,
None => {
default_range = 0..allocation_bytes;
std::slice::from_ref(&default_range)
}
};
let buffer = allocator_for_buffer.allocate_committed(
allocation_bytes,
TensorLayout::contiguous().alignment,
ranges,
)?;
let validation_registration = match allocator.register_device_validation_owner() {
Ok(registration) => registration,
Err(error) => {
let _ = allocator.deallocate(buffer);
return Err(error.into());
}
};
Ok(Self {
input_name,
bind_input,
output_name,
dtype,
physical_shape,
logical_shape,
expose_logical_input_shape,
decode_freeze_safe_mask,
fixed_physical_strides,
buffer: Some(buffer),
allocator,
transfer_stats: DeviceBindingTransferStats::default(),
device_graph_token: None,
validation_registration: Some(validation_registration),
device_validation: None,
})
}
pub(crate) unsafe fn from_external_memory(
allocator: Arc<dyn ExecutionProvider>,
spec: DeviceBindingSpec,
ptr: *mut core::ffi::c_void,
len_bytes: usize,
) -> Result<Self> {
let DeviceBindingSpec {
input_name,
bind_input,
output_name,
dtype,
physical_shape,
logical_shape,
expose_logical_input_shape,
decode_freeze_safe_mask,
fixed_physical_strides,
allocation_bytes: _,
committed_ranges: _,
} = spec;
validate_logical_shape(&physical_shape, &logical_shape)?;
let required = checked_expected_bytes(dtype, &physical_shape)
.ok_or_else(|| SessionError::ShapeOverflow {
value: format!("device binding '{input_name}'"),
dims: physical_shape.clone(),
})?
.max(1);
if len_bytes < required {
return Err(SessionError::ExternalBuffer {
binding: input_name,
reason: format!(
"it is {len_bytes} bytes but {physical_shape:?} of {dtype:?} needs \
{required}; pass a buffer at least that large or reduce the physical shape"
),
});
}
let alignment = crate::executor::host_dtype_alignment(dtype);
if !ptr.is_null() && !ptr.addr().is_multiple_of(alignment) {
return Err(SessionError::ExternalBuffer {
binding: input_name,
reason: format!(
"it is at address {:#x}, which is not a multiple of the {alignment}-byte \
alignment {dtype:?} requires; allocate it with at least that alignment",
ptr.addr()
),
});
}
let buffer = unsafe {
DeviceBuffer::from_borrowed_mut_parts(ptr, allocator.device_id(), required, alignment)
}
.ok_or_else(|| SessionError::ExternalBuffer {
binding: input_name.clone(),
reason: "it is null; pass the address of a real allocation".to_string(),
})?;
let validation_registration = allocator.register_device_validation_owner()?;
Ok(Self {
input_name,
bind_input,
output_name,
dtype,
physical_shape,
logical_shape,
expose_logical_input_shape,
decode_freeze_safe_mask,
fixed_physical_strides,
buffer: Some(buffer),
allocator,
transfer_stats: DeviceBindingTransferStats::default(),
device_graph_token: None,
validation_registration: Some(validation_registration),
device_validation: None,
})
}
pub fn input_name(&self) -> &str {
&self.input_name
}
pub(crate) fn binds_input(&self) -> bool {
self.bind_input
}
pub fn output_name(&self) -> Option<&str> {
self.output_name.as_deref()
}
pub fn physical_shape(&self) -> &[usize] {
&self.physical_shape
}
pub fn logical_shape(&self) -> &[usize] {
&self.logical_shape
}
pub(crate) fn kernel_input_shape(&self) -> &[usize] {
if self.expose_logical_input_shape {
&self.logical_shape
} else {
&self.physical_shape
}
}
pub fn has_dynamic_logical_input_shape(&self) -> bool {
self.bind_input
&& self.expose_logical_input_shape
&& !self.fixed_physical_strides
&& self.logical_shape != self.physical_shape
}
pub fn exposes_logical_input_shape(&self) -> bool {
self.bind_input && self.expose_logical_input_shape
}
pub fn mask_decode_freeze_safe(&self) -> bool {
self.bind_input && self.decode_freeze_safe_mask
}
pub fn fixed_physical_strides(&self) -> bool {
self.fixed_physical_strides
}
pub(crate) fn set_device_graph_token(&mut self, token: DeviceGraphToken) {
self.device_graph_token = Some(token);
}
pub(crate) fn validation_registration(&self) -> &DeviceValidationRegistration {
self.validation_registration
.as_ref()
.expect("device binding validation registration exists until Drop")
}
pub(crate) fn set_device_validation(&mut self, token: DeviceValidationToken) {
self.device_validation = Some(token);
}
#[cfg(test)]
pub(crate) fn device_validation_token_for_test(&self) -> Option<DeviceValidationToken> {
self.device_validation
}
pub fn set_logical_shape(&mut self, shape: Vec<usize>) -> Result<()> {
validate_logical_shape(&self.physical_shape, &shape)?;
self.logical_shape = shape;
Ok(())
}
pub fn set_physical_and_logical_shapes(
&mut self,
physical_shape: Vec<usize>,
logical_shape: Vec<usize>,
) -> Result<()> {
validate_logical_shape(&physical_shape, &logical_shape)?;
let required = checked_expected_bytes(self.dtype, &physical_shape).ok_or_else(|| {
SessionError::ShapeOverflow {
value: format!("device binding '{}'", self.input_name),
dims: physical_shape.clone(),
}
})?;
if required > self.buffer().len() {
return Err(SessionError::ExternalBuffer {
binding: self.input_name.clone(),
reason: format!(
"shape {physical_shape:?} needs {required} bytes but allocation has {}",
self.buffer().len()
),
});
}
self.physical_shape = physical_shape;
self.logical_shape = logical_shape;
Ok(())
}
pub fn commit_range(&mut self, byte_offset: usize, bytes: usize) -> Result<()> {
let buffer = self
.buffer
.as_ref()
.expect("DeviceIoBinding buffer taken only in Drop");
self.allocator
.commit_allocation_range(buffer, byte_offset, bytes)?;
Ok(())
}
pub fn commit_binding_ranges(&self, ranges: &[(&DeviceIoBinding, usize, usize)]) -> Result<()> {
let buffers = ranges
.iter()
.map(|&(binding, offset, bytes)| {
(
binding
.buffer
.as_ref()
.expect("DeviceIoBinding buffer taken only in Drop"),
offset,
bytes,
)
})
.collect::<Vec<_>>();
self.allocator.commit_allocation_ranges(&buffers)?;
Ok(())
}
pub fn commit_binding_ranges_with_mapped_growth(
&self,
ranges: &[(&DeviceIoBinding, usize, usize)],
grant: &mut onnx_runtime_memory_governor::MappedGrowthGrant,
) -> Result<u64> {
let buffers = ranges
.iter()
.map(|&(binding, offset, bytes)| {
(
binding
.buffer
.as_ref()
.expect("DeviceIoBinding buffer taken only in Drop"),
offset,
bytes,
)
})
.collect::<Vec<_>>();
Ok(self
.allocator
.commit_allocation_ranges_with_mapped_growth(&buffers, grant)?)
}
pub fn mapped_bytes_for_binding_ranges(
&self,
ranges: &[(&DeviceIoBinding, usize, usize)],
) -> Result<u64> {
let buffers = ranges
.iter()
.map(|&(binding, offset, bytes)| {
(
binding
.buffer
.as_ref()
.expect("DeviceIoBinding buffer taken only in Drop"),
offset,
bytes,
)
})
.collect::<Vec<_>>();
Ok(self
.allocator
.mapped_bytes_for_allocation_ranges(&buffers)?)
}
pub fn decommit_range(&mut self, byte_offset: usize, bytes: usize) -> Result<()> {
let buffer = self
.buffer
.as_ref()
.expect("DeviceIoBinding buffer taken only in Drop");
self.allocator
.decommit_allocation_range(buffer, byte_offset, bytes)?;
Ok(())
}
pub fn committed_bytes(&self) -> usize {
let buffer = self
.buffer
.as_ref()
.expect("DeviceIoBinding buffer taken only in Drop");
self.allocator.allocation_committed_bytes(buffer)
}
pub fn device_ptr(&self) -> *const std::ffi::c_void {
self.buffer().as_ptr()
}
pub fn allocator(&self) -> &Arc<dyn ExecutionProvider> {
&self.allocator
}
pub fn snapshot_device_into(&self, scratch: &mut DeviceBuffer, bytes: usize) -> Result<()> {
let buffer = self.buffer();
if bytes > buffer.len() {
return Err(SessionError::ExternalBuffer {
binding: self.input_name.clone(),
reason: format!(
"device snapshot of {bytes} bytes exceeds allocation of {}",
buffer.len()
),
});
}
self.allocator
.copy_device_to_device(buffer, 0, scratch, 0, bytes)?;
Ok(())
}
pub fn restore_device_from(&mut self, scratch: &DeviceBuffer, bytes: usize) -> Result<()> {
let buffer = self
.buffer
.as_mut()
.expect("DeviceIoBinding buffer taken only in Drop");
if bytes > buffer.len() {
return Err(SessionError::ExternalBuffer {
binding: self.input_name.clone(),
reason: format!(
"device restore of {bytes} bytes exceeds allocation of {}",
buffer.len()
),
});
}
self.allocator
.copy_device_to_device(scratch, 0, buffer, 0, bytes)?;
Ok(())
}
pub fn transfer_stats(&self) -> DeviceBindingTransferStats {
self.transfer_stats
}
pub fn write_bytes(&mut self, byte_offset: usize, bytes: &[u8]) -> Result<()> {
let buffer = self
.buffer
.as_mut()
.expect("DeviceIoBinding buffer taken only in Drop");
self.allocator
.copy_from_host_at(bytes, buffer, byte_offset)?;
self.transfer_stats.host_upload_calls += 1;
self.transfer_stats.host_upload_bytes += bytes.len() as u64;
Ok(())
}
pub fn read_bytes(&mut self) -> Result<Vec<u8>> {
let mut bytes = vec![0; self.buffer().len()];
self.read_bytes_into(&mut bytes)?;
Ok(bytes)
}
pub fn read_bytes_into(&mut self, bytes: &mut [u8]) -> Result<()> {
if bytes.is_empty() {
self.allocator.sync()?;
} else {
self.allocator.copy_to_host(self.buffer(), bytes)?;
}
self.check_and_reset_device_validation()?;
self.transfer_stats.host_download_calls += 1;
self.transfer_stats.host_download_bytes += bytes.len() as u64;
Ok(())
}
fn check_and_reset_device_validation(&self) -> Result<()> {
let Some(token) = self.device_validation else {
return Ok(());
};
let flags = self
.allocator
.consume_device_validation_error(self.validation_registration(), token)?;
if flags != 0 {
return Err(onnx_runtime_ep_api::EpError::KernelFailed(format!(
"{}: device validation failed (flags=0x{flags:x})",
self.allocator.name()
))
.into());
}
Ok(())
}
pub fn read_bytes_range(&mut self, byte_offset: usize, byte_len: usize) -> Result<Vec<u8>> {
let end =
byte_offset
.checked_add(byte_len)
.ok_or_else(|| SessionError::ExternalBuffer {
binding: self.input_name.clone(),
reason: format!(
"read range offset {byte_offset} plus {byte_len} bytes overflows"
),
})?;
let buffer = self.buffer();
if end > buffer.len() {
return Err(SessionError::ExternalBuffer {
binding: self.input_name.clone(),
reason: format!(
"read range {byte_offset}..{end} exceeds allocation of {} bytes",
buffer.len()
),
});
}
let mut bytes = vec![0; byte_len];
if byte_len == 0 {
self.allocator.sync()?;
self.check_and_reset_device_validation()?;
return Ok(bytes);
}
let alias = unsafe {
DeviceBuffer::from_borrowed_parts(
(buffer.as_ptr() as *const u8).add(byte_offset) as *mut std::ffi::c_void,
buffer.device(),
byte_len,
buffer.alignment(),
)
};
self.allocator.copy_to_host(&alias, &mut bytes)?;
self.check_and_reset_device_validation()?;
self.transfer_stats.host_download_calls += 1;
self.transfer_stats.host_download_bytes += bytes.len() as u64;
Ok(bytes)
}
pub fn device_argmax_supported(&self) -> bool {
self.allocator.device_argmax_supported()
&& matches!(
self.dtype,
DataType::Float32 | DataType::Float16 | DataType::BFloat16
)
}
pub fn device_argmax(
&self,
elements: usize,
batch: usize,
result: &mut DeviceIoBinding,
) -> Result<()> {
self.device_argmax_with_tie_break(
elements,
batch,
result,
onnx_runtime_ep_api::ArgmaxTieBreak::LowestIndex,
)
}
pub fn device_argmax_with_tie_break(
&self,
elements: usize,
batch: usize,
result: &mut DeviceIoBinding,
tie_break: onnx_runtime_ep_api::ArgmaxTieBreak,
) -> Result<()> {
if !matches!(
self.dtype,
DataType::Float32 | DataType::Float16 | DataType::BFloat16
) || result.dtype != DataType::Uint32
{
return Err(SessionError::Internal(format!(
"device argmax requires f32/f16/bf16 logits and u32 result, got {:?} and {:?}",
self.dtype, result.dtype
)));
}
if !Arc::ptr_eq(&self.allocator, &result.allocator) {
return Err(SessionError::Internal(
"device argmax bindings must belong to the same execution provider".into(),
));
}
Ok(self.allocator.device_argmax(
self.buffer(),
elements,
batch,
self.dtype,
result.buffer_mut(),
tie_break,
)?)
}
#[allow(clippy::too_many_arguments)]
pub fn device_token_writer(
&self,
input_ids: &DeviceIoBinding,
position_ids: Option<&DeviceIoBinding>,
attention_mask: &DeviceIoBinding,
scratch: &DeviceIoBinding,
capacity: usize,
next_position: i64,
mask_len: usize,
step: u32,
) -> Result<()> {
if self.dtype != DataType::Uint32 || scratch.dtype != DataType::Uint32 {
return Err(SessionError::Internal(format!(
"device token writer requires u32 result/scratch, got {:?} and {:?}",
self.dtype, scratch.dtype
)));
}
let write_position = position_ids.is_some();
let position_binding = position_ids.unwrap_or(input_ids);
for binding in [input_ids, position_binding, attention_mask, scratch] {
if !Arc::ptr_eq(&self.allocator, &binding.allocator) {
return Err(SessionError::Internal(
"device token writer bindings must belong to the same execution provider"
.into(),
));
}
}
Ok(self.allocator.device_token_writer(
self.buffer(),
input_ids.buffer(),
position_binding.buffer(),
attention_mask.buffer(),
scratch.buffer(),
capacity,
next_position,
mask_len,
write_position,
step,
)?)
}
pub(crate) fn buffer(&self) -> &DeviceBuffer {
self.buffer
.as_ref()
.expect("DeviceIoBinding buffer taken only in Drop")
}
pub(crate) fn buffer_mut(&mut self) -> &mut DeviceBuffer {
self.buffer
.as_mut()
.expect("DeviceIoBinding buffer taken only in Drop")
}
}
fn validate_logical_shape(physical: &[usize], logical: &[usize]) -> Result<()> {
if physical.len() != logical.len()
|| physical
.iter()
.zip(logical)
.any(|(&capacity, &valid)| valid > capacity)
{
return Err(SessionError::Internal(format!(
"device binding logical shape {logical:?} exceeds physical capacity {physical:?}"
)));
}
Ok(())
}
impl std::fmt::Debug for DeviceIoBinding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DeviceIoBinding")
.field("input_name", &self.input_name)
.field("bind_input", &self.bind_input)
.field("output_name", &self.output_name)
.field("dtype", &self.dtype)
.field("physical_shape", &self.physical_shape)
.field("logical_shape", &self.logical_shape)
.field(
"expose_logical_input_shape",
&self.expose_logical_input_shape,
)
.field("decode_freeze_safe_mask", &self.decode_freeze_safe_mask)
.field("device", &self.buffer().device())
.field("device_ptr", &self.device_ptr())
.field("transfer_stats", &self.transfer_stats)
.finish()
}
}
impl Drop for DeviceIoBinding {
fn drop(&mut self) {
if let Some(buffer) = self.buffer.take() {
let mut safe_to_release = true;
if let Err(error) = self.allocator.sync() {
safe_to_release = false;
eprintln!(
"[onnx-runtime-session] device binding drop could not synchronize deferred \
work before release: {error}"
);
}
if safe_to_release && let Some(token) = self.device_validation {
match self
.allocator
.consume_device_validation_error(self.validation_registration(), token)
{
Ok(0) => {}
Ok(flags) => eprintln!(
"[onnx-runtime-session] device binding drop consumed its deferred \
validation failure (flags=0x{flags:x})"
),
Err(error) => {
safe_to_release = false;
eprintln!(
"[onnx-runtime-session] device binding drop could not consume its \
deferred validation: {error}"
);
}
}
}
if let Some(token) = self.device_graph_token.take()
&& let Err(error) = self.allocator.reset_owned_device_graph(token)
{
safe_to_release = false;
eprintln!(
"[onnx-runtime-session] device binding drop could not retire its captured \
graph generation: {error}"
);
}
if safe_to_release {
let _ = self.allocator.deallocate(buffer);
} else {
eprintln!(
"[onnx-runtime-session] quarantining device binding allocation after failed \
drop cleanup"
);
drop(buffer);
}
}
if let Some(registration) = self.validation_registration.as_mut() {
let owner = registration.owner();
if let Err(error) = self
.allocator
.unregister_device_validation_owner(registration)
{
eprintln!(
"[onnx-runtime-session] device binding drop could not unregister validation \
owner {}: {error}",
owner.get()
);
} else {
self.validation_registration = None;
}
}
}
}
impl Tensor {
pub(crate) fn copy_from_device_buffer(
source_allocator: &Arc<dyn ExecutionProvider>,
source: &DeviceBuffer,
dtype: DataType,
shape: Vec<usize>,
) -> Result<Self> {
let mut tensor = Self::allocate_cpu(dtype, shape)?;
let destination = tensor.buffer.as_mut().ok_or_else(|| {
SessionError::Internal("new host output tensor has no backing buffer".into())
})?;
if source.len() != destination.len() {
return Err(SessionError::Internal(format!(
"device output has {} bytes but its host tensor allocation has {}",
source.len(),
destination.len()
)));
}
let host = unsafe {
std::slice::from_raw_parts_mut(destination.as_mut_ptr().cast::<u8>(), destination.len())
};
source_allocator.copy_to_host(source, host)?;
Ok(tensor)
}
pub(crate) fn allocate_cpu(dtype: DataType, shape: Vec<usize>) -> Result<Self> {
let numel = shape.iter().try_fold(1usize, |product, &dim| {
product.checked_mul(dim).ok_or_else(|| {
SessionError::Internal(format!(
"Tensor::allocate_cpu: element count overflows for shape {shape:?}"
))
})
})?;
let bytes = dtype.checked_storage_bytes(numel).ok_or_else(|| {
SessionError::Internal(format!(
"Tensor::allocate_cpu: byte count overflows for shape {shape:?} dtype {dtype:?}"
))
})?;
let allocator: Arc<dyn ExecutionProvider> = shared_cpu_ep();
let layout = TensorLayout::contiguous();
let buffer = allocator.allocate(bytes.max(1), layout.alignment)?;
Ok(Self {
dtype,
shape,
layout,
device: buffer.device(),
buffer: Some(buffer),
allocator,
import_guard: None,
})
}
pub(crate) fn copy_from_host_at(&mut self, offset: usize, bytes: &[u8]) -> Result<()> {
let buffer = self.buffer.as_mut().ok_or_else(|| {
SessionError::Internal("Tensor buffer is unavailable for writing".to_string())
})?;
self.allocator.copy_from_host_at(bytes, buffer, offset)?;
Ok(())
}
pub(crate) fn from_raw_in(
allocator: Arc<dyn ExecutionProvider>,
dtype: DataType,
shape: Vec<usize>,
bytes: &[u8],
) -> Result<Self> {
let expected =
checked_expected_bytes(dtype, &shape).ok_or_else(|| SessionError::ShapeOverflow {
value: "Tensor::from_raw_in".to_string(),
dims: shape.clone(),
})?;
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)?;
allocator.copy_from_host(bytes, &mut buffer)?;
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 zeros(dtype: DataType, shape: Vec<usize>) -> Result<Self> {
let allocator = shared_cpu_ep();
let numel: usize = shape.iter().product();
let expected = dtype.storage_bytes(numel);
let layout = TensorLayout::contiguous();
let mut buffer = allocator.allocate(expected.max(1), layout.alignment)?;
assert!(
buffer.device().is_host_accessible(),
"zeros on non-host device {:?}",
buffer.device()
);
if expected > 0 {
let dst = buffer.as_mut_ptr() as *mut u8;
unsafe { std::ptr::write_bytes(dst, 0, expected) };
}
Ok(Self::from_owned_buffer(allocator, dtype, shape, buffer))
}
pub(crate) fn from_host_fill(
dtype: DataType,
shape: Vec<usize>,
fill: impl FnOnce(&mut [u8]),
) -> Result<Self> {
let expected =
checked_expected_bytes(dtype, &shape).ok_or_else(|| SessionError::ShapeOverflow {
value: "Tensor::from_host_fill".to_string(),
dims: shape.clone(),
})?;
let allocator = shared_cpu_ep();
let layout = TensorLayout::contiguous();
let mut buffer = allocator.allocate(expected.max(1), layout.alignment)?;
assert!(
buffer.device().is_host_accessible(),
"from_host_fill on non-host device {:?}",
buffer.device()
);
if expected > 0 {
let dst =
unsafe { std::slice::from_raw_parts_mut(buffer.as_mut_ptr() as *mut u8, expected) };
fill(dst);
}
Ok(Self::from_owned_buffer(allocator, dtype, shape, buffer))
}
pub(crate) fn from_owned_buffer(
allocator: Arc<dyn ExecutionProvider>,
dtype: DataType,
shape: Vec<usize>,
buffer: DeviceBuffer,
) -> Self {
debug_assert!(
!buffer.is_borrowed(),
"from_owned_buffer requires an owned buffer"
);
debug_assert_eq!(
buffer.len(),
dtype.storage_bytes(shape.iter().product::<usize>()).max(1),
"from_owned_buffer size mismatch for shape {shape:?} dtype {dtype:?}",
);
let device = buffer.device();
Self {
dtype,
shape,
layout: TensorLayout::contiguous(),
device,
buffer: Some(buffer),
allocator,
import_guard: None,
}
}
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(crate) fn into_shared_parts(
mut self,
) -> (Arc<SharedTensorBuffer>, DataType, Vec<usize>, TensorLayout) {
let buffer = self
.buffer
.take()
.expect("Tensor buffer taken only by into_shared_parts or Drop");
let storage = SharedTensorBuffer::with_guard(
Arc::clone(&self.allocator),
buffer,
self.import_guard.take(),
);
let dtype = self.dtype;
let shape = std::mem::take(&mut self.shape);
let layout = std::mem::take(&mut self.layout);
(storage, dtype, shape, layout)
}
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(())
}
pub fn try_clone(&self) -> SequenceResult<Tensor> {
const OP: &str = "Tensor::try_clone";
let shape = clone_shape(OP, &self.shape)?;
if checked_expected_bytes(self.dtype, &shape).is_none() {
return Err(SequenceError::ShapeOverflow {
op: OP,
context: "tensor byte count",
shape,
});
}
Self::from_raw_in(
Arc::clone(&self.allocator),
self.dtype,
shape,
self.as_bytes(),
)
.map_err(|source| SequenceError::TensorCreation { op: OP, source })
}
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");
self.allocator.copy_from_host(bytes, buffer)?;
Ok(())
}
pub fn try_as_slice_f32(&self) -> Option<&[f32]> {
assert_eq!(
self.dtype,
DataType::Float32,
"try_as_slice_f32 on non-f32 tensor"
);
if cfg!(target_endian = "big") {
return None;
}
let bytes = self.as_bytes();
if bytes.as_ptr().align_offset(std::mem::align_of::<f32>()) != 0 {
return None;
}
Some(unsafe { std::slice::from_raw_parts(bytes.as_ptr().cast::<f32>(), self.numel()) })
}
pub fn try_as_slice_u16(&self) -> Option<&[u16]> {
assert!(
matches!(self.dtype, DataType::Float16 | DataType::BFloat16),
"try_as_slice_u16 on non-16-bit float tensor"
);
if cfg!(target_endian = "big") {
return None;
}
let bytes = self.as_bytes();
if bytes.as_ptr().align_offset(std::mem::align_of::<u16>()) != 0 {
return None;
}
Some(unsafe { std::slice::from_raw_parts(bytes.as_ptr().cast::<u16>(), self.numel()) })
}
pub fn to_vec_f32(&self) -> Vec<f32> {
assert_eq!(
self.dtype,
DataType::Float32,
"to_vec_f32 on non-f32 tensor"
);
self.try_as_slice_f32().map_or_else(
|| {
read_vec_le::<f32>(self.as_bytes())
.expect("Float32 tensor storage length must be a multiple of 4 bytes")
},
<[f32]>::to_vec,
)
}
pub fn to_vec_i64(&self) -> Vec<i64> {
assert_eq!(self.dtype, DataType::Int64, "to_vec_i64 on non-i64 tensor");
read_vec_le(self.as_bytes())
.expect("Int64 tensor storage length must be a multiple of 8 bytes")
}
}
impl Clone for Tensor {
fn clone(&self) -> Self {
self.try_clone()
.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 from_raw_rejects_geometry_overflow() {
let error = Tensor::from_raw(DataType::Float32, vec![usize::MAX, 2], &[])
.expect_err("overflowing tensor geometry must be rejected");
assert!(matches!(error, SessionError::ShapeOverflow { .. }));
}
#[test]
fn try_clone_deep_copies_shape_and_data() {
let tensor = Tensor::from_f32(&[2, 2], &[1.0, 2.0, 3.0, 4.0]).unwrap();
let cloned = tensor.try_clone().unwrap();
assert_eq!(cloned.dtype, tensor.dtype);
assert_eq!(cloned.shape, tensor.shape);
assert_eq!(cloned.layout, tensor.layout);
assert_eq!(cloned.as_bytes(), tensor.as_bytes());
assert_ne!(cloned.device_ptr(), tensor.device_ptr());
}
#[test]
fn device_binding_allocation_rejects_byte_overflow() {
let element_count = usize::MAX / 4;
let error = DeviceIoBinding::allocate(
shared_cpu_ep(),
DeviceBindingSpec {
input_name: "huge".into(),
bind_input: true,
output_name: None,
dtype: DataType::Float64,
physical_shape: vec![element_count],
logical_shape: vec![element_count],
expose_logical_input_shape: false,
decode_freeze_safe_mask: false,
fixed_physical_strides: false,
allocation_bytes: None,
committed_ranges: None,
},
)
.expect_err("overflowing device binding byte count must be rejected");
assert!(matches!(error, SessionError::ShapeOverflow { .. }));
}
#[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.try_as_slice_f32().unwrap(), &[1.0, 2.0, 3.0, 4.0]);
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"
);
}
#[test]
fn borrows_aligned_half_storage_as_raw_bits() {
let bits = [0x3c00u16, 0x4000];
let bytes = bits
.iter()
.flat_map(|value| value.to_le_bytes())
.collect::<Vec<_>>();
let tensor = Tensor::from_raw(DataType::Float16, vec![2], &bytes).unwrap();
assert_eq!(tensor.try_as_slice_u16().unwrap(), bits);
}
#[test]
fn exposes_logical_is_static_while_dynamic_tracks_current_shape() {
let mut logical_mask = DeviceIoBinding::allocate(
shared_cpu_ep(),
DeviceBindingSpec {
input_name: "attention_mask".into(),
bind_input: true,
output_name: None,
dtype: DataType::Int64,
physical_shape: vec![1, 4096],
logical_shape: vec![1, 4096],
expose_logical_input_shape: true,
decode_freeze_safe_mask: false,
fixed_physical_strides: false,
allocation_bytes: None,
committed_ranges: None,
},
)
.unwrap();
assert!(logical_mask.exposes_logical_input_shape());
assert!(!logical_mask.has_dynamic_logical_input_shape());
logical_mask.set_logical_shape(vec![1, 5]).unwrap();
assert!(logical_mask.exposes_logical_input_shape());
assert!(logical_mask.has_dynamic_logical_input_shape());
assert_eq!(logical_mask.kernel_input_shape(), &[1, 5]);
let mut physical_mask = DeviceIoBinding::allocate(
shared_cpu_ep(),
DeviceBindingSpec {
input_name: "attention_mask".into(),
bind_input: true,
output_name: None,
dtype: DataType::Int64,
physical_shape: vec![1, 4096],
logical_shape: vec![1, 5],
expose_logical_input_shape: false,
decode_freeze_safe_mask: false,
fixed_physical_strides: false,
allocation_bytes: None,
committed_ranges: None,
},
)
.unwrap();
assert!(!physical_mask.exposes_logical_input_shape());
assert!(!physical_mask.has_dynamic_logical_input_shape());
assert_eq!(physical_mask.kernel_input_shape(), &[1, 4096]);
physical_mask.set_logical_shape(vec![1, 4096]).unwrap();
assert!(!physical_mask.exposes_logical_input_shape());
}
#[test]
fn fixed_stride_binding_keeps_logical_shape_without_capture_decline() {
let binding = DeviceIoBinding::allocate(
shared_cpu_ep(),
DeviceBindingSpec {
input_name: "past_records".into(),
bind_input: true,
output_name: Some("present_records".into()),
dtype: DataType::Uint8,
physical_shape: vec![3, 65, 583],
logical_shape: vec![3, 2, 583],
expose_logical_input_shape: true,
decode_freeze_safe_mask: false,
fixed_physical_strides: true,
allocation_bytes: None,
committed_ranges: None,
},
)
.unwrap();
assert_eq!(binding.kernel_input_shape(), &[3, 2, 583]);
assert!(binding.fixed_physical_strides());
assert!(!binding.has_dynamic_logical_input_shape());
}
#[test]
fn a_zeroed_tensor_is_zero_even_over_dirty_memory() {
let poison = Tensor::from_raw(DataType::Float32, vec![2, 8], &[0xABu8; 64])
.expect("a poisoned tensor");
drop(poison);
let zeroed = Tensor::zeros(DataType::Float32, vec![2, 8]).expect("a zeroed tensor");
let values = zeroed.to_vec_f32();
assert_eq!(values.len(), 16);
assert!(
values.iter().all(|value| *value == 0.0),
"zeros() returned {values:?}"
);
}
#[test]
fn a_zeroed_tensor_of_no_elements_is_valid() {
let empty = Tensor::zeros(DataType::Float32, vec![1, 8, 0, 4]).expect("allocatable");
assert_eq!(empty.shape, vec![1, 8, 0, 4]);
assert!(empty.to_vec_f32().is_empty());
}
}