1use std::collections::HashMap;
24
25use crate::error::{EpError, Result};
26use crate::provider::{EpId, ExecutionProvider};
27
28#[derive(Clone, Debug, Default, PartialEq, Eq)]
37pub struct EpContext {
38 pub ep_name: String,
40 pub ep_version: String,
43 pub data: Vec<u8>,
45 pub covered_nodes: Vec<onnx_runtime_ir::NodeId>,
47 pub device_fingerprint: String,
50}
51
52impl EpContext {
53 pub fn new(
55 ep_name: impl Into<String>,
56 ep_version: impl Into<String>,
57 data: Vec<u8>,
58 covered_nodes: Vec<onnx_runtime_ir::NodeId>,
59 device_fingerprint: impl Into<String>,
60 ) -> Self {
61 Self {
62 ep_name: ep_name.into(),
63 ep_version: ep_version.into(),
64 data,
65 covered_nodes,
66 device_fingerprint: device_fingerprint.into(),
67 }
68 }
69}
70
71#[derive(Clone, Debug, Default)]
92pub struct EpContextRegistry {
93 by_source: HashMap<String, EpId>,
94}
95
96impl EpContextRegistry {
97 pub fn new() -> Self {
99 Self::default()
100 }
101
102 pub fn register(&mut self, ep: EpId, source_keys: &[String]) -> Result<()> {
114 for key in source_keys {
115 match self.by_source.get(key) {
116 Some(&existing) if existing == ep => {} Some(&existing) => {
118 return Err(EpError::DuplicateContextSource {
119 source_key: key.clone(),
120 existing,
121 new: ep,
122 });
123 }
124 None => {
125 self.by_source.insert(key.clone(), ep);
126 }
127 }
128 }
129 Ok(())
130 }
131
132 pub fn claim(&self, source: Option<&str>) -> Option<EpId> {
139 self.by_source.get(source?).copied()
140 }
141
142 pub fn len(&self) -> usize {
144 self.by_source.len()
145 }
146
147 pub fn is_empty(&self) -> bool {
149 self.by_source.is_empty()
150 }
151}
152
153pub fn build_ep_context_registry<'a, I>(eps: I) -> Result<EpContextRegistry>
166where
167 I: IntoIterator<Item = (EpId, &'a dyn ExecutionProvider)>,
168{
169 let mut registry = EpContextRegistry::new();
170 for (id, ep) in eps {
171 let keys = ep.context_source_keys();
172 if keys.is_empty() {
173 continue;
174 }
175 registry.register(id, &keys)?;
176 }
177 Ok(registry)
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crate::kernel::{Kernel, KernelMatch};
184 use crate::provider::{DeviceBuffer, EpConfig, Fence};
185 use onnx_runtime_ir::{DataType, DeviceId, DeviceType, Node, NodeId, Shape, TensorLayout};
186
187 struct MockCompiledEp {
191 source_keys: Vec<String>,
192 }
193
194 impl MockCompiledEp {
195 const BLOB: &'static [u8] = b"mock-compiled-context-v1";
196
197 fn new() -> Self {
198 Self {
200 source_keys: vec!["MOCK".to_string(), "MockExecutionProvider".to_string()],
201 }
202 }
203 }
204
205 impl ExecutionProvider for MockCompiledEp {
206 fn name(&self) -> &str {
207 "mock_compiled_ep"
208 }
209
210 fn device_type(&self) -> DeviceType {
211 DeviceType::Custom(0)
212 }
213
214 fn device_id(&self) -> DeviceId {
215 DeviceId::new(DeviceType::Custom(0), 0)
216 }
217
218 fn initialize(&mut self, _config: &EpConfig) -> Result<()> {
219 Ok(())
220 }
221
222 fn shutdown(&mut self) -> Result<()> {
223 Ok(())
224 }
225
226 fn supports_op(
227 &self,
228 _op: &Node,
229 _opset: u64,
230 _shapes: &[Shape],
231 _input_dtypes: &[DataType],
232 _layouts: &[TensorLayout],
233 ) -> KernelMatch {
234 KernelMatch::unsupported("test EP supports no ops")
235 }
236
237 fn get_kernel(
238 &self,
239 _op: &Node,
240 _shapes: &[Vec<usize>],
241 _opset: u64,
242 ) -> Result<Box<dyn Kernel>> {
243 Err(EpError::NoEpForOp {
244 domain: "ai.onnx".to_string(),
245 op_type: "<mock>".to_string(),
246 opset: _opset,
247 })
248 }
249
250 fn allocate(&self, _size: usize, _alignment: usize) -> Result<DeviceBuffer> {
251 Err(EpError::NotInitialized)
252 }
253
254 fn deallocate(&self, _buffer: DeviceBuffer) -> Result<()> {
255 Ok(())
256 }
257
258 fn copy(&self, _src: &DeviceBuffer, _dst: &mut DeviceBuffer, _size: usize) -> Result<()> {
259 Ok(())
260 }
261
262 fn copy_async(
263 &self,
264 _src: &DeviceBuffer,
265 _dst: &mut DeviceBuffer,
266 _size: usize,
267 ) -> Result<Fence> {
268 Ok(Fence::default())
269 }
270
271 fn sync(&self) -> Result<()> {
272 Ok(())
273 }
274
275 fn context_source_keys(&self) -> Vec<String> {
278 self.source_keys.clone()
279 }
280
281 fn save_context(&self) -> Result<EpContext> {
282 Ok(EpContext::new(
283 self.name(),
284 "1.2.3",
285 Self::BLOB.to_vec(),
286 vec![NodeId(7)],
287 "mock-device",
288 ))
289 }
290
291 fn load_context(&self, ctx: &EpContext) -> Result<()> {
292 if ctx.data == Self::BLOB {
293 Ok(())
294 } else {
295 Err(EpError::KernelFailed("mock: unexpected context blob".to_string()))
296 }
297 }
298 }
299
300 struct PlainEp;
302
303 impl ExecutionProvider for PlainEp {
304 fn name(&self) -> &str {
305 "plain_ep"
306 }
307 fn device_type(&self) -> DeviceType {
308 DeviceType::Cpu
309 }
310 fn device_id(&self) -> DeviceId {
311 DeviceId::cpu()
312 }
313 fn initialize(&mut self, _config: &EpConfig) -> Result<()> {
314 Ok(())
315 }
316 fn shutdown(&mut self) -> Result<()> {
317 Ok(())
318 }
319 fn supports_op(
320 &self,
321 _op: &Node,
322 _opset: u64,
323 _shapes: &[Shape],
324 _input_dtypes: &[DataType],
325 _layouts: &[TensorLayout],
326 ) -> KernelMatch {
327 KernelMatch::unsupported("test EP supports no ops")
328 }
329 fn get_kernel(
330 &self,
331 _op: &Node,
332 _shapes: &[Vec<usize>],
333 _opset: u64,
334 ) -> Result<Box<dyn Kernel>> {
335 Err(EpError::NoEpForOp {
336 domain: "ai.onnx".to_string(),
337 op_type: "<plain>".to_string(),
338 opset: _opset,
339 })
340 }
341 fn allocate(&self, _size: usize, _alignment: usize) -> Result<DeviceBuffer> {
342 Err(EpError::NotInitialized)
343 }
344 fn deallocate(&self, _buffer: DeviceBuffer) -> Result<()> {
345 Ok(())
346 }
347 fn copy(&self, _src: &DeviceBuffer, _dst: &mut DeviceBuffer, _size: usize) -> Result<()> {
348 Ok(())
349 }
350 fn copy_async(
351 &self,
352 _src: &DeviceBuffer,
353 _dst: &mut DeviceBuffer,
354 _size: usize,
355 ) -> Result<Fence> {
356 Ok(Fence::default())
357 }
358 fn sync(&self) -> Result<()> {
359 Ok(())
360 }
361 }
362
363 #[test]
364 fn register_and_claim_by_source_key() {
365 let mock = MockCompiledEp::new();
366 let mut reg = EpContextRegistry::new();
367 let ep_id = EpId(3);
368 reg.register(ep_id, &mock.context_source_keys()).unwrap();
369
370 assert_eq!(reg.claim(Some("MOCK")), Some(ep_id));
372 assert_eq!(reg.claim(Some("MockExecutionProvider")), Some(ep_id));
373 assert_eq!(reg.len(), 2);
374 }
375
376 #[test]
377 fn unmatched_and_absent_source_are_unclaimed() {
378 let mock = MockCompiledEp::new();
379 let mut reg = EpContextRegistry::new();
380 reg.register(EpId(0), &mock.context_source_keys()).unwrap();
381
382 assert_eq!(reg.claim(Some("QNN")), None);
384 assert_eq!(reg.claim(None), None);
386 }
387
388 #[test]
389 fn duplicate_source_key_is_rejected() {
390 let mut reg = EpContextRegistry::new();
391 reg.register(EpId(0), &["MOCK".to_string()]).unwrap();
392
393 let err = reg
395 .register(EpId(1), &["MOCK".to_string()])
396 .expect_err("duplicate source key must be rejected");
397 match err {
398 EpError::DuplicateContextSource {
399 source_key,
400 existing,
401 new,
402 } => {
403 assert_eq!(source_key, "MOCK");
404 assert_eq!(existing, EpId(0));
405 assert_eq!(new, EpId(1));
406 }
407 other => panic!("expected DuplicateContextSource, got {other:?}"),
408 }
409
410 assert_eq!(reg.claim(Some("MOCK")), Some(EpId(0)));
412 }
413
414 #[test]
415 fn re_registering_same_binding_is_idempotent() {
416 let mut reg = EpContextRegistry::new();
417 reg.register(EpId(2), &["MOCK".to_string()]).unwrap();
418 reg.register(EpId(2), &["MOCK".to_string()]).unwrap();
420 assert_eq!(reg.claim(Some("MOCK")), Some(EpId(2)));
421 assert_eq!(reg.len(), 1);
422 }
423
424 #[test]
425 fn build_registry_from_eps_skips_non_participants() {
426 let mock = MockCompiledEp::new();
427 let plain = PlainEp;
428 let eps: Vec<(EpId, &dyn ExecutionProvider)> =
429 vec![(EpId(0), &plain), (EpId(1), &mock)];
430
431 let reg = build_ep_context_registry(eps).unwrap();
432
433 assert_eq!(reg.len(), 2);
435 assert_eq!(reg.claim(Some("MOCK")), Some(EpId(1)));
436 assert_eq!(reg.claim(Some("MockExecutionProvider")), Some(EpId(1)));
437 }
438
439 #[test]
440 fn build_registry_from_eps_propagates_duplicate_error() {
441 let a = MockCompiledEp::new();
443 let b = MockCompiledEp::new();
444 let eps: Vec<(EpId, &dyn ExecutionProvider)> = vec![(EpId(0), &a), (EpId(1), &b)];
445
446 let err = build_ep_context_registry(eps)
447 .expect_err("two EPs on the same source key must conflict");
448 assert!(matches!(err, EpError::DuplicateContextSource { .. }));
449 }
450
451 #[test]
452 fn save_load_round_trip_preserves_bytes() {
453 let mock = MockCompiledEp::new();
454 let ctx = mock.save_context().unwrap();
455 assert_eq!(ctx.ep_name, "mock_compiled_ep");
456 assert_eq!(ctx.ep_version, "1.2.3");
457 assert_eq!(ctx.data, MockCompiledEp::BLOB);
458 assert_eq!(ctx.covered_nodes, vec![NodeId(7)]);
459 mock.load_context(&ctx).unwrap();
461
462 let mut bad = ctx.clone();
464 bad.data.push(0xFF);
465 assert!(mock.load_context(&bad).is_err());
466 }
467
468 #[test]
469 fn plain_ep_defaults_are_empty_and_unsupported() {
470 let plain = PlainEp;
471 assert!(plain.context_source_keys().is_empty());
472 assert!(matches!(
473 plain.save_context(),
474 Err(EpError::UnsupportedContext { .. })
475 ));
476
477 let ctx = EpContext::default();
478 assert!(matches!(
479 plain.load_context(&ctx),
480 Err(EpError::UnsupportedContext { .. })
481 ));
482 }
483
484 #[test]
485 fn no_ep_for_context_error_carries_source() {
486 let reg = EpContextRegistry::new();
488 let source = Some("QNN");
489 let err = reg
490 .claim(source)
491 .ok_or_else(|| EpError::NoEpForContext {
492 source_key: source.map(str::to_owned),
493 })
494 .unwrap_err();
495 match err {
496 EpError::NoEpForContext { source_key } => {
497 assert_eq!(source_key.as_deref(), Some("QNN"))
498 }
499 other => panic!("expected NoEpForContext, got {other:?}"),
500 }
501 }
502}