Skip to main content

tenferro_cpu/
provider_capability.rs

1use std::num::NonZeroUsize;
2
3use thiserror::Error;
4
5use crate::{CpuPlacementGuarantee, CpuSet, ParallelMode};
6
7/// Per-call control over the maximum number of threads used by a CPU provider.
8///
9/// # Examples
10///
11/// ```
12/// use tenferro_cpu::CpuThreadCountControl;
13/// assert_ne!(
14///     CpuThreadCountControl::PerCallUpperBound,
15///     CpuThreadCountControl::GlobalOrUncontrolled,
16/// );
17/// ```
18#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
19pub enum CpuThreadCountControl {
20    /// The provider is sequential by construction.
21    Sequential,
22    /// Every call accepts an arbitrary positive upper bound.
23    PerCallUpperBound,
24    /// Every finite-budget call is clamped to one thread by the adapter.
25    ///
26    /// The adapter must never select its provider-controlled `auto` mode for a
27    /// resource-domain call. Providers that cannot make that guarantee must
28    /// report [`CpuThreadCountControl::GlobalOrUncontrolled`] instead.
29    BinaryClampToOne,
30    /// Control is global, startup-fixed, absent, or otherwise unsafe per call.
31    #[default]
32    GlobalOrUncontrolled,
33}
34
35/// Per-call control over where a CPU provider executes.
36///
37/// # Examples
38///
39/// ```
40/// use tenferro_cpu::CpuPlacementControl;
41/// assert_ne!(
42///     CpuPlacementControl::EngineWorkers,
43///     CpuPlacementControl::ExternalWorkers,
44/// );
45/// ```
46#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
47pub enum CpuPlacementControl {
48    /// Parallel work stays on workers supplied by the selected executor.
49    EngineWorkers,
50    /// The provider executes entirely on the calling worker.
51    CallingThread,
52    /// Parallel work may use a provider-owned worker pool.
53    ExternalWorkers,
54    /// The provider makes no enforceable placement claim.
55    #[default]
56    None,
57}
58
59/// Immutable execution capabilities declared by one CPU provider.
60///
61/// The conservative default only permits provider-owned inner execution. A
62/// provider must opt in explicitly to sequential or engine-owned outer modes.
63///
64/// # Examples
65///
66/// ```
67/// use tenferro_cpu::{
68///     CpuPlacementControl, CpuProviderExecutionCapabilities, CpuThreadCountControl,
69/// };
70/// let capabilities = CpuProviderExecutionCapabilities {
71///     thread_count: CpuThreadCountControl::Sequential,
72///     placement: CpuPlacementControl::CallingThread,
73///     worker_local_sequential: true,
74///     accepts_sequential: true,
75///     accepts_outer: true,
76///     accepts_inner: true,
77/// };
78/// assert!(capabilities.worker_local_sequential);
79/// ```
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub struct CpuProviderExecutionCapabilities {
82    /// Per-call thread-count control implemented by the provider adapter.
83    pub thread_count: CpuThreadCountControl,
84    /// Placement control implemented independently from thread-count control.
85    pub placement: CpuPlacementControl,
86    /// Whether a call can be forced to stay sequential on its current worker.
87    pub worker_local_sequential: bool,
88    /// Whether the provider accepts a no-fan-out operation context.
89    pub accepts_sequential: bool,
90    /// Whether the provider accepts engine-owned fan-out with sequential children.
91    pub accepts_outer: bool,
92    /// Whether the provider accepts ownership of one inner parallel region.
93    pub accepts_inner: bool,
94}
95
96impl Default for CpuProviderExecutionCapabilities {
97    fn default() -> Self {
98        Self {
99            thread_count: CpuThreadCountControl::GlobalOrUncontrolled,
100            placement: CpuPlacementControl::None,
101            worker_local_sequential: false,
102            accepts_sequential: false,
103            accepts_outer: false,
104            accepts_inner: true,
105        }
106    }
107}
108
109/// Typed incompatibility between a CPU provider and a selected CPU domain.
110///
111/// # Examples
112///
113/// ```
114/// use tenferro_cpu::{CpuProviderDomainError, CpuThreadCountControl};
115/// let error = CpuProviderDomainError::ThreadCountNotEnforceable {
116///     thread_budget: 4,
117///     control: CpuThreadCountControl::GlobalOrUncontrolled,
118/// };
119/// assert!(error.to_string().contains("thread budget 4"));
120/// ```
121#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
122pub enum CpuProviderDomainError {
123    /// The provider cannot enforce the domain's per-call thread upper bound.
124    #[error(
125        "provider thread-count control {control:?} cannot enforce thread budget {thread_budget}"
126    )]
127    ThreadCountNotEnforceable {
128        /// Requested maximum number of participating threads.
129        thread_budget: usize,
130        /// Provider thread-count classification.
131        control: CpuThreadCountControl,
132    },
133    /// The provider cannot enforce the domain's placement guarantee.
134    #[error(
135        "provider placement control {placement:?} cannot enforce {guarantee:?} placement for thread budget {thread_budget}"
136    )]
137    PlacementNotEnforceable {
138        /// Requested maximum number of participating threads.
139        thread_budget: usize,
140        /// Provider placement classification.
141        placement: CpuPlacementControl,
142        /// Placement guarantee requested by the domain.
143        guarantee: CpuPlacementGuarantee,
144    },
145    /// The provider cannot honor the engine-selected fan-out mode.
146    #[error("provider cannot honor requested CPU parallel mode {mode:?}")]
147    ParallelModeNotSupported {
148        /// Mode selected by the execution engine.
149        mode: ParallelMode,
150    },
151}
152
153impl CpuProviderExecutionCapabilities {
154    pub(crate) fn accepts_mode(self, mode: ParallelMode) -> bool {
155        match mode {
156            ParallelMode::Sequential => self.accepts_sequential && self.worker_local_sequential,
157            ParallelMode::Outer => self.accepts_outer && self.worker_local_sequential,
158            ParallelMode::Inner => self.accepts_inner,
159        }
160    }
161}
162
163#[cfg(test)]
164#[derive(Clone, Copy, Debug, Eq, PartialEq)]
165pub(crate) enum OpenBlasParallelism {
166    Sequential,
167    Pthread,
168    OpenMp,
169    Unknown,
170}
171
172#[cfg(test)]
173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
174pub(crate) struct OpenBlasProbe {
175    pub(crate) parallelism: OpenBlasParallelism,
176    pub(crate) process_global_set_restore_wired: bool,
177}
178
179#[cfg(test)]
180#[derive(Clone, Copy, Debug, Eq, PartialEq)]
181pub(crate) struct AccelerateProbe {
182    pub(crate) binary_thread_local_control_wired: bool,
183}
184
185/// Construction-time facts supplied by provider-specific adapters.
186///
187/// A discovered symbol is not enough: a corresponding `*_wired` field is true
188/// only when the adapter applies and restores that control around every
189/// provider call. OpenBLAS set-and-restore remains process-global even when it
190/// is wired, so it never becomes per-call count control.
191#[cfg(test)]
192#[derive(Clone, Copy, Debug, Eq, PartialEq)]
193pub(crate) enum CpuProviderProbe {
194    FaerOrNative,
195    Mkl { thread_local_setter_wired: bool },
196    OpenBlas(OpenBlasProbe),
197    Accelerate(AccelerateProbe),
198    ArmPlOpenMp,
199    ArmPlSerial,
200    NvplSerial,
201    UnknownBlas,
202    Injected(Option<CpuProviderExecutionCapabilities>),
203}
204
205#[cfg(test)]
206pub(crate) fn classify_provider(probe: CpuProviderProbe) -> CpuProviderExecutionCapabilities {
207    match probe {
208        CpuProviderProbe::FaerOrNative => engine_worker_capabilities(),
209        CpuProviderProbe::Mkl {
210            thread_local_setter_wired: true,
211        } => controlled_external_capabilities(CpuThreadCountControl::PerCallUpperBound),
212        CpuProviderProbe::Mkl {
213            thread_local_setter_wired: false,
214        }
215        | CpuProviderProbe::ArmPlOpenMp => uncontrolled_external_capabilities(),
216        CpuProviderProbe::OpenBlas(probe) => classify_openblas(probe),
217        CpuProviderProbe::Accelerate(probe) => classify_accelerate(probe),
218        CpuProviderProbe::ArmPlSerial | CpuProviderProbe::NvplSerial => serial_capabilities(),
219        CpuProviderProbe::UnknownBlas | CpuProviderProbe::Injected(None) => {
220            CpuProviderExecutionCapabilities::default()
221        }
222        CpuProviderProbe::Injected(Some(capabilities)) => capabilities,
223    }
224}
225
226#[cfg(test)]
227fn classify_openblas(probe: OpenBlasProbe) -> CpuProviderExecutionCapabilities {
228    match (probe.parallelism, probe.process_global_set_restore_wired) {
229        (OpenBlasParallelism::Sequential, _) => serial_capabilities(),
230        (OpenBlasParallelism::Pthread | OpenBlasParallelism::OpenMp, _) => {
231            uncontrolled_external_capabilities()
232        }
233        (OpenBlasParallelism::Unknown, _) => CpuProviderExecutionCapabilities::default(),
234    }
235}
236
237#[cfg(test)]
238fn classify_accelerate(probe: AccelerateProbe) -> CpuProviderExecutionCapabilities {
239    if probe.binary_thread_local_control_wired {
240        controlled_external_capabilities(CpuThreadCountControl::BinaryClampToOne)
241    } else {
242        uncontrolled_external_capabilities()
243    }
244}
245
246pub(crate) fn engine_worker_capabilities() -> CpuProviderExecutionCapabilities {
247    CpuProviderExecutionCapabilities {
248        thread_count: CpuThreadCountControl::PerCallUpperBound,
249        placement: CpuPlacementControl::EngineWorkers,
250        worker_local_sequential: true,
251        accepts_sequential: true,
252        accepts_outer: true,
253        accepts_inner: true,
254    }
255}
256
257#[cfg(test)]
258fn controlled_external_capabilities(
259    thread_count: CpuThreadCountControl,
260) -> CpuProviderExecutionCapabilities {
261    CpuProviderExecutionCapabilities {
262        thread_count,
263        placement: CpuPlacementControl::ExternalWorkers,
264        worker_local_sequential: true,
265        accepts_sequential: true,
266        accepts_outer: true,
267        accepts_inner: true,
268    }
269}
270
271#[cfg(any(test, feature = "cpu-blas"))]
272fn uncontrolled_external_capabilities() -> CpuProviderExecutionCapabilities {
273    CpuProviderExecutionCapabilities {
274        thread_count: CpuThreadCountControl::GlobalOrUncontrolled,
275        placement: CpuPlacementControl::ExternalWorkers,
276        worker_local_sequential: false,
277        accepts_sequential: false,
278        accepts_outer: false,
279        accepts_inner: true,
280    }
281}
282
283#[cfg(any(test, not(feature = "cpu-blas")))]
284pub(crate) fn serial_capabilities() -> CpuProviderExecutionCapabilities {
285    CpuProviderExecutionCapabilities {
286        thread_count: CpuThreadCountControl::Sequential,
287        placement: CpuPlacementControl::CallingThread,
288        worker_local_sequential: true,
289        accepts_sequential: true,
290        accepts_outer: true,
291        accepts_inner: true,
292    }
293}
294
295/// Capabilities of the current built-in BLAS adapter.
296///
297/// The adapter does not yet install and restore any provider-specific local
298/// thread-count setter, so all BLAS builds are classified conservatively.
299#[cfg(any(test, feature = "cpu-blas"))]
300pub(crate) fn builtin_blas_execution_capabilities() -> CpuProviderExecutionCapabilities {
301    uncontrolled_external_capabilities()
302}
303
304pub(crate) fn validate_provider_for_domain(
305    capabilities: CpuProviderExecutionCapabilities,
306    thread_budget: NonZeroUsize,
307    placement_guarantee: CpuPlacementGuarantee,
308    domain_cpus: &CpuSet,
309    process_allowed_cpus: &CpuSet,
310) -> Result<(), CpuProviderDomainError> {
311    if enforced_provider_thread_limit(capabilities.thread_count, thread_budget).is_none() {
312        return Err(CpuProviderDomainError::ThreadCountNotEnforceable {
313            thread_budget: thread_budget.get(),
314            control: capabilities.thread_count,
315        });
316    }
317
318    match capabilities.placement {
319        CpuPlacementControl::EngineWorkers | CpuPlacementControl::CallingThread => Ok(()),
320        CpuPlacementControl::ExternalWorkers => {
321            if thread_budget.get() == 1 && capabilities.worker_local_sequential {
322                return Ok(());
323            }
324            if placement_guarantee == CpuPlacementGuarantee::AdvisoryDeclared
325                || domain_cpus == process_allowed_cpus
326            {
327                return Ok(());
328            }
329            Err(CpuProviderDomainError::PlacementNotEnforceable {
330                thread_budget: thread_budget.get(),
331                placement: capabilities.placement,
332                guarantee: placement_guarantee,
333            })
334        }
335        CpuPlacementControl::None => {
336            if placement_guarantee == CpuPlacementGuarantee::AdvisoryDeclared {
337                Ok(())
338            } else {
339                Err(CpuProviderDomainError::PlacementNotEnforceable {
340                    thread_budget: thread_budget.get(),
341                    placement: capabilities.placement,
342                    guarantee: placement_guarantee,
343                })
344            }
345        }
346    }
347}
348
349fn enforced_provider_thread_limit(
350    control: CpuThreadCountControl,
351    thread_budget: NonZeroUsize,
352) -> Option<NonZeroUsize> {
353    match control {
354        CpuThreadCountControl::Sequential | CpuThreadCountControl::BinaryClampToOne => {
355            NonZeroUsize::new(1)
356        }
357        CpuThreadCountControl::PerCallUpperBound => Some(thread_budget),
358        CpuThreadCountControl::GlobalOrUncontrolled => None,
359    }
360}
361
362#[cfg(test)]
363mod tests;