Skip to main content

tenferro_cpu/
placement.rs

1use thiserror::Error;
2
3use crate::{CpuBackendKind, CpuContextError, CpuSet, CpuTopology, CpuTopologyError, NumaNodeId};
4
5/// Typed failure raised while constructing a CPU execution engine.
6///
7/// The tensor-backed compatibility path and the managed engine path expose
8/// different concrete construction errors. This wrapper keeps both sources
9/// typed while allowing [`CpuPlacementError`] to present one public error
10/// shape.
11///
12/// # Examples
13///
14/// ```
15/// use tenferro_cpu::{CpuEngineConstructionError, CpuContextError};
16/// use std::error::Error;
17///
18/// let error = CpuEngineConstructionError::Context(CpuContextError::InvalidThreadCount);
19/// assert!(error.source().is_some());
20/// ```
21#[derive(Debug, Error)]
22pub enum CpuEngineConstructionError {
23    /// A managed CPU context or pinned worker engine could not be built.
24    #[error("managed CPU engine construction failed: {0}")]
25    Context(#[source] CpuContextError),
26    /// The tensor-backed compatibility engine could not be built.
27    #[error("tensor CPU engine construction failed: {0}")]
28    Tensor(#[source] tenferro_tensor::Error),
29}
30
31/// Requested CPU execution placement.
32///
33/// `AllAllowed` means all logical CPUs permitted by the process affinity mask,
34/// not every CPU installed in the host.
35///
36/// # Examples
37///
38/// ```
39/// use tenferro_cpu::{CpuPlacement, NumaNodeId};
40///
41/// let placement = CpuPlacement::NumaNode(NumaNodeId::new(2));
42/// assert!(matches!(placement, CpuPlacement::NumaNode(_)));
43/// ```
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
45pub enum CpuPlacement {
46    /// Let the selected provider choose its compatible default policy.
47    #[default]
48    Auto,
49    /// Restrict managed tenferro/faer execution to one usable OS NUMA node.
50    NumaNode(NumaNodeId),
51    /// Use the complete CPU set permitted to the process.
52    AllAllowed,
53}
54
55/// Strength of a caller's declared CPU placement for one resource domain.
56///
57/// This declaration does not verify executor worker affinity. Executor
58/// capabilities report affinity verification independently.
59///
60/// # Examples
61///
62/// ```rust
63/// use tenferro_cpu::CpuPlacementGuarantee;
64///
65/// assert_ne!(
66///     CpuPlacementGuarantee::ExactDeclared,
67///     CpuPlacementGuarantee::AdvisoryDeclared,
68/// );
69/// ```
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub enum CpuPlacementGuarantee {
72    /// The caller requires execution to remain within the declared CPU set.
73    ExactDeclared,
74    /// The declared CPU set is advisory rather than a strict placement bound.
75    AdvisoryDeclared,
76}
77
78/// Concrete CPU placement resolved for a managed domain or declared by an external domain.
79///
80/// # Examples
81///
82/// ```
83/// use tenferro_cpu::{CpuId, CpuSet, ResolvedCpuPlacement};
84///
85/// let placement = ResolvedCpuPlacement::AllAllowed {
86///     cpus: CpuSet::new([CpuId::new(0)])?,
87/// };
88/// assert_eq!(placement.cpus().len(), 1);
89/// # Ok::<(), tenferro_cpu::CpuSetError>(())
90/// ```
91#[derive(Clone, Debug, PartialEq, Eq)]
92pub enum ResolvedCpuPlacement {
93    /// A concrete OS NUMA-node placement.
94    NumaNode {
95        /// The sparse OS NUMA node ID.
96        id: NumaNodeId,
97        /// The logical CPUs resolved or declared for the node.
98        cpus: CpuSet,
99    },
100    /// A resolved or declared complete process-affinity CPU set.
101    AllAllowed {
102        /// Logical CPUs resolved or declared as process-permitted.
103        cpus: CpuSet,
104    },
105}
106
107impl ResolvedCpuPlacement {
108    /// Return the concrete logical CPU set resolved or declared for this placement.
109    ///
110    /// # Examples
111    ///
112    /// ```
113    /// use tenferro_cpu::{CpuId, CpuSet, ResolvedCpuPlacement};
114    ///
115    /// let placement = ResolvedCpuPlacement::AllAllowed {
116    ///     cpus: CpuSet::new([CpuId::new(1), CpuId::new(2)])?,
117    /// };
118    /// assert_eq!(placement.cpus().as_usize_vec(), vec![1, 2]);
119    /// # Ok::<(), tenferro_cpu::CpuSetError>(())
120    /// ```
121    pub fn cpus(&self) -> &CpuSet {
122        match self {
123            Self::NumaNode { cpus, .. } | Self::AllAllowed { cpus } => cpus,
124        }
125    }
126
127    /// Return the OS NUMA node ID for a node placement.
128    ///
129    /// # Examples
130    ///
131    /// ```
132    /// use tenferro_cpu::{CpuId, CpuSet, NumaNodeId, ResolvedCpuPlacement};
133    ///
134    /// let placement = ResolvedCpuPlacement::NumaNode {
135    ///     id: NumaNodeId::new(7),
136    ///     cpus: CpuSet::new([CpuId::new(3)])?,
137    /// };
138    /// assert_eq!(placement.node_id(), Some(NumaNodeId::new(7)));
139    /// # Ok::<(), tenferro_cpu::CpuSetError>(())
140    /// ```
141    pub fn node_id(&self) -> Option<NumaNodeId> {
142        match self {
143            Self::NumaNode { id, .. } => Some(*id),
144            Self::AllAllowed { .. } => None,
145        }
146    }
147}
148
149/// Failure to resolve a CPU placement for the selected public provider kind.
150///
151/// # Examples
152///
153/// ```
154/// use tenferro_cpu::{CpuBackendKind, CpuPlacement, CpuPlacementError};
155///
156/// let error = CpuPlacementError::ExternalProviderAffinityUnmanaged {
157///     requested: CpuPlacement::AllAllowed,
158///     backend: CpuBackendKind::Blas,
159/// };
160/// assert!(error.to_string().contains("affinity"));
161/// ```
162#[derive(Debug, Error)]
163pub enum CpuPlacementError {
164    /// Process-visible topology discovery failed before placement resolution.
165    #[error("cannot resolve {requested:?} for {backend:?}: topology discovery failed: {source}")]
166    TopologyDiscovery {
167        /// The placement requested by the caller.
168        requested: CpuPlacement,
169        /// The selected public backend kind.
170        backend: CpuBackendKind,
171        /// The preserved topology failure category.
172        #[source]
173        source: CpuTopologyError,
174    },
175    /// The current platform cannot construct verified pinned worker pools.
176    #[error(
177        "cannot resolve {requested:?} for {backend:?}: managed worker affinity is unavailable"
178    )]
179    ManagedAffinityUnavailable {
180        /// The explicit placement requested by the caller.
181        requested: CpuPlacement,
182        /// The selected public backend kind.
183        backend: CpuBackendKind,
184    },
185    /// NUMA-node placement was requested but OS NUMA discovery was unavailable.
186    #[error("cannot resolve {requested:?} for {backend:?}: NUMA discovery is unavailable")]
187    NumaDiscoveryUnavailable {
188        /// The placement requested by the caller.
189        requested: CpuPlacement,
190        /// The selected public backend kind.
191        backend: CpuBackendKind,
192    },
193    /// The requested OS NUMA node has no usable CPUs in this process.
194    #[error("cannot resolve {requested:?} for {backend:?}: NUMA node {node} is unavailable")]
195    UnknownNumaNode {
196        /// The placement requested by the caller.
197        requested: CpuPlacement,
198        /// The selected public backend kind.
199        backend: CpuBackendKind,
200        /// The unknown or process-unavailable OS node ID.
201        node: NumaNodeId,
202    },
203    /// An external provider owns worker affinity, so explicit placement is unsafe.
204    #[error(
205        "cannot resolve {requested:?} for {backend:?}: external provider worker affinity is unmanaged"
206    )]
207    ExternalProviderAffinityUnmanaged {
208        /// The explicit placement requested by the caller.
209        requested: CpuPlacement,
210        /// The selected public backend kind.
211        backend: CpuBackendKind,
212    },
213    /// An externally managed coordinator has no domain for the explicit placement.
214    #[error("externally managed CPU coordinator has no registered domain for {requested:?}")]
215    UnregisteredExternalPlacement {
216        /// The explicit registry-only placement request.
217        requested: CpuPlacement,
218    },
219    /// An externally managed coordinator has no domain with the requested ID.
220    #[error("externally managed CPU coordinator has no registered domain {domain:?}")]
221    UnregisteredExternalDomain {
222        /// Missing caller-stable domain identity.
223        domain: crate::CpuDomainId,
224    },
225    /// A pinned engine could not be built for an otherwise valid placement.
226    #[error("cannot resolve {requested:?} for {backend:?}: engine construction failed: {source}")]
227    EngineConstruction {
228        /// The placement requested by the caller.
229        requested: CpuPlacement,
230        /// The selected public backend kind.
231        backend: CpuBackendKind,
232        /// Typed worker-pool construction or affinity failure.
233        #[source]
234        source: CpuEngineConstructionError,
235    },
236    /// The placement state reached an impossible internal compatibility mode.
237    #[error("cannot resolve {requested:?} for {backend:?}: {message}")]
238    InternalState {
239        /// The placement requested by the caller.
240        requested: CpuPlacement,
241        /// The selected public backend kind.
242        backend: CpuBackendKind,
243        /// Stable internal-state diagnostic.
244        message: &'static str,
245    },
246}
247
248#[derive(Clone, Debug, PartialEq, Eq)]
249pub(crate) enum ResolvedCpuExecution {
250    Compatibility,
251    Managed(ResolvedCpuPlacement),
252    ExternalManaged(ResolvedCpuPlacement),
253    ExternalCallerManaged,
254    ProviderDefaultExclusive,
255}
256
257pub(crate) fn resolve_placement(
258    backend: CpuBackendKind,
259    requested: CpuPlacement,
260    topology: &CpuTopology,
261) -> Result<ResolvedCpuExecution, CpuPlacementError> {
262    resolve_placement_with_affinity(
263        backend,
264        requested,
265        topology,
266        cfg!(any(target_os = "linux", target_os = "android")),
267    )
268}
269
270pub(crate) fn resolve_placement_with_affinity(
271    backend: CpuBackendKind,
272    requested: CpuPlacement,
273    topology: &CpuTopology,
274    managed_affinity_available: bool,
275) -> Result<ResolvedCpuExecution, CpuPlacementError> {
276    if backend == CpuBackendKind::Blas {
277        return match requested {
278            CpuPlacement::Auto => Ok(ResolvedCpuExecution::ProviderDefaultExclusive),
279            CpuPlacement::NumaNode(_) | CpuPlacement::AllAllowed => {
280                Err(CpuPlacementError::ExternalProviderAffinityUnmanaged { requested, backend })
281            }
282        };
283    }
284
285    if !managed_affinity_available {
286        return match requested {
287            CpuPlacement::Auto => Ok(ResolvedCpuExecution::Compatibility),
288            CpuPlacement::NumaNode(_) | CpuPlacement::AllAllowed => {
289                Err(CpuPlacementError::ManagedAffinityUnavailable { requested, backend })
290            }
291        };
292    }
293
294    let placement = match requested {
295        CpuPlacement::Auto | CpuPlacement::AllAllowed => ResolvedCpuPlacement::AllAllowed {
296            cpus: topology.allowed_cpus().clone(),
297        },
298        CpuPlacement::NumaNode(node) => {
299            if !topology.has_numa_nodes() {
300                return Err(CpuPlacementError::NumaDiscoveryUnavailable { requested, backend });
301            }
302            let cpus = topology
303                .node(node)
304                .ok_or(CpuPlacementError::UnknownNumaNode {
305                    requested,
306                    backend,
307                    node,
308                })?;
309            ResolvedCpuPlacement::NumaNode {
310                id: node,
311                cpus: cpus.cpus().clone(),
312            }
313        }
314    };
315    Ok(ResolvedCpuExecution::Managed(placement))
316}
317
318#[cfg(test)]
319mod tests;