1use 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#[derive(Debug, Clone, thiserror::Error)]
29pub enum CpuExecutionContextError {
30 #[error("failed to initialize {component}: {source}")]
32 Initialization {
33 component: &'static str,
35 #[source]
37 source: Arc<dyn std::error::Error + Send + Sync + 'static>,
38 },
39 #[error("CPU graph {operation} failed: {source}")]
41 Graph {
42 operation: &'static str,
44 #[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
109pub 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 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 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 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 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 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 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 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 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 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 #[derive(Debug, Clone, thiserror::Error)]
402 pub enum EagerContextError {
403 #[error("failed to register tenferro linalg AD rule: {source}")]
405 Registration {
406 #[source]
408 source: Arc<dyn std::error::Error + Send + Sync + 'static>,
409 },
410 }
411
412 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 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}