Skip to main content

tenferro_fft/
cpu.rs

1/// The Rust scalar type behind a preset variant name a macro received.
2macro_rules! preset_scalar {
3    (F32) => {
4        f32
5    };
6    (F64) => {
7        f64
8    };
9    (I32) => {
10        i32
11    };
12    (I64) => {
13        i64
14    };
15    (Bool) => {
16        bool
17    };
18    (C32) => {
19        num_complex::Complex32
20    };
21    (C64) => {
22        num_complex::Complex64
23    };
24}
25mod lanes;
26
27use crate::backend::FftExecutionCache;
28use crate::cache::{CachedFftPlanScalar, ExtensionFftPlanCache, FftPlanProvider};
29use crate::{
30    expected_dtype_description, fft_op_name, output_shape_c2c, output_shape_c2r, output_shape_r2c,
31    transform_len, validate_c2r_spectrum_len, FftBackend, FftNorm, FftOperation, FftPlanSpec,
32};
33use num_complex::Complex;
34use num_traits::{Float, FromPrimitive};
35#[cfg(feature = "autodiff")]
36use std::mem::MaybeUninit;
37use tenferro_cpu::linalg_interop::{BufferPool, PoolScalar, PooledUninitOutput};
38use tenferro_cpu::CpuEngine;
39use tenferro_tensor::{
40    AllocationDomainId, DeviceKind, HostAccessError, MemoryKind, Placement,
41    SharedTensorAllocationDomain, Tensor, TensorRead, TensorScalar, TensorStructural, TensorView,
42    TypedTensor,
43};
44
45impl FftBackend for CpuEngine {
46    fn validate_fft_read_input(
47        &self,
48        op: &'static str,
49        input: &TensorRead<'_>,
50    ) -> tenferro_tensor::Result<()> {
51        validate_host_fft_read_input(op, input)
52    }
53
54    fn execute_fft_read(
55        &mut self,
56        input: TensorRead<'_>,
57        spec: &FftPlanSpec,
58        mut cache: FftExecutionCache<'_>,
59    ) -> tenferro_tensor::Result<Tensor> {
60        let view = match input {
61            TensorRead::Tensor(input) => return self.execute_fft(input, spec, cache),
62            TensorRead::View(view) => view,
63        };
64        validate_host_fft_read_input(
65            fft_op_name(spec.operation()),
66            &TensorRead::View(view.clone()),
67        )?;
68        validate_spec_metadata(view.dtype(), view.shape(), spec)?;
69        if !view.is_col_major_contiguous()? {
70            let owned = self.to_contiguous_read(TensorRead::View(view))?;
71            return self.execute_fft(&owned, spec, cache);
72        }
73        let mut plans = ExtensionFftPlanCache::new(cache.store_mut());
74        self.with_linalg_pool(|exec, buffers| {
75            macro_rules! transform {
76                ($input:expr, $kind:ident, $variant:ident, $project:expr) => {
77                    pooled_transform(
78                        lanes::Input::$kind($input.as_slice()?),
79                        $input.shape(),
80                        spec,
81                        &mut plans,
82                        buffers,
83                        $project,
84                        exec.token(),
85                    )
86                    .map(Tensor::from_typed::<preset_scalar!($variant)>)
87                };
88            }
89            match (spec.operation(), view) {
90                (FftOperation::C2cForward | FftOperation::C2cInverse, TensorView::C64(x)) => {
91                    transform!(x, Complex, C64, |v| v)
92                }
93                (FftOperation::C2cForward | FftOperation::C2cInverse, TensorView::C32(x)) => {
94                    transform!(x, Complex, C32, |v| v)
95                }
96                (FftOperation::R2cFull | FftOperation::R2cOnesided, TensorView::F64(x)) => {
97                    transform!(x, Real, C64, |v| v)
98                }
99                (FftOperation::R2cFull | FftOperation::R2cOnesided, TensorView::F32(x)) => {
100                    transform!(x, Real, C32, |v| v)
101                }
102                (FftOperation::C2r, TensorView::C64(x)) => {
103                    transform!(x, Complex, F64, |v: Complex<f64>| v.re)
104                }
105                (FftOperation::C2r, TensorView::C32(x)) => {
106                    transform!(x, Complex, F32, |v: Complex<f32>| v.re)
107                }
108                (operation, other) => Err(crate::tensor_unsupported_dtype(
109                    fft_op_name(operation),
110                    other.dtype(),
111                    expected_dtype_description(operation),
112                )),
113            }
114        })
115    }
116
117    fn execute_fft(
118        &mut self,
119        input: &Tensor,
120        spec: &FftPlanSpec,
121        mut cache: FftExecutionCache<'_>,
122    ) -> tenferro_tensor::Result<Tensor> {
123        validate_spec_input(input, spec)?;
124        let mut plans = ExtensionFftPlanCache::new(cache.store_mut());
125        let domain = self.shared_allocation_domain().cloned();
126        let managed = if input.is_backend_buffer() {
127            domain.as_deref()
128        } else {
129            None
130        };
131        if managed.is_none() {
132            validate_host_fft_input(fft_op_name(spec.operation()), input)?;
133        }
134        self.with_linalg_pool(|exec, buffers| {
135            {
136                execute_fft_with_plans(input, spec, &mut plans, buffers, managed, exec.token())
137            }
138        })
139    }
140}
141
142/// The typed tensor behind an FFT operand.
143///
144/// The tables that call this match on the payload's dtype first, so the refusal is unreachable; it
145/// names the operation rather than a dtype description, which the wildcard arms still report.
146fn fft_operand<'a, T: tenferro_tensor::TensorScalar>(
147    input: &'a Tensor,
148    op: &'static str,
149) -> tenferro_tensor::Result<&'a tenferro_tensor::TypedTensor<T>> {
150    input
151        .as_typed::<T>()
152        .ok_or_else(|| crate::tensor_unsupported_dtype(op, input.dtype(), "a supported dtype"))
153}
154
155#[cfg(feature = "autodiff")]
156pub(crate) fn execute_in_place(
157    session: &mut CpuEngine,
158    input: &mut Tensor,
159    spec: &FftPlanSpec,
160    mut cache: FftExecutionCache<'_>,
161) -> tenferro_tensor::Result<()> {
162    validate_spec_input(input, spec)?;
163    validate_host_fft_input("fft_in_place", input)?;
164    if !matches!(
165        spec.operation(),
166        FftOperation::C2cForward | FftOperation::C2cInverse
167    ) || spec
168        .requested_len()
169        .is_some_and(|n| n != input.shape()[spec.normalized_axis()])
170    {
171        return Err(tenferro_tensor::Error::unsupported(
172            "fft_in_place",
173            "in-place FFT requires shape-preserving complex input",
174        ));
175    }
176    let mut plans = ExtensionFftPlanCache::new(cache.store_mut());
177    session.with_linalg_pool(|exec, buffers| match input.dtype() {
178        tenferro_tensor::DType::C64 => in_place_typed(
179            in_place_operand::<f64>(&mut *input)?,
180            spec,
181            &mut plans,
182            buffers,
183            exec.token(),
184        ),
185        tenferro_tensor::DType::C32 => in_place_typed(
186            in_place_operand::<f32>(&mut *input)?,
187            spec,
188            &mut plans,
189            buffers,
190            exec.token(),
191        ),
192        other => Err(crate::tensor_unsupported_dtype(
193            "fft_in_place",
194            other,
195            "C32 or C64",
196        )),
197    })
198}
199
200/// The typed tensor behind an in-place FFT operand, which the plan writes through.
201///
202/// The arms that call this match on the payload's dtype first, so the refusal reports a dtype the plan
203/// cannot write rather than a caller mistake.
204#[cfg(feature = "autodiff")]
205fn in_place_operand<T: tenferro_tensor::TensorScalar>(
206    input: &mut Tensor,
207) -> tenferro_tensor::Result<&mut TypedTensor<Complex<T>>>
208where
209    Complex<T>: tenferro_tensor::TensorScalar,
210{
211    let dtype = input.dtype();
212    input
213        .as_typed_mut::<Complex<T>>()
214        .ok_or_else(|| crate::tensor_unsupported_dtype("fft_in_place", dtype, "C32 or C64"))
215}
216
217#[cfg(feature = "autodiff")]
218fn in_place_typed<T: CachedFftPlanScalar + TensorScalar>(
219    input: &mut TypedTensor<Complex<T>>,
220    spec: &FftPlanSpec,
221    plans: &mut (impl FftPlanProvider + ?Sized),
222    buffers: &mut BufferPool,
223    exec: tenferro_cpu::threadteam::Exec<'_>,
224) -> tenferro_tensor::Result<()>
225where
226    Complex<T>: TensorScalar + PoolScalar,
227{
228    let shape = input.shape().to_vec();
229    let axis = spec.normalized_axis();
230    let len = shape[axis];
231    // Use the descriptor-bounded write guard, not the whole root allocation:
232    // compact slices may have a nonzero offset or a smaller logical extent.
233    input.with_host_write(|values| {
234        // SAFETY: this is an exclusive borrow of initialized Complex<T> storage.
235        // Shape-preserving c2c with identity projection writes only valid Complex<T>
236        // values; on error or unwind unwritten values also remain initialized.
237        unsafe {
238            let output = std::slice::from_raw_parts_mut(
239                values.as_mut_ptr().cast::<MaybeUninit<Complex<T>>>(),
240                values.len(),
241            );
242            lanes::execute::<T, Complex<T>>(
243                None,
244                &shape,
245                axis,
246                len,
247                len,
248                spec.operation(),
249                spec.norm(),
250                plans,
251                buffers,
252                output,
253                |v| v,
254                exec,
255            )
256        }
257    })?
258}
259
260fn validate_spec_input(input: &Tensor, spec: &FftPlanSpec) -> tenferro_tensor::Result<()> {
261    validate_spec_metadata(input.dtype(), input.shape(), spec)
262}
263
264fn validate_spec_metadata(
265    dtype: tenferro_tensor::DType,
266    shape: &[usize],
267    spec: &FftPlanSpec,
268) -> tenferro_tensor::Result<()> {
269    if dtype != spec.input_dtype() {
270        return Err(tenferro_tensor::Error::dtype_mismatch(
271            fft_op_name(spec.operation()),
272            spec.input_dtype(),
273            dtype,
274        ));
275    }
276    if shape != spec.input_shape() {
277        return Err(tenferro_tensor::Error::invalid_argument(
278            fft_op_name(spec.operation()),
279            "input shape",
280            format!(
281                "validated FFT spec shape {:?} does not match execution input shape {:?}",
282                spec.input_shape(),
283                shape
284            ),
285        ));
286    }
287    if !spec.requires_compact_column_major() {
288        return Err(tenferro_tensor::Error::unsupported(
289            fft_op_name(spec.operation()),
290            "CpuBackend FFT requires compact column-major input",
291        ));
292    }
293    Ok(())
294}
295
296fn execute_fft_with_plans(
297    input: &Tensor,
298    spec: &FftPlanSpec,
299    plans: &mut (impl FftPlanProvider + ?Sized),
300    buffers: &mut BufferPool,
301    domain: Option<&dyn SharedTensorAllocationDomain>,
302    exec: tenferro_cpu::threadteam::Exec<'_>,
303) -> tenferro_tensor::Result<Tensor> {
304    macro_rules! transform {
305        ($input:expr, $kind:ident, $variant:ident, $project:expr) => {
306            transform(
307                $input,
308                spec,
309                plans,
310                buffers,
311                domain,
312                |values| lanes::Input::$kind(values),
313                $project,
314                exec,
315            )
316            .map(Tensor::from_typed::<preset_scalar!($variant)>)
317        };
318    }
319    match (spec.operation(), input.dtype()) {
320        (FftOperation::C2cForward | FftOperation::C2cInverse, tenferro_tensor::DType::C64) => {
321            let x = fft_operand::<Complex<f64>>(input, fft_op_name(spec.operation()))?;
322            transform!(x, Complex, C64, |v| v)
323        }
324        (FftOperation::C2cForward | FftOperation::C2cInverse, tenferro_tensor::DType::C32) => {
325            let x = fft_operand::<Complex<f32>>(input, fft_op_name(spec.operation()))?;
326            transform!(x, Complex, C32, |v| v)
327        }
328        (FftOperation::R2cFull | FftOperation::R2cOnesided, tenferro_tensor::DType::F64) => {
329            let x = fft_operand::<f64>(input, fft_op_name(spec.operation()))?;
330            transform!(x, Real, C64, |v| v)
331        }
332        (FftOperation::R2cFull | FftOperation::R2cOnesided, tenferro_tensor::DType::F32) => {
333            let x = fft_operand::<f32>(input, fft_op_name(spec.operation()))?;
334            transform!(x, Real, C32, |v| v)
335        }
336        (FftOperation::C2r, tenferro_tensor::DType::C64) => {
337            let x = fft_operand::<Complex<f64>>(input, fft_op_name(spec.operation()))?;
338            transform!(x, Complex, F64, |v: Complex<f64>| v.re)
339        }
340        (FftOperation::C2r, tenferro_tensor::DType::C32) => {
341            let x = fft_operand::<Complex<f32>>(input, fft_op_name(spec.operation()))?;
342            transform!(x, Complex, F32, |v: Complex<f32>| v.re)
343        }
344        (operation, other) => Err(crate::tensor_unsupported_dtype(
345            fft_op_name(operation),
346            other,
347            expected_dtype_description(operation),
348        )),
349    }
350}
351
352fn output_shape(in_shape: &[usize], spec: &FftPlanSpec) -> tenferro_tensor::Result<Vec<usize>> {
353    let axis = spec.normalized_axis();
354    let n = spec.requested_len();
355    match spec.operation() {
356        FftOperation::C2cForward | FftOperation::C2cInverse => output_shape_c2c(in_shape, axis, n),
357        FftOperation::R2cFull | FftOperation::R2cOnesided => {
358            output_shape_r2c(in_shape, axis, n, spec.operation().is_onesided())
359        }
360        FftOperation::C2r => output_shape_c2r(in_shape, axis, n),
361    }
362}
363
364// INVARIANT: this scalar-dispatched helper carries the validated spec and the
365// existing execution resources; it does not define a second public descriptor.
366#[allow(clippy::too_many_arguments)]
367fn transform<I: TensorScalar, T: CachedFftPlanScalar + PoolScalar, O: PoolScalar>(
368    input: &TypedTensor<I>,
369    spec: &FftPlanSpec,
370    plans: &mut (impl FftPlanProvider + ?Sized),
371    buffers: &mut BufferPool,
372    domain: Option<&dyn SharedTensorAllocationDomain>,
373    wrap: for<'a> fn(&'a [I]) -> lanes::Input<'a, T>,
374    project: impl Fn(Complex<T>) -> O + Sync,
375    exec: tenferro_cpu::threadteam::Exec<'_>,
376) -> tenferro_tensor::Result<TypedTensor<O>>
377where
378    Complex<T>: PoolScalar,
379{
380    let produce = |read: &[I]| {
381        pooled_transform(
382            wrap(read),
383            input.shape(),
384            spec,
385            plans,
386            buffers,
387            &project,
388            exec,
389        )
390    };
391    if let Some(domain) = domain {
392        let op = fft_op_name(spec.operation());
393        // Validate the input domain before allocating or mapping the output.
394        with_managed_read(input, domain.id(), op, |read| {
395            let output = domain.allocate(O::dtype(), &output_shape(input.shape(), spec)?)?;
396            let mut output = O::into_typed(output).map_err(|_| {
397                tenferro_tensor::Error::runtime_state(
398                    op,
399                    "shared allocation owner returned an output with the wrong dtype",
400                )
401            })?;
402            if output.allocation_domain() != Some(domain.id()) {
403                return Err(tenferro_tensor::Error::runtime_state(
404                    op,
405                    "shared allocation owner returned an output outside its domain",
406                ));
407            }
408            if output.placement().memory_kind != MemoryKind::Managed {
409                return Err(tenferro_tensor::Error::runtime_state(
410                    op,
411                    "shared allocation owner returned a non-managed output",
412                ));
413            }
414            // INVARIANT: BackendStorage::map_write exposes only a full-buffer
415            // copy callback, not a writable span. Preserve that explicit provider
416            // boundary; the host staging allocation is recycled, not recreated.
417            let staging = produce(read)?;
418            if let Some(buffer) = output.backend_buffer_mut() {
419                buffer
420                    .map_write()
421                    .map_err(|source| tenferro_tensor::Error::host_access(op, source))?
422                    .copy_from_slice(staging.host_data()?)
423                    .map_err(|source| tenferro_tensor::Error::host_access(op, source))?;
424            } else {
425                output.with_host_write(|write| {
426                    if write.len() != staging.host_data()?.len() {
427                        return Err(tenferro_tensor::Error::runtime_state(
428                            op,
429                            "shared allocation owner returned an output with the wrong length",
430                        ));
431                    }
432                    write.copy_from_slice(staging.host_data()?);
433                    Ok(())
434                })??;
435            }
436            Ok(output)
437        })
438    } else {
439        produce(input.host_data()?)
440    }
441}
442
443fn pooled_transform<T: CachedFftPlanScalar + PoolScalar, O: PoolScalar>(
444    input: lanes::Input<'_, T>,
445    in_shape: &[usize],
446    spec: &FftPlanSpec,
447    plans: &mut (impl FftPlanProvider + ?Sized),
448    buffers: &mut BufferPool,
449    project: impl Fn(Complex<T>) -> O + Sync,
450    exec: tenferro_cpu::threadteam::Exec<'_>,
451) -> tenferro_tensor::Result<TypedTensor<O>>
452where
453    Complex<T>: PoolScalar,
454{
455    let shape = output_shape(in_shape, spec)?;
456    let axis = spec.normalized_axis();
457    let out_axis_len = shape[axis];
458    let fft_len = if spec.operation() == FftOperation::C2r {
459        validate_c2r_spectrum_len(in_shape[axis], out_axis_len)?;
460        out_axis_len
461    } else {
462        transform_len(in_shape, axis, spec.requested_len())?
463    };
464    let mut output = PooledUninitOutput::<O>::new(buffers, shape)?;
465    // SAFETY: Some(input) selects the out-of-place path with borrowed reads and
466    // a disjoint fresh destination. Successful execution joins all writers and
467    // initializes every element before the owning handoff.
468    unsafe {
469        if matches!(
470            spec.operation(),
471            FftOperation::R2cFull | FftOperation::R2cOnesided | FftOperation::C2r
472        ) {
473            lanes::execute_real(
474                Some(input),
475                in_shape,
476                axis,
477                fft_len,
478                out_axis_len,
479                spec.operation(),
480                spec.norm(),
481                plans,
482                buffers,
483                output.as_uninit_slice_mut(),
484                project,
485                exec,
486            )?;
487        } else {
488            lanes::execute(
489                Some(input),
490                in_shape,
491                axis,
492                fft_len,
493                out_axis_len,
494                spec.operation(),
495                spec.norm(),
496                plans,
497                buffers,
498                output.as_uninit_slice_mut(),
499                project,
500                exec,
501            )?;
502        }
503        output.assume_init_recycled()
504    }
505}
506
507fn with_managed_read<T: TensorScalar, R>(
508    input: &TypedTensor<T>,
509    expected_domain: AllocationDomainId,
510    op: &'static str,
511    execute: impl FnOnce(&[T]) -> tenferro_tensor::Result<R>,
512) -> tenferro_tensor::Result<R> {
513    if input.placement().memory_kind != MemoryKind::Managed {
514        return Err(tenferro_tensor::Error::host_access(
515            op,
516            HostAccessError::Unsupported {
517                backend: if matches!(
518                    input.placement().memory_kind,
519                    MemoryKind::PinnedHost | MemoryKind::UnpinnedHost
520                ) {
521                    "host"
522                } else {
523                    "backend"
524                },
525            },
526        ));
527    }
528    match input.allocation_domain() {
529        Some(actual) if actual == expected_domain => {}
530        Some(actual) => {
531            return Err(tenferro_tensor::Error::host_access(
532                op,
533                HostAccessError::ForeignDomain {
534                    expected: expected_domain,
535                    actual,
536                },
537            ))
538        }
539        None => {
540            return Err(tenferro_tensor::Error::host_access(
541                op,
542                HostAccessError::Unsupported { backend: "backend" },
543            ))
544        }
545    }
546    if let Some(buffer) = input.backend_buffer() {
547        let read = buffer
548            .map_read()
549            .map_err(|source| tenferro_tensor::Error::host_access(op, source))?;
550        execute(&read)
551    } else {
552        input.with_host_read(execute)?
553    }
554}
555
556pub(crate) fn validate_host_fft_input(
557    op: &'static str,
558    input: &Tensor,
559) -> tenferro_tensor::Result<()> {
560    validate_host_fft_placement(op, input.placement(), input.is_backend_buffer())
561}
562
563pub(crate) fn validate_host_fft_read_input(
564    op: &'static str,
565    input: &TensorRead<'_>,
566) -> tenferro_tensor::Result<()> {
567    match input {
568        TensorRead::Tensor(tensor) => validate_host_fft_input(op, tensor),
569        TensorRead::View(view) => {
570            macro_rules! validate {
571                ($v:expr) => {
572                    validate_host_fft_placement(op, $v.placement(), $v.backend_buffer().is_some())
573                };
574            }
575            match view {
576                TensorView::F32(v) => validate!(v),
577                TensorView::F64(v) => validate!(v),
578                TensorView::I32(v) => validate!(v),
579                TensorView::I64(v) => validate!(v),
580                TensorView::Bool(v) => validate!(v),
581                TensorView::C32(v) => validate!(v),
582                TensorView::C64(v) => validate!(v),
583            }
584        }
585    }
586}
587
588fn validate_host_fft_placement(
589    op: &'static str,
590    placement: &Placement,
591    is_backend_buffer: bool,
592) -> tenferro_tensor::Result<()> {
593    let is_device = matches!(placement.memory_kind, MemoryKind::Device);
594    if !is_device && !is_backend_buffer {
595        return Ok(());
596    }
597    let location = match placement.device.as_ref().map(|device| &device.kind) {
598        Some(DeviceKind::Gpu(kind)) => format!("GPU backend {kind:?}"),
599        Some(kind) => format!("device kind {kind:?}"),
600        None if is_device => "device tensor without device metadata".to_string(),
601        None => "backend buffer".to_string(),
602    };
603    Err(tenferro_tensor::Error::unsupported(op, format!(
604        "tenferro-fft CpuBackend supports host tensors only; unsupported {location} input; download the tensor to CPU before FFT")))
605}
606
607fn scale_for<T: Float + FromPrimitive>(
608    norm: FftNorm,
609    forward: bool,
610    n: usize,
611) -> tenferro_tensor::Result<T> {
612    let len = T::from_usize(n).ok_or_else(|| {
613        tenferro_tensor::Error::invalid_argument(
614            "tenferro_fft::scale_for",
615            "FFT length",
616            format!("{n} cannot be represented as scalar"),
617        )
618    })?;
619    Ok(match (norm, forward) {
620        (FftNorm::Backward, true) | (FftNorm::Forward, false) => T::one(),
621        (FftNorm::Backward, false) | (FftNorm::Forward, true) => T::one() / len,
622        (FftNorm::Ortho, _) => T::one() / len.sqrt(),
623    })
624}
625
626#[derive(Debug)]
627pub(crate) struct LaneLayout {
628    stride: usize,
629    in_block: usize,
630    out_block: usize,
631    lanes: usize,
632    input_len: usize,
633    output_len: usize,
634}
635impl LaneLayout {
636    pub(crate) fn new(
637        in_shape: &[usize],
638        axis: usize,
639        out_axis_len: usize,
640    ) -> tenferro_tensor::Result<Self> {
641        let stride = checked_shape_product("fft", "axis stride", &in_shape[..axis])?;
642        let outer = checked_shape_product("fft", "outer lanes", &in_shape[axis + 1..])?;
643        let in_block = checked_mul("fft", "input block", stride, in_shape[axis])?;
644        let out_block = checked_mul("fft", "output block", stride, out_axis_len)?;
645        Ok(Self {
646            stride,
647            in_block,
648            out_block,
649            lanes: checked_mul("fft", "lane count", outer, stride)?,
650            input_len: checked_mul("fft", "input coverage", outer, in_block)?,
651            output_len: checked_mul("fft", "output coverage", outer, out_block)?,
652        })
653    }
654}
655
656pub(crate) fn checked_shape_product(
657    op: &'static str,
658    role: &'static str,
659    shape: &[usize],
660) -> tenferro_tensor::Result<usize> {
661    shape
662        .iter()
663        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
664        .ok_or_else(|| {
665            tenferro_tensor::Error::invalid_argument(
666                op,
667                "shape product",
668                format!("{role} shape product overflows usize"),
669            )
670        })
671}
672fn checked_mul(
673    op: &'static str,
674    role: &'static str,
675    lhs: usize,
676    rhs: usize,
677) -> tenferro_tensor::Result<usize> {
678    lhs.checked_mul(rhs).ok_or_else(|| {
679        tenferro_tensor::Error::invalid_argument(
680            op,
681            "arithmetic",
682            format!("{role} overflows usize"),
683        )
684    })
685}
686
687#[cfg(test)]
688#[path = "cpu/managed_tests.rs"]
689mod managed_tests;