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;