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::{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 _shapes: &[Shape],
230 _layouts: &[TensorLayout],
231 ) -> KernelMatch {
232 KernelMatch::Unsupported
233 }
234
235 fn get_kernel(
236 &self,
237 _op: &Node,
238 _shapes: &[Vec<usize>],
239 _opset: u64,
240 ) -> Result<Box<dyn Kernel>> {
241 Err(EpError::NoEpForOp {
242 op_type: "<mock>".to_string(),
243 })
244 }
245
246 fn allocate(&self, _size: usize, _alignment: usize) -> Result<DeviceBuffer> {
247 Err(EpError::NotInitialized)
248 }
249
250 fn deallocate(&self, _buffer: DeviceBuffer) -> Result<()> {
251 Ok(())
252 }
253
254 fn copy(&self, _src: &DeviceBuffer, _dst: &mut DeviceBuffer, _size: usize) -> Result<()> {
255 Ok(())
256 }
257
258 fn copy_async(
259 &self,
260 _src: &DeviceBuffer,
261 _dst: &mut DeviceBuffer,
262 _size: usize,
263 ) -> Result<Fence> {
264 Ok(Fence::default())
265 }
266
267 fn sync(&self) -> Result<()> {
268 Ok(())
269 }
270
271 fn context_source_keys(&self) -> Vec<String> {
274 self.source_keys.clone()
275 }
276
277 fn save_context(&self) -> Result<EpContext> {
278 Ok(EpContext::new(
279 self.name(),
280 "1.2.3",
281 Self::BLOB.to_vec(),
282 vec![NodeId(7)],
283 "mock-device",
284 ))
285 }
286
287 fn load_context(&self, ctx: &EpContext) -> Result<()> {
288 if ctx.data == Self::BLOB {
289 Ok(())
290 } else {
291 Err(EpError::KernelFailed("mock: unexpected context blob".to_string()))
292 }
293 }
294 }
295
296 struct PlainEp;
298
299 impl ExecutionProvider for PlainEp {
300 fn name(&self) -> &str {
301 "plain_ep"
302 }
303 fn device_type(&self) -> DeviceType {
304 DeviceType::Cpu
305 }
306 fn device_id(&self) -> DeviceId {
307 DeviceId::cpu()
308 }
309 fn initialize(&mut self, _config: &EpConfig) -> Result<()> {
310 Ok(())
311 }
312 fn shutdown(&mut self) -> Result<()> {
313 Ok(())
314 }
315 fn supports_op(
316 &self,
317 _op: &Node,
318 _shapes: &[Shape],
319 _layouts: &[TensorLayout],
320 ) -> KernelMatch {
321 KernelMatch::Unsupported
322 }
323 fn get_kernel(
324 &self,
325 _op: &Node,
326 _shapes: &[Vec<usize>],
327 _opset: u64,
328 ) -> Result<Box<dyn Kernel>> {
329 Err(EpError::NoEpForOp {
330 op_type: "<plain>".to_string(),
331 })
332 }
333 fn allocate(&self, _size: usize, _alignment: usize) -> Result<DeviceBuffer> {
334 Err(EpError::NotInitialized)
335 }
336 fn deallocate(&self, _buffer: DeviceBuffer) -> Result<()> {
337 Ok(())
338 }
339 fn copy(&self, _src: &DeviceBuffer, _dst: &mut DeviceBuffer, _size: usize) -> Result<()> {
340 Ok(())
341 }
342 fn copy_async(
343 &self,
344 _src: &DeviceBuffer,
345 _dst: &mut DeviceBuffer,
346 _size: usize,
347 ) -> Result<Fence> {
348 Ok(Fence::default())
349 }
350 fn sync(&self) -> Result<()> {
351 Ok(())
352 }
353 }
354
355 #[test]
356 fn register_and_claim_by_source_key() {
357 let mock = MockCompiledEp::new();
358 let mut reg = EpContextRegistry::new();
359 let ep_id = EpId(3);
360 reg.register(ep_id, &mock.context_source_keys()).unwrap();
361
362 assert_eq!(reg.claim(Some("MOCK")), Some(ep_id));
364 assert_eq!(reg.claim(Some("MockExecutionProvider")), Some(ep_id));
365 assert_eq!(reg.len(), 2);
366 }
367
368 #[test]
369 fn unmatched_and_absent_source_are_unclaimed() {
370 let mock = MockCompiledEp::new();
371 let mut reg = EpContextRegistry::new();
372 reg.register(EpId(0), &mock.context_source_keys()).unwrap();
373
374 assert_eq!(reg.claim(Some("QNN")), None);
376 assert_eq!(reg.claim(None), None);
378 }
379
380 #[test]
381 fn duplicate_source_key_is_rejected() {
382 let mut reg = EpContextRegistry::new();
383 reg.register(EpId(0), &["MOCK".to_string()]).unwrap();
384
385 let err = reg
387 .register(EpId(1), &["MOCK".to_string()])
388 .expect_err("duplicate source key must be rejected");
389 match err {
390 EpError::DuplicateContextSource {
391 source_key,
392 existing,
393 new,
394 } => {
395 assert_eq!(source_key, "MOCK");
396 assert_eq!(existing, EpId(0));
397 assert_eq!(new, EpId(1));
398 }
399 other => panic!("expected DuplicateContextSource, got {other:?}"),
400 }
401
402 assert_eq!(reg.claim(Some("MOCK")), Some(EpId(0)));
404 }
405
406 #[test]
407 fn re_registering_same_binding_is_idempotent() {
408 let mut reg = EpContextRegistry::new();
409 reg.register(EpId(2), &["MOCK".to_string()]).unwrap();
410 reg.register(EpId(2), &["MOCK".to_string()]).unwrap();
412 assert_eq!(reg.claim(Some("MOCK")), Some(EpId(2)));
413 assert_eq!(reg.len(), 1);
414 }
415
416 #[test]
417 fn build_registry_from_eps_skips_non_participants() {
418 let mock = MockCompiledEp::new();
419 let plain = PlainEp;
420 let eps: Vec<(EpId, &dyn ExecutionProvider)> =
421 vec![(EpId(0), &plain), (EpId(1), &mock)];
422
423 let reg = build_ep_context_registry(eps).unwrap();
424
425 assert_eq!(reg.len(), 2);
427 assert_eq!(reg.claim(Some("MOCK")), Some(EpId(1)));
428 assert_eq!(reg.claim(Some("MockExecutionProvider")), Some(EpId(1)));
429 }
430
431 #[test]
432 fn build_registry_from_eps_propagates_duplicate_error() {
433 let a = MockCompiledEp::new();
435 let b = MockCompiledEp::new();
436 let eps: Vec<(EpId, &dyn ExecutionProvider)> = vec![(EpId(0), &a), (EpId(1), &b)];
437
438 let err = build_ep_context_registry(eps)
439 .expect_err("two EPs on the same source key must conflict");
440 assert!(matches!(err, EpError::DuplicateContextSource { .. }));
441 }
442
443 #[test]
444 fn save_load_round_trip_preserves_bytes() {
445 let mock = MockCompiledEp::new();
446 let ctx = mock.save_context().unwrap();
447 assert_eq!(ctx.ep_name, "mock_compiled_ep");
448 assert_eq!(ctx.ep_version, "1.2.3");
449 assert_eq!(ctx.data, MockCompiledEp::BLOB);
450 assert_eq!(ctx.covered_nodes, vec![NodeId(7)]);
451 mock.load_context(&ctx).unwrap();
453
454 let mut bad = ctx.clone();
456 bad.data.push(0xFF);
457 assert!(mock.load_context(&bad).is_err());
458 }
459
460 #[test]
461 fn plain_ep_defaults_are_empty_and_unsupported() {
462 let plain = PlainEp;
463 assert!(plain.context_source_keys().is_empty());
464 assert!(matches!(
465 plain.save_context(),
466 Err(EpError::UnsupportedContext { .. })
467 ));
468
469 let ctx = EpContext::default();
470 assert!(matches!(
471 plain.load_context(&ctx),
472 Err(EpError::UnsupportedContext { .. })
473 ));
474 }
475
476 #[test]
477 fn no_ep_for_context_error_carries_source() {
478 let reg = EpContextRegistry::new();
480 let source = Some("QNN");
481 let err = reg
482 .claim(source)
483 .ok_or_else(|| EpError::NoEpForContext {
484 source_key: source.map(str::to_owned),
485 })
486 .unwrap_err();
487 match err {
488 EpError::NoEpForContext { source_key } => {
489 assert_eq!(source_key.as_deref(), Some("QNN"))
490 }
491 other => panic!("expected NoEpForContext, got {other:?}"),
492 }
493 }
494}