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 can leave the supplied executor in caller-managed mode.
146    #[error(
147        "provider placement control {placement:?} can leave the caller-managed executor for thread budget {thread_budget}"
148    )]
149    CallerManagedPlacementNotEnforceable {
150        /// Requested maximum number of participating threads.
151        thread_budget: usize,
152        /// Provider placement classification.
153        placement: CpuPlacementControl,
154    },
155    /// The provider cannot honor the engine-selected fan-out mode.
156    #[error("provider cannot honor requested CPU parallel mode {mode:?}")]
157    ParallelModeNotSupported {
158        /// Mode selected by the execution engine.
159        mode: ParallelMode,
160    },
161}
162
163impl CpuProviderExecutionCapabilities {
164    pub(crate) fn accepts_mode(self, mode: ParallelMode) -> bool {
165        match mode {
166            ParallelMode::Sequential => self.accepts_sequential && self.worker_local_sequential,
167            ParallelMode::Outer => self.accepts_outer && self.worker_local_sequential,
168            ParallelMode::Inner => self.accepts_inner,
169        }
170    }
171}
172
173#[cfg(test)]
174#[derive(Clone, Copy, Debug, Eq, PartialEq)]
175pub(crate) enum OpenBlasParallelism {
176    Sequential,
177    Pthread,
178    OpenMp,
179    Unknown,
180}
181
182#[cfg(test)]
183#[derive(Clone, Copy, Debug, Eq, PartialEq)]
184pub(crate) struct OpenBlasProbe {
185    pub(crate) parallelism: OpenBlasParallelism,
186    pub(crate) process_global_set_restore_wired: bool,
187}
188
189#[cfg(test)]
190#[derive(Clone, Copy, Debug, Eq, PartialEq)]
191pub(crate) struct AccelerateProbe {
192    pub(crate) binary_thread_local_control_wired: bool,
193}
194
195/// Construction-time facts supplied by provider-specific adapters.
196///
197/// A discovered symbol is not enough: a corresponding `*_wired` field is true
198/// only when the adapter applies and restores that control around every
199/// provider call. OpenBLAS set-and-restore remains process-global even when it
200/// is wired, so it never becomes per-call count control.
201#[cfg(test)]
202#[derive(Clone, Copy, Debug, Eq, PartialEq)]
203pub(crate) enum CpuProviderProbe {
204    FaerOrNative,
205    Mkl { thread_local_setter_wired: bool },
206    OpenBlas(OpenBlasProbe),
207    Accelerate(AccelerateProbe),
208    ArmPlOpenMp,
209    ArmPlSerial,
210    NvplSerial,
211    UnknownBlas,
212    Injected(Option<CpuProviderExecutionCapabilities>),
213}
214
215#[cfg(test)]
216pub(crate) fn classify_provider(probe: CpuProviderProbe) -> CpuProviderExecutionCapabilities {
217    match probe {
218        CpuProviderProbe::FaerOrNative => engine_worker_capabilities(),
219        CpuProviderProbe::Mkl {
220            thread_local_setter_wired: true,
221        } => controlled_external_capabilities(CpuThreadCountControl::PerCallUpperBound),
222        CpuProviderProbe::Mkl {
223            thread_local_setter_wired: false,
224        }
225        | CpuProviderProbe::ArmPlOpenMp => uncontrolled_external_capabilities(),
226        CpuProviderProbe::OpenBlas(probe) => classify_openblas(probe),
227        CpuProviderProbe::Accelerate(probe) => classify_accelerate(probe),
228        CpuProviderProbe::ArmPlSerial | CpuProviderProbe::NvplSerial => serial_capabilities(),
229        CpuProviderProbe::UnknownBlas | CpuProviderProbe::Injected(None) => {
230            CpuProviderExecutionCapabilities::default()
231        }
232        CpuProviderProbe::Injected(Some(capabilities)) => capabilities,
233    }
234}
235
236#[cfg(test)]
237fn classify_openblas(probe: OpenBlasProbe) -> CpuProviderExecutionCapabilities {
238    match (probe.parallelism, probe.process_global_set_restore_wired) {
239        (OpenBlasParallelism::Sequential, _) => serial_capabilities(),
240        (OpenBlasParallelism::Pthread | OpenBlasParallelism::OpenMp, _) => {
241            uncontrolled_external_capabilities()
242        }
243        (OpenBlasParallelism::Unknown, _) => CpuProviderExecutionCapabilities::default(),
244    }
245}
246
247#[cfg(test)]
248fn classify_accelerate(probe: AccelerateProbe) -> CpuProviderExecutionCapabilities {
249    if probe.binary_thread_local_control_wired {
250        controlled_external_capabilities(CpuThreadCountControl::BinaryClampToOne)
251    } else {
252        uncontrolled_external_capabilities()
253    }
254}
255
256pub(crate) fn engine_worker_capabilities() -> CpuProviderExecutionCapabilities {
257    CpuProviderExecutionCapabilities {
258        thread_count: CpuThreadCountControl::PerCallUpperBound,
259        placement: CpuPlacementControl::EngineWorkers,
260        worker_local_sequential: true,
261        accepts_sequential: true,
262        accepts_outer: true,
263        accepts_inner: true,
264    }
265}
266
267#[cfg(test)]
268fn controlled_external_capabilities(
269    thread_count: CpuThreadCountControl,
270) -> CpuProviderExecutionCapabilities {
271    CpuProviderExecutionCapabilities {
272        thread_count,
273        placement: CpuPlacementControl::ExternalWorkers,
274        worker_local_sequential: true,
275        accepts_sequential: true,
276        accepts_outer: true,
277        accepts_inner: true,
278    }
279}
280
281#[cfg(any(test, feature = "cpu-blas"))]
282fn uncontrolled_external_capabilities() -> CpuProviderExecutionCapabilities {
283    CpuProviderExecutionCapabilities {
284        thread_count: CpuThreadCountControl::GlobalOrUncontrolled,
285        placement: CpuPlacementControl::ExternalWorkers,
286        worker_local_sequential: false,
287        accepts_sequential: false,
288        accepts_outer: false,
289        accepts_inner: true,
290    }
291}
292
293#[cfg(any(test, not(feature = "cpu-blas")))]
294pub(crate) fn serial_capabilities() -> CpuProviderExecutionCapabilities {
295    CpuProviderExecutionCapabilities {
296        thread_count: CpuThreadCountControl::Sequential,
297        placement: CpuPlacementControl::CallingThread,
298        worker_local_sequential: true,
299        accepts_sequential: true,
300        accepts_outer: true,
301        accepts_inner: true,
302    }
303}
304
305/// Capabilities of the current built-in BLAS adapter.
306///
307/// The adapter does not yet install and restore any provider-specific local
308/// thread-count setter, so all BLAS builds are classified conservatively.
309#[cfg(any(test, feature = "cpu-blas"))]
310pub(crate) fn builtin_blas_execution_capabilities() -> CpuProviderExecutionCapabilities {
311    uncontrolled_external_capabilities()
312}
313
314pub(crate) fn validate_provider_for_caller_managed_domain(
315    capabilities: CpuProviderExecutionCapabilities,
316    thread_budget: NonZeroUsize,
317) -> Result<(), CpuProviderDomainError> {
318    if enforced_provider_thread_limit(capabilities.thread_count, thread_budget).is_none() {
319        return Err(CpuProviderDomainError::ThreadCountNotEnforceable {
320            thread_budget: thread_budget.get(),
321            control: capabilities.thread_count,
322        });
323    }
324    match capabilities.placement {
325        CpuPlacementControl::EngineWorkers | CpuPlacementControl::CallingThread => Ok(()),
326        CpuPlacementControl::ExternalWorkers | CpuPlacementControl::None => Err(
327            CpuProviderDomainError::CallerManagedPlacementNotEnforceable {
328                thread_budget: thread_budget.get(),
329                placement: capabilities.placement,
330            },
331        ),
332    }
333}
334
335pub(crate) fn validate_provider_for_domain(
336    capabilities: CpuProviderExecutionCapabilities,
337    thread_budget: NonZeroUsize,
338    placement_guarantee: CpuPlacementGuarantee,
339    domain_cpus: &CpuSet,
340    process_allowed_cpus: &CpuSet,
341) -> Result<(), CpuProviderDomainError> {
342    if enforced_provider_thread_limit(capabilities.thread_count, thread_budget).is_none() {
343        return Err(CpuProviderDomainError::ThreadCountNotEnforceable {
344            thread_budget: thread_budget.get(),
345            control: capabilities.thread_count,
346        });
347    }
348
349    match capabilities.placement {
350        CpuPlacementControl::EngineWorkers | CpuPlacementControl::CallingThread => Ok(()),
351        CpuPlacementControl::ExternalWorkers => {
352            if thread_budget.get() == 1 && capabilities.worker_local_sequential {
353                return Ok(());
354            }
355            if placement_guarantee == CpuPlacementGuarantee::AdvisoryDeclared
356                || domain_cpus == process_allowed_cpus
357            {
358                return Ok(());
359            }
360            Err(CpuProviderDomainError::PlacementNotEnforceable {
361                thread_budget: thread_budget.get(),
362                placement: capabilities.placement,
363                guarantee: placement_guarantee,
364            })
365        }
366        CpuPlacementControl::None => {
367            if placement_guarantee == CpuPlacementGuarantee::AdvisoryDeclared {
368                Ok(())
369            } else {
370                Err(CpuProviderDomainError::PlacementNotEnforceable {
371                    thread_budget: thread_budget.get(),
372                    placement: capabilities.placement,
373                    guarantee: placement_guarantee,
374                })
375            }
376        }
377    }
378}
379
380fn enforced_provider_thread_limit(
381    control: CpuThreadCountControl,
382    thread_budget: NonZeroUsize,
383) -> Option<NonZeroUsize> {
384    match control {
385        CpuThreadCountControl::Sequential | CpuThreadCountControl::BinaryClampToOne => {
386            NonZeroUsize::new(1)
387        }
388        CpuThreadCountControl::PerCallUpperBound => Some(thread_budget),
389        CpuThreadCountControl::GlobalOrUncontrolled => None,
390    }
391}
392
393#[cfg(test)]
394mod tests;