1macro_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
142fn 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#[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 input.with_host_write(|values| {
234 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#[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 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 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 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;