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    /// A pinned engine could not be built for an otherwise valid placement.
220    #[error("cannot resolve {requested:?} for {backend:?}: engine construction failed: {source}")]
221    EngineConstruction {
222        /// The placement requested by the caller.
223        requested: CpuPlacement,
224        /// The selected public backend kind.
225        backend: CpuBackendKind,
226        /// Typed worker-pool construction or affinity failure.
227        #[source]
228        source: CpuEngineConstructionError,
229    },
230    /// The placement state reached an impossible internal compatibility mode.
231    #[error("cannot resolve {requested:?} for {backend:?}: {message}")]
232    InternalState {
233        /// The placement requested by the caller.
234        requested: CpuPlacement,
235        /// The selected public backend kind.
236        backend: CpuBackendKind,
237        /// Stable internal-state diagnostic.
238        message: &'static str,
239    },
240}
241
242#[derive(Clone, Debug, PartialEq, Eq)]
243pub(crate) enum ResolvedCpuExecution {
244    Compatibility,
245    Managed(ResolvedCpuPlacement),
246    ExternalManaged(ResolvedCpuPlacement),
247    ProviderDefaultExclusive,
248}
249
250pub(crate) fn resolve_placement(
251    backend: CpuBackendKind,
252    requested: CpuPlacement,
253    topology: &CpuTopology,
254) -> Result<ResolvedCpuExecution, CpuPlacementError> {
255    resolve_placement_with_affinity(
256        backend,
257        requested,
258        topology,
259        cfg!(any(target_os = "linux", target_os = "android")),
260    )
261}
262
263pub(crate) fn resolve_placement_with_affinity(
264    backend: CpuBackendKind,
265    requested: CpuPlacement,
266    topology: &CpuTopology,
267    managed_affinity_available: bool,
268) -> Result<ResolvedCpuExecution, CpuPlacementError> {
269    if backend == CpuBackendKind::Blas {
270        return match requested {
271            CpuPlacement::Auto => Ok(ResolvedCpuExecution::ProviderDefaultExclusive),
272            CpuPlacement::NumaNode(_) | CpuPlacement::AllAllowed => {
273                Err(CpuPlacementError::ExternalProviderAffinityUnmanaged { requested, backend })
274            }
275        };
276    }
277
278    if !managed_affinity_available {
279        return match requested {
280            CpuPlacement::Auto => Ok(ResolvedCpuExecution::Compatibility),
281            CpuPlacement::NumaNode(_) | CpuPlacement::AllAllowed => {
282                Err(CpuPlacementError::ManagedAffinityUnavailable { requested, backend })
283            }
284        };
285    }
286
287    let placement = match requested {
288        CpuPlacement::Auto | CpuPlacement::AllAllowed => ResolvedCpuPlacement::AllAllowed {
289            cpus: topology.allowed_cpus().clone(),
290        },
291        CpuPlacement::NumaNode(node) => {
292            if !topology.has_numa_nodes() {
293                return Err(CpuPlacementError::NumaDiscoveryUnavailable { requested, backend });
294            }
295            let cpus = topology
296                .node(node)
297                .ok_or(CpuPlacementError::UnknownNumaNode {
298                    requested,
299                    backend,
300                    node,
301                })?;
302            ResolvedCpuPlacement::NumaNode {
303                id: node,
304                cpus: cpus.cpus().clone(),
305            }
306        }
307    };
308    Ok(ResolvedCpuExecution::Managed(placement))
309}
310
311#[cfg(test)]
312mod tests;