use std::ffi::c_void;
use std::ptr::NonNull;
use onnx_runtime_ir::{DataType, DeviceId, DeviceType, Graph, Node, NodeId, Shape, TensorLayout};
use crate::epcontext::EpContext;
use crate::error::{EpError, Result};
use crate::kernel::{Kernel, KernelMatch};
use crate::weight::ExecutionProviderCapabilities;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct EpId(pub u32);
#[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, Clone, Copy, PartialEq, Eq)]
enum BufferOwner {
Owned,
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 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 {
self.ptr.as_ptr()
}
}
unsafe impl Send for DeviceBuffer {}
unsafe impl Sync for DeviceBuffer {}
#[derive(Debug, Default)]
pub struct Fence {
pub id: u64,
}
#[derive(Debug, Default)]
pub struct OrtPluginExport {
pub register_symbol: String,
}
#[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 ExecutionProvider: Send + Sync {
fn name(&self) -> &str;
fn device_type(&self) -> DeviceType;
fn device_id(&self) -> DeviceId;
fn capabilities(&self) -> ExecutionProviderCapabilities {
ExecutionProviderCapabilities::stock()
}
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 get_kernel(&self, op: &Node, shapes: &[Vec<usize>], opset: u64) -> Result<Box<dyn Kernel>>;
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 deallocate(&self, buffer: DeviceBuffer) -> Result<()>;
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 device_argmax_supported(&self) -> bool {
false
}
fn device_argmax(
&self,
_logits: &DeviceBuffer,
_elements: usize,
_dtype: DataType,
_result: &mut DeviceBuffer,
) -> Result<()> {
Err(EpError::KernelFailed(format!(
"{}: device argmax 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 check_device_capture_error(&self) -> Result<u32> {
Ok(0)
}
fn device_allocation_counts(&self) -> Option<(u64, u64)> {
None
}
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 as_ort_plugin(&self) -> Option<OrtPluginExport> {
None
}
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 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 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);
}
#[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()
);
}
}