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;