use std::collections::HashMap;
use crate::error::{EpError, Result};
use crate::provider::{EpId, ExecutionProvider};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EpContext {
pub ep_name: String,
pub ep_version: String,
pub data: Vec<u8>,
pub covered_nodes: Vec<onnx_runtime_ir::NodeId>,
pub device_fingerprint: String,
}
impl EpContext {
pub fn new(
ep_name: impl Into<String>,
ep_version: impl Into<String>,
data: Vec<u8>,
covered_nodes: Vec<onnx_runtime_ir::NodeId>,
device_fingerprint: impl Into<String>,
) -> Self {
Self {
ep_name: ep_name.into(),
ep_version: ep_version.into(),
data,
covered_nodes,
device_fingerprint: device_fingerprint.into(),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct EpContextRegistry {
by_source: HashMap<String, EpId>,
}
impl EpContextRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, ep: EpId, source_keys: &[String]) -> Result<()> {
for key in source_keys {
match self.by_source.get(key) {
Some(&existing) if existing == ep => {} Some(&existing) => {
return Err(EpError::DuplicateContextSource {
source_key: key.clone(),
existing,
new: ep,
});
}
None => {
self.by_source.insert(key.clone(), ep);
}
}
}
Ok(())
}
pub fn claim(&self, source: Option<&str>) -> Option<EpId> {
self.by_source.get(source?).copied()
}
pub fn len(&self) -> usize {
self.by_source.len()
}
pub fn is_empty(&self) -> bool {
self.by_source.is_empty()
}
}
pub fn build_ep_context_registry<'a, I>(eps: I) -> Result<EpContextRegistry>
where
I: IntoIterator<Item = (EpId, &'a dyn ExecutionProvider)>,
{
let mut registry = EpContextRegistry::new();
for (id, ep) in eps {
let keys = ep.context_source_keys();
if keys.is_empty() {
continue;
}
registry.register(id, &keys)?;
}
Ok(registry)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::{Kernel, KernelMatch};
use crate::provider::{DeviceBuffer, EpConfig, Fence};
use onnx_runtime_ir::{DeviceId, DeviceType, Node, NodeId, Shape, TensorLayout};
struct MockCompiledEp {
source_keys: Vec<String>,
}
impl MockCompiledEp {
const BLOB: &'static [u8] = b"mock-compiled-context-v1";
fn new() -> Self {
Self {
source_keys: vec!["MOCK".to_string(), "MockExecutionProvider".to_string()],
}
}
}
impl ExecutionProvider for MockCompiledEp {
fn name(&self) -> &str {
"mock_compiled_ep"
}
fn device_type(&self) -> DeviceType {
DeviceType::Custom(0)
}
fn device_id(&self) -> DeviceId {
DeviceId::new(DeviceType::Custom(0), 0)
}
fn initialize(&mut self, _config: &EpConfig) -> Result<()> {
Ok(())
}
fn shutdown(&mut self) -> Result<()> {
Ok(())
}
fn supports_op(
&self,
_op: &Node,
_shapes: &[Shape],
_layouts: &[TensorLayout],
) -> KernelMatch {
KernelMatch::Unsupported
}
fn get_kernel(
&self,
_op: &Node,
_shapes: &[Vec<usize>],
_opset: u64,
) -> Result<Box<dyn Kernel>> {
Err(EpError::NoEpForOp {
op_type: "<mock>".to_string(),
})
}
fn allocate(&self, _size: usize, _alignment: usize) -> Result<DeviceBuffer> {
Err(EpError::NotInitialized)
}
fn deallocate(&self, _buffer: DeviceBuffer) -> Result<()> {
Ok(())
}
fn copy(&self, _src: &DeviceBuffer, _dst: &mut DeviceBuffer, _size: usize) -> Result<()> {
Ok(())
}
fn copy_async(
&self,
_src: &DeviceBuffer,
_dst: &mut DeviceBuffer,
_size: usize,
) -> Result<Fence> {
Ok(Fence::default())
}
fn sync(&self) -> Result<()> {
Ok(())
}
fn context_source_keys(&self) -> Vec<String> {
self.source_keys.clone()
}
fn save_context(&self) -> Result<EpContext> {
Ok(EpContext::new(
self.name(),
"1.2.3",
Self::BLOB.to_vec(),
vec![NodeId(7)],
"mock-device",
))
}
fn load_context(&self, ctx: &EpContext) -> Result<()> {
if ctx.data == Self::BLOB {
Ok(())
} else {
Err(EpError::KernelFailed("mock: unexpected context blob".to_string()))
}
}
}
struct PlainEp;
impl ExecutionProvider for PlainEp {
fn name(&self) -> &str {
"plain_ep"
}
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,
_shapes: &[Shape],
_layouts: &[TensorLayout],
) -> KernelMatch {
KernelMatch::Unsupported
}
fn get_kernel(
&self,
_op: &Node,
_shapes: &[Vec<usize>],
_opset: u64,
) -> Result<Box<dyn Kernel>> {
Err(EpError::NoEpForOp {
op_type: "<plain>".to_string(),
})
}
fn allocate(&self, _size: usize, _alignment: usize) -> Result<DeviceBuffer> {
Err(EpError::NotInitialized)
}
fn deallocate(&self, _buffer: DeviceBuffer) -> Result<()> {
Ok(())
}
fn copy(&self, _src: &DeviceBuffer, _dst: &mut DeviceBuffer, _size: usize) -> Result<()> {
Ok(())
}
fn copy_async(
&self,
_src: &DeviceBuffer,
_dst: &mut DeviceBuffer,
_size: usize,
) -> Result<Fence> {
Ok(Fence::default())
}
fn sync(&self) -> Result<()> {
Ok(())
}
}
#[test]
fn register_and_claim_by_source_key() {
let mock = MockCompiledEp::new();
let mut reg = EpContextRegistry::new();
let ep_id = EpId(3);
reg.register(ep_id, &mock.context_source_keys()).unwrap();
assert_eq!(reg.claim(Some("MOCK")), Some(ep_id));
assert_eq!(reg.claim(Some("MockExecutionProvider")), Some(ep_id));
assert_eq!(reg.len(), 2);
}
#[test]
fn unmatched_and_absent_source_are_unclaimed() {
let mock = MockCompiledEp::new();
let mut reg = EpContextRegistry::new();
reg.register(EpId(0), &mock.context_source_keys()).unwrap();
assert_eq!(reg.claim(Some("QNN")), None);
assert_eq!(reg.claim(None), None);
}
#[test]
fn duplicate_source_key_is_rejected() {
let mut reg = EpContextRegistry::new();
reg.register(EpId(0), &["MOCK".to_string()]).unwrap();
let err = reg
.register(EpId(1), &["MOCK".to_string()])
.expect_err("duplicate source key must be rejected");
match err {
EpError::DuplicateContextSource {
source_key,
existing,
new,
} => {
assert_eq!(source_key, "MOCK");
assert_eq!(existing, EpId(0));
assert_eq!(new, EpId(1));
}
other => panic!("expected DuplicateContextSource, got {other:?}"),
}
assert_eq!(reg.claim(Some("MOCK")), Some(EpId(0)));
}
#[test]
fn re_registering_same_binding_is_idempotent() {
let mut reg = EpContextRegistry::new();
reg.register(EpId(2), &["MOCK".to_string()]).unwrap();
reg.register(EpId(2), &["MOCK".to_string()]).unwrap();
assert_eq!(reg.claim(Some("MOCK")), Some(EpId(2)));
assert_eq!(reg.len(), 1);
}
#[test]
fn build_registry_from_eps_skips_non_participants() {
let mock = MockCompiledEp::new();
let plain = PlainEp;
let eps: Vec<(EpId, &dyn ExecutionProvider)> =
vec![(EpId(0), &plain), (EpId(1), &mock)];
let reg = build_ep_context_registry(eps).unwrap();
assert_eq!(reg.len(), 2);
assert_eq!(reg.claim(Some("MOCK")), Some(EpId(1)));
assert_eq!(reg.claim(Some("MockExecutionProvider")), Some(EpId(1)));
}
#[test]
fn build_registry_from_eps_propagates_duplicate_error() {
let a = MockCompiledEp::new();
let b = MockCompiledEp::new();
let eps: Vec<(EpId, &dyn ExecutionProvider)> = vec![(EpId(0), &a), (EpId(1), &b)];
let err = build_ep_context_registry(eps)
.expect_err("two EPs on the same source key must conflict");
assert!(matches!(err, EpError::DuplicateContextSource { .. }));
}
#[test]
fn save_load_round_trip_preserves_bytes() {
let mock = MockCompiledEp::new();
let ctx = mock.save_context().unwrap();
assert_eq!(ctx.ep_name, "mock_compiled_ep");
assert_eq!(ctx.ep_version, "1.2.3");
assert_eq!(ctx.data, MockCompiledEp::BLOB);
assert_eq!(ctx.covered_nodes, vec![NodeId(7)]);
mock.load_context(&ctx).unwrap();
let mut bad = ctx.clone();
bad.data.push(0xFF);
assert!(mock.load_context(&bad).is_err());
}
#[test]
fn plain_ep_defaults_are_empty_and_unsupported() {
let plain = PlainEp;
assert!(plain.context_source_keys().is_empty());
assert!(matches!(
plain.save_context(),
Err(EpError::UnsupportedContext { .. })
));
let ctx = EpContext::default();
assert!(matches!(
plain.load_context(&ctx),
Err(EpError::UnsupportedContext { .. })
));
}
#[test]
fn no_ep_for_context_error_carries_source() {
let reg = EpContextRegistry::new();
let source = Some("QNN");
let err = reg
.claim(source)
.ok_or_else(|| EpError::NoEpForContext {
source_key: source.map(str::to_owned),
})
.unwrap_err();
match err {
EpError::NoEpForContext { source_key } => {
assert_eq!(source_key.as_deref(), Some("QNN"))
}
other => panic!("expected NoEpForContext, got {other:?}"),
}
}
}