nvmath-python 0.9.0__cp314-cp314-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. nvmath/__init__.pxd +0 -0
  2. nvmath/__init__.py +45 -0
  3. nvmath/_internal/__init__.py +0 -0
  4. nvmath/_internal/attribute_ifc_factory.py +330 -0
  5. nvmath/_internal/layout.py +70 -0
  6. nvmath/_internal/templates.py +383 -0
  7. nvmath/_utils.py +150 -0
  8. nvmath/bindings/__init__.py +51 -0
  9. nvmath/bindings/_internal/__init__.pxd +0 -0
  10. nvmath/bindings/_internal/__init__.py +0 -0
  11. nvmath/bindings/_internal/cublas.cp314-win_amd64.pyd +0 -0
  12. nvmath/bindings/_internal/cublas.pxd +530 -0
  13. nvmath/bindings/_internal/cublasLt.cp314-win_amd64.pyd +0 -0
  14. nvmath/bindings/_internal/cublasLt.pxd +59 -0
  15. nvmath/bindings/_internal/cublasMp.pxd +52 -0
  16. nvmath/bindings/_internal/cudss.cp314-win_amd64.pyd +0 -0
  17. nvmath/bindings/_internal/cudss.pxd +47 -0
  18. nvmath/bindings/_internal/cufft.cp314-win_amd64.pyd +0 -0
  19. nvmath/bindings/_internal/cufft.pxd +70 -0
  20. nvmath/bindings/_internal/cufftMp.pxd +77 -0
  21. nvmath/bindings/_internal/curand.cp314-win_amd64.pyd +0 -0
  22. nvmath/bindings/_internal/curand.pxd +42 -0
  23. nvmath/bindings/_internal/cusolver.cp314-win_amd64.pyd +0 -0
  24. nvmath/bindings/_internal/cusolver.pxd +15 -0
  25. nvmath/bindings/_internal/cusolverDn.cp314-win_amd64.pyd +0 -0
  26. nvmath/bindings/_internal/cusolverDn.pxd +406 -0
  27. nvmath/bindings/_internal/cusparse.cp314-win_amd64.pyd +0 -0
  28. nvmath/bindings/_internal/cusparse.pxd +277 -0
  29. nvmath/bindings/_internal/cusparseLt.cp314-win_amd64.pyd +0 -0
  30. nvmath/bindings/_internal/cusparseLt.pxd +48 -0
  31. nvmath/bindings/_internal/cutensor.cp314-win_amd64.pyd +0 -0
  32. nvmath/bindings/_internal/cutensor.pxd +58 -0
  33. nvmath/bindings/_internal/mathdx.cp314-win_amd64.pyd +0 -0
  34. nvmath/bindings/_internal/mathdx.pxd +116 -0
  35. nvmath/bindings/_internal/nvshmem.pxd +29 -0
  36. nvmath/bindings/_internal/utils.cp314-win_amd64.pyd +0 -0
  37. nvmath/bindings/_internal/utils.pxd +179 -0
  38. nvmath/bindings/_internal/utils.pyi +10 -0
  39. nvmath/bindings/cublas.cp314-win_amd64.pyd +0 -0
  40. nvmath/bindings/cublas.pxd +558 -0
  41. nvmath/bindings/cublas.pyi +746 -0
  42. nvmath/bindings/cublasLt.cp314-win_amd64.pyd +0 -0
  43. nvmath/bindings/cublasLt.pxd +109 -0
  44. nvmath/bindings/cublasLt.pyi +1337 -0
  45. nvmath/bindings/cublasMp.pxd +85 -0
  46. nvmath/bindings/cublasMp.pyi +219 -0
  47. nvmath/bindings/cudss.cp314-win_amd64.pyd +0 -0
  48. nvmath/bindings/cudss.pxd +86 -0
  49. nvmath/bindings/cudss.pyi +282 -0
  50. nvmath/bindings/cufft.cp314-win_amd64.pyd +0 -0
  51. nvmath/bindings/cufft.pxd +118 -0
  52. nvmath/bindings/cufft.pyi +241 -0
  53. nvmath/bindings/cufftMp.pxd +124 -0
  54. nvmath/bindings/cufftMp.pyi +260 -0
  55. nvmath/bindings/curand.cp314-win_amd64.pyd +0 -0
  56. nvmath/bindings/curand.pxd +71 -0
  57. nvmath/bindings/curand.pyi +159 -0
  58. nvmath/bindings/cusolver.cp314-win_amd64.pyd +0 -0
  59. nvmath/bindings/cusolver.pxd +62 -0
  60. nvmath/bindings/cusolver.pyi +242 -0
  61. nvmath/bindings/cusolverDn.cp314-win_amd64.pyd +0 -0
  62. nvmath/bindings/cusolverDn.pxd +430 -0
  63. nvmath/bindings/cusolverDn.pyi +416 -0
  64. nvmath/bindings/cusparse.cp314-win_amd64.pyd +0 -0
  65. nvmath/bindings/cusparse.pxd +338 -0
  66. nvmath/bindings/cusparse.pyi +659 -0
  67. nvmath/bindings/cusparseLt.cp314-win_amd64.pyd +0 -0
  68. nvmath/bindings/cusparseLt.pxd +99 -0
  69. nvmath/bindings/cusparseLt.pyi +198 -0
  70. nvmath/bindings/cutensor.cp314-win_amd64.pyd +0 -0
  71. nvmath/bindings/cutensor.pxd +98 -0
  72. nvmath/bindings/cutensor.pyi +294 -0
  73. nvmath/bindings/cycublas.cp314-win_amd64.pyd +0 -0
  74. nvmath/bindings/cycublas.pxd +684 -0
  75. nvmath/bindings/cycublasLt.cp314-win_amd64.pyd +0 -0
  76. nvmath/bindings/cycublasLt.pxd +1055 -0
  77. nvmath/bindings/cycublasMp.pxd +183 -0
  78. nvmath/bindings/cycudss.cp314-win_amd64.pyd +0 -0
  79. nvmath/bindings/cycudss.pxd +224 -0
  80. nvmath/bindings/cycufft.cp314-win_amd64.pyd +0 -0
  81. nvmath/bindings/cycufft.pxd +326 -0
  82. nvmath/bindings/cycufftMp.pxd +334 -0
  83. nvmath/bindings/cycurand.cp314-win_amd64.pyd +0 -0
  84. nvmath/bindings/cycurand.pxd +146 -0
  85. nvmath/bindings/cycusolver.cp314-win_amd64.pyd +0 -0
  86. nvmath/bindings/cycusolver.pxd +154 -0
  87. nvmath/bindings/cycusolverDn.cp314-win_amd64.pyd +0 -0
  88. nvmath/bindings/cycusolverDn.pxd +446 -0
  89. nvmath/bindings/cycusparse.cp314-win_amd64.pyd +0 -0
  90. nvmath/bindings/cycusparse.pxd +470 -0
  91. nvmath/bindings/cycusparseLt.cp314-win_amd64.pyd +0 -0
  92. nvmath/bindings/cycusparseLt.pxd +150 -0
  93. nvmath/bindings/cycutensor.cp314-win_amd64.pyd +0 -0
  94. nvmath/bindings/cycutensor.pxd +192 -0
  95. nvmath/bindings/cymathdx.cp314-win_amd64.pyd +0 -0
  96. nvmath/bindings/cymathdx.pxd +552 -0
  97. nvmath/bindings/cynvshmem.pxd +126 -0
  98. nvmath/bindings/mathdx.cp314-win_amd64.pyd +0 -0
  99. nvmath/bindings/mathdx.pxd +182 -0
  100. nvmath/bindings/mathdx.pyi +1244 -0
  101. nvmath/bindings/nvpl/__init__.pxd +0 -0
  102. nvmath/bindings/nvpl/__init__.py +13 -0
  103. nvmath/bindings/nvpl/_internal/__init__.pxd +0 -0
  104. nvmath/bindings/nvpl/_internal/__init__.py +0 -0
  105. nvmath/bindings/nvpl/_internal/blas.cp314-win_amd64.pyd +0 -0
  106. nvmath/bindings/nvpl/_internal/blas.pxd +125 -0
  107. nvmath/bindings/nvpl/_internal/fft.cp314-win_amd64.pyd +0 -0
  108. nvmath/bindings/nvpl/_internal/fft.pxd +36 -0
  109. nvmath/bindings/nvpl/blas.cp314-win_amd64.pyd +0 -0
  110. nvmath/bindings/nvpl/blas.pxd +85 -0
  111. nvmath/bindings/nvpl/blas.pyi +122 -0
  112. nvmath/bindings/nvpl/cyblas.cp314-win_amd64.pyd +0 -0
  113. nvmath/bindings/nvpl/cyblas.pxd +166 -0
  114. nvmath/bindings/nvpl/cyfft.cp314-win_amd64.pyd +0 -0
  115. nvmath/bindings/nvpl/cyfft.pxd +92 -0
  116. nvmath/bindings/nvpl/fft.cp314-win_amd64.pyd +0 -0
  117. nvmath/bindings/nvpl/fft.pxd +100 -0
  118. nvmath/bindings/nvpl/fft.pyi +100 -0
  119. nvmath/bindings/nvshmem.pxd +54 -0
  120. nvmath/bindings/nvshmem.pyi +179 -0
  121. nvmath/device/__init__.py +21 -0
  122. nvmath/device/_deprecated.py +33 -0
  123. nvmath/device/common.py +313 -0
  124. nvmath/device/common_backend.py +131 -0
  125. nvmath/device/common_cuda.py +201 -0
  126. nvmath/device/common_numba.py +310 -0
  127. nvmath/device/common_opaque_tensor.py +201 -0
  128. nvmath/device/cublasdx.py +1805 -0
  129. nvmath/device/cublasdx_backend.py +807 -0
  130. nvmath/device/cublasdx_numba.py +1612 -0
  131. nvmath/device/cufftdx.py +510 -0
  132. nvmath/device/cufftdx_backend.py +196 -0
  133. nvmath/device/cufftdx_numba.py +196 -0
  134. nvmath/device/curand_kernel.py +9147 -0
  135. nvmath/device/cusolverdx.py +2690 -0
  136. nvmath/device/cusolverdx_backend.py +440 -0
  137. nvmath/device/cusolverdx_numba.py +1624 -0
  138. nvmath/device/llvm_array.py +29 -0
  139. nvmath/device/random.py +445 -0
  140. nvmath/device/random_helpers.py +23 -0
  141. nvmath/device/random_states.py +187 -0
  142. nvmath/device/types.py +147 -0
  143. nvmath/device/vector_types_numba.py +203 -0
  144. nvmath/distributed/__init__.py +205 -0
  145. nvmath/distributed/_internal/__init__.py +0 -0
  146. nvmath/distributed/_internal/nvshmem.py +302 -0
  147. nvmath/distributed/_internal/tensor_ifc.py +67 -0
  148. nvmath/distributed/_internal/tensor_ifc_cupy.py +62 -0
  149. nvmath/distributed/_internal/tensor_ifc_host_device.py +165 -0
  150. nvmath/distributed/_internal/tensor_ifc_numpy.py +41 -0
  151. nvmath/distributed/_internal/tensor_ifc_torch.py +141 -0
  152. nvmath/distributed/_internal/tensor_wrapper.py +78 -0
  153. nvmath/distributed/_utils.py +167 -0
  154. nvmath/distributed/distribution.py +770 -0
  155. nvmath/distributed/fft/__init__.py +7 -0
  156. nvmath/distributed/fft/_configuration.py +79 -0
  157. nvmath/distributed/fft/fft.py +2801 -0
  158. nvmath/distributed/linalg/__init__.py +12 -0
  159. nvmath/distributed/linalg/_internal/__init__.py +3 -0
  160. nvmath/distributed/linalg/_internal/epilog_protocol.py +496 -0
  161. nvmath/distributed/linalg/_internal/matmul_desc_ifc.py +28 -0
  162. nvmath/distributed/linalg/advanced/__init__.py +8 -0
  163. nvmath/distributed/linalg/advanced/_configuration.py +166 -0
  164. nvmath/distributed/linalg/advanced/matmulmod.py +2908 -0
  165. nvmath/distributed/process_group.py +408 -0
  166. nvmath/distributed/reshape/__init__.py +6 -0
  167. nvmath/distributed/reshape/_configuration.py +39 -0
  168. nvmath/distributed/reshape/reshape.py +1256 -0
  169. nvmath/fft/__init__.py +7 -0
  170. nvmath/fft/_configuration.py +208 -0
  171. nvmath/fft/_exec_utils.py +109 -0
  172. nvmath/fft/_helpers.py +237 -0
  173. nvmath/fft/fft.py +2903 -0
  174. nvmath/internal/__init__.pxd +3 -0
  175. nvmath/internal/__init__.py +10 -0
  176. nvmath/internal/_bindings.cp314-win_amd64.pyd +0 -0
  177. nvmath/internal/_bindings.pxd +12 -0
  178. nvmath/internal/_device_utils.py +54 -0
  179. nvmath/internal/enum_utils.py +142 -0
  180. nvmath/internal/formatters.py +87 -0
  181. nvmath/internal/mem_limit.py +51 -0
  182. nvmath/internal/memory.cp314-win_amd64.pyd +0 -0
  183. nvmath/internal/memory.pxd +10 -0
  184. nvmath/internal/memory.pyi +46 -0
  185. nvmath/internal/ndbuffer/__init__.pxd +3 -0
  186. nvmath/internal/ndbuffer/__init__.py +7 -0
  187. nvmath/internal/ndbuffer/copy_kernel/args.h +34 -0
  188. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/array_view.h +52 -0
  189. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/elementwise.h +68 -0
  190. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/grid_indexer.h +69 -0
  191. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/transposed.h +240 -0
  192. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/type_utils.h +39 -0
  193. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/utils.h +132 -0
  194. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/vec.h +159 -0
  195. nvmath/internal/ndbuffer/copy_kernel/elementwise.h +53 -0
  196. nvmath/internal/ndbuffer/copy_kernel/transposed.h +58 -0
  197. nvmath/internal/ndbuffer/copy_kernel.cp314-win_amd64.pyd +0 -0
  198. nvmath/internal/ndbuffer/copy_kernel.pxd +9 -0
  199. nvmath/internal/ndbuffer/copy_kernel.pyi +9 -0
  200. nvmath/internal/ndbuffer/data_layout.cp314-win_amd64.pyd +0 -0
  201. nvmath/internal/ndbuffer/data_layout.pxd +72 -0
  202. nvmath/internal/ndbuffer/data_layout.pyi +23 -0
  203. nvmath/internal/ndbuffer/jit.cp314-win_amd64.pyd +0 -0
  204. nvmath/internal/ndbuffer/jit.pxd +7 -0
  205. nvmath/internal/ndbuffer/jit.pyi +6 -0
  206. nvmath/internal/ndbuffer/ndbuffer.cp314-win_amd64.pyd +0 -0
  207. nvmath/internal/ndbuffer/ndbuffer.pxd +36 -0
  208. nvmath/internal/ndbuffer/ndbuffer.pyi +41 -0
  209. nvmath/internal/ndbuffer/package_utils.cp314-win_amd64.pyd +0 -0
  210. nvmath/internal/ndbuffer/package_utils.pxd +11 -0
  211. nvmath/internal/ndbuffer/package_utils.pyi +12 -0
  212. nvmath/internal/package_ifc.py +147 -0
  213. nvmath/internal/package_ifc_cuda.py +49 -0
  214. nvmath/internal/package_ifc_cupy.py +49 -0
  215. nvmath/internal/package_ifc_torch.py +31 -0
  216. nvmath/internal/package_wrapper.py +14 -0
  217. nvmath/internal/tensor_ifc.py +194 -0
  218. nvmath/internal/tensor_ifc_cupy.py +241 -0
  219. nvmath/internal/tensor_ifc_ndbuffer.py +153 -0
  220. nvmath/internal/tensor_ifc_numpy.py +183 -0
  221. nvmath/internal/tensor_ifc_torch.py +176 -0
  222. nvmath/internal/tensor_wrapper.py +157 -0
  223. nvmath/internal/typemaps.py +162 -0
  224. nvmath/internal/utils.py +771 -0
  225. nvmath/linalg/__init__.py +46 -0
  226. nvmath/linalg/_internal/__init__.py +3 -0
  227. nvmath/linalg/_internal/algo_cap_ifc.py +82 -0
  228. nvmath/linalg/_internal/algo_config_ifc.py +43 -0
  229. nvmath/linalg/_internal/batch.py +217 -0
  230. nvmath/linalg/_internal/enum_to_tuples.py +64 -0
  231. nvmath/linalg/_internal/epilog_protocol.py +766 -0
  232. nvmath/linalg/_internal/layout.py +624 -0
  233. nvmath/linalg/_internal/matmul_desc_ifc.py +28 -0
  234. nvmath/linalg/_internal/matmul_pref_ifc.py +27 -0
  235. nvmath/linalg/_internal/matrix_layout_ifc.py +26 -0
  236. nvmath/linalg/_internal/typemaps.py +144 -0
  237. nvmath/linalg/_internal/utils.py +134 -0
  238. nvmath/linalg/advanced/__init__.py +8 -0
  239. nvmath/linalg/advanced/_algorithmmod.py +149 -0
  240. nvmath/linalg/advanced/_configuration.py +349 -0
  241. nvmath/linalg/advanced/helpers/__init__.py +5 -0
  242. nvmath/linalg/advanced/helpers/matmul.py +1348 -0
  243. nvmath/linalg/advanced/matmulmod.py +3937 -0
  244. nvmath/linalg/generic/__init__.py +43 -0
  245. nvmath/linalg/generic/_configuration/__init__.py +37 -0
  246. nvmath/linalg/generic/_configuration/layout.py +263 -0
  247. nvmath/linalg/generic/_configuration/match.py +609 -0
  248. nvmath/linalg/generic/_configuration/qualifiers.py +494 -0
  249. nvmath/linalg/generic/_configuration/wrap.py +217 -0
  250. nvmath/linalg/generic/_dtype.py +15 -0
  251. nvmath/linalg/generic/matmulmod.py +1060 -0
  252. nvmath/memory.py +282 -0
  253. nvmath/sparse/__init__.py +38 -0
  254. nvmath/sparse/_internal/__init__.py +21 -0
  255. nvmath/sparse/_internal/common_utils.py +144 -0
  256. nvmath/sparse/_internal/cudss_config_ifc.py +741 -0
  257. nvmath/sparse/_internal/cudss_data_ifc.py +399 -0
  258. nvmath/sparse/_internal/cudss_utils.py +445 -0
  259. nvmath/sparse/_internal/cusparse_utils.py +382 -0
  260. nvmath/sparse/_internal/sparse_bsc_ifc.py +300 -0
  261. nvmath/sparse/_internal/sparse_bsr_ifc.py +303 -0
  262. nvmath/sparse/_internal/sparse_coo_ifc.py +254 -0
  263. nvmath/sparse/_internal/sparse_csc_ifc.py +266 -0
  264. nvmath/sparse/_internal/sparse_csr_ifc.py +266 -0
  265. nvmath/sparse/_internal/sparse_dia_ifc.py +239 -0
  266. nvmath/sparse/_internal/sparse_format_helpers.py +601 -0
  267. nvmath/sparse/_internal/sparse_tensor_ifc.py +121 -0
  268. nvmath/sparse/_internal/sparse_ust_ifc.py +146 -0
  269. nvmath/sparse/_internal/utils.py +56 -0
  270. nvmath/sparse/advanced/__init__.py +7 -0
  271. nvmath/sparse/advanced/_configuration.py +230 -0
  272. nvmath/sparse/advanced/direct_solver.py +1884 -0
  273. nvmath/sparse/generic/__init__.py +7 -0
  274. nvmath/sparse/generic/_configuration.py +129 -0
  275. nvmath/sparse/generic/_helpers.py +137 -0
  276. nvmath/sparse/generic/_thunks.py +21 -0
  277. nvmath/sparse/generic/matmulmod.py +2426 -0
  278. nvmath/sparse/ust/__init__.py +7 -0
  279. nvmath/sparse/ust/_converters.py +378 -0
  280. nvmath/sparse/ust/_drawer.py +552 -0
  281. nvmath/sparse/ust/_emitter.py +553 -0
  282. nvmath/sparse/ust/_jit.py +212 -0
  283. nvmath/sparse/ust/_kernel.py +346 -0
  284. nvmath/sparse/ust/_semiring.py +71 -0
  285. nvmath/sparse/ust/_utils.py +102 -0
  286. nvmath/sparse/ust/interfaces/__init__.py +0 -0
  287. nvmath/sparse/ust/interfaces/torch_interface.py +476 -0
  288. nvmath/sparse/ust/semiring_ops.py +71 -0
  289. nvmath/sparse/ust/tensor.py +1027 -0
  290. nvmath/sparse/ust/tensor_drawer.py +552 -0
  291. nvmath/sparse/ust/tensor_emitter.py +558 -0
  292. nvmath/sparse/ust/tensor_format.py +935 -0
  293. nvmath/sparse/ust/tensor_jit.py +212 -0
  294. nvmath/sparse/ust/tensor_kernel.py +348 -0
  295. nvmath/sparse/ust/tensor_utils.py +380 -0
  296. nvmath/tensor/__init__.py +6 -0
  297. nvmath/tensor/_configuration.py +95 -0
  298. nvmath/tensor/_internal/__init__.py +3 -0
  299. nvmath/tensor/_internal/cutensor_config_ifc.py +247 -0
  300. nvmath/tensor/_internal/cutensor_utils.py +162 -0
  301. nvmath/tensor/_internal/data.py +43 -0
  302. nvmath/tensor/_internal/einsum_parser.py +444 -0
  303. nvmath/tensor/_internal/typemaps.py +96 -0
  304. nvmath/tensor/contract.py +1861 -0
  305. nvmath_python-0.9.0.dist-info/METADATA +127 -0
  306. nvmath_python-0.9.0.dist-info/RECORD +309 -0
  307. nvmath_python-0.9.0.dist-info/WHEEL +5 -0
  308. nvmath_python-0.9.0.dist-info/licenses/LICENSE +177 -0
  309. nvmath_python-0.9.0.dist-info/top_level.txt +2 -0
