use std::collections::HashMap;
use std::ffi::{CStr, c_char, c_void};
use std::path::Path;
use std::ptr;
use std::sync::{
Arc, Mutex,
atomic::{AtomicU64, Ordering},
};
use onnx_genai_ort_sys as ort;
use super::ffi_helpers::MAX_PLUGIN_THREAD_STATES;
use super::host::{
HostKernelContext, check_compute_status, check_status, ort_api_base, release_status,
};
use crate::error::{EpError, Result};
use crate::kernel::{ARG_BYTES, ARG_DEVICE, ARG_FLOPS, CAT_KERNEL_WORKER, Kernel};
use crate::tensor::{TensorMut, TensorView};
pub(super) struct PluginRuntime {
pub(super) path: std::path::PathBuf,
#[allow(dead_code)]
pub(super) lib: libloading::Library,
pub(super) factory: *mut ort::OrtEpFactory,
pub(super) ep: *mut ort::OrtEp,
pub(super) release_factory:
Option<unsafe extern "C" fn(*mut ort::OrtEpFactory) -> *mut ort::OrtStatus>,
pub(super) compute_infos: Vec<*mut ort::OrtNodeComputeInfo>,
}
unsafe impl Send for PluginRuntime {}
unsafe impl Sync for PluginRuntime {}
impl PluginRuntime {
pub(super) fn load(library_path: &Path, registration_name: Option<&CStr>) -> Result<Self> {
let lib = unsafe { libloading::Library::new(library_path) }.map_err(|err| {
EpError::EpLoadFailed {
path: library_path.to_path_buf(),
reason: format!(
"failed to open plugin dynamic library ({err}); fix by building the plugin dylib and passing the correct absolute path"
),
}
})?;
type CreateEpFactories = unsafe extern "C" fn(
*const c_char,
*const ort::OrtApiBase,
*const ort::OrtLogger,
*mut *mut ort::OrtEpFactory,
usize,
*mut usize,
) -> *mut ort::OrtStatus;
type ReleaseEpFactory = unsafe extern "C" fn(*mut ort::OrtEpFactory) -> *mut ort::OrtStatus;
let create = unsafe { lib.get::<CreateEpFactories>(b"CreateEpFactories") }.map_err(|err| {
EpError::EpLoadFailed {
path: library_path.to_path_buf(),
reason: format!(
"CreateEpFactories symbol was not found ({err}); fix by using an ONNX Runtime plugin-EP library built against the plugin EP C ABI"
),
}
})?;
let release_factory = unsafe {
lib.get::<ReleaseEpFactory>(b"ReleaseEpFactory")
.ok()
.map(|symbol| *symbol)
};
let mut factories: [*mut ort::OrtEpFactory; 1] = [ptr::null_mut()];
let mut num_factories = 0usize;
let name_ptr = registration_name.map_or(ptr::null(), CStr::as_ptr);
let status = unsafe {
create(
name_ptr,
ort_api_base(),
ptr::null(),
factories.as_mut_ptr(),
factories.len(),
&mut num_factories,
)
};
check_status(library_path, "CreateEpFactories", status)?;
if num_factories == 0 || factories[0].is_null() {
return Err(EpError::EpLoadFailed {
path: library_path.to_path_buf(),
reason: "CreateEpFactories returned no factories; fix by checking that the plugin supports this platform and ORT API version".into(),
});
}
let factory = factories[0];
let supported_version = unsafe { (*factory).ort_version_supported };
if supported_version == 0 || supported_version > ort::ORT_API_VERSION {
if let Some(release_factory) = release_factory {
let status = unsafe { release_factory(factory) };
release_status(status);
}
return Err(EpError::EpLoadFailed {
path: library_path.to_path_buf(),
reason: format!(
"plugin factory requires ORT API version {supported_version}, but this host supports version {}; fix by using a plugin built for a compatible ORT plugin-EP ABI",
ort::ORT_API_VERSION
),
});
}
let mut ep: *mut ort::OrtEp = ptr::null_mut();
let status = unsafe {
let create_ep = (*factory).CreateEp.ok_or_else(|| EpError::EpLoadFailed {
path: library_path.to_path_buf(),
reason: "OrtEpFactory.CreateEp is null; fix by using a complete plugin EP factory"
.into(),
})?;
create_ep(
factory,
ptr::null(),
ptr::null(),
1,
ptr::null(),
ptr::null(),
&mut ep,
)
};
check_status(library_path, "OrtEpFactory.CreateEp", status)?;
if ep.is_null() {
return Err(EpError::EpLoadFailed {
path: library_path.to_path_buf(),
reason: "OrtEpFactory.CreateEp returned a null EP; fix by checking plugin device requirements and options".into(),
});
}
Ok(Self {
path: library_path.to_path_buf(),
lib,
factory,
ep,
release_factory,
compute_infos: Vec::new(),
})
}
}
impl Drop for PluginRuntime {
fn drop(&mut self) {
unsafe {
if !self.compute_infos.is_empty()
&& let Some(release_infos) = (*self.ep).ReleaseNodeComputeInfos
{
release_infos(
self.ep,
self.compute_infos.as_mut_ptr(),
self.compute_infos.len(),
);
}
if let Some(release_ep) = (*self.factory).ReleaseEp {
release_ep(self.factory, self.ep);
}
if let Some(release_factory) = &self.release_factory {
let st = release_factory(self.factory);
if !st.is_null() {
release_status(st);
}
}
}
}
}
pub(super) struct PluginKernelShared {
pub(super) runtime: Arc<PluginRuntime>,
pub(super) info: *mut ort::OrtNodeComputeInfo,
pub(super) create_state: unsafe extern "C" fn(
*mut ort::OrtNodeComputeInfo,
*mut ort::OrtNodeComputeContext,
*mut *mut c_void,
) -> *mut ort::OrtStatus,
pub(super) compute: unsafe extern "C" fn(
*mut ort::OrtNodeComputeInfo,
*mut c_void,
*mut ort::OrtKernelContext,
) -> *mut ort::OrtStatus,
pub(super) release_state:
Option<unsafe extern "C" fn(*mut ort::OrtNodeComputeInfo, *mut c_void)>,
pub(super) states: Mutex<HashMap<std::thread::ThreadId, *mut c_void>>,
pub(super) index: usize,
pub(super) calls: AtomicU64,
pub(super) device_label: Arc<str>,
}
unsafe impl Send for PluginKernelShared {}
unsafe impl Sync for PluginKernelShared {}
impl Drop for PluginKernelShared {
fn drop(&mut self) {
if let Some(release_state) = self.release_state
&& let Ok(states) = self.states.get_mut()
{
for (_, state) in states.drain() {
unsafe { release_state(self.info, state) };
}
}
}
}
pub struct PluginCompiledKernel {
pub(super) shared: Arc<PluginKernelShared>,
}
impl Kernel for PluginCompiledKernel {
fn execute(&self, inputs: &[TensorView], outputs: &mut [TensorMut]) -> Result<()> {
let mut states = self.shared.states.lock().map_err(|_| {
EpError::KernelFailed(
"plugin fused-subgraph state mutex was poisoned; recreate the session".into(),
)
})?;
let thread_id = std::thread::current().id();
let state = if let Some(&state) = states.get(&thread_id) {
state
} else {
if states.len() >= MAX_PLUGIN_THREAD_STATES {
return Err(EpError::KernelFailed(format!(
"the execution provider plugin has been asked to run fused subgraph {} from \
more than {MAX_PLUGIN_THREAD_STATES} threads, and it holds per-thread state \
for each. Why: plugin state is created per executing thread because plugins \
may be thread-affine, so a caller creating a fresh thread per call would \
grow it without bound. Fix by running generation from a bounded thread pool, \
or set ONNX_GENAI_BACKEND=ort to run without the plugin",
self.shared.index
)));
}
let mut state: *mut c_void = ptr::null_mut();
let status = unsafe {
(self.shared.create_state)(self.shared.info, ptr::null_mut(), &mut state)
};
check_status(
&self.shared.runtime.path,
"OrtNodeComputeInfo.CreateState",
status,
)?;
states.insert(thread_id, state);
state
};
drop(states);
let mut context = HostKernelContext::new(inputs, outputs)?;
let bytes = context.byte_size();
let _span = onnx_runtime_tracer::global_context().map(|trace| {
let span = trace
.span(
format!("plugin_fused_{}", self.shared.index),
CAT_KERNEL_WORKER,
)
.without_source();
onnx_runtime_tracer::annotate_current_span_with(|| {
onnx_runtime_tracer::Args::new()
.with(ARG_DEVICE, self.shared.device_label.to_string())
.with(ARG_BYTES, bytes as u64)
.with(ARG_FLOPS, 0_u64)
});
span
});
let status = unsafe {
(self.shared.compute)(
self.shared.info,
state,
(&mut context as *mut HostKernelContext).cast::<ort::OrtKernelContext>(),
)
};
check_compute_status(&self.shared.runtime.path, self.shared.index, status)?;
self.shared.calls.fetch_add(1, Ordering::Relaxed);
Ok(())
}
fn supports_strided_input(&self, _input_idx: usize) -> bool {
false
}
}