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;