Skip to main content

tensor4all_tensorbackend/
context.rs

1//! Explicit and optional process-global tenferro CPU execution contexts.
2
3use std::cell::Cell;
4use std::sync::{Arc, Mutex, OnceLock};
5
6use tenferro::{CompiledGraph, GraphCompiler, Runtime, Tensor, TracedGraph};
7use tenferro_ad::{AdContext, EagerRuntime};
8use tenferro_cpu::{BufferPoolStats, CpuBackend};
9use tenferro_tensor::{BackendSession, BackendSessionHost};
10
11/// Error returned by explicit CPU context graph or eager-runtime operations.
12///
13/// The original tenferro diagnostic is retained as the error source.
14///
15/// # Examples
16///
17/// ```
18/// use std::error::Error;
19/// use std::sync::Arc;
20/// use tensor4all_tensorbackend::CpuExecutionContextError;
21///
22/// let error = CpuExecutionContextError::Initialization {
23///     component: "graph runtime",
24///     source: Arc::new(std::io::Error::other("registration failed")),
25/// };
26/// assert!(error.source().is_some());
27/// ```
28#[derive(Debug, Clone, thiserror::Error)]
29pub enum CpuExecutionContextError {
30    /// A context-owned graph or eager runtime could not be initialized.
31    #[error("failed to initialize {component}: {source}")]
32    Initialization {
33        /// Context component being initialized.
34        component: &'static str,
35        /// Original tenferro diagnostic.
36        #[source]
37        source: Arc<dyn std::error::Error + Send + Sync + 'static>,
38    },
39    /// Graph compilation or execution failed.
40    #[error("CPU graph {operation} failed: {source}")]
41    Graph {
42        /// Graph operation that failed.
43        operation: &'static str,
44        /// Original tenferro diagnostic.
45        #[source]
46        source: Arc<dyn std::error::Error + Send + Sync + 'static>,
47    },
48}
49
50const CANONICAL_SESSION_REENTRY_MESSAGE: &str = "recursive tensorbackend canonical session entry";
51
52thread_local! {
53    static CANONICAL_SESSION_ACTIVE: Cell<bool> = const { Cell::new(false) };
54}
55
56struct CanonicalSessionGuard {
57    previous: bool,
58}
59
60impl CanonicalSessionGuard {
61    fn assert_inactive() {
62        CANONICAL_SESSION_ACTIVE.with(|active| {
63            assert!(!active.get(), "{CANONICAL_SESSION_REENTRY_MESSAGE}");
64        });
65    }
66
67    fn enter() -> Self {
68        Self::assert_inactive();
69        CANONICAL_SESSION_ACTIVE.with(|active| Self {
70            previous: active.replace(true),
71        })
72    }
73}
74
75impl Drop for CanonicalSessionGuard {
76    fn drop(&mut self) {
77        CANONICAL_SESSION_ACTIVE.with(|active| active.set(self.previous));
78    }
79}
80
81impl CpuExecutionContextError {
82    fn initialization(
83        component: &'static str,
84        source: impl std::error::Error + Send + Sync + 'static,
85    ) -> Self {
86        Self::Initialization {
87            component,
88            source: Arc::new(source),
89        }
90    }
91
92    fn graph(
93        operation: &'static str,
94        source: impl std::error::Error + Send + Sync + 'static,
95    ) -> Self {
96        Self::Graph {
97            operation,
98            source: Arc::new(source),
99        }
100    }
101}
102
103struct GraphState {
104    compiler: GraphCompiler,
105    runtime: Runtime,
106    backend: CpuBackend,
107}
108
109/// Caller-owned CPU execution domain for plain, graph, and eager-AD work.
110///
111/// The supplied backend is the only source of CPU execution resources. Backend
112/// clones preserve its runtime identity; this constructor never uses
113/// `CpuBackend::new`, `CpuContext::from_env`, or a process-global fallback.
114/// Graph preparation caches and the eager runtime are owned by this context and
115/// are released when it is dropped.
116///
117/// # Examples
118///
119/// ```
120/// use tensor4all_tensorbackend::CpuExecutionContext;
121/// use tenferro_cpu::CpuBackend;
122///
123/// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
124/// let threads = context.with_backend(|backend| backend.num_threads());
125/// assert_eq!(threads, 1);
126/// # Ok::<(), Box<dyn std::error::Error>>(())
127/// ```
128pub struct CpuExecutionContext {
129    backend: Mutex<CpuBackend>,
130    graph: OnceLock<Result<Mutex<GraphState>, CpuExecutionContextError>>,
131    eager: OnceLock<Result<Arc<EagerRuntime>, CpuExecutionContextError>>,
132}
133
134impl std::fmt::Debug for CpuExecutionContext {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        f.debug_struct("CpuExecutionContext")
137            .field("graph_initialized", &self.graph.get().is_some())
138            .field("eager_initialized", &self.eager.get().is_some())
139            .finish_non_exhaustive()
140    }
141}
142
143impl CpuExecutionContext {
144    /// Create an execution context from a caller-selected CPU backend.
145    ///
146    /// Runtime construction is lazy, so creating a context cannot fail and does
147    /// not allocate another executor or consult environment configuration.
148    pub fn from_backend(backend: CpuBackend) -> Self {
149        Self {
150            backend: Mutex::new(backend),
151            graph: OnceLock::new(),
152            eager: OnceLock::new(),
153        }
154    }
155
156    /// Run a plain tensor operation with this context's backend.
157    ///
158    /// The closure runs while the context-local backend lock is held. A poisoned
159    /// lock is recovered because tenferro validates every new backend session.
160    pub fn with_backend<R>(&self, f: impl FnOnce(&mut CpuBackend) -> R) -> R {
161        let mut backend = match self.backend.lock() {
162            Ok(guard) => guard,
163            Err(poisoned) => poisoned.into_inner(),
164        };
165        f(&mut backend)
166    }
167
168    pub(crate) fn with_session<R: Send>(
169        &self,
170        f: impl FnOnce(&mut dyn BackendSession) -> R + Send,
171    ) -> R {
172        CanonicalSessionGuard::assert_inactive();
173        let mut backend = match self.backend.lock() {
174            Ok(guard) => guard,
175            Err(poisoned) => poisoned.into_inner(),
176        };
177        backend.with_backend_session(|session| {
178            let _guard = CanonicalSessionGuard::enter();
179            f(session)
180        })
181    }
182
183    fn backend_clone(&self) -> CpuBackend {
184        self.with_backend(|backend| backend.clone())
185    }
186
187    fn graph_state(&self) -> Result<&Mutex<GraphState>, CpuExecutionContextError> {
188        self.graph
189            .get_or_init(|| {
190                let backend = self.backend_clone();
191                build_graph_runtime(&backend).map(|runtime| {
192                    Mutex::new(GraphState {
193                        compiler: GraphCompiler::new(),
194                        runtime,
195                        backend,
196                    })
197                })
198            })
199            .as_ref()
200            .map_err(Clone::clone)
201    }
202
203    fn with_graph_state<R>(
204        &self,
205        f: impl FnOnce(&mut GraphCompiler, &mut Runtime, &mut CpuBackend) -> R,
206    ) -> Result<R, CpuExecutionContextError> {
207        let mut graph = match self.graph_state()?.lock() {
208            Ok(guard) => guard,
209            Err(poisoned) => poisoned.into_inner(),
210        };
211        let GraphState {
212            compiler,
213            runtime,
214            backend,
215        } = &mut *graph;
216        Ok(f(compiler, runtime, backend))
217    }
218
219    /// Compile a backend-neutral traced graph using this context's compiler cache.
220    ///
221    /// # Errors
222    ///
223    /// Returns [`CpuExecutionContextError`] when graph-runtime initialization or
224    /// graph compilation fails.
225    pub fn compile_graph(
226        &self,
227        graph: &TracedGraph,
228    ) -> Result<CompiledGraph, CpuExecutionContextError> {
229        self.with_graph_state(|compiler, _, _| compiler.compile_traced_graph(graph))?
230            .map_err(|source| CpuExecutionContextError::graph("compilation", source))
231    }
232
233    /// Execute a compiled graph in this context's runtime and prepared-plan cache.
234    ///
235    /// `CompiledGraph` is backend-neutral. Backend-prepared executables and
236    /// workspaces never leave this context-owned runtime.
237    ///
238    /// # Errors
239    ///
240    /// Returns [`CpuExecutionContextError`] when runtime initialization,
241    /// preparation, or execution fails.
242    pub fn run_graph(
243        &self,
244        graph: &CompiledGraph,
245        inputs: &[&Tensor],
246    ) -> Result<Vec<Tensor>, CpuExecutionContextError> {
247        self.with_graph_state(|_, runtime, _| runtime.run_compiled(graph, inputs))?
248            .map_err(|source| CpuExecutionContextError::graph("execution", source))
249    }
250
251    /// Return this context's eager reverse-AD runtime.
252    ///
253    /// Repeated calls return the same runtime and therefore the same eager
254    /// compilation cache.
255    ///
256    /// # Errors
257    ///
258    /// Returns [`CpuExecutionContextError`] when linalg AD-rule or eager-runtime
259    /// registration fails.
260    pub fn eager_runtime(&self) -> Result<Arc<EagerRuntime>, CpuExecutionContextError> {
261        self.eager
262            .get_or_init(|| build_eager_runtime(self.backend_clone()))
263            .as_ref()
264            .map(Arc::clone)
265            .map_err(Clone::clone)
266    }
267
268    /// Return statistics for this context's runtime-owned graph caches.
269    ///
270    /// # Errors
271    ///
272    /// Returns [`CpuExecutionContextError`] when graph initialization or the
273    /// cache statistics query fails.
274    pub fn graph_cache_stats(
275        &self,
276    ) -> Result<tenferro::RuntimeCacheStats, CpuExecutionContextError> {
277        self.with_graph_state(|_, runtime, _| runtime.cache_stats())?
278            .map_err(|source| CpuExecutionContextError::graph("cache statistics", source))
279    }
280
281    /// Return retained-buffer statistics for this context's graph backend.
282    ///
283    /// # Errors
284    ///
285    /// Returns [`CpuExecutionContextError`] when graph initialization or the
286    /// backend statistics query fails.
287    pub fn graph_buffer_pool_stats(&self) -> Result<BufferPoolStats, CpuExecutionContextError> {
288        self.with_graph_state(|_, _, backend| backend.buffer_pool_stats())?
289            .map_err(|source| CpuExecutionContextError::graph("buffer-pool statistics", source))
290    }
291
292    /// Release retained buffers owned by this context's graph backend.
293    ///
294    /// # Errors
295    ///
296    /// Returns [`CpuExecutionContextError`] when graph initialization or reset
297    /// fails.
298    pub fn reset_graph_buffer_pool(&self) -> Result<(), CpuExecutionContextError> {
299        self.with_graph_state(|_, _, backend| backend.reset_buffer_pool())?
300            .map_err(|source| CpuExecutionContextError::graph("buffer-pool reset", source))
301    }
302
303    /// Recreate this context's graph runtime and release its prepared caches.
304    ///
305    /// # Errors
306    ///
307    /// Returns [`CpuExecutionContextError`] when runtime reconstruction or
308    /// buffer release fails.
309    pub fn reset_graph_runtime(&self) -> Result<(), CpuExecutionContextError> {
310        self.with_graph_state(|compiler, runtime, backend| {
311            let replacement = build_graph_runtime(backend)?;
312            *compiler = GraphCompiler::new();
313            let old = std::mem::replace(runtime, replacement);
314            drop(old);
315            backend
316                .reset_buffer_pool()
317                .map_err(|source| CpuExecutionContextError::graph("buffer-pool reset", source))
318        })??;
319        Ok(())
320    }
321}
322
323fn build_graph_runtime(backend: &CpuBackend) -> Result<Runtime, CpuExecutionContextError> {
324    let mut builder = Runtime::builder();
325    builder
326        .register_engine(
327            tenferro_cpu::runtime_engine_registration(backend).map_err(|source| {
328                CpuExecutionContextError::initialization("graph CPU engine", source)
329            })?,
330        )
331        .map_err(|source| CpuExecutionContextError::initialization("graph CPU engine", source))?;
332    builder
333        .install_extension_module(
334            tenferro_einsum::extension_module::<CpuBackend>(
335                tenferro_cpu::runtime_engine_id().map_err(|source| {
336                    CpuExecutionContextError::initialization("einsum extension", source)
337                })?,
338            )
339            .map_err(|source| {
340                CpuExecutionContextError::initialization("einsum extension", source)
341            })?,
342        )
343        .map_err(|source| CpuExecutionContextError::initialization("einsum extension", source))?;
344    builder
345        .build()
346        .map_err(|source| CpuExecutionContextError::initialization("graph runtime", source))
347}
348
349fn build_eager_runtime(backend: CpuBackend) -> Result<Arc<EagerRuntime>, CpuExecutionContextError> {
350    let ad_context = AdContext::builder()
351        .with_semantic_extension_rules(tenferro_linalg::semantic_ad_rules().map_err(|source| {
352            CpuExecutionContextError::initialization("linalg AD rules", source)
353        })?)
354        .map_err(|source| CpuExecutionContextError::initialization("linalg AD rules", source))?
355        .build()
356        .map_err(|source| CpuExecutionContextError::initialization("AD context", source))?;
357    EagerRuntime::with_cpu_backend_and_ad_context(backend, &ad_context)
358        .map_err(|source| CpuExecutionContextError::initialization("eager runtime", source))
359}
360
361#[cfg(feature = "global-defaults")]
362mod defaults {
363    use super::*;
364    use tenferro_cpu::CpuContext;
365
366    static DEFAULT_CONTEXT: OnceLock<Arc<CpuExecutionContext>> = OnceLock::new();
367
368    #[cfg(test)]
369    thread_local! {
370        static FORCE_EAGER_CONTEXT_FAILURE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
371    }
372
373    #[cfg(test)]
374    static DEFAULT_CONTEXT_HITS: std::sync::atomic::AtomicUsize =
375        std::sync::atomic::AtomicUsize::new(0);
376
377    fn default_context() -> &'static Arc<CpuExecutionContext> {
378        DEFAULT_CONTEXT.get_or_init(|| {
379            #[cfg(test)]
380            DEFAULT_CONTEXT_HITS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
381            Arc::new(CpuExecutionContext::from_backend(CpuBackend::from_context(
382                Arc::new(CpuContext::from_env()),
383            )))
384        })
385    }
386
387    /// Error returned when the process-global eager AD runtime cannot be initialized.
388    ///
389    /// # Examples
390    ///
391    /// ```
392    /// use std::error::Error;
393    /// use std::sync::Arc;
394    /// use tensor4all_tensorbackend::EagerContextError;
395    ///
396    /// let error = EagerContextError::Registration {
397    ///     source: Arc::new(std::io::Error::other("registration failed")),
398    /// };
399    /// assert!(error.source().is_some());
400    /// ```
401    #[derive(Debug, Clone, thiserror::Error)]
402    pub enum EagerContextError {
403        /// The tenferro linalg AD extension rule could not be registered.
404        #[error("failed to register tenferro linalg AD rule: {source}")]
405        Registration {
406            /// Original diagnostic returned by tenferro.
407            #[source]
408            source: Arc<dyn std::error::Error + Send + Sync + 'static>,
409        },
410    }
411
412    /// Run a closure against the optional process-global CPU backend.
413    pub fn with_default_backend<R>(f: impl FnOnce(&mut CpuBackend) -> R) -> R {
414        default_context().with_backend(f)
415    }
416
417    pub(crate) fn with_default_session<R: Send>(
418        f: impl FnOnce(&mut dyn BackendSession) -> R + Send,
419    ) -> R {
420        default_context().with_session(f)
421    }
422
423    pub(crate) fn with_default_graph_runtime<R>(
424        f: impl FnOnce(&mut GraphCompiler, &Runtime, &mut CpuBackend) -> R,
425    ) -> anyhow::Result<R> {
426        default_context()
427            .with_graph_state(|compiler, runtime, backend| f(compiler, runtime, backend))
428            .map_err(anyhow::Error::new)
429    }
430
431    pub(crate) fn default_engine_buffer_pool_stats() -> anyhow::Result<BufferPoolStats> {
432        default_context()
433            .graph_buffer_pool_stats()
434            .map_err(anyhow::Error::new)
435    }
436
437    pub(crate) fn reset_default_engine_buffer_pool() -> anyhow::Result<()> {
438        default_context()
439            .reset_graph_buffer_pool()
440            .map_err(anyhow::Error::new)
441    }
442
443    pub(crate) fn reset_default_engine() -> anyhow::Result<()> {
444        default_context()
445            .reset_graph_runtime()
446            .map_err(anyhow::Error::new)
447    }
448
449    /// Return the optional process-global eager context used by convenience APIs.
450    ///
451    /// # Errors
452    ///
453    /// Returns [`EagerContextError::Registration`] when eager runtime
454    /// initialization fails.
455    ///
456    /// # Examples
457    ///
458    /// ```
459    /// use std::sync::Arc;
460    /// use tensor4all_tensorbackend::default_eager_ctx;
461    ///
462    /// let first = default_eager_ctx().unwrap();
463    /// let second = default_eager_ctx().unwrap();
464    /// assert!(Arc::ptr_eq(&first, &second));
465    /// ```
466    pub fn default_eager_ctx() -> Result<Arc<EagerRuntime>, EagerContextError> {
467        #[cfg(test)]
468        if FORCE_EAGER_CONTEXT_FAILURE.with(std::cell::Cell::get) {
469            return Err(EagerContextError::Registration {
470                source: Arc::new(std::io::Error::other(
471                    "forced default eager context registration failure",
472                )),
473            });
474        }
475        default_context()
476            .eager_runtime()
477            .map_err(|source| EagerContextError::Registration {
478                source: Arc::new(source),
479            })
480    }
481
482    #[cfg(test)]
483    pub(crate) fn default_context_hits() -> usize {
484        DEFAULT_CONTEXT_HITS.load(std::sync::atomic::Ordering::Relaxed)
485    }
486
487    #[cfg(test)]
488    pub(crate) fn with_forced_eager_context_failure<T>(f: impl FnOnce() -> T) -> T {
489        let previous = FORCE_EAGER_CONTEXT_FAILURE.with(|failure| failure.replace(true));
490        let result = f();
491        FORCE_EAGER_CONTEXT_FAILURE.with(|failure| failure.set(previous));
492        result
493    }
494}
495
496#[cfg(all(test, feature = "global-defaults"))]
497pub(crate) use defaults::with_forced_eager_context_failure;
498#[cfg(feature = "global-defaults")]
499pub use defaults::{default_eager_ctx, with_default_backend, EagerContextError};
500#[cfg(feature = "global-defaults")]
501pub(crate) use defaults::{
502    default_engine_buffer_pool_stats, reset_default_engine, reset_default_engine_buffer_pool,
503    with_default_graph_runtime, with_default_session,
504};
505
506#[cfg(test)]
507mod tests {
508    use std::num::NonZeroUsize;
509    use std::sync::mpsc;
510    use std::time::Duration;
511
512    use super::*;
513    use tenferro::program::{CoreSemanticOp, ProgramInputSpec};
514    use tenferro::{DType, TensorSessionOpsExt, TraceContext};
515    use tenferro_ad::EagerTensor;
516    use tenferro_cpu::{CpuContext, ExternalCpuDomain};
517    use tenferro_tensor::CpuDomainId;
518
519    fn context() -> CpuExecutionContext {
520        CpuExecutionContext::from_backend(CpuBackend::with_threads(1).unwrap())
521    }
522
523    #[test]
524    fn explicit_session_runs_a_concrete_operation() {
525        let context = context();
526        let lhs = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap();
527        let rhs = Tensor::from_vec_col_major(vec![2, 1], vec![5.0_f64, 6.0]).unwrap();
528        let result = context
529            .with_session(|session| lhs.matmul(&rhs, session))
530            .unwrap();
531
532        assert_eq!(result.as_slice::<f64>().unwrap(), &[23.0, 34.0]);
533    }
534
535    #[test]
536    fn recursive_session_entry_fails_before_lock_and_restores_guard() {
537        let context = context();
538        let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
539            context.with_session(|_| context.with_session(|_| ()))
540        }))
541        .expect_err("recursive canonical session entry should panic");
542        let message = panic
543            .downcast_ref::<&str>()
544            .copied()
545            .or_else(|| panic.downcast_ref::<String>().map(String::as_str))
546            .expect("recursive entry panic should contain a string message");
547        assert_eq!(message, CANONICAL_SESSION_REENTRY_MESSAGE);
548
549        assert_eq!(context.with_session(|_| 7usize), 7);
550    }
551
552    #[test]
553    fn explicit_plain_graph_and_eager_paths_share_only_the_supplied_backend() {
554        let context = context();
555        assert!(format!("{context:?}").contains("graph_initialized: false"));
556        assert_eq!(context.with_backend(|backend| backend.num_threads()), 1);
557
558        let mut trace = TraceContext::new();
559        let input = trace
560            .input(ProgramInputSpec::new(DType::F64, [2_usize.into()]))
561            .unwrap();
562        let output = trace.add_op(CoreSemanticOp::Neg, &[input]).unwrap()[0];
563        let graph = trace.finish(&[output]).unwrap();
564        let compiled = context.compile_graph(&graph).unwrap();
565        let input = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, -2.0]).unwrap();
566        let output = context.run_graph(&compiled, &[&input]).unwrap();
567        assert_eq!(output[0].as_slice::<f64>().unwrap(), &[-1.0, 2.0]);
568        context.run_graph(&compiled, &[&input]).unwrap();
569        let cached = context.graph_cache_stats().unwrap().prepared_plans;
570        assert!(cached.entries > 0);
571        assert!(cached.hits > 0);
572        context.reset_graph_runtime().unwrap();
573        assert_eq!(
574            context.graph_cache_stats().unwrap().prepared_plans.entries,
575            0
576        );
577
578        let eager = context.eager_runtime().unwrap();
579        assert!(Arc::ptr_eq(&eager, &context.eager_runtime().unwrap()));
580    }
581
582    #[test]
583    fn separate_eager_contexts_reject_cross_context_operations() {
584        let first = context().eager_runtime().unwrap();
585        let second = context().eager_runtime().unwrap();
586        let a = EagerTensor::from_tensor_in(
587            Tensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap(),
588            first,
589        )
590        .unwrap();
591        let b = EagerTensor::from_tensor_in(
592            Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap(),
593            second,
594        )
595        .unwrap();
596        assert!(matches!(
597            a.add(&b),
598            Err(tenferro_ad::Error::ContextMismatch { .. })
599        ));
600    }
601
602    #[test]
603    fn caller_managed_backend_remains_caller_owned_after_context_drop() {
604        let executor = Arc::new(CpuContext::with_threads(1).unwrap());
605        let id = CpuDomainId::new(7);
606        let domain =
607            ExternalCpuDomain::new_caller_managed(id, executor.clone(), NonZeroUsize::MIN).unwrap();
608        let backend = CpuBackend::from_external_managed_domains(id, [domain]).unwrap();
609        let context = CpuExecutionContext::from_backend(backend);
610        assert_eq!(context.with_backend(|backend| backend.num_threads()), 1);
611        drop(context);
612        assert_eq!(executor.num_threads(), 1);
613    }
614
615    #[test]
616    fn independent_contexts_do_not_share_a_backend_mutex() {
617        let first = Arc::new(context());
618        let second = Arc::new(context());
619        let (entered_tx, entered_rx) = mpsc::channel();
620        let (release_tx, release_rx) = mpsc::channel();
621        let release_rx = Arc::new(Mutex::new(release_rx));
622        let handles = [first, second].map(|context| {
623            let entered_tx = entered_tx.clone();
624            let release_rx = Arc::clone(&release_rx);
625            std::thread::spawn(move || {
626                context.with_backend(|_| {
627                    entered_tx.send(()).unwrap();
628                    release_rx.lock().unwrap().recv().unwrap();
629                });
630            })
631        });
632        entered_rx.recv_timeout(Duration::from_secs(2)).unwrap();
633        entered_rx.recv_timeout(Duration::from_secs(2)).unwrap();
634        release_tx.send(()).unwrap();
635        release_tx.send(()).unwrap();
636        for handle in handles {
637            handle.join().unwrap();
638        }
639    }
640
641    #[cfg(feature = "global-defaults")]
642    #[test]
643    fn explicit_paths_do_not_initialize_the_default_context() {
644        let before = defaults::default_context_hits();
645        let context = context();
646        context.with_backend(|backend| assert_eq!(backend.num_threads(), 1));
647        context.eager_runtime().unwrap();
648        assert_eq!(defaults::default_context_hits(), before);
649    }
650}