1#![cfg_attr(docsrs, feature(doc_cfg))]
98#![cfg_attr(
103 all(test, feature = "provider-inject"),
104 allow(dead_code, unused_imports)
105)]
106
107macro_rules! preset_scalar {
109 (F32) => {
110 f32
111 };
112 (F64) => {
113 f64
114 };
115 (I32) => {
116 i32
117 };
118 (I64) => {
119 i64
120 };
121 (Bool) => {
122 bool
123 };
124 (C32) => {
125 num_complex::Complex32
126 };
127 (C64) => {
128 num_complex::Complex64
129 };
130}
131#[cfg(not(any(feature = "cpu-faer", feature = "cpu-blas")))]
132compile_error!("enable at least one CPU backend: cpu-faer or cpu-blas");
133
134#[cfg(all(feature = "provider-inject", not(feature = "cpu-blas")))]
135compile_error!("provider-inject requires cpu-blas");
136
137#[cfg(any(
138 all(feature = "blas-openblas", feature = "blas-accelerate"),
139 all(feature = "blas-openblas", feature = "blas-mkl"),
140 all(feature = "blas-accelerate", feature = "blas-mkl"),
141))]
142compile_error!(
143 "enable at most one explicit BLAS provider feature: blas-openblas, blas-accelerate, or blas-mkl"
144);
145
146#[cfg(all(
147 feature = "provider-inject",
148 any(
149 feature = "blas-openblas",
150 feature = "blas-accelerate",
151 feature = "blas-mkl"
152 )
153))]
154compile_error!("provider-inject cannot be combined with explicit BLAS provider features");
155
156pub mod affinity;
157mod affinity_policy;
158mod analytic;
159mod arbiter;
160pub mod backend;
161mod blas1;
162pub(crate) mod buffer_pool {
163 pub use tenferro_cpu_basic::buffer_pool::*;
164}
165mod capability;
166pub mod context;
167mod domain_executor;
168mod dot_runtime;
169pub(crate) use tenferro_cpu_basic::PooledUninitOutput;
170pub(crate) use tenferro_cpu_basic::{erased_raw_strided_ref, erased_raw_strided_uninit_mut};
171pub(crate) use tenferro_internal_cpu_kernels::elementwise;
172mod batch_policy;
173mod engine;
174mod exec_session;
175mod gemm;
176mod indexed_plan_cache;
177mod indexing;
178#[cfg(feature = "provider-inject")]
179pub mod inject;
180mod placement;
181pub mod provider;
182mod provider_capability;
183mod provider_extensions;
184mod reduction;
185mod resource_domain;
186mod runtime_adapter;
187mod structural;
188mod topology;
189
190use num_complex::{Complex32, Complex64};
191#[cfg(test)]
192use strided_kernel::col_major_strides as kernel_col_major_strides;
193#[cfg(test)]
194use strided_kernel::StridedArray;
195
196use crate::buffer_pool::BufferPool;
197pub(crate) use tenferro_tensor::*;
198
199pub(crate) fn cpu_contraction_unsupported_dtype_message(dtype: DType) -> String {
200 let remedy = matches!(dtype, DType::I32 | DType::I64)
201 .then_some(format!("; convert {dtype:?} to F64 before contraction"));
202 format!(
203 "CPU contraction providers support F32/F64/C32/C64{}",
204 remedy.unwrap_or_default()
205 )
206}
207
208#[cfg(feature = "provider-src")]
209extern crate blas_src as _;
210#[cfg(feature = "provider-inject")]
211extern crate cblas_inject as _;
212#[cfg(feature = "provider-inject")]
213extern crate lapack_inject as _;
214#[cfg(feature = "provider-src")]
215extern crate lapack_src as _;
216
217pub use affinity::{
218 available_parallelism, process_cpu_affinity, process_cpu_affinity_count, CpuAffinityError,
219};
220pub use affinity_policy::{
221 resolve_cpu_affinity, resolve_cpu_affinity_with_override, CpuAffinityInput,
222 CpuAffinityInputError, CpuAffinityPolicy, CpuAffinityResolutionError, CpuAffinitySelection,
223 CpuAffinitySelectionReason,
224};
225pub use backend::execution_scope::{current_cpu_execution, CpuThreadExecution};
226pub use backend::{
227 CpuBackend, CpuBackendError, CpuBackendKind, CpuExecutionInfo, CpuExecutionMode,
228 CpuRuntimeIdentity, ExternalCpuDomainRegistryError,
229};
230pub use batch_policy::{with_batch_policy, CpuBatchPolicy, CpuBatchStrategy, CpuBatchThresholds};
231
232pub use buffer_pool::BufferPoolStats;
233pub use capability::cpu_capabilities;
234pub use context::{CpuContext, CpuContextError, DEFAULT_WORKER_STACK_BYTES};
235pub use domain_executor::{
236 CpuDomainExecutor, CpuDomainExecutorCapabilities, CpuDomainExecutorError, CpuExecutorAffinity,
237 CpuExecutorReentrancy, CpuExecutorShutdown, CpuInnerParallelism, RayonCpuDomainExecutor,
238 ScopedCpuJob, ScopedCpuJobs,
239};
240pub use dot_runtime::{
241 CpuProviderBundle, CpuProviderBundleBuildError, CpuProviderBundleBuilder,
242 CpuProviderBundleInstallError, CpuProviderSlot, GeneralContractionPolicy,
243};
244#[doc(hidden)]
245pub use exec_session::CpuExecSession;
246pub use indexed_plan_cache::IndexedPlanCacheLimits;
247pub use placement::{
248 CpuEngineConstructionError, CpuPlacement, CpuPlacementError, ResolvedCpuPlacement,
249};
250pub use provider::{CpuExecutionContext, ParallelMode};
251pub use provider_capability::{
252 CpuPlacementControl, CpuProviderDomainError, CpuProviderExecutionCapabilities,
253 CpuThreadCountControl,
254};
255pub use resource_domain::{
256 CpuAdmissionMode, CpuDomainOwnership, ExternalCpuDomain, ExternalCpuDomainError,
257};
258pub use runtime_adapter::{
259 runtime_engine_id, runtime_engine_registration, runtime_engine_registration_with_id,
260 runtime_hardware_class,
261};
262pub use tenferro_internal_cpu_kernels::scalar_ops::{
265 scalar_binary_into, scalar_fold, AddOp, BinaryScalarOp, MulOp, SubOp,
266};
267pub use tenferro_internal_cpu_kernels::{same_variant_pair, same_variant_unary};
268pub use topology::{
269 discover_cpu_topology, CpuId, CpuNode, CpuSet, CpuSetError, CpuTopology, CpuTopologyError,
270 NumaNodeId,
271};
272
273#[doc(hidden)]
280pub fn with_cpu_exec_session<B, R>(
281 session: &mut B,
282 f: impl for<'a> FnOnce(&'a mut CpuExecSession<'a>) -> R,
283) -> Option<R>
284where
285 B: tenferro_tensor::BackendSession + ?Sized,
286{
287 let data = session
288 .native_session()?
289 .into_marked_ptr::<exec_session::CpuExecSessionMarker>()?;
290 Some(unsafe { f(data.cast::<CpuExecSession<'static>>().as_mut()) })
297}
298
299#[cfg(feature = "cpu-faer")]
326#[cfg_attr(docsrs, doc(cfg(feature = "cpu-faer")))]
327pub trait FaerParallelismExt {
328 fn with_faer_parallelism(
349 &mut self,
350 callback: impl FnOnce(faer::Par) -> tenferro_tensor::Result<()> + Send,
351 ) -> tenferro_tensor::Result<()>;
352}
353
354#[cfg(feature = "cpu-faer")]
355impl<S> FaerParallelismExt for S
356where
357 S: tenferro_tensor::BackendSession + ?Sized,
358{
359 fn with_faer_parallelism(
360 &mut self,
361 callback: impl FnOnce(faer::Par) -> tenferro_tensor::Result<()> + Send,
362 ) -> tenferro_tensor::Result<()> {
363 with_cpu_exec_session(self, |session| session.with_faer_parallelism(callback))
364 .unwrap_or_else(|| {
365 Err(tenferro_tensor::Error::unsupported(
366 "with_faer_parallelism",
367 "selected session is not a CPU/faer execution session",
368 ))
369 })
370 }
371}
372
373#[cfg(test)]
376pub(crate) use analytic::pow;
377#[cfg(test)]
378macro_rules! test_elementwise_wrapper {
379 ($name:ident($($arg:ident: $ty:ty),*) => $with_pool:ident) => {
380 pub(crate) fn $name($($arg: $ty),*) -> crate::Result<Tensor> {
381 let mut buffers = BufferPool::new();
382 elementwise::$with_pool(&mut buffers, &strided_kernel::ExecContext::serial(), $($arg),*)
383 }
384 };
385}
386#[cfg(test)]
387test_elementwise_wrapper!(abs(input: &Tensor) => abs_with_pool);
388#[cfg(test)]
389test_elementwise_wrapper!(add(lhs: &Tensor, rhs: &Tensor) => add_with_pool);
390#[cfg(test)]
391test_elementwise_wrapper!(clamp(input: &Tensor, lower: &Tensor, upper: &Tensor) => clamp_with_pool);
392#[cfg(test)]
393test_elementwise_wrapper!(compare(lhs: &Tensor, rhs: &Tensor, dir: &CompareDir) => compare_with_pool);
394#[cfg(test)]
395test_elementwise_wrapper!(conj(input: &Tensor) => conj_with_pool);
396#[cfg(test)]
397test_elementwise_wrapper!(div(lhs: &Tensor, rhs: &Tensor) => div_with_pool);
398#[cfg(test)]
399test_elementwise_wrapper!(maximum(lhs: &Tensor, rhs: &Tensor) => maximum_with_pool);
400#[cfg(test)]
401test_elementwise_wrapper!(minimum(lhs: &Tensor, rhs: &Tensor) => minimum_with_pool);
402#[cfg(test)]
403test_elementwise_wrapper!(mul(lhs: &Tensor, rhs: &Tensor) => mul_with_pool);
404#[cfg(test)]
405test_elementwise_wrapper!(neg(input: &Tensor) => neg_with_pool);
406#[cfg(test)]
407test_elementwise_wrapper!(rem(lhs: &Tensor, rhs: &Tensor) => rem_with_pool);
408#[cfg(test)]
409test_elementwise_wrapper!(select(pred: &Tensor, on_true: &Tensor, on_false: &Tensor) => select_with_pool);
410#[cfg(test)]
411test_elementwise_wrapper!(sign(input: &Tensor) => sign_with_pool);
412#[cfg(test)]
413test_elementwise_wrapper!(sub(lhs: &Tensor, rhs: &Tensor) => sub_with_pool);
414#[cfg(test)]
415pub(crate) use indexing::{dynamic_slice, dynamic_update_slice, gather, pad, scatter};
416#[cfg(test)]
417pub(crate) use reduction::{reduce_max, reduce_min, reduce_prod, reduce_sum, reduce_sum_squares};
418#[cfg(test)]
419pub(crate) use structural::{
420 broadcast_in_dim, embed_diagonal, extract_diagonal, reshape, transpose, tril, triu,
421};
422
423#[doc(hidden)]
429pub mod linalg_interop {
430 pub use crate::buffer_pool::{BufferPool, PoolScalar};
431 pub use tenferro_cpu_basic::PooledUninitOutput;
432}
433
434#[derive(Debug, thiserror::Error)]
435pub(crate) enum CpuNumericalError {
436 #[error("{op} received a negative integer exponent for dtype {dtype:?}")]
437 NegativeIntegerExponent { op: &'static str, dtype: DType },
438}
439
440pub(crate) fn cpu_negative_integer_exponent(op: &'static str, dtype: DType) -> crate::Error {
441 crate::Error::extension(
442 op,
443 "cpu",
444 ErrorKind::NumericalFailure,
445 CpuNumericalError::NegativeIntegerExponent { op, dtype },
446 )
447}
448
449pub(crate) use tenferro_cpu_basic::{
450 cpu_backend_buffer_error, typed_host_data, typed_view, typed_view_from_view, ConjElem,
451};
452pub(crate) fn materialize_tensor_read_in_domain(
453 buffers: &mut BufferPool,
454 op: &'static str,
455 input: TensorRead<'_>,
456 domain: Option<&dyn SharedTensorAllocationDomain>,
457) -> crate::Result<Tensor> {
458 if let Some(domain) = domain {
459 if input.backend_family().is_some() {
460 return materialize_managed_read(op, input, domain);
461 }
462 }
463 materialize_tensor_read(buffers, op, input)
464}
465
466pub(crate) fn materialize_tensor_read(
467 buffers: &mut BufferPool,
468 op: &'static str,
469 input: TensorRead<'_>,
470) -> crate::Result<Tensor> {
471 match input {
472 TensorRead::Tensor(tensor) => clone_host_tensor_read(op, tensor),
473 TensorRead::View(view) => materialize_tensor_view(buffers, op, view),
474 }
475}
476
477fn materialize_managed_read(
478 op: &'static str,
479 input: TensorRead<'_>,
480 domain: &dyn SharedTensorAllocationDomain,
481) -> crate::Result<Tensor> {
482 if input.placement().memory_kind != MemoryKind::Managed {
483 return Err(Error::host_access(
484 op,
485 HostAccessError::Unsupported { backend: "backend" },
486 ));
487 }
488 match input.allocation_domain() {
489 Some(actual) if actual == domain.id() => {}
490 Some(actual) => {
491 return Err(Error::host_access(
492 op,
493 HostAccessError::ForeignDomain {
494 expected: domain.id(),
495 actual,
496 },
497 ))
498 }
499 None => {
500 return Err(Error::host_access(
501 op,
502 HostAccessError::Unsupported { backend: "backend" },
503 ))
504 }
505 }
506 fn copy<T: TensorScalar>(
507 op: &'static str,
508 input: TypedTensorView<'_, T>,
509 domain: &dyn SharedTensorAllocationDomain,
510 ) -> crate::Result<Tensor> {
511 input.with_host_read(|source| {
514 let mut output = domain.allocate(T::dtype(), input.shape())?;
515 if output.shape() != input.shape()
516 || output.placement().memory_kind != MemoryKind::Managed
517 || TensorRead::from_tensor(&output).allocation_domain() != Some(domain.id())
518 {
519 return Err(Error::runtime_state(
520 op,
521 "shared allocator returned incompatible output",
522 ));
523 }
524 let typed = output.as_typed_mut::<T>().ok_or_else(|| {
525 Error::runtime_state(op, "shared allocator returned the wrong dtype")
526 })?;
527 if let Some(buffer) = typed.backend_buffer_mut() {
528 buffer
529 .map_write()
530 .map_err(|error| Error::host_access(op, error))?
531 .copy_from_slice(source)
532 .map_err(|error| Error::host_access(op, error))?;
533 } else {
534 typed.with_host_write(|target| target.copy_from_slice(source))?;
535 }
536 Ok(output)
537 })?
538 }
539 match input.tensor_view() {
542 TensorView::F32(view) => copy(op, view, domain),
543 TensorView::F64(view) => copy(op, view, domain),
544 TensorView::I32(view) => copy(op, view, domain),
545 TensorView::I64(view) => copy(op, view, domain),
546 TensorView::Bool(view) => copy(op, view, domain),
547 TensorView::C32(view) => copy(op, view, domain),
548 TensorView::C64(view) => copy(op, view, domain),
549 }
550}
551
552pub(crate) fn copy_tensor_read_into(
553 op: &'static str,
554 src: TensorRead<'_>,
555 dst: TensorWrite<'_>,
556) -> crate::Result<()> {
557 let src_dtype = src.dtype();
558 let dst_dtype = dst.dtype();
559 macro_rules! copy_source {
560 ($variant:ident, $src:expr) => {{
561 let src = $src;
562 match dst {
563 TensorWrite::Tensor(dst)
564 if dst.dtype()
565 == <preset_scalar!($variant) as tenferro_tensor::TensorScalar>::dtype() =>
566 {
567 let dst = dst
568 .as_typed_mut::<preset_scalar!($variant)>()
569 .expect("the dtype guard selects this arm");
570 let mut dst = dst.as_view_mut();
571 structural::typed_copy_view_into(&src, &mut dst, op)
572 }
573 TensorWrite::View(TensorViewMut::$variant(mut dst)) => {
574 structural::typed_copy_view_into(&src, &mut dst, op)
575 }
576 _ => Err(crate::Error::dtype_mismatch(op, src_dtype, dst_dtype)),
577 }
578 }};
579 }
580 fn read_refusal(tensor: &Tensor) -> crate::Error {
582 crate::Error::unsupported_dtype(
583 "copy_tensor_read_into",
584 tensor.dtype(),
585 "an externally defined payload is not a runtime read",
586 )
587 }
588
589 match src {
590 TensorRead::Tensor(tensor) => match tensor.dtype() {
591 DType::F32 => copy_source!(
592 F32,
593 tensor
594 .as_typed::<f32>()
595 .ok_or_else(|| read_refusal(tensor))?
596 .as_view()
597 ),
598 DType::F64 => copy_source!(
599 F64,
600 tensor
601 .as_typed::<f64>()
602 .ok_or_else(|| read_refusal(tensor))?
603 .as_view()
604 ),
605 DType::I32 => copy_source!(
606 I32,
607 tensor
608 .as_typed::<i32>()
609 .ok_or_else(|| read_refusal(tensor))?
610 .as_view()
611 ),
612 DType::I64 => copy_source!(
613 I64,
614 tensor
615 .as_typed::<i64>()
616 .ok_or_else(|| read_refusal(tensor))?
617 .as_view()
618 ),
619 DType::Bool => copy_source!(
620 Bool,
621 tensor
622 .as_typed::<bool>()
623 .ok_or_else(|| read_refusal(tensor))?
624 .as_view()
625 ),
626 DType::C32 => copy_source!(
627 C32,
628 tensor
629 .as_typed::<Complex32>()
630 .ok_or_else(|| read_refusal(tensor))?
631 .as_view()
632 ),
633 DType::C64 => copy_source!(
634 C64,
635 tensor
636 .as_typed::<Complex64>()
637 .ok_or_else(|| read_refusal(tensor))?
638 .as_view()
639 ),
640 DType::External(_) => Err(read_refusal(tensor)),
642 },
643 TensorRead::View(TensorView::F32(src)) => copy_source!(F32, src),
644 TensorRead::View(TensorView::F64(src)) => copy_source!(F64, src),
645 TensorRead::View(TensorView::I32(src)) => copy_source!(I32, src),
646 TensorRead::View(TensorView::I64(src)) => copy_source!(I64, src),
647 TensorRead::View(TensorView::Bool(src)) => copy_source!(Bool, src),
648 TensorRead::View(TensorView::C32(src)) => copy_source!(C32, src),
649 TensorRead::View(TensorView::C64(src)) => copy_source!(C64, src),
650 }
652}
653
654fn clone_host_tensor_read(op: &'static str, tensor: &Tensor) -> crate::Result<Tensor> {
662 macro_rules! clone_host {
663 ($variant:ident, $tensor:expr) => {{
664 structural::validate_cpu_host_placement(op, "source", $tensor.placement())?;
665 typed_host_data(op, $tensor)?;
666 $tensor
667 .duplicate()
668 .map(Tensor::from_typed::<preset_scalar!($variant)>)
669 }};
670 }
671
672 match tensor.dtype() {
673 DType::F32 => {
674 let tensor = host_typed::<f32>(op, tensor)?;
675 clone_host!(F32, tensor)
676 }
677 DType::F64 => {
678 let tensor = host_typed::<f64>(op, tensor)?;
679 clone_host!(F64, tensor)
680 }
681 DType::I32 => {
682 let tensor = host_typed::<i32>(op, tensor)?;
683 clone_host!(I32, tensor)
684 }
685 DType::I64 => {
686 let tensor = host_typed::<i64>(op, tensor)?;
687 clone_host!(I64, tensor)
688 }
689 DType::Bool => {
690 let tensor = host_typed::<bool>(op, tensor)?;
691 clone_host!(Bool, tensor)
692 }
693 DType::C32 => {
694 let tensor = host_typed::<Complex32>(op, tensor)?;
695 clone_host!(C32, tensor)
696 }
697 DType::C64 => {
698 let tensor = host_typed::<Complex64>(op, tensor)?;
699 clone_host!(C64, tensor)
700 }
701 DType::External(_) => Ok(Tensor::external_with_placement(
705 tensor
706 .external_payload()
707 .ok_or_else(|| host_typed_error(op, tensor))?
708 .duplicate(),
709 tensor.placement().clone(),
710 )),
711 }
712}
713
714fn host_typed<'a, T: TensorScalar>(
719 op: &'static str,
720 tensor: &'a Tensor,
721) -> crate::Result<&'a TypedTensor<T>> {
722 tensor
723 .as_typed::<T>()
724 .ok_or_else(|| host_typed_error(op, tensor))
725}
726
727fn host_typed_error(op: &'static str, tensor: &Tensor) -> crate::Error {
729 crate::Error::unsupported_dtype(
730 op,
731 tensor.dtype(),
732 "the CPU host clone requires a preset scalar",
733 )
734}
735
736fn materialize_tensor_view(
737 buffers: &mut BufferPool,
738 op: &'static str,
739 view: TensorView<'_>,
740) -> crate::Result<Tensor> {
741 macro_rules! materialize {
742 ($variant:ident, $view:expr) => {{
743 Ok(Tensor::from_typed::<preset_scalar!($variant)>(
744 structural::typed_materialize_view_with_pool(buffers, &$view, op)?,
745 ))
746 }};
747 }
748
749 match view {
750 TensorView::F32(view) => materialize!(F32, view),
751 TensorView::F64(view) => materialize!(F64, view),
752 TensorView::I32(view) => materialize!(I32, view),
753 TensorView::I64(view) => materialize!(I64, view),
754 TensorView::Bool(view) => materialize!(Bool, view),
755 TensorView::C32(view) => materialize!(C32, view),
756 TensorView::C64(view) => materialize!(C64, view),
757 }
758}
759
760#[allow(clippy::uninit_vec)]
766#[cfg(test)]
767pub(crate) unsafe fn typed_array_uninit<T>(shape: &[usize]) -> StridedArray<T> {
768 let total: usize = shape.iter().product();
769 let strides = kernel_col_major_strides(shape);
770 let mut data = Vec::with_capacity(total);
771 unsafe { data.set_len(total) };
773 StridedArray::from_parts(data, shape, &strides, 0).expect("column-major output array")
776}
777
778#[cfg(test)]
779pub(crate) fn tensor_from_array<T: Clone + tenferro_tensor::TensorScalar>(
780 array: StridedArray<T>,
781) -> TypedTensor<T> {
782 TypedTensor::from_vec_col_major(array.dims().to_vec(), array.into_data())
784 .expect("strided array dimensions match owned data length")
785}
786
787#[cfg(all(test, not(feature = "provider-inject")))]
792mod tests;