use std::any::Any;
use std::ffi::c_void;
use std::ptr::NonNull;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::epcontext::EpContext;
use crate::error::{EpError, Result};
use crate::kernel::{Kernel, KernelMatch};
use crate::weight::ExecutionProviderCapabilities;
use onnx_runtime_ir::{
DataType, DeviceId, DeviceType, Graph, GraphView, Node, NodeId, NodeIndex, Shape, TensorLayout,
};
use onnx_runtime_memory_governor::{
AllocationIdentity, ManagedAllocation, MemoryLease, MemoryRole, OwningAllocation,
ProviderContextIdentity,
};
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct EpId(pub u32);
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct ExecutorInstanceId(u64);
impl ExecutorInstanceId {
pub const UNSCOPED: Self = Self(0);
pub fn get(self) -> u64 {
self.0
}
#[doc(hidden)]
pub const fn from_raw(id: u64) -> Self {
Self(id)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ExecutorArtifactProviderId(u64);
impl ExecutorArtifactProviderId {
pub const UNSCOPED: Self = Self(0);
#[doc(hidden)]
pub const fn from_raw(id: u64) -> Self {
Self(id)
}
pub const fn get(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum ExecutorRouteResidencyConfig {
#[default]
Disabled,
Enabled,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ExecutorKernelScope {
#[default]
Unscoped,
Required,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ExecutorArtifactPolicy {
provider: ExecutorArtifactProviderId,
device: DeviceId,
route_residency: ExecutorRouteResidencyConfig,
}
impl ExecutorArtifactPolicy {
pub const fn new(
provider: ExecutorArtifactProviderId,
device: DeviceId,
route_residency: ExecutorRouteResidencyConfig,
) -> Self {
Self {
provider,
device,
route_residency,
}
}
pub const fn provider(self) -> ExecutorArtifactProviderId {
self.provider
}
pub const fn device(self) -> DeviceId {
self.device
}
pub const fn route_residency(self) -> ExecutorRouteResidencyConfig {
self.route_residency
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ExecutorArtifactGeneration(u64);
impl ExecutorArtifactGeneration {
#[doc(hidden)]
pub const fn from_raw(generation: u64) -> Self {
Self(generation)
}
pub const fn get(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ExecutorArtifactReadinessEpoch(u64);
impl ExecutorArtifactReadinessEpoch {
pub const INITIAL: Self = Self(0);
pub const fn new(epoch: u64) -> Self {
Self(epoch)
}
pub const fn get(self) -> u64 {
self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ExecutorArtifactPending {
ProducerUnavailable { node: NodeId },
ProviderReadiness { reason: String },
}
impl ExecutorArtifactPending {
pub fn reason(&self) -> String {
match self {
Self::ProducerUnavailable { node } => {
format!("producer for graph node {node:?} is not registered")
}
Self::ProviderReadiness { reason } => reason.clone(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ExecutorArtifactState {
Disabled,
Declined,
Required,
Pending(ExecutorArtifactPending),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExecutorArtifactReport {
provider: ExecutorArtifactProviderId,
executor: ExecutorInstanceId,
generation: ExecutorArtifactGeneration,
readiness: ExecutorArtifactReadinessEpoch,
state: ExecutorArtifactState,
}
impl ExecutorArtifactReport {
pub fn observed(
provider: ExecutorArtifactProviderId,
executor: ExecutorInstanceId,
generation: ExecutorArtifactGeneration,
readiness: ExecutorArtifactReadinessEpoch,
state: ExecutorArtifactState,
) -> Self {
Self {
provider,
executor,
generation,
readiness,
state,
}
}
pub const fn provider(&self) -> ExecutorArtifactProviderId {
self.provider
}
pub const fn executor(&self) -> ExecutorInstanceId {
self.executor
}
pub const fn generation(&self) -> ExecutorArtifactGeneration {
self.generation
}
pub const fn readiness(&self) -> ExecutorArtifactReadinessEpoch {
self.readiness
}
pub fn into_state(self) -> ExecutorArtifactState {
self.state
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
pub enum ArgmaxTieBreak {
#[default]
LowestIndex,
HighestIndex,
}
impl ArgmaxTieBreak {
#[must_use]
pub fn select_last_index(self) -> bool {
matches!(self, ArgmaxTieBreak::HighestIndex)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
pub enum DeviceGraphSlot {
#[default]
Primary,
Verify,
}
impl DeviceGraphSlot {
pub const COUNT: usize = 2;
#[inline]
pub const fn index(self) -> usize {
match self {
DeviceGraphSlot::Primary => 0,
DeviceGraphSlot::Verify => 1,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct DeviceGraphOwner(u64);
impl DeviceGraphOwner {
pub fn new() -> Self {
static NEXT_OWNER: AtomicU64 = AtomicU64::new(1);
let owner = NEXT_OWNER
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |next| {
next.checked_add(1)
})
.unwrap_or_else(|_| {
panic!(
"device validation owner identity space exhausted; refusing to wrap and \
create an ABA collision"
)
});
Self(owner)
}
pub const fn get(self) -> u64 {
self.0
}
}
impl Default for DeviceGraphOwner {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct DeviceValidationOwner(u64);
impl DeviceValidationOwner {
pub fn new() -> Self {
static NEXT_OWNER: AtomicU64 = AtomicU64::new(1);
let owner = NEXT_OWNER
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |next| {
next.checked_add(1)
})
.unwrap_or_else(|_| {
panic!(
"device validation owner identity space exhausted; refusing to wrap and \
create an ABA collision"
)
});
Self(owner)
}
pub const fn get(self) -> u64 {
self.0
}
}
impl Default for DeviceValidationOwner {
fn default() -> Self {
Self::new()
}
}
pub struct DeviceValidationRegistration {
owner: DeviceValidationOwner,
state: Box<dyn Any + Send + Sync>,
}
pub trait ExecutorArtifactUseGuard: Send + Sync {}
pub trait ExecutorArtifactRequirementState: Send + Sync {
fn acquire_use(&self) -> Result<Box<dyn ExecutorArtifactUseGuard>>;
}
impl DeviceValidationRegistration {
pub fn new<T>(owner: DeviceValidationOwner, state: T) -> Self
where
T: Any + Send + Sync,
{
Self {
owner,
state: Box::new(state),
}
}
pub const fn owner(&self) -> DeviceValidationOwner {
self.owner
}
#[doc(hidden)]
pub fn state<T: Any>(&self) -> Option<&T> {
self.state.downcast_ref()
}
#[doc(hidden)]
pub fn state_mut<T: Any>(&mut self) -> Option<&mut T> {
self.state.downcast_mut()
}
}
impl std::fmt::Debug for DeviceValidationRegistration {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("DeviceValidationRegistration")
.field("owner", &self.owner)
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct DeviceValidationToken {
owner: DeviceValidationOwner,
generation: u64,
}
impl DeviceValidationToken {
pub const fn new(owner: DeviceValidationOwner, generation: u64) -> Self {
Self { owner, generation }
}
pub const fn owner(self) -> DeviceValidationOwner {
self.owner
}
pub const fn generation(self) -> u64 {
self.generation
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct DeviceGraphToken {
owner: DeviceGraphOwner,
slot: DeviceGraphSlot,
generation: u64,
}
impl DeviceGraphToken {
pub const fn new(owner: DeviceGraphOwner, slot: DeviceGraphSlot, generation: u64) -> Self {
Self {
owner,
slot,
generation,
}
}
pub const fn owner(self) -> DeviceGraphOwner {
self.owner
}
pub const fn slot(self) -> DeviceGraphSlot {
self.slot
}
pub const fn generation(self) -> u64 {
self.generation
}
}
#[derive(Clone, Debug, Default)]
pub struct EpConfig {
pub options: std::collections::HashMap<String, String>,
}
#[derive(Debug)]
pub struct DeviceBuffer {
device: DeviceId,
size: usize,
align: usize,
ptr: NonNull<c_void>,
owner: BufferOwner,
}
#[derive(Debug)]
enum BufferOwner {
Owned,
Bound(Box<OwningAllocation>),
Managed(Box<ManagedAllocation>),
Borrowed,
BorrowedMut,
}
impl DeviceBuffer {
pub unsafe fn from_raw_parts(
ptr: *mut c_void,
device: DeviceId,
size: usize,
align: usize,
) -> Self {
debug_assert!(align.is_power_of_two(), "alignment must be a power of two");
Self {
device,
size,
align,
ptr: NonNull::new(ptr).expect("DeviceBuffer::from_raw_parts: null pointer"),
owner: BufferOwner::Owned,
}
}
pub unsafe fn from_borrowed_parts(
ptr: *mut c_void,
device: DeviceId,
size: usize,
align: usize,
) -> Self {
debug_assert!(align.is_power_of_two(), "alignment must be a power of two");
Self {
device,
size,
align,
ptr: NonNull::new(ptr).expect("DeviceBuffer::from_borrowed_parts: null pointer"),
owner: BufferOwner::Borrowed,
}
}
pub unsafe fn from_borrowed_mut_parts(
ptr: *mut c_void,
device: DeviceId,
size: usize,
align: usize,
) -> Option<Self> {
debug_assert!(align.is_power_of_two(), "alignment must be a power of two");
Some(Self {
device,
size,
align,
ptr: NonNull::new(ptr)?,
owner: BufferOwner::BorrowedMut,
})
}
pub fn is_borrowed(&self) -> bool {
matches!(self.owner, BufferOwner::Borrowed | BufferOwner::BorrowedMut)
}
pub fn from_owning_allocation(owner: OwningAllocation, device: DeviceId) -> Self {
let ptr = owner.as_ptr();
let size = owner.len();
let align = owner.alignment().max(1);
Self {
device,
size,
align,
ptr: NonNull::new(ptr.as_ptr().cast::<c_void>())
.expect("an owning allocation holds a non-null address"),
owner: BufferOwner::Bound(Box::new(owner)),
}
}
pub fn from_managed_allocation(owner: ManagedAllocation, device: DeviceId) -> Self {
let ptr = owner.as_ptr();
let size = owner.len();
let align = owner.alignment().max(1);
Self {
device,
size,
align,
ptr: NonNull::new(ptr.as_ptr().cast::<c_void>())
.expect("a managed allocation holds a non-null address"),
owner: BufferOwner::Managed(Box::new(owner)),
}
}
pub fn is_bound(&self) -> bool {
matches!(self.owner, BufferOwner::Bound(_) | BufferOwner::Managed(_))
}
pub fn bound_owner(&self) -> Option<&OwningAllocation> {
match &self.owner {
BufferOwner::Bound(owner) => Some(owner),
BufferOwner::Managed(owner) => Some(owner.owner_ref()),
_ => None,
}
}
pub fn managed_owner(&self) -> Option<&ManagedAllocation> {
match &self.owner {
BufferOwner::Managed(owner) => Some(owner),
_ => None,
}
}
pub fn managed_settlement_wait(
&self,
) -> Option<onnx_runtime_memory_governor::AllocationSettlementWait> {
self.managed_owner().map(ManagedAllocation::settlement_wait)
}
pub fn into_bound_owner(self) -> std::result::Result<BoundBufferOwnership, Self> {
match self.owner {
BufferOwner::Bound(owner) => Ok(BoundBufferOwnership::Binding(*owner)),
BufferOwner::Managed(owner) => Ok(BoundBufferOwnership::Managed(*owner)),
owner => Err(Self { owner, ..self }),
}
}
pub fn into_bound_ownership(self) -> std::result::Result<BoundBufferOwnership, Self> {
self.into_bound_owner()
}
pub fn device(&self) -> DeviceId {
self.device
}
pub fn len(&self) -> usize {
self.size
}
pub fn is_empty(&self) -> bool {
self.size == 0
}
pub fn alignment(&self) -> usize {
self.align
}
pub fn as_ptr(&self) -> *const c_void {
self.ptr.as_ptr()
}
pub fn as_mut_ptr(&mut self) -> *mut c_void {
self.ptr.as_ptr()
}
pub fn into_raw(self) -> *mut c_void {
assert!(
!self.is_bound(),
"DeviceBuffer::into_raw: this buffer carries a binding-issued owning allocation, so \
returning the raw pointer alone would bypass binding-identity and allocation-\
generation validation on release. Use into_raw_with_owner or into_bound_owner."
);
self.ptr.as_ptr()
}
pub fn into_raw_with_owner(self) -> (*mut c_void, Option<BoundBufferOwnership>) {
let ptr = self.ptr.as_ptr();
match self.owner {
BufferOwner::Bound(owner) => (ptr, Some(BoundBufferOwnership::Binding(*owner))),
BufferOwner::Managed(owner) => (ptr, Some(BoundBufferOwnership::Managed(*owner))),
_ => (ptr, None),
}
}
pub fn into_raw_with_bound_ownership(self) -> (*mut c_void, Option<BoundBufferOwnership>) {
let ptr = self.ptr.as_ptr();
match self.owner {
BufferOwner::Bound(owner) => (ptr, Some(BoundBufferOwnership::Binding(*owner))),
BufferOwner::Managed(owner) => (ptr, Some(BoundBufferOwnership::Managed(*owner))),
_ => (ptr, None),
}
}
}
#[derive(Debug)]
pub enum BoundBufferOwnership {
Binding(OwningAllocation),
Managed(ManagedAllocation),
}
impl BoundBufferOwnership {
pub fn owner(&self) -> &OwningAllocation {
match self {
Self::Binding(owner) => owner,
Self::Managed(owner) => owner.owner_ref(),
}
}
}
#[derive(Debug)]
pub struct WorkspaceAllocation {
buffer: DeviceBuffer,
lease: Option<MemoryLease>,
}
static QUARANTINED_WORKSPACE_LEASES: std::sync::OnceLock<std::sync::Mutex<Vec<MemoryLease>>> =
std::sync::OnceLock::new();
fn quarantine_failed_workspace_lease(lease: MemoryLease) {
eprintln!(
"execution provider workspace deallocation failed before physical release was proven; \
retaining its {} byte {:?} lease in compatibility quarantine",
lease.bytes(),
lease.tier()
);
QUARANTINED_WORKSPACE_LEASES
.get_or_init(|| std::sync::Mutex::new(Vec::new()))
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.push(lease);
}
#[cfg(test)]
fn quarantined_workspace_lease_count() -> usize {
QUARANTINED_WORKSPACE_LEASES
.get_or_init(|| std::sync::Mutex::new(Vec::new()))
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.len()
}
impl WorkspaceAllocation {
pub fn new(buffer: DeviceBuffer, lease: Option<MemoryLease>) -> Self {
Self { buffer, lease }
}
pub fn buffer(&self) -> &DeviceBuffer {
&self.buffer
}
pub fn buffer_mut(&mut self) -> &mut DeviceBuffer {
&mut self.buffer
}
pub fn into_parts(self) -> (DeviceBuffer, Option<MemoryLease>) {
(self.buffer, self.lease)
}
}
impl std::ops::Deref for WorkspaceAllocation {
type Target = DeviceBuffer;
fn deref(&self) -> &Self::Target {
&self.buffer
}
}
impl std::ops::DerefMut for WorkspaceAllocation {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.buffer
}
}
unsafe impl Send for DeviceBuffer {}
unsafe impl Sync for DeviceBuffer {}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Fence {
pub id: u64,
}
impl Fence {
pub fn signalled() -> Self {
Self { id: 0 }
}
pub fn new(id: u64) -> Self {
Self { id }
}
pub fn is_signalled(&self) -> bool {
self.id == 0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CaptureRegionShapeStatus {
pub inputs_resolved: bool,
pub outputs_resolved: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StructuralCaptureDecline {
HostControlFlowOrSequence,
UnresolvedOutputShape,
UnresolvedInputShape,
}
impl StructuralCaptureDecline {
pub const fn reason(self) -> &'static str {
match self {
Self::HostControlFlowOrSequence => {
"control-flow and sequence nodes are not device-graph capturable"
}
Self::UnresolvedOutputShape => {
"data-dependent output shape was unresolved before capture"
}
Self::UnresolvedInputShape => {
"data-dependent input shape was unresolved before capture"
}
}
}
}
pub trait HostToDeviceCopier: Send + Sync {
unsafe fn copy_host_to_device(&self, src: &[u8], dst: *mut c_void) -> Result<()>;
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct RawDeviceAllocationSiteStats {
pub file: &'static str,
pub line: u32,
pub requests: u64,
pub requested_bytes: u64,
pub driver_allocations: u64,
pub driver_bytes: u64,
pub pool_hits: u64,
pub pool_hit_bytes: u64,
}
pub trait SealedDeviceAllocation: Send + Sync {
fn ptr(&self) -> crate::DevicePtr;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn device(&self) -> DeviceId;
fn provider_context(&self) -> ProviderContextIdentity;
fn allocation_identity(&self) -> AllocationIdentity;
fn runtime_identity(&self) -> usize;
}
pub trait ExecutionProvider: Send + Sync {
fn name(&self) -> &str;
fn device_type(&self) -> DeviceType;
fn device_id(&self) -> DeviceId;
fn memory_vendor_id(&self) -> u32 {
0
}
fn host_to_device_copier(&self) -> Option<std::sync::Arc<dyn HostToDeviceCopier>> {
None
}
fn capabilities(&self) -> ExecutionProviderCapabilities {
ExecutionProviderCapabilities::stock()
}
fn runtime_identity(&self) -> Option<usize> {
None
}
fn provider_context_identity(&self) -> Option<ProviderContextIdentity> {
None
}
fn prepares_immutable_constant(&self, node: &Node, input_idx: usize) -> bool {
let _ = (node, input_idx);
false
}
fn upload_sealed_constant(
&self,
bytes: &[u8],
alignment: usize,
) -> Result<Arc<dyn SealedDeviceAllocation>> {
let _ = (bytes, alignment);
Err(EpError::KernelFailed(format!(
"{} does not support sealed constant admission",
self.name()
)))
}
fn page_lazy_weight(
&self,
key: u64,
weight: &crate::LazyWeight,
source: &dyn crate::MmapRegionSource,
) -> Result<Option<crate::PagedWeight>> {
let _ = (key, weight, source);
Ok(None)
}
fn page_lazy_weight_for_executor(
&self,
_executor: ExecutorInstanceId,
key: u64,
weight: &crate::LazyWeight,
source: &dyn crate::MmapRegionSource,
) -> Result<Option<crate::PagedWeight>> {
self.page_lazy_weight(key, weight, source)
}
fn acquire_routed_residency(
&self,
key: u64,
requirement: crate::RoutedResidencyRequirement,
catalog: &onnx_runtime_loader::WeightRegionCatalog,
) -> Result<Option<Box<dyn crate::RoutedResidencyGuardHandle>>> {
let _ = (key, requirement, catalog);
Ok(None)
}
fn acquire_routed_residency_for_executor(
&self,
_executor: ExecutorInstanceId,
key: u64,
requirement: crate::RoutedResidencyRequirement,
catalog: &onnx_runtime_loader::WeightRegionCatalog,
) -> Result<Option<Box<dyn crate::RoutedResidencyGuardHandle>>> {
self.acquire_routed_residency(key, requirement, catalog)
}
fn prefetch_lazy_weight(
&self,
key: u64,
weight: &crate::LazyWeight,
source: &dyn crate::MmapRegionSource,
) -> Result<bool> {
let _ = (key, weight, source);
Ok(false)
}
fn prefetch_lazy_weight_for_executor(
&self,
_executor: ExecutorInstanceId,
key: u64,
weight: &crate::LazyWeight,
source: &dyn crate::MmapRegionSource,
) -> Result<bool> {
self.prefetch_lazy_weight(key, weight, source)
}
fn initialize(&mut self, config: &EpConfig) -> Result<()>;
fn shutdown(&mut self) -> Result<()>;
fn supports_op(
&self,
op: &Node,
opset: u64,
shapes: &[Shape],
input_dtypes: &[DataType],
layouts: &[TensorLayout],
) -> KernelMatch;
fn supports_node(&self, view: &GraphView<'_>, node: NodeIndex, opset: u64) -> KernelMatch {
let inputs = view.node_inputs(node);
let shapes = inputs
.iter()
.map(|input| {
input
.map(|value| view.value(value).shape.clone())
.unwrap_or_default()
})
.collect::<Vec<_>>();
let input_dtypes = inputs
.iter()
.map(|input| {
input
.map(|value| view.value(value).dtype)
.unwrap_or(DataType::Undefined)
})
.collect::<Vec<_>>();
let layouts = inputs
.iter()
.map(|input| {
input
.map(|value| view.value(value).layout.clone())
.unwrap_or_else(TensorLayout::contiguous)
})
.collect::<Vec<_>>();
self.supports_op(view.node(node), opset, &shapes, &input_dtypes, &layouts)
}
fn get_kernel(&self, op: &Node, shapes: &[Vec<usize>], opset: u64) -> Result<Box<dyn Kernel>>;
fn get_kernel_for_executor(
&self,
provider: ExecutorArtifactProviderId,
executor: ExecutorInstanceId,
generation: ExecutorArtifactGeneration,
op: &Node,
shapes: &[Vec<usize>],
opset: u64,
) -> Result<Box<dyn Kernel>> {
let _ = (provider, executor, generation);
self.get_kernel(op, shapes, opset)
}
fn executor_kernel_scope(&self, _op: &Node) -> ExecutorKernelScope {
ExecutorKernelScope::Unscoped
}
fn plan_capture_region(
&self,
node: &Node,
shape_status: CaptureRegionShapeStatus,
) -> Option<StructuralCaptureDecline> {
if is_control_flow_or_sequence(node) {
return Some(StructuralCaptureDecline::HostControlFlowOrSequence);
}
if !shape_status.outputs_resolved {
return Some(StructuralCaptureDecline::UnresolvedOutputShape);
}
if !shape_status.inputs_resolved {
return Some(StructuralCaptureDecline::UnresolvedInputShape);
}
None
}
fn allocate(&self, size: usize, alignment: usize) -> Result<DeviceBuffer>;
fn allocate_with_mapped_growth(
&self,
size: usize,
alignment: usize,
grant: onnx_runtime_memory_governor::MappedGrowthGrant,
) -> Result<DeviceBuffer> {
let newly_mapped_bytes = self.mapped_bytes_for_allocation(size, alignment)?;
let allocation = self.allocate(size, alignment)?;
if let Err(error) = grant.commit_bytes(newly_mapped_bytes) {
let _ = self.deallocate(allocation);
return Err(EpError::Memory(error));
}
Ok(allocation)
}
fn allocate_workspace(
&self,
size: usize,
alignment: usize,
role: MemoryRole,
) -> Result<WorkspaceAllocation> {
let target_mapped = self.mapped_bytes_for_allocation(size, alignment)?;
let mut grant = self.prepare_mapped_growth(target_mapped, role)?;
let lease = match self.reserve_workspace(size as u64, role) {
Ok(lease) => lease,
Err(error) => {
drop(grant);
return Err(error);
}
};
let buffer = match grant.take() {
Some(grant) => self.allocate_with_mapped_growth(size, alignment, grant)?,
None => self.allocate(size, alignment)?,
};
Ok(WorkspaceAllocation::new(buffer, lease))
}
fn replace_workspace(
&self,
old: Option<WorkspaceAllocation>,
size: usize,
alignment: usize,
role: MemoryRole,
) -> Result<WorkspaceAllocation> {
if let Some(old) = old {
self.deallocate_workspace(old)?;
}
self.allocate_workspace(size, alignment, role)
}
fn allocate_committed(
&self,
size: usize,
alignment: usize,
committed_ranges: &[std::ops::Range<usize>],
) -> Result<DeviceBuffer> {
let _ = committed_ranges;
self.allocate(size, alignment)
}
fn commit_allocation_range(
&self,
buffer: &DeviceBuffer,
offset: usize,
bytes: usize,
) -> Result<()> {
let _ = (buffer, offset, bytes);
Ok(())
}
fn commit_allocation_ranges(&self, ranges: &[(&DeviceBuffer, usize, usize)]) -> Result<()> {
for &(buffer, offset, bytes) in ranges {
self.commit_allocation_range(buffer, offset, bytes)?;
}
Ok(())
}
fn commit_allocation_ranges_with_mapped_growth(
&self,
ranges: &[(&DeviceBuffer, usize, usize)],
grant: &mut onnx_runtime_memory_governor::MappedGrowthGrant,
) -> Result<u64> {
let _ = grant;
self.commit_allocation_ranges(ranges)?;
self.mapped_bytes_for_allocation_ranges(ranges)
}
fn mapped_bytes_for_allocation_ranges(
&self,
ranges: &[(&DeviceBuffer, usize, usize)],
) -> Result<u64> {
Ok(ranges.iter().fold(0_u64, |total, (_, _, bytes)| {
total.saturating_add(*bytes as u64)
}))
}
fn decommit_allocation_range(
&self,
buffer: &DeviceBuffer,
offset: usize,
bytes: usize,
) -> Result<u64> {
let _ = (buffer, offset, bytes);
Err(EpError::KernelFailed(format!(
"{}: partial decommit requires a VirtualBacking capability",
self.name()
)))
}
fn allocation_committed_bytes(&self, buffer: &DeviceBuffer) -> usize {
buffer.len()
}
fn deallocate(&self, buffer: DeviceBuffer) -> Result<()>;
fn wait_for_deferred_releases(&self) -> Result<()> {
Ok(())
}
fn deallocate_workspace(&self, workspace: WorkspaceAllocation) -> Result<()> {
let (buffer, lease) = workspace.into_parts();
match self.deallocate(buffer) {
Ok(()) => {
drop(lease);
Ok(())
}
Err(error) => {
if let Some(lease) = lease {
quarantine_failed_workspace_lease(lease);
}
Err(error)
}
}
}
fn deallocate_with_unmapped(&self, buffer: DeviceBuffer) -> Result<u64> {
self.deallocate(buffer)?;
Ok(0)
}
fn copy(&self, src: &DeviceBuffer, dst: &mut DeviceBuffer, size: usize) -> Result<()>;
fn copy_async(&self, src: &DeviceBuffer, dst: &mut DeviceBuffer, size: usize) -> Result<Fence>;
fn wait_fence(&self, _fence: &Fence) -> Result<()> {
Ok(())
}
fn record_compute_fence(&self) -> Result<Fence> {
Ok(Fence::signalled())
}
fn copy_wait_fence(&self, _fence: &Fence) -> Result<()> {
Ok(())
}
fn device_argmax_supported(&self) -> bool {
false
}
fn device_argmax(
&self,
_logits: &DeviceBuffer,
_elements: usize,
_batch: usize,
_dtype: DataType,
_result: &mut DeviceBuffer,
_tie_break: ArgmaxTieBreak,
) -> Result<()> {
Err(EpError::KernelFailed(format!(
"{}: device argmax is not supported",
self.name()
)))
}
#[allow(clippy::too_many_arguments)]
fn device_token_writer(
&self,
_result: &DeviceBuffer,
_input_ids: &DeviceBuffer,
_position_ids: &DeviceBuffer,
_attention_mask: &DeviceBuffer,
_scratch: &DeviceBuffer,
_capacity: usize,
_next_position: i64,
_mask_len: usize,
_write_position: bool,
_step: u32,
) -> Result<()> {
Err(EpError::KernelFailed(format!(
"{}: device token writer is not supported",
self.name()
)))
}
fn begin_device_graph_capture(&self, _kernels: &[&dyn Kernel]) -> Result<()> {
Err(EpError::KernelFailed(format!(
"{}: device graph capture is not supported",
self.name()
)))
}
fn end_device_graph_capture(&self) -> Result<()> {
Err(EpError::KernelFailed(format!(
"{}: device graph capture is not supported",
self.name()
)))
}
fn abort_device_graph_capture(&self) -> Result<()> {
Ok(())
}
fn replay_device_graph(&self) -> Result<()> {
Err(EpError::KernelFailed(format!(
"{}: device graph replay is not supported",
self.name()
)))
}
fn replay_device_graph_segment(&self, _index: usize) -> Result<()> {
Err(EpError::KernelFailed(format!(
"{}: segmented device graph replay is not supported",
self.name()
)))
}
fn reset_device_graph(&self) -> Result<bool> {
Ok(false)
}
fn begin_device_graph_capture_in(
&self,
slot: DeviceGraphSlot,
kernels: &[&dyn Kernel],
) -> Result<()> {
match slot {
DeviceGraphSlot::Primary => self.begin_device_graph_capture(kernels),
other => Err(unsupported_graph_slot(self.name(), other)),
}
}
fn end_device_graph_capture_in(&self, slot: DeviceGraphSlot) -> Result<()> {
match slot {
DeviceGraphSlot::Primary => self.end_device_graph_capture(),
other => Err(unsupported_graph_slot(self.name(), other)),
}
}
fn abort_device_graph_capture_in(&self, slot: DeviceGraphSlot) -> Result<()> {
match slot {
DeviceGraphSlot::Primary => self.abort_device_graph_capture(),
other => Err(unsupported_graph_slot(self.name(), other)),
}
}
fn replay_device_graph_in(&self, slot: DeviceGraphSlot) -> Result<()> {
match slot {
DeviceGraphSlot::Primary => self.replay_device_graph(),
other => Err(unsupported_graph_slot(self.name(), other)),
}
}
fn replay_device_graph_segment_in(&self, slot: DeviceGraphSlot, index: usize) -> Result<()> {
match slot {
DeviceGraphSlot::Primary => self.replay_device_graph_segment(index),
other => Err(unsupported_graph_slot(self.name(), other)),
}
}
fn reset_device_graph_in(&self, slot: DeviceGraphSlot) -> Result<bool> {
match slot {
DeviceGraphSlot::Primary => self.reset_device_graph(),
DeviceGraphSlot::Verify => Ok(false),
}
}
fn has_device_graph_in(&self, slot: DeviceGraphSlot) -> Result<bool> {
let _ = slot;
Ok(true)
}
fn begin_owned_device_graph_capture(
&self,
owner: DeviceGraphOwner,
slot: DeviceGraphSlot,
continuation: Option<DeviceGraphToken>,
kernels: &[&dyn Kernel],
) -> Result<DeviceGraphToken> {
self.begin_device_graph_capture_in(slot, kernels)?;
Ok(continuation.unwrap_or_else(|| DeviceGraphToken::new(owner, slot, 1)))
}
fn end_owned_device_graph_capture(&self, token: DeviceGraphToken) -> Result<()> {
self.end_device_graph_capture_in(token.slot())
}
fn abort_owned_device_graph_capture(&self, token: DeviceGraphToken) -> Result<()> {
self.abort_device_graph_capture_in(token.slot())
}
fn replay_owned_device_graph(&self, token: DeviceGraphToken) -> Result<()> {
self.replay_device_graph_in(token.slot())
}
fn replay_owned_device_graph_segment(
&self,
token: DeviceGraphToken,
index: usize,
) -> Result<()> {
self.replay_device_graph_segment_in(token.slot(), index)
}
fn reset_owned_device_graph(&self, token: DeviceGraphToken) -> Result<bool> {
self.reset_device_graph_in(token.slot())
}
fn retire_owned_device_graphs(&self, _owner: DeviceGraphOwner) -> Result<()> {
Ok(())
}
fn has_owned_device_graph(&self, token: DeviceGraphToken) -> Result<bool> {
self.has_device_graph_in(token.slot())
}
fn register_device_validation_owner(&self) -> Result<DeviceValidationRegistration> {
let owner = DeviceValidationOwner::new();
Ok(DeviceValidationRegistration::new(owner, ()))
}
fn unregister_device_validation_owner(
&self,
_registration: &mut DeviceValidationRegistration,
) -> Result<()> {
Ok(())
}
fn begin_device_validation(
&self,
registration: &DeviceValidationRegistration,
) -> Result<DeviceValidationToken> {
Ok(DeviceValidationToken::new(registration.owner(), 0))
}
fn add_device_validation_recipient(
&self,
submission: DeviceValidationToken,
recipient: &DeviceValidationRegistration,
) -> Result<DeviceValidationToken> {
Ok(DeviceValidationToken::new(
recipient.owner(),
submission.generation(),
))
}
fn activate_device_validation(&self, _submission: DeviceValidationToken) -> Result<()> {
Ok(())
}
fn abort_device_validation_submission(
&self,
_submission: DeviceValidationToken,
) -> Result<u32> {
Ok(0)
}
fn defers_device_validation(&self) -> bool {
false
}
fn consume_device_validation_error(
&self,
_registration: &DeviceValidationRegistration,
_token: DeviceValidationToken,
) -> Result<u32> {
Ok(0)
}
fn consume_route_residency_at_boundary_for_executor(
&self,
_executor: ExecutorInstanceId,
) -> Result<()> {
Ok(())
}
fn executor_artifact_policy(&self) -> Result<ExecutorArtifactPolicy> {
Ok(ExecutorArtifactPolicy::new(
ExecutorArtifactProviderId::UNSCOPED,
self.device_id(),
ExecutorRouteResidencyConfig::Disabled,
))
}
fn inspect_executor_artifacts(
&self,
_provider: ExecutorArtifactProviderId,
executor: ExecutorInstanceId,
generation: ExecutorArtifactGeneration,
readiness: ExecutorArtifactReadinessEpoch,
_graph: &Graph,
_banks: &[crate::FinalizedExpertBank],
) -> Result<ExecutorArtifactReport> {
Ok(ExecutorArtifactReport::observed(
self.executor_artifact_policy()?.provider(),
executor,
generation,
readiness,
match self.executor_artifact_policy()?.route_residency() {
ExecutorRouteResidencyConfig::Disabled => ExecutorArtifactState::Disabled,
ExecutorRouteResidencyConfig::Enabled => ExecutorArtifactState::Declined,
},
))
}
fn drain_executor_artifacts(
&self,
_provider: ExecutorArtifactProviderId,
_executor: ExecutorInstanceId,
_generation: ExecutorArtifactGeneration,
) -> Result<()> {
Ok(())
}
fn executor_artifact_requirement(
&self,
_provider: ExecutorArtifactProviderId,
_executor: ExecutorInstanceId,
_generation: ExecutorArtifactGeneration,
) -> Result<Option<Arc<dyn ExecutorArtifactRequirementState>>> {
Ok(None)
}
fn device_allocation_counts(&self) -> Option<(u64, u64)> {
None
}
fn raw_device_allocation_site_stats(&self) -> Vec<RawDeviceAllocationSiteStats> {
Vec::new()
}
fn reserve_workspace(
&self,
_bytes: u64,
_role: onnx_runtime_memory_governor::MemoryRole,
) -> Result<Option<onnx_runtime_memory_governor::MemoryLease>> {
Ok(None)
}
fn prepare_mapped_growth(
&self,
bytes: u64,
role: onnx_runtime_memory_governor::MemoryRole,
) -> Result<Option<onnx_runtime_memory_governor::MappedGrowthGrant>> {
let _ = (bytes, role);
Ok(None)
}
fn mapped_bytes_for_allocation(&self, bytes: usize, alignment: usize) -> Result<u64> {
let _ = alignment;
Ok(bytes as u64)
}
fn release_mapped_growth(&self, bytes: u64, role: onnx_runtime_memory_governor::MemoryRole) {
let _ = (bytes, role);
}
fn commits_on_demand(&self) -> bool {
false
}
fn set_weight_residency_budget(&self, _budget_bytes: u64) -> Result<Option<u64>> {
Ok(None)
}
fn adopt_memory_governor(
&self,
_governor: &dyn onnx_runtime_memory_governor::MemoryGovernor,
_tier: onnx_runtime_memory_governor::Tier,
_holder: onnx_runtime_memory_governor::HolderId,
) -> Result<u64> {
Ok(0)
}
fn copy_from_host(&self, src: &[u8], dst: &mut DeviceBuffer) -> Result<()> {
if !dst.device().is_host_accessible() {
return Err(EpError::KernelFailed(format!(
"{}: host upload is not implemented for device {:?}",
self.name(),
dst.device()
)));
}
if src.len() > dst.len() {
return Err(EpError::KernelFailed(format!(
"{}: host upload of {} bytes exceeds destination {} bytes",
self.name(),
src.len(),
dst.len()
)));
}
if src.is_empty() {
return Ok(());
}
unsafe {
std::ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr().cast(), src.len());
}
Ok(())
}
fn copy_from_host_at(
&self,
src: &[u8],
dst: &mut DeviceBuffer,
byte_offset: usize,
) -> Result<()> {
let end = byte_offset.checked_add(src.len()).ok_or_else(|| {
EpError::KernelFailed(format!("{}: host upload range overflows", self.name()))
})?;
if end > dst.len() {
return Err(EpError::KernelFailed(format!(
"{}: host upload range {byte_offset}..{end} exceeds destination {} bytes",
self.name(),
dst.len()
)));
}
if src.is_empty() {
return Ok(());
}
if !dst.device().is_host_accessible() {
return Err(EpError::KernelFailed(format!(
"{}: ranged host upload is not implemented for device {:?}",
self.name(),
dst.device()
)));
}
unsafe {
std::ptr::copy_nonoverlapping(
src.as_ptr(),
dst.as_mut_ptr().cast::<u8>().add(byte_offset),
src.len(),
);
}
Ok(())
}
fn copy_to_host(&self, src: &DeviceBuffer, dst: &mut [u8]) -> Result<()> {
if !src.device().is_host_accessible() {
return Err(EpError::KernelFailed(format!(
"{}: host download is not implemented for device {:?}",
self.name(),
src.device()
)));
}
if dst.len() > src.len() {
return Err(EpError::KernelFailed(format!(
"{}: host download of {} bytes exceeds source {} bytes",
self.name(),
dst.len(),
src.len()
)));
}
if dst.is_empty() {
return Ok(());
}
unsafe {
std::ptr::copy_nonoverlapping(src.as_ptr().cast(), dst.as_mut_ptr(), dst.len());
}
Ok(())
}
fn sync(&self) -> Result<()>;
fn copy_device_to_device(
&self,
_src: &DeviceBuffer,
_src_offset: usize,
_dst: &mut DeviceBuffer,
_dst_offset: usize,
_bytes: usize,
) -> Result<()> {
Err(EpError::KernelFailed(format!(
"{}: device-to-device copy is not implemented",
self.name()
)))
}
fn custom_passes(&self) -> Vec<Box<dyn onnx_runtime_optimizer::OptimizationPass>> {
Vec::new()
}
fn claim_nodes(&self, graph: &Graph) -> Vec<NodeId> {
let _ = graph;
Vec::new()
}
fn context_source_keys(&self) -> Vec<String> {
Vec::new()
}
fn save_context(&self) -> Result<EpContext> {
Err(EpError::UnsupportedContext {
ep: self.name().to_string(),
})
}
fn load_context(&self, ctx: &EpContext) -> Result<()> {
let _ = ctx;
Err(EpError::UnsupportedContext {
ep: self.name().to_string(),
})
}
}
fn unsupported_graph_slot(ep: &str, slot: DeviceGraphSlot) -> EpError {
EpError::KernelFailed(format!(
"{ep}: device graph slot {slot:?} is not supported (this EP owns only the Primary slot)"
))
}
fn is_control_flow_or_sequence(node: &Node) -> bool {
if !(node.domain.is_empty() || node.domain == "ai.onnx") {
return false;
}
matches!(
node.op_type.as_str(),
"If" | "Loop"
| "Scan"
| "SequenceEmpty"
| "SequenceConstruct"
| "SequenceInsert"
| "SequenceErase"
| "SequenceAt"
| "SequenceLength"
| "SplitToSequence"
| "ConcatFromSequence"
)
}
#[cfg(test)]
mod tests {
use super::*;
fn _assert_send_sync<T: Send + Sync>() {}
fn host_alloc(size: usize, align: usize) -> DeviceBuffer {
let boxed = vec![0u8; size].into_boxed_slice();
let ptr = Box::into_raw(boxed) as *mut c_void;
unsafe { DeviceBuffer::from_raw_parts(ptr, DeviceId::cpu(), size, align) }
}
fn host_free(buf: DeviceBuffer) {
let size = buf.len();
let ptr = buf.into_raw() as *mut u8;
unsafe {
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, size)));
}
}
#[test]
fn device_buffer_is_send_sync() {
_assert_send_sync::<DeviceBuffer>();
}
#[test]
fn artifact_policy_is_non_authoritative_provider_data() {
let policy = ExecutorArtifactPolicy::new(
ExecutorArtifactProviderId::UNSCOPED,
DeviceId::cpu(),
ExecutorRouteResidencyConfig::Enabled,
);
assert_eq!(policy.device(), DeviceId::cpu());
assert_eq!(
policy.route_residency(),
ExecutorRouteResidencyConfig::Enabled
);
}
#[test]
fn buffer_metadata_and_single_free() {
let mut buf = host_alloc(128, 64);
assert_eq!(buf.len(), 128);
assert!(!buf.is_empty());
assert_eq!(buf.alignment(), 64);
assert_eq!(buf.device(), DeviceId::cpu());
assert!(!buf.as_ptr().is_null());
assert!(!buf.as_mut_ptr().is_null());
host_free(buf);
}
#[test]
fn buffer_moves_across_thread() {
let buf = host_alloc(64, 16);
let base = buf.as_ptr() as usize;
let handle = std::thread::spawn(move || {
assert_eq!(buf.len(), 64);
assert_eq!(buf.as_ptr() as usize, base);
buf });
let buf = handle.join().unwrap();
host_free(buf);
}
#[test]
fn owned_buffer_is_not_borrowed() {
let buf = host_alloc(32, 16);
assert!(
!buf.is_borrowed(),
"from_raw_parts must produce an owned buffer"
);
host_free(buf);
}
#[test]
fn borrowed_buffer_aliases_without_owning() {
let mut backing = vec![7u8; 64];
let ptr = backing.as_mut_ptr() as *mut c_void;
let buf = unsafe { DeviceBuffer::from_borrowed_parts(ptr, DeviceId::cpu(), 64, 1) };
assert!(buf.is_borrowed());
assert_eq!(buf.len(), 64);
assert_eq!(buf.as_ptr(), ptr as *const c_void);
let raw = buf.into_raw();
assert_eq!(raw, ptr);
assert!(backing.iter().all(|&b| b == 7));
backing[0] = 9;
assert_eq!(backing[0], 9);
}
fn host_binding() -> onnx_runtime_memory_governor::MemoryBinding {
use onnx_runtime_memory_governor::{BindingRegistry, DeviceKey, HostAllocator};
use std::sync::Arc;
#[derive(Debug)]
struct Pin;
let registry = BindingRegistry::new().expect("registry");
let context = registry
.register_provider_context(DeviceKey::HOST, Arc::new(Pin))
.expect("provider context");
let authority = registry
.register_authority(DeviceKey::HOST, Arc::new(Pin))
.expect("authority");
let mechanism = registry
.register_allocator(context, authority, Arc::new(HostAllocator))
.expect("allocator");
registry.select(mechanism).expect("selection");
registry.bind(DeviceKey::HOST).expect("binding")
}
fn reclaim_quarantined(binding: &onnx_runtime_memory_governor::MemoryBinding) -> usize {
use onnx_runtime_memory_governor::{DeviceAllocator, HostAllocator};
let quarantined = binding.quarantined().expect("quarantine list");
for record in &quarantined {
let Some(ptr) = std::ptr::NonNull::new(record.address as *mut u8) else {
continue;
};
unsafe { HostAllocator.deallocate(ptr, record.bytes, record.align) };
}
quarantined.len()
}
#[test]
fn a_bound_buffer_carries_the_owner_that_minted_it() {
let binding = host_binding();
let owner = binding.allocate_owning(256, 64).expect("owning allocation");
let identity = owner.identity();
let address = owner.as_ptr().as_ptr() as usize;
let buffer = DeviceBuffer::from_owning_allocation(owner, DeviceId::cpu());
assert!(buffer.is_bound());
assert!(!buffer.is_borrowed(), "a bound buffer owns its allocation");
assert_eq!(buffer.len(), 256);
assert_eq!(buffer.alignment(), 64);
assert_eq!(buffer.as_ptr() as usize, address);
assert_eq!(
buffer.bound_owner().expect("bound owner").identity(),
identity,
"the buffer never describes a different allocation than its owner"
);
let BoundBufferOwnership::Binding(recovered) = buffer.into_bound_owner().expect("bound")
else {
panic!("plain binding owner changed representation");
};
assert_eq!(recovered.identity(), identity);
let outcome = recovered.release_now().expect("release");
assert!(outcome.is_complete());
}
#[test]
fn a_managed_buffer_keeps_charge_attached_to_bound_ownership() {
use onnx_runtime_memory_governor::{
AllocationPublication, AllocationRequest, DeviceKey, HostAllocator, LeaseLedger,
LedgerGovernor, MemoryGovernor, MemoryRole, ProcessMemoryManager, Tier,
};
use std::sync::Arc;
#[derive(Debug)]
struct Pin;
let manager = ProcessMemoryManager::new().unwrap();
let context = manager
.register_provider_context(DeviceKey::HOST, "host context", Arc::new(Pin))
.unwrap();
let governor = Arc::new(LedgerGovernor::new(LeaseLedger::new_for_device(
DeviceKey::HOST,
0,
1024,
0,
)));
let authority = manager
.register_authority(
DeviceKey::HOST,
"host authority",
Arc::new(Pin),
governor.clone() as Arc<dyn MemoryGovernor + Send + Sync>,
)
.unwrap();
let holder = manager
.register_holder(&authority, "workspace", None)
.unwrap();
let mechanism = manager
.register_allocator(
&context,
&authority,
"host allocator",
Arc::new(HostAllocator),
)
.unwrap();
let owner = manager
.bind_registered(&mechanism)
.unwrap()
.allocate(
AllocationRequest::managed(
128,
16,
Tier::Host,
MemoryRole::Workspace { step_scoped: false },
holder,
128,
),
AllocationPublication::exclusive(128, 128, 128),
)
.unwrap();
assert_eq!(governor.used(Tier::Host), 128);
let buffer = DeviceBuffer::from_managed_allocation(owner, DeviceId::cpu());
assert!(buffer.is_bound());
assert!(buffer.managed_owner().is_some());
let BoundBufferOwnership::Managed(owner) =
buffer.into_bound_owner().expect("managed ownership")
else {
panic!("manager ownership changed representation");
};
owner.release_now().unwrap();
assert_eq!(governor.used(Tier::Host), 0);
}
#[test]
fn failed_workspace_deallocation_quarantine_keeps_compatibility_charge() {
use onnx_runtime_memory_governor::{
DeviceKey, HolderId, LeaseLedger, LedgerGovernor, MemoryGovernor, MemoryRole, Tier,
};
#[derive(Debug)]
struct WorkspaceDeallocationEp {
fail: bool,
}
impl ExecutionProvider for WorkspaceDeallocationEp {
fn name(&self) -> &str {
"workspace-deallocation-test"
}
fn device_type(&self) -> DeviceType {
DeviceType::Cpu
}
fn device_id(&self) -> DeviceId {
DeviceId::cpu()
}
fn initialize(&mut self, _config: &EpConfig) -> Result<()> {
Ok(())
}
fn shutdown(&mut self) -> Result<()> {
Ok(())
}
fn supports_op(
&self,
_op: &Node,
_opset: u64,
_shapes: &[Shape],
_input_dtypes: &[DataType],
_layouts: &[TensorLayout],
) -> KernelMatch {
KernelMatch::unsupported("unused test provider")
}
fn get_kernel(
&self,
_op: &Node,
_shapes: &[Vec<usize>],
_opset: u64,
) -> Result<Box<dyn Kernel>> {
Err(EpError::KernelFailed("unused test kernel".into()))
}
fn allocate(&self, _size: usize, _alignment: usize) -> Result<DeviceBuffer> {
Err(EpError::KernelFailed("unused test allocation".into()))
}
fn deallocate(&self, _buffer: DeviceBuffer) -> Result<()> {
if self.fail {
Err(EpError::KernelFailed(
"injected workspace deallocation failure".into(),
))
} else {
Ok(())
}
}
fn copy(
&self,
_src: &DeviceBuffer,
_dst: &mut DeviceBuffer,
_size: usize,
) -> Result<()> {
Err(EpError::KernelFailed("unused test copy".into()))
}
fn copy_async(
&self,
_src: &DeviceBuffer,
_dst: &mut DeviceBuffer,
_size: usize,
) -> Result<Fence> {
Err(EpError::KernelFailed("unused test async copy".into()))
}
fn sync(&self) -> Result<()> {
Ok(())
}
}
fn borrowed_workspace(lease: MemoryLease, backing: &mut [u8]) -> WorkspaceAllocation {
let buffer = unsafe {
DeviceBuffer::from_borrowed_parts(
backing.as_mut_ptr().cast(),
DeviceId::cpu(),
backing.len(),
1,
)
};
WorkspaceAllocation::new(buffer, Some(lease))
}
let failed_governor =
LedgerGovernor::new(LeaseLedger::new_for_device(DeviceKey::HOST, 0, 1024, 0));
let failed_lease = failed_governor
.reserve(
Tier::Host,
64,
MemoryRole::Workspace { step_scoped: true },
HolderId::new(9),
)
.unwrap();
let before = quarantined_workspace_lease_count();
let mut failed_backing = vec![0_u8; 64];
let error = WorkspaceDeallocationEp { fail: true }
.deallocate_workspace(borrowed_workspace(failed_lease, &mut failed_backing))
.unwrap_err();
assert!(error.to_string().contains("injected"));
assert_eq!(quarantined_workspace_lease_count(), before + 1);
assert_eq!(
failed_governor.used(Tier::Host),
64,
"failed deallocation must not advertise unsettled bytes as free"
);
let success_governor =
LedgerGovernor::new(LeaseLedger::new_for_device(DeviceKey::HOST, 0, 1024, 0));
let success_lease = success_governor
.reserve(
Tier::Host,
64,
MemoryRole::Workspace { step_scoped: true },
HolderId::new(10),
)
.unwrap();
let mut success_backing = vec![0_u8; 64];
WorkspaceDeallocationEp { fail: false }
.deallocate_workspace(borrowed_workspace(success_lease, &mut success_backing))
.unwrap();
assert_eq!(
success_governor.used(Tier::Host),
0,
"successful synchronous deallocation must refund its outer lease"
);
assert_eq!(
quarantined_workspace_lease_count(),
before + 1,
"success and failure paths must not be swapped"
);
}
#[test]
fn a_raw_or_borrowed_buffer_has_no_bound_owner() {
let raw = host_alloc(64, 16);
assert!(!raw.is_bound());
assert!(raw.bound_owner().is_none());
let raw = raw.into_bound_owner().expect_err("not bound");
assert_eq!(raw.len(), 64);
host_free(raw);
}
#[test]
fn into_raw_refuses_to_strip_bound_ownership() {
let binding = host_binding();
let owner = binding.allocate_owning(128, 16).expect("owning allocation");
let buffer = DeviceBuffer::from_owning_allocation(owner, DeviceId::cpu());
let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = buffer.into_raw();
}))
.expect_err("into_raw must refuse a bound buffer");
let message = panic
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| panic.downcast_ref::<&str>().copied())
.expect("panic payload is a string");
assert!(
message.contains("would bypass binding-identity"),
"unexpected panic message: {message}"
);
assert_eq!(
reclaim_quarantined(&binding),
1,
"the refused buffer is retained, not silently freed"
);
}
#[test]
fn into_raw_with_owner_is_the_explicit_escape_hatch() {
let binding = host_binding();
let owner = binding.allocate_owning(128, 16).expect("owning allocation");
let expected = owner.as_ptr().as_ptr() as usize;
let buffer = DeviceBuffer::from_owning_allocation(owner, DeviceId::cpu());
let (ptr, owner) = buffer.into_raw_with_owner();
assert_eq!(ptr as usize, expected);
let BoundBufferOwnership::Binding(owner) =
owner.expect("the release obligation travels with the pointer")
else {
panic!("plain binding owner changed representation");
};
assert!(owner.release_now().expect("release").is_complete());
let raw = host_alloc(32, 8);
let (ptr, owner) = raw.into_raw_with_owner();
assert!(owner.is_none());
unsafe {
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
ptr as *mut u8,
32,
)));
}
}
#[test]
fn dropping_a_bound_buffer_quarantines_instead_of_freeing() {
let binding = host_binding();
let owner = binding.allocate_owning(64, 16).expect("owning allocation");
let buffer = DeviceBuffer::from_owning_allocation(owner, DeviceId::cpu());
drop(buffer);
let quarantined = binding.quarantined().expect("quarantine list");
assert_eq!(
quarantined.len(),
1,
"a dropped bound buffer stays accounted for instead of being freed"
);
assert_eq!(quarantined[0].retained_bytes, 64);
assert_eq!(reclaim_quarantined(&binding), 1);
}
#[test]
fn borrowed_mut_buffer_writes_without_owning() {
let mut backing = vec![0u8; 8];
let ptr = backing.as_mut_ptr() as *mut c_void;
let mut buffer =
unsafe { DeviceBuffer::from_borrowed_mut_parts(ptr, DeviceId::cpu(), 8, 1) }
.expect("non-null backing");
assert!(buffer.is_borrowed());
unsafe {
std::ptr::copy_nonoverlapping([1u8, 2, 3].as_ptr(), buffer.as_mut_ptr().cast(), 3);
}
assert_eq!(buffer.into_raw(), ptr);
assert_eq!(&backing[..3], &[1, 2, 3]);
assert!(
unsafe {
DeviceBuffer::from_borrowed_mut_parts(std::ptr::null_mut(), DeviceId::cpu(), 0, 1)
}
.is_none()
);
}
}