Skip to main content

tensor4all_tensorbackend/
context.rs

1//! Explicit and optional process-global tenferro CPU execution contexts.
2
3use std::cell::RefCell;
4#[cfg(feature = "global-defaults")]
5use std::collections::HashMap;
6use std::num::NonZeroUsize;
7use std::ops::{Deref, DerefMut};
8use std::sync::atomic::{AtomicU64, Ordering};
9#[cfg(feature = "global-defaults")]
10use std::sync::Weak;
11use std::sync::{Arc, Mutex, OnceLock};
12
13use tenferro::{
14    CompiledGraph, ExecutionEngine, GraphCompiler, Runtime, Tensor, TensorRead, TracedGraph,
15};
16#[cfg(feature = "global-defaults")]
17use tenferro_ad::ContextId;
18use tenferro_ad::{AdContext, EagerRuntime};
19use tenferro_cpu::{BufferPoolStats, CpuEngine, Pool};
20
21/// Linux CPU-affinity FFI, kept in one place so every `unsafe` block has one
22/// auditable owner.
23///
24/// The mask is a growable word buffer in the kernel's `sched_*affinity` layout.
25/// No fixed `cpu_set_t` is used: a mask larger than glibc's 1024-CPU set is
26/// retried with a larger buffer instead of failing, and CPU ids are validated
27/// before they index the buffer.
28#[cfg(target_os = "linux")]
29mod affinity {
30    use std::io;
31
32    use super::CpuThreadError;
33
34    const BITS_PER_WORD: usize = libc::c_ulong::BITS as usize;
35    const INITIAL_WORDS: usize = 1;
36    /// 512 KiB of mask on a 64-bit host, far beyond any current CPU count.
37    const MAX_WORDS: usize = 1 << 16;
38
39    /// One CPU set in the layout `sched_getaffinity` and `sched_setaffinity`
40    /// expect. The words are owned by the `Vec`, so the buffer is word-aligned.
41    #[derive(Clone, Debug)]
42    pub(super) struct CpuMask {
43        words: Vec<libc::c_ulong>,
44    }
45
46    impl CpuMask {
47        /// The mask holding exactly `cpu`, with at least `min_words` words.
48        ///
49        /// Callers validate `cpu` against the allowed set first, so the small
50        /// allocation is bounded by the current kernel mask. `min_words` keeps
51        /// the buffer at the kernel's mask size on hosts where the highest
52        /// allowed CPU is in a low word.
53        pub(super) fn single(cpu: usize, min_words: usize) -> Self {
54            let word = cpu / BITS_PER_WORD;
55            let bit = cpu % BITS_PER_WORD;
56            let mut words = vec![0; (word + 1).max(min_words)];
57            words[word] |= 1 << bit;
58            Self { words }
59        }
60
61        /// The number of words in this mask's kernel buffer.
62        pub(super) fn words_len(&self) -> usize {
63            self.words.len()
64        }
65
66        /// The CPU ids in this mask, in ascending order.
67        pub(super) fn cpus(&self) -> Vec<usize> {
68            let mut cpus = Vec::new();
69            for (index, &word) in self.words.iter().enumerate() {
70                let mut bits = word;
71                while bits != 0 {
72                    let bit = bits.trailing_zeros() as usize;
73                    cpus.push(index * BITS_PER_WORD + bit);
74                    bits &= bits - 1;
75                }
76            }
77            cpus
78        }
79
80        /// Whether this mask holds exactly the same CPUs as `other`.
81        pub(super) fn equals(&self, other: &Self) -> bool {
82            let len = self.words.len().max(other.words.len());
83            (0..len).all(|index| self.word(index) == other.word(index))
84        }
85
86        fn word(&self, index: usize) -> libc::c_ulong {
87            self.words.get(index).copied().unwrap_or(0)
88        }
89
90        fn as_bytes(&self) -> usize {
91            std::mem::size_of_val(self.words.as_slice())
92        }
93
94        fn as_ptr(&self) -> *const libc::c_void {
95            self.words.as_ptr().cast()
96        }
97    }
98
99    /// Read the mask of the thread identified by `pid` (`0` is the calling
100    /// thread).
101    fn get_mask(pid: libc::pid_t) -> io::Result<CpuMask> {
102        let mut words = vec![0 as libc::c_ulong; INITIAL_WORDS];
103        loop {
104            let bytes = std::mem::size_of_val(words.as_slice());
105            // SAFETY: `words` is an owned, word-aligned buffer and `bytes` is
106            // its exact byte length; the kernel writes at most that many bytes.
107            let result = unsafe { libc::sched_getaffinity(pid, bytes, words.as_mut_ptr().cast()) };
108            if result == 0 {
109                // Keep the successful kernel buffer size: `sched_setaffinity`
110                // requires at least that many bytes even when the high words
111                // are empty.
112                return Ok(CpuMask { words });
113            }
114            let error = io::Error::last_os_error();
115            if error.raw_os_error() != Some(libc::EINVAL) {
116                return Err(error);
117            }
118            let next = words.len().checked_mul(2).filter(|&len| len <= MAX_WORDS);
119            let Some(next) = next else {
120                return Err(io::Error::other(
121                    "CPU affinity mask is larger than the supported buffer",
122                ));
123            };
124            words.resize(next, 0);
125        }
126    }
127
128    /// Replace the mask of the thread identified by `pid` (`0` is the calling
129    /// thread) with `mask`.
130    fn set_mask(pid: libc::pid_t, mask: &CpuMask) -> io::Result<()> {
131        // SAFETY: `mask` owns a word-aligned buffer and `as_bytes` is its exact
132        // byte length; the kernel reads at most that many bytes.
133        let result = unsafe { libc::sched_setaffinity(pid, mask.as_bytes(), mask.as_ptr().cast()) };
134        if result == 0 {
135            Ok(())
136        } else {
137            Err(io::Error::last_os_error())
138        }
139    }
140
141    /// The main thread's mask at construction, a snapshot taken with
142    /// `sched_getaffinity(getpid())`.
143    ///
144    /// The process id names the thread-group leader, so a pool built lazily
145    /// from a worker of an already-pinned pool does not inherit that worker's
146    /// single CPU.
147    pub(super) fn main_thread_mask() -> Result<CpuMask, CpuThreadError> {
148        get_mask(std::process::id() as libc::pid_t).map_err(|source| CpuThreadError::Affinity {
149            operation: "sched_getaffinity",
150            source,
151        })
152    }
153
154    /// Set the calling thread's mask and verify the kernel kept all of it.
155    fn set_and_verify_current_thread(mask: &CpuMask) -> io::Result<()> {
156        set_mask(0, mask)?;
157        let actual = get_mask(0)?;
158        if actual.equals(mask) {
159            Ok(())
160        } else {
161            Err(io::Error::other(
162                "the effective CPU mask differs from the requested mask",
163            ))
164        }
165    }
166
167    /// Set the calling thread's mask to `mask`.
168    ///
169    /// A threaded cgroup may silently intersect the request with a narrower
170    /// cpuset, so the read-back is what turns a partially applied mask into a
171    /// typed failure before Rayon builds any worker.
172    pub(super) fn set_current_thread_to(mask: &CpuMask) -> Result<(), CpuThreadError> {
173        set_and_verify_current_thread(mask).map_err(|source| CpuThreadError::Affinity {
174            operation: "sched_setaffinity",
175            source,
176        })
177    }
178
179    /// Pin the calling thread to exactly `cpu` and verify the effective mask.
180    pub(super) fn pin_current_thread(cpu: usize) -> io::Result<()> {
181        let min_words = get_mask(0)?.words_len();
182        let expected = CpuMask::single(cpu, min_words);
183        set_and_verify_current_thread(&expected)
184    }
185
186    /// The calling thread's effective CPU ids.
187    pub(super) fn current_thread_cpus() -> Result<Vec<usize>, CpuThreadError> {
188        get_mask(0)
189            .map(|mask| mask.cpus())
190            .map_err(|source| CpuThreadError::Affinity {
191                operation: "sched_getaffinity",
192                source,
193            })
194    }
195}
196
197/// Failure reported when building a [`CpuBackend`] pool or applying its
198/// affinity policy.
199///
200/// A pool build and, on Linux, the affinity snapshot are the fallible steps of a
201/// [`CpuBackend`]: tenferro itself never builds threads, so the caller's thread
202/// policy is the caller's error to
203/// carry.
204///
205/// # Examples
206///
207/// ```
208/// use tensor4all_tensorbackend::CpuThreadError;
209///
210/// let error = CpuThreadError::ZeroThreads;
211/// assert!(error.to_string().contains("at least one"));
212/// ```
213#[derive(Debug, thiserror::Error)]
214pub enum CpuThreadError {
215    /// A [`CpuBackend`] was asked for zero workers.
216    #[error("a CPU backend needs at least one worker thread")]
217    ZeroThreads,
218    /// The worker pool for the requested worker count could not be built.
219    #[error("failed to build the CPU worker pool: {source}")]
220    Pool {
221        /// Original pool diagnostic.
222        #[source]
223        source: Box<tenferro_cpu::threadteam::Error>,
224    },
225    /// [`CpuBackend::pinned`] was given an empty CPU list.
226    #[error("a pinned CPU backend needs at least one CPU")]
227    EmptyCpuList,
228    /// [`CpuBackend::pinned`] was given the same CPU twice.
229    #[error("the pinned CPU list repeats CPU {cpu}")]
230    DuplicateCpu {
231        /// The repeated CPU id.
232        cpu: usize,
233    },
234    /// [`CpuBackend::pinned`] named a CPU outside the main thread's mask.
235    #[error("CPU {cpu} is outside the main thread's allowed CPU set")]
236    CpuNotAllowed {
237        /// The rejected CPU id.
238        cpu: usize,
239    },
240    /// CPU affinity is only implemented on Linux.
241    #[error("CPU affinity is not supported on this platform")]
242    UnsupportedPlatform,
243    /// An affinity system call failed.
244    #[error("{operation} failed: {source}")]
245    Affinity {
246        /// The system call that failed.
247        operation: &'static str,
248        /// Original operating-system diagnostic.
249        #[source]
250        source: std::io::Error,
251    },
252    /// The short-lived thread that builds a pool could not be started.
253    #[error("failed to start the CPU pool construction thread: {source}")]
254    ConstructionThread {
255        /// Original thread-spawn diagnostic.
256        #[source]
257        source: std::io::Error,
258    },
259    /// The CPU pool construction thread panicked before returning the pool.
260    #[error("the CPU pool construction thread panicked")]
261    ConstructionThreadPanic,
262    /// A pinned worker could not be confined to its CPU, or its startup
263    /// handshake failed.
264    #[error("CPU worker {worker} failed its affinity handshake: {source}")]
265    WorkerAffinity {
266        /// Rayon worker index, or zero for a closed startup channel.
267        worker: usize,
268        /// Original operating-system or verification diagnostic.
269        #[source]
270        source: std::io::Error,
271    },
272}
273
274/// Failure reported by [`set_default_cpu_threads`] when the default is fixed.
275///
276/// # Examples
277///
278/// ```
279/// use tensor4all_tensorbackend::AlreadyInitialized;
280///
281/// let error = AlreadyInitialized;
282/// assert_eq!(
283///     error.to_string(),
284///     "the process-wide default CPU thread count is already initialized"
285/// );
286/// ```
287#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
288#[error("the process-wide default CPU thread count is already initialized")]
289pub struct AlreadyInitialized;
290
291static DEFAULT_CPU_THREADS: OnceLock<usize> = OnceLock::new();
292
293/// Fix the worker count [`CpuBackend::new`] uses for the rest of the process.
294///
295/// The value is read once, by the first [`CpuBackend::new`] call that resolves a
296/// count; a backend built later keeps whatever policy it was built with. A
297/// first [`CpuBackend::new`] that cannot build its pool falls back to serial for
298/// that backend only and does not fix the default, so a later call tries again.
299/// Call this before any other thread can construct a default backend.
300///
301/// # Errors
302///
303/// Returns [`AlreadyInitialized`] when the default thread configuration was
304/// already fixed successfully, so a change is unsupported: either an earlier
305/// call or a [`CpuBackend::new`] that resolved a count fixed it. A failed first
306/// construction does not fix it.
307///
308/// # Examples
309///
310/// ```
311/// use std::num::NonZeroUsize;
312/// use tensor4all_tensorbackend::{set_default_cpu_threads, AlreadyInitialized};
313///
314/// // Whichever call sets the default first wins, and every later one is
315/// // rejected typed instead of panicking.
316/// let _ = set_default_cpu_threads(NonZeroUsize::new(3).unwrap());
317/// assert_eq!(
318///     set_default_cpu_threads(NonZeroUsize::MAX),
319///     Err(AlreadyInitialized)
320/// );
321/// ```
322pub fn set_default_cpu_threads(threads: NonZeroUsize) -> Result<(), AlreadyInitialized> {
323    DEFAULT_CPU_THREADS
324        .set(threads.get())
325        .map_err(|_| AlreadyInitialized)
326}
327
328/// The CPU set each worker of a pool reported when it started, by worker index.
329type WorkerCpus = Vec<Vec<usize>>;
330
331/// The calling thread's CPU set, empty off Linux.
332#[cfg(target_os = "linux")]
333fn current_worker_cpus() -> Vec<usize> {
334    affinity::current_thread_cpus().unwrap_or_default()
335}
336
337/// The calling thread's CPU set, empty off Linux.
338#[cfg(not(target_os = "linux"))]
339fn current_worker_cpus() -> Vec<usize> {
340    Vec::new()
341}
342
343/// Build one worker pool with `threads` (or the environment's count when
344/// `None`), recording the CPU set each worker starts on.
345///
346/// On Linux the build runs on a short-lived construction thread whose mask is
347/// first set to the main thread's mask snapshot, so a pool built from a singly
348/// pinned worker still spans the main thread's CPUs. The workers inherit the
349/// construction thread's mask.
350///
351/// Off Linux the pool is built on the calling thread and no affinity call is
352/// made.
353fn build_pool(threads: Option<usize>) -> Result<(Pool, WorkerCpus), CpuThreadError> {
354    #[cfg(target_os = "linux")]
355    {
356        let mask = affinity::main_thread_mask()?;
357        std::thread::Builder::new()
358            .name("tensor4all-cpu-pool".to_owned())
359            .spawn(move || {
360                affinity::set_current_thread_to(&mask)?;
361                build_pool_here(threads)
362            })
363            .map_err(|source| CpuThreadError::ConstructionThread { source })?
364            .join()
365            .map_err(|_| CpuThreadError::ConstructionThreadPanic)?
366    }
367    #[cfg(not(target_os = "linux"))]
368    {
369        build_pool_here(threads)
370    }
371}
372
373/// The worker count Rayon itself resolves from the environment, in Rayon's
374/// order: `RAYON_NUM_THREADS`, the deprecated `RAYON_RS_NUM_CPUS`, then
375/// [`std::thread::available_parallelism`] (which already accounts for the
376/// process affinity mask and cgroup quotas). `RAYON_NUM_THREADS=0` or a
377/// malformed value means "use the platform default", as in Rayon.
378///
379/// The pool builder this type uses does not read the environment, so the
380/// precedence of [`set_default_cpu_threads`] → `RAYON_NUM_THREADS` → affinity
381/// is resolved here (R-performance-61).
382fn rayon_env_worker_count() -> usize {
383    let platform_default = || std::thread::available_parallelism().map_or(1, NonZeroUsize::get);
384    match std::env::var("RAYON_NUM_THREADS")
385        .ok()
386        .and_then(|value| value.parse::<usize>().ok())
387    {
388        Some(count) if count >= 1 => return count,
389        Some(_) => return platform_default(),
390        None => {}
391    }
392    match std::env::var("RAYON_RS_NUM_CPUS")
393        .ok()
394        .and_then(|value| value.parse::<usize>().ok())
395    {
396        Some(count) if count >= 1 => count,
397        _ => platform_default(),
398    }
399}
400
401/// Build one worker pool on the calling thread.
402fn build_pool_here(threads: Option<usize>) -> Result<(Pool, WorkerCpus), CpuThreadError> {
403    let threads = threads.unwrap_or_else(rayon_env_worker_count);
404    let (sender, receiver) = std::sync::mpsc::channel();
405    let pool = Pool::builder()
406        .threads(threads)
407        .on_start(move |worker| {
408            let _ = sender.send((worker, current_worker_cpus()));
409        })
410        .build()
411        .map_err(|source| CpuThreadError::Pool {
412            source: Box::new(source),
413        })?;
414    // A worker runs its start hook in its own thread, so the reports arrive
415    // while and after the pool is built; collect one per worker before the
416    // backend exposes them.
417    let mut worker_cpus: WorkerCpus = vec![Vec::new(); pool.width().get()];
418    for _ in 0..worker_cpus.len() {
419        match receiver.recv_timeout(std::time::Duration::from_secs(1)) {
420            Ok((worker, cpus)) => {
421                if let Some(slot) = worker_cpus.get_mut(worker) {
422                    *slot = cpus;
423                }
424            }
425            // A worker that never reports leaves its entry empty rather than
426            // blocking the pool build.
427            Err(_) => break,
428        }
429    }
430    Ok((pool, worker_cpus))
431}
432
433/// tensor4all's owned CPU execution handle: one [`CpuEngine`].
434///
435/// tenferro no longer has a backend session or a `CpuContext`; a caller builds
436/// a Rayon pool, hands it to [`CpuEngine::new`], and keeps the engine for as
437/// long as it wants the thread policy, the buffer pool, the prepared-plan cache
438/// and the contraction scratch to stay warm. This type is that caller-held
439/// owner for tensor4all: the implicit frontend's resource owner during the
440/// transition to engine-explicit APIs, which will replace it later.
441///
442/// It is **not** [`Clone`]. Cloning an execution domain was how the previous
443/// session API shared one engine between callers; with a caller-held engine the
444/// caller owns it outright, and a second execution domain needs a second
445/// backend.
446///
447/// [`CpuBackend::new`] resolves its worker count in this order: the value
448/// [`set_default_cpu_threads`] fixed, else the Rayon environment
449/// (`RAYON_NUM_THREADS`, then the deprecated `RAYON_RS_NUM_CPUS`), else the
450/// platform's parallelism, which is evaluated under the main thread's mask and
451/// may be lower than that mask's CPU count because of cgroup quotas. A
452/// resolved count of one uses [`CpuEngine::serial`] and owns no pool. The count
453/// is fixed by the first successful [`CpuBackend::new`] of the process, or by the
454/// setter. If the pool or the Linux mask snapshot cannot be used,
455/// [`CpuBackend::new`] falls back to [`CpuEngine::serial`] for that backend only
456/// instead of panicking, and a later call retries.
457///
458/// Every pool this type builds on Linux confines its workers to the main
459/// thread's allowed CPU set, captured as a snapshot when the pool is built.
460/// This applies to the pool's workers only: operations that run inline on the
461/// caller (small operations, Rayon-worker entries that use the serial inline
462/// engine, and hataori lane threads) are not moved. A cpuset or CPU-hotplug
463/// change needs a new backend, not a continuous watcher.
464///
465/// [`CpuBackend::pinned`] is an opt-in per-worker pinning mode whose benefit is
466/// unmeasured. Disjoint pinned backends confine their *pools'* workers; they do
467/// not isolate all execution of their engines, because an entry from any Rayon
468/// worker runs inline on that worker. A provider team created from a pinned
469/// worker inherits that worker's one CPU.
470///
471/// # Examples
472///
473/// ```
474/// use tensor4all_tensorbackend::CpuBackend;
475/// use tenferro_cpu::CpuEngine;
476///
477/// // A serial backend runs every operation on the caller's thread.
478/// let backend = CpuBackend::with_threads(1)?;
479/// assert_eq!(backend.num_threads(), 1);
480///
481/// // A caller that already owns an engine can hand it in unchanged.
482/// let backend = CpuBackend::from(CpuEngine::serial());
483/// assert_eq!(backend.num_threads(), 1);
484/// # Ok::<(), Box<dyn std::error::Error>>(())
485/// ```
486pub struct CpuBackend {
487    engine: CpuEngine,
488    /// The CPU set each pool worker reported at start, by worker index; empty
489    /// for a serial engine or a caller-supplied engine.
490    worker_cpus: WorkerCpus,
491}
492
493impl std::fmt::Debug for CpuBackend {
494    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
495        f.debug_struct("CpuBackend")
496            .field("engine", &self.engine)
497            .field("worker_cpus", &self.worker_cpus())
498            .finish()
499    }
500}
501
502impl CpuBackend {
503    /// Build a backend with the default thread policy.
504    ///
505    /// The worker count is [`set_default_cpu_threads`] if it was called; else
506    /// `RAYON_NUM_THREADS` (then the deprecated `RAYON_RS_NUM_CPUS`); else the
507    /// platform's parallelism, evaluated under the main thread's mask, which may
508    /// be lower than that mask's CPU count because of cgroup quotas. The pool's
509    /// default 16 MiB worker stack, which the CPU providers need, is kept. The
510    /// count is fixed by the first successful call, or by the setter.
511    ///
512    /// On Linux the pool is built on a short-lived construction thread whose
513    /// mask is first set to the main thread's mask snapshot, so building from a
514    /// worker of a pinned pool does not inherit that worker's single CPU. The
515    /// pool's workers inherit the snapshot; inline operations of the caller are
516    /// not moved.
517    ///
518    /// The pool build and the Linux affinity snapshot are the fallible steps,
519    /// and this constructor does not report them: if either cannot be used,
520    /// this backend is [`CpuEngine::serial`] instead, which is correct but uses
521    /// no worker threads, and the failure is not stored as the default. A later
522    /// call retries the construction. Use [`CpuBackend::with_threads`] or
523    /// [`CpuBackend::pinned`] when that difference matters.
524    ///
525    /// # Examples
526    ///
527    /// ```
528    /// use tensor4all_tensorbackend::CpuBackend;
529    ///
530    /// let backend = CpuBackend::new();
531    /// assert!(backend.num_threads() >= 1);
532    /// ```
533    pub fn new() -> Self {
534        if let Some(&threads) = DEFAULT_CPU_THREADS.get() {
535            return Self::with_threads_or_serial(threads);
536        }
537        match build_pool(None) {
538            Ok((pool, worker_cpus)) => {
539                let resolved = pool.width().get();
540                let fixed = match DEFAULT_CPU_THREADS.set(resolved) {
541                    Ok(()) => resolved,
542                    Err(_) => *DEFAULT_CPU_THREADS.get().unwrap_or(&resolved),
543                };
544                if fixed != resolved {
545                    return Self::with_threads_or_serial(fixed);
546                }
547                if resolved == 1 {
548                    // The pool is dropped with this branch: the backend is serial
549                    // and owns no workers, so it records none.
550                    Self {
551                        engine: CpuEngine::serial(),
552                        worker_cpus: WorkerCpus::default(),
553                    }
554                } else {
555                    Self {
556                        engine: CpuEngine::new(pool),
557                        worker_cpus,
558                    }
559                }
560            }
561            Err(_) => Self {
562                engine: CpuEngine::serial(),
563                worker_cpus: WorkerCpus::default(),
564            },
565        }
566    }
567
568    /// Build a backend with `threads` workers, or serial for one.
569    fn with_threads_or_serial(threads: usize) -> Self {
570        Self::with_threads(threads).unwrap_or_else(|_| Self {
571            engine: CpuEngine::serial(),
572            worker_cpus: WorkerCpus::default(),
573        })
574    }
575
576    /// Build a backend that may use `threads` workers.
577    ///
578    /// One worker builds a [`CpuEngine::serial`] engine, which owns no pool, so
579    /// it stays usable from inside another pool. Larger counts build a Rayon
580    /// pool with [`tenferro_cpu::Pool::builder`], so provider code gets the
581    /// worker stack it needs; the pool's workers are confined to the main
582    /// thread's allowed CPU set snapshot on Linux.
583    ///
584    /// # Errors
585    ///
586    /// Returns [`CpuThreadError::ZeroThreads`] for zero,
587    /// [`CpuThreadError::Affinity`] when the mask snapshot or its application
588    /// fails, [`CpuThreadError::ConstructionThread`] when the construction
589    /// thread cannot start, and [`CpuThreadError::Pool`] when the pool cannot be
590    /// built.
591    ///
592    /// # Examples
593    ///
594    /// ```
595    /// use tensor4all_tensorbackend::{CpuBackend, CpuThreadError};
596    ///
597    /// assert!(matches!(
598    ///     CpuBackend::with_threads(0),
599    ///     Err(CpuThreadError::ZeroThreads)
600    /// ));
601    /// let backend = CpuBackend::with_threads(2)?;
602    /// assert_eq!(backend.num_threads(), 2);
603    /// # Ok::<(), Box<dyn std::error::Error>>(())
604    /// ```
605    pub fn with_threads(threads: usize) -> Result<Self, CpuThreadError> {
606        if threads == 0 {
607            return Err(CpuThreadError::ZeroThreads);
608        }
609        if threads == 1 {
610            return Ok(Self {
611                engine: CpuEngine::serial(),
612                worker_cpus: WorkerCpus::default(),
613            });
614        }
615        let (pool, worker_cpus) = build_pool(Some(threads))?;
616        Ok(Self {
617            engine: CpuEngine::new(pool),
618            worker_cpus,
619        })
620    }
621
622    /// Build a backend whose worker `i` is pinned to `cpus[i]`.
623    ///
624    /// This is opt-in per-worker pinning on Linux; its performance benefit is
625    /// unmeasured. Every listed CPU must be in the main thread's allowed set
626    /// ([`Self::allowed_cpus`]), which lets a caller split that set into
627    /// disjoint lists, one per engine. Each worker gets exactly its one CPU,
628    /// so a provider team created from a pinned worker inherits that one CPU.
629    /// A one-element list still builds a one-worker pool rather than a serial
630    /// engine; inline operations of a one-worker engine still run on the
631    /// caller.
632    ///
633    /// The mask is a snapshot: a cpuset or CPU-hotplug change needs a new
634    /// backend. Only the pool's workers are pinned; lane threads and inline
635    /// operations are not moved.
636    ///
637    /// # Errors
638    ///
639    /// Returns [`CpuThreadError::EmptyCpuList`] for an empty list,
640    /// [`CpuThreadError::DuplicateCpu`] for a repeated CPU,
641    /// [`CpuThreadError::CpuNotAllowed`] for a CPU outside the allowed set,
642    /// [`CpuThreadError::UnsupportedPlatform`] off Linux,
643    /// [`CpuThreadError::Affinity`] when the snapshot or a worker's mask fails,
644    /// [`CpuThreadError::WorkerAffinity`] when a worker cannot be pinned to
645    /// exactly its CPU, and [`CpuThreadError::Pool`] when the pool cannot be
646    /// built.
647    ///
648    /// # Examples
649    ///
650    /// ```
651    /// use tensor4all_tensorbackend::{CpuBackend, CpuThreadError};
652    ///
653    /// assert!(matches!(
654    ///     CpuBackend::pinned(&[]),
655    ///     Err(CpuThreadError::EmptyCpuList)
656    /// ));
657    /// # #[cfg(target_os = "linux")]
658    /// # {
659    /// let allowed = CpuBackend::allowed_cpus()?;
660    /// let backend = CpuBackend::pinned(&allowed[..1])?;
661    /// assert_eq!(backend.num_threads(), 1);
662    /// # }
663    /// # Ok::<(), Box<dyn std::error::Error>>(())
664    /// ```
665    pub fn pinned(cpus: &[usize]) -> Result<Self, CpuThreadError> {
666        if cpus.is_empty() {
667            return Err(CpuThreadError::EmptyCpuList);
668        }
669        #[cfg(not(target_os = "linux"))]
670        {
671            let _ = cpus;
672            Err(CpuThreadError::UnsupportedPlatform)
673        }
674        #[cfg(target_os = "linux")]
675        {
676            let allowed = affinity::main_thread_mask()?.cpus();
677            let mut seen = std::collections::HashSet::with_capacity(cpus.len());
678            for &cpu in cpus {
679                if !seen.insert(cpu) {
680                    return Err(CpuThreadError::DuplicateCpu { cpu });
681                }
682                if allowed.binary_search(&cpu).is_err() {
683                    return Err(CpuThreadError::CpuNotAllowed { cpu });
684                }
685            }
686
687            let workers = cpus.to_vec();
688            let (sender, receiver) = std::sync::mpsc::channel();
689            let pool = Pool::builder()
690                .threads(workers.len())
691                .on_start(move |worker| {
692                    let outcome = workers
693                        .get(worker)
694                        .copied()
695                        .ok_or_else(|| {
696                            std::io::Error::other("worker index outside the pinned CPU list")
697                        })
698                        .and_then(affinity::pin_current_thread);
699                    let _ = sender.send((worker, outcome, current_worker_cpus()));
700                })
701                .build()
702                .map_err(|source| CpuThreadError::Pool {
703                    source: Box::new(source),
704                })?;
705
706            // Every worker of a successfully built pool runs the start handler,
707            // which sends exactly one report, and the registry keeps its sender
708            // alive while the pool is alive. The loop therefore receives one
709            // report per worker. A closed channel is not expected, but it is
710            // still a worker affinity-handshake failure rather than a separate
711            // unreachable error case.
712            let mut worker_cpus: WorkerCpus = vec![Vec::new(); cpus.len()];
713            for _ in 0..cpus.len() {
714                let (worker, outcome, own_cpus) =
715                    receiver
716                        .recv()
717                        .map_err(|source| CpuThreadError::WorkerAffinity {
718                            worker: 0,
719                            source: std::io::Error::other(source),
720                        })?;
721                if let Some(slot) = worker_cpus.get_mut(worker) {
722                    *slot = own_cpus;
723                }
724                if let Err(source) = outcome {
725                    return Err(CpuThreadError::WorkerAffinity { worker, source });
726                }
727            }
728            Ok(Self {
729                engine: CpuEngine::new(pool),
730                worker_cpus,
731            })
732        }
733    }
734
735    /// The CPUs the main thread is allowed to run on right now, in ascending
736    /// order.
737    ///
738    /// This is a snapshot, read with `sched_getaffinity(getpid())` on Linux. A
739    /// caller can split it into disjoint lists and pass one list to each
740    /// [`CpuBackend::pinned`] engine. Off Linux this reports
741    /// [`CpuThreadError::UnsupportedPlatform`].
742    ///
743    /// # Errors
744    ///
745    /// Returns [`CpuThreadError::UnsupportedPlatform`] off Linux and
746    /// [`CpuThreadError::Affinity`] when the affinity query fails.
747    ///
748    /// # Examples
749    ///
750    /// ```
751    /// use tensor4all_tensorbackend::CpuBackend;
752    ///
753    /// # #[cfg(target_os = "linux")]
754    /// # {
755    /// let allowed = CpuBackend::allowed_cpus()?;
756    /// assert!(!allowed.is_empty());
757    /// assert!(allowed.windows(2).all(|pair| pair[0] < pair[1]));
758    /// # }
759    /// # Ok::<(), Box<dyn std::error::Error>>(())
760    /// ```
761    pub fn allowed_cpus() -> Result<Vec<usize>, CpuThreadError> {
762        #[cfg(not(target_os = "linux"))]
763        {
764            Err(CpuThreadError::UnsupportedPlatform)
765        }
766        #[cfg(target_os = "linux")]
767        {
768            Ok(affinity::main_thread_mask()?.cpus())
769        }
770    }
771
772    /// The number of workers operations on this backend may use.
773    ///
774    /// # Examples
775    ///
776    /// ```
777    /// use tensor4all_tensorbackend::CpuBackend;
778    ///
779    /// assert_eq!(CpuBackend::with_threads(1)?.num_threads(), 1);
780    /// # Ok::<(), Box<dyn std::error::Error>>(())
781    /// ```
782    pub fn num_threads(&self) -> usize {
783        self.engine.num_threads()
784    }
785
786    /// The CPU set each worker of this backend's pool reported when it started,
787    /// by worker index.
788    ///
789    /// Empty for a serial engine, for a caller-supplied engine
790    /// ([`CpuBackend::from`]) and off Linux. It is a snapshot taken at pool
791    /// build time, so it shows what the workers actually inherited — the main
792    /// thread's allowed set for [`CpuBackend::new`] and
793    /// [`CpuBackend::with_threads`], and its one CPU for
794    /// [`CpuBackend::pinned`].
795    ///
796    /// # Examples
797    ///
798    /// ```
799    /// use tensor4all_tensorbackend::CpuBackend;
800    ///
801    /// let backend = CpuBackend::with_threads(1)?;
802    /// assert!(backend.worker_cpus().is_empty());
803    /// # Ok::<(), Box<dyn std::error::Error>>(())
804    /// ```
805    pub fn worker_cpus(&self) -> Vec<Vec<usize>> {
806        self.worker_cpus.clone()
807    }
808
809    /// Borrow the engine this backend owns.
810    ///
811    /// # Examples
812    ///
813    /// ```
814    /// use tensor4all_tensorbackend::CpuBackend;
815    ///
816    /// let backend = CpuBackend::with_threads(1)?;
817    /// assert_eq!(backend.engine().buffer_pool_len(), 0);
818    /// # Ok::<(), Box<dyn std::error::Error>>(())
819    /// ```
820    pub fn engine(&self) -> &CpuEngine {
821        &self.engine
822    }
823
824    /// Borrow the engine mutably, for one operation or one session.
825    ///
826    /// # Examples
827    ///
828    /// ```
829    /// use tensor4all_tensorbackend::CpuBackend;
830    ///
831    /// let mut backend = CpuBackend::with_threads(1)?;
832    /// backend.engine_mut().reset_buffer_pool();
833    /// assert_eq!(backend.engine().buffer_pool_len(), 0);
834    /// # Ok::<(), Box<dyn std::error::Error>>(())
835    /// ```
836    pub fn engine_mut(&mut self) -> &mut CpuEngine {
837        &mut self.engine
838    }
839
840    /// Consume the backend and return the engine it owns.
841    ///
842    /// A caller that hands engines to another owner, such as a lane executor,
843    /// uses this instead of borrowing and cloning: [`CpuBackend`] deliberately
844    /// does not clone an execution domain.
845    ///
846    /// # Examples
847    ///
848    /// ```
849    /// use tensor4all_tensorbackend::CpuBackend;
850    ///
851    /// let engine = CpuBackend::with_threads(1)?.into_engine();
852    /// assert_eq!(engine.num_threads(), 1);
853    /// # Ok::<(), Box<dyn std::error::Error>>(())
854    /// ```
855    pub fn into_engine(self) -> CpuEngine {
856        self.engine
857    }
858}
859
860impl Default for CpuBackend {
861    fn default() -> Self {
862        Self::new()
863    }
864}
865
866impl Deref for CpuBackend {
867    type Target = CpuEngine;
868
869    fn deref(&self) -> &Self::Target {
870        &self.engine
871    }
872}
873
874impl DerefMut for CpuBackend {
875    fn deref_mut(&mut self) -> &mut Self::Target {
876        &mut self.engine
877    }
878}
879
880impl From<CpuEngine> for CpuBackend {
881    fn from(engine: CpuEngine) -> Self {
882        Self {
883            engine,
884            worker_cpus: WorkerCpus::default(),
885        }
886    }
887}
888
889/// Caller-owned execution domain used by context-aware tensor algorithms.
890///
891/// Values are validated against the exact runtime represented by the selected
892/// context; no implicit host/device transfer is performed by this enum.
893#[derive(Clone, Debug)]
894pub enum ExecutionContext {
895    /// Host execution through one caller-owned CPU context.
896    Cpu(Arc<CpuExecutionContext>),
897    /// CUDA execution through one caller-owned CUDA context.
898    #[cfg(feature = "tenferro-cuda")]
899    Cuda(Arc<crate::cuda::CudaExecutionContext>),
900}
901
902impl ExecutionContext {
903    /// Check whether this is the process-global CPU context.
904    ///
905    /// Compatibility boundary: legacy CPU-global callers route through the
906    /// historical host code paths (bitwise-identical numerics) while explicit
907    /// contexts use the scoped primitives. Without the global-defaults
908    /// feature there is no global context, so this always reports false.
909    pub fn is_global_default_cpu(&self) -> bool {
910        #[cfg(feature = "global-defaults")]
911        {
912            match self {
913                ExecutionContext::Cpu(context) => {
914                    let own = context.eager_runtime().map(|runtime| runtime.id());
915                    let global = defaults::default_eager_ctx().map(|runtime| runtime.id());
916                    matches!((own, global), (Ok(a), Ok(b)) if a == b)
917                }
918                #[cfg(feature = "tenferro-cuda")]
919                ExecutionContext::Cuda(_) => false,
920            }
921        }
922        #[cfg(not(feature = "global-defaults"))]
923        {
924            let _ = self;
925            false
926        }
927    }
928}
929
930/// Error returned by explicit CPU context graph or eager-runtime operations.
931///
932/// The original tenferro diagnostic is retained as the error source.
933///
934/// # Examples
935///
936/// ```
937/// use std::error::Error;
938/// use std::sync::Arc;
939/// use tensor4all_tensorbackend::CpuExecutionContextError;
940///
941/// let error = CpuExecutionContextError::Initialization {
942///     component: "graph runtime",
943///     source: Arc::new(std::io::Error::other("registration failed")),
944/// };
945/// assert!(error.source().is_some());
946/// ```
947#[derive(Debug, Clone, thiserror::Error)]
948pub enum CpuExecutionContextError {
949    /// A context-owned graph or eager runtime could not be initialized.
950    #[error("failed to initialize {component}: {source}")]
951    Initialization {
952        /// Context component being initialized.
953        component: &'static str,
954        /// Original tenferro diagnostic.
955        #[source]
956        source: Arc<dyn std::error::Error + Send + Sync + 'static>,
957    },
958    /// The calling thread is already inside a session of this context.
959    ///
960    /// One context owns one engine and lends it to one caller at a time. A
961    /// session entered from inside another session on the same thread would
962    /// wait for a lock that thread already holds, so it is reported as this
963    /// typed error before any lock is taken.
964    #[error("recursive CPU execution-context session entry on one thread")]
965    Reentered,
966    /// Graph compilation or execution failed.
967    #[error("CPU graph {operation} failed: {source}")]
968    Graph {
969        /// Graph operation that failed.
970        operation: &'static str,
971        /// Original tenferro diagnostic.
972        #[source]
973        source: Arc<dyn std::error::Error + Send + Sync + 'static>,
974    },
975}
976
977thread_local! {
978    /// Identities of the execution contexts whose engines this thread holds.
979    ///
980    /// Re-entry is per context: holding one context's engine must not block an
981    /// implicit entry that uses another context's engine.
982    static ACTIVE_CONTEXTS: RefCell<Vec<u64>> = const { RefCell::new(Vec::new()) };
983}
984
985static NEXT_CONTEXT_ID: AtomicU64 = AtomicU64::new(1);
986
987pub(crate) fn next_context_id() -> u64 {
988    NEXT_CONTEXT_ID.fetch_add(1, Ordering::Relaxed)
989}
990
991pub(crate) struct CanonicalSessionGuard {
992    context_id: u64,
993}
994
995impl CanonicalSessionGuard {
996    /// Whether this context's engine is already held on this thread.
997    pub(crate) fn is_active(context_id: u64) -> bool {
998        ACTIVE_CONTEXTS.with(|active| active.borrow().contains(&context_id))
999    }
1000
1001    pub(crate) fn enter(context_id: u64) -> Self {
1002        ACTIVE_CONTEXTS.with(|active| active.borrow_mut().push(context_id));
1003        Self { context_id }
1004    }
1005}
1006
1007impl Drop for CanonicalSessionGuard {
1008    fn drop(&mut self) {
1009        ACTIVE_CONTEXTS.with(|active| {
1010            let mut active = active.borrow_mut();
1011            if active.last() == Some(&self.context_id) {
1012                active.pop();
1013            } else {
1014                active.retain(|context_id| *context_id != self.context_id);
1015            }
1016        });
1017    }
1018}
1019
1020impl CpuExecutionContextError {
1021    fn initialization(
1022        component: &'static str,
1023        source: impl std::error::Error + Send + Sync + 'static,
1024    ) -> Self {
1025        Self::Initialization {
1026            component,
1027            source: Arc::new(source),
1028        }
1029    }
1030
1031    fn graph(
1032        operation: &'static str,
1033        source: impl std::error::Error + Send + Sync + 'static,
1034    ) -> Self {
1035        Self::Graph {
1036            operation,
1037            source: Arc::new(source),
1038        }
1039    }
1040}
1041
1042/// Failure entering the engine of the context that owns an eager runtime.
1043///
1044/// The implicit frontend resolves the engine from the runtime of the operand it
1045/// acts on; the owner may be a CPU or a CUDA execution context, so this error
1046/// keeps the owning context's typed diagnostic instead of flattening both into
1047/// one string.
1048///
1049/// # Examples
1050///
1051/// ```
1052/// use tensor4all_tensorbackend::{
1053///     CpuExecutionContextError, ExecutionContextEntryError,
1054/// };
1055///
1056/// let error = ExecutionContextEntryError::from(CpuExecutionContextError::Reentered);
1057/// assert!(matches!(
1058///     error,
1059///     ExecutionContextEntryError::Cpu(CpuExecutionContextError::Reentered)
1060/// ));
1061/// ```
1062#[cfg(feature = "global-defaults")]
1063#[derive(Debug, Clone, thiserror::Error)]
1064pub enum ExecutionContextEntryError {
1065    /// The owning CPU context rejected the session entry.
1066    #[error(transparent)]
1067    Cpu(#[from] CpuExecutionContextError),
1068    /// The owning CUDA context rejected the session entry.
1069    #[cfg(feature = "tenferro-cuda")]
1070    #[error(transparent)]
1071    Cuda(#[from] crate::cuda::CudaExecutionContextError),
1072}
1073
1074/// The engine a context owns, plus the graph runtime derived from it.
1075///
1076/// The registration the graph runtime holds is derived from the engine, and
1077/// `Runtime::run` needs `&mut engine`, so both live under one lock. The
1078/// [`GraphCompiler`] is not here: it never touches the engine, so it has its own
1079/// lock and a compile never waits for a running graph.
1080struct EngineState {
1081    backend: CpuBackend,
1082    runtime: Option<Runtime>,
1083}
1084
1085impl EngineState {
1086    fn new(backend: CpuBackend) -> Self {
1087        Self {
1088            backend,
1089            runtime: None,
1090        }
1091    }
1092
1093    /// The graph runtime of this context, built from the engine on first use.
1094    fn runtime(&mut self) -> Result<&mut Runtime, CpuExecutionContextError> {
1095        let Self { backend, runtime } = self;
1096        match runtime {
1097            Some(runtime) => Ok(runtime),
1098            None => {
1099                let built = build_graph_runtime(backend)?;
1100                Ok(runtime.insert(built))
1101            }
1102        }
1103    }
1104}
1105
1106/// The runtime engine id `engine` is registered under.
1107///
1108/// An engine built by [`CpuEngine::with_runtime_engine_id`] runs under its own
1109/// id, so every registration and extension module installed on its behalf must
1110/// use that id rather than the canonical [`tenferro_cpu::runtime_engine_id`].
1111fn engine_runtime_id(engine: &CpuEngine) -> tenferro::EngineId {
1112    ExecutionEngine::target(engine).engine_id().clone()
1113}
1114
1115/// The engine state one context owns, behind a handle that can also be reached
1116/// through the eager-runtime owner table.
1117///
1118/// A [`CpuExecutionContext`] holds this in an [`Arc`]; the process-wide owner
1119/// table holds only a [`Weak`](std::sync::Weak), so the entry dangles as soon as the context is
1120/// dropped instead of keeping the engine alive.
1121pub(crate) struct EngineOwner {
1122    id: u64,
1123    /// The engine's registered runtime id, captured once at construction.
1124    ///
1125    /// The eager runtime installs its extension modules under this id, and it is
1126    /// read without taking the engine lock so building the eager runtime can
1127    /// never wait for a running session.
1128    engine_id: tenferro::EngineId,
1129    /// The engine's prepared-plan cache, captured once at construction.
1130    ///
1131    /// A session entered from a Rayon worker runs inline on a fresh serial
1132    /// engine that shares this cache (tensor4all-rs#830). Holding the handle
1133    /// here keeps that entry off the engine lock: the worker never waits for an
1134    /// engine its own pool may be holding.
1135    plans: tenferro_cpu::CpuPlanCache,
1136    engine: Mutex<EngineState>,
1137}
1138
1139impl EngineOwner {
1140    fn new(backend: CpuBackend) -> Self {
1141        Self {
1142            id: next_context_id(),
1143            engine_id: engine_runtime_id(backend.engine()),
1144            plans: backend.engine().plan_cache().clone(),
1145            engine: Mutex::new(EngineState::new(backend)),
1146        }
1147    }
1148
1149    /// Lock the engine state, recovering a lock poisoned by a panic.
1150    ///
1151    /// A panic while the engine was borrowed leaves tenferro's engine marked
1152    /// broken, and the lock poisoned. The engine is kept: it carries the
1153    /// caller's pool, allocation domain, batch policy and runtime engine id, so
1154    /// replacing it with a default engine would silently change the caller's
1155    /// execution policy. Recovering the poison only makes the lock usable again;
1156    /// operations on the broken engine are refused with tenferro's typed
1157    /// `CpuEngineError::Broken`, and the caller builds a new context for a new
1158    /// engine.
1159    fn lock_engine(&self) -> std::sync::MutexGuard<'_, EngineState> {
1160        match self.engine.lock() {
1161            Ok(guard) => guard,
1162            Err(poisoned) => {
1163                let guard = poisoned.into_inner();
1164                self.engine.clear_poison();
1165                guard
1166            }
1167        }
1168    }
1169
1170    /// Check the per-context re-entry guard and install it for one entry.
1171    ///
1172    /// Every fallible entry checks before it locks: a nested entry would wait
1173    /// for an engine lock this thread already holds, so it is reported as
1174    /// [`CpuExecutionContextError::Reentered`] instead.
1175    fn enter_session(&self) -> Result<CanonicalSessionGuard, CpuExecutionContextError> {
1176        if CanonicalSessionGuard::is_active(self.id) {
1177            return Err(CpuExecutionContextError::Reentered);
1178        }
1179        Ok(CanonicalSessionGuard::enter(self.id))
1180    }
1181
1182    /// The single guarded entry that locks this owner's engine.
1183    ///
1184    /// This is the only way to lock the engine from a fallible entry: the
1185    /// re-entry guard is checked and installed here, so no caller can hold the
1186    /// lock without holding the guard too. [`CpuExecutionContext::with_backend`]
1187    /// is the one remaining locking entry, and it installs the same guard
1188    /// without being able to report the rejection.
1189    fn begin_session(&self) -> Result<EngineEntry<'_>, CpuExecutionContextError> {
1190        let guard = self.enter_session()?;
1191        Ok(EngineEntry {
1192            state: self.lock_engine(),
1193            _guard: guard,
1194        })
1195    }
1196
1197    /// Run `f` in one session of this owner's engine.
1198    ///
1199    /// This is the single session-entry implementation shared by the owning
1200    /// context and the eager-runtime route: the same per-context re-entry guard
1201    /// and the same inline-serial rule for Rayon workers apply either way.
1202    fn with_session<R>(
1203        &self,
1204        f: impl FnOnce(&mut CpuEngine) -> R,
1205    ) -> Result<R, CpuExecutionContextError> {
1206        // A worker runs inline on its own serial engine instead of waiting for a
1207        // pool it belongs to (tensor4all-rs#830). The fresh engine shares the
1208        // plan cache, whose entries are immutable prepared plans, so a stage
1209        // entered per item does not re-plan. It does **not** share the context
1210        // engine's buffer pool, extension caches, batch policy or allocation
1211        // domain: an entry from a worker therefore allocates and batches
1212        // independently, and only while it runs.
1213        if rayon::current_thread_index().is_some() {
1214            let _guard = self.enter_session()?;
1215            let mut engine = CpuEngine::serial().with_plan_cache(self.plans.clone());
1216            return Ok(f(&mut engine));
1217        }
1218        let mut entry = self.begin_session()?;
1219        Ok(f(entry.backend.engine_mut()))
1220    }
1221}
1222
1223/// One held engine session: the re-entry guard and the locked engine state.
1224///
1225/// The guard lives exactly as long as the lock it makes safe, so dropping the
1226/// entry releases the engine before the thread is allowed to enter again.
1227struct EngineEntry<'a> {
1228    state: std::sync::MutexGuard<'a, EngineState>,
1229    _guard: CanonicalSessionGuard,
1230}
1231
1232impl Deref for EngineEntry<'_> {
1233    type Target = EngineState;
1234
1235    fn deref(&self) -> &EngineState {
1236        &self.state
1237    }
1238}
1239
1240impl DerefMut for EngineEntry<'_> {
1241    fn deref_mut(&mut self) -> &mut EngineState {
1242        &mut self.state
1243    }
1244}
1245
1246/// Caller-owned CPU execution domain for plain, graph, and eager-AD work.
1247///
1248/// The context owns one [`CpuBackend`] and lends its engine to one caller at a
1249/// time. A session entered from a Rayon worker runs inline on a fresh serial
1250/// engine that shares the context engine's prepared-plan cache instead of
1251/// waiting for the context engine: a worker that waits for a pool install is
1252/// handed more of the enclosing pool's work, and that work may enter a session
1253/// itself (tensor4all-rs#830). Graph preparation caches, the engine's buffer
1254/// pool and the eager runtime are owned by this context and are released when
1255/// it is dropped.
1256///
1257/// # Examples
1258///
1259/// ```
1260/// use tensor4all_tensorbackend::CpuExecutionContext;
1261/// use tensor4all_tensorbackend::CpuBackend;
1262///
1263/// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1264/// let threads = context.with_backend(|backend| backend.num_threads());
1265/// assert_eq!(threads, 1);
1266/// # Ok::<(), Box<dyn std::error::Error>>(())
1267/// ```
1268pub struct CpuExecutionContext {
1269    owner: Arc<EngineOwner>,
1270    compiler: Mutex<GraphCompiler>,
1271    eager: OnceLock<Result<Arc<EagerRuntime>, CpuExecutionContextError>>,
1272}
1273
1274impl std::fmt::Debug for CpuExecutionContext {
1275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1276        f.debug_struct("CpuExecutionContext")
1277            .field("graph_initialized", &self.engine_initialized())
1278            .field("eager_initialized", &self.eager.get().is_some())
1279            .finish_non_exhaustive()
1280    }
1281}
1282
1283impl CpuExecutionContext {
1284    /// Create an execution context from a caller-selected CPU backend.
1285    ///
1286    /// Runtime construction is lazy, so creating a context cannot fail and does
1287    /// not allocate another executor or consult environment configuration.
1288    pub fn from_backend(backend: CpuBackend) -> Self {
1289        Self {
1290            owner: Arc::new(EngineOwner::new(backend)),
1291            compiler: Mutex::new(GraphCompiler::new()),
1292            eager: OnceLock::new(),
1293        }
1294    }
1295
1296    fn engine_initialized(&self) -> bool {
1297        self.owner
1298            .engine
1299            .try_lock()
1300            .is_ok_and(|state| state.runtime.is_some())
1301    }
1302
1303    /// Lock the compiler, discarding one a panic left behind.
1304    ///
1305    /// A `GraphCompiler` holds planning state that a panic may have left half
1306    /// written; rebuilding it costs nothing a compile would not pay for the next
1307    /// graph anyway. The poison is cleared after the rebuild so later compiles
1308    /// reuse the fresh compiler instead of rebuilding it on every access.
1309    fn lock_compiler(&self) -> std::sync::MutexGuard<'_, GraphCompiler> {
1310        match self.compiler.lock() {
1311            Ok(guard) => guard,
1312            Err(poisoned) => {
1313                let mut guard = poisoned.into_inner();
1314                *guard = GraphCompiler::new();
1315                self.compiler.clear_poison();
1316                guard
1317            }
1318        }
1319    }
1320
1321    /// Run a plain tensor operation with this context's engine.
1322    ///
1323    /// The closure runs while the context-local engine lock is held. A lock
1324    /// poisoned by a panic is recovered, and the engine is kept: it carries the
1325    /// caller's pool, allocation domain, batch policy and runtime engine id, so
1326    /// a broken engine keeps refusing operations with tenferro's typed
1327    /// `CpuEngineError::Broken` instead of being silently replaced by a default
1328    /// one. The caller builds a new context when it needs a new engine. The
1329    /// session entries recover the same way.
1330    ///
1331    /// This borrows the engine for the whole closure, so a session entry from
1332    /// inside `f` on this thread is rejected with
1333    /// [`CpuExecutionContextError::Reentered`] instead of waiting for the engine.
1334    /// Calling `with_backend` from inside a session of this context is the
1335    /// caller's obligation not to do: this entry cannot report an error, so it
1336    /// would wait for the engine the session already holds.
1337    pub fn with_backend<R>(&self, f: impl FnOnce(&mut CpuBackend) -> R) -> R {
1338        // The guard makes a nested session entry of this context a typed error,
1339        // not a wait; another context's engine stays independently enterable.
1340        let _guard = CanonicalSessionGuard::enter(self.owner.id);
1341        f(&mut self.owner.lock_engine().backend)
1342    }
1343
1344    /// Run `f` in one session of this context's engine.
1345    ///
1346    /// This is the engine-borrow entry for callers that need the concrete
1347    /// engine (for example to hand it to an eager AD session). The context lends
1348    /// its engine to one caller at a time for the whole of `f`.
1349    ///
1350    /// # Errors
1351    ///
1352    /// Returns [`CpuExecutionContextError::Reentered`] when this thread is
1353    /// already inside a session of this context.
1354    ///
1355    /// # Examples
1356    ///
1357    /// ```
1358    /// use tensor4all_tensorbackend::{CpuBackend, CpuExecutionContext};
1359    ///
1360    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1361    /// let threads = context.with_session(|engine| engine.num_threads())?;
1362    /// assert_eq!(threads, 1);
1363    /// # Ok::<(), Box<dyn std::error::Error>>(())
1364    /// ```
1365    pub fn with_session<R>(
1366        &self,
1367        f: impl FnOnce(&mut CpuEngine) -> R,
1368    ) -> Result<R, CpuExecutionContextError> {
1369        self.owner.with_session(f)
1370    }
1371
1372    /// Compile a backend-neutral traced graph using this context's compiler cache.
1373    ///
1374    /// # Errors
1375    ///
1376    /// Returns [`CpuExecutionContextError::Graph`] when graph compilation fails.
1377    pub fn compile_graph(
1378        &self,
1379        graph: &TracedGraph,
1380    ) -> Result<CompiledGraph, CpuExecutionContextError> {
1381        self.lock_compiler()
1382            .compile_traced_graph(graph)
1383            .map_err(|source| CpuExecutionContextError::graph("compilation", source))
1384    }
1385
1386    /// Execute a compiled graph in this context's runtime and prepared-plan cache.
1387    ///
1388    /// `CompiledGraph` is backend-neutral. Backend-prepared executables and
1389    /// workspaces never leave this context-owned runtime.
1390    ///
1391    /// # Errors
1392    ///
1393    /// Returns [`CpuExecutionContextError::Reentered`] when this thread is
1394    /// already inside a session of this context, and
1395    /// [`CpuExecutionContextError::Initialization`], [`CpuExecutionContextError::Graph`]
1396    /// when runtime initialization, preparation, or execution fails.
1397    pub fn run_graph(
1398        &self,
1399        graph: &CompiledGraph,
1400        inputs: &[&Tensor],
1401    ) -> Result<Vec<Tensor>, CpuExecutionContextError> {
1402        let reads = inputs
1403            .iter()
1404            .map(|input| TensorRead::from_tensor(input))
1405            .collect::<Vec<_>>();
1406        self.run_graph_reads(graph, &reads)
1407    }
1408
1409    /// Execute a compiled graph on borrowed tensor views.
1410    ///
1411    /// The read form is what the bridge uses: an operand that is already a
1412    /// borrowed view stays borrowed instead of being copied into an owner first.
1413    ///
1414    /// # Errors
1415    ///
1416    /// Returns [`CpuExecutionContextError::Reentered`] when this thread is
1417    /// already inside a session of this context, and
1418    /// [`CpuExecutionContextError::Initialization`], [`CpuExecutionContextError::Graph`]
1419    /// when runtime initialization, preparation, or execution fails.
1420    pub(crate) fn run_graph_reads(
1421        &self,
1422        graph: &CompiledGraph,
1423        inputs: &[TensorRead<'_>],
1424    ) -> Result<Vec<Tensor>, CpuExecutionContextError> {
1425        let mut state = self.owner.begin_session()?;
1426        let EngineState { backend, runtime } = &mut *state;
1427        let runtime = match runtime {
1428            Some(runtime) => &*runtime,
1429            None => &*runtime.insert(build_graph_runtime(backend)?),
1430        };
1431        let target = backend.engine().target();
1432        let prepared = runtime
1433            .prepare(graph, inputs, std::slice::from_ref(&target))
1434            .map_err(|source| CpuExecutionContextError::graph("preparation", source))?;
1435        let outputs = {
1436            let mut engines: [&mut dyn ExecutionEngine; 1] = [backend.engine_mut()];
1437            runtime
1438                .run(&mut engines, &prepared, inputs)
1439                .map_err(|source| CpuExecutionContextError::graph("execution", source))?
1440        };
1441        outputs
1442            .into_iter()
1443            .map(|output| {
1444                output
1445                    .into_tensor(backend.engine_mut())
1446                    .map_err(|source| CpuExecutionContextError::graph("output", source))
1447            })
1448            .collect()
1449    }
1450
1451    /// Return this context's eager reverse-AD runtime.
1452    ///
1453    /// The runtime is engine-independent: it is built once from an
1454    /// [`AdContext`] with the linalg AD rules and the einsum/linalg extension
1455    /// modules installed under this context engine's runtime id, and every call
1456    /// takes the caller's engine ([`EagerRuntime::with_eager_session`]). Repeated
1457    /// calls return the same runtime and therefore the same eager compilation
1458    /// cache. The runtime registers itself with the eager-runtime owner table once,
1459    /// when it is built.
1460    ///
1461    /// # Errors
1462    ///
1463    /// Returns [`CpuExecutionContextError`] when linalg AD-rule or eager-runtime
1464    /// registration fails.
1465    pub fn eager_runtime(&self) -> Result<Arc<EagerRuntime>, CpuExecutionContextError> {
1466        self.eager
1467            .get_or_init(|| {
1468                let runtime = build_eager_runtime(&self.owner.engine_id)?;
1469                #[cfg(feature = "global-defaults")]
1470                register_eager_runtime_owner(
1471                    &runtime,
1472                    RuntimeOwner::Cpu(Arc::downgrade(&self.owner)),
1473                );
1474                Ok(runtime)
1475            })
1476            .as_ref()
1477            .map(Arc::clone)
1478            .map_err(Clone::clone)
1479    }
1480
1481    /// Return statistics for this context's runtime-owned graph caches.
1482    ///
1483    /// # Errors
1484    ///
1485    /// Returns [`CpuExecutionContextError::Reentered`] when this thread is
1486    /// already inside a session of this context, and
1487    /// [`CpuExecutionContextError`] when graph initialization or the cache
1488    /// statistics query fails.
1489    pub fn graph_cache_stats(
1490        &self,
1491    ) -> Result<tenferro::RuntimeCacheStats, CpuExecutionContextError> {
1492        let mut state = self.owner.begin_session()?;
1493        let runtime = state.runtime()?;
1494        runtime
1495            .cache_stats()
1496            .map_err(|source| CpuExecutionContextError::graph("cache statistics", source))
1497    }
1498
1499    /// Return retained-buffer statistics for this context's engine.
1500    ///
1501    /// `buffers` counts the retained allocations and `capacity_bytes` their
1502    /// total element capacity. A context whose pool has just been released by
1503    /// [`Self::reset_graph_buffer_pool`] reports zero.
1504    ///
1505    /// # Errors
1506    ///
1507    /// Returns [`CpuExecutionContextError::Reentered`] when this thread is
1508    /// already inside a session of this context.
1509    ///
1510    /// # Examples
1511    ///
1512    /// ```
1513    /// use tensor4all_tensorbackend::CpuExecutionContext;
1514    /// use tenferro::program::{CoreSemanticOp, ProgramInputSpec};
1515    /// use tenferro::{DType, Tensor, TraceContext};
1516    /// use tensor4all_tensorbackend::CpuBackend;
1517    ///
1518    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1519    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1520    ///
1521    /// let mut trace = TraceContext::new();
1522    /// let input = trace.input(ProgramInputSpec::new(DType::F64, [2_usize.into()]))?;
1523    /// let output = trace.add_op(CoreSemanticOp::Neg, &[input])?[0];
1524    /// let graph = trace.finish(&[output])?;
1525    /// let compiled = context.compile_graph(&graph)?;
1526    /// let values = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, -2.0])?;
1527    /// let output = context.run_graph(&compiled, &[&values])?;
1528    /// assert_eq!(output[0].as_slice::<f64>()?, &[-1.0, 2.0]);
1529    ///
1530    /// // Releasing the pool empties it.
1531    /// context.reset_graph_buffer_pool()?;
1532    /// let after = context.graph_buffer_pool_stats()?;
1533    /// assert_eq!(after.buffers, 0);
1534    /// assert_eq!(after.capacity_bytes, 0);
1535    /// # Ok(())
1536    /// # }
1537    /// ```
1538    pub fn graph_buffer_pool_stats(&self) -> Result<BufferPoolStats, CpuExecutionContextError> {
1539        Ok(self
1540            .owner
1541            .begin_session()?
1542            .backend
1543            .engine()
1544            .buffer_pool_stats())
1545    }
1546
1547    /// Release retained buffers owned by this context's engine.
1548    ///
1549    /// The graph runtime, its compiled plans and its prepared-plan cache are
1550    /// kept, so callers can keep running previously compiled graphs afterwards.
1551    ///
1552    /// # Errors
1553    ///
1554    /// Returns [`CpuExecutionContextError::Reentered`] when this thread is
1555    /// already inside a session of this context.
1556    ///
1557    /// # Examples
1558    ///
1559    /// ```
1560    /// use tensor4all_tensorbackend::CpuExecutionContext;
1561    /// use tensor4all_tensorbackend::CpuBackend;
1562    ///
1563    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1564    /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1565    /// context.reset_graph_buffer_pool()?;
1566    ///
1567    /// // The pool retains nothing afterwards, and the runtime stays usable.
1568    /// let stats = context.graph_buffer_pool_stats()?;
1569    /// assert_eq!(stats.buffers, 0);
1570    /// assert_eq!(stats.capacity_bytes, 0);
1571    /// # Ok(())
1572    /// # }
1573    /// ```
1574    pub fn reset_graph_buffer_pool(&self) -> Result<(), CpuExecutionContextError> {
1575        self.owner
1576            .begin_session()?
1577            .backend
1578            .engine_mut()
1579            .reset_buffer_pool();
1580        Ok(())
1581    }
1582
1583    /// Recreate this context's graph runtime and release its prepared caches.
1584    ///
1585    /// # Errors
1586    ///
1587    /// Returns [`CpuExecutionContextError::Reentered`] when this thread is
1588    /// already inside a session of this context.
1589    pub fn reset_graph_runtime(&self) -> Result<(), CpuExecutionContextError> {
1590        {
1591            let mut state = self.owner.begin_session()?;
1592            state.runtime = None;
1593            state.backend.engine_mut().reset_buffer_pool();
1594        }
1595        *self.lock_compiler() = GraphCompiler::new();
1596        Ok(())
1597    }
1598}
1599
1600fn build_graph_runtime(backend: &CpuBackend) -> Result<Runtime, CpuExecutionContextError> {
1601    // Register and extend the runtime under the engine's *own* runtime id: an
1602    // engine built with `CpuEngine::with_runtime_engine_id` would not be found
1603    // by a preparation targeting the canonical CPU id.
1604    let engine_id = engine_runtime_id(backend.engine());
1605    let mut builder = Runtime::builder();
1606    builder
1607        .register_engine(
1608            tenferro_cpu::runtime_engine_registration_with_id(backend.engine(), engine_id.clone())
1609                .map_err(|source| {
1610                    CpuExecutionContextError::initialization("graph CPU engine", source)
1611                })?,
1612        )
1613        .map_err(|source| CpuExecutionContextError::initialization("graph CPU engine", source))?;
1614    builder
1615        .install_extension_module(
1616            tenferro_einsum::extension_module::<CpuEngine>(engine_id).map_err(|source| {
1617                CpuExecutionContextError::initialization("einsum extension", source)
1618            })?,
1619        )
1620        .map_err(|source| CpuExecutionContextError::initialization("einsum extension", source))?;
1621    builder
1622        .build()
1623        .map_err(|source| CpuExecutionContextError::initialization("graph runtime", source))
1624}
1625
1626/// Build an AD context with the built-in linalg semantic rules installed.
1627///
1628/// The context is backend-neutral: CPU and CUDA eager runtimes share the same
1629/// rule set and differ only in the execution extension modules installed on the
1630/// runtime.
1631pub(crate) fn build_ad_context(
1632) -> Result<AdContext, tenferro_ad::semantic_extension::SemanticExtensionRegistryError> {
1633    let rules = tenferro_linalg::semantic_ad_rules()?;
1634    let builder = AdContext::builder().with_semantic_extension_rules(rules)?;
1635    Ok(match builder.build() {
1636        Ok(context) => context,
1637        Err(never) => match never {},
1638    })
1639}
1640
1641fn build_eager_runtime(
1642    engine_id: &tenferro::EngineId,
1643) -> Result<Arc<EagerRuntime>, CpuExecutionContextError> {
1644    let ad_context = build_ad_context()
1645        .map_err(|source| CpuExecutionContextError::initialization("linalg AD rules", source))?;
1646    let runtime = EagerRuntime::with_ad_context(&ad_context)
1647        .map_err(|source| CpuExecutionContextError::initialization("eager runtime", source))?;
1648    // [AI Supplied] Install the built-in extension modules before publishing
1649    // the shared context. Lazy first use reconfigures the runtime and advances
1650    // its epoch; doing that after tensors have prepared AD derivatives can
1651    // invalidate those prepared programs under parallel first use.
1652    //
1653    // The modules are installed under the *engine's own* runtime id: an engine
1654    // built with `CpuEngine::with_runtime_engine_id` would otherwise not find
1655    // its einsum/linalg extensions.
1656    let einsum_module = tenferro_einsum::extension_module::<CpuEngine>(engine_id.clone())
1657        .map_err(|source| CpuExecutionContextError::initialization("einsum extension", source))?;
1658    runtime
1659        .install_extension_module(einsum_module)
1660        .map_err(|source| CpuExecutionContextError::initialization("einsum runtime", source))?;
1661    let linalg_module = tenferro_linalg::extension_module::<CpuEngine>(engine_id.clone())
1662        .map_err(|source| CpuExecutionContextError::initialization("linalg extension", source))?;
1663    runtime
1664        .install_extension_module(linalg_module)
1665        .map_err(|source| CpuExecutionContextError::initialization("linalg runtime", source))?;
1666    Ok(runtime)
1667}
1668
1669/// A context that owns an eager runtime, held weakly by the process-wide table.
1670#[cfg(feature = "global-defaults")]
1671pub(crate) enum RuntimeOwner {
1672    Cpu(Weak<EngineOwner>),
1673    #[cfg(feature = "tenferro-cuda")]
1674    Cuda(Weak<crate::cuda::CudaEngineOwner>),
1675}
1676
1677#[cfg(feature = "global-defaults")]
1678impl RuntimeOwner {
1679    fn upgrade(&self) -> Option<LiveRuntimeOwner> {
1680        match self {
1681            Self::Cpu(owner) => owner.upgrade().map(LiveRuntimeOwner::Cpu),
1682            #[cfg(feature = "tenferro-cuda")]
1683            Self::Cuda(owner) => owner.upgrade().map(LiveRuntimeOwner::Cuda),
1684        }
1685    }
1686
1687    fn is_live(&self) -> bool {
1688        match self {
1689            Self::Cpu(owner) => owner.strong_count() > 0,
1690            #[cfg(feature = "tenferro-cuda")]
1691            Self::Cuda(owner) => owner.strong_count() > 0,
1692        }
1693    }
1694}
1695
1696/// An upgraded [`RuntimeOwner`] that keeps its context alive for one session.
1697#[cfg(feature = "global-defaults")]
1698enum LiveRuntimeOwner {
1699    Cpu(Arc<EngineOwner>),
1700    #[cfg(feature = "tenferro-cuda")]
1701    Cuda(Arc<crate::cuda::CudaEngineOwner>),
1702}
1703
1704/// Process-wide map from an eager runtime's id to the context engine that owns it.
1705///
1706/// The entry holds a [`Weak`] handle: a context registers its runtime when it
1707/// builds it, and the entry dangles when the context (the only strong owner) is
1708/// dropped. Lookups prune the dead entry and fall back to the process-global
1709/// context, so no background cleanup is needed.
1710#[cfg(feature = "global-defaults")]
1711static EAGER_RUNTIME_OWNERS: OnceLock<Mutex<HashMap<ContextId, RuntimeOwner>>> = OnceLock::new();
1712
1713#[cfg(feature = "global-defaults")]
1714fn eager_runtime_owners() -> std::sync::MutexGuard<'static, HashMap<ContextId, RuntimeOwner>> {
1715    EAGER_RUNTIME_OWNERS
1716        .get_or_init(|| Mutex::new(HashMap::new()))
1717        .lock()
1718        .unwrap_or_else(|poisoned| poisoned.into_inner())
1719}
1720
1721#[cfg(feature = "global-defaults")]
1722pub(crate) fn register_eager_runtime_owner(runtime: &EagerRuntime, owner: RuntimeOwner) {
1723    let mut owners = eager_runtime_owners();
1724    owners.retain(|_, owner| owner.is_live());
1725    owners.insert(runtime.id(), owner);
1726}
1727
1728/// Run `f` on the engine of the context that owns `runtime`'s eager runtime.
1729///
1730/// This is the transitional implicit route: an implicit (context-free) eager
1731/// operation has no context argument, so it resolves the engine from the
1732/// runtime of the tensor it acts on instead of always taking the
1733/// process-global default context's engine. That keeps a caller-owned context's
1734/// thread policy, device domain, and engine serialization intact for
1735/// context-free readouts and intermediate operations, including on a context
1736/// that is not the global one.
1737///
1738/// The closure receives the owning engine as [`ExecutionEngine`]: the owner may
1739/// be a CPU context ([`CpuEngine`]) or, with the `tenferro-cuda` feature, a CUDA
1740/// context (`tenferro_gpu::cuda::CudaEngine`). The closure runs through the
1741/// owning context's normal session entry, so a nested entry on the same thread
1742/// is reported as [`ExecutionContextEntryError`]'s typed CPU or CUDA re-entry
1743/// variant instead of waiting for the engine. An entry from a Rayon worker runs
1744/// inline on a serial CPU engine that shares the context's prepared-plan cache;
1745/// a CUDA context keeps its single engine and never opens a second one.
1746/// When no live context owns `runtime` (for example after the context was
1747/// dropped), the entry falls back to the process-global CPU context; the global
1748/// context's own runtime resolves to itself.
1749///
1750/// # Errors
1751///
1752/// Returns [`ExecutionContextEntryError`] when the owning context's session
1753/// entry is rejected.
1754///
1755/// # Examples
1756///
1757/// ```
1758/// use tensor4all_tensorbackend::{with_runtime_engine, CpuBackend, CpuExecutionContext};
1759/// use tenferro_cpu::CpuEngine;
1760///
1761/// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1762/// let runtime = context.eager_runtime()?;
1763/// let threads = with_runtime_engine(&runtime, |engine| {
1764///     engine
1765///         .as_any_mut()
1766///         .downcast_mut::<CpuEngine>()
1767///         .map(|engine| engine.num_threads())
1768/// })?;
1769/// assert_eq!(threads, Some(1));
1770/// # Ok::<(), Box<dyn std::error::Error>>(())
1771/// ```
1772#[cfg(feature = "global-defaults")]
1773pub fn with_runtime_engine<R>(
1774    runtime: &EagerRuntime,
1775    f: impl FnOnce(&mut dyn ExecutionEngine) -> R,
1776) -> Result<R, ExecutionContextEntryError> {
1777    let owner = {
1778        let mut owners = eager_runtime_owners();
1779        match owners.get(&runtime.id()).and_then(RuntimeOwner::upgrade) {
1780            Some(owner) => owner,
1781            None => {
1782                owners.remove(&runtime.id());
1783                LiveRuntimeOwner::Cpu(Arc::clone(&defaults::default_context().owner))
1784            }
1785        }
1786    };
1787    match owner {
1788        LiveRuntimeOwner::Cpu(owner) => owner
1789            .with_session(|engine| f(engine))
1790            .map_err(ExecutionContextEntryError::Cpu),
1791        #[cfg(feature = "tenferro-cuda")]
1792        LiveRuntimeOwner::Cuda(owner) => owner
1793            .with_execution_engine(f)
1794            .map_err(ExecutionContextEntryError::Cuda),
1795    }
1796}
1797
1798#[cfg(feature = "global-defaults")]
1799mod defaults {
1800    use super::*;
1801
1802    static DEFAULT_CONTEXT: OnceLock<Arc<CpuExecutionContext>> = OnceLock::new();
1803
1804    #[cfg(test)]
1805    thread_local! {
1806        static FORCE_EAGER_CONTEXT_FAILURE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1807    }
1808
1809    /// Borrow the process-global CPU execution context.
1810    ///
1811    /// New code must take a caller-owned context; this accessor exists for the
1812    /// process-global convenience path and for callers that map the session-entry
1813    /// rejection into their own error type.
1814    pub(crate) fn default_context() -> &'static Arc<CpuExecutionContext> {
1815        DEFAULT_CONTEXT
1816            .get_or_init(|| Arc::new(CpuExecutionContext::from_backend(CpuBackend::new())))
1817    }
1818
1819    /// Error returned when the process-global eager AD runtime cannot be initialized.
1820    ///
1821    /// # Examples
1822    ///
1823    /// ```
1824    /// use std::error::Error;
1825    /// use std::sync::Arc;
1826    /// use tensor4all_tensorbackend::EagerContextError;
1827    ///
1828    /// let error = EagerContextError::Registration {
1829    ///     source: Arc::new(std::io::Error::other("registration failed")),
1830    /// };
1831    /// assert!(error.source().is_some());
1832    /// ```
1833    #[derive(Debug, Clone, thiserror::Error)]
1834    pub enum EagerContextError {
1835        /// The tenferro linalg AD extension rule could not be registered.
1836        #[error("failed to register tenferro linalg AD rule: {source}")]
1837        Registration {
1838            /// Original diagnostic returned by tenferro.
1839            #[source]
1840            source: Arc<dyn std::error::Error + Send + Sync + 'static>,
1841        },
1842    }
1843
1844    /// Run a closure against the optional process-global CPU backend.
1845    pub fn with_default_backend<R>(f: impl FnOnce(&mut CpuBackend) -> R) -> R {
1846        default_context().with_backend(f)
1847    }
1848
1849    /// Run `f` in one session of the process-global context's engine.
1850    ///
1851    /// The entry rejection stays inside tenferro's error type: it is reported as
1852    /// a `tenferro_tensor::Error` classified as runtime state, so callers keep
1853    /// one error type for session work.
1854    ///
1855    /// # Errors
1856    ///
1857    /// Returns the tenferro diagnostic when the entry is rejected or when `f`
1858    /// fails.
1859    pub(crate) fn with_default_session<T: Send>(
1860        f: impl FnOnce(&mut CpuEngine) -> Result<T, tenferro_tensor::Error> + Send,
1861    ) -> Result<T, tenferro_tensor::Error> {
1862        default_context()
1863            .with_session(f)
1864            .map_err(|source| {
1865                tenferro_tensor::Error::runtime_state_source("canonical session entry", source)
1866            })
1867            .and_then(std::convert::identity)
1868    }
1869
1870    /// Run `f` against the process-global graph compiler.
1871    ///
1872    /// The compiler lock is released before the caller runs the compiled graph,
1873    /// so a compile never waits for a context engine that is busy running one.
1874    pub(crate) fn with_default_graph_compiler<R>(f: impl FnOnce(&mut GraphCompiler) -> R) -> R {
1875        f(&mut default_context().lock_compiler())
1876    }
1877
1878    pub(crate) fn default_engine_buffer_pool_stats() -> anyhow::Result<BufferPoolStats> {
1879        default_context()
1880            .graph_buffer_pool_stats()
1881            .map_err(anyhow::Error::new)
1882    }
1883
1884    pub(crate) fn reset_default_engine_buffer_pool() -> anyhow::Result<()> {
1885        default_context()
1886            .reset_graph_buffer_pool()
1887            .map_err(anyhow::Error::new)
1888    }
1889
1890    pub(crate) fn reset_default_engine() -> anyhow::Result<()> {
1891        default_context()
1892            .reset_graph_runtime()
1893            .map_err(anyhow::Error::new)
1894    }
1895
1896    /// Return the optional process-global eager context used by convenience APIs.
1897    ///
1898    /// # Errors
1899    ///
1900    /// Returns [`EagerContextError::Registration`] when eager runtime
1901    /// initialization fails.
1902    ///
1903    /// # Examples
1904    ///
1905    /// ```
1906    /// use std::sync::Arc;
1907    /// use tensor4all_tensorbackend::default_eager_ctx;
1908    ///
1909    /// let first = default_eager_ctx().unwrap();
1910    /// let second = default_eager_ctx().unwrap();
1911    /// assert!(Arc::ptr_eq(&first, &second));
1912    /// ```
1913    pub fn default_eager_ctx() -> Result<Arc<EagerRuntime>, EagerContextError> {
1914        #[cfg(test)]
1915        if FORCE_EAGER_CONTEXT_FAILURE.with(std::cell::Cell::get) {
1916            return Err(EagerContextError::Registration {
1917                source: Arc::new(std::io::Error::other(
1918                    "forced default eager context registration failure",
1919                )),
1920            });
1921        }
1922        default_context()
1923            .eager_runtime()
1924            .map_err(|source| EagerContextError::Registration {
1925                source: Arc::new(source),
1926            })
1927    }
1928
1929    /// Borrow the process-global CPU execution context.
1930    ///
1931    /// Compatibility entry for CPU-global convenience APIs (e.g. the legacy
1932    /// context-free SRC entry): host tensors constructed through the global
1933    /// default belong to this exact context, so they validate against it.
1934    /// New code must take a caller-owned context instead of consulting this.
1935    ///
1936    /// # Examples
1937    ///
1938    /// ```
1939    /// use tensor4all_tensorbackend::{default_cpu_execution_context, ExecutionContext};
1940    ///
1941    /// let context = ExecutionContext::Cpu(default_cpu_execution_context());
1942    /// assert!(matches!(context, ExecutionContext::Cpu(_)));
1943    /// ```
1944    pub fn default_cpu_execution_context() -> Arc<CpuExecutionContext> {
1945        Arc::clone(default_context())
1946    }
1947
1948    #[cfg(test)]
1949    pub(crate) fn with_forced_eager_context_failure<T>(f: impl FnOnce() -> T) -> T {
1950        let previous = FORCE_EAGER_CONTEXT_FAILURE.with(|failure| failure.replace(true));
1951        let result = f();
1952        FORCE_EAGER_CONTEXT_FAILURE.with(|failure| failure.set(previous));
1953        result
1954    }
1955}
1956
1957#[cfg(feature = "global-defaults")]
1958pub(crate) use defaults::default_context;
1959#[cfg(all(test, feature = "global-defaults"))]
1960pub(crate) use defaults::with_forced_eager_context_failure;
1961#[cfg(feature = "global-defaults")]
1962pub use defaults::{
1963    default_cpu_execution_context, default_eager_ctx, with_default_backend, EagerContextError,
1964};
1965#[cfg(feature = "global-defaults")]
1966pub(crate) use defaults::{
1967    default_engine_buffer_pool_stats, reset_default_engine, reset_default_engine_buffer_pool,
1968    with_default_graph_compiler, with_default_session,
1969};
1970
1971#[cfg(test)]
1972mod tests;