nvmath/fft/fft.py ADDED
@@ -0,0 +1,2903 @@
1
+ # Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ __all__ = ["FFT", "fft", "ifft", "rfft", "irfft", "UnsupportedLayoutError", "estimate_workspace_size"]
6
+
7
+ import enum
8
+ import functools
9
+ import logging
10
+ import operator
11
+ from collections.abc import Sequence
12
+ from dataclasses import astuple as data_cls_astuple
13
+ from dataclasses import dataclass
14
+ from typing import Literal
15
+
16
+ from nvmath.bindings import cufft # type: ignore
17
+
18
+ from ._configuration import DeviceCallable, ExecutionCPU, ExecutionCUDA, FFTDirection, FFTOptions
19
+
20
+ try:
21
+ from nvmath.bindings.nvpl import fft as fftw # type: ignore
22
+ except ImportError:
23
+ fftw = None # type: ignore
24
+ from nvmath import memory
25
+ from nvmath._internal.layout import is_contiguous_in_memory, is_contiguous_layout, is_overlapping_layout
26
+ from nvmath.bindings._internal import utils as _bindings_utils # type: ignore
27
+ from nvmath.fft._exec_utils import _cross_setup_execution_and_options
28
+ from nvmath.internal import formatters, tensor_wrapper, utils
29
+ from nvmath.internal.package_wrapper import AnyStream, StreamHolder
30
+ from nvmath.internal.typemaps import (
31
+ DATA_TYPE_TO_NAME,
32
+ FFTW_SUPPORTED_COMPLEX,
33
+ FFTW_SUPPORTED_DOUBLE,
34
+ FFTW_SUPPORTED_FLOAT,
35
+ FFTW_SUPPORTED_SINGLE,
36
+ FFTW_SUPPORTED_TYPES,
37
+ NAME_TO_DATA_TYPE,
38
+ cudaDataType,
39
+ )
40
+
41
+
42
+ class UnsupportedLayoutError(Exception):
43
+ """
44
+ Error type for layouts not supported by the library.
45
+
46
+ Args:
47
+ message: The error message.
48
+
49
+ permutation: The permutation needed to convert the input layout to a supported
50
+ layout to the FFT operation. The same permutation needs to be applied to the
51
+ result to obtain the axis sequence corresponding to the non-permuted input.
52
+
53
+ axes: The dimensions along which the FFT is performed corresponding to the permuted
54
+ operand layout.
55
+ """
56
+
57
+ def __init__(self, message, permutation, axes):
58
+ self.message = message
59
+ self.permutation = permutation
60
+ self.axes = axes
61
+
62
+ def __str__(self):
63
+ return self.message
64
+
65
+
66
+ @dataclass
67
+ class TensorLayout:
68
+ """An internal data class for capturing the tensor layout."""
69
+
70
+ shape: Sequence[int]
71
+ strides: Sequence[int]
72
+
73
+
74
+ @dataclass
75
+ class PlanTraits:
76
+ """An internal data class for capturing FFT plan traits."""
77
+
78
+ result_shape: Sequence[int]
79
+ result_strides: Sequence[int]
80
+ optimized_result_layout: bool
81
+ ordered_axes: Sequence[int]
82
+ ordered_fft_in_shape: Sequence[int]
83
+ ordered_fft_in_embedding_shape: Sequence[int]
84
+ ordered_fft_out_shape: Sequence[int]
85
+ fft_batch_size: int
86
+ istride: int
87
+ idistance: int
88
+ ostride: int
89
+ odistance: int
90
+
91
+
92
+ class CBLoadType(enum.IntEnum):
93
+ COMPLEX64 = (0x0,)
94
+ COMPLEX128 = (0x1,)
95
+ FLOAT32 = (0x2,)
96
+ FLOAT64 = (0x3,)
97
+ UNDEFINED = 0x8
98
+
99
+
100
+ class CBStoreType(enum.IntEnum):
101
+ COMPLEX64 = (0x4,)
102
+ COMPLEX128 = (0x5,)
103
+ FLOAT32 = (0x6,)
104
+ FLOAT64 = (0x7,)
105
+ UNDEFINED = 0x8
106
+
107
+
108
+ SHARED_FFT_DOCUMENTATION = utils.COMMON_SHARED_DOC_MAP.copy()
109
+ SHARED_FFT_DOCUMENTATION.update(
110
+ {
111
+ "axes": """\
112
+ The dimensions along which the FFT is performed. ``axes[-1]`` is the 'last transformed' axis for rffts. Currently, it is
113
+ required that the axes are contiguous and include the first or the last dimension. Only up to 3D FFTs are
114
+ supported.""".replace("\n", " "),
115
+ #
116
+ "options": """\
117
+ Specify options for the FFT as a :class:`FFTOptions` object. Alternatively, a `dict` containing the parameters for the
118
+ ``FFTOptions`` constructor can also be provided. If not specified, the value will be set to the default-constructed
119
+ ``FFTOptions`` object.""".replace("\n", " "),
120
+ #
121
+ "execution": """\
122
+ Specify execution space options for the FFT as a :class:`ExecutionCUDA` or :class:`ExecutionCPU` object. Alternatively,
123
+ a string ('cuda' or 'cpu'), or a `dict` with the 'name' key set to 'cpu' or 'cuda' and optional parameters relevant to
124
+ the given execution space. If not specified, the execution space will be selected to match operand's storage (in GPU or
125
+ host memory), and the corresponding :class:`ExecutionCUDA` or :class:`ExecutionCPU` object will be
126
+ default-constructed.""".replace("\n", " "),
127
+ #
128
+ "prolog": """\
129
+ Provide device-callable function in LTO-IR format to use as load-callback as an object of type :class:`DeviceCallable`.
130
+ Alternatively, a `dict` containing the parameters for the ``DeviceCallable`` constructor can also be provided. The
131
+ default is no prolog. Currently, callbacks are supported only with CUDA execution.""".replace("\n", " "),
132
+ #
133
+ "epilog": """\
134
+ Provide device-callable function in LTO-IR format to use as store-callback as an object of type :class:`DeviceCallable`.
135
+ Alternatively, a `dict` containing the parameters for the ``DeviceCallable`` constructor can also be provided. The
136
+ default is no epilog. Currently, callbacks are supported only with CUDA execution.""".replace("\n", " "),
137
+ #
138
+ "direction": """\
139
+ Specify whether forward or inverse FFT is performed (:class:`FFTDirection` object, or as a string from ['forward',
140
+ 'inverse'], "or as an int from [-1, 1] denoting forward and inverse directions respectively).""".replace("\n", " "),
141
+ #
142
+ "fft_key": """\
143
+ A tuple as the key to represent the input FFT problem.""".replace("\n", " "),
144
+ #
145
+ "function_signature": """\
146
+ operand,
147
+ /,
148
+ *,
149
+ axes: Sequence[int] | None = None,
150
+ options: FFTOptions | None = None,
151
+ execution: ExecutionCPU | ExecutionCUDA | None = None,
152
+ prolog: DeviceCallable | None = None,
153
+ epilog: DeviceCallable | None = None,
154
+ stream: AnyStream | None = None
155
+ """.replace("\n", " "),
156
+ }
157
+ )
158
+
159
+
160
+ def complex_to_real_equivalent(name):
161
+ assert "complex" in name, f"Internal Error ({name=})"
162
+ m = name.split("complex")
163
+ assert len(m) in (1, 2)
164
+ size = int(m[-1]) // 2
165
+ if len(m) == 1:
166
+ return f"float{size}"
167
+ else:
168
+ return f"{m[0]}float{size}"
169
+
170
+
171
+ def real_to_complex_equivalent(name):
172
+ assert "float" in name, f"Internal Error ({name=})"
173
+ m = name.split("float")
174
+ assert len(m) in (1, 2)
175
+ size = int(m[-1])
176
+ if len(m) == 1:
177
+ return f"complex{size * 2}"
178
+ else:
179
+ return f"{m[0]}complex{size * 2}"
180
+
181
+
182
+ def _get_default_fft_abstract_type(dtype, fft_type):
183
+ if fft_type is not None:
184
+ return fft_type
185
+
186
+ f, c = "float", "complex"
187
+ if dtype[: len(f)] == f:
188
+ fft_type = "R2C"
189
+ elif dtype[: len(c)] == c:
190
+ fft_type = "C2C"
191
+ else:
192
+ raise ValueError(f"Unsupported dtype '{dtype}' for FFT.")
193
+ return fft_type
194
+
195
+
196
+ def _get_fft_result_and_compute_types(dtype, fft_abstract_type):
197
+ """
198
+ Return result and compute data type given the input data type and the FFT type.
199
+ """
200
+ if fft_abstract_type == "C2C":
201
+ return dtype, dtype
202
+ elif fft_abstract_type == "C2R":
203
+ return complex_to_real_equivalent(dtype), dtype
204
+ elif fft_abstract_type == "R2C":
205
+ return real_to_complex_equivalent(dtype), dtype
206
+ else:
207
+ raise ValueError(f"Unsupported FFT Type: '{fft_abstract_type}'")
208
+
209
+
210
+ def _get_fft_default_direction(fft_abstract_type):
211
+ """
212
+ Return the default FFT direction (as object of type configuration.FFTDirection) based on
213
+ the FFT type.
214
+ """
215
+ if fft_abstract_type in ["C2C", "R2C"]:
216
+ return FFTDirection.FORWARD
217
+ elif fft_abstract_type == "C2R":
218
+ return FFTDirection.INVERSE
219
+ else:
220
+ raise ValueError(f"Unsupported FFT Type: '{fft_abstract_type}'")
221
+
222
+
223
+ def _get_size(shape):
224
+ return functools.reduce(operator.mul, shape, 1)
225
+
226
+
227
+ def _get_last_axis_id_and_size(
228
+ axes: Sequence[int],
229
+ operand_shape: Sequence[int],
230
+ fft_abstract_type: Literal["C2C", "C2R", "R2C"],
231
+ last_axis_parity: Literal["even", "odd"],
232
+ ) -> tuple[int, int]:
233
+ """
234
+ Args:
235
+ axes: The user-specified or default FFT axes.
236
+
237
+ operand_shape: The input operand shape.
238
+
239
+ fft_abstract_type: The "abstract" type of the FFT ('C2C', 'C2R', 'R2C').
240
+
241
+ last_axis_parity: For 'C2R' FFTs, specify whether the last axis size is even or odd.
242
+
243
+ Returns the last axis ID and the corresponding axis size required for the result.
244
+ """
245
+ last_axis_id = axes[-1]
246
+
247
+ if fft_abstract_type == "C2C":
248
+ return last_axis_id, operand_shape[last_axis_id]
249
+
250
+ if fft_abstract_type == "C2R":
251
+ if last_axis_parity == "even":
252
+ return last_axis_id, 2 * (operand_shape[last_axis_id] - 1)
253
+ elif last_axis_parity == "odd":
254
+ return last_axis_id, 2 * operand_shape[last_axis_id] - 1
255
+ else:
256
+ raise AssertionError("Unreachable.")
257
+
258
+ if fft_abstract_type == "R2C":
259
+ return last_axis_id, operand_shape[last_axis_id] // 2 + 1
260
+
261
+
262
+ def check_inplace_overlapping_layout(operand: utils.TensorHolder):
263
+ if is_overlapping_layout(operand.shape, operand.strides):
264
+ raise ValueError(
265
+ f"In-place transform is not supported because the tensor with shape "
266
+ f"{operand.shape} and strides {operand.strides} overlaps in memory."
267
+ )
268
+
269
+
270
+ def check_embedding_possible(strides, presorted=False):
271
+ """
272
+ Check if the strides allow for calculating an embedding dimension.
273
+ """
274
+ if not presorted:
275
+ strides = sorted(strides)
276
+ # with a broadcasted view, stride can be 0
277
+ if any(strides[i - 1] == 0 for i in range(1, len(strides))):
278
+ return False
279
+ return all(strides[i] % strides[i - 1] == 0 for i in range(1, len(strides)))
280
+
281
+
282
+ def check_batch_tileable(sorted_batch_shape, sorted_batch_strides):
283
+ """
284
+ Check if FFT layout is tileable across the specified batch layout.
285
+ """
286
+ return is_contiguous_layout(sorted_batch_shape, sorted_batch_strides)
287
+
288
+
289
+ def check_contiguous_layout(axes, strides, shape):
290
+ if not axes:
291
+ return True
292
+ sorted_batch_strides, sorted_batch_shape = zip(*sorted((strides[a], shape[a]) for a in axes), strict=True)
293
+ return is_contiguous_layout(sorted_batch_shape, sorted_batch_strides)
294
+
295
+
296
+ def calculate_embedding_shape(shape: Sequence[int], strides: Sequence[int]):
297
+ """
298
+ Calculate the embedding shape for the given shape and strides.
299
+ """
300
+ n = len(strides)
301
+ # The shape is used to resolve cases like (1, 2, 1) : (2, 1, 1) in CuTe notation.
302
+ ordered_strides, _, order = zip(*sorted(zip(strides, shape, range(n), strict=True)), strict=True)
303
+
304
+ ordered_shape = [ordered_strides[i] // ordered_strides[i - 1] for i in range(1, len(ordered_strides))] + [shape[order[-1]]]
305
+
306
+ embedding_shape = [0] * n
307
+ for o in range(n):
308
+ embedding_shape[order[o]] = ordered_shape[o]
309
+
310
+ return embedding_shape, order
311
+
312
+
313
+ def axis_order_in_memory(shape, strides):
314
+ """
315
+ Compute the order in which the axes appear in memory.
316
+ """
317
+ # The shape is used to resolve cases like (1, 2, 1) : (2, 1, 1) in CuTe notation.
318
+ _, _, axis_order = zip(*sorted(zip(strides, shape, range(len(strides)), strict=True)), strict=True)
319
+
320
+ return axis_order
321
+
322
+
323
+ def calculate_strides(shape, axis_order):
324
+ """
325
+ Calculate the strides for the provided shape and axis order.
326
+ """
327
+ strides = [None] * len(shape)
328
+
329
+ stride = 1
330
+ for axis in axis_order:
331
+ strides[axis] = stride
332
+ stride *= shape[axis]
333
+
334
+ return strides
335
+
336
+
337
+ def unsupported_layout_exception(operand_dim, axes, message, logger):
338
+ logger.error(message)
339
+
340
+ permutation = tuple(a for a in range(operand_dim) if a not in axes) + tuple(axes)
341
+ fft_dim = len(axes)
342
+ axes = tuple(range(operand_dim - fft_dim, operand_dim))
343
+
344
+ message = (
345
+ f"To convert to a supported layout, create a transposed view using transpose{permutation} and copy the "
346
+ f"view into a new tensor, using view.copy() for instance, and use axes={axes}."
347
+ )
348
+ logger.error(message)
349
+
350
+ raise UnsupportedLayoutError(message, permutation, axes)
351
+
352
+
353
+ def get_null_logger(name):
354
+ logger = logging.getLogger(name)
355
+ logger.addHandler(logging.NullHandler())
356
+ logger.propagate = False
357
+
358
+ return logger
359
+
360
+
361
+ def get_fft_plan_traits(
362
+ operand_shape: Sequence[int],
363
+ operand_strides: Sequence[int],
364
+ operand_dtype,
365
+ axes: Sequence[int],
366
+ execution: ExecutionCUDA | ExecutionCPU,
367
+ *,
368
+ fft_abstract_type: Literal["C2C", "C2R", "R2C"] = "C2C",
369
+ last_axis_parity: Literal["even", "odd"] = "even",
370
+ result_layout: Literal["optimized", "natural"] = "optimized",
371
+ logger: logging.Logger | None = None,
372
+ ) -> PlanTraits:
373
+ """
374
+ Extract the FFT shape from the operand shape, compute the ordered axes so that the data
375
+ is C-contiguous in memory, and compute the result shape and strides.
376
+
377
+ Args:
378
+ operand_shape: The operand shape
379
+
380
+ operand_strides: The operand strides
381
+
382
+ axes: The axes over which the FFT is performed. For R2C and C2R transforms, the size
383
+ of the last axis in `axes` will change.
384
+
385
+ execution: The execution options, an instance of either ExecutionCUDA or
386
+ ExecutionCPU class.
387
+
388
+ fft_abstract_type: The "abstract" type of the FFT ('C2C', 'C2R', 'R2C').
389
+
390
+ last_axis_parity: For 'C2R' FFTs, specify whether the last axis size is even or odd.
391
+
392
+ The data needed for creating a cuFFT plan is returned in the following order:
393
+ (result_shape, result_strides), ordered_axes, ordered_fft_in_shape,
394
+ ordered_fft_out_shape, (istride, idistance), (ostride, odistance)
395
+ """
396
+ logger = logger if logger is not None else get_null_logger("get_fft_plan_traits_null")
397
+
398
+ if len(axes) > 3:
399
+ raise ValueError(
400
+ "Only up to 3D FFTs are currently supported. You can use the 'axes' option to specify up to three axes "
401
+ f"along which to perform the FFT. The current number of dimensions is {len(axes)} corresponding to the "
402
+ f"axes {axes}."
403
+ )
404
+
405
+ # Check for duplicate axis IDs.
406
+ if len(axes) != len(set(axes)):
407
+ raise ValueError(f"The specified FFT axes = {axes} contains duplicate axis IDs, which is not supported.")
408
+
409
+ operand_dim = len(operand_shape)
410
+ batch_axes = [axis for axis in range(operand_dim) if axis not in axes]
411
+
412
+ # Check if an embedding is possible for the provided operand layout.
413
+ if not check_embedding_possible(operand_strides):
414
+ message = (
415
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} is "
416
+ "not currently supported because it does not have a suitable embedding dimension."
417
+ )
418
+ unsupported_layout_exception(operand_dim, axes, message, logger)
419
+
420
+ # Compute the embedding shape for the operand.
421
+ operand_embedding_shape, axis_order = calculate_embedding_shape(operand_shape, operand_strides)
422
+ logger.debug(f"The operand embedding shape = {operand_embedding_shape}.")
423
+
424
+ # The first or the last *ordered* axis must be present in the specified axes to be able
425
+ # to use the "advanced" layout.
426
+ first, last = axis_order[-1], axis_order[0]
427
+ if first not in axes and last not in axes:
428
+ raise ValueError(
429
+ f"The first ({first}) or the last ({last}) tensor axis in stride order {axis_order} must be present in the "
430
+ f"specified FFT axes {axes}."
431
+ )
432
+
433
+ # Compute the embedding input shape for the FFT.
434
+ fft_in_embedding_shape = [operand_embedding_shape[a] for a in axes]
435
+
436
+ # Compute the input shape for the FFT.
437
+ fft_in_shape, fft_in_strides = zip(*[(operand_shape[a], operand_strides[a]) for a in axes], strict=True)
438
+ if not is_contiguous_in_memory(fft_in_embedding_shape, fft_in_strides):
439
+ message = (
440
+ f"The FFT axes {axes} cannot be reordered so that the data is contiguous in memory for "
441
+ f"operand shape = {operand_shape} and operand strides = {operand_strides}."
442
+ )
443
+ unsupported_layout_exception(operand_dim, axes, message, logger)
444
+
445
+ # Reorder the FFT axes and input shape so that they are contiguous or separated by
446
+ # constant stride in memory.
447
+ quadruple = sorted(
448
+ zip(fft_in_strides, fft_in_shape, fft_in_embedding_shape, axes, strict=True), key=lambda v: v[:2], reverse=True
449
+ )
450
+
451
+ ordered_in_strides, ordered_fft_in_shape, ordered_fft_in_embedding_shape, ordered_axes = zip(*quadruple, strict=True)
452
+
453
+ # Check if R2C and C2R can be supported without copying.
454
+ if fft_abstract_type in ["R2C", "C2R"] and ordered_axes[-1] != axes[-1]:
455
+ message = (
456
+ f"The last FFT axis specified ({axes[-1]}) must have the smallest stride of all the FFT axes' "
457
+ f"strides {fft_in_strides} for FFT type '{fft_abstract_type}'."
458
+ )
459
+ unsupported_layout_exception(operand_dim, axes, message, logger)
460
+
461
+ # Input FFT size and batch size.
462
+ fft_in_size = _get_size(fft_in_shape)
463
+ if fft_in_size == 0:
464
+ raise ValueError("Invalid number of FFT data points (0) specified.")
465
+ fft_batch_size = _get_size(operand_shape) // fft_in_size
466
+
467
+ # Output FFT (ordered) shape and size.
468
+ last_axis_id, last_axis_size = _get_last_axis_id_and_size(axes, operand_shape, fft_abstract_type, last_axis_parity)
469
+ if last_axis_size == 0:
470
+ raise ValueError(
471
+ f"The size of the last FFT axis in the result for FFT type '{fft_abstract_type}' is 0 for operand shape = "
472
+ f"{operand_shape} and axes = {axes}. To fix this, provide 'last_axis_parity' = 'odd' to the FFT options."
473
+ )
474
+ ordered_fft_out_shape = list(ordered_fft_in_shape)
475
+ index = ordered_axes.index(last_axis_id)
476
+ ordered_fft_out_shape[index] = last_axis_size
477
+ fft_out_size = _get_size(ordered_fft_out_shape)
478
+
479
+ # Check that batch dimensions are tileable, as required by the "advanced" layout.
480
+ sorted_batch_shape: Sequence[int] = []
481
+ sorted_batch_strides: Sequence[int] = []
482
+ if batch_axes:
483
+ sorted_batch_strides, sorted_batch_shape = zip(
484
+ *sorted((operand_strides[a], operand_shape[a]) for a in batch_axes), strict=True
485
+ )
486
+ if not check_embedding_possible(sorted_batch_strides, presorted=True):
487
+ raise ValueError(
488
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} "
489
+ f"together with the specified axes = {axes} is currently not supported because it is not tileable."
490
+ )
491
+ logger.debug(f"The sorted batch shape is {sorted_batch_shape}.")
492
+ logger.debug(f"The sorted batch strides are {sorted_batch_strides}.")
493
+ if not check_batch_tileable(sorted_batch_shape, sorted_batch_strides):
494
+ message = (
495
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} "
496
+ f"together with the specified axes = {axes} is currently not supported because it is not tileable."
497
+ )
498
+ unsupported_layout_exception(operand_dim, axes, message, logger)
499
+ logger.debug(
500
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} together with "
501
+ f"the specified axes = {axes} IS tileable."
502
+ )
503
+
504
+ # The result tensor has updated shape for R2C and C2R transforms.
505
+ result_shape = list(operand_shape)
506
+ result_shape[last_axis_id] = last_axis_size
507
+
508
+ # The result tensor layout is either natural or chosen for optimal cuFFT performance,
509
+ # based on the operand layout and user-provided option.
510
+
511
+ # We can keep the input's layout (i.e. operand's extents order of increasing strides)
512
+ # without performance hit, if the samples do not interleave.
513
+ # Otherwise, we try to keep it only when explicitly asked (result_layout=natural)
514
+ is_sample_interleaved = bool(sorted_batch_strides and sorted_batch_strides[0] <= ordered_in_strides[0])
515
+ logger.debug(f"Are the samples interleaved? {is_sample_interleaved}.")
516
+
517
+ use_optimized_result_layout = is_sample_interleaved and result_layout != "natural"
518
+ if not use_optimized_result_layout: # Natural (== operand) layout.
519
+ axis_order = axis_order_in_memory(operand_shape, operand_strides)
520
+ result_strides = calculate_strides(result_shape, axis_order)
521
+ # If the resulting output operand is not tilable, keeping the original layout is not
522
+ # possible. If `not is_sample_interleaved` the batch must be tilable, because the
523
+ # min batch stride is bigger than max fft stride
524
+ if is_sample_interleaved:
525
+ if not check_contiguous_layout(batch_axes, result_strides, result_shape):
526
+ message = (
527
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} "
528
+ f"together with the specified axes = {axes} is currently not supported with "
529
+ "result_layout='natural', because the output batch would not be tileable."
530
+ )
531
+ unsupported_layout_exception(operand_dim, axes, message, logger)
532
+ if not check_contiguous_layout(axes, result_strides, result_shape):
533
+ message = (
534
+ f"The operand layout corresponding to shape = {operand_shape} and strides = {operand_strides} "
535
+ f"together with the specified axes = {axes} is currently not supported with "
536
+ "result_layout='natural', because the output sample would be non-contiguous."
537
+ )
538
+ unsupported_layout_exception(operand_dim, axes, message, logger)
539
+ else: # Optimized layout.
540
+ axis_order = tuple(
541
+ list(reversed(ordered_axes)) + sorted((a for a in batch_axes), key=lambda v: (operand_strides[v], operand_shape[v]))
542
+ )
543
+ result_strides = calculate_strides(result_shape, axis_order)
544
+ logger.debug(f"The result layout is '{result_layout}' with the result_strides {result_strides}.")
545
+
546
+ # Compute the operand linear stride and distance needed for the cuFFT plan.
547
+ last_ordered_in_stride = ordered_in_strides[-1]
548
+ min_in_stride = min(operand_strides)
549
+
550
+ if last_ordered_in_stride == min_in_stride:
551
+ istride, idistance = (
552
+ min_in_stride,
553
+ _get_size(ordered_fft_in_embedding_shape) if not sorted_batch_strides else sorted_batch_strides[0],
554
+ )
555
+ else:
556
+ istride, idistance = (
557
+ last_ordered_in_stride,
558
+ min_in_stride if not sorted_batch_strides else sorted_batch_strides[0],
559
+ )
560
+
561
+ # Compute the result linear stride and distance needed for the cuFFT plan.
562
+ ostride = result_strides[ordered_axes[-1]] # minimal output fft stride
563
+ odistance = fft_out_size if not batch_axes else min(result_strides[axis] for axis in batch_axes)
564
+
565
+ if execution.name == "cpu":
566
+ if fft_out_size == 1:
567
+ istride = ostride = 1
568
+ else:
569
+ assert execution.name == "cuda"
570
+ if operand_dtype in ("float16", "complex32"):
571
+ if fft_abstract_type == "R2C" and istride != 1:
572
+ raise ValueError(
573
+ f"The {fft_abstract_type} FFT of half-precision tensor ({operand_dtype}) "
574
+ f"is currently not supported for strided inputs "
575
+ f"(got input stride {istride})."
576
+ )
577
+ if fft_abstract_type == "C2R" and ostride != 1:
578
+ raise ValueError(
579
+ f"The {fft_abstract_type} FFT of half-precision tensor ({operand_dtype}) "
580
+ f"is currently not supported for strided outputs "
581
+ f"(got output stride {ostride})."
582
+ )
583
+ if fft_out_size == 1:
584
+ if cufft.get_version() < 10702: # 10702 is shipped with CTK 11.7
585
+ raise ValueError(
586
+ f"The FFT of sample size 1 and half-precision type ({operand_dtype}) "
587
+ f"of size 1 is not supported by the installed cuFFT version. "
588
+ )
589
+ # There is a bug that leads to invalid memory access (CTK 12.1) for
590
+ # one-element, strided C2C complex32 tensors (either in the input or output)
591
+ # or results in CUFFT_INVALID_SIZE (CTK 12.3). This workaround relies on the
592
+ # fact that the [i|o]stride effectively does not matter in a one-element
593
+ # sample.
594
+ elif fft_abstract_type == "C2C":
595
+ istride = ostride = 1
596
+
597
+ # There's a bug in cuFFT in CTKs prior to 11.4U2
598
+ if len(axes) == 3 and fft_batch_size > 1 and cufft.get_version() < 10502:
599
+ raise ValueError(
600
+ "The 3D batched FFT is not supported by the installed cuFFT version. "
601
+ "Please update your CUDA Toolkit (to 11.4.2 or newer)"
602
+ )
603
+
604
+ plan_traits = PlanTraits(
605
+ result_shape=tuple(result_shape),
606
+ result_strides=tuple(result_strides),
607
+ optimized_result_layout=use_optimized_result_layout,
608
+ ordered_axes=tuple(ordered_axes),
609
+ ordered_fft_in_shape=tuple(ordered_fft_in_shape),
610
+ ordered_fft_in_embedding_shape=tuple(ordered_fft_in_embedding_shape),
611
+ ordered_fft_out_shape=tuple(ordered_fft_out_shape),
612
+ fft_batch_size=fft_batch_size,
613
+ istride=istride,
614
+ idistance=idistance,
615
+ ostride=ostride,
616
+ odistance=odistance,
617
+ )
618
+ return plan_traits
619
+
620
+
621
+ def _allocate_operand_or_identity(
622
+ user_operand: utils.TensorHolder,
623
+ stream_holder,
624
+ execution_space,
625
+ memory_space,
626
+ device_id: int | Literal["cpu"],
627
+ fft_abstract_type,
628
+ logger,
629
+ ):
630
+ """
631
+ Allocate the internal operand for the given execution/memory space, or
632
+ return the user operand as-is (identity) when
633
+ no internal buffer/mirror is needed.
634
+
635
+ Used during construction and after release_operand() when the internal
636
+ buffer needs to be created from scratch.
637
+ """
638
+ if execution_space == memory_space:
639
+ if fft_abstract_type != "C2R":
640
+ return user_operand, None
641
+ else:
642
+ # For C2R, we need to take a copy to avoid input being overwritten
643
+ logger.info("For C2R FFT with input operand on GPU, the input is copied to avoid being overwritten by cuFFT.")
644
+ internal_operand = utils.create_empty_tensor(
645
+ user_operand.__class__,
646
+ user_operand.shape,
647
+ user_operand.dtype,
648
+ device_id,
649
+ stream_holder,
650
+ verify_strides=True,
651
+ strides=user_operand.strides,
652
+ )
653
+ internal_operand.copy_(user_operand, stream_holder=stream_holder)
654
+ # We don't need to keep the operand backup, because C2R precludes `inplace=True`
655
+ return internal_operand, None
656
+ else:
657
+ # Copy the `operand` to memory that matches the exec space
658
+ # and keep the original `operand` to handle `options.inplace=True`
659
+ if execution_space == "cuda":
660
+ assert isinstance(device_id, int)
661
+ to_device: int | Literal["cpu"] = device_id
662
+ else:
663
+ assert execution_space == "cpu"
664
+ to_device = "cpu"
665
+ exec_space_copy = user_operand.to(to_device, stream_holder)
666
+ return exec_space_copy, user_operand
667
+
668
+
669
+ def _copy_operand_or_identity(
670
+ internal_operand: utils.TensorHolder,
671
+ new_operand: utils.TensorHolder,
672
+ stream_holder,
673
+ execution_space,
674
+ memory_space,
675
+ fft_abstract_type,
676
+ logger,
677
+ ):
678
+ """
679
+ Copy new operand data into an existing internal buffer, or return the
680
+ new operand as-is (identity) when no internal buffer is needed.
681
+
682
+ Used by reset_operand() when the operand has NOT been released, so the
683
+ internal buffer is still valid and can be reused via in-place copy.
684
+ """
685
+ if execution_space == memory_space:
686
+ if fft_abstract_type != "C2R":
687
+ return new_operand, None
688
+ else:
689
+ logger.info("For C2R FFT with input operand on GPU, the input is copied to avoid being overwritten by cuFFT.")
690
+ internal_operand.copy_(new_operand, stream_holder=stream_holder)
691
+ return internal_operand, None
692
+ else:
693
+ internal_operand.copy_(new_operand, stream_holder=stream_holder)
694
+ return internal_operand, new_operand
695
+
696
+
697
+ def create_xt_plan_args(*, plan_traits=None, fft_abstract_type=None, operand_data_type=None, inplace=None):
698
+ """
699
+ Create the arguments to xt_make_plan_many() except for the handle. This is also used for
700
+ computing the FFT key.
701
+ """
702
+ assert plan_traits is not None, "Internal error."
703
+ assert fft_abstract_type is not None, "Internal error."
704
+ assert operand_data_type is not None, "Internal error."
705
+ assert inplace is not None, "Internal error."
706
+
707
+ result_data_type, compute_data_type = _get_fft_result_and_compute_types(operand_data_type, fft_abstract_type)
708
+
709
+ # The input shape to the plan should be the logical FFT shape.
710
+ ordered_plan_shape = plan_traits.ordered_fft_out_shape if fft_abstract_type == "C2R" else plan_traits.ordered_fft_in_shape
711
+
712
+ # Handle in-place transforms.
713
+ if inplace:
714
+ ordered_fft_out_shape, ostride, odistance = (
715
+ plan_traits.ordered_fft_in_embedding_shape,
716
+ plan_traits.istride,
717
+ plan_traits.idistance,
718
+ )
719
+ else:
720
+ ordered_fft_out_shape, ostride, odistance = (
721
+ plan_traits.ordered_fft_out_shape,
722
+ plan_traits.ostride,
723
+ plan_traits.odistance,
724
+ )
725
+
726
+ return (
727
+ len(ordered_plan_shape),
728
+ ordered_plan_shape,
729
+ plan_traits.ordered_fft_in_embedding_shape,
730
+ plan_traits.istride,
731
+ plan_traits.idistance,
732
+ NAME_TO_DATA_TYPE[operand_data_type],
733
+ ordered_fft_out_shape,
734
+ ostride,
735
+ odistance,
736
+ NAME_TO_DATA_TYPE[result_data_type],
737
+ plan_traits.fft_batch_size,
738
+ NAME_TO_DATA_TYPE[compute_data_type],
739
+ )
740
+
741
+
742
+ def fftw_plan_args(xt_plan_args, operand_ptr, result_ptr, fft_abstract_type, direction):
743
+ """
744
+ Create the arguments for fftw API based on the args created by create_xt_plan_args and
745
+ pointers to the input and the output tensors. Note, that while the pointers to the data
746
+ are required in planning, different pointers may be passed to the same plan in
747
+ subsequent execute call (assuming dtype, memory layout, alignment, and inplace
748
+ properties do not change).
749
+ """
750
+ (
751
+ rank,
752
+ n,
753
+ inembed,
754
+ istride,
755
+ idist,
756
+ input_t,
757
+ onembed,
758
+ ostride,
759
+ odist,
760
+ output_t,
761
+ batch,
762
+ execution_t,
763
+ ) = xt_plan_args
764
+
765
+ if input_t in FFTW_SUPPORTED_SINGLE:
766
+ assert output_t in FFTW_SUPPORTED_SINGLE, "Expected single precision output for single precision input"
767
+ precision = fftw.Precision.FLOAT
768
+ elif input_t in FFTW_SUPPORTED_DOUBLE:
769
+ assert output_t in FFTW_SUPPORTED_DOUBLE, "Expected double precision output for double precision input"
770
+ precision = fftw.Precision.DOUBLE
771
+ else:
772
+ supported_types_str = ", ".join(DATA_TYPE_TO_NAME[dtype] for dtype in FFTW_SUPPORTED_TYPES)
773
+ raise ValueError(
774
+ f"Currently, the CPU FFT supports following input types: {supported_types_str}. "
775
+ f"Got input of type {DATA_TYPE_TO_NAME[input_t]}."
776
+ )
777
+ kind = getattr(fftw.Kind, fft_abstract_type)
778
+ if kind == fftw.Kind.C2C:
779
+ supported_in_t, supported_out_t = FFTW_SUPPORTED_COMPLEX, FFTW_SUPPORTED_COMPLEX
780
+ elif kind == fftw.Kind.C2R:
781
+ supported_in_t, supported_out_t = FFTW_SUPPORTED_COMPLEX, FFTW_SUPPORTED_FLOAT
782
+ else:
783
+ assert kind == fftw.Kind.R2C
784
+ supported_in_t, supported_out_t = FFTW_SUPPORTED_FLOAT, FFTW_SUPPORTED_COMPLEX
785
+ if input_t not in supported_in_t:
786
+ supported_types_str = ", ".join(DATA_TYPE_TO_NAME[dtype] for dtype in supported_in_t)
787
+ raise ValueError(
788
+ f"Got unsupported input data type {DATA_TYPE_TO_NAME[input_t]} "
789
+ f"for the {fft_abstract_type} transform. "
790
+ f"Supported types are {supported_types_str}."
791
+ )
792
+ assert output_t in supported_out_t, "Mismatched data type and FFT transform type"
793
+ if direction is None:
794
+ sign = fftw.Sign.UNSPECIFIED
795
+ else:
796
+ sign = fftw.Sign(direction)
797
+ return (
798
+ precision,
799
+ kind,
800
+ sign,
801
+ rank,
802
+ n,
803
+ batch,
804
+ operand_ptr,
805
+ inembed,
806
+ istride,
807
+ idist,
808
+ result_ptr,
809
+ onembed,
810
+ ostride,
811
+ odist,
812
+ fftw.PlannerFlags.ESTIMATE,
813
+ )
814
+
815
+
816
+ def setup_options(operand: utils.TensorHolder, options, execution) -> tuple[FFTOptions, ExecutionCUDA | ExecutionCPU]:
817
+ default_exec_space = operand.device
818
+ execution = utils.check_or_create_one_of_options(
819
+ (ExecutionCUDA, ExecutionCPU),
820
+ execution,
821
+ "'execution' options",
822
+ default_name=default_exec_space,
823
+ )
824
+ # Process options.
825
+ options = utils.check_or_create_options(FFTOptions, options, "FFT options")
826
+ return _cross_setup_execution_and_options(options, execution)
827
+
828
+
829
+ def _compute_fft_plan_args_and_traits(
830
+ operand: utils.TensorHolder,
831
+ *,
832
+ axes: Sequence[int] | None = None,
833
+ options: FFTOptions,
834
+ execution: ExecutionCPU | ExecutionCUDA,
835
+ inplace: bool | None = None,
836
+ ) -> tuple[tuple, PlanTraits]:
837
+ """
838
+ (private method) Compute plan_args and plan_traits for FFT operations.
839
+
840
+ Args:
841
+ operand: The input operand wrapped as a TensorHolder.
842
+ axes: The axes along which to perform the FFT.
843
+ options: Normalized FFTOptions object (use setup_options to normalize).
844
+ execution: ExecutionCPU or ExecutionCUDA object.
845
+ inplace: Whether the operation is in-place. If None, computed from
846
+ options.inplace with cross-device override.
847
+
848
+ Returns:
849
+ tuple: (plan_args, plan_traits) containing the plan arguments and traits.
850
+ """
851
+ fft_abstract_type = _get_default_fft_abstract_type(operand.dtype, options.fft_type)
852
+ if axes is None:
853
+ axes = range(len(operand.shape))
854
+ else:
855
+ operand_dim = len(operand.shape)
856
+ # Mirror FFT.__init__: enforce bounds and support negative indices.
857
+ if any(axis >= operand_dim or axis < -operand_dim for axis in axes):
858
+ raise ValueError(f"The specified FFT axes {tuple(axes)} are out of bounds for a {operand_dim}-D tensor.")
859
+ axes = tuple(axis % operand_dim for axis in axes)
860
+
861
+ # Determine plan traits.
862
+ plan_traits = get_fft_plan_traits(
863
+ operand.shape,
864
+ operand.strides,
865
+ operand.dtype,
866
+ axes,
867
+ execution,
868
+ fft_abstract_type=fft_abstract_type,
869
+ last_axis_parity=options.last_axis_parity,
870
+ result_layout=options.result_layout,
871
+ logger=None,
872
+ )
873
+
874
+ # If inplace is not explicitly provided, use options.inplace.
875
+ # For cross-device, inplace is always True (the operand needs to be copied once anyway).
876
+ if inplace is None:
877
+ memory_space = operand.device
878
+ execution_space = execution.name
879
+ assert execution.name in ("cpu", "cuda")
880
+ inplace = memory_space != execution_space or options.inplace
881
+
882
+ if inplace:
883
+ check_inplace_overlapping_layout(operand)
884
+
885
+ # Get the arguments to xt_make_plan_many.
886
+ plan_args = create_xt_plan_args(
887
+ plan_traits=plan_traits,
888
+ fft_abstract_type=fft_abstract_type,
889
+ operand_data_type=operand.dtype,
890
+ inplace=inplace,
891
+ )
892
+
893
+ return plan_args, plan_traits
894
+
895
+
896
+ def create_fft_key(
897
+ plan_args: tuple,
898
+ execution: ExecutionCPU | ExecutionCUDA,
899
+ *,
900
+ prolog: DeviceCallable | None = None,
901
+ epilog: DeviceCallable | None = None,
902
+ ) -> tuple[tuple, tuple | None, tuple]:
903
+ """
904
+ Create an FFT key from plan_args and execution options.
905
+
906
+ This key is not designed to be serialized and used on a different machine. It is meant
907
+ for runtime use only.
908
+
909
+ It is the user's responsibility to augment this key with the stream in case they use
910
+ stream-ordered memory pools.
911
+
912
+ Args:
913
+ plan_args: The plan arguments from create_xt_plan_args.
914
+ execution: The execution options (ExecutionCPU or ExecutionCUDA).
915
+ prolog: Optional prolog callback.
916
+ epilog: Optional epilog callback.
917
+
918
+ Returns:
919
+ tuple: (plan_args, callable_data, execution_tuple) representing the FFT key.
920
+ """
921
+ # Prolog and epilog, if used.
922
+ if prolog is not None or epilog is not None:
923
+ prolog = utils.check_or_create_options(DeviceCallable, prolog, "prolog", keep_none=True)
924
+ epilog = utils.check_or_create_options(DeviceCallable, epilog, "epilog", keep_none=True)
925
+
926
+ def get_data(device_callable):
927
+ return None if device_callable is None else (device_callable.ltoir, device_callable.data)
928
+
929
+ callable_data = get_data(prolog), get_data(epilog)
930
+ else:
931
+ callable_data = None
932
+
933
+ # The key is based on plan arguments, callback data (a callable object of type
934
+ # DeviceCallback or None) and the execution options (in "normalized" form of
935
+ # ("cpu"/"cuda", *execution_options)).
936
+ return plan_args, callable_data, data_cls_astuple(execution) # type: ignore[arg-type]
937
+
938
+
939
+ _CUDA_TYPES_TO_CUFFT_TYPE = {
940
+ (cudaDataType.CUDA_C_32F, cudaDataType.CUDA_C_32F): cufft.Type.C2C,
941
+ (cudaDataType.CUDA_R_32F, cudaDataType.CUDA_C_32F): cufft.Type.R2C,
942
+ (cudaDataType.CUDA_C_32F, cudaDataType.CUDA_R_32F): cufft.Type.C2R,
943
+ (cudaDataType.CUDA_C_64F, cudaDataType.CUDA_C_64F): cufft.Type.Z2Z,
944
+ (cudaDataType.CUDA_R_64F, cudaDataType.CUDA_C_64F): cufft.Type.D2Z,
945
+ (cudaDataType.CUDA_C_64F, cudaDataType.CUDA_R_64F): cufft.Type.Z2D,
946
+ }
947
+
948
+
949
+ def estimate_workspace_size(
950
+ key: tuple[tuple, tuple | None, tuple],
951
+ *,
952
+ technique: str = "default",
953
+ handle: int | None = None,
954
+ ) -> int:
955
+ """
956
+ Estimate the workspace size in bytes for the given FFT key.
957
+
958
+ Args:
959
+ key: The FFT key returned by :meth:`FFT.create_key`.
960
+
961
+ technique: The estimation technique to use.
962
+
963
+ - ``"default"``: Uses ``cufftEstimateMany``, which returns
964
+ the workspace size for default plan settings without
965
+ requiring a handle. Does not support half or bfloat16
966
+ precision keys.
967
+ - ``"refined"``: Uses ``cufftXtGetSizeMany``, which requires
968
+ a ``handle`` and returns the workspace size accounting for
969
+ any settings in the handle (e.g., plan properties).
970
+ With a freshly-created handle, this returns the
971
+ same value as ``"default"``.
972
+
973
+ handle: A cuFFT handle as returned by
974
+ ``nvmath.bindings.cufft.create()``. Required when
975
+ ``technique="refined"``; ignored otherwise. The caller is
976
+ responsible to ensure the handle was created on the same
977
+ CUDA device that the key targets.
978
+
979
+ Returns:
980
+ int: The estimated workspace size in bytes.
981
+
982
+ Semantics:
983
+ - This function does not create a full plan. The callback data
984
+ (prolog/epilog) in the key is ignored, as the underlying
985
+ cuFFT size estimation APIs do not account for callbacks.
986
+
987
+ - The returned value is 0 in two cases: (1) the key specifies
988
+ CPU execution, since workspace management is not applicable to
989
+ the CPU backend, or (2) the key specifies CUDA execution but
990
+ no temporary workspace is needed for the given FFT configuration.
991
+
992
+ .. seealso::
993
+ :meth:`FFT.create_key`
994
+ """
995
+ if technique not in ("default", "refined"):
996
+ raise ValueError(f"technique must be 'default' or 'refined', got {technique!r}.")
997
+
998
+ if not isinstance(key, tuple) or len(key) != 3:
999
+ raise ValueError(f"The key must be a 3-tuple as returned by FFT.create_key(). Got {key}.")
1000
+
1001
+ plan_args, _, execution_tuple = key
1002
+ if not isinstance(plan_args, tuple) or len(plan_args) != 12:
1003
+ raise ValueError("The first element of the key (plan_args) must be a 12-element tuple as returned by FFT.create_key().")
1004
+ if not isinstance(execution_tuple, tuple) or len(execution_tuple) < 1:
1005
+ raise ValueError("The third element of the key (execution) must be a non-empty tuple as returned by FFT.create_key().")
1006
+
1007
+ assert execution_tuple[0] in ("cpu", "cuda"), (
1008
+ f"Internal error: expected execution_tuple[0] to be 'cuda' or 'cpu', got {execution_tuple[0]!r}."
1009
+ )
1010
+ if execution_tuple[0] == "cpu":
1011
+ return 0
1012
+
1013
+ if technique == "refined":
1014
+ if handle is None:
1015
+ raise ValueError("A cuFFT handle is required when technique='refined'.")
1016
+ return cufft.xt_get_size_many(handle, *plan_args)
1017
+
1018
+ # technique == "default"
1019
+ inputtype = plan_args[5]
1020
+ outputtype = plan_args[9]
1021
+ cufft_type = _CUDA_TYPES_TO_CUFFT_TYPE.get((inputtype, outputtype))
1022
+ if cufft_type is None:
1023
+ raise ValueError(
1024
+ f"technique='default' (cufftEstimateMany) does not support "
1025
+ f"the data types in this key (inputtype={inputtype}, "
1026
+ f"outputtype={outputtype}). Use technique='refined' instead."
1027
+ )
1028
+ return cufft.estimate_many(
1029
+ plan_args[0], # rank
1030
+ plan_args[1], # n
1031
+ plan_args[2], # inembed
1032
+ plan_args[3], # istride
1033
+ plan_args[4], # idist
1034
+ plan_args[6], # onembed
1035
+ plan_args[7], # ostride
1036
+ plan_args[8], # odist
1037
+ cufft_type,
1038
+ plan_args[10], # batch
1039
+ )
1040
+
1041
+
1042
+ def set_prolog_and_epilog(handle, prolog, epilog, operand_dtype, result_dtype, logger):
1043
+ def set_callback(cbkind, cbobj, dtype):
1044
+ if cbobj is None:
1045
+ return
1046
+
1047
+ assert cbkind in ["prolog", "epilog"], "Internal error."
1048
+ CBType = CBLoadType if cbkind == "prolog" else CBStoreType
1049
+
1050
+ try:
1051
+ cufft.xt_set_jit_callback(handle, 0, cbobj.ltoir, cbobj.size, CBType[dtype.upper()], [cbobj.data])
1052
+ except _bindings_utils.FunctionNotFoundError as e:
1053
+ version = cufft.get_version()
1054
+ raise RuntimeError(
1055
+ f"The currently running cuFFT version {version} does not support LTO callbacks. \n"
1056
+ f"cuFFT LTO callbacks are supported starting with cuFFT 11.3, "
1057
+ f"shipped with CUDA Toolkit 12.6U2 (11.3.0) or newer. \n"
1058
+ ) from e
1059
+
1060
+ logger.info(f"The specified LTO-IR {cbkind} has been set.")
1061
+ if isinstance(cbobj.ltoir, int):
1062
+ logger.debug(f"The {cbkind} LTO-IR pointer is {cbobj.ltoir}.")
1063
+ logger.debug(f"The {cbkind} LTO-IR size is {cbobj.size}, and data is {cbobj.data}.")
1064
+
1065
+ if prolog is not None:
1066
+ set_callback("prolog", prolog, operand_dtype)
1067
+ if epilog is not None:
1068
+ set_callback("epilog", epilog, result_dtype)
1069
+
1070
+
1071
+ class InvalidFFTState(Exception):
1072
+ pass
1073
+
1074
+
1075
+ @utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=False)
1076
+ class FFT:
1077
+ """
1078
+ Create a stateful object that encapsulates the specified FFT computations and required
1079
+ resources. This object ensures the validity of resources during use and releases them
1080
+ when they are no longer needed to prevent misuse.
1081
+
1082
+ This object encompasses all functionalities of function-form APIs :func:`fft`,
1083
+ :func:`ifft`, :func:`rfft`, and :func:`irfft`, which are convenience wrappers around it.
1084
+ The stateful object also allows for the amortization of preparatory costs when the same
1085
+ FFT operation is to be performed on multiple operands with the same problem
1086
+ specification (see :meth:`reset_operand`, :meth:`reset_operand_unchecked`,
1087
+ and :meth:`create_key` for more details).
1088
+
1089
+ Using the stateful object typically involves the following steps:
1090
+
1091
+ 1. **Problem Specification**: Initialize the object with a defined operation and
1092
+ options.
1093
+ 2. **Preparation**: Use :meth:`plan` to determine the best algorithmic implementation
1094
+ for this specific FFT operation.
1095
+ 3. **Execution**: Perform the FFT computation with :meth:`execute`, which can be either
1096
+ forward or inverse FFT transformation.
1097
+ 4. **Resource Management**: Ensure all resources are released either by explicitly
1098
+ calling :meth:`free` or by managing the stateful object within a context manager.
1099
+
1100
+ Detailed information on each step described above can be obtained by passing in a
1101
+ :class:`logging.Logger` object to :class:`FFTOptions` or by setting the appropriate
1102
+ options in the root logger object, which is used by default:
1103
+
1104
+ >>> import logging
1105
+ >>> logging.basicConfig(
1106
+ ... level=logging.INFO,
1107
+ ... format="%(asctime)s %(levelname)-8s %(message)s",
1108
+ ... datefmt="%m-%d %H:%M:%S",
1109
+ ... )
1110
+
1111
+ .. versionchanged:: 0.9.0
1112
+ The `operand` parameter is now positional-only.
1113
+
1114
+ Args:
1115
+ operand: {operand}
1116
+
1117
+ axes: {axes}
1118
+
1119
+ options: {options}
1120
+
1121
+ execution: {execution}
1122
+
1123
+ stream: {stream}
1124
+
1125
+ .. seealso::
1126
+ :meth:`plan`, :meth:`reset_operand`, :meth:`reset_operand_unchecked`,
1127
+ :meth:`release_operand`, :meth:`execute`, :meth:`create_key`
1128
+
1129
+ Examples:
1130
+
1131
+ >>> import cupy as cp
1132
+ >>> import nvmath
1133
+
1134
+ Create a 3-D complex128 ndarray on the GPU:
1135
+
1136
+ >>> shape = 128, 128, 128
1137
+ >>> a = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
1138
+
1139
+ We will define a 2-D C2C FFT operation along the first two dimensions, batched along
1140
+ the last dimension:
1141
+
1142
+ >>> axes = 0, 1
1143
+
1144
+ Create an FFT object encapsulating the problem specification above:
1145
+
1146
+ >>> f = nvmath.fft.FFT(a, axes=axes)
1147
+
1148
+ Options can be provided above to control the behavior of the operation using the
1149
+ `options` argument (see :class:`FFTOptions`). Similarly, the execution space (CUDA
1150
+ or CPU) and execution options can be passed using the `execution` argument (see
1151
+ :class:`ExecutionCUDA`, :class:`ExecutionCPU`).
1152
+
1153
+ Next, plan the FFT. Load and/or store callback functions can be provided to
1154
+ :meth:`plan` using the `prolog` and `epilog` option:
1155
+
1156
+ >>> f.plan()
1157
+
1158
+ Now execute the FFT, and obtain the result `r1` as a CuPy ndarray. The transform
1159
+ will be performed on GPU, because ``execution`` was not explicitly specified and
1160
+ ``a`` resides in GPU memory.
1161
+
1162
+ >>> r1 = f.execute()
1163
+
1164
+ Finally, free the FFT object's resources. To avoid this explicit call, it's
1165
+ recommended to use the FFT object as a context manager as shown below, if possible.
1166
+
1167
+ >>> f.free()
1168
+
1169
+ Note that all :class:`FFT` methods execute on the current stream by default.
1170
+ Alternatively, the `stream` argument can be used to run a method on a specified
1171
+ stream.
1172
+
1173
+ Let's now look at the same problem with NumPy ndarrays on the CPU.
1174
+
1175
+ Create a 3-D complex128 NumPy ndarray on the CPU:
1176
+
1177
+ >>> import numpy as np
1178
+ >>> shape = 128, 128, 128
1179
+ >>> a = np.random.rand(*shape) + 1j * np.random.rand(*shape)
1180
+
1181
+ Create an FFT object encapsulating the problem specification described earlier and
1182
+ use it as a context manager.
1183
+
1184
+ >>> with nvmath.fft.FFT(a, axes=axes) as f:
1185
+ ... f.plan()
1186
+ ...
1187
+ ... # Execute the FFT to get the first result.
1188
+ ... r1 = f.execute()
1189
+
1190
+ All the resources used by the object are released at the end of the block.
1191
+
1192
+ The operation was performed on the CPU because ``a`` resides in host memory. With
1193
+ ``execution`` specified to 'cuda', the NumPy array would be temporarily copied to
1194
+ device memory and transformed on the GPU:
1195
+
1196
+ >>> with nvmath.fft.FFT(a, axes=axes, execution="cuda") as f:
1197
+ ... f.plan()
1198
+ ...
1199
+ ... # Execute the FFT to get the first result.
1200
+ ... r1 = f.execute()
1201
+
1202
+ Further examples can be found in the `nvmath/examples/fft
1203
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft>`_ directory.
1204
+
1205
+ Notes:
1206
+
1207
+ - The input must be Hermitian-symmetric when :attr:`FFTOptions.fft_type` is
1208
+ ``'C2R'``, otherwise the result is undefined. As a specific example, if the input
1209
+ for a C2R FFT was generated using an R2C FFT with an odd last axis size, then
1210
+ :attr:`FFTOptions.last_axis_parity` must be set to `odd` to recover the original
1211
+ signal.
1212
+ """
1213
+
1214
+ def __init__(
1215
+ self,
1216
+ operand,
1217
+ /,
1218
+ *,
1219
+ axes: Sequence[int] | None = None,
1220
+ options: FFTOptions | None = None,
1221
+ execution: ExecutionCPU | ExecutionCUDA | None = None,
1222
+ stream: AnyStream | None = None,
1223
+ ):
1224
+ self.operand = operand = tensor_wrapper.wrap_operand(operand)
1225
+ options, execution = setup_options(operand, options, execution)
1226
+ self.options = options
1227
+ self.execution_options = execution
1228
+
1229
+ self.operand_dim = len(operand.shape)
1230
+
1231
+ if not axes and self.operand_dim > 3:
1232
+ raise ValueError(
1233
+ f"The tensor is {self.operand_dim}-D and FFTs in number of dimensions > 3 is not supported. The FFT "
1234
+ "axes need to be specified using the 'axes' option."
1235
+ )
1236
+
1237
+ if self.operand_dim == 0:
1238
+ raise ValueError(f"The tensor is {self.operand_dim}-D (i.e. a scalar). FFT does not support scalars.")
1239
+
1240
+ self.operand_data_type = operand.dtype
1241
+ self.fft_abstract_type = _get_default_fft_abstract_type(self.operand_data_type, options.fft_type)
1242
+
1243
+ self.result_data_type, self.compute_data_type = _get_fft_result_and_compute_types(operand.dtype, self.fft_abstract_type)
1244
+
1245
+ self.logger = options.logger if options.logger is not None else logging.getLogger()
1246
+ self.logger.info(f"The FFT type is {self.fft_abstract_type}.")
1247
+ self.logger.info(
1248
+ f"The input data type is {self.operand_data_type}, and the result data type is {self.result_data_type}."
1249
+ )
1250
+
1251
+ if axes is None:
1252
+ axes = range(self.operand_dim)
1253
+
1254
+ if any(axis >= self.operand_dim or axis < -self.operand_dim for axis in axes):
1255
+ raise ValueError(f"The specified FFT axes {axes} are out of bounds for a {self.operand_dim}-D tensor.")
1256
+
1257
+ # Handle negative axis indices.
1258
+ self.axes = tuple(axis % self.operand_dim for axis in axes)
1259
+ self.logger.info(f"The specified FFT axes are {self.axes}.")
1260
+
1261
+ self.package = utils.infer_object_package(operand.tensor)
1262
+
1263
+ # NumPy and CuPy don't support complex32 yet.
1264
+ if self.package in ["numpy", "cupy"] and self.result_data_type == "complex32":
1265
+ raise TypeError(
1266
+ f"The result data type {self.result_data_type} is not supported by the operand package '{self.package}'."
1267
+ )
1268
+
1269
+ # Infer operand package, execution space, and memory space.
1270
+ if execution.name == "cuda":
1271
+ if operand.device == "cuda": # exec space matches the mem space
1272
+ self.memory_space = "cuda"
1273
+ self.device_id = operand.device_id
1274
+ else: # we need to move inputs cpu -> gpu and outputs gpu -> cpu
1275
+ self.memory_space = "cpu"
1276
+ self.device_id = execution.device_id
1277
+ else:
1278
+ assert execution.name == "cpu"
1279
+ self.device_id = "cpu"
1280
+ if operand.device_id == "cpu": # exec space matches the mem space
1281
+ self.memory_space = "cpu"
1282
+ else: # we need to move inputs gpu -> cpu and outputs cpu -> gpu
1283
+ self.memory_space = "cuda"
1284
+ self.execution_space = execution.name
1285
+ self.operand_device_id = operand.device_id
1286
+ self.internal_op_package = self._internal_operand_package(self.package)
1287
+ exec_stream_holder, operand_stream_holder = self._get_or_create_stream_maybe(stream)
1288
+
1289
+ self.logger.info(
1290
+ f"The input tensor's memory space is {self.memory_space}, and the execution space "
1291
+ f"is {self.execution_space}, with device {self.device_id}."
1292
+ )
1293
+
1294
+ self.logger.info(
1295
+ f"The specified stream for the FFT ctor is "
1296
+ f"{(exec_stream_holder or operand_stream_holder) and getattr(exec_stream_holder or operand_stream_holder, 'obj', None)}." # noqa: E501
1297
+ )
1298
+
1299
+ # In-place FFT option, available only for C2C transforms.
1300
+ self.inplace = self.options.inplace
1301
+ if self.inplace and self.fft_abstract_type != "C2C":
1302
+ raise ValueError(
1303
+ f"The in-place option (FFTOptions.inplace=True) is only supported for complex-to-complex FFT. "
1304
+ f"The FFT type is '{self.fft_abstract_type}'."
1305
+ )
1306
+
1307
+ # Key and plan_args computed from the user's original operand
1308
+ # before any copy across memory spaces occurs.
1309
+ #
1310
+ # When a copy occurs (cross-device or C2R), the copied operand may have different
1311
+ # strides than the original. We always compute the key from the user's original
1312
+ # operand to match what create_key() does. This ensures:
1313
+ # 1. get_key() returns the same value as create_key() for the same operand
1314
+ # 2. reset_operand() validates against the user-facing key, not the internal copy
1315
+ # We only need one key because reset_operand() validation
1316
+ # ensures the new operand produces the same key.
1317
+ self.key_from_user_operand: tuple[tuple, tuple | None, tuple] | None = None
1318
+ self.plan_args_from_user_operand: tuple | None = None
1319
+
1320
+ # Compute from original operand whenever a copy will occur:
1321
+ # - Cross-device: .to() may change strides
1322
+ # - C2R: copy preserves strides, but we compute for consistency
1323
+ if self.memory_space != self.execution_space or self.fft_abstract_type == "C2R":
1324
+ try:
1325
+ # Pass inplace explicitly to match what create_key() does.
1326
+ self.plan_args_from_user_operand, _ = _compute_fft_plan_args_and_traits(
1327
+ operand,
1328
+ axes=self.axes,
1329
+ options=self.options,
1330
+ execution=self.execution_options,
1331
+ inplace=self.options.inplace,
1332
+ )
1333
+ self.key_from_user_operand = create_fft_key(self.plan_args_from_user_operand, self.execution_options)
1334
+ except UnsupportedLayoutError:
1335
+ # If the layout is unsupported, we won't be able to compute a key.
1336
+ # This is fine - validation will fall back to other checks.
1337
+ self.key_from_user_operand = None
1338
+ self.plan_args_from_user_operand = None
1339
+
1340
+ # Copy the operand to execution_space's device if needed.
1341
+ self.operand, self.operand_backup = _allocate_operand_or_identity(
1342
+ operand,
1343
+ operand_stream_holder,
1344
+ self.execution_space,
1345
+ self.memory_space,
1346
+ self.device_id,
1347
+ self.fft_abstract_type,
1348
+ self.logger,
1349
+ )
1350
+
1351
+ # Track whether the user has called release_operand(). This flag is
1352
+ # checked in _check_valid_operand to prevent execution after the user
1353
+ # has released their operand. It is cleared by reset_operand() and
1354
+ # reset_operand_unchecked().
1355
+ self._operand_released = False
1356
+
1357
+ # For C2R transforms, cuFFT and FFTW overwrite the input buffer
1358
+ # during execute(). This flag marks the operand as stale after each
1359
+ # execute() call so that users must call reset_operand() or
1360
+ # reset_operand_unchecked() before executing again.
1361
+ self._c2r_operand_stale = False
1362
+
1363
+ # For C2R with same-space CUDA, we allocated an auxiliary buffer above
1364
+ # on operand_stream_holder. Track its allocation stream so we can ensure
1365
+ # proper ordering, for example when the buffer is freed in free().
1366
+ self.c2r_buffer_stream = None
1367
+ if self.execution_space == "cuda" and self.execution_space == self.memory_space and self.fft_abstract_type == "C2R":
1368
+ assert operand_stream_holder is not None
1369
+ self.c2r_buffer_stream = operand_stream_holder.obj
1370
+
1371
+ operand = self.operand
1372
+ # Class invariant: self.internal_operand_layout always reflects the layout of
1373
+ # self.operand. For same-space this is the user's operand; for
1374
+ # cross-space it is the execution-space copy.
1375
+ self.internal_operand_layout = TensorLayout(shape=operand.shape, strides=operand.strides)
1376
+
1377
+ self._preallocated_result: utils.TensorHolder | None = None
1378
+
1379
+ if self.options.inplace: # Don't use self.inplace here, because we always set it to True for CPU tensors.
1380
+ self.logger.info("The FFT will be performed in-place, with the result overwriting the input.")
1381
+ else:
1382
+ self.logger.info("The FFT will be performed out-of-place.")
1383
+
1384
+ # Check if FFT is supported and calculate plan traits.
1385
+ self.plan_traits = get_fft_plan_traits(
1386
+ operand.shape,
1387
+ operand.strides,
1388
+ operand.dtype,
1389
+ self.axes,
1390
+ self.execution_options,
1391
+ fft_abstract_type=self.fft_abstract_type,
1392
+ last_axis_parity=self.options.last_axis_parity,
1393
+ result_layout=self.options.result_layout,
1394
+ logger=self.logger,
1395
+ )
1396
+
1397
+ # Derive plan_args_from_user_operand from plan_traits if not already set.
1398
+ if self.plan_args_from_user_operand is None:
1399
+ self.plan_args_from_user_operand = create_xt_plan_args(
1400
+ plan_traits=self.plan_traits,
1401
+ fft_abstract_type=self.fft_abstract_type,
1402
+ operand_data_type=self.operand_data_type,
1403
+ inplace=self.options.inplace,
1404
+ )
1405
+
1406
+ # Ensure key_from_user_operand is set (for same-device case
1407
+ # and cross-device fallback).
1408
+ if self.key_from_user_operand is None:
1409
+ self.key_from_user_operand = create_fft_key(self.plan_args_from_user_operand, self.execution_options)
1410
+
1411
+ self.logger.info(
1412
+ f"The operand data type = {self.operand_data_type}, shape = {self.internal_operand_layout.shape}, and "
1413
+ f"strides = {self.internal_operand_layout.strides}."
1414
+ )
1415
+ result_data_type, result_shape, result_strides = (
1416
+ (self.operand_data_type, self.internal_operand_layout.shape, self.internal_operand_layout.strides)
1417
+ if self.inplace
1418
+ else (self.result_data_type, self.plan_traits.result_shape, self.plan_traits.result_strides)
1419
+ )
1420
+ self.logger.info(f"The result data type = {result_data_type}, shape = {result_shape}, and strides = {result_strides}.")
1421
+ self.logger.info(f"The FFT batch size is {self.plan_traits.fft_batch_size}.")
1422
+
1423
+ ordered_fft_out_shape, ostride, odistance = (
1424
+ (self.plan_traits.ordered_fft_in_shape, self.plan_traits.istride, self.plan_traits.idistance)
1425
+ if self.inplace
1426
+ else (self.plan_traits.ordered_fft_out_shape, self.plan_traits.ostride, self.plan_traits.odistance)
1427
+ )
1428
+ self.logger.debug(
1429
+ f"The plan ordered axes = {self.plan_traits.ordered_axes}, ordered input shape = "
1430
+ f"{self.plan_traits.ordered_fft_in_shape}, ordered input embedding shape = "
1431
+ f"{self.plan_traits.ordered_fft_in_embedding_shape}, ordered output shape = {ordered_fft_out_shape}."
1432
+ )
1433
+ self.logger.debug(f"The plan input stride is {self.plan_traits.istride} with distance {self.plan_traits.idistance}.")
1434
+ self.logger.debug(f"The plan output stride is {ostride} with distance {odistance}.")
1435
+
1436
+ # The result's package and device.
1437
+ self.result_class = operand.__class__
1438
+
1439
+ # Set blocking or non-blocking behavior.
1440
+ self.blocking = self.options.blocking is True or self.memory_space == "cpu" or self.execution_space == "cpu"
1441
+ if self.blocking:
1442
+ self.call_prologue = "This call is blocking and will return only after the operation is complete."
1443
+ else:
1444
+ self.call_prologue = (
1445
+ "This call is non-blocking and will return immediately after the operation is launched on the device."
1446
+ )
1447
+
1448
+ # Set memory allocator.
1449
+ if self.execution_space == "cpu":
1450
+ self.allocator = None # currently, the nvpl/fftw does not support custom workspace allocation
1451
+ else:
1452
+ self.allocator = (
1453
+ options.allocator
1454
+ if options.allocator is not None
1455
+ else memory._MEMORY_MANAGER[self.internal_op_package](self.device_id, self.logger)
1456
+ )
1457
+
1458
+ if self.execution_space == "cpu":
1459
+ # the handle is created alongside planning
1460
+ self.handle = None
1461
+ else:
1462
+ # Create handle.
1463
+ with utils.device_ctx(self.device_id):
1464
+ self.handle = cufft.create()
1465
+
1466
+ # Set stream for the FFT.
1467
+ cufft.set_stream(self.handle, exec_stream_holder.ptr) # type: ignore[union-attr]
1468
+
1469
+ # Plan attributes.
1470
+ cufft.set_auto_allocation(self.handle, 0)
1471
+
1472
+ self.fft_planned = False
1473
+
1474
+ # Workspace attributes.
1475
+ self.workspace_ptr: None | memory.MemoryPointer = None
1476
+ self.workspace_size = 0
1477
+ self._workspace_allocated_here = False
1478
+
1479
+ # Attributes to establish stream ordering.
1480
+ self.workspace_stream = None
1481
+ self.last_compute_event = None
1482
+
1483
+ self.valid_state = True
1484
+ self.logger.info("The FFT operation has been created.")
1485
+
1486
+ def _check_planned(self, *args, **kwargs):
1487
+ """ """
1488
+ what = kwargs["what"]
1489
+ if not self.fft_planned:
1490
+ raise RuntimeError(f"{what} cannot be performed before plan() has been called.")
1491
+
1492
+ @utils.precondition(_check_planned, "Returning the key")
1493
+ def get_key(self, *, prolog: DeviceCallable | None = None, epilog: DeviceCallable | None = None):
1494
+ """
1495
+ Get the key based on the current state of this FFT object,
1496
+ supplemented with the callbacks. The key is computed from the object's
1497
+ current plan, execution options, and operand properties.
1498
+
1499
+ Args:
1500
+ prolog: {prolog}
1501
+ epilog: {epilog}
1502
+
1503
+ Returns:
1504
+ {fft_key}
1505
+
1506
+ .. seealso::
1507
+ :meth:`create_key`, :func:`estimate_workspace_size`
1508
+ """
1509
+ # plan_args_from_user_operand is guaranteed to be set after __init__
1510
+ return create_fft_key(
1511
+ self.plan_args_from_user_operand, # type: ignore[arg-type]
1512
+ self.execution_options,
1513
+ prolog=prolog,
1514
+ epilog=epilog,
1515
+ )
1516
+
1517
+ @staticmethod
1518
+ def create_key(
1519
+ operand,
1520
+ *,
1521
+ axes: Sequence[int] | None = None,
1522
+ options: FFTOptions | None = None,
1523
+ execution: ExecutionCPU | ExecutionCUDA | None = None,
1524
+ prolog: DeviceCallable | None = None,
1525
+ epilog: DeviceCallable | None = None,
1526
+ ):
1527
+ """
1528
+ Create a key as a compact representation of the FFT problem specification based on
1529
+ the given operand, axes and the FFT options. Note that different combinations of
1530
+ operand layout, axes and options can potentially correspond to the same underlying
1531
+ problem specification (key). Users may reuse the FFT objects when different input
1532
+ problems map to an identical key.
1533
+
1534
+ Args:
1535
+ operand: {operand}
1536
+
1537
+ axes: {axes}
1538
+
1539
+ options: {options}
1540
+
1541
+ execution: {execution}
1542
+
1543
+ prolog: {prolog}
1544
+
1545
+ epilog: {epilog}
1546
+
1547
+ Returns:
1548
+ {fft_key}
1549
+
1550
+ Notes:
1551
+ - Users may take advantage of this method to create cached version of
1552
+ :func:`fft` based on the stateful object APIs (see `caching.py
1553
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft/caching.py>`_
1554
+ for an example implementation).
1555
+ - This key is meant for runtime use only and not designed to be serialized or
1556
+ used on a different machine.
1557
+ - It is the user's responsibility to augment this key with the stream in case
1558
+ they use stream-ordered memory pools.
1559
+
1560
+ .. seealso::
1561
+ :meth:`get_key`, :func:`estimate_workspace_size`
1562
+ """
1563
+ operand = tensor_wrapper.wrap_operand(operand)
1564
+ options, execution = setup_options(operand, options, execution)
1565
+ plan_args, _ = _compute_fft_plan_args_and_traits(
1566
+ operand, axes=axes, options=options, execution=execution, inplace=options.inplace
1567
+ )
1568
+ return create_fft_key(plan_args, execution, prolog=prolog, epilog=epilog)
1569
+
1570
+ def __enter__(self):
1571
+ return self
1572
+
1573
+ def __exit__(self, exc_type, exc_value, traceback):
1574
+ self.free()
1575
+
1576
+ def _check_valid_fft(self, *args, **kwargs):
1577
+ """
1578
+ Check if FFT object is alive and well.
1579
+ """
1580
+ if not self.valid_state:
1581
+ raise InvalidFFTState("The FFT object cannot be used after resources are free'd")
1582
+
1583
+ def _free_plan_resources(self, exception: Exception | None = None) -> bool:
1584
+ """
1585
+ Free resources allocated in planning.
1586
+ """
1587
+
1588
+ self.workspace_ptr = None
1589
+ self.fft_planned = False
1590
+ return True
1591
+
1592
+ def _internal_operand_package(self, package_name):
1593
+ if self.execution_space == "cuda":
1594
+ return package_name if package_name != "numpy" else "cuda"
1595
+ else:
1596
+ return package_name if package_name != "cupy" else "cupy_host"
1597
+
1598
+ def _get_or_create_stream_maybe(self, stream: AnyStream) -> tuple[StreamHolder | None, StreamHolder | None]:
1599
+ if self.execution_space == "cuda":
1600
+ assert isinstance(self.device_id, int), self.device_id
1601
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
1602
+ return stream_holder, stream_holder
1603
+ elif self.memory_space == "cuda":
1604
+ assert isinstance(self.operand_device_id, int), self.operand_device_id
1605
+ operand_device_steam = utils.get_or_create_stream(self.operand_device_id, stream, self.package)
1606
+ return None, operand_device_steam
1607
+ else:
1608
+ return None, None
1609
+
1610
+ def _allocate_result_operand(self, exec_stream_holder: StreamHolder | None, log_debug):
1611
+ if log_debug:
1612
+ self.logger.debug("Beginning output (empty) tensor creation...")
1613
+ self.logger.debug(
1614
+ f"The output tensor shape = {self.plan_traits.result_shape} with strides = "
1615
+ f"{self.plan_traits.result_strides} and data type '{self.result_data_type}'."
1616
+ )
1617
+ result = utils.create_empty_tensor(
1618
+ self.result_class,
1619
+ self.plan_traits.result_shape,
1620
+ self.result_data_type,
1621
+ self.device_id,
1622
+ exec_stream_holder,
1623
+ verify_strides=False, # the strides are computed so that they are contiguous
1624
+ strides=self.plan_traits.result_strides,
1625
+ )
1626
+ if log_debug:
1627
+ self.logger.debug("The output (empty) tensor has been created.")
1628
+ return result
1629
+
1630
+ def _get_validate_direction(self, direction):
1631
+ if isinstance(direction, str) and (d := direction.upper()) in ["FORWARD", "INVERSE"]:
1632
+ direction = FFTDirection[d]
1633
+ else:
1634
+ direction = FFTDirection(direction)
1635
+
1636
+ if self.fft_abstract_type == "C2R":
1637
+ if direction != FFTDirection.INVERSE:
1638
+ raise ValueError(
1639
+ f"The specified direction {direction.name} is not compatible with the FFT type '{self.fft_abstract_type}'."
1640
+ )
1641
+ elif self.fft_abstract_type == "R2C": # noqa: SIM102
1642
+ if direction != FFTDirection.FORWARD:
1643
+ raise ValueError(
1644
+ f"The specified direction {direction.name} is not compatible with the FFT type '{self.fft_abstract_type}'."
1645
+ )
1646
+ return direction
1647
+
1648
+ @utils.precondition(_check_valid_fft)
1649
+ @utils.atomic(_free_plan_resources, method=True)
1650
+ def plan(
1651
+ self,
1652
+ *,
1653
+ prolog: DeviceCallable | None = None,
1654
+ epilog: DeviceCallable | None = None,
1655
+ stream: AnyStream | None = None,
1656
+ direction: FFTDirection | None = None,
1657
+ ):
1658
+ """Plan the FFT.
1659
+
1660
+ Args:
1661
+ prolog: {prolog}
1662
+
1663
+ epilog: {epilog}
1664
+
1665
+ stream: {stream}
1666
+
1667
+ direction: If specified, the same direction must be passed to subsequent
1668
+ :meth:`execute` calls. It may be used as a hint to optimize C2C planning for
1669
+ CPU FFT calls.
1670
+ """
1671
+ log_info = self.logger.isEnabledFor(logging.INFO)
1672
+ log_debug = self.logger.isEnabledFor(logging.DEBUG)
1673
+
1674
+ if self.fft_planned:
1675
+ self.logger.debug("The FFT has already been planned, and redoing the plan is not supported.")
1676
+ return
1677
+
1678
+ if self.execution_space == "cpu":
1679
+ stream_holder = None
1680
+ if prolog is not None or epilog is not None:
1681
+ raise ValueError("The 'prolog' and 'epilog' are not supported with CPU 'execution'.")
1682
+ else:
1683
+ assert isinstance(self.device_id, int)
1684
+ stream_holder = utils.get_or_create_stream(self.device_id, stream, self.internal_op_package)
1685
+
1686
+ # Set stream for the FFT.
1687
+ cufft.set_stream(self.handle, stream_holder.ptr)
1688
+
1689
+ # Set LTO-IR callbacks, if present.
1690
+ prolog = utils.check_or_create_options(DeviceCallable, prolog, "prolog", keep_none=True)
1691
+ epilog = utils.check_or_create_options(DeviceCallable, epilog, "epilog", keep_none=True)
1692
+ set_prolog_and_epilog(self.handle, prolog, epilog, self.operand_data_type, self.result_data_type, self.logger)
1693
+
1694
+ # Get all the arguments to xt_make_plan_many except for the first (the handle).
1695
+ if self.inplace:
1696
+ check_inplace_overlapping_layout(self.operand)
1697
+ if self.operand_backup is not None:
1698
+ check_inplace_overlapping_layout(self.operand_backup)
1699
+
1700
+ plan_args = create_xt_plan_args(
1701
+ plan_traits=self.plan_traits,
1702
+ fft_abstract_type=self.fft_abstract_type,
1703
+ operand_data_type=self.operand_data_type,
1704
+ inplace=self.inplace,
1705
+ )
1706
+
1707
+ if log_debug:
1708
+ self.logger.debug(f"The FFT key (sans callback) is {self.key_from_user_operand}.")
1709
+
1710
+ self.logger.debug(
1711
+ f"The operand CUDA type is {NAME_TO_DATA_TYPE[self.operand_data_type].name}, and the result CUDA type is "
1712
+ f"{NAME_TO_DATA_TYPE[self.result_data_type].name}."
1713
+ )
1714
+ self.logger.debug(f"The CUDA type used for compute is {NAME_TO_DATA_TYPE[self.compute_data_type].name}.")
1715
+ if log_info:
1716
+ self.logger.info("Starting FFT planning...")
1717
+
1718
+ if self.execution_space == "cpu":
1719
+ if direction is not None:
1720
+ direction = self._get_validate_direction(direction)
1721
+ if self.inplace:
1722
+ result_ptr = self.operand.data_ptr
1723
+ else:
1724
+ # FFTW3 API requires passing pointers to the input and output during
1725
+ # planning. Passing different pointers to (properly strided and aligned)
1726
+ # data in subsequent execute calls is supported, but it is not clear what
1727
+ # planning is allowed to do with the provided pointers. For one, planning
1728
+ # can compare the two pointers for equality to decide if it is inplace or
1729
+ # out-of-place operation. To avoid subtle issues, just preallocate the
1730
+ # result tensor earlier.
1731
+ self._preallocated_result = self._allocate_result_operand(None, True)
1732
+ result_ptr = self._preallocated_result.data_ptr # type: ignore[attr-defined, union-attr]
1733
+ precision, *plan_args = fftw_plan_args(
1734
+ plan_args,
1735
+ self.operand.data_ptr,
1736
+ result_ptr,
1737
+ fft_abstract_type=self.fft_abstract_type,
1738
+ direction=direction,
1739
+ )
1740
+ with utils.host_call_ctx(timing=log_info) as elapsed:
1741
+ fftw.plan_with_nthreads(precision, self.execution_options.num_threads) # type: ignore[union-attr]
1742
+ try:
1743
+ assert self.handle is None
1744
+ self.handle = fftw.plan_many(precision, *plan_args)
1745
+ except OverflowError as e:
1746
+ raise ValueError(
1747
+ "Currently, the CPU FFT only supports the problem sizes that "
1748
+ "can be expressed as 32-bit signed integer. "
1749
+ "Tensors with shape extents or stride larger than "
1750
+ "`2147483647` are not currently supported."
1751
+ ) from e
1752
+ else:
1753
+ # FIXME: Move creation of stream_holder into this block, so assertion is not
1754
+ # needed
1755
+ assert isinstance(self.device_id, int), self.device_id
1756
+ assert stream_holder is not None
1757
+ with utils.cuda_call_ctx(stream_holder, blocking=True, timing=log_info) as (
1758
+ self.last_compute_event,
1759
+ elapsed,
1760
+ ):
1761
+ self.workspace_size = cufft.xt_make_plan_many(self.handle, *plan_args)
1762
+
1763
+ self.fft_planned = True
1764
+
1765
+ if log_info and elapsed.data is not None:
1766
+ self.logger.info(f"The FFT planning phase took {elapsed.data:.3f} ms to complete.")
1767
+
1768
+ def _maybe_wait_c2r_aux_buffer_on_last_compute(self):
1769
+ """
1770
+ (private) Make the C2R same-space auxiliary buffer's alloc stream
1771
+ wait on ``self.last_compute_event``, so any in-flight compute using
1772
+ the buffer is ordered before its (potentially stream-ordered-async)
1773
+ free. No-op in every other configuration, or when there is no
1774
+ outstanding compute event.
1775
+ """
1776
+ if (
1777
+ self.execution_space == self.memory_space
1778
+ and self.fft_abstract_type == "C2R"
1779
+ and self.c2r_buffer_stream is not None
1780
+ and self.last_compute_event is not None
1781
+ ):
1782
+ self.c2r_buffer_stream.wait(self.last_compute_event)
1783
+
1784
+ def _reset_operand_validate(self, operand):
1785
+ """(private method) Validate operand compatibility.
1786
+ This method is used by :meth:`reset_operand` to validate that the new
1787
+ user-provided operand satisfies all the requirements listed in
1788
+ the :meth:`reset_operand` docstring.
1789
+
1790
+ Args:
1791
+ operand: The new operand tensor to validate.
1792
+
1793
+ Raises:
1794
+ TypeError: If the library package or data type doesn't match.
1795
+ ValueError: If the device doesn't match or if the operand's traits
1796
+ are incompatible with the original operand.
1797
+ """
1798
+
1799
+ # Validate package match
1800
+ package = utils.infer_object_package(operand.tensor)
1801
+ if self.package != package:
1802
+ message = f"Library package mismatch: '{self.package}' => '{package}'"
1803
+ raise TypeError(message)
1804
+
1805
+ # Validate data type match
1806
+ utils.check_attribute_match(self.operand_data_type, operand.dtype, "data type")
1807
+
1808
+ # Validate device match
1809
+ # In principle, we could support memory_space change, but it would require
1810
+ # updating self.memory_space and dependent properties like self.blocking,
1811
+ # which could be error-prone and would prevent inplace optimizations.
1812
+ operand_device_id = operand.device_id
1813
+ if operand_device_id != self.operand_device_id:
1814
+
1815
+ def device_str(device_id: int | Literal["cpu"]) -> str:
1816
+ return f"cuda:{device_id}" if isinstance(device_id, int) else f"{device_id}"
1817
+
1818
+ raise ValueError(
1819
+ f"The new operand must be on the same device as the original one. "
1820
+ f"The new operand's device is {device_str(operand_device_id)}, "
1821
+ f"the original device is {device_str(self.operand_device_id)}"
1822
+ )
1823
+
1824
+ # Validate FFT plan compatibility using key_from_user_operand,
1825
+ # matches create_key() behavior.
1826
+ # key_from_user_operand is always set after __init__.
1827
+ assert self.key_from_user_operand is not None, "Internal error: key_from_user_operand not set."
1828
+
1829
+ try:
1830
+ # Pass inplace explicitly to match what create_key() does.
1831
+ new_plan_args, _ = _compute_fft_plan_args_and_traits(
1832
+ operand,
1833
+ axes=self.axes,
1834
+ options=self.options,
1835
+ execution=self.execution_options,
1836
+ inplace=self.options.inplace,
1837
+ )
1838
+ new_key = create_fft_key(new_plan_args, self.execution_options)
1839
+ except UnsupportedLayoutError:
1840
+ new_key = None
1841
+
1842
+ if self.key_from_user_operand != new_key:
1843
+ self.logger.debug(f"The FFT key corresponding to the original operand is: {self.key_from_user_operand}.")
1844
+ if new_key is None:
1845
+ self.logger.debug(
1846
+ "The FFT key for the new operand cannot be computed since the layout "
1847
+ f"(shape = {operand.shape}, strides = {operand.strides}) and axes = {self.axes} combination "
1848
+ "is unsupported."
1849
+ )
1850
+ else:
1851
+ self.logger.debug(f"The FFT key corresponding to the new operand is: {new_key}.")
1852
+ raise ValueError(
1853
+ "The new operand's traits (data type, shape, or strides) are incompatible with that of the original operand."
1854
+ )
1855
+
1856
+ # Validation passed. No updates needed because:
1857
+ # - key_from_user_operand == new_key
1858
+ # - plan_args_from_user_operand produces same key, get_key() returns correct value
1859
+ # - plan_traits stays unchanged (copy produces the same layout)
1860
+ self.logger.debug("Validation passed; no updates needed since keys match.")
1861
+
1862
+ def _update_plan_traits_for_new_operand(self, new_operand_shape, new_operand_strides):
1863
+ """
1864
+ (private) Recompute plan_traits.result_shape/strides after
1865
+ a reset whose operand has different properties but a matching key.
1866
+
1867
+ Args:
1868
+ new_operand_shape: Shape of the internal buffer. Batch
1869
+ dimensions may differ from the original (e.g.
1870
+ (2,3,64) -> (3,2,64) with axes=(2,)).
1871
+ new_operand_strides: Strides of the internal buffer. Used to
1872
+ recompute the result axis order.
1873
+
1874
+ The FFT axis sizes in result_shape may differ from the operand
1875
+ (R2C/C2R), so we preserve them from the old result_shape and only
1876
+ replace the batch dimensions from the new operand.
1877
+
1878
+ The axis order used to compute result_strides depends on the
1879
+ layout strategy chosen at plan time (stored in
1880
+ plan_traits.optimized_result_layout):
1881
+ - Natural: axis_order_in_memory (mirrors the operand layout).
1882
+ - Optimized: reversed FFT axes + batch axes sorted by stride.
1883
+ """
1884
+ new_shape = list(new_operand_shape)
1885
+ old_result_shape = self.plan_traits.result_shape
1886
+ # Preserve FFT axis sizes (they differ from operand for R2C/C2R).
1887
+ for ax in self.plan_traits.ordered_axes:
1888
+ new_shape[ax] = old_result_shape[ax]
1889
+ new_result_shape = tuple(new_shape)
1890
+
1891
+ if self.plan_traits.optimized_result_layout:
1892
+ fft_axes_set = set(self.plan_traits.ordered_axes)
1893
+ batch_axes = [a for a in range(len(new_operand_shape)) if a not in fft_axes_set]
1894
+ axis_order = tuple(
1895
+ list(reversed(self.plan_traits.ordered_axes))
1896
+ + sorted(batch_axes, key=lambda v: (new_operand_strides[v], new_operand_shape[v]))
1897
+ )
1898
+ else:
1899
+ axis_order = axis_order_in_memory(new_operand_shape, new_operand_strides)
1900
+
1901
+ self.plan_traits.result_shape = new_result_shape
1902
+ self.plan_traits.result_strides = calculate_strides(new_result_shape, axis_order)
1903
+
1904
+ def _update_layout_and_plan_traits_if_needed(self):
1905
+ """
1906
+ (private) Update internal_operand_layout from the current internal operand,
1907
+ and recompute plan_traits if shape or strides changed.
1908
+
1909
+ The comparison is against self.operand (the internal buffer), not the
1910
+ user's original operand. For cross-space scenarios, the internal
1911
+ buffer is always a contiguous copy, so stride-only user changes are
1912
+ invisible here — which is correct because cuFFT sees contiguous data.
1913
+
1914
+ When does _update_plan_traits_for_new_operand get called?
1915
+ The columns below refer to the *internal buffer* (self.operand),
1916
+ not the user's original operand.
1917
+
1918
+ internal internal
1919
+ shape strides plan_traits
1920
+ Scenario (user's operand) changed changed updated
1921
+ -------------------------------- -------- -------- -----------
1922
+ Same operand (no change) No No No
1923
+ Different shape, same-space Yes Yes Yes
1924
+ Different shape, cross-space Yes Yes Yes
1925
+ Different strides only, same-space No Yes Yes
1926
+ Different strides only, cross-space No No No
1927
+ (cross-space copy is always contiguous, so internal strides
1928
+ are unchanged regardless of the user's strides)
1929
+ """
1930
+ new_layout = TensorLayout(shape=self.operand.shape, strides=self.operand.strides)
1931
+ if tuple(self.internal_operand_layout.shape) != tuple(new_layout.shape) or tuple(
1932
+ self.internal_operand_layout.strides
1933
+ ) != tuple(new_layout.strides):
1934
+ self._update_plan_traits_for_new_operand(new_layout.shape, new_layout.strides)
1935
+
1936
+ # Always update: the new operand may have a different layout but a
1937
+ # compatible key. Class invariant: self.internal_operand_layout
1938
+ # always reflects the layout of the currently stored operand.
1939
+ self.internal_operand_layout = new_layout
1940
+
1941
+ def _reset_operand_set_stream_and_update_operand(self, operand, stream: AnyStream | None):
1942
+ """(private method) Set the stream, copy the operand, and update layout information.
1943
+ This method is used by ``reset_operand()`` after validation has passed.
1944
+ It performs the following operations:
1945
+
1946
+ 1. Gets or creates the execution and operand streams
1947
+ 2. Sets the stream for the cuFFT handle (for CUDA execution)
1948
+ 3. Copies the operand tensor if necessary (reallocates when shape changed)
1949
+ 4. Updates internal_operand_layout and plan_traits if needed
1950
+
1951
+ Args:
1952
+ operand: The validated operand tensor to use.
1953
+ stream: Optional stream to use for execution.
1954
+ If None, a stream is created or retrieved.
1955
+ """
1956
+
1957
+ exec_stream_holder, operand_stream_holder = self._get_or_create_stream_maybe(stream)
1958
+ self.logger.info(
1959
+ "The specified stream for reset_operand() is "
1960
+ f"{(exec_stream_holder or operand_stream_holder) and (exec_stream_holder or operand_stream_holder).obj}." # type: ignore[union-attr]
1961
+ )
1962
+
1963
+ # Allocate a new internal buffer when the previous one was released
1964
+ # or when the new operand has a different shape (e.g., same FFT key but
1965
+ # rearranged batch dimensions). copy_() rejects incompatible shapes
1966
+ # due to broadcasting, and the underlying tensor type varies across
1967
+ # backends, so there is no uniform reshape; we reallocate instead.
1968
+ needs_reallocation = self._operand_released or self.operand.shape != operand.shape
1969
+ if needs_reallocation:
1970
+ # When the shape changes, the old self.operand buffer is implicitly
1971
+ # released here (replaced by the new allocation). For the C2R
1972
+ # same-space case this is an internal aux buffer the user cannot
1973
+ # stream-order against; in the other cases the old buffer is
1974
+ # either the user's tensor (same-space non-C2R, user orders) or a
1975
+ # cross-space mirror (cross-space operations are blocking).
1976
+ # When the operand was already released, release_operand() already
1977
+ # performed this ordering, so we skip it here.
1978
+ if not self._operand_released:
1979
+ self._maybe_wait_c2r_aux_buffer_on_last_compute()
1980
+ self.operand, self.operand_backup = _allocate_operand_or_identity(
1981
+ operand,
1982
+ operand_stream_holder,
1983
+ self.execution_space,
1984
+ self.memory_space,
1985
+ self.device_id,
1986
+ self.fft_abstract_type,
1987
+ self.logger,
1988
+ )
1989
+ else:
1990
+ self.operand, self.operand_backup = _copy_operand_or_identity(
1991
+ self.operand,
1992
+ operand,
1993
+ operand_stream_holder,
1994
+ self.execution_space,
1995
+ self.memory_space,
1996
+ self.fft_abstract_type,
1997
+ self.logger,
1998
+ )
1999
+
2000
+ # No-op for non-C2R; for C2R same-space, the operand copy above
2001
+ # provides fresh data so the stale flag can be cleared.
2002
+ self._c2r_operand_stale = False
2003
+
2004
+ if (
2005
+ needs_reallocation
2006
+ and self.execution_space == "cuda"
2007
+ and self.execution_space == self.memory_space
2008
+ and self.fft_abstract_type == "C2R"
2009
+ ):
2010
+ assert operand_stream_holder is not None
2011
+ self.c2r_buffer_stream = operand_stream_holder.obj
2012
+
2013
+ self._update_layout_and_plan_traits_if_needed()
2014
+
2015
+ # Log final result layout
2016
+ self.logger.info(
2017
+ f"The reset operand shape = {self.internal_operand_layout.shape}, "
2018
+ f"and strides = {self.internal_operand_layout.strides}."
2019
+ )
2020
+ result_shape, result_strides = (
2021
+ (self.internal_operand_layout.shape, self.internal_operand_layout.strides)
2022
+ if self.inplace
2023
+ else (self.plan_traits.result_shape, self.plan_traits.result_strides)
2024
+ )
2025
+ self.logger.info(f"The result shape = {result_shape}, and strides = {result_strides}.")
2026
+ self.logger.info("The operand has been reset to the specified operand.")
2027
+
2028
+ @utils.precondition(_check_valid_fft)
2029
+ def reset_operand(self, operand, *, stream: AnyStream | None = None):
2030
+ """
2031
+ Reset the operand held by this :class:`FFT` instance to a new compatible
2032
+ operand for subsequent execution.
2033
+
2034
+ Args:
2035
+ operand: A tensor (ndarray-like object) compatible with the previous one.
2036
+ The new operand is considered compatible if all the
2037
+ following properties match with the previous one:
2038
+
2039
+ - The problem specification key for the new operand. Generally the keys will
2040
+ match if the operand shares the same layout (shape, strides and data
2041
+ type). The keys may still match for certain operands with different
2042
+ layout, see :meth:`create_key` for details.
2043
+ - The package that the new operand belongs to.
2044
+ - The memory space of the new operand (CPU or GPU).
2045
+ - The device that new operand belongs to if it is on GPU.
2046
+
2047
+ stream: {stream}
2048
+
2049
+ Semantics:
2050
+ - If execution space == memory space and the FFT is not a C2R transform:
2051
+ operand reference update with no data copying.
2052
+
2053
+ - If execution space == memory space, the FFT is a C2R transform:
2054
+ one data copy to an auxiliary tensor, required to prevent cuFFT from
2055
+ overwriting the user's input.
2056
+
2057
+ - If execution space != memory space:
2058
+ data must be copied between different memory spaces.
2059
+
2060
+ Examples:
2061
+
2062
+ >>> import cupy as cp
2063
+ >>> import nvmath
2064
+
2065
+ Create a 3-D complex128 ndarray on the GPU:
2066
+
2067
+ >>> shape = 128, 128, 128
2068
+ >>> a = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
2069
+
2070
+ Create an FFT object as a context manager
2071
+
2072
+ >>> axes = 0, 1
2073
+ >>> with nvmath.fft.FFT(a, axes=axes) as f:
2074
+ ... # Plan the FFT
2075
+ ... f.plan()
2076
+ ...
2077
+ ... # Execute the FFT to get the first result.
2078
+ ... r1 = f.execute()
2079
+ ...
2080
+ ... # Reset the operand to a new CuPy ndarray.
2081
+ ... b = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
2082
+ ... f.reset_operand(b)
2083
+ ...
2084
+ ... # Execute to get the new result corresponding to the updated operand.
2085
+ ... r2 = f.execute()
2086
+
2087
+ With :meth:`reset_operand`, minimal overhead is achieved as problem
2088
+ specification and planning are only performed once.
2089
+ However it still performs validation to ensure that the operand is compatible
2090
+ with the original, and, if enabled, logging. See :meth:`reset_operand_unchecked`
2091
+ for an alternative when the caller has already validated the operand or chooses
2092
+ to skip validation and logging.
2093
+
2094
+ For the particular example above, explicitly calling :meth:`reset_operand` is
2095
+ equivalent to updating the operand in-place, i.e, replacing
2096
+ ``f.reset_operand(b)`` with ``a[:]=b``. Note that updating the operand in-place
2097
+ should be adopted with caution as it can only yield the expected result and
2098
+ incur no additional copies under the additional constraints below:
2099
+
2100
+ - The operation is not a complex-to-real (C2R) FFT.
2101
+ - The operand's memory matches the FFT execution space. More precisely, the
2102
+ operand memory space should be accessible from the execution space (CPU or
2103
+ CUDA).
2104
+
2105
+ For more details, please refer to `inplace update example
2106
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft/example05_stateful_inplace.py>`_.
2107
+
2108
+ .. seealso::
2109
+ :meth:`reset_operand_unchecked`, :meth:`release_operand`
2110
+ """
2111
+
2112
+ self.logger.info("Resetting operand...")
2113
+ if operand is None:
2114
+ raise ValueError("Resetting operand requires a valid operand. Use release_operand() to release the operand.")
2115
+
2116
+ operand = tensor_wrapper.wrap_operand(operand)
2117
+ self._reset_operand_validate(operand)
2118
+ self._reset_operand_set_stream_and_update_operand(operand, stream)
2119
+ self._operand_released = False
2120
+
2121
+ def reset_operand_unchecked(self, operand, *, stream: AnyStream | None = None):
2122
+ """
2123
+ .. experimental:: method
2124
+
2125
+ This method is a performance-optimized alternative to :meth:`reset_operand` that
2126
+ eliminates validation and logging overhead, making it ideal for
2127
+ performance-critical loops where operand compatibility is guaranteed by the caller.
2128
+
2129
+ Args:
2130
+ operand: A tensor (ndarray-like object) that is **guaranteed** by the user
2131
+ to be compatible with the original operand used during planning.
2132
+ See the ``operand`` parameter in :meth:`reset_operand` for the definition
2133
+ of compatibility.
2134
+
2135
+ stream: {stream}
2136
+
2137
+ Returns:
2138
+ None
2139
+
2140
+ Semantics:
2141
+ The semantics are the same as in :meth:`reset_operand`,
2142
+ except that this method does not perform any validation (e.g. package
2143
+ match, data type match, key match, etc.) or logging.
2144
+
2145
+ When to Use:
2146
+ - Performance-critical loops with repeated FFT executions on different operands
2147
+
2148
+ - After verifying correctness with :meth:`reset_operand` during development
2149
+
2150
+ - When operand compatibility is guaranteed by construction or invariant
2151
+
2152
+ Examples:
2153
+
2154
+ **Example 1: Optimizing a processing loop**
2155
+
2156
+ .. code-block:: python
2157
+
2158
+ import cupy as cp
2159
+ import nvmath
2160
+
2161
+ shape = (1024, 1024)
2162
+ operand = cp.random.rand(*shape, dtype=cp.complex64)
2163
+
2164
+ fft = nvmath.fft.FFT(operand, execution="cuda")
2165
+ with fft:
2166
+ fft.plan()
2167
+ for i in range(10000):
2168
+ # Process and create new operand with the same shape, dtype,
2169
+ # and device as the original operand
2170
+ new_operand = process_data(...)
2171
+ fft.reset_operand_unchecked(new_operand)
2172
+ result = fft.execute()
2173
+ # block until the result is ready
2174
+ ...
2175
+
2176
+ **Example 2: Streaming data processing**
2177
+
2178
+ Processing a stream of incoming data operands with identical layout:
2179
+
2180
+ .. code-block:: python
2181
+
2182
+ import cupy as cp
2183
+ import nvmath
2184
+
2185
+ # Create a stateful FFT object and prepare it once.
2186
+ shape = (512, 512)
2187
+ initial_operand = cp.empty(shape, dtype=cp.complex64)
2188
+
2189
+ fft = nvmath.fft.FFT(initial_operand, execution="cuda")
2190
+ with fft:
2191
+ fft.plan()
2192
+
2193
+ # Process stream of incoming operands
2194
+ for operand in incoming_data_stream():
2195
+ # The user guarantees that the operand is compatible
2196
+ # with the original (same shape, dtype, device, ...).
2197
+ fft.reset_operand_unchecked(operand)
2198
+ result = fft.execute()
2199
+ # block until the result is ready
2200
+ process_spectrum(result)
2201
+ ...
2202
+
2203
+ .. seealso::
2204
+ :meth:`reset_operand`: Safe, validated method for changing operands.
2205
+ :meth:`release_operand`: Release the internal references to the operand.
2206
+ :meth:`create_key`: For understanding FFT key compatibility.
2207
+ :func:`estimate_workspace_size`: For estimating workspace size.
2208
+ """
2209
+ # Case 1: Same execution and memory space, non-C2R transform
2210
+ # The user guarantees that the operand is in the same memory space as the original
2211
+ # operand so we can directly update the operand reference without copying data.
2212
+ # The TensorHolder wrapper is always alive (release_operand only clears
2213
+ # .tensor), so we can unconditionally swap the inner tensor.
2214
+ if self.execution_space == self.memory_space and self.fft_abstract_type != "C2R":
2215
+ self.operand.tensor = operand
2216
+ self.operand_backup = None
2217
+ self._operand_released = False
2218
+ self._update_layout_and_plan_traits_if_needed()
2219
+ return
2220
+
2221
+ # Cases 2 and 3 require data copying, so we need the stream
2222
+ _, operand_stream_holder = self._get_or_create_stream_maybe(stream)
2223
+ # and also require the wrapped operand
2224
+ operand_wrapped = tensor_wrapper.wrap_operand(operand)
2225
+
2226
+ # Case 2: C2R transform with same execution and memory space
2227
+ # Data must be copied to prevent cuFFT from overwriting the user's input buffer.
2228
+ # This is a corner case that stems from cuFFT behavior.
2229
+ if self.execution_space == self.memory_space:
2230
+ needs_reallocation = self._operand_released or self.operand.shape != operand_wrapped.shape
2231
+ if needs_reallocation:
2232
+ self.operand = utils.create_empty_tensor(
2233
+ operand_wrapped.__class__,
2234
+ operand_wrapped.shape,
2235
+ operand_wrapped.dtype,
2236
+ self.device_id,
2237
+ operand_stream_holder,
2238
+ verify_strides=True,
2239
+ strides=operand_wrapped.strides,
2240
+ )
2241
+ if self.execution_space == "cuda":
2242
+ self.c2r_buffer_stream = operand_stream_holder.obj # type: ignore[union-attr]
2243
+
2244
+ self.operand.copy_(operand_wrapped, stream_holder=operand_stream_holder)
2245
+ self.operand_backup = None
2246
+ self._operand_released = False
2247
+ self._c2r_operand_stale = False
2248
+ self._update_layout_and_plan_traits_if_needed()
2249
+ return
2250
+
2251
+ # Case 3: Cross-space scenario (execution_space != memory_space)
2252
+ # Example: CPU operand with CUDA execution, or CUDA operand with CPU execution.
2253
+ # Data must be copied between memory spaces.
2254
+ needs_reallocation = self._operand_released or self.operand.shape != operand_wrapped.shape
2255
+ if needs_reallocation:
2256
+ if self.execution_space == "cuda":
2257
+ to_device = self.device_id
2258
+ else:
2259
+ to_device = "cpu"
2260
+ self.operand = operand_wrapped.to(to_device, operand_stream_holder)
2261
+ else:
2262
+ self.operand.copy_(operand_wrapped, stream_holder=operand_stream_holder)
2263
+ self.operand_backup.tensor = operand_wrapped.tensor
2264
+ self._operand_released = False
2265
+ self._c2r_operand_stale = False
2266
+ self._update_layout_and_plan_traits_if_needed()
2267
+
2268
+ @utils.precondition(_check_valid_fft)
2269
+ def release_operand(self):
2270
+ """
2271
+ {release_operand}
2272
+ """
2273
+ # We release the references to the user-provided
2274
+ # operand and/or GPU mirrors of the user-provided operand
2275
+ # and/or the internal auxiliary buffer for C2R transforms
2276
+ # since it can be non-negligible memory.
2277
+ # Note that we do not release the whole wrapper objects,
2278
+ # but only their internal tensor references, which allow
2279
+ # us to reuse them without re-wrapping saving some overhead.
2280
+
2281
+ # Case 1 (same-space, non-C2R):
2282
+ # self.operand is the user's tensor,
2283
+ # self.operand_backup is None.
2284
+ # Case 2 (same-space, C2R):
2285
+ # self.operand references the internal auxiliary buffer
2286
+ # The user's tensor is not referenced. We must ensure the compute
2287
+ # that used this buffer has completed before releasing it.
2288
+ # Case 3 (cross-space):
2289
+ # self.operand is an internal mirror, self.operand_backup
2290
+ # is the user's tensor. Release both.
2291
+
2292
+ self._maybe_wait_c2r_aux_buffer_on_last_compute()
2293
+ self.c2r_buffer_stream = None
2294
+ self.operand.tensor = None
2295
+ if self.operand_backup is not None:
2296
+ self.operand_backup.tensor = None
2297
+
2298
+ self._operand_released = True
2299
+ self.logger.info("User-provided operand has been released.")
2300
+
2301
+ def get_input_layout(self):
2302
+ """
2303
+ Returns a pair of tuples: shape and strides of the FFT input.
2304
+
2305
+ .. note::
2306
+ In some cases, the FFT operation requires taking a copy of the input tensor
2307
+ (e.g. C2R cuFFT, or provided tensor resides on CPU but FFT is executed on GPU).
2308
+ The copied tensor strides may differ from the input tensor passed by the user,
2309
+ if the original tensor's strides do not conform to dense C-like layout.
2310
+ """
2311
+ return self.internal_operand_layout.shape, self.internal_operand_layout.strides
2312
+
2313
+ def get_output_layout(self):
2314
+ """
2315
+ Returns a pair of tuples: shape and strides of the FFT output.
2316
+ """
2317
+ return (
2318
+ (self.internal_operand_layout.shape, self.internal_operand_layout.strides)
2319
+ if self.inplace
2320
+ else (self.plan_traits.result_shape, self.plan_traits.result_strides)
2321
+ )
2322
+
2323
+ def _check_valid_operand(self, *args, **kwargs):
2324
+ """ """
2325
+ what = kwargs["what"]
2326
+ if self._operand_released:
2327
+ raise RuntimeError(
2328
+ f"{what} cannot be performed after the operand has been released. Use reset_operand() to provide a new "
2329
+ f"operand before performing the {what.lower()}."
2330
+ )
2331
+
2332
+ def _free_workspace_memory(self, exception: Exception | None = None) -> bool:
2333
+ """
2334
+ Free workspace by releasing the MemoryPointer object.
2335
+ """
2336
+ if self.workspace_ptr is None:
2337
+ return True
2338
+
2339
+ self.workspace_ptr = None
2340
+ self.logger.debug("[_free_workspace_memory] The workspace has been released.")
2341
+
2342
+ return True
2343
+
2344
+ @utils.precondition(_check_valid_fft)
2345
+ @utils.precondition(_check_planned, "Workspace memory allocation")
2346
+ @utils.atomic(_free_workspace_memory, method=True)
2347
+ def _allocate_workspace_memory(self, stream_holder: StreamHolder):
2348
+ """
2349
+ Allocate workspace memory using the specified allocator.
2350
+ """
2351
+
2352
+ assert self._workspace_allocated_here is False, "Internal Error."
2353
+
2354
+ self.logger.debug("Allocating workspace for performing the FFT...")
2355
+ with utils.device_ctx(self.device_id), stream_holder.ctx:
2356
+ try:
2357
+ if isinstance(self.allocator, memory.BaseCUDAMemoryManagerAsync):
2358
+ self.workspace_ptr = self.allocator.memalloc_async(self.workspace_size, stream_holder.obj)
2359
+ else:
2360
+ self.workspace_ptr = self.allocator.memalloc(self.workspace_size) # type: ignore[union-attr]
2361
+ self._workspace_allocated_here = True
2362
+ except TypeError as e:
2363
+ message = (
2364
+ "The method 'memalloc' in the allocator object must conform to the interface in the "
2365
+ "'BaseCUDAMemoryManager' protocol."
2366
+ )
2367
+ raise TypeError(message) from e
2368
+ raw_workspace_ptr = utils.get_ptr_from_memory_pointer(self.workspace_ptr)
2369
+ cufft.set_work_area(self.handle, raw_workspace_ptr)
2370
+
2371
+ self.workspace_stream = stream_holder.obj
2372
+ self.logger.debug(
2373
+ f"Finished allocating device workspace of size {formatters.MemoryStr(self.workspace_size)} in the context "
2374
+ f"of stream {self.workspace_stream}."
2375
+ )
2376
+
2377
+ def _allocate_workspace_memory_perhaps(self, stream_holder: StreamHolder):
2378
+ """
2379
+ Allocate workspace memory using the specified allocator, if it hasn't already been
2380
+ done.
2381
+ """
2382
+ if self.execution_space != "cuda" or self.workspace_ptr is not None:
2383
+ return
2384
+
2385
+ return self._allocate_workspace_memory(stream_holder)
2386
+
2387
+ @utils.precondition(_check_valid_fft)
2388
+ def _free_workspace_memory_perhaps(self, release_workspace):
2389
+ """
2390
+ Free workspace memory if if 'release_workspace' is True.
2391
+ """
2392
+ if not release_workspace:
2393
+ return
2394
+
2395
+ # Establish ordering wrt the computation and free workspace if it's more than the
2396
+ # specified cache limit.
2397
+ if self.last_compute_event is not None:
2398
+ self.workspace_stream.wait(self.last_compute_event)
2399
+ self.logger.debug("Established ordering with respect to the computation before releasing the workspace.")
2400
+ self.last_compute_event = None
2401
+
2402
+ self.logger.debug("[_free_workspace_memory_perhaps] The workspace memory will be released.")
2403
+ self._free_workspace_memory()
2404
+
2405
+ return True
2406
+
2407
+ def _release_workspace_memory_perhaps(self, exception: Exception | None = None) -> bool:
2408
+ """
2409
+ Free workspace memory if it was allocated in this call
2410
+ (self._workspace_allocated_here == True) when an exception occurs.
2411
+ """
2412
+ release_workspace = self._workspace_allocated_here
2413
+ self.logger.debug(
2414
+ f"[_release_workspace_memory_perhaps] The release_workspace flag is set to {release_workspace} based upon "
2415
+ "the value of 'workspace_allocated_here'."
2416
+ )
2417
+ self._free_workspace_memory_perhaps(release_workspace)
2418
+ self._workspace_allocated_here = False
2419
+ return True
2420
+
2421
+ @utils.precondition(_check_valid_fft)
2422
+ @utils.precondition(_check_planned, "Execution")
2423
+ @utils.precondition(_check_valid_operand, "Execution")
2424
+ @utils.atomic(_release_workspace_memory_perhaps, method=True)
2425
+ def execute(self, direction: FFTDirection | None = None, stream: AnyStream | None = None, release_workspace: bool = False):
2426
+ """
2427
+ Execute the FFT operation.
2428
+
2429
+ Args:
2430
+ direction: {direction}
2431
+
2432
+ stream: {stream}
2433
+
2434
+ release_workspace: {release_workspace}
2435
+
2436
+ Returns:
2437
+ The transformed operand, which remains on the same device and utilizes the same
2438
+ package as the input operand. The data type and shape of the transformed operand
2439
+ depend on the type of input operand:
2440
+
2441
+ - For C2C FFT, the data type and shape remain identical to the input.
2442
+ - For R2C and C2R FFT, both data type and shape differ from the input.
2443
+ """
2444
+
2445
+ if self.fft_abstract_type == "C2R" and self._c2r_operand_stale:
2446
+ raise RuntimeError(
2447
+ "For C2R FFTs, execute() cannot be called multiple times without "
2448
+ "resetting the operand in between. Please call reset_operand() or "
2449
+ "reset_operand_unchecked() before executing again."
2450
+ )
2451
+
2452
+ log_info = self.logger.isEnabledFor(logging.INFO)
2453
+ log_debug = self.logger.isEnabledFor(logging.DEBUG)
2454
+
2455
+ if direction is None:
2456
+ direction = _get_fft_default_direction(self.fft_abstract_type)
2457
+ else:
2458
+ direction = self._get_validate_direction(direction)
2459
+
2460
+ exec_stream_holder, operand_stream_holder = self._get_or_create_stream_maybe(stream)
2461
+
2462
+ if self.execution_space == "cuda":
2463
+ # Set stream for the FFT.
2464
+ cufft.set_stream(self.handle, exec_stream_holder.ptr) # type: ignore[union-attr]
2465
+
2466
+ # Allocate workspace if needed.
2467
+ self._allocate_workspace_memory_perhaps(exec_stream_holder) # type: ignore[arg-type]
2468
+ # Allocate output operand if needed
2469
+ if self.inplace:
2470
+ result_ptr = self.operand.data_ptr
2471
+ else:
2472
+ if self._preallocated_result is not None:
2473
+ assert self.execution_space == "cpu"
2474
+ self.result = self._preallocated_result
2475
+ self._preallocated_result = None
2476
+ else:
2477
+ self.result = self._allocate_result_operand(exec_stream_holder, log_debug)
2478
+ result_ptr = self.result.data_ptr
2479
+
2480
+ if log_info:
2481
+ self.logger.info(f"Starting FFT {self.fft_abstract_type} calculation in the {direction.name} direction...") # type: ignore[union-attr]
2482
+ self.logger.info(f"{self.call_prologue}")
2483
+
2484
+ if self.execution_space == "cpu":
2485
+ with utils.host_call_ctx(timing=log_info) as elapsed:
2486
+ fftw.execute(self.handle, self.operand.data_ptr, result_ptr, direction)
2487
+ else:
2488
+ assert isinstance(self.device_id, int), self.device_id
2489
+ assert exec_stream_holder is not None
2490
+ with utils.cuda_call_ctx(exec_stream_holder, self.blocking, timing=log_info) as (
2491
+ self.last_compute_event,
2492
+ elapsed,
2493
+ ):
2494
+ if log_debug:
2495
+ self.logger.debug("The cuFFT execution function is 'xt_exec'.")
2496
+ cufft.xt_exec(self.handle, self.operand.data_ptr, result_ptr, direction)
2497
+
2498
+ if self.fft_abstract_type == "C2R":
2499
+ self._c2r_operand_stale = True
2500
+
2501
+ if log_info and elapsed.data is not None:
2502
+ self.logger.info(f"The FFT calculation took {elapsed.data:.3f} ms to complete.")
2503
+
2504
+ # Establish ordering wrt the computation and free workspace if it's more than the
2505
+ # specified cache limit.
2506
+ self._free_workspace_memory_perhaps(release_workspace)
2507
+
2508
+ # reset workspace allocation tracking to False at the end of the methods where
2509
+ # workspace memory is potentially allocated. This is necessary to prevent any
2510
+ # exceptions raised before method entry from using stale tracking values.
2511
+ self._workspace_allocated_here = False
2512
+
2513
+ # Return the result.
2514
+ result = self.operand if self.inplace else self.result
2515
+ if self.memory_space == self.execution_space:
2516
+ out = result.tensor
2517
+ else:
2518
+ if self.options.inplace: # Don't use self.inplace here, because we always set it to True for CPU tensors.
2519
+ self.operand_backup.copy_(result, stream_holder=operand_stream_holder)
2520
+ out = self.operand_backup.tensor
2521
+ else:
2522
+ target_dev = "cpu" if self.memory_space == "cpu" else self.operand_device_id
2523
+ out = result.to(target_dev, stream_holder=operand_stream_holder).tensor # type: ignore[arg-type]
2524
+
2525
+ # Release internal reference to the result to permit recycling of memory.
2526
+ self.result = None # type: ignore
2527
+
2528
+ return out
2529
+
2530
+ def free(self):
2531
+ """Free FFT resources.
2532
+
2533
+ It is recommended that the :class:`FFT` object be used within a context, but if it
2534
+ is not possible then this method must be called explicitly to ensure that the FFT
2535
+ resources (especially internal library objects) are properly cleaned up.
2536
+ """
2537
+
2538
+ if not self.valid_state:
2539
+ return
2540
+
2541
+ try:
2542
+ # Ensure ordering with respect to the last computation
2543
+ # to avoid race conditions when releasing internal resources.
2544
+ self._maybe_wait_c2r_aux_buffer_on_last_compute()
2545
+ if self.last_compute_event is not None and self.workspace_stream is not None:
2546
+ self.workspace_stream.wait(self.last_compute_event)
2547
+ self.last_compute_event = None
2548
+
2549
+ self._free_workspace_memory()
2550
+
2551
+ if self.handle is not None:
2552
+ if self.execution_space == "cuda":
2553
+ cufft.destroy(self.handle)
2554
+ else:
2555
+ fftw.destroy(self.handle)
2556
+ self.handle = None
2557
+
2558
+ # Set all attributes to None except for logger and valid_state
2559
+ _keep = {"logger", "valid_state"}
2560
+ for attr in list(vars(self)):
2561
+ if attr not in _keep:
2562
+ setattr(self, attr, None)
2563
+
2564
+ except Exception as e:
2565
+ self.logger.critical("Internal error: only part of the FFT object's resources have been released.")
2566
+ self.logger.critical(str(e))
2567
+ raise e
2568
+ finally:
2569
+ self.valid_state = False
2570
+
2571
+ self.logger.info("The FFT object's resources have been released.")
2572
+
2573
+
2574
+ def _fft(
2575
+ operand,
2576
+ /,
2577
+ *,
2578
+ axes: Sequence[int] | None = None,
2579
+ direction: FFTDirection | None = None,
2580
+ options: FFTOptions | None = None,
2581
+ execution: ExecutionCPU | ExecutionCUDA | None = None,
2582
+ prolog: DeviceCallable | None = None,
2583
+ epilog: DeviceCallable | None = None,
2584
+ stream: AnyStream | None = None,
2585
+ check_dtype: str | None = None,
2586
+ ):
2587
+ r"""
2588
+ fft({function_signature})
2589
+
2590
+ Perform an N-D *complex-to-complex* (C2C) FFT on the provided complex operand.
2591
+
2592
+ Args:
2593
+ operand: {operand}
2594
+
2595
+ axes: {axes}
2596
+
2597
+ options: {options}
2598
+
2599
+ execution: {execution}
2600
+
2601
+ prolog: {prolog}
2602
+
2603
+ epilog: {epilog}
2604
+
2605
+ stream: {stream}
2606
+
2607
+ Returns:
2608
+ A transformed operand that retains the same data type and shape as the input. It
2609
+ remains on the same device and uses the same package as the input operand.
2610
+
2611
+ .. seealso::
2612
+ :func:`ifft`, :func:`irfft`, :func:`rfft`, :class:`FFT`
2613
+
2614
+ Examples:
2615
+
2616
+ >>> import cupy as cp
2617
+ >>> import nvmath
2618
+
2619
+ Create a 3-D complex128 ndarray on the GPU:
2620
+
2621
+ >>> shape = 256, 256, 256
2622
+ >>> a = cp.random.rand(*shape, dtype=cp.float64) + 1j * cp.random.rand(
2623
+ ... *shape, dtype=cp.float64
2624
+ ... )
2625
+
2626
+ Perform a 3-D C2C FFT using :func:`fft`. The result `r` is also a CuPy complex128
2627
+ ndarray:
2628
+
2629
+ >>> r = nvmath.fft.fft(a)
2630
+
2631
+ User may also perform FFT along a subset of dimensions, e.g, 2-D C2C FFT along the
2632
+ first two dimensions, batched along the last dimension:
2633
+
2634
+ >>> axes = 0, 1
2635
+ >>> r = nvmath.fft.fft(a, axes=axes)
2636
+
2637
+ For C2C type FFT operation, the output can be directly computed inplace thus
2638
+ overwriting the input operand. This can be specified using options to the FFT:
2639
+
2640
+ >>> o = nvmath.fft.FFTOptions(inplace=True)
2641
+ >>> r = nvmath.fft.fft(a, options=o)
2642
+ >>> r is a
2643
+ True
2644
+
2645
+ See :class:`FFTOptions` for the complete list of available options.
2646
+
2647
+ The package current stream is used by default, but a stream can be explicitly
2648
+ provided to the FFT operation. This can be done if the FFT operand is computed on a
2649
+ different stream, for example:
2650
+
2651
+ >>> s = cp.cuda.Stream()
2652
+ >>> with s:
2653
+ ... a = cp.random.rand(*shape) + 1j * cp.random.rand(*shape)
2654
+ >>> r = nvmath.fft.fft(a, stream=s)
2655
+
2656
+ The operation above runs on stream `s` and is ordered with respect to the input
2657
+ computation.
2658
+
2659
+ Create a NumPy ndarray on the CPU.
2660
+
2661
+ >>> import numpy as np
2662
+ >>> b = np.random.rand(*shape) + 1j * np.random.rand(*shape)
2663
+
2664
+ Provide the NumPy ndarray to :func:`fft`, with the result also being a NumPy
2665
+ ndarray:
2666
+
2667
+ >>> r = nvmath.fft.fft(b)
2668
+
2669
+ Notes:
2670
+ - This function only takes complex operand for C2C transformation. If the user
2671
+ wishes to perform full FFT transformation on real input, please cast the input to
2672
+ the corresponding complex data type.
2673
+ - This function is a convenience wrapper around :class:`FFT` and is specifically
2674
+ meant for *single* use. The same computation can be performed with the stateful
2675
+ API using the default `direction` argument in :meth:`FFT.execute`.
2676
+
2677
+ Further examples can be found in the `nvmath/examples/fft
2678
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft>`_ directory.
2679
+ """
2680
+ if check_dtype is not None:
2681
+ assert check_dtype in {"real", "complex"}, "internal error"
2682
+ wrapped = tensor_wrapper.wrap_operand(operand)
2683
+ if ("complex" in wrapped.dtype) != (check_dtype == "complex"):
2684
+ raise ValueError(f"This function expects {check_dtype} operand, found {wrapped.dtype}")
2685
+
2686
+ with FFT(operand, axes=axes, options=options, execution=execution, stream=stream) as fftobj:
2687
+ # Plan the FFT.
2688
+ fftobj.plan(stream=stream, prolog=prolog, epilog=epilog, direction=direction)
2689
+
2690
+ # Execute the FFT.
2691
+ result = fftobj.execute(direction=direction, stream=stream)
2692
+
2693
+ return result
2694
+
2695
+
2696
+ # Forward C2C FFT Function.
2697
+ fft = functools.wraps(_fft)(functools.partial(_fft, direction=FFTDirection.FORWARD, check_dtype="complex"))
2698
+ fft.__doc__ = fft.__doc__.format(**SHARED_FFT_DOCUMENTATION) # type: ignore
2699
+ fft.__name__ = "fft"
2700
+
2701
+
2702
+ # Forward R2C FFT Function
2703
+ @utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=False)
2704
+ def rfft(
2705
+ operand,
2706
+ /,
2707
+ *,
2708
+ axes: Sequence[int] | None = None,
2709
+ options: FFTOptions | None = None,
2710
+ execution: ExecutionCPU | ExecutionCUDA | None = None,
2711
+ prolog: DeviceCallable | None = None,
2712
+ epilog: DeviceCallable | None = None,
2713
+ stream: AnyStream | None = None,
2714
+ ):
2715
+ r"""
2716
+ rfft({function_signature})
2717
+
2718
+ Perform an N-D *real-to-complex* (R2C) FFT on the provided real operand.
2719
+
2720
+ .. versionchanged:: 0.9.0
2721
+ The ``operand`` parameter is now positional-only.
2722
+
2723
+ Args:
2724
+ operand: {operand}
2725
+
2726
+ axes: {axes}
2727
+
2728
+ options: {options}
2729
+
2730
+ execution: {execution}
2731
+
2732
+ prolog: {prolog}
2733
+
2734
+ epilog: {epilog}
2735
+
2736
+ stream: {stream}
2737
+
2738
+ Returns:
2739
+ A complex tensor that remains on the same device and belongs to the same package as
2740
+ the input operand. The extent of the last transformed axis in the result will be
2741
+ ``operand.shape[axes[-1]] // 2 + 1``.
2742
+
2743
+
2744
+ .. seealso::
2745
+ :func:`fft`, :func:`irfft`, :class:`FFT`.
2746
+ """
2747
+ wrapped_operand = tensor_wrapper.wrap_operand(operand)
2748
+ # check if input operand if real type
2749
+ if "complex" in wrapped_operand.dtype:
2750
+ raise RuntimeError(f"rfft expects a real input, but got {wrapped_operand.dtype}. Please use fft for complex input.")
2751
+
2752
+ return _fft(
2753
+ operand,
2754
+ axes=axes,
2755
+ options=options,
2756
+ execution=execution,
2757
+ prolog=prolog,
2758
+ epilog=epilog,
2759
+ stream=stream,
2760
+ check_dtype="real",
2761
+ )
2762
+
2763
+
2764
+ # Inverse C2C/R2C FFT Function.
2765
+ ifft = functools.wraps(_fft)(functools.partial(_fft, direction=FFTDirection.INVERSE, check_dtype="complex"))
2766
+ ifft.__doc__ = """
2767
+ ifft({function_signature})
2768
+
2769
+ Perform an N-D *complex-to-complex* (C2C) inverse FFT on the provided complex operand.
2770
+ The direction is implicitly inverse.
2771
+
2772
+ Args:
2773
+ operand: {operand}
2774
+
2775
+ axes: {axes}
2776
+
2777
+ options: {options}
2778
+
2779
+ execution: {execution}
2780
+
2781
+ prolog: {prolog}
2782
+
2783
+ epilog: {epilog}
2784
+
2785
+ stream: {stream}
2786
+
2787
+ Returns:
2788
+ A transformed operand that retains the same data type and shape as the input. It
2789
+ remains on the same device and uses the same package as the input operand.
2790
+
2791
+ .. seealso::
2792
+ :func:`fft`, :func:`irfft`, :class:`FFT`.
2793
+
2794
+ Notes:
2795
+ - This function only takes complex operand for C2C transformation. If the user wishes
2796
+ to perform full FFT transformation on real input, please cast the input to the
2797
+ corresponding complex data type.
2798
+ - This function is a convenience wrapper around :class:`FFT` and is specifically
2799
+ meant for *single* use. The same computation can be performed with the stateful
2800
+ API by passing the argument ``direction='inverse'`` when calling
2801
+ :meth:`FFT.execute`.
2802
+ """.format(**SHARED_FFT_DOCUMENTATION)
2803
+ ifft.__name__ = "ifft"
2804
+
2805
+
2806
+ # Inverse C2R FFT Function.
2807
+ @utils.docstring_decorator(SHARED_FFT_DOCUMENTATION, skip_missing=False)
2808
+ def irfft(
2809
+ operand,
2810
+ /,
2811
+ *,
2812
+ axes: Sequence[int] | None = None,
2813
+ options: FFTOptions | None = None,
2814
+ execution: ExecutionCPU | ExecutionCUDA | None = None,
2815
+ prolog: DeviceCallable | None = None,
2816
+ epilog: DeviceCallable | None = None,
2817
+ stream: AnyStream | None = None,
2818
+ ):
2819
+ """
2820
+ irfft({function_signature})
2821
+
2822
+ Perform an N-D *complex-to-real* (C2R) FFT on the provided complex operand. The
2823
+ direction is implicitly inverse.
2824
+
2825
+ Args:
2826
+ operand: {operand}
2827
+
2828
+ axes: {axes}
2829
+
2830
+ options: {options}
2831
+
2832
+ execution: {execution}
2833
+
2834
+ prolog: {prolog}
2835
+
2836
+ epilog: {epilog}
2837
+
2838
+ stream: {stream}
2839
+
2840
+ Returns:
2841
+ A real tensor that remains on the same device and belongs to the same package as the
2842
+ input operand. The extent of the last transformed axis in the result will be
2843
+ ``(operand.shape[axes[-1]] - 1) * 2`` if :attr:`FFTOptions.last_axis_parity` is
2844
+ ``even``, or ``operand.shape[axes[-1]] * 2 - 1`` if
2845
+ :attr:`FFTOptions.last_axis_parity` is ``odd``.
2846
+
2847
+ .. seealso::
2848
+ :func:`fft`, :func:`ifft`, :class:`FFT`.
2849
+
2850
+ Examples:
2851
+
2852
+ >>> import cupy as cp
2853
+ >>> import nvmath
2854
+
2855
+ Create a 3-D symmetric complex128 ndarray on the GPU:
2856
+
2857
+ >>> shape = 512, 768, 256
2858
+ >>> a = nvmath.fft.rfft(cp.random.rand(*shape, dtype=cp.float64))
2859
+
2860
+ Perform a 3-D C2R FFT using the :func:`irfft` wrapper. The result `r` is a CuPy
2861
+ float64 ndarray:
2862
+
2863
+ >>> r = nvmath.fft.irfft(a)
2864
+ >>> r.dtype
2865
+ dtype('float64')
2866
+
2867
+ Notes:
2868
+
2869
+ - This function performs an inverse C2R N-D FFT, which is similar to `irfftn` but
2870
+ different from `irfft` in various numerical packages.
2871
+ - This function is a convenience wrapper around :class:`FFT` and is specifically
2872
+ meant for *single* use. The same computation can be performed with the stateful
2873
+ API by setting :attr:`FFTOptions.fft_type` to ``'C2R'`` and passing the argument
2874
+ ``direction='inverse'`` when calling :meth:`FFT.execute`.
2875
+ - **The input to this function must be Hermitian-symmetric, otherwise the result is
2876
+ undefined.** While the symmetry requirement is partially captured by the different
2877
+ extents in the last transformed dimension between the input and result, there are
2878
+ additional `constraints
2879
+ <https://docs.nvidia.com/cuda/cufft/#fourier-transform-types>`_. As a specific
2880
+ example, 1-D transforms require the first element (and the last element, if the
2881
+ extent is even) of the input to be purely real-valued. In addition, if the input
2882
+ to `irfft` was generated using an R2C FFT with an odd last axis size,
2883
+ :attr:`FFTOptions.last_axis_parity` must be set to ``odd`` to recover the original
2884
+ signal.
2885
+ - For more details, please refer to `C2R example
2886
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft/example07_c2r.py>`_
2887
+ and `odd C2R example
2888
+ <https://github.com/NVIDIA/nvmath-python/tree/main/examples/fft/example07_c2r_odd.py>`_.
2889
+ """
2890
+ options = utils.check_or_create_options(FFTOptions, options, "FFT options")
2891
+ assert options is not None
2892
+ options.fft_type = "C2R"
2893
+ return _fft(
2894
+ operand,
2895
+ axes=axes,
2896
+ direction=FFTDirection.INVERSE,
2897
+ options=options,
2898
+ execution=execution,
2899
+ prolog=prolog,
2900
+ epilog=epilog,
2901
+ stream=stream,
2902
+ check_dtype="complex",
2903
+ )