1use std::num::NonZeroUsize;
2
3use thiserror::Error;
4
5use crate::{CpuPlacementGuarantee, CpuSet, ParallelMode};
6
7#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
19pub enum CpuThreadCountControl {
20 Sequential,
22 PerCallUpperBound,
24 BinaryClampToOne,
30 #[default]
32 GlobalOrUncontrolled,
33}
34
35#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
47pub enum CpuPlacementControl {
48 EngineWorkers,
50 CallingThread,
52 ExternalWorkers,
54 #[default]
56 None,
57}
58
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub struct CpuProviderExecutionCapabilities {
82 pub thread_count: CpuThreadCountControl,
84 pub placement: CpuPlacementControl,
86 pub worker_local_sequential: bool,
88 pub accepts_sequential: bool,
90 pub accepts_outer: bool,
92 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#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
122pub enum CpuProviderDomainError {
123 #[error(
125 "provider thread-count control {control:?} cannot enforce thread budget {thread_budget}"
126 )]
127 ThreadCountNotEnforceable {
128 thread_budget: usize,
130 control: CpuThreadCountControl,
132 },
133 #[error(
135 "provider placement control {placement:?} cannot enforce {guarantee:?} placement for thread budget {thread_budget}"
136 )]
137 PlacementNotEnforceable {
138 thread_budget: usize,
140 placement: CpuPlacementControl,
142 guarantee: CpuPlacementGuarantee,
144 },
145 #[error(
147 "provider placement control {placement:?} can leave the caller-managed executor for thread budget {thread_budget}"
148 )]
149 CallerManagedPlacementNotEnforceable {
150 thread_budget: usize,
152 placement: CpuPlacementControl,
154 },
155 #[error("provider cannot honor requested CPU parallel mode {mode:?}")]
157 ParallelModeNotSupported {
158 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#[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#[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;