tcontract_exec/pool.rs
1use crate::ArenaProvider;
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::sync::{Arc, Mutex};
4
5/// A wrapper around a host's borrowed, owned or shared Rayon pool.
6///
7/// tcontract never creates threads beyond this pool. Its size is fixed at
8/// construction (Rayon pools cannot resize). The embedded mutex serializes
9/// SPMD broadcasts through this wrapper. Hosts sharing a raw pool must retain
10/// one wrapper and serialize potentially conflicting numerical work, rather
11/// than constructing independent wrappers with separate SPMD mutexes.
12///
13/// # Examples
14///
15/// ```
16/// let tp = rayon::ThreadPoolBuilder::new().num_threads(3).build().unwrap();
17/// let pool = tcontract_exec::Pool::borrow(&tp);
18/// assert_eq!(pool.size(), 3);
19/// assert_eq!(pool.stats().entries, 0);
20/// ```
21pub struct Pool<'p> {
22 pool: PoolRef<'p>,
23 pub(crate) spmd: Mutex<()>,
24 /// Retained storage for operations through this wrapper. Reusing the wrapper
25 /// reuses its worker buffers, B panel and barriered team sets.
26 workspace: ArenaProvider,
27 entries: AtomicU64,
28 broadcasts: AtomicU64,
29 inline_runs: AtomicU64,
30}
31
32enum PoolRef<'p> {
33 Borrowed(&'p rayon::ThreadPool),
34 Owned(Box<rayon::ThreadPool>),
35 Shared(Arc<rayon::ThreadPool>),
36}
37
38/// Counters of how often a [`Pool`] was entered, for tests and benchmarks.
39///
40/// # Examples
41///
42/// ```
43/// assert_eq!(tcontract_exec::PoolStats::default().entries, 0);
44/// ```
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
46pub struct PoolStats {
47 /// Entries from outside the pool (`install` or partitions).
48 pub entries: u64,
49 /// SPMD broadcasts started from outside the pool.
50 pub broadcasts: u64,
51 /// Parallel requests served in place because the caller was a worker.
52 pub inline_runs: u64,
53}
54
55impl<'p> Pool<'p> {
56 /// Borrow a host pool.
57 pub fn borrow(pool: &'p rayon::ThreadPool) -> Self {
58 Self::with(PoolRef::Borrowed(pool))
59 }
60
61 fn with(pool: PoolRef<'p>) -> Self {
62 Self {
63 pool,
64 spmd: Mutex::new(()),
65 workspace: ArenaProvider::new(),
66 entries: AtomicU64::new(0),
67 broadcasts: AtomicU64::new(0),
68 inline_runs: AtomicU64::new(0),
69 }
70 }
71
72 /// The storage this pool lends to every operation that runs on it.
73 ///
74 /// Borrowed, not copied: one pool is one owner, and a driver that is given
75 /// this provider returns its team sets to it on drop.
76 pub fn workspace(&self) -> &ArenaProvider {
77 &self.workspace
78 }
79
80 /// Release the pool's idle workspace storage. Live leases and borrowed
81 /// worker buffers survive; a worker parked on this pool keeps its slot
82 /// until it is idle again.
83 pub fn trim_workspace(&self) {
84 use crate::WorkspaceProvider;
85 self.workspace.trim();
86 }
87
88 /// Number of workers.
89 pub fn size(&self) -> usize {
90 self.tp().current_num_threads()
91 }
92
93 /// The raw Rayon pool this wrapper wraps.
94 ///
95 /// For a provider that takes a `rayon::ThreadPool` directly (faer through
96 /// `tlinalg`, LAPACK) and must run on the host's pool rather than a second
97 /// one. The wrapper's retained workspace and its SPMD gate stay with the
98 /// wrapper: a provider that takes the bare pool does not share them, so a
99 /// host that mixes such a provider with this stack keeps one pool but two
100 /// workspace owners.
101 pub fn rayon_pool(&self) -> &rayon::ThreadPool {
102 self.tp()
103 }
104
105 pub(crate) fn tp(&self) -> &rayon::ThreadPool {
106 match &self.pool {
107 PoolRef::Borrowed(p) => p,
108 PoolRef::Owned(p) => p,
109 PoolRef::Shared(p) => p,
110 }
111 }
112
113 /// Give back an owned pool (`None` for a borrowed or shared one), for example
114 /// so a C host can drop it and join its workers. A shared wrapper releases
115 /// only its Arc reference; other owners keep the raw pool alive.
116 pub fn into_owned(self) -> Option<rayon::ThreadPool> {
117 match self.pool {
118 PoolRef::Owned(p) => Some(*p),
119 PoolRef::Borrowed(_) | PoolRef::Shared(_) => None,
120 }
121 }
122
123 /// Snapshot of the entry counters.
124 pub fn stats(&self) -> PoolStats {
125 PoolStats {
126 entries: self.entries.load(Ordering::Relaxed),
127 broadcasts: self.broadcasts.load(Ordering::Relaxed),
128 inline_runs: self.inline_runs.load(Ordering::Relaxed),
129 }
130 }
131
132 /// Zero the counters.
133 pub fn reset_stats(&self) {
134 self.entries.store(0, Ordering::Relaxed);
135 self.broadcasts.store(0, Ordering::Relaxed);
136 self.inline_runs.store(0, Ordering::Relaxed);
137 }
138
139 pub(crate) fn is_worker(&self) -> bool {
140 self.tp().current_thread_index().is_some()
141 }
142
143 pub(crate) fn count_entry(&self) {
144 self.entries.fetch_add(1, Ordering::Relaxed);
145 }
146
147 pub(crate) fn count_broadcast(&self) {
148 self.broadcasts.fetch_add(1, Ordering::Relaxed);
149 }
150
151 pub(crate) fn count_inline(&self) {
152 self.inline_runs.fetch_add(1, Ordering::Relaxed);
153 }
154}
155
156impl Pool<'static> {
157 /// Share the host's existing Rayon pool without creating another worker team.
158 ///
159 /// Retain this wrapper across calls and share it (for example through an
160 /// `Arc<Pool<'static>>`) across host clones to reuse workspace and SPMD
161 /// serialization. Other numerical libraries may borrow the host's original
162 /// raw pool, but their work must obey the host's same resource-loan protocol.
163 ///
164 /// # Examples
165 ///
166 /// ```
167 /// use std::sync::Arc;
168 /// let raw = Arc::new(rayon::ThreadPoolBuilder::new().num_threads(2).build().unwrap());
169 /// let pool = tcontract_exec::Pool::shared(Arc::clone(&raw));
170 /// assert_eq!(pool.size(), raw.current_num_threads());
171 /// ```
172 pub fn shared(pool: Arc<rayon::ThreadPool>) -> Self {
173 Self::with(PoolRef::Shared(pool))
174 }
175
176 /// Take ownership of a pool, for hosts without one of their own (the C
177 /// ABI's `tcontract_tapp_executor_create_rayon`). Rust hosts normally [`Pool::borrow`].
178 ///
179 /// # Examples
180 ///
181 /// ```
182 /// let tp = rayon::ThreadPoolBuilder::new().num_threads(2).build().unwrap();
183 /// let pool = tcontract_exec::Pool::owned(tp);
184 /// assert_eq!(pool.size(), 2);
185 /// ```
186 pub fn owned(pool: rayon::ThreadPool) -> Self {
187 Self::with(PoolRef::Owned(Box::new(pool)))
188 }
189}
190
191impl std::fmt::Debug for Pool<'_> {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 f.debug_struct("Pool")
194 .field("size", &self.size())
195 .field("stats", &self.stats())
196 .finish()
197 }
198}