1use std::any::Any;
52use std::collections::HashMap;
53use std::hash::Hasher;
54use std::mem::MaybeUninit;
55use std::sync::{Arc, Mutex, OnceLock};
56
57#[cfg(feature = "autodiff")]
58use computegraph::types::{LocalValueId, OperationRole, ValueKey, ValueRef};
59use num_complex::Complex;
60use num_traits::{Float, FromPrimitive, Zero};
61use rustfft::{Fft, FftNum, FftPlanner};
62#[cfg(feature = "autodiff")]
63use tenferro_ad::extension::{
64 ExtensionLinearTransposeRule, ExtensionLinearizeRule, ExtensionPrimalVjpRule,
65 ExtensionRegistryError, ExtensionRuleSet,
66};
67use tenferro_extension_macros::define_extension_runtime;
68#[cfg(feature = "autodiff")]
69use tenferro_ops::ad::{transpose_input::TransposeInputRef, PrimitiveRuleBuilder};
70#[cfg(feature = "autodiff")]
71use tenferro_ops::std_tensor_op::StdTensorOp;
72#[cfg(feature = "autodiff")]
73use tenferro_ops::ShapeGuardContext;
74use tenferro_ops::SymDim;
75use tenferro_runtime::extension::{apply, ExtensionExecutionContext, ExtensionOp, HostReference};
76use tenferro_runtime::{Error, Result, TracedTensor};
77use tenferro_tensor::{
78 DType, DeviceKind, MemoryKind, Placement, Tensor, TensorBackend, TensorRead, TypedTensor,
79};
80#[cfg(feature = "autodiff")]
81use tidu::{ADRuleError, ADRuleKind, ADRuleResult};
82
83pub const FFT_EXTENSION_FAMILY_ID: &str = "tenferro-fft.fft.v1";
94
95pub trait TracedTensorFftExt {
97 fn fft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
98 fn ifft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
99 fn rfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
100 fn irfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
101}
102
103impl TracedTensorFftExt for TracedTensor {
104 fn fft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
105 fft(self, n, axis, norm)
106 }
107
108 fn ifft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
109 ifft(self, n, axis, norm)
110 }
111
112 fn rfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
113 rfft(self, n, axis, norm)
114 }
115
116 fn irfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
117 irfft(self, n, axis, norm)
118 }
119}
120
121pub trait TensorFftExt {
145 fn fft<B: TensorBackend>(
147 &self,
148 n: Option<usize>,
149 axis: isize,
150 norm: FftNorm,
151 backend: &mut B,
152 ) -> tenferro_tensor::Result<Tensor>;
153
154 fn ifft<B: TensorBackend>(
156 &self,
157 n: Option<usize>,
158 axis: isize,
159 norm: FftNorm,
160 backend: &mut B,
161 ) -> tenferro_tensor::Result<Tensor>;
162
163 fn rfft<B: TensorBackend>(
165 &self,
166 n: Option<usize>,
167 axis: isize,
168 norm: FftNorm,
169 backend: &mut B,
170 ) -> tenferro_tensor::Result<Tensor>;
171
172 fn irfft<B: TensorBackend>(
174 &self,
175 n: Option<usize>,
176 axis: isize,
177 norm: FftNorm,
178 backend: &mut B,
179 ) -> tenferro_tensor::Result<Tensor>;
180}
181
182impl TensorFftExt for Tensor {
183 fn fft<B: TensorBackend>(
184 &self,
185 n: Option<usize>,
186 axis: isize,
187 norm: FftNorm,
188 backend: &mut B,
189 ) -> tenferro_tensor::Result<Tensor> {
190 let op = concrete_fft_op(
191 "TensorFftExt::fft",
192 concrete_fft_kind("TensorFftExt::fft", self.dtype())?,
193 self.shape(),
194 n,
195 axis,
196 norm,
197 )?;
198 execute_concrete_fft_op(self, &op, backend)
199 }
200
201 fn ifft<B: TensorBackend>(
202 &self,
203 n: Option<usize>,
204 axis: isize,
205 norm: FftNorm,
206 backend: &mut B,
207 ) -> tenferro_tensor::Result<Tensor> {
208 let op = concrete_fft_op(
209 "TensorFftExt::ifft",
210 concrete_ifft_kind("TensorFftExt::ifft", self.dtype())?,
211 self.shape(),
212 n,
213 axis,
214 norm,
215 )?;
216 execute_concrete_fft_op(self, &op, backend)
217 }
218
219 fn rfft<B: TensorBackend>(
220 &self,
221 n: Option<usize>,
222 axis: isize,
223 norm: FftNorm,
224 backend: &mut B,
225 ) -> tenferro_tensor::Result<Tensor> {
226 let op = concrete_fft_op(
227 "TensorFftExt::rfft",
228 concrete_rfft_kind("TensorFftExt::rfft", self.dtype())?,
229 self.shape(),
230 n,
231 axis,
232 norm,
233 )?;
234 execute_concrete_fft_op(self, &op, backend)
235 }
236
237 fn irfft<B: TensorBackend>(
238 &self,
239 n: Option<usize>,
240 axis: isize,
241 norm: FftNorm,
242 backend: &mut B,
243 ) -> tenferro_tensor::Result<Tensor> {
244 let op = concrete_fft_op(
245 "TensorFftExt::irfft",
246 concrete_irfft_kind("TensorFftExt::irfft", self.dtype())?,
247 self.shape(),
248 n,
249 axis,
250 norm,
251 )?;
252 execute_concrete_fft_op(self, &op, backend)
253 }
254}
255
256pub trait TensorReadFftExt {
279 fn fft_read<B: TensorBackend>(
281 &self,
282 n: Option<usize>,
283 axis: isize,
284 norm: FftNorm,
285 backend: &mut B,
286 ) -> tenferro_tensor::Result<Tensor>;
287
288 fn ifft_read<B: TensorBackend>(
290 &self,
291 n: Option<usize>,
292 axis: isize,
293 norm: FftNorm,
294 backend: &mut B,
295 ) -> tenferro_tensor::Result<Tensor>;
296
297 fn rfft_read<B: TensorBackend>(
299 &self,
300 n: Option<usize>,
301 axis: isize,
302 norm: FftNorm,
303 backend: &mut B,
304 ) -> tenferro_tensor::Result<Tensor>;
305
306 fn irfft_read<B: TensorBackend>(
308 &self,
309 n: Option<usize>,
310 axis: isize,
311 norm: FftNorm,
312 backend: &mut B,
313 ) -> tenferro_tensor::Result<Tensor>;
314}
315
316impl TensorReadFftExt for TensorRead<'_> {
317 fn fft_read<B: TensorBackend>(
318 &self,
319 n: Option<usize>,
320 axis: isize,
321 norm: FftNorm,
322 backend: &mut B,
323 ) -> tenferro_tensor::Result<Tensor> {
324 execute_concrete_fft_read_op(
325 self,
326 concrete_fft_kind("TensorReadFftExt::fft_read", self.dtype())?,
327 "TensorReadFftExt::fft_read",
328 n,
329 axis,
330 norm,
331 backend,
332 )
333 }
334
335 fn ifft_read<B: TensorBackend>(
336 &self,
337 n: Option<usize>,
338 axis: isize,
339 norm: FftNorm,
340 backend: &mut B,
341 ) -> tenferro_tensor::Result<Tensor> {
342 execute_concrete_fft_read_op(
343 self,
344 concrete_ifft_kind("TensorReadFftExt::ifft_read", self.dtype())?,
345 "TensorReadFftExt::ifft_read",
346 n,
347 axis,
348 norm,
349 backend,
350 )
351 }
352
353 fn rfft_read<B: TensorBackend>(
354 &self,
355 n: Option<usize>,
356 axis: isize,
357 norm: FftNorm,
358 backend: &mut B,
359 ) -> tenferro_tensor::Result<Tensor> {
360 execute_concrete_fft_read_op(
361 self,
362 concrete_rfft_kind("TensorReadFftExt::rfft_read", self.dtype())?,
363 "TensorReadFftExt::rfft_read",
364 n,
365 axis,
366 norm,
367 backend,
368 )
369 }
370
371 fn irfft_read<B: TensorBackend>(
372 &self,
373 n: Option<usize>,
374 axis: isize,
375 norm: FftNorm,
376 backend: &mut B,
377 ) -> tenferro_tensor::Result<Tensor> {
378 execute_concrete_fft_read_op(
379 self,
380 concrete_irfft_kind("TensorReadFftExt::irfft_read", self.dtype())?,
381 "TensorReadFftExt::irfft_read",
382 n,
383 axis,
384 norm,
385 backend,
386 )
387 }
388}
389
390#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
403pub enum FftNorm {
404 #[default]
406 Backward,
407 Forward,
409 Ortho,
411}
412
413#[cfg(feature = "autodiff")]
414impl FftNorm {
415 fn c2c_adjoint(self) -> Self {
416 match self {
417 Self::Backward => Self::Forward,
418 Self::Forward => Self::Backward,
419 Self::Ortho => Self::Ortho,
420 }
421 }
422}
423
424#[derive(Clone, Copy, Debug, Eq, PartialEq)]
425enum FftKind {
426 C2C { forward: bool },
427 R2C { onesided: bool },
428 C2R,
429}
430
431#[derive(Clone, Debug, PartialEq)]
432struct FftOp {
433 kind: FftKind,
434 axis: usize,
435 n: Option<usize>,
436 norm: FftNorm,
437}
438
439impl FftOp {
440 fn new(kind: FftKind, axis: usize, n: Option<usize>, norm: FftNorm) -> Self {
441 Self {
442 kind,
443 axis,
444 n,
445 norm,
446 }
447 }
448
449 #[cfg(feature = "autodiff")]
450 fn c2c_adjoint(&self) -> Option<Self> {
451 match self.kind {
452 FftKind::C2C { forward } => Some(Self {
453 kind: FftKind::C2C { forward: !forward },
454 axis: self.axis,
455 n: self.n,
456 norm: self.norm.c2c_adjoint(),
457 }),
458 FftKind::R2C { .. } | FftKind::C2R => None,
459 }
460 }
461}
462
463impl ExtensionOp for FftOp {
464 fn family_id(&self) -> &'static str {
465 FFT_EXTENSION_FAMILY_ID
466 }
467
468 fn payload_hash(&self, hasher: &mut dyn Hasher) {
469 let kind = match self.kind {
470 FftKind::C2C { forward: true } => 0,
471 FftKind::C2C { forward: false } => 1,
472 FftKind::R2C { onesided: true } => 2,
473 FftKind::R2C { onesided: false } => 3,
474 FftKind::C2R => 4,
475 };
476 hasher.write_u8(kind);
477 hasher.write_usize(self.axis);
478 match self.n {
479 Some(n) => {
480 hasher.write_u8(1);
481 hasher.write_usize(n);
482 }
483 None => hasher.write_u8(0),
484 }
485 let norm = match self.norm {
486 FftNorm::Backward => 0,
487 FftNorm::Forward => 1,
488 FftNorm::Ortho => 2,
489 };
490 hasher.write_u8(norm);
491 }
492
493 fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
494 other
495 .as_any()
496 .downcast_ref::<FftOp>()
497 .is_some_and(|that| self == that)
498 }
499
500 fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
501 Arc::new(self.clone())
502 }
503
504 fn as_any(&self) -> &dyn Any {
505 self
506 }
507
508 fn input_count(&self) -> usize {
509 1
510 }
511
512 fn output_count(&self) -> usize {
513 1
514 }
515
516 fn infer_output_meta(
517 &self,
518 input_dtypes: &[DType],
519 input_shapes: &[&[SymDim]],
520 ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
521 let [input_dtype] = input_dtypes else {
522 return Err(tenferro_tensor::Error::InvalidConfig {
523 op: "tenferro-fft",
524 message: format!("expected 1 input dtype, got {}", input_dtypes.len()),
525 });
526 };
527 let [input_shape] = input_shapes else {
528 return Err(tenferro_tensor::Error::InvalidConfig {
529 op: "tenferro-fft",
530 message: format!("expected 1 input shape, got {}", input_shapes.len()),
531 });
532 };
533 if self.axis >= input_shape.len() {
534 return Err(tenferro_tensor::Error::AxisOutOfBounds {
535 op: "tenferro-fft",
536 axis: self.axis,
537 rank: input_shape.len(),
538 });
539 }
540
541 let mut out_shape = input_shape.to_vec();
542 let output_dtype = match self.kind {
543 FftKind::C2C { .. } => {
544 if !matches!(input_dtype, DType::C32 | DType::C64) {
545 return Err(tenferro_tensor::Error::backend_failure(
546 "tenferro-fft",
547 format!("unsupported dtype {input_dtype:?} for complex FFT"),
548 ));
549 }
550 *input_dtype
551 }
552 FftKind::R2C { onesided } => {
553 let len = transform_len_dim(self.n, &input_shape[self.axis]);
554 out_shape[self.axis] = if onesided { len / 2usize + 1usize } else { len };
555 match input_dtype {
556 DType::F32 => DType::C32,
557 DType::F64 => DType::C64,
558 _ => {
559 return Err(tenferro_tensor::Error::backend_failure(
560 "tenferro-fft",
561 format!("unsupported dtype {input_dtype:?} for real FFT"),
562 ));
563 }
564 }
565 }
566 FftKind::C2R => {
567 out_shape[self.axis] = output_dim_c2r(&input_shape[self.axis], self.n)?;
568 match input_dtype {
569 DType::C32 => DType::F32,
570 DType::C64 => DType::F64,
571 _ => {
572 return Err(tenferro_tensor::Error::backend_failure(
573 "tenferro-fft",
574 format!("unsupported dtype {input_dtype:?} for inverse real FFT"),
575 ));
576 }
577 }
578 }
579 };
580
581 if matches!(self.kind, FftKind::C2C { .. }) {
582 out_shape[self.axis] = transform_len_dim(self.n, &input_shape[self.axis]);
583 }
584
585 Ok(vec![(output_dtype, out_shape)])
586 }
587
588 fn host_reference(&self) -> Option<&dyn HostReference> {
589 Some(self)
590 }
591}
592
593impl HostReference for FftOp {
594 fn execute(&self, inputs: &[&Tensor]) -> tenferro_tensor::Result<Vec<Tensor>> {
595 execute_host_fft_op(self, inputs)
596 }
597}
598
599fn execute_host_fft_op(op: &FftOp, inputs: &[&Tensor]) -> tenferro_tensor::Result<Vec<Tensor>> {
600 if inputs.len() != 1 {
601 return Err(tenferro_tensor::Error::InvalidConfig {
602 op: "tenferro-fft",
603 message: format!("expected 1 input, got {}", inputs.len()),
604 });
605 }
606 validate_host_fft_input(fft_op_name(op.kind), inputs[0])?;
607
608 let output = match (op.kind, inputs[0]) {
609 (FftKind::C2C { forward }, Tensor::C64(input)) => {
610 Tensor::C64(TypedTensor::from_vec_col_major(
611 output_shape_c2c(input.shape(), op.axis, op.n)?,
612 execute_c2c(input, op.axis, op.n, forward, op.norm)?,
613 )?)
614 }
615 (FftKind::C2C { forward }, Tensor::C32(input)) => {
616 Tensor::C32(TypedTensor::from_vec_col_major(
617 output_shape_c2c(input.shape(), op.axis, op.n)?,
618 execute_c2c(input, op.axis, op.n, forward, op.norm)?,
619 )?)
620 }
621 (FftKind::R2C { onesided }, Tensor::F64(input)) => {
622 Tensor::C64(TypedTensor::from_vec_col_major(
623 output_shape_r2c(input.shape(), op.axis, op.n, onesided)?,
624 execute_r2c(input, op.axis, op.n, onesided, op.norm)?,
625 )?)
626 }
627 (FftKind::R2C { onesided }, Tensor::F32(input)) => {
628 Tensor::C32(TypedTensor::from_vec_col_major(
629 output_shape_r2c(input.shape(), op.axis, op.n, onesided)?,
630 execute_r2c(input, op.axis, op.n, onesided, op.norm)?,
631 )?)
632 }
633 (FftKind::C2R, Tensor::C64(input)) => Tensor::F64(TypedTensor::from_vec_col_major(
634 output_shape_c2r(input.shape(), op.axis, op.n)?,
635 execute_c2r(input, op.axis, op.n, op.norm)?,
636 )?),
637 (FftKind::C2R, Tensor::C32(input)) => Tensor::F32(TypedTensor::from_vec_col_major(
638 output_shape_c2r(input.shape(), op.axis, op.n)?,
639 execute_c2r(input, op.axis, op.n, op.norm)?,
640 )?),
641 (kind, other) => {
642 return Err(tenferro_tensor::Error::DTypeMismatch {
643 op: match kind {
644 FftKind::C2C { .. } => "fft",
645 FftKind::R2C { .. } => "rfft",
646 FftKind::C2R => "irfft",
647 },
648 lhs: expected_dtype_for(kind),
649 rhs: other.dtype(),
650 });
651 }
652 };
653 Ok(vec![output])
654}
655
656fn execute_concrete_fft_op<B: TensorBackend>(
657 input: &Tensor,
658 op: &FftOp,
659 backend: &mut B,
660) -> tenferro_tensor::Result<Tensor> {
661 backend.with_backend_session(|_exec| single_fft_output(execute_host_fft_op(op, &[input])?))
662}
663
664#[allow(clippy::too_many_arguments)]
665fn execute_concrete_fft_read_op<B: TensorBackend>(
666 input: &TensorRead<'_>,
667 kind: FftKind,
668 op_name: &'static str,
669 n: Option<usize>,
670 axis: isize,
671 norm: FftNorm,
672 backend: &mut B,
673) -> tenferro_tensor::Result<Tensor> {
674 let op = concrete_fft_op(op_name, kind, input.shape(), n, axis, norm)?;
675 let materialized = input.to_tensor()?;
676 execute_concrete_fft_op(&materialized, &op, backend)
677}
678
679fn single_fft_output(mut outputs: Vec<Tensor>) -> tenferro_tensor::Result<Tensor> {
680 if outputs.len() != 1 {
681 return Err(tenferro_tensor::Error::InvalidConfig {
682 op: "tenferro-fft",
683 message: format!("expected 1 FFT output, got {}", outputs.len()),
684 });
685 }
686 Ok(outputs.remove(0))
687}
688
689fn concrete_fft_op(
690 op: &'static str,
691 kind: FftKind,
692 input_shape: &[usize],
693 n: Option<usize>,
694 axis: isize,
695 norm: FftNorm,
696) -> tenferro_tensor::Result<FftOp> {
697 validate_concrete_n(op, n)?;
698 let axis = normalize_concrete_axis(op, axis, input_shape.len())?;
699 validate_concrete_transform_len(op, input_shape, n, axis)?;
700 if matches!(kind, FftKind::C2R) {
701 output_shape_c2r(input_shape, axis, n)?;
702 }
703 Ok(FftOp::new(kind, axis, n, norm))
704}
705
706fn concrete_fft_kind(op: &'static str, dtype: DType) -> tenferro_tensor::Result<FftKind> {
707 match dtype {
708 DType::C32 | DType::C64 => Ok(FftKind::C2C { forward: true }),
709 DType::F32 | DType::F64 => Ok(FftKind::R2C { onesided: false }),
710 DType::I32 | DType::I64 | DType::Bool => Err(tensor_fft_config_error(
711 op,
712 format!("fft expects real or complex floating input, got {dtype:?}"),
713 )),
714 }
715}
716
717fn concrete_ifft_kind(op: &'static str, dtype: DType) -> tenferro_tensor::Result<FftKind> {
718 match dtype {
719 DType::C32 | DType::C64 => Ok(FftKind::C2C { forward: false }),
720 DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool => Err(
721 tensor_fft_config_error(op, format!("ifft expects C32 or C64 input; got {dtype:?}")),
722 ),
723 }
724}
725
726fn concrete_rfft_kind(op: &'static str, dtype: DType) -> tenferro_tensor::Result<FftKind> {
727 match dtype {
728 DType::F32 | DType::F64 => Ok(FftKind::R2C { onesided: true }),
729 DType::C32 | DType::C64 | DType::I32 | DType::I64 | DType::Bool => Err(
730 tensor_fft_config_error(op, format!("rfft expects F32 or F64 input; got {dtype:?}")),
731 ),
732 }
733}
734
735fn concrete_irfft_kind(op: &'static str, dtype: DType) -> tenferro_tensor::Result<FftKind> {
736 match dtype {
737 DType::C32 | DType::C64 => Ok(FftKind::C2R),
738 DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool => Err(
739 tensor_fft_config_error(op, format!("irfft expects C32 or C64 input; got {dtype:?}")),
740 ),
741 }
742}
743
744fn validate_concrete_n(op: &'static str, n: Option<usize>) -> tenferro_tensor::Result<()> {
745 if n == Some(0) {
746 return Err(tensor_fft_config_error(
747 op,
748 "tenferro-fft transform length n must be positive",
749 ));
750 }
751 Ok(())
752}
753
754fn validate_concrete_transform_len(
755 op: &'static str,
756 input_shape: &[usize],
757 n: Option<usize>,
758 axis: usize,
759) -> tenferro_tensor::Result<()> {
760 if n.is_none() && input_shape.get(axis).copied() == Some(0) {
761 return Err(tensor_fft_config_error(
762 op,
763 "tenferro-fft transform length n must be positive",
764 ));
765 }
766 Ok(())
767}
768
769fn normalize_concrete_axis(
770 op: &'static str,
771 axis: isize,
772 rank: usize,
773) -> tenferro_tensor::Result<usize> {
774 if rank == 0 {
775 return Err(tensor_fft_config_error(
776 op,
777 "tenferro-fft requires rank >= 1",
778 ));
779 }
780 let normalized = if axis >= 0 {
781 axis as usize
782 } else {
783 rank.checked_sub(axis.unsigned_abs()).ok_or_else(|| {
784 tensor_fft_config_error(
785 op,
786 format!("tenferro-fft axis {axis} out of bounds for rank {rank}"),
787 )
788 })?
789 };
790 if normalized >= rank {
791 return Err(tensor_fft_config_error(
792 op,
793 format!("tenferro-fft axis {axis} out of bounds for rank {rank}"),
794 ));
795 }
796 Ok(normalized)
797}
798
799fn tensor_fft_config_error(
800 op: &'static str,
801 message: impl std::fmt::Display,
802) -> tenferro_tensor::Error {
803 tenferro_tensor::Error::InvalidConfig {
804 op,
805 message: message.to_string(),
806 }
807}
808
809fn tensor_placement(input: &Tensor) -> &Placement {
810 input.placement()
811}
812
813fn tensor_has_backend_buffer(input: &Tensor) -> bool {
814 input.is_backend_buffer()
815}
816
817fn validate_host_fft_input(op: &'static str, input: &Tensor) -> tenferro_tensor::Result<()> {
818 let placement = tensor_placement(input);
819 let is_device = matches!(placement.memory_kind, MemoryKind::Device);
820 if !is_device && !tensor_has_backend_buffer(input) {
821 return Ok(());
822 }
823
824 let location = match placement.device.as_ref().map(|device| &device.kind) {
825 Some(DeviceKind::Gpu(kind)) => format!("GPU backend {kind:?}"),
826 Some(kind) => format!("device kind {kind:?}"),
827 None if is_device => "device tensor without device metadata".to_string(),
828 None => "backend buffer".to_string(),
829 };
830 Err(tenferro_tensor::Error::backend_failure(
831 op,
832 format!(
833 "tenferro-fft supports host tensors only; unsupported {location} input; \
834 download the tensor to CPU before FFT"
835 ),
836 ))
837}
838
839#[cfg(feature = "autodiff")]
840#[derive(Debug)]
841struct FftAdRule;
842
843#[cfg(feature = "autodiff")]
844impl ExtensionLinearizeRule for FftAdRule {
845 fn family_id(&self) -> &'static str {
846 FFT_EXTENSION_FAMILY_ID
847 }
848
849 fn linearize(
850 &self,
851 op: &dyn ExtensionOp,
852 builder: &mut dyn PrimitiveRuleBuilder,
853 _primal_in: &[ValueKey<StdTensorOp>],
854 _primal_out: &[ValueKey<StdTensorOp>],
855 tangent_in: &[Option<LocalValueId>],
856 _ctx: &mut ShapeGuardContext,
857 ) -> ADRuleResult<Vec<Option<LocalValueId>>> {
858 let fft_op = fft_payload(op, ADRuleKind::Jvp)?;
859 if !matches!(fft_op.kind, FftKind::C2C { .. }) {
860 return Err(ADRuleError::unsupported(
861 fft_ad_family_id(fft_op.kind),
862 ADRuleKind::Jvp,
863 ));
864 }
865
866 match tangent_in[0] {
867 Some(dx) => {
868 let outputs = builder.add_operation(
869 StdTensorOp::Extension(Arc::new(fft_op.clone())),
870 vec![ValueRef::Local(dx)],
871 OperationRole::Linearized {
872 active_mask: vec![true],
873 },
874 );
875 Ok(vec![Some(outputs[0])])
876 }
877 None => Ok(vec![None]),
878 }
879 }
880}
881
882#[cfg(feature = "autodiff")]
883impl ExtensionLinearTransposeRule for FftAdRule {
884 fn family_id(&self) -> &'static str {
885 FFT_EXTENSION_FAMILY_ID
886 }
887
888 fn linear_transpose(
889 &self,
890 op: &dyn ExtensionOp,
891 builder: &mut dyn PrimitiveRuleBuilder,
892 cotangent_out: &[Option<LocalValueId>],
893 inputs: &[tidu::PrimitiveTransposeInput<StdTensorOp>],
894 active_mask: &[bool],
895 ctx: &mut ShapeGuardContext,
896 ) -> ADRuleResult<Vec<Option<LocalValueId>>> {
897 let inputs: Vec<_> = inputs.iter().map(TransposeInputRef::new).collect();
898 transpose_fft_adjoint_from_transpose_inputs(
899 op,
900 builder,
901 cotangent_out,
902 &inputs,
903 active_mask,
904 ctx,
905 )
906 }
907}
908
909#[cfg(feature = "autodiff")]
910impl ExtensionPrimalVjpRule for FftAdRule {
911 fn family_id(&self) -> &'static str {
912 FFT_EXTENSION_FAMILY_ID
913 }
914
915 fn primal_vjp(
916 &self,
917 op: &dyn ExtensionOp,
918 builder: &mut dyn PrimitiveRuleBuilder,
919 cotangent_out: &[Option<LocalValueId>],
920 inputs: &[ValueRef<StdTensorOp>],
921 ctx: &mut ShapeGuardContext,
922 ) -> ADRuleResult<Vec<Option<LocalValueId>>> {
923 transpose_fft_adjoint(op, builder, cotangent_out, inputs, None, ctx)
924 }
925}
926
927#[cfg(feature = "autodiff")]
928fn transpose_fft_adjoint(
929 op: &dyn ExtensionOp,
930 builder: &mut dyn PrimitiveRuleBuilder,
931 cotangent_out: &[Option<LocalValueId>],
932 inputs: &[ValueRef<StdTensorOp>],
933 active_mask: Option<&[bool]>,
934 ctx: &mut ShapeGuardContext,
935) -> ADRuleResult<Vec<Option<LocalValueId>>> {
936 let Some((adjoint, fft_op)) = emit_c2c_adjoint(op, builder, cotangent_out, active_mask)? else {
937 return Ok(vec![None]);
938 };
939 let restored = restore_c2c_adjoint_input_length(builder, adjoint, inputs, fft_op, ctx)?;
940 Ok(vec![Some(restored)])
941}
942
943#[cfg(feature = "autodiff")]
944fn transpose_fft_adjoint_from_transpose_inputs(
945 op: &dyn ExtensionOp,
946 builder: &mut dyn PrimitiveRuleBuilder,
947 cotangent_out: &[Option<LocalValueId>],
948 inputs: &[TransposeInputRef<'_>],
949 active_mask: &[bool],
950 ctx: &mut ShapeGuardContext,
951) -> ADRuleResult<Vec<Option<LocalValueId>>> {
952 let Some((adjoint, fft_op)) = emit_c2c_adjoint(op, builder, cotangent_out, Some(active_mask))?
953 else {
954 return Ok(vec![None]);
955 };
956 let restored = restore_c2c_adjoint_input_length_from_transpose_input(
957 builder, adjoint, inputs, fft_op, ctx,
958 )?;
959 Ok(vec![Some(restored)])
960}
961
962#[cfg(feature = "autodiff")]
963fn emit_c2c_adjoint<'a>(
964 op: &'a dyn ExtensionOp,
965 builder: &mut dyn PrimitiveRuleBuilder,
966 cotangent_out: &[Option<LocalValueId>],
967 active_mask: Option<&[bool]>,
968) -> ADRuleResult<Option<(LocalValueId, &'a FftOp)>> {
969 let fft_op = fft_payload(op, ADRuleKind::Transpose)?;
970 if !matches!(fft_op.kind, FftKind::C2C { .. }) {
971 return Err(ADRuleError::unsupported(
972 fft_ad_family_id(fft_op.kind),
973 ADRuleKind::Transpose,
974 ));
975 }
976 if active_mask.is_some_and(|mask| !mask.first().copied().unwrap_or(false)) {
977 return Ok(None);
978 }
979
980 let Some(ct) = cotangent_out.first().copied().flatten() else {
981 return Ok(None);
982 };
983 let adjoint_op = fft_op
984 .c2c_adjoint()
985 .ok_or_else(|| ADRuleError::unsupported(FFT_EXTENSION_FAMILY_ID, ADRuleKind::Transpose))?;
986 let outputs = builder.add_operation(
987 StdTensorOp::Extension(Arc::new(adjoint_op)),
988 vec![ValueRef::Local(ct)],
989 OperationRole::Linearized {
990 active_mask: vec![true],
991 },
992 );
993 Ok(Some((outputs[0], fft_op)))
994}
995
996#[cfg(feature = "autodiff")]
997fn restore_c2c_adjoint_input_length(
998 builder: &mut dyn PrimitiveRuleBuilder,
999 adjoint: LocalValueId,
1000 inputs: &[ValueRef<StdTensorOp>],
1001 fft_op: &FftOp,
1002 ctx: &mut ShapeGuardContext,
1003) -> ADRuleResult<LocalValueId> {
1004 let Some(transform_len) = fft_op.n else {
1005 return Ok(adjoint);
1006 };
1007 let Some(input) = inputs.first() else {
1008 return Err(ADRuleError::invalid_input(
1009 FFT_EXTENSION_FAMILY_ID,
1010 ADRuleKind::Transpose,
1011 "FFT transpose rule expected one primal input",
1012 ));
1013 };
1014 if ctx
1015 .shape_of(input)
1016 .ok()
1017 .and_then(|shape| shape.get(fft_op.axis).and_then(SymDim::constant_value))
1018 == Some(transform_len)
1019 {
1020 return Ok(adjoint);
1021 }
1022
1023 let size = builder.add_operation(
1024 StdTensorOp::ShapeOf { axis: fft_op.axis },
1025 vec![input.clone()],
1026 OperationRole::Linearized {
1027 active_mask: vec![false],
1028 },
1029 )[0];
1030 let truncated = builder.add_operation(
1031 StdTensorOp::DynamicTruncate { axis: fft_op.axis },
1032 vec![ValueRef::Local(adjoint), ValueRef::Local(size)],
1033 OperationRole::Linearized {
1034 active_mask: vec![true, false],
1035 },
1036 )[0];
1037 let padded = builder.add_operation(
1038 StdTensorOp::PadToMatch { axis: fft_op.axis },
1039 vec![ValueRef::Local(truncated), input.clone()],
1040 OperationRole::Linearized {
1041 active_mask: vec![true, false],
1042 },
1043 )[0];
1044 Ok(padded)
1045}
1046
1047#[cfg(feature = "autodiff")]
1048fn restore_c2c_adjoint_input_length_from_transpose_input(
1049 builder: &mut dyn PrimitiveRuleBuilder,
1050 adjoint: LocalValueId,
1051 inputs: &[TransposeInputRef<'_>],
1052 fft_op: &FftOp,
1053 ctx: &mut ShapeGuardContext,
1054) -> ADRuleResult<LocalValueId> {
1055 let Some(transform_len) = fft_op.n else {
1056 return Ok(adjoint);
1057 };
1058 let Some(input) = inputs.first() else {
1059 return Err(ADRuleError::invalid_input(
1060 FFT_EXTENSION_FAMILY_ID,
1061 ADRuleKind::Transpose,
1062 "FFT transpose rule expected one primal input",
1063 ));
1064 };
1065 let metadata = input.metadata_value();
1066 if ctx
1067 .shape_of(&metadata)
1068 .ok()
1069 .and_then(|shape| shape.get(fft_op.axis).and_then(SymDim::constant_value))
1070 == Some(transform_len)
1071 {
1072 return Ok(adjoint);
1073 }
1074
1075 let shape_source = input.shape_source_value(FFT_EXTENSION_FAMILY_ID, 0)?;
1076 let size = builder.add_operation(
1077 StdTensorOp::ShapeOf { axis: fft_op.axis },
1078 vec![shape_source.clone()],
1079 OperationRole::Linearized {
1080 active_mask: vec![false],
1081 },
1082 )[0];
1083 let truncated = builder.add_operation(
1084 StdTensorOp::DynamicTruncate { axis: fft_op.axis },
1085 vec![ValueRef::Local(adjoint), ValueRef::Local(size)],
1086 OperationRole::Linearized {
1087 active_mask: vec![true, false],
1088 },
1089 )[0];
1090 let padded = builder.add_operation(
1091 StdTensorOp::PadToMatch { axis: fft_op.axis },
1092 vec![ValueRef::Local(truncated), shape_source],
1093 OperationRole::Linearized {
1094 active_mask: vec![true, false],
1095 },
1096 )[0];
1097 Ok(padded)
1098}
1099
1100#[cfg(feature = "autodiff")]
1102pub fn ad_rules() -> std::result::Result<ExtensionRuleSet, ExtensionRegistryError> {
1103 ExtensionRuleSet::new()
1104 .with_linearize(Arc::new(FftAdRule))?
1105 .with_linear_transpose(Arc::new(FftAdRule))?
1106 .with_primal_vjp(Arc::new(FftAdRule))
1107}
1108
1109fn execute_fft_extension<B: TensorBackend + 'static>(
1110 op: &FftOp,
1111 inputs: &[&Tensor],
1112 _ctx: &mut ExtensionExecutionContext<'_, B>,
1113) -> tenferro_tensor::Result<Vec<Tensor>> {
1114 execute_host_fft_op(op, inputs)
1115}
1116
1117fn execute_fft_extension_reads<B: TensorBackend + 'static>(
1118 op: &FftOp,
1119 inputs: &[TensorRead<'_>],
1120 ctx: &mut ExtensionExecutionContext<'_, B>,
1121) -> tenferro_tensor::Result<Vec<Tensor>> {
1122 let _ = ctx;
1123 let materialized_inputs: Vec<Tensor> = inputs
1126 .iter()
1127 .map(TensorRead::to_tensor)
1128 .collect::<tenferro_tensor::Result<_>>()?;
1129 let input_refs: Vec<&Tensor> = materialized_inputs.iter().collect();
1130 execute_host_fft_op(op, &input_refs)
1131}
1132
1133define_extension_runtime! {
1134 runtime = FftRuntime,
1135 family_id = FFT_EXTENSION_FAMILY_ID,
1136 op_type = FftOp,
1137 execute = execute_fft_extension,
1138 execute_reads = execute_fft_extension_reads,
1139 register_fn = register_runtime,
1140}
1141
1142fn fft(input: &TracedTensor, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
1166 let kind = match input.dtype {
1167 DType::C32 | DType::C64 => FftKind::C2C { forward: true },
1168 DType::F32 | DType::F64 => FftKind::R2C { onesided: false },
1169 DType::I32 | DType::I64 | DType::Bool => {
1170 return Err(fft_config_error(
1171 "fft",
1172 format!(
1173 "fft expects real or complex floating input, got {:?}",
1174 input.dtype
1175 ),
1176 ))
1177 }
1178 };
1179 apply_unary_fft("fft", input, kind, n, axis, norm)
1180}
1181
1182fn ifft(
1203 input: &TracedTensor,
1204 n: Option<usize>,
1205 axis: isize,
1206 norm: FftNorm,
1207) -> Result<TracedTensor> {
1208 if !matches!(input.dtype, DType::C32 | DType::C64) {
1209 return Err(fft_config_error(
1210 "ifft",
1211 format!("ifft expects C32 or C64 input; got {:?}", input.dtype),
1212 ));
1213 }
1214 apply_unary_fft(
1215 "ifft",
1216 input,
1217 FftKind::C2C { forward: false },
1218 n,
1219 axis,
1220 norm,
1221 )
1222}
1223
1224fn rfft(
1249 input: &TracedTensor,
1250 n: Option<usize>,
1251 axis: isize,
1252 norm: FftNorm,
1253) -> Result<TracedTensor> {
1254 if !matches!(input.dtype, DType::F32 | DType::F64) {
1255 return Err(fft_config_error(
1256 "rfft",
1257 format!("rfft expects F32 or F64 input; got {:?}", input.dtype),
1258 ));
1259 }
1260 apply_unary_fft(
1261 "rfft",
1262 input,
1263 FftKind::R2C { onesided: true },
1264 n,
1265 axis,
1266 norm,
1267 )
1268}
1269
1270fn irfft(
1298 input: &TracedTensor,
1299 n: Option<usize>,
1300 axis: isize,
1301 norm: FftNorm,
1302) -> Result<TracedTensor> {
1303 if !matches!(input.dtype, DType::C32 | DType::C64) {
1304 return Err(fft_config_error(
1305 "irfft",
1306 format!("irfft expects C32 or C64 input; got {:?}", input.dtype),
1307 ));
1308 }
1309 apply_unary_fft("irfft", input, FftKind::C2R, n, axis, norm)
1310}
1311
1312fn apply_unary_fft(
1313 op_name: &'static str,
1314 input: &TracedTensor,
1315 kind: FftKind,
1316 n: Option<usize>,
1317 axis: isize,
1318 norm: FftNorm,
1319) -> Result<TracedTensor> {
1320 validate_n(op_name, n)?;
1321 let axis = normalize_axis(op_name, axis, input.rank)?;
1322 validate_resolved_transform_len(op_name, input, n, axis)?;
1323 if matches!(kind, FftKind::C2R) {
1324 if let Some(shape) = input.try_concrete_shape() {
1325 output_shape_c2r(&shape, axis, n)?;
1326 }
1327 }
1328 let op = Arc::new(FftOp::new(kind, axis, n, norm));
1329 let mut outputs = apply(op, &[input])?;
1330 outputs
1331 .pop()
1332 .ok_or_else(|| Error::Internal("FFT extension declares exactly one output".into()))
1333}
1334
1335fn normalize_axis(op: &'static str, axis: isize, rank: usize) -> Result<usize> {
1336 if rank == 0 {
1337 return Err(fft_config_error(op, "tenferro-fft requires rank >= 1"));
1338 }
1339 let normalized = if axis >= 0 {
1340 axis as usize
1341 } else {
1342 rank.checked_sub(axis.unsigned_abs()).ok_or_else(|| {
1343 fft_config_error(
1344 op,
1345 format!("tenferro-fft axis {axis} out of bounds for rank {rank}"),
1346 )
1347 })?
1348 };
1349 if normalized >= rank {
1350 return Err(fft_config_error(
1351 op,
1352 format!("tenferro-fft axis {axis} out of bounds for rank {rank}"),
1353 ));
1354 }
1355 Ok(normalized)
1356}
1357
1358fn validate_n(op: &'static str, n: Option<usize>) -> Result<()> {
1359 if n == Some(0) {
1360 return Err(fft_config_error(
1361 op,
1362 "tenferro-fft transform length n must be positive",
1363 ));
1364 }
1365 Ok(())
1366}
1367
1368fn validate_resolved_transform_len(
1369 op: &'static str,
1370 input: &TracedTensor,
1371 n: Option<usize>,
1372 axis: usize,
1373) -> Result<()> {
1374 if n.is_some() {
1375 return Ok(());
1376 }
1377 if input
1378 .try_concrete_shape()
1379 .and_then(|shape| shape.get(axis).copied())
1380 == Some(0)
1381 {
1382 return Err(fft_config_error(
1383 op,
1384 "tenferro-fft transform length n must be positive",
1385 ));
1386 }
1387 Ok(())
1388}
1389
1390fn fft_config_error(op: &'static str, message: impl std::fmt::Display) -> Error {
1391 Error::TensorRuntime(tenferro_tensor::Error::InvalidConfig {
1392 op,
1393 message: message.to_string(),
1394 })
1395}
1396
1397fn transform_len_dim(n: Option<usize>, input_dim: &SymDim) -> SymDim {
1398 n.map(SymDim::from).unwrap_or_else(|| input_dim.clone())
1399}
1400
1401fn expected_dtype_for(kind: FftKind) -> DType {
1402 match kind {
1403 FftKind::C2C { .. } | FftKind::C2R => DType::C64,
1404 FftKind::R2C { .. } => DType::F64,
1405 }
1406}
1407
1408fn fft_op_name(kind: FftKind) -> &'static str {
1409 match kind {
1410 FftKind::C2C { forward: true } => "fft",
1411 FftKind::C2C { forward: false } => "ifft",
1412 FftKind::R2C { .. } => "rfft",
1413 FftKind::C2R => "irfft",
1414 }
1415}
1416
1417#[cfg(feature = "autodiff")]
1418fn fft_ad_family_id(kind: FftKind) -> &'static str {
1419 match kind {
1420 FftKind::C2C { .. } => FFT_EXTENSION_FAMILY_ID,
1421 FftKind::R2C { .. } => "tenferro-fft.rfft.v1",
1422 FftKind::C2R => "tenferro-fft.irfft.v1",
1423 }
1424}
1425
1426#[cfg(feature = "autodiff")]
1427fn fft_payload<'a>(op: &'a dyn ExtensionOp, rule: ADRuleKind) -> ADRuleResult<&'a FftOp> {
1428 op.as_any()
1429 .downcast_ref::<FftOp>()
1430 .ok_or_else(|| ADRuleError::unsupported(FFT_EXTENSION_FAMILY_ID, rule))
1431}
1432
1433fn output_shape_c2c(
1434 shape: &[usize],
1435 axis: usize,
1436 n: Option<usize>,
1437) -> tenferro_tensor::Result<Vec<usize>> {
1438 let len = transform_len(shape, axis, n)?;
1439 let mut out_shape = shape.to_vec();
1440 out_shape[axis] = len;
1441 Ok(out_shape)
1442}
1443
1444fn output_shape_r2c(
1445 shape: &[usize],
1446 axis: usize,
1447 n: Option<usize>,
1448 onesided: bool,
1449) -> tenferro_tensor::Result<Vec<usize>> {
1450 let len = transform_len(shape, axis, n)?;
1451 let mut out_shape = shape.to_vec();
1452 out_shape[axis] = if onesided { len / 2 + 1 } else { len };
1453 Ok(out_shape)
1454}
1455
1456fn output_shape_c2r(
1457 shape: &[usize],
1458 axis: usize,
1459 n: Option<usize>,
1460) -> tenferro_tensor::Result<Vec<usize>> {
1461 validate_axis("irfft", shape, axis)?;
1462 let input_len = shape[axis];
1463 let len = match n {
1464 Some(len) => len,
1465 None => default_c2r_output_len(input_len)?,
1466 };
1467 if len == 0 {
1468 return Err(tenferro_tensor::Error::InvalidConfig {
1469 op: "irfft",
1470 message: "output length must be positive".to_string(),
1471 });
1472 }
1473 validate_c2r_spectrum_len(input_len, len)?;
1474 let mut out_shape = shape.to_vec();
1475 out_shape[axis] = len;
1476 Ok(out_shape)
1477}
1478
1479fn output_dim_c2r(input_dim: &SymDim, n: Option<usize>) -> tenferro_tensor::Result<SymDim> {
1480 match (input_dim.constant_value(), n) {
1481 (Some(input_len), Some(output_len)) => {
1482 if output_len == 0 {
1483 return Err(tenferro_tensor::Error::InvalidConfig {
1484 op: "irfft",
1485 message: "output length must be positive".to_string(),
1486 });
1487 }
1488 validate_c2r_spectrum_len(input_len, output_len)?;
1489 Ok(SymDim::from(output_len))
1490 }
1491 (Some(input_len), None) => Ok(SymDim::from(default_c2r_output_len(input_len)?)),
1492 (None, Some(output_len)) => {
1493 if output_len == 0 {
1494 return Err(tenferro_tensor::Error::InvalidConfig {
1495 op: "irfft",
1496 message: "output length must be positive".to_string(),
1497 });
1498 }
1499 Ok(SymDim::from(output_len))
1500 }
1501 (None, None) => Ok((input_dim.clone() - 1usize) * 2usize),
1502 }
1503}
1504
1505fn default_c2r_output_len(input_len: usize) -> tenferro_tensor::Result<usize> {
1506 if input_len == 0 {
1507 return Err(tenferro_tensor::Error::InvalidConfig {
1508 op: "irfft",
1509 message: "input spectrum axis length must be positive".to_string(),
1510 });
1511 }
1512 input_len
1513 .checked_sub(1)
1514 .and_then(|len| len.checked_mul(2))
1515 .ok_or_else(|| tenferro_tensor::Error::InvalidConfig {
1516 op: "irfft",
1517 message: "default output length overflows usize".to_string(),
1518 })
1519}
1520
1521fn validate_c2r_spectrum_len(
1522 input_len: usize,
1523 output_len: usize,
1524) -> tenferro_tensor::Result<usize> {
1525 let expected = output_len / 2 + 1;
1526 if input_len != expected {
1527 return Err(tenferro_tensor::Error::InvalidConfig {
1528 op: "irfft",
1529 message: format!(
1530 "one-sided spectrum axis length mismatch: expected {expected} for output length {output_len}, got {input_len}"
1531 ),
1532 });
1533 }
1534 Ok(expected)
1535}
1536
1537fn transform_len(shape: &[usize], axis: usize, n: Option<usize>) -> tenferro_tensor::Result<usize> {
1538 validate_axis("fft", shape, axis)?;
1539 let len = n.unwrap_or(shape[axis]);
1540 if len == 0 {
1541 return Err(tenferro_tensor::Error::InvalidConfig {
1542 op: "fft",
1543 message: "transform length must be positive".to_string(),
1544 });
1545 }
1546 Ok(len)
1547}
1548
1549fn validate_axis(op: &'static str, shape: &[usize], axis: usize) -> tenferro_tensor::Result<()> {
1550 if axis >= shape.len() {
1551 return Err(tenferro_tensor::Error::AxisOutOfBounds {
1552 op,
1553 axis,
1554 rank: shape.len(),
1555 });
1556 }
1557 Ok(())
1558}
1559
1560fn checked_shape_product(
1561 op: &'static str,
1562 role: &'static str,
1563 shape: &[usize],
1564) -> tenferro_tensor::Result<usize> {
1565 shape
1566 .iter()
1567 .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
1568 .ok_or_else(|| tenferro_tensor::Error::InvalidConfig {
1569 op,
1570 message: format!("{role} shape product overflows usize"),
1571 })
1572}
1573
1574fn checked_mul(
1575 op: &'static str,
1576 role: &'static str,
1577 lhs: usize,
1578 rhs: usize,
1579) -> tenferro_tensor::Result<usize> {
1580 lhs.checked_mul(rhs)
1581 .ok_or_else(|| tenferro_tensor::Error::InvalidConfig {
1582 op,
1583 message: format!("{role} overflows usize"),
1584 })
1585}
1586
1587fn checked_add(
1588 op: &'static str,
1589 role: &'static str,
1590 lhs: usize,
1591 rhs: usize,
1592) -> tenferro_tensor::Result<usize> {
1593 lhs.checked_add(rhs)
1594 .ok_or_else(|| tenferro_tensor::Error::InvalidConfig {
1595 op,
1596 message: format!("{role} overflows usize"),
1597 })
1598}
1599
1600fn uninit_output_vec<T>(len: usize) -> Vec<MaybeUninit<T>> {
1601 let mut output = Vec::with_capacity(len);
1602 unsafe { output.set_len(len) };
1605 output
1606}
1607
1608unsafe fn assume_init_output_vec<T>(mut output: Vec<MaybeUninit<T>>) -> Vec<T> {
1609 let len = output.len();
1610 let capacity = output.capacity();
1611 let ptr = output.as_mut_ptr().cast::<T>();
1612 std::mem::forget(output);
1613 unsafe { Vec::from_raw_parts(ptr, len, capacity) }
1616}
1617
1618#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1619struct FftPlanKey {
1620 len: usize,
1621 forward: bool,
1622}
1623
1624trait CachedFftPlanScalar: FftNum + Float + FromPrimitive + 'static {
1625 fn cached_fft_plan(len: usize, forward: bool) -> tenferro_tensor::Result<Arc<dyn Fft<Self>>>;
1626}
1627
1628type FftPlanStore<T> = HashMap<FftPlanKey, Arc<dyn Fft<T>>>;
1629type FftPlanCache<T> = OnceLock<Mutex<FftPlanStore<T>>>;
1630
1631static F32_FFT_PLAN_CACHE: FftPlanCache<f32> = OnceLock::new();
1632static F64_FFT_PLAN_CACHE: FftPlanCache<f64> = OnceLock::new();
1633
1634impl CachedFftPlanScalar for f32 {
1635 fn cached_fft_plan(len: usize, forward: bool) -> tenferro_tensor::Result<Arc<dyn Fft<Self>>> {
1636 cached_fft_plan_from_cache(&F32_FFT_PLAN_CACHE, len, forward)
1637 }
1638}
1639
1640impl CachedFftPlanScalar for f64 {
1641 fn cached_fft_plan(len: usize, forward: bool) -> tenferro_tensor::Result<Arc<dyn Fft<Self>>> {
1642 cached_fft_plan_from_cache(&F64_FFT_PLAN_CACHE, len, forward)
1643 }
1644}
1645
1646fn cached_fft_plan<T: CachedFftPlanScalar>(
1647 len: usize,
1648 forward: bool,
1649) -> tenferro_tensor::Result<Arc<dyn Fft<T>>> {
1650 T::cached_fft_plan(len, forward)
1651}
1652
1653fn cached_fft_plan_from_cache<T: FftNum + 'static>(
1654 cache: &'static FftPlanCache<T>,
1655 len: usize,
1656 forward: bool,
1657) -> tenferro_tensor::Result<Arc<dyn Fft<T>>> {
1658 let key = FftPlanKey { len, forward };
1659 let mut guard = cache
1660 .get_or_init(|| Mutex::new(HashMap::new()))
1661 .lock()
1662 .map_err(|_| {
1663 tenferro_tensor::Error::backend_failure(
1664 "fft_plan_cache",
1665 "FFT plan cache lock is poisoned",
1666 )
1667 })?;
1668 if let Some(plan) = guard.get(&key) {
1669 return Ok(Arc::clone(plan));
1670 }
1671
1672 let mut planner = FftPlanner::<T>::new();
1673 let plan = if forward {
1674 planner.plan_fft_forward(len)
1675 } else {
1676 planner.plan_fft_inverse(len)
1677 };
1678 guard.insert(key, Arc::clone(&plan));
1679 Ok(plan)
1680}
1681
1682fn execute_c2c<T>(
1683 input: &TypedTensor<Complex<T>>,
1684 axis: usize,
1685 n: Option<usize>,
1686 forward: bool,
1687 norm: FftNorm,
1688) -> tenferro_tensor::Result<Vec<Complex<T>>>
1689where
1690 T: CachedFftPlanScalar,
1691{
1692 let in_shape = input.shape();
1693 let fft_len = transform_len(in_shape, axis, n)?;
1694 let out_shape = output_shape_c2c(in_shape, axis, n)?;
1695 let out_axis_len = out_shape[axis];
1696 let input_data = input.host_data()?;
1697 let output_len = checked_shape_product("fft", "output", &out_shape)?;
1698 let mut output = uninit_output_vec(output_len);
1699 let fft_plan = cached_fft_plan::<T>(fft_len, forward)?;
1701 let scale: T = scale_for(norm, forward, fft_len)?;
1702 let mut lane = vec![Complex::zero(); fft_len];
1703
1704 for_axis_lane(in_shape, axis, out_axis_len, |lane_ctx| {
1705 lane.fill(Complex::zero());
1708 let copy_len = lane_ctx.in_axis_len.min(fft_len);
1709 for (slot, offset) in lane
1710 .iter_mut()
1711 .take(copy_len)
1712 .zip(lane_ctx.input_offsets(copy_len))
1713 {
1714 *slot = input_data[offset];
1715 }
1716 fft_plan.process(&mut lane);
1717 if scale != T::one() {
1718 for value in &mut lane {
1719 *value = *value * scale;
1720 }
1721 }
1722 for (value, offset) in lane
1723 .iter()
1724 .take(out_axis_len)
1725 .copied()
1726 .zip(lane_ctx.output_offsets(out_axis_len))
1727 {
1728 output[offset].write(value);
1729 }
1730 Ok(())
1731 })?;
1732
1733 Ok(unsafe { assume_init_output_vec(output) })
1736}
1737
1738fn execute_r2c<T>(
1739 input: &TypedTensor<T>,
1740 axis: usize,
1741 n: Option<usize>,
1742 onesided: bool,
1743 norm: FftNorm,
1744) -> tenferro_tensor::Result<Vec<Complex<T>>>
1745where
1746 T: CachedFftPlanScalar,
1747{
1748 let in_shape = input.shape();
1749 let fft_len = transform_len(in_shape, axis, n)?;
1750 let out_shape = output_shape_r2c(in_shape, axis, n, onesided)?;
1751 let out_axis_len = out_shape[axis];
1752 let input_data = input.host_data()?;
1753 let output_len = checked_shape_product("rfft", "output", &out_shape)?;
1754 let mut output = uninit_output_vec(output_len);
1755 let fft_plan = cached_fft_plan::<T>(fft_len, true)?;
1757 let scale: T = scale_for(norm, true, fft_len)?;
1758 let mut lane = vec![Complex::zero(); fft_len];
1759
1760 for_axis_lane(in_shape, axis, out_axis_len, |lane_ctx| {
1761 lane.fill(Complex::zero());
1764 let copy_len = lane_ctx.in_axis_len.min(fft_len);
1765 for (slot, offset) in lane
1766 .iter_mut()
1767 .take(copy_len)
1768 .zip(lane_ctx.input_offsets(copy_len))
1769 {
1770 *slot = Complex::new(input_data[offset], T::zero());
1771 }
1772 fft_plan.process(&mut lane);
1773 if scale != T::one() {
1774 for value in &mut lane {
1775 *value = *value * scale;
1776 }
1777 }
1778 for (value, offset) in lane
1779 .iter()
1780 .take(out_axis_len)
1781 .copied()
1782 .zip(lane_ctx.output_offsets(out_axis_len))
1783 {
1784 output[offset].write(value);
1785 }
1786 Ok(())
1787 })?;
1788
1789 Ok(unsafe { assume_init_output_vec(output) })
1792}
1793
1794fn execute_c2r<T>(
1795 input: &TypedTensor<Complex<T>>,
1796 axis: usize,
1797 n: Option<usize>,
1798 norm: FftNorm,
1799) -> tenferro_tensor::Result<Vec<T>>
1800where
1801 T: CachedFftPlanScalar,
1802{
1803 let in_shape = input.shape();
1804 let out_shape = output_shape_c2r(in_shape, axis, n)?;
1805 let out_axis_len = out_shape[axis];
1806 let expected_half = validate_c2r_spectrum_len(in_shape[axis], out_axis_len)?;
1807 let input_data = input.host_data()?;
1808 let output_len = checked_shape_product("irfft", "output", &out_shape)?;
1809 let mut output = uninit_output_vec(output_len);
1810 let fft_plan = cached_fft_plan::<T>(out_axis_len, false)?;
1812 let scale: T = scale_for(norm, false, out_axis_len)?;
1813 let mut lane = vec![Complex::zero(); out_axis_len];
1814
1815 for_axis_lane(in_shape, axis, out_axis_len, |lane_ctx| {
1816 lane.fill(Complex::zero());
1819 for (slot, offset) in lane
1820 .iter_mut()
1821 .take(expected_half)
1822 .zip(lane_ctx.input_offsets(expected_half))
1823 {
1824 *slot = input_data[offset];
1825 }
1826 for k in expected_half..out_axis_len {
1827 let mirror = out_axis_len - k;
1828 if mirror < lane.len() {
1829 lane[k] = lane[mirror].conj();
1830 }
1831 }
1832 fft_plan.process(&mut lane);
1833 for (value, offset) in lane
1834 .iter()
1835 .take(out_axis_len)
1836 .zip(lane_ctx.output_offsets(out_axis_len))
1837 {
1838 output[offset].write(value.re * scale);
1839 }
1840 Ok(())
1841 })?;
1842
1843 Ok(unsafe { assume_init_output_vec(output) })
1846}
1847
1848fn scale_for<T>(norm: FftNorm, forward: bool, n: usize) -> tenferro_tensor::Result<T>
1849where
1850 T: Float + FromPrimitive,
1851{
1852 let len = T::from_usize(n).ok_or_else(|| tenferro_tensor::Error::InvalidConfig {
1853 op: "tenferro_fft::scale_for",
1854 message: format!("FFT length {n} cannot be represented as scalar"),
1855 })?;
1856 Ok(match (norm, forward) {
1857 (FftNorm::Backward, true) | (FftNorm::Forward, false) => T::one(),
1858 (FftNorm::Backward, false) | (FftNorm::Forward, true) => T::one() / len,
1859 (FftNorm::Ortho, _) => T::one() / len.sqrt(),
1860 })
1861}
1862
1863#[derive(Clone, Copy)]
1864struct LaneContext {
1865 input_base: usize,
1866 output_base: usize,
1867 axis_stride: usize,
1868 in_axis_len: usize,
1869 out_axis_len: usize,
1870}
1871
1872impl LaneContext {
1873 fn input_offsets(self, count: usize) -> impl Iterator<Item = usize> {
1874 debug_assert!(count <= self.in_axis_len);
1875 lane_offsets(self.input_base, self.axis_stride, count)
1876 }
1877
1878 fn output_offsets(self, count: usize) -> impl Iterator<Item = usize> {
1879 debug_assert!(count <= self.out_axis_len);
1880 lane_offsets(self.output_base, self.axis_stride, count)
1881 }
1882}
1883
1884fn lane_offsets(base: usize, stride: usize, count: usize) -> impl Iterator<Item = usize> {
1885 (0..count).map(move |k| base + k * stride)
1889}
1890
1891fn for_axis_lane(
1892 in_shape: &[usize],
1893 axis: usize,
1894 out_axis_len: usize,
1895 mut f: impl FnMut(LaneContext) -> tenferro_tensor::Result<()>,
1896) -> tenferro_tensor::Result<()> {
1897 let in_axis_len = in_shape[axis];
1898 let axis_stride = checked_shape_product("fft", "axis stride", &in_shape[..axis])?;
1899 let outer = checked_shape_product("fft", "outer lane count", &in_shape[axis + 1..])?;
1900 let in_block = checked_mul("fft", "input lane block", axis_stride, in_axis_len)?;
1901 let out_block = checked_mul("fft", "output lane block", axis_stride, out_axis_len)?;
1902 let _input_len = checked_mul("fft", "input lane coverage", outer, in_block)?;
1903 let _output_len = checked_mul("fft", "output lane coverage", outer, out_block)?;
1904
1905 for outer_idx in 0..outer {
1909 let in_outer_base = checked_mul("fft", "input outer base", outer_idx, in_block)?;
1910 let out_outer_base = checked_mul("fft", "output outer base", outer_idx, out_block)?;
1911 for inner in 0..axis_stride {
1912 let input_base = checked_add("fft", "input lane base", in_outer_base, inner)?;
1913 let output_base = checked_add("fft", "output lane base", out_outer_base, inner)?;
1914 f(LaneContext {
1915 input_base,
1916 output_base,
1917 axis_stride,
1918 in_axis_len,
1919 out_axis_len,
1920 })?;
1921 }
1922 }
1923 Ok(())
1924}
1925
1926#[cfg(test)]
1927mod concrete_tests;
1928
1929#[cfg(test)]
1930mod tests {
1931 use super::*;
1932
1933 #[test]
1934 fn fft_infer_output_meta_rejects_invalid_trait_inputs_without_panicking() {
1935 let op = FftOp::new(FftKind::R2C { onesided: true }, 0, None, FftNorm::Backward);
1936 let shape = [SymDim::from(4usize)];
1937
1938 assert!(op.infer_output_meta(&[], &[&shape]).is_err());
1939 assert!(op.infer_output_meta(&[DType::F64], &[]).is_err());
1940 assert!(op.infer_output_meta(&[DType::I64], &[&shape]).is_err());
1941
1942 let bad_axis = FftOp::new(FftKind::C2C { forward: true }, 2, None, FftNorm::Backward);
1943 assert!(bad_axis
1944 .infer_output_meta(&[DType::C64], &[&shape])
1945 .is_err());
1946 }
1947
1948 #[test]
1949 fn checked_shape_product_rejects_overflow_before_allocation() {
1950 let err = checked_shape_product("fft", "output", &[usize::MAX, 2])
1951 .expect_err("overflowing output shape should be rejected");
1952
1953 assert!(err.to_string().contains("overflows usize"), "{err}");
1954 }
1955
1956 #[test]
1957 fn irfft_default_output_length_rejects_overflow() {
1958 let err = output_shape_c2r(&[usize::MAX], 0, None)
1959 .expect_err("default irfft output length should reject overflow");
1960
1961 assert!(err.to_string().contains("overflows usize"), "{err}");
1962 }
1963
1964 #[test]
1965 fn normalize_axis_handles_large_rank_without_isize_cast_wrap() {
1966 assert_eq!(normalize_axis("fft", 0, usize::MAX).unwrap(), 0);
1967 assert_eq!(
1968 normalize_axis("fft", -1, usize::MAX).unwrap(),
1969 usize::MAX - 1
1970 );
1971 assert!(normalize_axis("fft", isize::MIN, 3).is_err());
1972 }
1973
1974 #[test]
1975 fn axis_lane_layout_rejects_stride_overflow() {
1976 let err = for_axis_lane(&[usize::MAX, 2], 1, 2, |_| Ok(()))
1977 .expect_err("lane layout should reject stride overflow");
1978
1979 assert!(err.to_string().contains("overflows usize"), "{err}");
1980 }
1981
1982 #[cfg(feature = "autodiff")]
1983 #[test]
1984 fn fft_transpose_rule_respects_inactive_linearized_input() {
1985 let rule = FftAdRule;
1986 let op = FftOp::new(FftKind::C2C { forward: true }, 0, None, FftNorm::Backward);
1987 let mut builder = computegraph::graph::GraphBuilder::<StdTensorOp>::new();
1988 let cotangent = builder.add_input(tenferro_ops::input_key::TensorInputKey::User { id: 0 });
1989 let result = rule
1990 .linear_transpose(
1991 &op,
1992 &mut builder,
1993 &[Some(cotangent)],
1994 &[],
1995 &[false],
1996 &mut ShapeGuardContext::default(),
1997 )
1998 .unwrap();
1999
2000 assert_eq!(result, vec![None]);
2001 assert!(builder.build().operations().is_empty());
2002 }
2003
2004 #[cfg(feature = "autodiff")]
2005 #[test]
2006 fn fft_transpose_rule_uses_metadata_for_linear_only_matching_length() {
2007 let rule = FftAdRule;
2008 let op = FftOp::new(
2009 FftKind::C2C { forward: true },
2010 0,
2011 Some(4),
2012 FftNorm::Backward,
2013 );
2014 let active_key = ValueKey::Input(tenferro_ops::input_key::TensorInputKey::User { id: 1 });
2015 let mut ctx = ShapeGuardContext::default();
2016 ctx.insert_metadata(
2017 active_key.clone(),
2018 tenferro_ops::TensorMeta::exact(DType::C64, vec![SymDim::from(4usize)]),
2019 );
2020
2021 let mut builder = computegraph::graph::GraphBuilder::<StdTensorOp>::new();
2022 let cotangent = builder.add_input(tenferro_ops::input_key::TensorInputKey::User { id: 0 });
2023 let result = rule
2024 .linear_transpose(
2025 &op,
2026 &mut builder,
2027 &[Some(cotangent)],
2028 &[tidu::PrimitiveTransposeInput::Linear {
2029 key: active_key.clone(),
2030 primal: None,
2031 }],
2032 &[true],
2033 &mut ctx,
2034 )
2035 .unwrap();
2036
2037 assert_eq!(result.len(), 1);
2038 assert!(result[0].is_some());
2039 let active_ref = ValueRef::External(active_key);
2040 let graph = builder.build();
2041 assert!(graph
2042 .operations()
2043 .iter()
2044 .all(|node| !node.inputs.iter().any(|input| input == &active_ref)));
2045 assert!(graph.operations().iter().all(|node| {
2046 !matches!(
2047 node.operation,
2048 StdTensorOp::ShapeOf { .. }
2049 | StdTensorOp::DynamicTruncate { .. }
2050 | StdTensorOp::PadToMatch { .. }
2051 )
2052 }));
2053 }
2054}