1#![cfg_attr(docsrs, feature(doc_cfg))]
23#![cfg_attr(
26 all(not(feature = "native"), not(feature = "blas")),
27 allow(dead_code, unused_imports)
28)]
29#![cfg_attr(
34 all(test, feature = "provider-inject"),
35 allow(dead_code, unused_imports)
36)]
37
38macro_rules! preset_scalar {
40 (F32) => {
41 f32
42 };
43 (F64) => {
44 f64
45 };
46 (I32) => {
47 i32
48 };
49 (I64) => {
50 i64
51 };
52 (Bool) => {
53 bool
54 };
55 (C32) => {
56 num_complex::Complex32
57 };
58 (C64) => {
59 num_complex::Complex64
60 };
61}
62#[cfg(all(feature = "native", feature = "blas"))]
68compile_error!("native and blas are mutually exclusive; use --no-default-features for blas");
69
70#[cfg(all(feature = "provider-inject", not(feature = "blas")))]
71compile_error!("provider-inject requires blas");
72
73#[cfg(any(
74 all(feature = "blas-openblas", feature = "blas-accelerate"),
75 all(feature = "blas-openblas", feature = "blas-mkl"),
76 all(feature = "blas-accelerate", feature = "blas-mkl"),
77))]
78compile_error!(
79 "enable at most one explicit BLAS provider feature: blas-openblas, blas-accelerate, or blas-mkl"
80);
81
82#[cfg(all(
83 feature = "provider-inject",
84 any(
85 feature = "blas-openblas",
86 feature = "blas-accelerate",
87 feature = "blas-mkl"
88 )
89))]
90compile_error!("provider-inject cannot be combined with explicit BLAS provider features");
91
92mod activation;
93mod analytic;
94pub mod backend;
95mod batch_policy;
96mod blas1;
97pub(crate) mod buffer_pool {
98 pub use tenferro_cpu_basic::buffer_pool::*;
99}
100mod capability;
101mod dot_runtime;
102pub(crate) use tenferro_cpu_basic::PooledUninitOutput;
103pub(crate) use tenferro_cpu_basic::{erased_raw_strided_ref, erased_raw_strided_uninit_mut};
104pub(crate) use tenferro_internal_cpu_kernels::elementwise;
105mod contraction;
106mod engine;
107mod exec_session;
108mod plan_cache;
109#[doc(hidden)]
110pub use contraction::ContractionWorkspaces;
111mod gemm;
112mod indexed_plan_cache;
113mod indexing;
114#[cfg(feature = "provider-inject")]
115pub mod inject;
116mod operand;
117mod parallel;
118mod provider;
119mod reduction;
120mod runtime_adapter;
121mod simd_math;
122mod softmax;
123mod structural;
124#[cfg(test)]
125pub(crate) mod test_support;
126
127use num_complex::{Complex32, Complex64};
128#[cfg(test)]
129use strided_kernel::col_major_strides as kernel_col_major_strides;
130#[cfg(test)]
131use strided_kernel::StridedArray;
132
133use crate::buffer_pool::BufferPool;
134pub(crate) use tenferro_tensor::*;
135use threadteam::Exec;
136
137#[doc(hidden)]
139pub fn cpu_provider_id() -> &'static str {
140 #[cfg(feature = "native")]
141 {
142 "tenferro.cpu.faer"
143 }
144 #[cfg(feature = "blas")]
145 {
146 "tenferro.cpu.blas"
147 }
148 #[cfg(all(not(feature = "native"), not(feature = "blas")))]
149 {
150 "tenferro.cpu.none"
151 }
152}
153
154pub(crate) fn cpu_contraction_unsupported_dtype_message(dtype: DType) -> String {
155 let remedy = matches!(dtype, DType::I32 | DType::I64)
156 .then_some(format!("; convert {dtype:?} to F64 before contraction"));
157 format!(
158 "CPU contraction providers support F32/F64/C32/C64{}",
159 remedy.unwrap_or_default()
160 )
161}
162
163#[cfg(feature = "provider-src")]
164extern crate blas_src as _;
165#[cfg(feature = "provider-inject")]
166extern crate cblas_inject as _;
167#[cfg(feature = "provider-inject")]
168extern crate lapack_inject as _;
169#[cfg(feature = "provider-src")]
170extern crate lapack_src as _;
171
172pub use batch_policy::{with_batch_policy, CpuBatchPolicy, CpuBatchStrategy, CpuBatchThresholds};
173
174pub use buffer_pool::BufferPoolStats;
175pub use capability::cpu_capabilities;
176#[doc(hidden)]
177pub use dot_runtime::{validate_cpu_host_read, validate_cpu_host_write};
178pub use engine::{CpuEngine, CpuEngineError};
179#[doc(hidden)]
180pub use gemm::GemmAnalysisCache;
181pub use indexed_plan_cache::IndexedPlanCacheLimits;
182pub use plan_cache::CpuPlanCache;
183#[doc(hidden)]
184pub use provider::{reshape_tensor, with_materialized_tensor_read};
185pub use runtime_adapter::{
186 runtime_engine_id, runtime_engine_registration, runtime_engine_registration_with_id,
187 runtime_hardware_class,
188};
189pub use tenferro_internal_cpu_kernels::scalar_ops::{
192 scalar_binary_into, scalar_fold, AddOp, BinaryScalarOp, MulOp, SubOp,
193};
194pub use tenferro_internal_cpu_kernels::{same_variant_pair, same_variant_unary};
195pub use threadteam;
201pub use threadteam::{Pool, PoolBuilder};
202
203#[doc(hidden)]
221pub fn with_cpu_engine<B, R>(engine: &mut B, f: impl FnOnce(&mut CpuEngine) -> R) -> Option<R>
222where
223 B: tenferro_tensor::Engine + ?Sized,
224{
225 let data = engine
226 .native_engine()?
227 .into_marked_ptr::<exec_session::CpuEngineMarker>()?;
228 Some(unsafe { f(data.cast::<CpuEngine>().as_mut()) })
235}
236
237#[cfg(feature = "native")]
272#[cfg_attr(docsrs, doc(cfg(feature = "native")))]
273pub use faer;
274
275#[cfg(test)]
278pub(crate) use analytic::pow;
279#[cfg(test)]
280macro_rules! test_elementwise_wrapper {
281 ($name:ident($($arg:ident: $ty:ty),*) => $with_pool:ident) => {
282 pub(crate) fn $name($($arg: $ty),*) -> crate::Result<Tensor> {
283 let mut buffers = BufferPool::new();
284 elementwise::$with_pool(&mut buffers, $($arg),*, Exec::serial())
285 }
286 };
287}
288#[cfg(test)]
289test_elementwise_wrapper!(abs(input: &Tensor) => abs_with_pool);
290#[cfg(test)]
291test_elementwise_wrapper!(add(lhs: &Tensor, rhs: &Tensor) => add_with_pool);
292#[cfg(test)]
293test_elementwise_wrapper!(clamp(input: &Tensor, lower: &Tensor, upper: &Tensor) => clamp_with_pool);
294#[cfg(test)]
295test_elementwise_wrapper!(compare(lhs: &Tensor, rhs: &Tensor, dir: &CompareDir) => compare_with_pool);
296#[cfg(test)]
297test_elementwise_wrapper!(conj(input: &Tensor) => conj_with_pool);
298#[cfg(test)]
299test_elementwise_wrapper!(div(lhs: &Tensor, rhs: &Tensor) => div_with_pool);
300#[cfg(test)]
301test_elementwise_wrapper!(maximum(lhs: &Tensor, rhs: &Tensor) => maximum_with_pool);
302#[cfg(test)]
303test_elementwise_wrapper!(minimum(lhs: &Tensor, rhs: &Tensor) => minimum_with_pool);
304#[cfg(test)]
305test_elementwise_wrapper!(mul(lhs: &Tensor, rhs: &Tensor) => mul_with_pool);
306#[cfg(test)]
307test_elementwise_wrapper!(neg(input: &Tensor) => neg_with_pool);
308#[cfg(test)]
309test_elementwise_wrapper!(rem(lhs: &Tensor, rhs: &Tensor) => rem_with_pool);
310#[cfg(test)]
311test_elementwise_wrapper!(select(pred: &Tensor, on_true: &Tensor, on_false: &Tensor) => select_with_pool);
312#[cfg(test)]
313test_elementwise_wrapper!(sign(input: &Tensor) => sign_with_pool);
314#[cfg(test)]
315test_elementwise_wrapper!(sub(lhs: &Tensor, rhs: &Tensor) => sub_with_pool);
316#[cfg(test)]
317pub(crate) use indexing::{dynamic_slice, dynamic_update_slice, gather, pad, scatter};
318#[cfg(test)]
319pub(crate) use reduction::{reduce_max, reduce_min, reduce_prod, reduce_sum, reduce_sum_squares};
320#[cfg(test)]
321pub(crate) use structural::{
322 broadcast_in_dim, embed_diagonal, extract_diagonal, reshape, transpose, tril, triu,
323};
324
325#[doc(hidden)]
331pub mod linalg_interop {
332 pub use crate::buffer_pool::{BufferPool, PoolScalar};
333 pub use tenferro_cpu_basic::PooledUninitOutput;
334}
335
336#[derive(Debug, thiserror::Error)]
337pub(crate) enum CpuNumericalError {
338 #[error("{op} received a negative integer exponent for dtype {dtype:?}")]
339 NegativeIntegerExponent { op: &'static str, dtype: DType },
340}
341
342pub(crate) fn cpu_negative_integer_exponent(op: &'static str, dtype: DType) -> crate::Error {
343 crate::Error::extension(
344 op,
345 "cpu",
346 ErrorKind::NumericalFailure,
347 CpuNumericalError::NegativeIntegerExponent { op, dtype },
348 )
349}
350
351pub(crate) use tenferro_cpu_basic::{
352 cpu_backend_buffer_error, typed_host_data, typed_view, typed_view_from_view,
353};
354pub(crate) fn materialize_tensor_read_in_domain(
355 buffers: &mut BufferPool,
356 op: &'static str,
357 input: TensorRead<'_>,
358 domain: Option<&dyn SharedTensorAllocationDomain>,
359 exec: Exec<'_>,
360) -> crate::Result<Tensor> {
361 if let Some(domain) = domain {
362 if input.backend_family().is_some() {
363 return materialize_managed_read(op, input, domain);
364 }
365 }
366 materialize_tensor_read(buffers, op, input, exec)
367}
368
369pub(crate) fn materialize_tensor_read(
370 buffers: &mut BufferPool,
371 op: &'static str,
372 input: TensorRead<'_>,
373 exec: Exec<'_>,
374) -> crate::Result<Tensor> {
375 match input {
376 TensorRead::Tensor(tensor) => clone_host_tensor_read(op, tensor),
377 TensorRead::View(view) => materialize_tensor_view(buffers, op, view, exec),
378 }
379}
380
381fn materialize_managed_read(
382 op: &'static str,
383 input: TensorRead<'_>,
384 domain: &dyn SharedTensorAllocationDomain,
385) -> crate::Result<Tensor> {
386 if input.placement().memory_kind != MemoryKind::Managed {
387 return Err(Error::host_access(
388 op,
389 HostAccessError::Unsupported { backend: "backend" },
390 ));
391 }
392 match input.allocation_domain() {
393 Some(actual) if actual == domain.id() => {}
394 Some(actual) => {
395 return Err(Error::host_access(
396 op,
397 HostAccessError::ForeignDomain {
398 expected: domain.id(),
399 actual,
400 },
401 ))
402 }
403 None => {
404 return Err(Error::host_access(
405 op,
406 HostAccessError::Unsupported { backend: "backend" },
407 ))
408 }
409 }
410 fn copy<T: TensorScalar>(
411 op: &'static str,
412 input: TypedTensorView<'_, T>,
413 domain: &dyn SharedTensorAllocationDomain,
414 ) -> crate::Result<Tensor> {
415 input.with_host_read(|source| {
418 let mut output = domain.allocate(T::dtype(), input.shape())?;
419 if output.shape() != input.shape()
420 || output.placement().memory_kind != MemoryKind::Managed
421 || TensorRead::from_tensor(&output).allocation_domain() != Some(domain.id())
422 {
423 return Err(Error::runtime_state(
424 op,
425 "shared allocator returned incompatible output",
426 ));
427 }
428 let typed = output.as_typed_mut::<T>().ok_or_else(|| {
429 Error::runtime_state(op, "shared allocator returned the wrong dtype")
430 })?;
431 if let Some(buffer) = typed.backend_buffer_mut() {
432 buffer
433 .map_write()
434 .map_err(|error| Error::host_access(op, error))?
435 .copy_from_slice(source)
436 .map_err(|error| Error::host_access(op, error))?;
437 } else {
438 typed.with_host_write(|target| target.copy_from_slice(source))?;
439 }
440 Ok(output)
441 })?
442 }
443 match input.tensor_view() {
446 TensorView::F32(view) => copy(op, view, domain),
447 TensorView::F64(view) => copy(op, view, domain),
448 TensorView::I32(view) => copy(op, view, domain),
449 TensorView::I64(view) => copy(op, view, domain),
450 TensorView::Bool(view) => copy(op, view, domain),
451 TensorView::C32(view) => copy(op, view, domain),
452 TensorView::C64(view) => copy(op, view, domain),
453 }
454}
455
456pub(crate) fn copy_tensor_read_into(
457 op: &'static str,
458 src: TensorRead<'_>,
459 dst: TensorWrite<'_>,
460 exec: Exec<'_>,
461) -> crate::Result<()> {
462 let src_dtype = src.dtype();
463 let dst_dtype = dst.dtype();
464 macro_rules! copy_source {
465 ($variant:ident, $src:expr) => {{
466 let src = $src;
467 match dst {
468 TensorWrite::Tensor(dst)
469 if dst.dtype()
470 == <preset_scalar!($variant) as tenferro_tensor::TensorScalar>::dtype() =>
471 {
472 let dst = dst
473 .as_typed_mut::<preset_scalar!($variant)>()
474 .expect("the dtype guard selects this arm");
475 let mut dst = dst.as_view_mut();
476 structural::typed_copy_view_into(&src, &mut dst, op, exec)
477 }
478 TensorWrite::View(TensorViewMut::$variant(mut dst)) => {
479 structural::typed_copy_view_into(&src, &mut dst, op, exec)
480 }
481 _ => Err(crate::Error::dtype_mismatch(op, src_dtype, dst_dtype)),
482 }
483 }};
484 }
485 fn read_refusal(tensor: &Tensor) -> crate::Error {
487 crate::Error::unsupported_dtype(
488 "copy_tensor_read_into",
489 tensor.dtype(),
490 "an externally defined payload is not a runtime read",
491 )
492 }
493
494 match src {
495 TensorRead::Tensor(tensor) => match tensor.dtype() {
496 DType::F32 => copy_source!(
497 F32,
498 tensor
499 .as_typed::<f32>()
500 .ok_or_else(|| read_refusal(tensor))?
501 .as_view()
502 ),
503 DType::F64 => copy_source!(
504 F64,
505 tensor
506 .as_typed::<f64>()
507 .ok_or_else(|| read_refusal(tensor))?
508 .as_view()
509 ),
510 DType::I32 => copy_source!(
511 I32,
512 tensor
513 .as_typed::<i32>()
514 .ok_or_else(|| read_refusal(tensor))?
515 .as_view()
516 ),
517 DType::I64 => copy_source!(
518 I64,
519 tensor
520 .as_typed::<i64>()
521 .ok_or_else(|| read_refusal(tensor))?
522 .as_view()
523 ),
524 DType::Bool => copy_source!(
525 Bool,
526 tensor
527 .as_typed::<bool>()
528 .ok_or_else(|| read_refusal(tensor))?
529 .as_view()
530 ),
531 DType::C32 => copy_source!(
532 C32,
533 tensor
534 .as_typed::<Complex32>()
535 .ok_or_else(|| read_refusal(tensor))?
536 .as_view()
537 ),
538 DType::C64 => copy_source!(
539 C64,
540 tensor
541 .as_typed::<Complex64>()
542 .ok_or_else(|| read_refusal(tensor))?
543 .as_view()
544 ),
545 DType::External(_) => Err(read_refusal(tensor)),
547 },
548 TensorRead::View(TensorView::F32(src)) => copy_source!(F32, src),
549 TensorRead::View(TensorView::F64(src)) => copy_source!(F64, src),
550 TensorRead::View(TensorView::I32(src)) => copy_source!(I32, src),
551 TensorRead::View(TensorView::I64(src)) => copy_source!(I64, src),
552 TensorRead::View(TensorView::Bool(src)) => copy_source!(Bool, src),
553 TensorRead::View(TensorView::C32(src)) => copy_source!(C32, src),
554 TensorRead::View(TensorView::C64(src)) => copy_source!(C64, src),
555 }
557}
558
559fn clone_host_tensor_read(op: &'static str, tensor: &Tensor) -> crate::Result<Tensor> {
567 macro_rules! clone_host {
568 ($variant:ident, $tensor:expr) => {{
569 structural::validate_cpu_host_placement(op, "source", $tensor.placement())?;
570 typed_host_data(op, $tensor)?;
571 $tensor
572 .duplicate()
573 .map(Tensor::from_typed::<preset_scalar!($variant)>)
574 }};
575 }
576
577 match tensor.dtype() {
578 DType::F32 => {
579 let tensor = host_typed::<f32>(op, tensor)?;
580 clone_host!(F32, tensor)
581 }
582 DType::F64 => {
583 let tensor = host_typed::<f64>(op, tensor)?;
584 clone_host!(F64, tensor)
585 }
586 DType::I32 => {
587 let tensor = host_typed::<i32>(op, tensor)?;
588 clone_host!(I32, tensor)
589 }
590 DType::I64 => {
591 let tensor = host_typed::<i64>(op, tensor)?;
592 clone_host!(I64, tensor)
593 }
594 DType::Bool => {
595 let tensor = host_typed::<bool>(op, tensor)?;
596 clone_host!(Bool, tensor)
597 }
598 DType::C32 => {
599 let tensor = host_typed::<Complex32>(op, tensor)?;
600 clone_host!(C32, tensor)
601 }
602 DType::C64 => {
603 let tensor = host_typed::<Complex64>(op, tensor)?;
604 clone_host!(C64, tensor)
605 }
606 DType::External(_) => Ok(Tensor::external_with_placement(
610 tensor
611 .external_payload()
612 .ok_or_else(|| host_typed_error(op, tensor))?
613 .duplicate(),
614 tensor.placement().clone(),
615 )),
616 }
617}
618
619fn host_typed<'a, T: TensorScalar>(
624 op: &'static str,
625 tensor: &'a Tensor,
626) -> crate::Result<&'a TypedTensor<T>> {
627 tensor
628 .as_typed::<T>()
629 .ok_or_else(|| host_typed_error(op, tensor))
630}
631
632fn host_typed_error(op: &'static str, tensor: &Tensor) -> crate::Error {
634 crate::Error::unsupported_dtype(
635 op,
636 tensor.dtype(),
637 "the CPU host clone requires a preset scalar",
638 )
639}
640
641fn materialize_tensor_view(
642 buffers: &mut BufferPool,
643 op: &'static str,
644 view: TensorView<'_>,
645 exec: Exec<'_>,
646) -> crate::Result<Tensor> {
647 macro_rules! materialize {
648 ($variant:ident, $view:expr) => {{
649 Ok(Tensor::from_typed::<preset_scalar!($variant)>(
650 structural::typed_materialize_view_with_pool(buffers, &$view, op, exec)?,
651 ))
652 }};
653 }
654
655 match view {
656 TensorView::F32(view) => materialize!(F32, view),
657 TensorView::F64(view) => materialize!(F64, view),
658 TensorView::I32(view) => materialize!(I32, view),
659 TensorView::I64(view) => materialize!(I64, view),
660 TensorView::Bool(view) => materialize!(Bool, view),
661 TensorView::C32(view) => materialize!(C32, view),
662 TensorView::C64(view) => materialize!(C64, view),
663 }
664}
665
666#[allow(clippy::uninit_vec)]
672#[cfg(test)]
673pub(crate) unsafe fn typed_array_uninit<T>(shape: &[usize]) -> StridedArray<T> {
674 let total: usize = shape.iter().product();
675 let strides = kernel_col_major_strides(shape);
676 let mut data = Vec::with_capacity(total);
677 unsafe { data.set_len(total) };
679 StridedArray::from_parts(data, shape, &strides, 0).expect("column-major output array")
682}
683
684#[cfg(test)]
685pub(crate) fn tensor_from_array<T: Clone + tenferro_tensor::TensorScalar>(
686 array: StridedArray<T>,
687) -> TypedTensor<T> {
688 TypedTensor::from_vec_col_major(array.dims().to_vec(), array.into_data())
690 .expect("strided array dimensions match owned data length")
691}
692
693#[cfg(all(test, not(feature = "provider-inject")))]
698mod tests;