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(
296 "mock: unexpected context blob".to_string(),
297 ))
298 }
299 }
300 }
301
302 struct PlainEp;
304
305 impl ExecutionProvider for PlainEp {
306 fn name(&self) -> &str {
307 "plain_ep"
308 }
309 fn device_type(&self) -> DeviceType {
310 DeviceType::Cpu
311 }
312 fn device_id(&self) -> DeviceId {
313 DeviceId::cpu()
314 }
315 fn initialize(&mut self, _config: &EpConfig) -> Result<()> {
316 Ok(())
317 }
318 fn shutdown(&mut self) -> Result<()> {
319 Ok(())
320 }
321 fn supports_op(
322 &self,
323 _op: &Node,
324 _opset: u64,
325 _shapes: &[Shape],
326 _input_dtypes: &[DataType],
327 _layouts: &[TensorLayout],
328 ) -> KernelMatch {
329 KernelMatch::unsupported("test EP supports no ops")
330 }
331 fn get_kernel(
332 &self,
333 _op: &Node,
334 _shapes: &[Vec<usize>],
335 _opset: u64,
336 ) -> Result<Box<dyn Kernel>> {
337 Err(EpError::NoEpForOp {
338 domain: "ai.onnx".to_string(),
339 op_type: "<plain>".to_string(),
340 opset: _opset,
341 })
342 }
343 fn allocate(&self, _size: usize, _alignment: usize) -> Result<DeviceBuffer> {
344 Err(EpError::NotInitialized)
345 }
346 fn deallocate(&self, _buffer: DeviceBuffer) -> Result<()> {
347 Ok(())
348 }
349 fn copy(&self, _src: &DeviceBuffer, _dst: &mut DeviceBuffer, _size: usize) -> Result<()> {
350 Ok(())
351 }
352 fn copy_async(
353 &self,
354 _src: &DeviceBuffer,
355 _dst: &mut DeviceBuffer,
356 _size: usize,
357 ) -> Result<Fence> {
358 Ok(Fence::default())
359 }
360 fn sync(&self) -> Result<()> {
361 Ok(())
362 }
363 }
364
365 #[test]
366 fn register_and_claim_by_source_key() {
367 let mock = MockCompiledEp::new();
368 let mut reg = EpContextRegistry::new();
369 let ep_id = EpId(3);
370 reg.register(ep_id, &mock.context_source_keys()).unwrap();
371
372 assert_eq!(reg.claim(Some("MOCK")), Some(ep_id));
374 assert_eq!(reg.claim(Some("MockExecutionProvider")), Some(ep_id));
375 assert_eq!(reg.len(), 2);
376 }
377
378 #[test]
379 fn unmatched_and_absent_source_are_unclaimed() {
380 let mock = MockCompiledEp::new();
381 let mut reg = EpContextRegistry::new();
382 reg.register(EpId(0), &mock.context_source_keys()).unwrap();
383
384 assert_eq!(reg.claim(Some("QNN")), None);
386 assert_eq!(reg.claim(None), None);
388 }
389
390 #[test]
391 fn duplicate_source_key_is_rejected() {
392 let mut reg = EpContextRegistry::new();
393 reg.register(EpId(0), &["MOCK".to_string()]).unwrap();
394
395 let err = reg
397 .register(EpId(1), &["MOCK".to_string()])
398 .expect_err("duplicate source key must be rejected");
399 match err {
400 EpError::DuplicateContextSource {
401 source_key,
402 existing,
403 new,
404 } => {
405 assert_eq!(source_key, "MOCK");
406 assert_eq!(existing, EpId(0));
407 assert_eq!(new, EpId(1));
408 }
409 other => panic!("expected DuplicateContextSource, got {other:?}"),
410 }
411
412 assert_eq!(reg.claim(Some("MOCK")), Some(EpId(0)));
414 }
415
416 #[test]
417 fn re_registering_same_binding_is_idempotent() {
418 let mut reg = EpContextRegistry::new();
419 reg.register(EpId(2), &["MOCK".to_string()]).unwrap();
420 reg.register(EpId(2), &["MOCK".to_string()]).unwrap();
422 assert_eq!(reg.claim(Some("MOCK")), Some(EpId(2)));
423 assert_eq!(reg.len(), 1);
424 }
425
426 #[test]
427 fn build_registry_from_eps_skips_non_participants() {
428 let mock = MockCompiledEp::new();
429 let plain = PlainEp;
430 let eps: Vec<(EpId, &dyn ExecutionProvider)> = vec![(EpId(0), &plain), (EpId(1), &mock)];
431
432 let reg = build_ep_context_registry(eps).unwrap();
433
434 assert_eq!(reg.len(), 2);
436 assert_eq!(reg.claim(Some("MOCK")), Some(EpId(1)));
437 assert_eq!(reg.claim(Some("MockExecutionProvider")), Some(EpId(1)));
438 }
439
440 #[test]
441 fn build_registry_from_eps_propagates_duplicate_error() {
442 let a = MockCompiledEp::new();
444 let b = MockCompiledEp::new();
445 let eps: Vec<(EpId, &dyn ExecutionProvider)> = vec![(EpId(0), &a), (EpId(1), &b)];
446
447 let err = build_ep_context_registry(eps)
448 .expect_err("two EPs on the same source key must conflict");
449 assert!(matches!(err, EpError::DuplicateContextSource { .. }));
450 }
451
452 #[test]
453 fn save_load_round_trip_preserves_bytes() {
454 let mock = MockCompiledEp::new();
455 let ctx = mock.save_context().unwrap();
456 assert_eq!(ctx.ep_name, "mock_compiled_ep");
457 assert_eq!(ctx.ep_version, "1.2.3");
458 assert_eq!(ctx.data, MockCompiledEp::BLOB);
459 assert_eq!(ctx.covered_nodes, vec![NodeId(7)]);
460 mock.load_context(&ctx).unwrap();
462
463 let mut bad = ctx.clone();
465 bad.data.push(0xFF);
466 assert!(mock.load_context(&bad).is_err());
467 }
468
469 #[test]
470 fn plain_ep_defaults_are_empty_and_unsupported() {
471 let plain = PlainEp;
472 assert!(plain.context_source_keys().is_empty());
473 assert!(matches!(
474 plain.save_context(),
475 Err(EpError::UnsupportedContext { .. })
476 ));
477
478 let ctx = EpContext::default();
479 assert!(matches!(
480 plain.load_context(&ctx),
481 Err(EpError::UnsupportedContext { .. })
482 ));
483 }
484
485 #[test]
486 fn no_ep_for_context_error_carries_source() {
487 let reg = EpContextRegistry::new();
489 let source = Some("QNN");
490 let err = reg
491 .claim(source)
492 .ok_or_else(|| EpError::NoEpForContext {
493 source_key: source.map(str::to_owned),
494 })
495 .unwrap_err();
496 match err {
497 EpError::NoEpForContext { source_key } => {
498 assert_eq!(source_key.as_deref(), Some("QNN"))
499 }
500 other => panic!("expected NoEpForContext, got {other:?}"),
501 }
502 }
503}