Skip to main content

tenferro_cpu/
lib.rs

1//! CPU execution engine, kernels and provider selection.
2//!
3//! A [`CpuEngine`] owns one [`Pool`] built by the caller (or none) and the
4//! workspace its operations write. Every operation takes the engine as `&mut`:
5//!
6//! ```rust
7//! use tenferro_cpu::{Pool, CpuEngine};
8//! use tenferro_tensor::{Tensor, TensorElementwise, TensorRead};
9//!
10//! let a = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0])?;
11//! let b = Tensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0])?;
12//!
13//! let mut serial = CpuEngine::serial();
14//! let c = serial.add_read(TensorRead::from_tensor(&a), TensorRead::from_tensor(&b))?;
15//! assert_eq!(c.as_slice::<f64>()?, &[4.0, 6.0]);
16//!
17//! let mut parallel = CpuEngine::new(Pool::builder().threads(2).build()?);
18//! let d = parallel.add_read(TensorRead::from_tensor(&a), TensorRead::from_tensor(&b))?;
19//! assert_eq!(d.as_slice::<f64>()?, &[4.0, 6.0]);
20//! # Ok::<(), Box<dyn std::error::Error>>(())
21//! ```
22#![cfg_attr(docsrs, feature(doc_cfg))]
23// With neither backend the numerical routes are unreachable by design, so the
24// implementation they would call is dead code in that configuration only.
25#![cfg_attr(
26    all(not(feature = "native"), not(feature = "blas")),
27    allow(dead_code, unused_imports)
28)]
29// `provider-inject` unit tests deliberately omit the broad default-backend
30// suite below because no fixture has registered its FFI symbols. That makes
31// private helpers referenced only by the broad suite appear unused in this one
32// test build; call-through coverage lives in the registered integration test.
33#![cfg_attr(
34    all(test, feature = "provider-inject"),
35    allow(dead_code, unused_imports)
36)]
37
38/// The Rust scalar type behind a preset variant name a macro received.
39macro_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// A build with neither backend compiles: the CPU numerical routes report a
63// typed `Unsupported` instead. `native` is the default feature, so a normal
64// dependency always gets a backend, and selecting `blas` alone is the supported
65// alternative. This keeps any subset of the stack compilable, which is what
66// consumers that select a backend crate by crate rely on.
67#[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/// Stable provider identity of the compile-time-selected CPU backend.
138#[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};
189/// Ordinary CPU entry points that take a caller-provided destination and the
190/// caller's own arithmetic instead of the typed pool.
191pub 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};
195/// The placement types of the CPU stack, re-exported so callers need no separate
196/// dependency: build a [`Pool`] with [`Pool::builder`] and hand it to
197/// [`CpuEngine::new`]. The token every fan-out entry takes is
198/// `tenferro_cpu::threadteam::Exec`; it is not re-exported at the root because
199/// [`CpuEngine::exec`] returns the different `tcontract_exec::Exec` bundle.
200pub use threadteam;
201pub use threadteam::{Pool, PoolBuilder};
202
203/// Visit the [`CpuEngine`] behind a type-erased engine, if it is one.
204///
205/// This is a backend-leaf capability bridge. The exact engine marker is checked
206/// before the erased pointer is reconstructed, and the callback cannot return a
207/// borrow of the engine, so the reconstructed reference stays within the
208/// caller's borrow of `engine`.
209///
210/// # Examples
211///
212/// ```
213/// use tenferro_cpu::{with_cpu_engine, CpuEngine};
214/// use tenferro_tensor::Engine;
215///
216/// let mut engine = CpuEngine::serial();
217/// let erased: &mut dyn Engine = &mut engine;
218/// assert_eq!(with_cpu_engine(erased, |cpu| cpu.num_threads()), Some(1));
219/// ```
220#[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    // SAFETY: only `CpuEngine::native_engine` creates a token with the
229    // crate-private `CpuEngineMarker`, and it points that token at a live
230    // `CpuEngine`. The token borrowed `*engine` exclusively, and this function
231    // keeps holding `engine: &mut B` for the whole visit. `R` is chosen by the
232    // caller before the callback's borrow exists, so the reconstructed
233    // reference cannot escape the original borrow.
234    Some(unsafe { f(data.cast::<CpuEngine>().as_mut()) })
235}
236
237/// The faer build this crate links, re-exported for downstream direct calls.
238///
239/// Direct faer work on tenferro-owned host storage borrows column-major slices
240/// and needs no engine handle, so no faer value crosses a tenferro boundary and
241/// a separately pinned `faer` cannot meet a confusing type mismatch (#1939).
242/// Spelling the call through this path still resolves the exact build tenferro
243/// links, which avoids compiling a second faer into one binary; an application
244/// that must control the faer version itself can add a direct dependency
245/// instead. For work that should use the engine's threads, take the engine's
246/// token (`engine.exec()?`): a multi-thread engine's token carries its
247/// `threadteam::Team` (`token.carries_team()`), and `token.team(|team| ..)` hands
248/// faer `Par::Team(team)`. Keep `Par::Seq` otherwise; `Par::rayon` is never used.
249///
250/// # Examples
251///
252/// ```rust
253/// # #[cfg(feature = "native")]
254/// # {
255/// use tenferro_cpu::faer::{linalg::matmul::matmul, Accum, Mat, MatRef, Par};
256///
257/// let identity = [1.0_f64, 0.0, 0.0, 1.0];
258/// let a = [1.0_f64, 3.0, 2.0, 4.0];
259/// let mut out = Mat::<f64>::zeros(2, 2);
260/// matmul(
261///     out.as_mut(),
262///     Accum::Replace,
263///     MatRef::from_column_major_slice(&identity, 2, 2),
264///     MatRef::from_column_major_slice(&a, 2, 2),
265///     1.0,
266///     Par::Seq,
267/// );
268/// assert_eq!(*out.as_ref().get(1, 0), 3.0);
269/// # }
270/// ```
271#[cfg(feature = "native")]
272#[cfg_attr(docsrs, doc(cfg(feature = "native")))]
273pub use faer;
274
275// Unit tests exercise the pool-aware kernels through the former convenience
276// names without restoring those names to the production crate surface.
277#[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/// Engine-scoped CPU scratch-pool API for operation-family crates.
326///
327/// This module is not an application-facing tensor API. It exists so
328/// operation crates that implement CPU kernels can share a `CpuEngine`'s
329/// allocation pool without exposing the pool as a general public contract.
330#[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        // INVARIANT: semantic snapshots require independent storage, not a new
416        // writable alias. Both mappings remain scoped to this same-domain copy.
417        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    // Compact managed descriptors cover retained eager/traced values. Strided
444    // managed canonicalization remains an explicit unsupported boundary.
445    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    /// The typed tensor behind a read adapter's tensor, or the refusal this adapter reports.
486    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            // A caller-owned payload has no compact runtime read.
546            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        // A caller-owned payload is opaque here, so it cannot be copied into a
556    }
557}
558
559/// Clone one owned host tensor into a fresh allocation.
560///
561/// The accepted inputs are: host placement and a preset scalar
562/// (`validate_cpu_host_placement` + `typed_host_data`). `tenferro-ad`'s eager
563/// leaf path states the same acceptance in `cpu_host_owned_read` so it can skip
564/// the session, and `host_leaf_materialization_matches_the_cpu_backend_acceptance`
565/// pins the two against each other. Change both together.
566fn 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        // A caller-owned payload is a compact host tensor, so a contiguous copy is
607        // the payload itself, copied into storage this value owns. Sharing the
608        // payload would alias the caller's storage instead of copying it.
609        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
619/// The typed tensor behind `tensor`, or this module's refusal for a dtype it cannot clone.
620///
621/// Callers reach this from a match on `tensor.dtype()`, so `None` means the tag table and the
622/// runtime dtype disagree rather than a caller mistake.
623fn 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
632/// The refusal the accessor reports when the tag and the runtime dtype disagree.
633fn 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/// Create an output array WITHOUT initializing element values.
667///
668/// # Safety
669/// Caller must write every element before reading. The returned array
670/// contains uninitialized data.
671#[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    // SAFETY: test-only helper is used for outputs whose elements are fully overwritten.
678    unsafe { data.set_len(total) };
679    // Invariant: `kernel_col_major_strides(shape)` and `total` describe the
680    // compact column-major array for this validated test output shape.
681    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    // Invariant: `StridedArray` owns data whose length matches its validated dimensions.
689    TypedTensor::from_vec_col_major(array.dims().to_vec(), array.into_data())
690        .expect("strided array dimensions match owned data length")
691}
692
693// `provider-inject` owns call-through coverage in the serialized integration
694// fixture, which registers every BLAS symbol before the first operation.  The
695// broad unit suite selects the compiled default backend and therefore must not
696// call an intentionally unregistered injected symbol.
697#[cfg(all(test, not(feature = "provider-inject")))]
698mod tests;